From f402a4e5ce12e5409404efdd5121ee8c56ff4577 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 10 Apr 2024 15:57:29 +0200 Subject: [PATCH 001/650] WIP --- Cargo.lock | 52 +++++++++ Cargo.toml | 6 ++ crates/dap/Cargo.toml | 27 +++++ crates/dap/src/dap.rs | 8 ++ crates/debugger_ui/Cargo.toml | 24 +++++ crates/debugger_ui/src/debugger_panel.rs | 132 +++++++++++++++++++++++ crates/debugger_ui/src/lib.rs | 23 ++++ crates/debuggers/Cargo.toml | 13 +++ crates/debuggers/src/lib.rs | 2 + crates/debuggers/src/node.rs | 9 ++ crates/debuggers/src/xdebug.rs | 12 +++ crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 7 ++ 14 files changed, 317 insertions(+) create mode 100644 crates/dap/Cargo.toml create mode 100644 crates/dap/src/dap.rs create mode 100644 crates/debugger_ui/Cargo.toml create mode 100644 crates/debugger_ui/src/debugger_panel.rs create mode 100644 crates/debugger_ui/src/lib.rs create mode 100644 crates/debuggers/Cargo.toml create mode 100644 crates/debuggers/src/lib.rs create mode 100644 crates/debuggers/src/node.rs create mode 100644 crates/debuggers/src/xdebug.rs diff --git a/Cargo.lock b/Cargo.lock index 8a775d2f853f31..a2c812eba6069f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2991,6 +2991,34 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +[[package]] +name = "dap" +version = "0.1.0" +dependencies = [ + "anyhow", + "dap 0.4.1-alpha1", + "futures 0.3.28", + "gpui", + "log", + "parking_lot", + "postage", + "release_channel", + "serde", + "serde_json", + "smol", + "util", +] + +[[package]] +name = "dap" +version = "0.4.1-alpha1" +source = "git+https://github.com/sztomi/dap-rs?branch=main#913a2a520416a2a81a1f09a7946f595d5ae3ee4a" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -3039,6 +3067,29 @@ dependencies = [ "util", ] +[[package]] +name = "debugger_ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "dap 0.1.0", + "db", + "editor", + "gpui", + "serde", + "serde_derive", + "ui", + "workspace", +] + +[[package]] +name = "debuggers" +version = "0.1.0" +dependencies = [ + "anyhow", + "dap 0.1.0", +] + [[package]] name = "der" version = "0.6.1" @@ -12598,6 +12649,7 @@ dependencies = [ "copilot", "copilot_ui", "db", + "debugger_ui", "diagnostics", "editor", "embed-manifest", diff --git a/Cargo.toml b/Cargo.toml index 6f35a6b41af848..35b9d81c9552de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,9 @@ members = [ "crates/command_palette_hooks", "crates/copilot", "crates/copilot_ui", + "crates/dap", + "crates/debuggers", + "crates/debugger_ui", "crates/db", "crates/diagnostics", "crates/editor", @@ -140,6 +143,9 @@ command_palette = { path = "crates/command_palette" } command_palette_hooks = { path = "crates/command_palette_hooks" } copilot = { path = "crates/copilot" } copilot_ui = { path = "crates/copilot_ui" } +dap = { path = "crates/dap" } +debuggers = { path = "crates/debuggers" } +debugger_ui = { path = "crates/debugger_ui" } db = { path = "crates/db" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml new file mode 100644 index 00000000000000..19d217a639e15b --- /dev/null +++ b/crates/dap/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "dap" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/dap.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +dap = { git = "https://github.com/sztomi/dap-rs", branch = "main" } +futures.workspace = true +gpui.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +release_channel.workspace = true +serde.workspace = true +serde_json.workspace = true +smol.workspace = true +util.workspace = true diff --git a/crates/dap/src/dap.rs b/crates/dap/src/dap.rs new file mode 100644 index 00000000000000..1b918a4f42d559 --- /dev/null +++ b/crates/dap/src/dap.rs @@ -0,0 +1,8 @@ +use dap::requests::InitializeArguments; +pub use dap::*; + +pub trait DebuggerAdapter { + fn initialize(&self, args: InitializeArguments) -> anyhow::Result<()>; +} + +// impl dyn DebuggerAdapter {} diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml new file mode 100644 index 00000000000000..0e9eebe4cbb7b1 --- /dev/null +++ b/crates/debugger_ui/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "debugger_ui" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[dependencies] +anyhow.workspace = true +dap.workspace = true +db.workspace = true +editor.workspace = true +gpui.workspace = true +serde.workspace = true +serde_derive.workspace = true +ui.workspace = true +workspace.workspace = true + +[dev-dependencies] +editor = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs new file mode 100644 index 00000000000000..66ff905ff43ff2 --- /dev/null +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -0,0 +1,132 @@ +use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, ViewContext}; +use ui::{ + div, h_flex, prelude, px, ButtonCommon, Element, IconButton, ParentElement, Pixels, Render, + Styled, Tooltip, WindowContext, +}; +use workspace::dock::{DockPosition, Panel, PanelEvent}; + +actions!(debug, [TogglePanel]); + +pub struct DebugPanel { + pub position: DockPosition, + pub zoomed: bool, + pub active: bool, + pub focus_handle: FocusHandle, + pub size: Pixels, +} + +impl DebugPanel { + pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self { + Self { + position, + zoomed: false, + active: false, + focus_handle: cx.focus_handle(), + size: px(300.), + } + } +} + +impl EventEmitter for DebugPanel {} + +impl FocusableView for DebugPanel { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Panel for DebugPanel { + fn persistent_name() -> &'static str { + "DebugPanel" + } + + fn position(&self, _cx: &prelude::WindowContext) -> workspace::dock::DockPosition { + self.position + } + + fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool { + true + } + + fn set_position( + &mut self, + position: workspace::dock::DockPosition, + _cx: &mut ViewContext, + ) { + self.position = position; + // TODO: + // cx.update_global::(f) + } + + fn size(&self, _cx: &prelude::WindowContext) -> prelude::Pixels { + self.size + } + + fn set_size(&mut self, size: Option, _cx: &mut ViewContext) { + self.size = size.unwrap(); + } + + fn icon(&self, _cx: &prelude::WindowContext) -> Option { + None + } + + fn icon_tooltip(&self, _cx: &prelude::WindowContext) -> Option<&'static str> { + None + } + + fn toggle_action(&self) -> Box { + Box::new(TogglePanel) + } + + fn icon_label(&self, _: &WindowContext) -> Option { + None + } + + fn is_zoomed(&self, _cx: &WindowContext) -> bool { + false + } + + fn starts_open(&self, _cx: &WindowContext) -> bool { + false + } + + fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} + + fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} +} + +impl Render for DebugPanel { + fn render(&mut self, _: &mut ViewContext) -> impl prelude::IntoElement { + div() + .child( + h_flex() + .p_2() + .gap_2() + .child( + IconButton::new("debug-play", ui::IconName::Play) + .tooltip(move |cx| Tooltip::text("Start debug", cx)), + ) + .child( + IconButton::new("debug-step-over", ui::IconName::Play) + .tooltip(move |cx| Tooltip::text("Step over", cx)), + ) + .child( + IconButton::new("debug-go-in", ui::IconName::Play) + .tooltip(move |cx| Tooltip::text("Go in", cx)), + ) + .child( + IconButton::new("debug-go-out", ui::IconName::Play) + .tooltip(move |cx| Tooltip::text("Go out", cx)), + ) + .child( + IconButton::new("debug-restart", ui::IconName::Play) + .tooltip(move |cx| Tooltip::text("Restart", cx)), + ) + .child( + IconButton::new("debug-stop", ui::IconName::Play) + .tooltip(move |cx| Tooltip::text("Stop", cx)), + ), + ) + .into_any() + } +} diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs new file mode 100644 index 00000000000000..0b4539ced8570c --- /dev/null +++ b/crates/debugger_ui/src/lib.rs @@ -0,0 +1,23 @@ +use debugger_panel::{DebugPanel, TogglePanel}; +use gpui::{AppContext, ViewContext}; +use serde::{Deserialize, Serialize}; +use ui::Pixels; +use workspace::Workspace; + +pub mod debugger_panel; + +#[derive(Serialize, Deserialize)] +struct SerializedDebugPanel { + width: Option, +} + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _: &mut ViewContext| { + workspace.register_action(|workspace, _action: &TogglePanel, cx| { + workspace.focus_panel::(cx); + }); + }, + ) + .detach(); +} diff --git a/crates/debuggers/Cargo.toml b/crates/debuggers/Cargo.toml new file mode 100644 index 00000000000000..6a3638be22f89e --- /dev/null +++ b/crates/debuggers/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "debuggers" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[dependencies] +dap.workspace = true +anyhow.workspace = true + +[lints] +workspace = true diff --git a/crates/debuggers/src/lib.rs b/crates/debuggers/src/lib.rs new file mode 100644 index 00000000000000..a0ebc5f51bf5bd --- /dev/null +++ b/crates/debuggers/src/lib.rs @@ -0,0 +1,2 @@ +mod node; +mod xdebug; diff --git a/crates/debuggers/src/node.rs b/crates/debuggers/src/node.rs new file mode 100644 index 00000000000000..a3da178bf35998 --- /dev/null +++ b/crates/debuggers/src/node.rs @@ -0,0 +1,9 @@ +use dap::DebuggerAdapter; + +pub struct JsAdapter; + +impl DebuggerAdapter for JsAdapter { + fn initialize(&self, args: dap::requests::InitializeArguments) -> anyhow::Result<()> { + todo!() + } +} diff --git a/crates/debuggers/src/xdebug.rs b/crates/debuggers/src/xdebug.rs new file mode 100644 index 00000000000000..89c300fd3f90bf --- /dev/null +++ b/crates/debuggers/src/xdebug.rs @@ -0,0 +1,12 @@ +use dap::{requests::InitializeArguments, DebuggerAdapter}; + +pub struct Xdebug; + +impl DebuggerAdapter for Xdebug { + fn initialize(&self, _args: InitializeArguments) -> anyhow::Result<()> { + if true != false { + println!("Hello, world!"); + } + todo!() + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 599c9ddd162730..90a0b414eb0b10 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -35,6 +35,7 @@ collections.workspace = true command_palette.workspace = true copilot.workspace = true copilot_ui.workspace = true +debugger_ui.workspace = true db.workspace = true diagnostics.workspace = true editor.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index dac37ba7f0ae50..13836c28523fe3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -272,6 +272,7 @@ fn main() { project_symbols::init(cx); project_panel::init(Assets, cx); tasks_ui::init(cx); + debugger_ui::init(cx); channel::init(&client, user_store.clone(), cx); search::init(cx); vim::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index e8018b362c411a..b642b994528281 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -7,6 +7,7 @@ use assistant::AssistantPanel; use breadcrumbs::Breadcrumbs; use client::ZED_URL_SCHEME; use collections::VecDeque; +use debugger_ui::debugger_panel::DebugPanel; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; use gpui::{ actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, PromptLevel, @@ -201,6 +202,11 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace_handle.clone(), cx.clone(), ); + + let debug_panel = workspace_handle.update(&mut cx.clone(), |workspace, cx| { + cx.new_view(|cx| DebugPanel::new(workspace::dock::DockPosition::Bottom, cx)) + })?; + let ( project_panel, terminal_panel, @@ -224,6 +230,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace.add_panel(channels_panel, cx); workspace.add_panel(chat_panel, cx); workspace.add_panel(notification_panel, cx); + workspace.add_panel(debug_panel, cx); cx.focus_self(); }) }) From c18db76862b4a1157466e4f11fb621c7a72480a7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 9 Jun 2024 12:27:47 +0200 Subject: [PATCH 002/650] Merge main --- crates/debugger_ui/src/debugger_panel.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 66ff905ff43ff2..8a9243c4043d3f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,9 +1,15 @@ -use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, ViewContext}; +use gpui::{ + actions, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, View, + ViewContext, WeakView, +}; use ui::{ div, h_flex, prelude, px, ButtonCommon, Element, IconButton, ParentElement, Pixels, Render, - Styled, Tooltip, WindowContext, + Styled, Tooltip, VisualContext, WindowContext, +}; +use workspace::{ + dock::{DockPosition, Panel, PanelEvent}, + Workspace, }; -use workspace::dock::{DockPosition, Panel, PanelEvent}; actions!(debug, [TogglePanel]); @@ -25,6 +31,15 @@ impl DebugPanel { size: px(300.), } } + + pub async fn load( + workspace: WeakView, + mut cx: AsyncWindowContext, + ) -> anyhow::Result> { + workspace.update(&mut cx, |workspace, cx| { + cx.new_view(|cx| DebugPanel::new(workspace::dock::DockPosition::Bottom, cx)) + }) + } } impl EventEmitter for DebugPanel {} From 7b71119094add91dab76d64a65bfaf2e193e8d0e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 9 Jun 2024 13:01:46 +0200 Subject: [PATCH 003/650] Move to imports --- crates/debugger_ui/src/debugger_panel.rs | 51 ++++++++++++------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 8a9243c4043d3f..bb2d1f8b390b49 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,10 +1,13 @@ +use anyhow::Result; use gpui::{ - actions, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, View, - ViewContext, WeakView, + actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, + View, ViewContext, WeakView, }; use ui::{ - div, h_flex, prelude, px, ButtonCommon, Element, IconButton, ParentElement, Pixels, Render, - Styled, Tooltip, VisualContext, WindowContext, + div, h_flex, + prelude::{IntoElement, Pixels, WindowContext}, + px, ButtonCommon, Element, IconButton, IconName, ParentElement, Render, Styled, Tooltip, + VisualContext, }; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -35,9 +38,9 @@ impl DebugPanel { pub async fn load( workspace: WeakView, mut cx: AsyncWindowContext, - ) -> anyhow::Result> { - workspace.update(&mut cx, |workspace, cx| { - cx.new_view(|cx| DebugPanel::new(workspace::dock::DockPosition::Bottom, cx)) + ) -> Result> { + workspace.update(&mut cx, |_, cx| { + cx.new_view(|cx| DebugPanel::new(DockPosition::Bottom, cx)) }) } } @@ -55,41 +58,37 @@ impl Panel for DebugPanel { "DebugPanel" } - fn position(&self, _cx: &prelude::WindowContext) -> workspace::dock::DockPosition { + fn position(&self, _cx: &WindowContext) -> DockPosition { self.position } - fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool { + fn position_is_valid(&self, _position: DockPosition) -> bool { true } - fn set_position( - &mut self, - position: workspace::dock::DockPosition, - _cx: &mut ViewContext, - ) { + fn set_position(&mut self, position: DockPosition, _cx: &mut ViewContext) { self.position = position; // TODO: // cx.update_global::(f) } - fn size(&self, _cx: &prelude::WindowContext) -> prelude::Pixels { + fn size(&self, _cx: &WindowContext) -> Pixels { self.size } - fn set_size(&mut self, size: Option, _cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, _cx: &mut ViewContext) { self.size = size.unwrap(); } - fn icon(&self, _cx: &prelude::WindowContext) -> Option { + fn icon(&self, _cx: &WindowContext) -> Option { None } - fn icon_tooltip(&self, _cx: &prelude::WindowContext) -> Option<&'static str> { + fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { None } - fn toggle_action(&self) -> Box { + fn toggle_action(&self) -> Box { Box::new(TogglePanel) } @@ -111,34 +110,34 @@ impl Panel for DebugPanel { } impl Render for DebugPanel { - fn render(&mut self, _: &mut ViewContext) -> impl prelude::IntoElement { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { div() .child( h_flex() .p_2() .gap_2() .child( - IconButton::new("debug-play", ui::IconName::Play) + IconButton::new("debug-play", IconName::Play) .tooltip(move |cx| Tooltip::text("Start debug", cx)), ) .child( - IconButton::new("debug-step-over", ui::IconName::Play) + IconButton::new("debug-step-over", IconName::Play) .tooltip(move |cx| Tooltip::text("Step over", cx)), ) .child( - IconButton::new("debug-go-in", ui::IconName::Play) + IconButton::new("debug-go-in", IconName::Play) .tooltip(move |cx| Tooltip::text("Go in", cx)), ) .child( - IconButton::new("debug-go-out", ui::IconName::Play) + IconButton::new("debug-go-out", IconName::Play) .tooltip(move |cx| Tooltip::text("Go out", cx)), ) .child( - IconButton::new("debug-restart", ui::IconName::Play) + IconButton::new("debug-restart", IconName::Play) .tooltip(move |cx| Tooltip::text("Restart", cx)), ) .child( - IconButton::new("debug-stop", ui::IconName::Play) + IconButton::new("debug-stop", IconName::Play) .tooltip(move |cx| Tooltip::text("Stop", cx)), ), ) From 08300c6e9029de70e860862dbd687c4778ecd30d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 10 Jun 2024 20:38:00 +0200 Subject: [PATCH 004/650] Wip debugger client --- Cargo.lock | 9 +- Cargo.toml | 2 - crates/dap/Cargo.toml | 4 - crates/dap/src/client.rs | 151 +++++++++ crates/dap/src/dap.rs | 8 - crates/dap/src/events.rs | 150 ++++++++ crates/dap/src/lib.rs | 5 + crates/dap/src/requests.rs | 413 +++++++++++++++++++++++ crates/dap/src/transport.rs | 238 +++++++++++++ crates/dap/src/types.rs | 325 ++++++++++++++++++ crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/debugger_panel.rs | 71 +++- crates/debuggers/Cargo.toml | 13 - crates/debuggers/src/lib.rs | 2 - crates/debuggers/src/node.rs | 9 - crates/debuggers/src/xdebug.rs | 12 - 16 files changed, 1349 insertions(+), 64 deletions(-) create mode 100644 crates/dap/src/client.rs delete mode 100644 crates/dap/src/dap.rs create mode 100644 crates/dap/src/events.rs create mode 100644 crates/dap/src/lib.rs create mode 100644 crates/dap/src/requests.rs create mode 100644 crates/dap/src/transport.rs create mode 100644 crates/dap/src/types.rs delete mode 100644 crates/debuggers/Cargo.toml delete mode 100644 crates/debuggers/src/lib.rs delete mode 100644 crates/debuggers/src/node.rs delete mode 100644 crates/debuggers/src/xdebug.rs diff --git a/Cargo.lock b/Cargo.lock index 1b02376be65c5a..e7c7832940a0db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3211,6 +3211,7 @@ dependencies = [ "dap 0.1.0", "db", "editor", + "futures 0.3.28", "gpui", "serde", "serde_derive", @@ -3218,14 +3219,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "debuggers" -version = "0.1.0" -dependencies = [ - "anyhow", - "dap 0.1.0", -] - [[package]] name = "deflate" version = "0.8.6" diff --git a/Cargo.toml b/Cargo.toml index e5191e19d263da..5437f59ff3d7aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ members = [ "crates/command_palette_hooks", "crates/copilot", "crates/dap", - "crates/debuggers", "crates/debugger_ui", "crates/db", "crates/diagnostics", @@ -170,7 +169,6 @@ command_palette = { path = "crates/command_palette" } command_palette_hooks = { path = "crates/command_palette_hooks" } copilot = { path = "crates/copilot" } dap = { path = "crates/dap" } -debuggers = { path = "crates/debuggers" } debugger_ui = { path = "crates/debugger_ui" } db = { path = "crates/db" } diagnostics = { path = "crates/diagnostics" } diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 19d217a639e15b..2fec49b31f1853 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -8,10 +8,6 @@ license = "GPL-3.0-or-later" [lints] workspace = true -[lib] -path = "src/dap.rs" -doctest = false - [dependencies] anyhow.workspace = true dap = { git = "https://github.com/sztomi/dap-rs", branch = "main" } diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs new file mode 100644 index 00000000000000..785ec29751ec8c --- /dev/null +++ b/crates/dap/src/client.rs @@ -0,0 +1,151 @@ +use crate::transport::{Payload, Request, Transport}; +use anyhow::{anyhow, Context, Result}; +use futures::{ + channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, + AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, +}; +use gpui::{AsyncWindowContext, Flatten}; +use serde_json::Value; +use smol::{ + io::BufReader, + net::TcpStream, + process::{self, Child}, +}; +use std::{ + net::{Ipv4Addr, SocketAddrV4}, + process::Stdio, + sync::atomic::{AtomicU64, Ordering}, +}; +use util::ResultExt; + +pub enum TransportType { + TCP, + STDIO, +} + +#[derive(Debug)] +pub struct Client { + _process: Option, + server_tx: UnboundedSender, + request_count: AtomicU64, +} + +impl Client { + pub async fn new( + transport_type: TransportType, + command: &str, + args: Vec<&str>, + port_arg: Option<&str>, + cx: &mut AsyncWindowContext, + ) -> Result<(Self, UnboundedReceiver)> { + match transport_type { + TransportType::TCP => Self::create_tcp_client(command, args, port_arg, cx).await, + TransportType::STDIO => Self::create_stdio_client(command, args, port_arg, cx).await, + } + } + + async fn create_tcp_client( + command: &str, + args: Vec<&str>, + port_arg: Option<&str>, + cx: &mut AsyncWindowContext, + ) -> Result<(Self, UnboundedReceiver)> { + let mut command = process::Command::new("python3"); + command + .current_dir("/Users/remcosmits/Documents/code/debugpy") + .arg("-m debugpy --listen localhost:5679 --wait-for-client test.py") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true); + + let process = command + .spawn() + .with_context(|| "failed to spawn command.")?; + + let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 5679); + + let (rx, tx) = TcpStream::connect(addr).await?.split(); + + Self::handle_transport( + Box::new(BufReader::new(rx)), + Box::new(tx), + None, + Some(process), + cx, + ) + } + + async fn create_stdio_client( + command: &str, + args: Vec<&str>, + port_arg: Option<&str>, + cx: &mut AsyncWindowContext, + ) -> Result<(Self, UnboundedReceiver)> { + todo!("not implemented") + } + + pub fn handle_transport( + rx: Box, + tx: Box, + err: Option>, + process: Option, + cx: &mut AsyncWindowContext, + ) -> Result<(Self, UnboundedReceiver)> { + let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); + let (client_tx, client_rx) = unbounded::(); + + let client = Self { + server_tx: server_tx.clone(), + _process: process, + request_count: AtomicU64::new(0), + }; + + cx.spawn(move |_| Self::recv(server_rx, server_tx, client_tx)) + .detach(); + + Ok((client, client_rx)) + } + + async fn recv( + mut server_rx: UnboundedReceiver, + mut server_tx: UnboundedSender, + mut client_tx: UnboundedSender, + ) { + while let Some(payload) = server_rx.next().await { + match payload { + Payload::Event(ev) => client_tx.send(Payload::Event(ev)).await.log_err(), + Payload::Response(res) => server_tx.send(Payload::Response(res)).await.log_err(), + Payload::Request(req) => client_tx.send(Payload::Request(req)).await.log_err(), + }; + } + } + + pub async fn request( + &mut self, + arguments: R::Arguments, + ) -> Result { + let arguments = Some(serde_json::to_value(arguments)?); + + let (callback_tx, mut callback_rx) = channel(1); + + let request = Request { + back_ch: Some(callback_tx), + seq: self.next_request_id(), + command: R::COMMAND.to_string(), + arguments, + }; + + self.server_tx.send(Payload::Request(request)).await?; + + callback_rx + .next() + .await + .ok_or(anyhow!("no response"))? + .map(|response| response.body.unwrap_or_default()) + } + + fn next_request_id(&mut self) -> u64 { + self.request_count.fetch_add(1, Ordering::Relaxed) + } +} diff --git a/crates/dap/src/dap.rs b/crates/dap/src/dap.rs deleted file mode 100644 index 1b918a4f42d559..00000000000000 --- a/crates/dap/src/dap.rs +++ /dev/null @@ -1,8 +0,0 @@ -use dap::requests::InitializeArguments; -pub use dap::*; - -pub trait DebuggerAdapter { - fn initialize(&self, args: InitializeArguments) -> anyhow::Result<()>; -} - -// impl dyn DebuggerAdapter {} diff --git a/crates/dap/src/events.rs b/crates/dap/src/events.rs new file mode 100644 index 00000000000000..5f8de379bbc9e9 --- /dev/null +++ b/crates/dap/src/events.rs @@ -0,0 +1,150 @@ +use crate::types::{DebuggerCapabilities, Source, ThreadId}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "event", content = "body")] +// seq is omitted as unused and is not sent by some implementations +pub enum Event { + Initialized(Option), + Stopped(Stopped), + Continued(Continued), + Exited(Exited), + Terminated(Option), + Thread(Thread), + Output(Output), + Breakpoint(Breakpoint), + Module(Module), + LoadedSource(LoadedSource), + Process(Process), + Capabilities(Capabilities), + // ProgressStart(), + // ProgressUpdate(), + // ProgressEnd(), + // Invalidated(), + Memory(Memory), +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Stopped { + pub reason: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub preserve_focus_hint: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub all_threads_stopped: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hit_breakpoint_ids: Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Continued { + pub thread_id: ThreadId, + #[serde(skip_serializing_if = "Option::is_none")] + pub all_threads_continued: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Exited { + pub exit_code: usize, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Terminated { + #[serde(skip_serializing_if = "Option::is_none")] + pub restart: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Thread { + pub reason: String, + pub thread_id: ThreadId, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Output { + pub output: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub category: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub group: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub variables_reference: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Breakpoint { + pub reason: String, + pub breakpoint: crate::types::Breakpoint, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Module { + pub reason: String, + pub module: crate::types::Module, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LoadedSource { + pub reason: String, + pub source: crate::types::Source, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Process { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub system_process_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_local_process: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_method: Option, // TODO: use enum + #[serde(skip_serializing_if = "Option::is_none")] + pub pointer_size: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Capabilities { + pub capabilities: crate::types::DebuggerCapabilities, +} + +// #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +// #[serde(rename_all = "camelCase")] +// pub struct Invalidated { +// pub areas: Vec, +// pub thread_id: Option, +// pub stack_frame_id: Option, +// } + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Memory { + pub memory_reference: String, + pub offset: usize, + pub count: usize, +} diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs new file mode 100644 index 00000000000000..26b1df72d97d3a --- /dev/null +++ b/crates/dap/src/lib.rs @@ -0,0 +1,5 @@ +pub mod client; +pub mod events; +pub mod requests; +pub mod transport; +pub mod types; diff --git a/crates/dap/src/requests.rs b/crates/dap/src/requests.rs new file mode 100644 index 00000000000000..848c5ff27aac4b --- /dev/null +++ b/crates/dap/src/requests.rs @@ -0,0 +1,413 @@ +use crate::types::{ + Breakpoint, DebuggerCapabilities, Scope, Source, SourceBreakpoint, StackFrame, + StackFrameFormat, Thread, ThreadId, ValueFormat, Variable, VariablePresentationHint, +}; +use ::serde::{Deserialize, Serialize}; +use serde::de::DeserializeOwned; +use serde_json::Value; +use std::collections::HashMap; + +pub trait Request { + type Arguments: DeserializeOwned + Serialize; + type Result: DeserializeOwned + Serialize; + const COMMAND: &'static str; +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InitializeArguments { + #[serde(rename = "clientID", skip_serializing_if = "Option::is_none")] + pub client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_name: Option, + #[serde(rename = "adapterID")] + pub adapter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub locale: Option, + #[serde(rename = "linesStartAt1", skip_serializing_if = "Option::is_none")] + pub lines_start_at_one: Option, + #[serde(rename = "columnsStartAt1", skip_serializing_if = "Option::is_none")] + pub columns_start_at_one: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_variable_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_variable_paging: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_run_in_terminal_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_memory_references: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_progress_reporting: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_invalidated_event: Option, +} + +#[derive(Debug)] +pub enum Initialize {} + +impl Request for Initialize { + type Arguments = InitializeArguments; + type Result = DebuggerCapabilities; + const COMMAND: &'static str = "initialize"; +} + +#[derive(Debug)] +pub enum Launch {} + +impl Request for Launch { + type Arguments = Value; + type Result = (); + const COMMAND: &'static str = "launch"; +} + +#[derive(Debug)] +pub enum Attach {} + +impl Request for Attach { + type Arguments = Value; + type Result = (); + const COMMAND: &'static str = "attach"; +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DisconnectArguments { + #[serde(skip_serializing_if = "Option::is_none")] + pub restart: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub terminate_debuggee: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub suspend_debuggee: Option, +} + +#[derive(Debug)] +pub enum Restart {} + +impl Request for Restart { + type Arguments = Value; + type Result = (); + const COMMAND: &'static str = "restart"; +} + +#[derive(Debug)] +pub enum Disconnect {} + +impl Request for Disconnect { + type Arguments = Option; + type Result = (); + const COMMAND: &'static str = "disconnect"; +} + +#[derive(Debug)] +pub enum ConfigurationDone {} + +impl Request for ConfigurationDone { + type Arguments = (); + type Result = (); + const COMMAND: &'static str = "configurationDone"; +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SetBreakpointsArguments { + pub source: Source, + #[serde(skip_serializing_if = "Option::is_none")] + pub breakpoints: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_modified: Option, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SetBreakpointsResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub breakpoints: Option>, +} + +#[derive(Debug)] +pub enum SetBreakpoints {} + +impl Request for SetBreakpoints { + type Arguments = SetBreakpointsArguments; + type Result = SetBreakpointsResponse; + const COMMAND: &'static str = "setBreakpoints"; +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ContinueArguments { + pub thread_id: ThreadId, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ContinueResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub all_threads_continued: Option, +} + +#[derive(Debug)] +pub enum Continue {} + +impl Request for Continue { + type Arguments = ContinueArguments; + type Result = ContinueResponse; + const COMMAND: &'static str = "continue"; +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StackTraceArguments { + pub thread_id: ThreadId, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_frame: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub levels: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StackTraceResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub total_frames: Option, + pub stack_frames: Vec, +} + +#[derive(Debug)] +pub enum StackTrace {} + +impl Request for StackTrace { + type Arguments = StackTraceArguments; + type Result = StackTraceResponse; + const COMMAND: &'static str = "stackTrace"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreadsResponse { + pub threads: Vec, +} + +#[derive(Debug)] +pub enum Threads {} + +impl Request for Threads { + type Arguments = (); + type Result = ThreadsResponse; + const COMMAND: &'static str = "threads"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ScopesArguments { + pub frame_id: usize, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ScopesResponse { + pub scopes: Vec, +} + +#[derive(Debug)] +pub enum Scopes {} + +impl Request for Scopes { + type Arguments = ScopesArguments; + type Result = ScopesResponse; + const COMMAND: &'static str = "scopes"; +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VariablesArguments { + pub variables_reference: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub start: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VariablesResponse { + pub variables: Vec, +} + +#[derive(Debug)] +pub enum Variables {} + +impl Request for Variables { + type Arguments = VariablesArguments; + type Result = VariablesResponse; + const COMMAND: &'static str = "variables"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StepInArguments { + pub thread_id: ThreadId, + #[serde(skip_serializing_if = "Option::is_none")] + pub target_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub granularity: Option, +} + +#[derive(Debug)] +pub enum StepIn {} + +impl Request for StepIn { + type Arguments = StepInArguments; + type Result = (); + const COMMAND: &'static str = "stepIn"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StepOutArguments { + pub thread_id: ThreadId, + #[serde(skip_serializing_if = "Option::is_none")] + pub granularity: Option, +} + +#[derive(Debug)] +pub enum StepOut {} + +impl Request for StepOut { + type Arguments = StepOutArguments; + type Result = (); + const COMMAND: &'static str = "stepOut"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NextArguments { + pub thread_id: ThreadId, + #[serde(skip_serializing_if = "Option::is_none")] + pub granularity: Option, +} + +#[derive(Debug)] +pub enum Next {} + +impl Request for Next { + type Arguments = NextArguments; + type Result = (); + const COMMAND: &'static str = "next"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PauseArguments { + pub thread_id: ThreadId, +} + +#[derive(Debug)] +pub enum Pause {} + +impl Request for Pause { + type Arguments = PauseArguments; + type Result = (); + const COMMAND: &'static str = "pause"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EvaluateArguments { + pub expression: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub frame_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EvaluateResponse { + pub result: String, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub _type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + pub variables_reference: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub named_variables: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_variables: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_reference: Option, +} + +#[derive(Debug)] +pub enum Evaluate {} + +impl Request for Evaluate { + type Arguments = EvaluateArguments; + type Result = EvaluateResponse; + const COMMAND: &'static str = "evaluate"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SetExceptionBreakpointsArguments { + pub filters: Vec, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SetExceptionBreakpointsResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub breakpoints: Option>, +} + +#[derive(Debug)] +pub enum SetExceptionBreakpoints {} + +impl Request for SetExceptionBreakpoints { + type Arguments = SetExceptionBreakpointsArguments; + type Result = SetExceptionBreakpointsResponse; + const COMMAND: &'static str = "setExceptionBreakpoints"; +} + +// Reverse Requests + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RunInTerminalResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub process_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub shell_process_id: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RunInTerminalArguments { + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + pub cwd: String, + pub args: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub env: Option>>, +} + +#[derive(Debug)] +pub enum RunInTerminal {} + +impl Request for RunInTerminal { + type Arguments = RunInTerminalArguments; + type Result = RunInTerminalResponse; + const COMMAND: &'static str = "runInTerminal"; +} diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs new file mode 100644 index 00000000000000..a73bc3120f3c48 --- /dev/null +++ b/crates/dap/src/transport.rs @@ -0,0 +1,238 @@ +use crate::events::Event; +use anyhow::{anyhow, Context, Result}; +use futures::{ + channel::mpsc::{unbounded, Sender, UnboundedReceiver, UnboundedSender}, + AsyncBufRead, AsyncWrite, SinkExt as _, StreamExt, +}; +use gpui::AsyncWindowContext; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use smol::io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _}; +use std::{collections::HashMap, sync::Arc}; +use util::ResultExt; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Request { + #[serde(skip)] + pub back_ch: Option>>, + pub seq: u64, + pub command: String, + pub arguments: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub struct Response { + pub request_seq: u64, + pub success: bool, + pub command: String, + pub message: Option, + pub body: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Payload { + Event(Box), + Response(Response), + Request(Request), +} + +#[derive(Debug)] +pub struct Transport { + pending_requests: Mutex>>>, +} + +impl Transport { + pub fn start( + server_stdout: Box, + server_stdin: Box, + server_stderr: Option>, + cx: &mut AsyncWindowContext, + ) -> (UnboundedReceiver, UnboundedSender) { + let (client_tx, server_rx) = unbounded::(); + let (server_tx, client_rx) = unbounded::(); + + let transport = Self { + pending_requests: Mutex::new(HashMap::default()), + }; + + let transport = Arc::new(transport); + + cx.update(|cx| { + cx.spawn(|_| Self::recv(transport.clone(), server_stdout, client_tx)) + .detach_and_log_err(cx); + cx.spawn(|_| Self::send(transport, server_stdin, client_rx)) + .detach_and_log_err(cx); + + if let Some(stderr) = server_stderr { + cx.spawn(|_| Self::err(stderr)).detach(); + } + }); + + (server_rx, server_tx) + } + + async fn recv_server_message( + reader: &mut Box, + buffer: &mut String, + ) -> Result { + let mut content_length = None; + loop { + buffer.truncate(0); + if reader.read_line(buffer).await? == 0 { + return Err(anyhow!("stream closed")); + }; + + if buffer == "\r\n" { + break; + } + + let parts = buffer.trim().split_once(": "); + + match parts { + Some(("Content-Length", value)) => { + content_length = Some(value.parse().context("invalid content length")?); + } + _ => {} + } + } + + let content_length = content_length.context("missing content length")?; + + //TODO: reuse vector + let mut content = vec![0; content_length]; + reader.read_exact(&mut content).await?; + let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; + + dbg!("<- DAP {}", msg); + + Ok(serde_json::from_str::(msg)?) + } + + async fn recv_server_error( + err: &mut (impl AsyncBufRead + Unpin + Send), + buffer: &mut String, + ) -> Result<()> { + buffer.truncate(0); + if err.read_line(buffer).await? == 0 { + return Err(anyhow!("stream closed")); + }; + + Ok(()) + } + + async fn send_payload_to_server( + &self, + server_stdin: &mut Box, + mut payload: Payload, + ) -> Result<()> { + if let Payload::Request(request) = &mut payload { + if let Some(back) = request.back_ch.take() { + self.pending_requests.lock().insert(request.seq, back); + } + } + self.send_string_to_server(server_stdin, serde_json::to_string(&payload)?) + .await + } + + async fn send_string_to_server( + &self, + server_stdin: &mut Box, + request: String, + ) -> Result<()> { + dbg!("Request {}", &request); + + server_stdin + .write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes()) + .await?; + + server_stdin.write_all(request.as_bytes()).await?; + + server_stdin.flush().await?; + + Ok(()) + } + + fn process_response(response: Response) -> Result { + if response.success { + Ok(response) + } else { + Err(anyhow!("some error")) + } + } + + async fn process_server_message( + &self, + mut client_tx: &UnboundedSender, + msg: Payload, + ) -> Result<()> { + match msg { + Payload::Response(res) => { + match self.pending_requests.lock().remove(&res.request_seq) { + Some(mut tx) => match tx.send(Self::process_response(res)).await { + Ok(_) => (), + Err(_) => (), + }, + None => { + dbg!("Response to nonexistent request #{}", res.request_seq); + client_tx.send(Payload::Response(res)).await.log_err(); + } + } + + Ok(()) + } + Payload::Request(_) => { + client_tx.send(msg).await.log_err(); + Ok(()) + } + Payload::Event(_) => { + client_tx.send(msg).await.log_err(); + Ok(()) + } + } + } + + async fn recv( + transport: Arc, + mut server_stdout: Box, + client_tx: UnboundedSender, + ) -> Result<()> { + let mut recv_buffer = String::new(); + loop { + transport + .process_server_message( + &client_tx, + Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?, + ) + .await?; + } + } + + async fn send( + transport: Arc, + mut server_stdin: Box, + mut client_rx: UnboundedReceiver, + ) -> Result<()> { + while let Some(payload) = client_rx.next().await { + transport + .send_payload_to_server(&mut server_stdin, payload) + .await?; + } + + Ok(()) + } + + async fn err(mut server_stderr: Box) { + let mut recv_buffer = String::new(); + loop { + match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await { + Ok(_) => {} + Err(err) => { + dbg!("err: <- {:?}", err); + break; + } + } + } + } +} diff --git a/crates/dap/src/types.rs b/crates/dap/src/types.rs new file mode 100644 index 00000000000000..0cbe738c5888e0 --- /dev/null +++ b/crates/dap/src/types.rs @@ -0,0 +1,325 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{collections::HashMap, fmt, path::PathBuf}; + +#[derive( + Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, +)] +pub struct ThreadId(isize); + +impl fmt::Display for ThreadId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +pub type ThreadStates = HashMap; + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ColumnDescriptor { + pub attribute_name: String, + pub label: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub ty: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExceptionBreakpointsFilter { + pub filter: String, + pub label: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub condition_description: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DebuggerCapabilities { + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_configuration_done_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_function_breakpoints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_conditional_breakpoints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_hit_conditional_breakpoints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_evaluate_for_hovers: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_step_back: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_set_variable: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_restart_frame: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_goto_targets_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_step_in_targets_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_completions_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_modules_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_restart_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_exception_options: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_value_formatting_options: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_exception_info_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub support_terminate_debuggee: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub support_suspend_debuggee: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_delayed_stack_trace_loading: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_loaded_sources_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_log_points: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_terminate_threads_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_set_expression: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_terminate_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_data_breakpoints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_read_memory_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_write_memory_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_disassemble_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_cancel_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_breakpoint_locations_request: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_clipboard_context: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_stepping_granularity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_instruction_breakpoints: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_exception_filter_options: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exception_breakpoint_filters: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_trigger_characters: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_module_columns: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_checksum_algorithms: Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Checksum { + pub algorithm: String, + pub checksum: String, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Source { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_reference: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub origin: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sources: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub adapter_data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub checksums: Option>, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceBreakpoint { + pub line: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hit_condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub log_message: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Breakpoint { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + pub verified: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_reference: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StackFrameFormat { + #[serde(skip_serializing_if = "Option::is_none")] + pub parameters: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_types: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_names: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_values: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub module: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_all: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StackFrame { + pub id: usize, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + pub line: usize, + pub column: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub can_restart: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_pointer_reference: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub module_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Thread { + pub id: ThreadId, + pub name: String, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Scope { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + pub variables_reference: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub named_variables: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_variables: Option, + pub expensive: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ValueFormat { + #[serde(skip_serializing_if = "Option::is_none")] + pub hex: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VariablePresentationHint { + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub attributes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Variable { + pub name: String, + pub value: String, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub ty: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub evaluate_name: Option, + pub variables_reference: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub named_variables: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_variables: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_reference: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Module { + pub id: isize, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_optimized: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_user_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol_status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol_file_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub date_time_stamp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub address_range: Option, +} diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 0e9eebe4cbb7b1..21ad84afc3ded9 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -13,6 +13,7 @@ anyhow.workspace = true dap.workspace = true db.workspace = true editor.workspace = true +futures.workspace = true gpui.workspace = true serde.workspace = true serde_derive.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index bb2d1f8b390b49..5074f9e7f6d197 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,4 +1,10 @@ use anyhow::Result; +use dap::{ + client::{Client, TransportType}, + requests::{Disconnect, Initialize, InitializeArguments}, + transport::Payload, +}; +use futures::channel::mpsc::UnboundedReceiver; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, View, ViewContext, WeakView, @@ -6,8 +12,8 @@ use gpui::{ use ui::{ div, h_flex, prelude::{IntoElement, Pixels, WindowContext}, - px, ButtonCommon, Element, IconButton, IconName, ParentElement, Render, Styled, Tooltip, - VisualContext, + px, ButtonCommon, Clickable, Element, IconButton, IconName, ParentElement, Render, Styled, + Tooltip, VisualContext, }; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -22,16 +28,22 @@ pub struct DebugPanel { pub active: bool, pub focus_handle: FocusHandle, pub size: Pixels, + debug_client: (Client, UnboundedReceiver), } impl DebugPanel { - pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self { + pub fn new( + position: DockPosition, + debug_client: (Client, UnboundedReceiver), + cx: &mut WindowContext, + ) -> Self { Self { position, zoomed: false, active: false, focus_handle: cx.focus_handle(), size: px(300.), + debug_client, } } @@ -39,9 +51,53 @@ impl DebugPanel { workspace: WeakView, mut cx: AsyncWindowContext, ) -> Result> { - workspace.update(&mut cx, |_, cx| { - cx.new_view(|cx| DebugPanel::new(DockPosition::Bottom, cx)) - }) + let mut cx = cx.clone(); + let debug_client = Client::new( + TransportType::TCP, + "python3", + vec![ + "-m", + "debugpy", + "--listen", + "localhost:5668", + "--wait-for-client", + "test.py", + ], + None, + &mut cx, + ) + .await; + + if let Ok(mut debug_client) = debug_client { + let mut cx = cx.clone(); + + let args = dap::requests::InitializeArguments { + client_id: Some("hx".to_owned()), + client_name: Some("helix".to_owned()), + adapter_id: "debugpy".into(), + locale: Some("en-us".to_owned()), + lines_start_at_one: Some(true), + columns_start_at_one: Some(true), + path_format: Some("path".to_owned()), + supports_variable_type: Some(true), + supports_variable_paging: Some(false), + supports_run_in_terminal_request: Some(false), + supports_memory_references: Some(false), + supports_progress_reporting: Some(false), + supports_invalidated_event: Some(false), + }; + + debug_client.0.request::(args).await; + + debug_client.0.request::(None).await; + + workspace.update(&mut cx, |_, cx| { + cx.new_view(|cx| DebugPanel::new(DockPosition::Bottom, debug_client, cx)) + }) + } else { + dbg!(&debug_client); + Err(anyhow::anyhow!("Failed to start debug client")) + } } } @@ -118,6 +174,9 @@ impl Render for DebugPanel { .gap_2() .child( IconButton::new("debug-play", IconName::Play) + // .on_click(|_, cx| { + // self.debug_client.0.request("continue", None); + // }) .tooltip(move |cx| Tooltip::text("Start debug", cx)), ) .child( diff --git a/crates/debuggers/Cargo.toml b/crates/debuggers/Cargo.toml deleted file mode 100644 index 6a3638be22f89e..00000000000000 --- a/crates/debuggers/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "debuggers" -version = "0.1.0" -edition = "2021" -publish = false -license = "GPL-3.0-or-later" - -[dependencies] -dap.workspace = true -anyhow.workspace = true - -[lints] -workspace = true diff --git a/crates/debuggers/src/lib.rs b/crates/debuggers/src/lib.rs deleted file mode 100644 index a0ebc5f51bf5bd..00000000000000 --- a/crates/debuggers/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod node; -mod xdebug; diff --git a/crates/debuggers/src/node.rs b/crates/debuggers/src/node.rs deleted file mode 100644 index a3da178bf35998..00000000000000 --- a/crates/debuggers/src/node.rs +++ /dev/null @@ -1,9 +0,0 @@ -use dap::DebuggerAdapter; - -pub struct JsAdapter; - -impl DebuggerAdapter for JsAdapter { - fn initialize(&self, args: dap::requests::InitializeArguments) -> anyhow::Result<()> { - todo!() - } -} diff --git a/crates/debuggers/src/xdebug.rs b/crates/debuggers/src/xdebug.rs deleted file mode 100644 index 89c300fd3f90bf..00000000000000 --- a/crates/debuggers/src/xdebug.rs +++ /dev/null @@ -1,12 +0,0 @@ -use dap::{requests::InitializeArguments, DebuggerAdapter}; - -pub struct Xdebug; - -impl DebuggerAdapter for Xdebug { - fn initialize(&self, _args: InitializeArguments) -> anyhow::Result<()> { - if true != false { - println!("Hello, world!"); - } - todo!() - } -} From c130f9c2f264ea4992a7c278da8529b84ae36226 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 10 Jun 2024 21:41:36 +0200 Subject: [PATCH 005/650] Wip --- crates/dap/src/client.rs | 12 +++- crates/dap/src/lib.rs | 1 + crates/dap/src/requests.rs | 11 +++- crates/dap/src/responses.rs | 16 +++++ crates/debugger_ui/src/debugger_panel.rs | 79 +++++++++++++----------- 5 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 crates/dap/src/responses.rs diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 785ec29751ec8c..97d3c352b12658 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -4,7 +4,7 @@ use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; -use gpui::{AsyncWindowContext, Flatten}; +use gpui::AsyncWindowContext; use serde_json::Value; use smol::{ io::BufReader, @@ -12,9 +12,10 @@ use smol::{ process::{self, Child}, }; use std::{ - net::{Ipv4Addr, SocketAddrV4}, + net::{Ipv4Addr, SocketAddrV4, TcpListener}, process::Stdio, sync::atomic::{AtomicU64, Ordering}, + time::Duration, }; use util::ResultExt; @@ -53,7 +54,10 @@ impl Client { let mut command = process::Command::new("python3"); command .current_dir("/Users/remcosmits/Documents/code/debugpy") - .arg("-m debugpy --listen localhost:5679 --wait-for-client test.py") + .arg(format!( + "-m debugpy --listen localhost:{} --wait-for-client test.py", + 5679 + )) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) @@ -65,6 +69,8 @@ impl Client { let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 5679); + dbg!(addr); + let (rx, tx) = TcpStream::connect(addr).await?.split(); Self::handle_transport( diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index 26b1df72d97d3a..f1030c5fe6f52f 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,5 +1,6 @@ pub mod client; pub mod events; pub mod requests; +pub mod responses; pub mod transport; pub mod types; diff --git a/crates/dap/src/requests.rs b/crates/dap/src/requests.rs index 848c5ff27aac4b..be134ce2f4758a 100644 --- a/crates/dap/src/requests.rs +++ b/crates/dap/src/requests.rs @@ -53,11 +53,20 @@ impl Request for Initialize { const COMMAND: &'static str = "initialize"; } +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LaunchRequestArguments { + #[serde(skip_serializing_if = "Option::is_none")] + pub no_debug: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub __restart: Option, +} + #[derive(Debug)] pub enum Launch {} impl Request for Launch { - type Arguments = Value; + type Arguments = LaunchRequestArguments; type Result = (); const COMMAND: &'static str = "launch"; } diff --git a/crates/dap/src/responses.rs b/crates/dap/src/responses.rs new file mode 100644 index 00000000000000..a97afcbc0889b4 --- /dev/null +++ b/crates/dap/src/responses.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Response { + _type: String, + success: bool, + arguments: Option, + request_seq: i32, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +enum ResponseArguments { + LaunchResponse, +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 5074f9e7f6d197..a1d71ce638dfe4 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,7 +1,7 @@ use anyhow::Result; use dap::{ client::{Client, TransportType}, - requests::{Disconnect, Initialize, InitializeArguments}, + requests::{Initialize, Launch, LaunchRequestArguments}, transport::Payload, }; use futures::channel::mpsc::UnboundedReceiver; @@ -12,8 +12,8 @@ use gpui::{ use ui::{ div, h_flex, prelude::{IntoElement, Pixels, WindowContext}, - px, ButtonCommon, Clickable, Element, IconButton, IconName, ParentElement, Render, Styled, - Tooltip, VisualContext, + px, ButtonCommon, Element, IconButton, IconName, ParentElement, Render, Styled, Tooltip, + VisualContext, }; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -49,10 +49,10 @@ impl DebugPanel { pub async fn load( workspace: WeakView, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Result> { let mut cx = cx.clone(); - let debug_client = Client::new( + let (mut debug_client, events) = Client::new( TransportType::TCP, "python3", vec![ @@ -66,38 +66,45 @@ impl DebugPanel { None, &mut cx, ) - .await; - - if let Ok(mut debug_client) = debug_client { - let mut cx = cx.clone(); - - let args = dap::requests::InitializeArguments { - client_id: Some("hx".to_owned()), - client_name: Some("helix".to_owned()), - adapter_id: "debugpy".into(), - locale: Some("en-us".to_owned()), - lines_start_at_one: Some(true), - columns_start_at_one: Some(true), - path_format: Some("path".to_owned()), - supports_variable_type: Some(true), - supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(false), - supports_memory_references: Some(false), - supports_progress_reporting: Some(false), - supports_invalidated_event: Some(false), - }; - - debug_client.0.request::(args).await; - - debug_client.0.request::(None).await; - - workspace.update(&mut cx, |_, cx| { - cx.new_view(|cx| DebugPanel::new(DockPosition::Bottom, debug_client, cx)) + .await?; + + let mut cx = cx.clone(); + + let args = dap::requests::InitializeArguments { + client_id: Some("zed".to_owned()), + client_name: Some("zed".to_owned()), + adapter_id: "debugpy".into(), + locale: Some("en-us".to_owned()), + lines_start_at_one: Some(true), + columns_start_at_one: Some(true), + path_format: Some("path".to_owned()), + supports_variable_type: Some(true), + supports_variable_paging: Some(false), + supports_run_in_terminal_request: Some(false), + supports_memory_references: Some(false), + supports_progress_reporting: Some(false), + supports_invalidated_event: Some(false), + }; + + let capabilities = debug_client.request::(args).await; + + dbg!(capabilities); + + // launch/attach + let launch = debug_client + .request::(LaunchRequestArguments { + no_debug: Some(true), + __restart: None, }) - } else { - dbg!(&debug_client); - Err(anyhow::anyhow!("Failed to start debug client")) - } + .await; + + dbg!(launch); + + // configure request + + workspace.update(&mut cx, |_, cx| { + cx.new_view(|cx| DebugPanel::new(DockPosition::Bottom, (debug_client, events), cx)) + }) } } From 0e2a0b9edc1a30addc4c04f48c37507c40109a3d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 22 Jun 2024 20:22:12 +0200 Subject: [PATCH 006/650] Remove deps --- Cargo.lock | 14 ++------------ crates/dap/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7c7832940a0db..6e1c820d69f64c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3132,7 +3132,7 @@ name = "dap" version = "0.1.0" dependencies = [ "anyhow", - "dap 0.4.1-alpha1", + "async-std", "futures 0.3.28", "gpui", "log", @@ -3145,16 +3145,6 @@ dependencies = [ "util", ] -[[package]] -name = "dap" -version = "0.4.1-alpha1" -source = "git+https://github.com/sztomi/dap-rs?branch=main#913a2a520416a2a81a1f09a7946f595d5ae3ee4a" -dependencies = [ - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "dashmap" version = "5.5.3" @@ -3208,7 +3198,7 @@ name = "debugger_ui" version = "0.1.0" dependencies = [ "anyhow", - "dap 0.1.0", + "dap", "db", "editor", "futures 0.3.28", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 2fec49b31f1853..300fea3d28432b 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -10,7 +10,6 @@ workspace = true [dependencies] anyhow.workspace = true -dap = { git = "https://github.com/sztomi/dap-rs", branch = "main" } futures.workspace = true gpui.workspace = true log.workspace = true @@ -21,3 +20,4 @@ serde.workspace = true serde_json.workspace = true smol.workspace = true util.workspace = true +async-std = "1.12.0" From 7c355fdb0fc0f0970ccf71fb2d24fc3c548ebd85 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 22 Jun 2024 20:22:23 +0200 Subject: [PATCH 007/650] clean up --- crates/dap/src/transport.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index a73bc3120f3c48..701dd8b68b8546 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -12,7 +12,7 @@ use smol::io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _}; use std::{collections::HashMap, sync::Arc}; use util::ResultExt; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Request { #[serde(skip)] pub back_ch: Option>>, @@ -30,7 +30,7 @@ pub struct Response { pub body: Option, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Payload { Event(Box), @@ -59,7 +59,7 @@ impl Transport { let transport = Arc::new(transport); - cx.update(|cx| { + let _ = cx.update(|cx| { cx.spawn(|_| Self::recv(transport.clone(), server_stdout, client_tx)) .detach_and_log_err(cx); cx.spawn(|_| Self::send(transport, server_stdin, client_rx)) @@ -100,12 +100,11 @@ impl Transport { let content_length = content_length.context("missing content length")?; - //TODO: reuse vector let mut content = vec![0; content_length]; reader.read_exact(&mut content).await?; let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; - dbg!("<- DAP {}", msg); + dbg!(msg); Ok(serde_json::from_str::(msg)?) } From 1128fce61a6cb3604d3de75e2a357ac23e6cb751 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 22 Jun 2024 20:22:32 +0200 Subject: [PATCH 008/650] make thread id public --- crates/dap/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dap/src/types.rs b/crates/dap/src/types.rs index 0cbe738c5888e0..3ad683c2c8c3ad 100644 --- a/crates/dap/src/types.rs +++ b/crates/dap/src/types.rs @@ -5,7 +5,7 @@ use std::{collections::HashMap, fmt, path::PathBuf}; #[derive( Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, )] -pub struct ThreadId(isize); +pub struct ThreadId(pub isize); impl fmt::Display for ThreadId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { From 6b9295b6c4dce0d65b153a6641b942e7dbbc4f4c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 22 Jun 2024 21:17:32 +0200 Subject: [PATCH 009/650] wip write through buttons Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- crates/dap/src/client.rs | 165 +++++++++++++++++++---- crates/debugger_ui/src/debugger_panel.rs | 73 +++++----- 2 files changed, 173 insertions(+), 65 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 97d3c352b12658..901bd8e3638549 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,4 +1,12 @@ -use crate::transport::{Payload, Request, Transport}; +use crate::{ + requests::{ + Continue, ContinueArguments, Initialize, InitializeArguments, Launch, + LaunchRequestArguments, Next, NextArguments, SetBreakpoints, SetBreakpointsArguments, + StepIn, StepInArguments, StepOut, StepOutArguments, + }, + transport::{self, Payload, Request, Transport}, + types::{Source, SourceBreakpoint, ThreadId}, +}; use anyhow::{anyhow, Context, Result}; use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, @@ -12,7 +20,7 @@ use smol::{ process::{self, Child}, }; use std::{ - net::{Ipv4Addr, SocketAddrV4, TcpListener}, + net::{Ipv4Addr, SocketAddrV4}, process::Stdio, sync::atomic::{AtomicU64, Ordering}, time::Duration, @@ -29,6 +37,7 @@ pub struct Client { _process: Option, server_tx: UnboundedSender, request_count: AtomicU64, + thread_id: Option, } impl Client { @@ -51,27 +60,30 @@ impl Client { port_arg: Option<&str>, cx: &mut AsyncWindowContext, ) -> Result<(Self, UnboundedReceiver)> { - let mut command = process::Command::new("python3"); + let mut command = process::Command::new("bun"); command - .current_dir("/Users/remcosmits/Documents/code/debugpy") - .arg(format!( - "-m debugpy --listen localhost:{} --wait-for-client test.py", - 5679 - )) + .current_dir("/Users/remcosmits/Documents/code/symfony_demo") + .args([ + "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", + "--server=8123", + ]) .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) .kill_on_drop(true); let process = command .spawn() .with_context(|| "failed to spawn command.")?; - let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 5679); + // give the adapter some time to spin up the tcp server + cx.background_executor() + .timer(Duration::from_millis(500)) + .await; - dbg!(addr); + let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8123); - let (rx, tx) = TcpStream::connect(addr).await?.split(); + let (rx, tx) = TcpStream::connect(address).await?.split(); Self::handle_transport( Box::new(BufReader::new(rx)), @@ -105,6 +117,7 @@ impl Client { server_tx: server_tx.clone(), _process: process, request_count: AtomicU64::new(0), + thread_id: Some(ThreadId(1)), }; cx.spawn(move |_| Self::recv(server_rx, server_tx, client_tx)) @@ -128,30 +141,134 @@ impl Client { } pub async fn request( - &mut self, + &self, arguments: R::Arguments, ) -> Result { - let arguments = Some(serde_json::to_value(arguments)?); + let serialized_arguments = serde_json::to_value(arguments)?; - let (callback_tx, mut callback_rx) = channel(1); + let (callback_tx, mut callback_rx) = channel::>(1); let request = Request { back_ch: Some(callback_tx), seq: self.next_request_id(), command: R::COMMAND.to_string(), - arguments, + arguments: Some(serialized_arguments), }; - self.server_tx.send(Payload::Request(request)).await?; + self.server_tx + .clone() + .send(Payload::Request(request)) + .await?; + + let response = callback_rx.next().await.ok_or(anyhow!("no response"))??; - callback_rx - .next() - .await - .ok_or(anyhow!("no response"))? - .map(|response| response.body.unwrap_or_default()) + match response.success { + true => Ok(response.body.unwrap_or_default()), + false => Err(anyhow!("Request failed")), + } } - fn next_request_id(&mut self) -> u64 { + pub fn next_request_id(&self) -> u64 { self.request_count.fetch_add(1, Ordering::Relaxed) } + + pub async fn initialize(&self) -> Result { + let args = InitializeArguments { + client_id: Some("zed".to_owned()), + client_name: Some("zed".to_owned()), + adapter_id: "xdebug".into(), + locale: Some("en-us".to_owned()), + lines_start_at_one: Some(true), + columns_start_at_one: Some(true), + path_format: Some("path".to_owned()), + supports_variable_type: Some(true), + supports_variable_paging: Some(false), + supports_run_in_terminal_request: Some(false), + supports_memory_references: Some(false), + supports_progress_reporting: Some(false), + supports_invalidated_event: Some(false), + }; + + self.request::(args).await + } + + pub async fn launch(&mut self) -> Result { + self.request::(LaunchRequestArguments { + no_debug: Some(false), + __restart: None, + }) + .await + } + + pub async fn next_thread(&self) { + if let Some(thread_id) = self.thread_id { + let _ = self + .request::(NextArguments { + thread_id, + granularity: None, + }) + .await; + } + } + + pub async fn continue_thread(&self) { + if let Some(thread_id) = self.thread_id { + let _ = self + .request::(ContinueArguments { thread_id }) + .await; + } + } + + pub async fn step_in(&self) { + if let Some(thread_id) = self.thread_id { + let _ = self + .request::(StepInArguments { + thread_id, + target_id: None, + granularity: None, + }) + .await; + } + } + + pub async fn step_out(&self) { + if let Some(thread_id) = self.thread_id { + let _ = self + .request::(StepOutArguments { + thread_id, + granularity: None, + }) + .await; + } + } + + pub async fn step_back(&self) { + if let Some(thread_id) = self.thread_id { + let _ = self + .request::(StepInArguments { + thread_id, + target_id: None, + granularity: None, + }) + .await; + } + } + + pub async fn set_breakpoints(&self, line: usize) -> Result { + self.request::(SetBreakpointsArguments { + source: Source { + path: Some("/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into()), + ..Default::default() + }, + breakpoints: Some(vec![SourceBreakpoint { + line, + column: None, + condition: None, + hit_condition: None, + log_message: None, + }]), + source_modified: None, + }) + .await + } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a1d71ce638dfe4..8d7348096d3aa7 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,7 +1,8 @@ -use anyhow::Result; +use std::sync::Arc; + +use anyhow::{anyhow, Result}; use dap::{ client::{Client, TransportType}, - requests::{Initialize, Launch, LaunchRequestArguments}, transport::Payload, }; use futures::channel::mpsc::UnboundedReceiver; @@ -12,8 +13,8 @@ use gpui::{ use ui::{ div, h_flex, prelude::{IntoElement, Pixels, WindowContext}, - px, ButtonCommon, Element, IconButton, IconName, ParentElement, Render, Styled, Tooltip, - VisualContext, + px, ButtonCommon, Clickable, Element, IconButton, IconName, ParentElement, Render, Styled, + Tooltip, VisualContext, }; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -28,13 +29,15 @@ pub struct DebugPanel { pub active: bool, pub focus_handle: FocusHandle, pub size: Pixels, - debug_client: (Client, UnboundedReceiver), + pub debug_client: Arc, + pub events: UnboundedReceiver, } impl DebugPanel { pub fn new( position: DockPosition, - debug_client: (Client, UnboundedReceiver), + debug_client: Arc, + events: UnboundedReceiver, cx: &mut WindowContext, ) -> Self { Self { @@ -44,6 +47,7 @@ impl DebugPanel { focus_handle: cx.focus_handle(), size: px(300.), debug_client, + events, } } @@ -52,7 +56,7 @@ impl DebugPanel { cx: AsyncWindowContext, ) -> Result> { let mut cx = cx.clone(); - let (mut debug_client, events) = Client::new( + let c = Client::new( TransportType::TCP, "python3", vec![ @@ -66,44 +70,28 @@ impl DebugPanel { None, &mut cx, ) - .await?; - - let mut cx = cx.clone(); + .await; - let args = dap::requests::InitializeArguments { - client_id: Some("zed".to_owned()), - client_name: Some("zed".to_owned()), - adapter_id: "debugpy".into(), - locale: Some("en-us".to_owned()), - lines_start_at_one: Some(true), - columns_start_at_one: Some(true), - path_format: Some("path".to_owned()), - supports_variable_type: Some(true), - supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(false), - supports_memory_references: Some(false), - supports_progress_reporting: Some(false), - supports_invalidated_event: Some(false), + let Ok((mut debug_client, events)) = c else { + dbg!(&c); + return Err(anyhow!("Failed to create debug client")); }; - let capabilities = debug_client.request::(args).await; + // initialize request + debug_client.initialize().await; - dbg!(capabilities); + // launch/attach request + debug_client.launch().await; - // launch/attach - let launch = debug_client - .request::(LaunchRequestArguments { - no_debug: Some(true), - __restart: None, - }) - .await; - - dbg!(launch); + // TODO: we should send configure request here - // configure request + // set break point + debug_client.set_breakpoints(14).await; workspace.update(&mut cx, |_, cx| { - cx.new_view(|cx| DebugPanel::new(DockPosition::Bottom, (debug_client, events), cx)) + cx.new_view(|cx| { + DebugPanel::new(DockPosition::Bottom, Arc::new(debug_client), events, cx) + }) }) } } @@ -173,7 +161,7 @@ impl Panel for DebugPanel { } impl Render for DebugPanel { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() .child( h_flex() @@ -181,9 +169,12 @@ impl Render for DebugPanel { .gap_2() .child( IconButton::new("debug-play", IconName::Play) - // .on_click(|_, cx| { - // self.debug_client.0.request("continue", None); - // }) + .on_click(cx.listener(|view, _, cx| { + let client = view.debug_client.clone(); + cx.background_executor() + .spawn(async move { client.continue_thread().await }) + .detach(); + })) .tooltip(move |cx| Tooltip::text("Start debug", cx)), ) .child( From 95a814ed4143c33ccc4d81cbe2c2f39507b0ae04 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 22 Jun 2024 22:55:39 +0200 Subject: [PATCH 010/650] Wip breakpoints Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- Cargo.lock | 1 + crates/dap/src/client.rs | 18 ++++-- crates/debugger_ui/src/debugger_panel.rs | 34 ++++++++--- crates/editor/src/editor.rs | 33 ++++++++++- crates/editor/src/element.rs | 3 +- crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 72 +++++++++++++++++++++++- crates/workspace/src/workspace.rs | 1 + 8 files changed, 145 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e1c820d69f64c..5e54858ec3cfb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7798,6 +7798,7 @@ dependencies = [ "client", "clock", "collections", + "dap", "dev_server_projects", "env_logger", "fs", diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 901bd8e3638549..65dc4dbf023fa2 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,6 +1,6 @@ use crate::{ requests::{ - Continue, ContinueArguments, Initialize, InitializeArguments, Launch, + ConfigurationDone, Continue, ContinueArguments, Initialize, InitializeArguments, Launch, LaunchRequestArguments, Next, NextArguments, SetBreakpoints, SetBreakpointsArguments, StepIn, StepInArguments, StepOut, StepOutArguments, }, @@ -21,6 +21,7 @@ use smol::{ }; use std::{ net::{Ipv4Addr, SocketAddrV4}, + path::{Path, PathBuf}, process::Stdio, sync::atomic::{AtomicU64, Ordering}, time::Duration, @@ -31,16 +32,19 @@ pub enum TransportType { TCP, STDIO, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct DebugAdapterClientId(pub usize); #[derive(Debug)] -pub struct Client { +pub struct DebugAdapterClient { _process: Option, server_tx: UnboundedSender, request_count: AtomicU64, thread_id: Option, } -impl Client { +impl DebugAdapterClient { pub async fn new( transport_type: TransportType, command: &str, @@ -254,10 +258,10 @@ impl Client { } } - pub async fn set_breakpoints(&self, line: usize) -> Result { + pub async fn set_breakpoints(&self, path: PathBuf, line: usize) -> Result { self.request::(SetBreakpointsArguments { source: Source { - path: Some("/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into()), + path: Some(path), ..Default::default() }, breakpoints: Some(vec![SourceBreakpoint { @@ -271,4 +275,8 @@ impl Client { }) .await } + + pub async fn configuration_done(&self) -> Result { + self.request::(()).await + } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 8d7348096d3aa7..d9314bd6553a78 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use dap::{ - client::{Client, TransportType}, + client::{DebugAdapterClient, TransportType}, transport::Payload, }; use futures::channel::mpsc::UnboundedReceiver; @@ -29,14 +29,14 @@ pub struct DebugPanel { pub active: bool, pub focus_handle: FocusHandle, pub size: Pixels, - pub debug_client: Arc, + pub debug_client: Arc, pub events: UnboundedReceiver, } impl DebugPanel { pub fn new( position: DockPosition, - debug_client: Arc, + debug_client: Arc, events: UnboundedReceiver, cx: &mut WindowContext, ) -> Self { @@ -56,7 +56,7 @@ impl DebugPanel { cx: AsyncWindowContext, ) -> Result> { let mut cx = cx.clone(); - let c = Client::new( + let c = DebugAdapterClient::new( TransportType::TCP, "python3", vec![ @@ -80,13 +80,19 @@ impl DebugPanel { // initialize request debug_client.initialize().await; + // set break point + debug_client + .set_breakpoints( + "/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into(), + 14, + ) + .await; + // launch/attach request debug_client.launch().await; - // TODO: we should send configure request here - - // set break point - debug_client.set_breakpoints(14).await; + // configuration done + debug_client.configuration_done().await; workspace.update(&mut cx, |_, cx| { cx.new_view(|cx| { @@ -183,10 +189,22 @@ impl Render for DebugPanel { ) .child( IconButton::new("debug-go-in", IconName::Play) + .on_click(cx.listener(|view, _, cx| { + let client = view.debug_client.clone(); + cx.background_executor() + .spawn(async move { client.step_in().await }) + .detach(); + })) .tooltip(move |cx| Tooltip::text("Go in", cx)), ) .child( IconButton::new("debug-go-out", IconName::Play) + .on_click(cx.listener(|view, _, cx| { + let client = view.debug_client.clone(); + cx.background_executor() + .spawn(async move { client.step_out().await }) + .detach(); + })) .tooltip(move |cx| Tooltip::text("Go out", cx)), ) .child( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f888534f6ba381..523e46573039bb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -145,7 +145,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId}; use workspace::{ searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId, }; -use workspace::{OpenInTerminal, OpenTerminal, Toast}; +use workspace::{OpenInTerminal, OpenTerminal, Toast, ToggleBreakpoint}; use crate::hover_links::find_url; @@ -420,6 +420,12 @@ struct ResolvedTasks { templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, position: Anchor, } + +#[derive(Clone, Debug)] +struct Breakpoint { + line: BufferRow, +} + #[derive(Copy, Clone, Debug)] struct MultiBufferOffset(usize); #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] @@ -524,6 +530,7 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, + breakpoints: BTreeMap<(BufferId, BufferRow), Breakpoint>, } #[derive(Clone)] @@ -1784,6 +1791,7 @@ impl Editor { blame: None, blame_subscription: None, tasks: Default::default(), + breakpoints: Default::default(), _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), @@ -5523,6 +5531,29 @@ impl Editor { } } + pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { + let Some(project) = &self.project else { + return; + }; + let cursor_position: Point = self.selections.newest(cx).head(); + let target_row = cursor_position.row; + let Some(buffer) = self.buffer.read(cx).as_singleton() else { + return; + }; + + let buffer_id = buffer.read(cx).remote_id(); + let key = (buffer_id, target_row); + + if self.breakpoints.remove(&key).is_none() { + self.breakpoints + .insert(key, Breakpoint { line: target_row }); + } + + project.update(cx, |project, cx| { + project.update_breakpoint(buffer, target_row, cx); + }); + } + fn gather_revert_changes( &mut self, selections: &[Selection], diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 280be0252304b2..524080877c9a5f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -389,7 +389,8 @@ impl EditorElement { register_action(view, cx, Editor::accept_partial_inline_completion); register_action(view, cx, Editor::accept_inline_completion); register_action(view, cx, Editor::revert_selected_hunks); - register_action(view, cx, Editor::open_active_item_in_terminal) + register_action(view, cx, Editor::open_active_item_in_terminal); + register_action(view, cx, Editor::toggle_breakpoint); } fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) { diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index a35d2f73c4047f..892824e6ffd54c 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -30,6 +30,7 @@ async-trait.workspace = true client.workspace = true clock.workspace = true collections.workspace = true +dap.workspace = true dev_server_projects.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3de4567265ff75..ad4cc549c14098 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -20,6 +20,7 @@ use client::{ }; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; +use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use debounced_delay::DebouncedDelay; use futures::{ channel::{ @@ -46,8 +47,8 @@ use language::{ deserialize_anchor, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, split_operations, }, - range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel, - ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, + range_from_lsp, Bias, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, Capability, + CodeLabel, ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, @@ -80,7 +81,7 @@ use similar::{ChangeTag, TextDiff}; use smol::channel::{Receiver, Sender}; use smol::lock::Semaphore; use std::{ - borrow::Cow, + borrow::{BorrowMut, Cow}, cmp::{self, Ordering}, convert::TryInto, env, @@ -170,6 +171,8 @@ pub struct Project { HashMap)>, language_servers: HashMap, language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, + debug_adapters: HashMap, + breakpoints: HashMap>, language_server_statuses: BTreeMap, last_formatting_failure: Option, last_workspace_edits_by_language_server: HashMap, @@ -306,6 +309,10 @@ impl PartialEq for LanguageServerPromptRequest { } } +struct Breakpoint { + row: BufferRow, +} + #[derive(Clone, Debug, PartialEq)] pub enum Event { LanguageServerAdded(LanguageServerId), @@ -373,6 +380,11 @@ pub struct LanguageServerProgress { pub last_update_at: Instant, } +pub enum DebugAdapterClientState { + Starting(Task>>), + Running(Arc), +} + #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] pub struct ProjectPath { pub worktree_id: WorktreeId, @@ -750,6 +762,8 @@ impl Project { next_diagnostic_group_id: Default::default(), supplementary_language_servers: HashMap::default(), language_servers: Default::default(), + debug_adapters: Default::default(), + breakpoints: Default::default(), language_server_ids: HashMap::default(), language_server_statuses: Default::default(), last_formatting_failure: None, @@ -904,6 +918,8 @@ impl Project { ) }) .collect(), + debug_adapters: Default::default(), + breakpoints: Default::default(), last_formatting_failure: None, last_workspace_edits_by_language_server: Default::default(), language_server_watched_paths: HashMap::default(), @@ -1007,6 +1023,56 @@ impl Project { } } + pub fn update_breakpoint( + &mut self, + buffer: Model, + row: BufferRow, + cx: &mut ModelContext, + ) { + let buffer = buffer.read(cx); + let Some(abs_path) = maybe!({ + let project_path = buffer.project_path(cx)?; + let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + worktree.read(cx).absolutize(&project_path.path).ok() + }) else { + return; + }; + + let breakpoints_for_buffer = self + .breakpoints + .entry(buffer.remote_id()) + .or_insert(Vec::new()); + + if let Some(ix) = breakpoints_for_buffer + .iter() + .position(|breakpoint| breakpoint.row == row) + { + breakpoints_for_buffer.remove(ix); + } else { + breakpoints_for_buffer.push(Breakpoint { row }); + } + + let clients = self + .debug_adapters + .iter() + .filter_map(|(_, state)| match state { + DebugAdapterClientState::Starting(_) => None, + DebugAdapterClientState::Running(client) => Some(client.clone()), + }) + .collect::>(); + + cx.background_executor() + .spawn(async move { + for client in clients { + client + .set_breakpoints(abs_path.clone(), row as usize) + .await?; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + fn shutdown_language_servers( &mut self, _cx: &mut ModelContext, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d4989d0d58b422..9512f24c5ebdce 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -137,6 +137,7 @@ actions!( ToggleBottomDock, ToggleCenteredLayout, CloseAllDocks, + ToggleBreakpoint, ] ); From 944a52ce911bfd7d2d58d944cece1dc74d06abbb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 22 Jun 2024 23:47:08 +0200 Subject: [PATCH 011/650] Wip break points Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- crates/dap/src/client.rs | 32 +++++----- crates/dap/src/transport.rs | 4 +- crates/debugger_ui/src/debugger_panel.rs | 79 ++++++++---------------- crates/editor/src/editor.rs | 33 ++++++++-- crates/editor/src/element.rs | 52 ++++++++++++++++ crates/project/src/project.rs | 58 ++++++++++++++++- 6 files changed, 177 insertions(+), 81 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 65dc4dbf023fa2..9028ddc87a1406 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -12,7 +12,7 @@ use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; -use gpui::AsyncWindowContext; +use gpui::AsyncAppContext; use serde_json::Value; use smol::{ io::BufReader, @@ -21,7 +21,7 @@ use smol::{ }; use std::{ net::{Ipv4Addr, SocketAddrV4}, - path::{Path, PathBuf}, + path::PathBuf, process::Stdio, sync::atomic::{AtomicU64, Ordering}, time::Duration, @@ -49,28 +49,24 @@ impl DebugAdapterClient { transport_type: TransportType, command: &str, args: Vec<&str>, - port_arg: Option<&str>, - cx: &mut AsyncWindowContext, + port: Option, + cx: &mut AsyncAppContext, ) -> Result<(Self, UnboundedReceiver)> { match transport_type { - TransportType::TCP => Self::create_tcp_client(command, args, port_arg, cx).await, - TransportType::STDIO => Self::create_stdio_client(command, args, port_arg, cx).await, + TransportType::TCP => Self::create_tcp_client(command, args, port, cx).await, + TransportType::STDIO => Self::create_stdio_client(command, args, port, cx).await, } } async fn create_tcp_client( command: &str, args: Vec<&str>, - port_arg: Option<&str>, - cx: &mut AsyncWindowContext, + port: Option, + cx: &mut AsyncAppContext, ) -> Result<(Self, UnboundedReceiver)> { - let mut command = process::Command::new("bun"); + let mut command = process::Command::new(command); command - .current_dir("/Users/remcosmits/Documents/code/symfony_demo") - .args([ - "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", - "--server=8123", - ]) + .args(args) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -85,7 +81,7 @@ impl DebugAdapterClient { .timer(Duration::from_millis(500)) .await; - let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8123); + let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port.unwrap_or(0)); let (rx, tx) = TcpStream::connect(address).await?.split(); @@ -101,8 +97,8 @@ impl DebugAdapterClient { async fn create_stdio_client( command: &str, args: Vec<&str>, - port_arg: Option<&str>, - cx: &mut AsyncWindowContext, + port: Option, + cx: &mut AsyncAppContext, ) -> Result<(Self, UnboundedReceiver)> { todo!("not implemented") } @@ -112,7 +108,7 @@ impl DebugAdapterClient { tx: Box, err: Option>, process: Option, - cx: &mut AsyncWindowContext, + cx: &mut AsyncAppContext, ) -> Result<(Self, UnboundedReceiver)> { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 701dd8b68b8546..0ca9d580e66f9d 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -4,7 +4,7 @@ use futures::{ channel::mpsc::{unbounded, Sender, UnboundedReceiver, UnboundedSender}, AsyncBufRead, AsyncWrite, SinkExt as _, StreamExt, }; -use gpui::AsyncWindowContext; +use gpui::AsyncAppContext; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -48,7 +48,7 @@ impl Transport { server_stdout: Box, server_stdin: Box, server_stderr: Option>, - cx: &mut AsyncWindowContext, + cx: &mut AsyncAppContext, ) -> (UnboundedReceiver, UnboundedSender) { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index d9314bd6553a78..bb2466026007cd 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use dap::{ - client::{DebugAdapterClient, TransportType}, + client::{DebugAdapterClient, DebugAdapterClientId, TransportType}, transport::Payload, }; use futures::channel::mpsc::UnboundedReceiver; @@ -29,15 +29,13 @@ pub struct DebugPanel { pub active: bool, pub focus_handle: FocusHandle, pub size: Pixels, - pub debug_client: Arc, - pub events: UnboundedReceiver, + pub workspace: WeakView, } impl DebugPanel { pub fn new( position: DockPosition, - debug_client: Arc, - events: UnboundedReceiver, + workspace: WeakView, cx: &mut WindowContext, ) -> Self { Self { @@ -46,59 +44,32 @@ impl DebugPanel { active: false, focus_handle: cx.focus_handle(), size: px(300.), - debug_client, - events, + workspace, } } pub async fn load( workspace: WeakView, - cx: AsyncWindowContext, + mut cx: AsyncWindowContext, ) -> Result> { - let mut cx = cx.clone(); - let c = DebugAdapterClient::new( - TransportType::TCP, - "python3", - vec![ - "-m", - "debugpy", - "--listen", - "localhost:5668", - "--wait-for-client", - "test.py", - ], - None, - &mut cx, - ) - .await; - - let Ok((mut debug_client, events)) = c else { - dbg!(&c); - return Err(anyhow!("Failed to create debug client")); - }; - - // initialize request - debug_client.initialize().await; - - // set break point - debug_client - .set_breakpoints( - "/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into(), - 14, - ) - .await; - - // launch/attach request - debug_client.launch().await; - - // configuration done - debug_client.configuration_done().await; - - workspace.update(&mut cx, |_, cx| { - cx.new_view(|cx| { - DebugPanel::new(DockPosition::Bottom, Arc::new(debug_client), events, cx) + workspace.update(&mut cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.start_debug_adapter_client(DebugAdapterClientId(1), cx); + }); + })?; + cx.new_view(|cx| DebugPanel::new(DockPosition::Bottom, workspace, cx)) + } + + fn debug_adapter(&self, cx: &mut ViewContext) -> Arc { + self.workspace + .update(cx, |this, cx| { + this.project() + .read(cx) + .running_debug_adapters() + .next() + .unwrap() }) - }) + .unwrap() } } @@ -176,7 +147,7 @@ impl Render for DebugPanel { .child( IconButton::new("debug-play", IconName::Play) .on_click(cx.listener(|view, _, cx| { - let client = view.debug_client.clone(); + let client = view.debug_adapter(cx); cx.background_executor() .spawn(async move { client.continue_thread().await }) .detach(); @@ -190,7 +161,7 @@ impl Render for DebugPanel { .child( IconButton::new("debug-go-in", IconName::Play) .on_click(cx.listener(|view, _, cx| { - let client = view.debug_client.clone(); + let client = view.debug_adapter(cx); cx.background_executor() .spawn(async move { client.step_in().await }) .detach(); @@ -200,7 +171,7 @@ impl Render for DebugPanel { .child( IconButton::new("debug-go-out", IconName::Play) .on_click(cx.listener(|view, _, cx| { - let client = view.debug_client.clone(); + let client = view.debug_adapter(cx); cx.background_executor() .spawn(async move { client.step_out().await }) .detach(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 523e46573039bb..7799dcaed8ecda 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -423,6 +423,7 @@ struct ResolvedTasks { #[derive(Clone, Debug)] struct Breakpoint { + row: MultiBufferRow, line: BufferRow, } @@ -4762,6 +4763,17 @@ impl Editor { } } + fn render_breakpoint(&self, row: DisplayRow, cx: &mut ViewContext) -> IconButton { + IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) + .icon_color(Color::Error) + .on_click(cx.listener(move |editor, _e, cx| { + editor.focus(cx); + editor.toggle_breakpoint_at_row(row.0, cx) //TODO handle folded + })) + } + fn render_run_indicator( &self, _style: &EditorStyle, @@ -5532,26 +5544,35 @@ impl Editor { } pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { + let cursor_position: Point = self.selections.newest(cx).head(); + self.toggle_breakpoint_at_row(cursor_position.row, cx); + } + + pub fn toggle_breakpoint_at_row(&mut self, row: u32, cx: &mut ViewContext) { let Some(project) = &self.project else { return; }; - let cursor_position: Point = self.selections.newest(cx).head(); - let target_row = cursor_position.row; let Some(buffer) = self.buffer.read(cx).as_singleton() else { return; }; let buffer_id = buffer.read(cx).remote_id(); - let key = (buffer_id, target_row); + let key = (buffer_id, row); if self.breakpoints.remove(&key).is_none() { - self.breakpoints - .insert(key, Breakpoint { line: target_row }); + self.breakpoints.insert( + key, + Breakpoint { + row: MultiBufferRow(row), + line: row, + }, + ); } project.update(cx, |project, cx| { - project.update_breakpoint(buffer, target_row, cx); + project.update_breakpoint(buffer, row, cx); }); + cx.notify(); } fn gather_revert_changes( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 524080877c9a5f..0fa69da7b1d2a3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1511,6 +1511,43 @@ impl EditorElement { (offset_y, length) } + fn layout_breakpoints( + &self, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + gutter_dimensions: &GutterDimensions, + gutter_hitbox: &Hitbox, + snapshot: &EditorSnapshot, + cx: &mut WindowContext, + ) -> Vec { + self.editor.update(cx, |editor, cx| { + editor + .breakpoints + .iter() + .filter_map(|(_, breakpoint)| { + if snapshot.is_line_folded(breakpoint.row) { + return None; + } + let display_row = Point::new(breakpoint.row.0, 0) + .to_display_point(snapshot) + .row(); + let button = editor.render_breakpoint(display_row, cx); + + let button = prepaint_gutter_button( + button, + display_row, + line_height, + gutter_dimensions, + scroll_pixel_position, + gutter_hitbox, + cx, + ); + Some(button) + }) + .collect_vec() + }) + } + fn layout_run_indicators( &self, line_height: Pixels, @@ -2857,6 +2894,10 @@ impl EditorElement { } }); + for breakpoint in layout.breakpoints.iter_mut() { + breakpoint.paint(cx); + } + for test_indicators in layout.test_indicators.iter_mut() { test_indicators.paint(cx); } @@ -4826,6 +4867,15 @@ impl Element for EditorElement { } } + let breakpoints = self.layout_breakpoints( + line_height, + scroll_pixel_position, + &gutter_dimensions, + &gutter_hitbox, + &snapshot, + cx, + ); + let test_indicators = self.layout_run_indicators( line_height, scroll_pixel_position, @@ -4932,6 +4982,7 @@ impl Element for EditorElement { selections, mouse_context_menu, test_indicators, + breakpoints, code_actions_indicator, gutter_fold_toggles, flap_trailers, @@ -5056,6 +5107,7 @@ pub struct EditorLayout { selections: Vec<(PlayerColor, Vec)>, code_actions_indicator: Option, test_indicators: Vec, + breakpoints: Vec, gutter_fold_toggles: Vec>, flap_trailers: Vec>, mouse_context_menu: Option, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ad4cc549c14098..93a3221a358f15 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -20,7 +20,7 @@ use client::{ }; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; -use dap::client::{DebugAdapterClient, DebugAdapterClientId}; +use dap::client::{DebugAdapterClient, DebugAdapterClientId, TransportType}; use debounced_delay::DebouncedDelay; use futures::{ channel::{ @@ -1023,6 +1023,61 @@ impl Project { } } + pub fn running_debug_adapters(&self) -> impl Iterator> + '_ { + self.debug_adapters + .values() + .filter_map(|state| match state { + DebugAdapterClientState::Starting(_) => None, + DebugAdapterClientState::Running(client) => Some(client.clone()), + }) + } + + pub fn start_debug_adapter_client( + &mut self, + id: DebugAdapterClientId, + cx: &mut ModelContext, + ) { + let task = cx.spawn(|this, mut cx| async move { + let (mut client, _) = DebugAdapterClient::new( + TransportType::TCP, + "bun", + vec![ + "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", + "--server=8123", + ], + Some(8123), + &mut cx, + ) + .await + .log_err()?; + + client.initialize().await.log_err()?; + + // configuration done + client.configuration_done().await.log_err()?; + + // launch/attach request + client.launch().await.log_err()?; + + let client = Arc::new(client); + + this.update(&mut cx, |this, _| { + let handle = this + .debug_adapters + .get_mut(&id) + .with_context(|| "Failed to find debug adapter with given id")?; + *handle = DebugAdapterClientState::Running(client.clone()); + anyhow::Ok(()) + }) + .log_err(); + + Some(client) + }); + + self.debug_adapters + .insert(id, DebugAdapterClientState::Starting(task)); + } + pub fn update_breakpoint( &mut self, buffer: Model, @@ -1035,6 +1090,7 @@ impl Project { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; worktree.read(cx).absolutize(&project_path.path).ok() }) else { + dbg!("Failed to get abs_path"); return; }; From 7e438bc1f3ac63949462e3ba77dfdde93f048800 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 23 Jun 2024 00:36:31 +0200 Subject: [PATCH 012/650] Wip breakpoints Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- crates/dap/src/client.rs | 20 +++++----- crates/project/src/project.rs | 74 +++++++++++++++++++---------------- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 9028ddc87a1406..f8a6b462200874 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -42,6 +42,7 @@ pub struct DebugAdapterClient { server_tx: UnboundedSender, request_count: AtomicU64, thread_id: Option, + client_rx: UnboundedReceiver, } impl DebugAdapterClient { @@ -49,9 +50,9 @@ impl DebugAdapterClient { transport_type: TransportType, command: &str, args: Vec<&str>, - port: Option, + port: u16, cx: &mut AsyncAppContext, - ) -> Result<(Self, UnboundedReceiver)> { + ) -> Result { match transport_type { TransportType::TCP => Self::create_tcp_client(command, args, port, cx).await, TransportType::STDIO => Self::create_stdio_client(command, args, port, cx).await, @@ -61,9 +62,9 @@ impl DebugAdapterClient { async fn create_tcp_client( command: &str, args: Vec<&str>, - port: Option, + port: u16, cx: &mut AsyncAppContext, - ) -> Result<(Self, UnboundedReceiver)> { + ) -> Result { let mut command = process::Command::new(command); command .args(args) @@ -81,7 +82,7 @@ impl DebugAdapterClient { .timer(Duration::from_millis(500)) .await; - let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port.unwrap_or(0)); + let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port); let (rx, tx) = TcpStream::connect(address).await?.split(); @@ -97,9 +98,9 @@ impl DebugAdapterClient { async fn create_stdio_client( command: &str, args: Vec<&str>, - port: Option, + port: u16, cx: &mut AsyncAppContext, - ) -> Result<(Self, UnboundedReceiver)> { + ) -> Result { todo!("not implemented") } @@ -109,7 +110,7 @@ impl DebugAdapterClient { err: Option>, process: Option, cx: &mut AsyncAppContext, - ) -> Result<(Self, UnboundedReceiver)> { + ) -> Result { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); @@ -118,12 +119,13 @@ impl DebugAdapterClient { _process: process, request_count: AtomicU64::new(0), thread_id: Some(ThreadId(1)), + client_rx, // TODO: remove this here }; cx.spawn(move |_| Self::recv(server_rx, server_tx, client_tx)) .detach(); - Ok((client, client_rx)) + Ok(client) } async fn recv( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 93a3221a358f15..ff2cebe0c3152a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1037,45 +1037,52 @@ impl Project { id: DebugAdapterClientId, cx: &mut ModelContext, ) { - let task = cx.spawn(|this, mut cx| async move { - let (mut client, _) = DebugAdapterClient::new( - TransportType::TCP, - "bun", - vec![ - "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", - "--server=8123", - ], - Some(8123), - &mut cx, - ) - .await - .log_err()?; - - client.initialize().await.log_err()?; + self.debug_adapters + .insert(id, DebugAdapterClientState::Starting(Task::Ready(None))); + + let task = cx + .spawn(|this, mut cx| async move { + let mut client = DebugAdapterClient::new( + TransportType::TCP, + "bun", + vec![ + "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", + "--server=8125", + ], + 8125, + &mut cx, + ) + .await?; - // configuration done - client.configuration_done().await.log_err()?; + // initialize + client.initialize().await?; - // launch/attach request - client.launch().await.log_err()?; + // set break point + client + .set_breakpoints( + "/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into(), + 14, + ) + .await; - let client = Arc::new(client); + // launch/attach request + client.launch().await?; - this.update(&mut cx, |this, _| { - let handle = this - .debug_adapters - .get_mut(&id) - .with_context(|| "Failed to find debug adapter with given id")?; - *handle = DebugAdapterClientState::Running(client.clone()); - anyhow::Ok(()) - }) - .log_err(); + // configuration done + client.configuration_done().await?; - Some(client) - }); + let client = Arc::new(client); - self.debug_adapters - .insert(id, DebugAdapterClientState::Starting(task)); + this.update(&mut cx, |this, _| { + let handle = this + .debug_adapters + .get_mut(&id) + .with_context(|| "Failed to find debug adapter with given id")?; + *handle = DebugAdapterClientState::Running(client.clone()); + anyhow::Ok(()) + }) + }) + .detach_and_log_err(cx); } pub fn update_breakpoint( @@ -1090,7 +1097,6 @@ impl Project { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; worktree.read(cx).absolutize(&project_path.path).ok() }) else { - dbg!("Failed to get abs_path"); return; }; From 9cff6d5aa5430cfb09efcf980f85db217e00678c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 23 Jun 2024 01:53:41 +0200 Subject: [PATCH 013/650] Wire through project path Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- crates/dap/src/client.rs | 14 ++++-- crates/project/src/project.rs | 81 ++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f8a6b462200874..52348a83074151 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -51,11 +51,16 @@ impl DebugAdapterClient { command: &str, args: Vec<&str>, port: u16, + project_path: PathBuf, cx: &mut AsyncAppContext, ) -> Result { match transport_type { - TransportType::TCP => Self::create_tcp_client(command, args, port, cx).await, - TransportType::STDIO => Self::create_stdio_client(command, args, port, cx).await, + TransportType::TCP => { + Self::create_tcp_client(command, args, port, project_path, cx).await + } + TransportType::STDIO => { + Self::create_stdio_client(command, args, port, project_path, cx).await + } } } @@ -63,10 +68,12 @@ impl DebugAdapterClient { command: &str, args: Vec<&str>, port: u16, + project_path: PathBuf, cx: &mut AsyncAppContext, ) -> Result { let mut command = process::Command::new(command); command + .current_dir(project_path) .args(args) .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -99,6 +106,7 @@ impl DebugAdapterClient { command: &str, args: Vec<&str>, port: u16, + project_path: PathBuf, cx: &mut AsyncAppContext, ) -> Result { todo!("not implemented") @@ -177,7 +185,7 @@ impl DebugAdapterClient { pub async fn initialize(&self) -> Result { let args = InitializeArguments { client_id: Some("zed".to_owned()), - client_name: Some("zed".to_owned()), + client_name: Some("Zed".to_owned()), adapter_id: "xdebug".into(), locale: Some("en-us".to_owned()), lines_start_at_one: Some(true), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ff2cebe0c3152a..02b53e29d1cb6e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1037,52 +1037,55 @@ impl Project { id: DebugAdapterClientId, cx: &mut ModelContext, ) { - self.debug_adapters - .insert(id, DebugAdapterClientState::Starting(Task::Ready(None))); - - let task = cx - .spawn(|this, mut cx| async move { - let mut client = DebugAdapterClient::new( - TransportType::TCP, - "bun", - vec![ - "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", - "--server=8125", - ], - 8125, - &mut cx, - ) - .await?; + let task = cx.spawn(|this, mut cx| async move { + let mut client = DebugAdapterClient::new( + TransportType::TCP, + "bun", + vec![ + "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", + "--server=8127", + ], + 8127, + "/Users/remcosmits/Documents/code/symfony_demo".into(), + &mut cx, + ) + .await + .log_err()?; - // initialize - client.initialize().await?; + // initialize + client.initialize().await.log_err()?; - // set break point - client - .set_breakpoints( - "/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into(), - 14, - ) - .await; + // set break point + client + .set_breakpoints( + "/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into(), + 14, + ) + .await; - // launch/attach request - client.launch().await?; + // configuration done + client.configuration_done().await.log_err()?; - // configuration done - client.configuration_done().await?; + // launch/attach request + client.launch().await.log_err()?; - let client = Arc::new(client); + let client = Arc::new(client); - this.update(&mut cx, |this, _| { - let handle = this - .debug_adapters - .get_mut(&id) - .with_context(|| "Failed to find debug adapter with given id")?; - *handle = DebugAdapterClientState::Running(client.clone()); - anyhow::Ok(()) - }) + this.update(&mut cx, |this, _| { + let handle = this + .debug_adapters + .get_mut(&id) + .with_context(|| "Failed to find debug adapter with given id")?; + *handle = DebugAdapterClientState::Running(client.clone()); + anyhow::Ok(()) }) - .detach_and_log_err(cx); + .log_err(); + + Some(client) + }); + + self.debug_adapters + .insert(id, DebugAdapterClientState::Starting(task)); } pub fn update_breakpoint( From 547c40e332db824139a63d5c2902cae59eb00e25 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 23 Jun 2024 10:42:34 +0200 Subject: [PATCH 014/650] change duration --- crates/dap/src/client.rs | 2 +- crates/project/src/project.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 52348a83074151..cc3e5c42bf47bd 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -86,7 +86,7 @@ impl DebugAdapterClient { // give the adapter some time to spin up the tcp server cx.background_executor() - .timer(Duration::from_millis(500)) + .timer(Duration::from_millis(1000)) .await; let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 02b53e29d1cb6e..7a0dc97653b14e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1043,9 +1043,9 @@ impl Project { "bun", vec![ "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", - "--server=8127", + "--server=8130", ], - 8127, + 8130, "/Users/remcosmits/Documents/code/symfony_demo".into(), &mut cx, ) From 14b913fb4b1e800f99b1fa0af969b1cfa7466c8b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 23 Jun 2024 11:21:15 +0200 Subject: [PATCH 015/650] Make request return result of struct instead of Value --- crates/dap/src/client.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index cc3e5c42bf47bd..acd911227b1c76 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,11 +1,12 @@ use crate::{ requests::{ ConfigurationDone, Continue, ContinueArguments, Initialize, InitializeArguments, Launch, - LaunchRequestArguments, Next, NextArguments, SetBreakpoints, SetBreakpointsArguments, - StepIn, StepInArguments, StepOut, StepOutArguments, + LaunchRequestArguments, Next, NextArguments, ScopesResponse, SetBreakpoints, + SetBreakpointsArguments, SetBreakpointsResponse, StepIn, StepInArguments, StepOut, + StepOutArguments, }, transport::{self, Payload, Request, Transport}, - types::{Source, SourceBreakpoint, ThreadId}, + types::{DebuggerCapabilities, Source, SourceBreakpoint, ThreadId}, }; use anyhow::{anyhow, Context, Result}; use futures::{ @@ -43,6 +44,7 @@ pub struct DebugAdapterClient { request_count: AtomicU64, thread_id: Option, client_rx: UnboundedReceiver, + capabilities: Option, } impl DebugAdapterClient { @@ -128,6 +130,7 @@ impl DebugAdapterClient { request_count: AtomicU64::new(0), thread_id: Some(ThreadId(1)), client_rx, // TODO: remove this here + capabilities: None, }; cx.spawn(move |_| Self::recv(server_rx, server_tx, client_tx)) @@ -153,7 +156,7 @@ impl DebugAdapterClient { pub async fn request( &self, arguments: R::Arguments, - ) -> Result { + ) -> Result { let serialized_arguments = serde_json::to_value(arguments)?; let (callback_tx, mut callback_rx) = channel::>(1); @@ -173,7 +176,7 @@ impl DebugAdapterClient { let response = callback_rx.next().await.ok_or(anyhow!("no response"))??; match response.success { - true => Ok(response.body.unwrap_or_default()), + true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), false => Err(anyhow!("Request failed")), } } @@ -182,7 +185,7 @@ impl DebugAdapterClient { self.request_count.fetch_add(1, Ordering::Relaxed) } - pub async fn initialize(&self) -> Result { + pub async fn initialize(&mut self) -> Result { let args = InitializeArguments { client_id: Some("zed".to_owned()), client_name: Some("Zed".to_owned()), @@ -199,10 +202,14 @@ impl DebugAdapterClient { supports_invalidated_event: Some(false), }; - self.request::(args).await + let capabilities = self.request::(args).await?; + + self.capabilities = Some(capabilities.clone()); + + Ok(capabilities) } - pub async fn launch(&mut self) -> Result { + pub async fn launch(&mut self) -> Result<()> { self.request::(LaunchRequestArguments { no_debug: Some(false), __restart: None, @@ -264,7 +271,11 @@ impl DebugAdapterClient { } } - pub async fn set_breakpoints(&self, path: PathBuf, line: usize) -> Result { + pub async fn set_breakpoints( + &self, + path: PathBuf, + line: usize, + ) -> Result { self.request::(SetBreakpointsArguments { source: Source { path: Some(path), @@ -282,7 +293,7 @@ impl DebugAdapterClient { .await } - pub async fn configuration_done(&self) -> Result { + pub async fn configuration_done(&self) -> Result<()> { self.request::(()).await } } From 0d97e9e5797fa410f25fe2f01f79fe87fbfdd205 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 23 Jun 2024 14:27:07 +0200 Subject: [PATCH 016/650] Send debug adapter event through project and listen in debug panel --- Cargo.lock | 1 + crates/dap/src/client.rs | 68 ++++++++++++++++++------ crates/dap/src/events.rs | 2 +- crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/debugger_panel.rs | 41 +++++++++++--- crates/editor/src/editor.rs | 2 +- crates/project/src/project.rs | 40 +++++++++++--- 7 files changed, 124 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b974f723829a29..cf8a20db5cc320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3265,6 +3265,7 @@ dependencies = [ "editor", "futures 0.3.28", "gpui", + "project", "serde", "serde_derive", "ui", diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index acd911227b1c76..b74302e522540c 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,9 +1,9 @@ use crate::{ + events::{self, Event}, requests::{ ConfigurationDone, Continue, ContinueArguments, Initialize, InitializeArguments, Launch, - LaunchRequestArguments, Next, NextArguments, ScopesResponse, SetBreakpoints, - SetBreakpointsArguments, SetBreakpointsResponse, StepIn, StepInArguments, StepOut, - StepOutArguments, + LaunchRequestArguments, Next, NextArguments, SetBreakpoints, SetBreakpointsArguments, + SetBreakpointsResponse, StepIn, StepInArguments, StepOut, StepOutArguments, }, transport::{self, Payload, Request, Transport}, types::{DebuggerCapabilities, Source, SourceBreakpoint, ThreadId}, @@ -13,8 +13,7 @@ use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; -use gpui::AsyncAppContext; -use serde_json::Value; +use gpui::{AppContext, AsyncAppContext}; use smol::{ io::BufReader, net::TcpStream, @@ -22,6 +21,7 @@ use smol::{ }; use std::{ net::{Ipv4Addr, SocketAddrV4}, + ops::DerefMut, path::PathBuf, process::Stdio, sync::atomic::{AtomicU64, Ordering}, @@ -43,22 +43,25 @@ pub struct DebugAdapterClient { server_tx: UnboundedSender, request_count: AtomicU64, thread_id: Option, - client_rx: UnboundedReceiver, capabilities: Option, } impl DebugAdapterClient { - pub async fn new( + pub async fn new( transport_type: TransportType, command: &str, args: Vec<&str>, port: u16, project_path: PathBuf, cx: &mut AsyncAppContext, - ) -> Result { + event_handler: F, + ) -> Result + where + F: FnMut(events::Event, &mut AppContext) + 'static + Send + Sync + Clone, + { match transport_type { TransportType::TCP => { - Self::create_tcp_client(command, args, port, project_path, cx).await + Self::create_tcp_client(command, args, port, project_path, cx, event_handler).await } TransportType::STDIO => { Self::create_stdio_client(command, args, port, project_path, cx).await @@ -66,13 +69,17 @@ impl DebugAdapterClient { } } - async fn create_tcp_client( + async fn create_tcp_client( command: &str, args: Vec<&str>, port: u16, project_path: PathBuf, cx: &mut AsyncAppContext, - ) -> Result { + event_handler: F, + ) -> Result + where + F: FnMut(events::Event, &mut AppContext) + 'static + Send + Sync + Clone, + { let mut command = process::Command::new(command); command .current_dir(project_path) @@ -101,6 +108,7 @@ impl DebugAdapterClient { None, Some(process), cx, + event_handler, ) } @@ -114,13 +122,17 @@ impl DebugAdapterClient { todo!("not implemented") } - pub fn handle_transport( + pub fn handle_transport( rx: Box, tx: Box, err: Option>, process: Option, cx: &mut AsyncAppContext, - ) -> Result { + event_handler: F, + ) -> Result + where + F: FnMut(events::Event, &mut AppContext) + 'static + Send + Sync + Clone, + { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); @@ -129,17 +141,37 @@ impl DebugAdapterClient { _process: process, request_count: AtomicU64::new(0), thread_id: Some(ThreadId(1)), - client_rx, // TODO: remove this here capabilities: None, }; - cx.spawn(move |_| Self::recv(server_rx, server_tx, client_tx)) + cx.spawn(move |cx| Self::handle_events(client_rx, event_handler, cx)) + .detach(); + + cx.spawn(move |_| Self::handle_recv(server_rx, server_tx, client_tx)) .detach(); Ok(client) } - async fn recv( + async fn handle_events( + mut client_rx: UnboundedReceiver, + mut event_handler: F, + cx: AsyncAppContext, + ) -> Result<()> + where + F: FnMut(events::Event, &mut AppContext) + 'static + Send + Sync + Clone, + { + while let Some(payload) = client_rx.next().await { + cx.update(|cx| match payload { + Payload::Event(event) => event_handler(*event, cx), + _ => unreachable!(), + })?; + } + + anyhow::Ok(()) + } + + async fn handle_recv( mut server_rx: UnboundedReceiver, mut server_tx: UnboundedSender, mut client_tx: UnboundedSender, @@ -185,6 +217,10 @@ impl DebugAdapterClient { self.request_count.fetch_add(1, Ordering::Relaxed) } + pub fn update_thread_id(&mut self, thread_id: ThreadId) { + self.thread_id = Some(thread_id); + } + pub async fn initialize(&mut self) -> Result { let args = InitializeArguments { client_id: Some("zed".to_owned()), diff --git a/crates/dap/src/events.rs b/crates/dap/src/events.rs index 5f8de379bbc9e9..052514098d468d 100644 --- a/crates/dap/src/events.rs +++ b/crates/dap/src/events.rs @@ -2,7 +2,7 @@ use crate::types::{DebuggerCapabilities, Source, ThreadId}; use serde::{Deserialize, Serialize}; use serde_json::Value; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] #[serde(tag = "event", content = "body")] // seq is omitted as unused and is not sent by some implementations diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 21ad84afc3ded9..2f70db0ce0e8fd 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -15,6 +15,7 @@ db.workspace = true editor.workspace = true futures.workspace = true gpui.workspace = true +project.workspace = true serde.workspace = true serde_derive.workspace = true ui.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index bb2466026007cd..365503b6e27daf 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,14 +1,10 @@ use std::sync::Arc; -use anyhow::{anyhow, Result}; -use dap::{ - client::{DebugAdapterClient, DebugAdapterClientId, TransportType}, - transport::Payload, -}; -use futures::channel::mpsc::UnboundedReceiver; +use anyhow::Result; +use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, - View, ViewContext, WeakView, + Subscription, View, ViewContext, WeakView, }; use ui::{ div, h_flex, @@ -30,6 +26,7 @@ pub struct DebugPanel { pub focus_handle: FocusHandle, pub size: Pixels, pub workspace: WeakView, + _subscriptions: Vec, } impl DebugPanel { @@ -38,6 +35,35 @@ impl DebugPanel { workspace: WeakView, cx: &mut WindowContext, ) -> Self { + let project = workspace + .update(cx, |workspace, cx| workspace.project().clone()) + .unwrap(); + + let _subscriptions = vec![cx.subscribe(&project, { + move |this, event, cx| { + if let project::Event::DebugClientStarted(client_id) = event { + dbg!(&event, &client_id); + } + if let project::Event::DebugClientEvent { client_id, event } = event { + match event { + dap::events::Event::Initialized(_) => todo!(), + dap::events::Event::Stopped(_) => todo!(), + dap::events::Event::Continued(_) => todo!(), + dap::events::Event::Exited(_) => todo!(), + dap::events::Event::Terminated(_) => todo!(), + dap::events::Event::Thread(_) => todo!(), + dap::events::Event::Output(_) => todo!(), + dap::events::Event::Breakpoint(_) => todo!(), + dap::events::Event::Module(_) => todo!(), + dap::events::Event::LoadedSource(_) => todo!(), + dap::events::Event::Process(_) => todo!(), + dap::events::Event::Capabilities(_) => todo!(), + dap::events::Event::Memory(_) => todo!(), + } + } + } + })]; + Self { position, zoomed: false, @@ -45,6 +71,7 @@ impl DebugPanel { focus_handle: cx.focus_handle(), size: px(300.), workspace, + _subscriptions, } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 997d1c473d3bf5..a816d52670c755 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -148,7 +148,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId}; use workspace::{ searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId, }; -use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast,ToggleBreakpoint}; +use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast, ToggleBreakpoint}; use crate::hover_links::find_url; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0af1d233c504d1..73dd1ac39fb242 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -19,8 +19,8 @@ use client::{ TypedEnvelope, UserStore, }; use clock::ReplicaId; -use dap::client::{DebugAdapterClient, DebugAdapterClientId, TransportType}; use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet, VecDeque}; +use dap::client::{DebugAdapterClient, DebugAdapterClientId, TransportType}; use debounced_delay::DebouncedDelay; use futures::{ channel::{ @@ -86,7 +86,7 @@ use smol::channel::{Receiver, Sender}; use smol::lock::Semaphore; use snippet::Snippet; use std::{ - borrow::{BorrowMut, Cow}, + borrow::Cow, cmp::{self, Ordering}, convert::TryInto, env, @@ -333,6 +333,11 @@ pub enum Event { Notification(String), LanguageServerPrompt(LanguageServerPromptRequest), LanguageNotFound(Model), + DebugClientStarted(DebugAdapterClientId), + DebugClientEvent { + client_id: DebugAdapterClientId, + event: dap::events::Event, + }, ActiveEntryChanged(Option), ActivateProjectPanel, WorktreeAdded, @@ -1042,22 +1047,41 @@ impl Project { }) } + pub fn debug_adapter_by_id( + &self, + id: DebugAdapterClientId, + ) -> Option<&Arc> { + self.debug_adapters.get(&id).and_then(|state| match state { + DebugAdapterClientState::Starting(_) => None, + DebugAdapterClientState::Running(client) => Some(client), + }) + } + pub fn start_debug_adapter_client( &mut self, id: DebugAdapterClientId, cx: &mut ModelContext, ) { let task = cx.spawn(|this, mut cx| async move { + let this2 = this.clone(); let mut client = DebugAdapterClient::new( TransportType::TCP, "bun", vec![ "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", - "--server=8130", + "--server=8131", ], - 8130, + 8131, "/Users/remcosmits/Documents/code/symfony_demo".into(), &mut cx, + move |event, cx| { + this2.update(cx, |_, cx| { + cx.emit(Event::DebugClientEvent { + client_id: id, + event, + }) + }); + }, ) .await .log_err()?; @@ -1071,7 +1095,8 @@ impl Project { "/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into(), 14, ) - .await; + .await + .log_err(); // configuration done client.configuration_done().await.log_err()?; @@ -1081,12 +1106,15 @@ impl Project { let client = Arc::new(client); - this.update(&mut cx, |this, _| { + this.update(&mut cx, |this, cx| { let handle = this .debug_adapters .get_mut(&id) .with_context(|| "Failed to find debug adapter with given id")?; *handle = DebugAdapterClientState::Running(client.clone()); + + cx.emit(Event::DebugClientStarted(id)); + anyhow::Ok(()) }) .log_err(); From 0f4f8abbaa1a4dc6d620bb21555fabc819007c62 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 23 Jun 2024 23:51:53 +0200 Subject: [PATCH 017/650] Add start debugger command --- crates/project/src/project.rs | 8 +++----- crates/workspace/src/workspace.rs | 8 ++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 73dd1ac39fb242..4d701877eb3501 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1057,11 +1057,9 @@ impl Project { }) } - pub fn start_debug_adapter_client( - &mut self, - id: DebugAdapterClientId, - cx: &mut ModelContext, - ) { + pub fn start_debug_adapter_client(&mut self, cx: &mut ModelContext) { + let id = DebugAdapterClientId(1); + let task = cx.spawn(|this, mut cx| async move { let this2 = this.clone(); let mut client = DebugAdapterClient::new( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1a6e99ccbd8e71..65cd1e896fa744 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -130,6 +130,7 @@ actions!( ReloadActiveItem, SaveAs, SaveWithoutFormat, + StartDebugger, ToggleBreakpoint, ToggleBottomDock, ToggleCenteredLayout, @@ -3912,6 +3913,13 @@ impl Workspace { }), ) .on_action(cx.listener(Workspace::toggle_centered_layout)) + .on_action( + cx.listener(|workspace: &mut Workspace, _: &StartDebugger, cx| { + workspace.project.update(cx, |project, cx| { + project.start_debug_adapter_client(cx); + }) + }), + ) } #[cfg(any(test, feature = "test-support"))] From 89b203d03af0d9c49c2dd3bb82accc91bdeb3040 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 23 Jun 2024 23:52:10 +0200 Subject: [PATCH 018/650] Move current thread to debug panel instead of client --- crates/dap/src/client.rs | 87 ++++++-------- crates/debugger_ui/src/debugger_panel.rs | 137 +++++++++++++---------- 2 files changed, 110 insertions(+), 114 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index b74302e522540c..c69e1ee25f6160 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,5 +1,5 @@ use crate::{ - events::{self, Event}, + events::{self}, requests::{ ConfigurationDone, Continue, ContinueArguments, Initialize, InitializeArguments, Launch, LaunchRequestArguments, Next, NextArguments, SetBreakpoints, SetBreakpointsArguments, @@ -21,7 +21,6 @@ use smol::{ }; use std::{ net::{Ipv4Addr, SocketAddrV4}, - ops::DerefMut, path::PathBuf, process::Stdio, sync::atomic::{AtomicU64, Ordering}, @@ -42,7 +41,6 @@ pub struct DebugAdapterClient { _process: Option, server_tx: UnboundedSender, request_count: AtomicU64, - thread_id: Option, capabilities: Option, } @@ -140,7 +138,6 @@ impl DebugAdapterClient { server_tx: server_tx.clone(), _process: process, request_count: AtomicU64::new(0), - thread_id: Some(ThreadId(1)), capabilities: None, }; @@ -217,10 +214,6 @@ impl DebugAdapterClient { self.request_count.fetch_add(1, Ordering::Relaxed) } - pub fn update_thread_id(&mut self, thread_id: ThreadId) { - self.thread_id = Some(thread_id); - } - pub async fn initialize(&mut self) -> Result { let args = InitializeArguments { client_id: Some("zed".to_owned()), @@ -253,58 +246,48 @@ impl DebugAdapterClient { .await } - pub async fn next_thread(&self) { - if let Some(thread_id) = self.thread_id { - let _ = self - .request::(NextArguments { - thread_id, - granularity: None, - }) - .await; - } + pub async fn next_thread(&self, thread_id: ThreadId) { + let _ = self + .request::(NextArguments { + thread_id, + granularity: None, + }) + .await; } - pub async fn continue_thread(&self) { - if let Some(thread_id) = self.thread_id { - let _ = self - .request::(ContinueArguments { thread_id }) - .await; - } + pub async fn continue_thread(&self, thread_id: ThreadId) { + let _ = self + .request::(ContinueArguments { thread_id }) + .await; } - pub async fn step_in(&self) { - if let Some(thread_id) = self.thread_id { - let _ = self - .request::(StepInArguments { - thread_id, - target_id: None, - granularity: None, - }) - .await; - } + pub async fn step_in(&self, thread_id: ThreadId) { + let _ = self + .request::(StepInArguments { + thread_id, + target_id: None, + granularity: None, + }) + .await; } - pub async fn step_out(&self) { - if let Some(thread_id) = self.thread_id { - let _ = self - .request::(StepOutArguments { - thread_id, - granularity: None, - }) - .await; - } + pub async fn step_out(&self, thread_id: ThreadId) { + let _ = self + .request::(StepOutArguments { + thread_id, + granularity: None, + }) + .await; } - pub async fn step_back(&self) { - if let Some(thread_id) = self.thread_id { - let _ = self - .request::(StepInArguments { - thread_id, - target_id: None, - granularity: None, - }) - .await; - } + pub async fn step_back(&self, thread_id: ThreadId) { + let _ = self + .request::(StepInArguments { + thread_id, + target_id: None, + granularity: None, + }) + .await; } pub async fn set_breakpoints( diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 365503b6e27daf..172739ca368721 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,11 +1,14 @@ -use std::sync::Arc; - use anyhow::Result; -use dap::client::{DebugAdapterClient, DebugAdapterClientId}; +use dap::{ + client::{DebugAdapterClient, DebugAdapterClientId}, + types::ThreadId, +}; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, - Subscription, View, ViewContext, WeakView, + Subscription, Task, View, ViewContext, WeakView, }; +use project::Project; +use std::sync::Arc; use ui::{ div, h_flex, prelude::{IntoElement, Pixels, WindowContext}, @@ -25,66 +28,67 @@ pub struct DebugPanel { pub active: bool, pub focus_handle: FocusHandle, pub size: Pixels, - pub workspace: WeakView, _subscriptions: Vec, + pub thread_id: Option, + pub workspace: WeakView, } impl DebugPanel { - pub fn new( - position: DockPosition, - workspace: WeakView, - cx: &mut WindowContext, - ) -> Self { - let project = workspace - .update(cx, |workspace, cx| workspace.project().clone()) - .unwrap(); - - let _subscriptions = vec![cx.subscribe(&project, { - move |this, event, cx| { - if let project::Event::DebugClientStarted(client_id) = event { - dbg!(&event, &client_id); - } - if let project::Event::DebugClientEvent { client_id, event } = event { - match event { - dap::events::Event::Initialized(_) => todo!(), - dap::events::Event::Stopped(_) => todo!(), - dap::events::Event::Continued(_) => todo!(), - dap::events::Event::Exited(_) => todo!(), - dap::events::Event::Terminated(_) => todo!(), - dap::events::Event::Thread(_) => todo!(), - dap::events::Event::Output(_) => todo!(), - dap::events::Event::Breakpoint(_) => todo!(), - dap::events::Event::Module(_) => todo!(), - dap::events::Event::LoadedSource(_) => todo!(), - dap::events::Event::Process(_) => todo!(), - dap::events::Event::Capabilities(_) => todo!(), - dap::events::Event::Memory(_) => todo!(), + pub fn new(workspace: WeakView, cx: &mut WindowContext) -> View { + cx.new_view(|cx: &mut ViewContext| { + let project = workspace + .update(cx, |workspace, _| workspace.project().clone()) + .unwrap(); + + let _subscriptions = vec![cx.subscribe(&project, { + move |this: &mut Self, model, event, cx| { + if let project::Event::DebugClientStarted(client_id) = event { + dbg!(&event, &client_id); + } + + if let project::Event::DebugClientEvent { client_id, event } = event { + match event { + dap::events::Event::Initialized(_) => return, + dap::events::Event::Stopped(_) => todo!(), + dap::events::Event::Continued(_) => todo!(), + dap::events::Event::Exited(_) => todo!(), + dap::events::Event::Terminated(_) => todo!(), + dap::events::Event::Thread(event) => { + this.thread_id = Some(event.thread_id); + } + dap::events::Event::Output(_) => todo!(), + dap::events::Event::Breakpoint(_) => todo!(), + dap::events::Event::Module(_) => todo!(), + dap::events::Event::LoadedSource(_) => todo!(), + dap::events::Event::Process(_) => todo!(), + dap::events::Event::Capabilities(_) => todo!(), + dap::events::Event::Memory(_) => todo!(), + } } } + })]; + + Self { + position: DockPosition::Bottom, + zoomed: false, + active: false, + focus_handle: cx.focus_handle(), + size: px(300.), + _subscriptions, + thread_id: Some(ThreadId(1)), + workspace: workspace.clone(), } - })]; - - Self { - position, - zoomed: false, - active: false, - focus_handle: cx.focus_handle(), - size: px(300.), - workspace, - _subscriptions, - } + }) } - pub async fn load( + pub fn load( workspace: WeakView, mut cx: AsyncWindowContext, - ) -> Result> { - workspace.update(&mut cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client(DebugAdapterClientId(1), cx); - }); - })?; - cx.new_view(|cx| DebugPanel::new(DockPosition::Bottom, workspace, cx)) + ) -> Task>> { + // workspace.project().update(cx, |project, cx| { + // project.start_debug_adapter_client(DebugAdapterClientId(1), cx); + // }); + cx.spawn(|mut cx| async move { cx.update(|cx| DebugPanel::new(workspace, cx)) }) } fn debug_adapter(&self, cx: &mut ViewContext) -> Arc { @@ -175,9 +179,13 @@ impl Render for DebugPanel { IconButton::new("debug-play", IconName::Play) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); - cx.background_executor() - .spawn(async move { client.continue_thread().await }) - .detach(); + if let Some(thread_id) = view.thread_id { + cx.background_executor() + .spawn( + async move { client.continue_thread(thread_id).await }, + ) + .detach(); + } })) .tooltip(move |cx| Tooltip::text("Start debug", cx)), ) @@ -189,9 +197,12 @@ impl Render for DebugPanel { IconButton::new("debug-go-in", IconName::Play) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); - cx.background_executor() - .spawn(async move { client.step_in().await }) - .detach(); + + if let Some(thread_id) = view.thread_id { + cx.background_executor() + .spawn(async move { client.step_in(thread_id).await }) + .detach(); + } })) .tooltip(move |cx| Tooltip::text("Go in", cx)), ) @@ -199,9 +210,11 @@ impl Render for DebugPanel { IconButton::new("debug-go-out", IconName::Play) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); - cx.background_executor() - .spawn(async move { client.step_out().await }) - .detach(); + if let Some(thread_id) = view.thread_id { + cx.background_executor() + .spawn(async move { client.step_out(thread_id).await }) + .detach(); + } })) .tooltip(move |cx| Tooltip::text("Go out", cx)), ) From 5fe110c1ddc4c89341c9d7b9796e3bd56e32a172 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 24 Jun 2024 00:07:43 +0200 Subject: [PATCH 019/650] Rename play to continue debug --- crates/debugger_ui/src/debugger_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 172739ca368721..3c2a4b0cd30115 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -176,7 +176,7 @@ impl Render for DebugPanel { .p_2() .gap_2() .child( - IconButton::new("debug-play", IconName::Play) + IconButton::new("debug-continue", IconName::Play) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); if let Some(thread_id) = view.thread_id { @@ -187,7 +187,7 @@ impl Render for DebugPanel { .detach(); } })) - .tooltip(move |cx| Tooltip::text("Start debug", cx)), + .tooltip(move |cx| Tooltip::text("Continue debug", cx)), ) .child( IconButton::new("debug-step-over", IconName::Play) From e1de8dc50e79bb131322f3bc9a3e311504619988 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 24 Jun 2024 18:47:17 +0200 Subject: [PATCH 020/650] Remove unused code --- crates/debugger_ui/src/debugger_panel.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3c2a4b0cd30115..0468f484426b08 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -83,11 +83,8 @@ impl DebugPanel { pub fn load( workspace: WeakView, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Task>> { - // workspace.project().update(cx, |project, cx| { - // project.start_debug_adapter_client(DebugAdapterClientId(1), cx); - // }); cx.spawn(|mut cx| async move { cx.update(|cx| DebugPanel::new(workspace, cx)) }) } From d303ebd46e5f351b2c440b0330a2122454867c47 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 24 Jun 2024 19:58:40 +0200 Subject: [PATCH 021/650] Wip add select launch config modal Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- Cargo.lock | 3 + crates/dap/Cargo.toml | 1 + crates/dap/src/configh_templates.rs | 17 +++ crates/dap/src/lib.rs | 1 + crates/debugger_ui/Cargo.toml | 4 + crates/debugger_ui/src/lib.rs | 46 ++++++-- crates/debugger_ui/src/modal.rs | 138 +++++++++++++++++++++++ crates/paths/src/paths.rs | 12 ++ crates/project/src/debugger_inventory.rs | 23 ++++ crates/project/src/project.rs | 8 +- crates/workspace/src/workspace.rs | 7 -- crates/zed/src/zed.rs | 1 + 12 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 crates/dap/src/configh_templates.rs create mode 100644 crates/debugger_ui/src/modal.rs create mode 100644 crates/project/src/debugger_inventory.rs diff --git a/Cargo.lock b/Cargo.lock index cf8a20db5cc320..5012c1d2f03d7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3200,6 +3200,7 @@ dependencies = [ "parking_lot", "postage", "release_channel", + "schemars", "serde", "serde_json", "smol", @@ -3264,7 +3265,9 @@ dependencies = [ "db", "editor", "futures 0.3.28", + "fuzzy", "gpui", + "picker", "project", "serde", "serde_derive", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 300fea3d28432b..f88c2b0e0bad7c 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -16,6 +16,7 @@ log.workspace = true parking_lot.workspace = true postage.workspace = true release_channel.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true diff --git a/crates/dap/src/configh_templates.rs b/crates/dap/src/configh_templates.rs new file mode 100644 index 00000000000000..1af1c8d8949f16 --- /dev/null +++ b/crates/dap/src/configh_templates.rs @@ -0,0 +1,17 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// A template definition of a Zed task to run. +/// May use the [`VariableName`] to get the corresponding substitutions into its fields. +/// +/// Template itself is not ready to spawn a task, it needs to be resolved with a [`TaskContext`] first, that +/// contains all relevant Zed state in task variables. +/// A single template may produce different tasks (or none) for different contexts. +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DebuggerConfigTemplate { + pub _type: String, + pub request: String, + #[serde(default)] + pub args: Vec, +} diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index f1030c5fe6f52f..bfabd43be927f5 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,4 +1,5 @@ pub mod client; +pub mod configh_templates; pub mod events; pub mod requests; pub mod responses; diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 2f70db0ce0e8fd..2ad433f813a1eb 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -14,7 +14,9 @@ dap.workspace = true db.workspace = true editor.workspace = true futures.workspace = true +fuzzy.workspace = true gpui.workspace = true +picker.workspace = true project.workspace = true serde.workspace = true serde_derive.workspace = true @@ -24,3 +26,5 @@ workspace.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 0b4539ced8570c..cb4a816845b2e1 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,23 +1,45 @@ use debugger_panel::{DebugPanel, TogglePanel}; -use gpui::{AppContext, ViewContext}; -use serde::{Deserialize, Serialize}; -use ui::Pixels; -use workspace::Workspace; +use gpui::{AppContext, Task, ViewContext}; +use modal::DebuggerSelectModal; +use workspace::{StartDebugger, Workspace}; pub mod debugger_panel; - -#[derive(Serialize, Deserialize)] -struct SerializedDebugPanel { - width: Option, -} +pub mod modal; pub fn init(cx: &mut AppContext) { cx.observe_new_views( |workspace: &mut Workspace, _: &mut ViewContext| { - workspace.register_action(|workspace, _action: &TogglePanel, cx| { - workspace.focus_panel::(cx); - }); + workspace + .register_action(|workspace, _action: &TogglePanel, cx| { + workspace.focus_panel::(cx); + }) + .register_action( + |workspace: &mut Workspace, + action: &StartDebugger, + cx: &mut ViewContext<'_, Workspace>| { + select_debugger(workspace, action, cx).detach(); + }, + ); }, ) .detach(); } + +fn select_debugger( + workspace: &mut Workspace, + _: &StartDebugger, + cx: &mut ViewContext, +) -> Task<()> { + let project = workspace.project().clone(); + let workspace_handle = workspace.weak_handle(); + + cx.spawn(|workspace, mut cx| async move { + workspace + .update(&mut cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| { + DebuggerSelectModal::new(project, workspace_handle, cx) + }) + }) + .ok(); + }) +} diff --git a/crates/debugger_ui/src/modal.rs b/crates/debugger_ui/src/modal.rs new file mode 100644 index 00000000000000..02e9b008fc090f --- /dev/null +++ b/crates/debugger_ui/src/modal.rs @@ -0,0 +1,138 @@ +use fuzzy::StringMatch; +use gpui::{ + rems, DismissEvent, EventEmitter, FocusableView, Model, Render, Subscription, + Task as AsyncTask, View, ViewContext, WeakView, +}; +use picker::{Picker, PickerDelegate}; +use project::Project; +use std::sync::Arc; +use ui::{prelude::*, ListItem, ListItemSpacing}; +use workspace::{ModalView, Workspace}; + +use crate::debugger_panel::DebugPanel; + +pub struct DebuggerSelectModal { + picker: View>, + _subscription: Subscription, +} + +impl DebuggerSelectModal { + pub fn new( + _project: Model, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + let picker = + cx.new_view(|cx| Picker::uniform_list(DebuggerModelDelegate::new(workspace), cx)); + + let _subscription = cx.subscribe(&picker, |_, _, _, cx| { + cx.emit(DismissEvent); + }); + + Self { + picker, + _subscription, + } + } +} + +impl ModalView for DebuggerSelectModal {} + +impl EventEmitter for DebuggerSelectModal {} + +impl FocusableView for DebuggerSelectModal { + fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + self.picker.read(cx).focus_handle(cx) + } +} + +impl Render for DebuggerSelectModal { + fn render(&mut self, _: &mut ViewContext) -> impl gpui::prelude::IntoElement { + v_flex() + .id("DebuggerSelectModel") + .key_context("DebuggerSelectModel") + .w(rems(34.)) + .child(self.picker.clone()) + } +} + +struct DebuggerModelDelegate { + matches: Vec, + selected_index: usize, + workspace: WeakView, + placeholder_text: Arc, +} + +impl DebuggerModelDelegate { + fn new(workspace: WeakView) -> Self { + Self { + workspace, + matches: vec![StringMatch { + candidate_id: 0, + score: 1.0, + positions: vec![0], + string: String::from("Mock debugger config"), + }], + selected_index: 0, + placeholder_text: Arc::from("Select & Start a debugger config"), + } + } +} + +impl PickerDelegate for DebuggerModelDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> std::sync::Arc { + self.placeholder_text.clone() + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + AsyncTask::Ready(Some(())) + } + + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { + self.workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.start_debug_adapter_client(cx); + }); + + workspace.focus_panel::(cx); + }); + + cx.emit(DismissEvent); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Option { + Some( + ListItem::new(SharedString::from("ajklsdf")) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .child(self.matches[ix].string.clone()), + ) + } +} diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 8f24873a8a73bb..08a4bfe91072fb 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -247,3 +247,15 @@ pub fn local_vscode_tasks_file_relative_path() -> &'static Path { static LOCAL_VSCODE_TASKS_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new(); LOCAL_VSCODE_TASKS_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".vscode/tasks.json")) } + +/// Returns the relative path to a `launch.json` file within a project. +pub fn local_launch_file_relative_path() -> &'static Path { + static LOCAL_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new(); + LOCAL_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".zed/debug.json")) +} + +/// Returns the relative path to a `.vscode/launch.json` file within a project. +pub fn local_vscode_launch_file_relative_path() -> &'static Path { + static LOCAL_VSCODE_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new(); + LOCAL_VSCODE_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".vscode/launch.json")) +} diff --git a/crates/project/src/debugger_inventory.rs b/crates/project/src/debugger_inventory.rs new file mode 100644 index 00000000000000..a27e2491eefd4c --- /dev/null +++ b/crates/project/src/debugger_inventory.rs @@ -0,0 +1,23 @@ +use std::collections::VecDeque; + +use dap::configh_templates::DebuggerConfigTemplate; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use task::static_source::TrackedFile; + +pub struct DebuggerInventory { + sources: Vec, +} + +struct SourceInInventory { + source: StaticSource, + // kind: TaskSourceKind, TODO: Change this and impl Debugger Source Kind (Might not be needed) +} + +pub struct StaticSource { + tasks: TrackedFile, +} + +/// A group of Tasks defined in a JSON file. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct DebuggerConfigTemplates(pub Vec); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4d701877eb3501..baa5da6fe6d596 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,5 +1,6 @@ pub mod connection_manager; pub mod debounced_delay; +mod debugger_inventory; pub mod lsp_command; pub mod lsp_ext_command; mod prettier_support; @@ -68,7 +69,8 @@ use lsp_command::*; use node_runtime::NodeRuntime; use parking_lot::{Mutex, RwLock}; use paths::{ - local_settings_file_relative_path, local_tasks_file_relative_path, + local_launch_file_relative_path, local_settings_file_relative_path, + local_tasks_file_relative_path, local_vscode_launch_file_relative_path, local_vscode_tasks_file_relative_path, }; use postage::watch; @@ -8412,6 +8414,10 @@ impl Project { ); } }) + } else if abs_path.ends_with(local_launch_file_relative_path()) { + // TODO: handle local launch file (.zed/launch.json) + } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) { + // TODO: handle vscode launch file (.vscode/launch.json) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 65cd1e896fa744..968cdc4eb23375 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3913,13 +3913,6 @@ impl Workspace { }), ) .on_action(cx.listener(Workspace::toggle_centered_layout)) - .on_action( - cx.listener(|workspace: &mut Workspace, _: &StartDebugger, cx| { - workspace.project.update(cx, |project, cx| { - project.start_debug_adapter_client(cx); - }) - }), - ) } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f8f6616af527a5..66520890aa1492 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3184,6 +3184,7 @@ mod tests { terminal_view::init(cx); assistant::init(app_state.client.clone(), cx); tasks_ui::init(cx); + debugger_ui::init(cx); initialize_workspace(app_state.clone(), cx); app_state }) From 11a4fc8b0270877793e42b75a23048ddac64aeec Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 25 Jun 2024 02:13:52 -0400 Subject: [PATCH 022/650] Start work on debugger inventory The debugger config inventory is used to track config files for the debugger. Currently, only .zed/debug.json is set up to work, but it's still untested. --- Cargo.lock | 1 + crates/dap/Cargo.toml | 1 + crates/dap/src/config_templates.rs | 46 ++++++++++++ crates/dap/src/configh_templates.rs | 17 ----- crates/dap/src/lib.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 5 +- crates/project/src/debugger_inventory.rs | 90 +++++++++++++++++++++++- crates/project/src/project.rs | 46 +++++++++--- 8 files changed, 177 insertions(+), 31 deletions(-) create mode 100644 crates/dap/src/config_templates.rs delete mode 100644 crates/dap/src/configh_templates.rs diff --git a/Cargo.lock b/Cargo.lock index 5012c1d2f03d7e..7c057cf84e9d10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3203,6 +3203,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "serde_json_lenient", "smol", "util", ] diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index f88c2b0e0bad7c..300dcc27989101 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -19,6 +19,7 @@ release_channel.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true +serde_json_lenient.workspace = true smol.workspace = true util.workspace = true async-std = "1.12.0" diff --git a/crates/dap/src/config_templates.rs b/crates/dap/src/config_templates.rs new file mode 100644 index 00000000000000..5d6d94a364e0b6 --- /dev/null +++ b/crates/dap/src/config_templates.rs @@ -0,0 +1,46 @@ +use schemars::{gen::SchemaSettings, JsonSchema}; +use serde::{Deserialize, Serialize}; + +/// A template definition of a Zed task to run. +/// May use the [`VariableName`] to get the corresponding substitutions into its fields. +/// +/// Template itself is not ready to spawn a task, it needs to be resolved with a [`TaskContext`] first, that +/// contains all relevant Zed state in task variables. +/// A single template may produce different tasks (or none) for different contexts. +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DebuggerConfigTemplate { + pub _type: String, + pub request: String, + #[serde(default)] + pub args: Vec, +} + +impl DebuggerConfigTemplate { + /// Generates JSON schema of Tasks JSON template format. + pub fn generate_json_schema() -> serde_json_lenient::Value { + let schema = SchemaSettings::draft07() + .with(|settings| settings.option_add_null_type = false) + .into_generator() + .into_root_schema_for::(); + + serde_json_lenient::to_value(schema).unwrap() + } +} + +/// [`VsCodeTaskFile`] is a superset of Code's task definition format. +#[derive(Debug, Deserialize, PartialEq)] +pub struct ZedDebugConfigFile { + debugger_configs: Vec, +} + +// impl TryFrom for DebuggerConfigTemplate { +// type Error = anyhow::Error; + +// fn try_from(value: ZedDebugConfigFile) -> Result { + +// let templates = value +// . +// Ok(Self(templates)) +// } +// } diff --git a/crates/dap/src/configh_templates.rs b/crates/dap/src/configh_templates.rs deleted file mode 100644 index 1af1c8d8949f16..00000000000000 --- a/crates/dap/src/configh_templates.rs +++ /dev/null @@ -1,17 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// A template definition of a Zed task to run. -/// May use the [`VariableName`] to get the corresponding substitutions into its fields. -/// -/// Template itself is not ready to spawn a task, it needs to be resolved with a [`TaskContext`] first, that -/// contains all relevant Zed state in task variables. -/// A single template may produce different tasks (or none) for different contexts. -#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct DebuggerConfigTemplate { - pub _type: String, - pub request: String, - #[serde(default)] - pub args: Vec, -} diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index bfabd43be927f5..423407bda4034c 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,5 +1,5 @@ pub mod client; -pub mod configh_templates; +pub mod config_templates; pub mod events; pub mod requests; pub mod responses; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 0468f484426b08..3c2a4b0cd30115 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -83,8 +83,11 @@ impl DebugPanel { pub fn load( workspace: WeakView, - cx: AsyncWindowContext, + mut cx: AsyncWindowContext, ) -> Task>> { + // workspace.project().update(cx, |project, cx| { + // project.start_debug_adapter_client(DebugAdapterClientId(1), cx); + // }); cx.spawn(|mut cx| async move { cx.update(|cx| DebugPanel::new(workspace, cx)) }) } diff --git a/crates/project/src/debugger_inventory.rs b/crates/project/src/debugger_inventory.rs index a27e2491eefd4c..72833cb7f65da9 100644 --- a/crates/project/src/debugger_inventory.rs +++ b/crates/project/src/debugger_inventory.rs @@ -1,17 +1,101 @@ -use std::collections::VecDeque; +use std::{ + borrow::Cow, + collections::VecDeque, + path::{Path, PathBuf}, + sync::Arc, +}; -use dap::configh_templates::DebuggerConfigTemplate; +use dap::config_templates::DebuggerConfigTemplate; +use futures::{ + channel::mpsc::{unbounded, UnboundedSender}, + StreamExt, +}; +use gpui::{AppContext, Context, Model, ModelContext, Task}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use task::static_source::TrackedFile; pub struct DebuggerInventory { sources: Vec, + update_sender: UnboundedSender<()>, + _update_pooler: Task>, +} + +impl DebuggerInventory { + pub fn new(cx: &mut AppContext) -> Model { + cx.new_model(|cx| { + let (update_sender, mut rx) = unbounded(); + let _update_pooler = cx.spawn(|this, mut cx| async move { + while let Some(()) = rx.next().await { + this.update(&mut cx, |_, cx| { + cx.notify(); + })?; + } + Ok(()) + }); + Self { + sources: Vec::new(), + update_sender, + _update_pooler, + } + }) + } + + pub fn remove_source(&mut self, abs_path: &PathBuf) { + todo!(); + } + + pub fn add_source( + &mut self, + kind: DebuggerConfigSourceKind, + create_source: impl FnOnce(UnboundedSender<()>, &mut AppContext) -> StaticSource, + cx: &mut ModelContext, + ) { + let abs_path = kind.abs_path(); + if abs_path.is_some() { + if let Some(a) = self.sources.iter().find(|s| s.kind.abs_path() == abs_path) { + log::debug!("Source for path {abs_path:?} already exists, not adding. Old kind: {OLD_KIND:?}, new kind: {kind:?}", OLD_KIND = a.kind); + return; + } + } + + let source = create_source(self.update_sender.clone(), cx); + let source = SourceInInventory { source, kind }; + self.sources.push(source); + cx.notify(); + } +} + +/// Kind of a source the tasks are fetched from, used to display more source information in the UI. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum DebuggerConfigSourceKind { + /// Tasks from the worktree's .zed/task.json + Worktree { + id: usize, + abs_path: PathBuf, + id_base: Cow<'static, str>, + }, + /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path + AbsPath { + id_base: Cow<'static, str>, + abs_path: PathBuf, + }, + /// Languages-specific tasks coming from extensions. + Language { name: Arc }, +} + +impl DebuggerConfigSourceKind { + fn abs_path(&self) -> Option<&Path> { + match self { + Self::AbsPath { abs_path, .. } | Self::Worktree { abs_path, .. } => Some(abs_path), + Self::Language { .. } => None, + } + } } struct SourceInInventory { source: StaticSource, - // kind: TaskSourceKind, TODO: Change this and impl Debugger Source Kind (Might not be needed) + kind: DebuggerConfigSourceKind, } pub struct StaticSource { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index baa5da6fe6d596..8313b75eeeb56b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -21,8 +21,12 @@ use client::{ }; use clock::ReplicaId; use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet, VecDeque}; -use dap::client::{DebugAdapterClient, DebugAdapterClientId, TransportType}; +use dap::{ + client::{DebugAdapterClient, DebugAdapterClientId, TransportType}, + config_templates::DebuggerConfigTemplate, +}; use debounced_delay::DebouncedDelay; +use debugger_inventory::{DebuggerConfigSourceKind, DebuggerInventory}; use futures::{ channel::{ mpsc::{self, UnboundedReceiver}, @@ -231,6 +235,7 @@ pub struct Project { prettiers_per_worktree: HashMap>>, prettier_instances: HashMap, tasks: Model, + debugger_configs: Model, hosted_project_id: Option, dev_server_project_id: Option, search_history: SearchHistory, @@ -738,6 +743,7 @@ impl Project { cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) .detach(); let tasks = Inventory::new(cx); + let debugger_configs = DebuggerInventory::new(cx); Self { worktrees: Vec::new(), @@ -795,6 +801,7 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), tasks, + debugger_configs, hosted_project_id: None, dev_server_project_id: None, search_history: Self::new_search_history(), @@ -862,6 +869,7 @@ impl Project { let this = cx.new_model(|cx| { let replica_id = response.payload.replica_id as ReplicaId; let tasks = Inventory::new(cx); + let debugger_configs = DebuggerInventory::new(cx); // BIG CAUTION NOTE: The order in which we initialize fields here matters and it should match what's done in Self::local. // Otherwise, you might run into issues where worktree id on remote is different than what's on local host. // That's because Worktree's identifier is entity id, which should probably be changed. @@ -956,6 +964,7 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), tasks, + debugger_configs, hosted_project_id: None, dev_server_project_id: response .payload @@ -1495,6 +1504,10 @@ impl Project { &self.tasks } + pub fn debugger_configs(&self) -> &Model { + &self.debugger_configs + } + pub fn search_history(&self) -> &SearchHistory { &self.search_history } @@ -8403,19 +8416,34 @@ impl Project { abs_path, id_base: "local_vscode_tasks_for_worktree".into(), }, - |tx, cx| { - StaticSource::new(TrackedFile::new_convertible::< - task::VsCodeTaskFile, - >( - tasks_file_rx, tx, cx - )) - }, + |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)), cx, ); } }) } else if abs_path.ends_with(local_launch_file_relative_path()) { - // TODO: handle local launch file (.zed/launch.json) + // TODO: handle local launch file (.zed/debug.json) + self.debugger_configs().update(cx, |debugger_configs, cx| { + if removed { + debugger_configs.remove_source(&abs_path); + } else { + let fs = self.fs.clone(); + let debugger_configs_file_rx = + watch_config_file(&cx.background_executor(), fs, abs_path.clone()); + + debugger_configs.add_source( + DebuggerConfigSourceKind::Worktree { + id: remote_worktree_id.to_usize(), + abs_path, + id_base: "local_debug_File_for_worktree".into(), + }, + |tx, cx| { + todo!(); + }, + cx, + ); + } + }); } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) { // TODO: handle vscode launch file (.vscode/launch.json) } From 5442e116ceaf5d69f65aeb390f053992ebcf5a08 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 25 Jun 2024 02:45:20 -0400 Subject: [PATCH 023/650] Get debugger inventory to add configs from .zed/debug.json --- crates/dap/src/config_templates.rs | 1 + crates/project/src/debugger_inventory.rs | 10 +++++++++- crates/project/src/project.rs | 6 +++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/dap/src/config_templates.rs b/crates/dap/src/config_templates.rs index 5d6d94a364e0b6..9297ae5c761b12 100644 --- a/crates/dap/src/config_templates.rs +++ b/crates/dap/src/config_templates.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct DebuggerConfigTemplate { + #[serde(rename = "type")] pub _type: String, pub request: String, #[serde(default)] diff --git a/crates/project/src/debugger_inventory.rs b/crates/project/src/debugger_inventory.rs index 72833cb7f65da9..0a79b53efc246a 100644 --- a/crates/project/src/debugger_inventory.rs +++ b/crates/project/src/debugger_inventory.rs @@ -99,7 +99,15 @@ struct SourceInInventory { } pub struct StaticSource { - tasks: TrackedFile, + configs: TrackedFile, +} + +impl StaticSource { + pub fn new(debugger_configs: TrackedFile) -> Self { + Self { + configs: debugger_configs, + } + } } /// A group of Tasks defined in a JSON file. diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8313b75eeeb56b..765537842d6435 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8438,7 +8438,11 @@ impl Project { id_base: "local_debug_File_for_worktree".into(), }, |tx, cx| { - todo!(); + debugger_inventory::StaticSource::new(TrackedFile::new( + debugger_configs_file_rx, + tx, + cx, + )) }, cx, ); From 61949fb3485f4803a3f2d81af3835340c5c2d59f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 25 Jun 2024 21:06:33 +0200 Subject: [PATCH 024/650] Add enum for SteppingGranularity --- crates/dap/src/requests.rs | 9 +++++---- crates/dap/src/types.rs | 8 ++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/dap/src/requests.rs b/crates/dap/src/requests.rs index be134ce2f4758a..9688a2e07fd604 100644 --- a/crates/dap/src/requests.rs +++ b/crates/dap/src/requests.rs @@ -1,6 +1,7 @@ use crate::types::{ Breakpoint, DebuggerCapabilities, Scope, Source, SourceBreakpoint, StackFrame, - StackFrameFormat, Thread, ThreadId, ValueFormat, Variable, VariablePresentationHint, + StackFrameFormat, SteppingGranularity, Thread, ThreadId, ValueFormat, Variable, + VariablePresentationHint, }; use ::serde::{Deserialize, Serialize}; use serde::de::DeserializeOwned; @@ -267,7 +268,7 @@ pub struct StepInArguments { #[serde(skip_serializing_if = "Option::is_none")] pub target_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub granularity: Option, + pub granularity: Option, } #[derive(Debug)] @@ -284,7 +285,7 @@ impl Request for StepIn { pub struct StepOutArguments { pub thread_id: ThreadId, #[serde(skip_serializing_if = "Option::is_none")] - pub granularity: Option, + pub granularity: Option, } #[derive(Debug)] @@ -301,7 +302,7 @@ impl Request for StepOut { pub struct NextArguments { pub thread_id: ThreadId, #[serde(skip_serializing_if = "Option::is_none")] - pub granularity: Option, + pub granularity: Option, } #[derive(Debug)] diff --git a/crates/dap/src/types.rs b/crates/dap/src/types.rs index 3ad683c2c8c3ad..39ddb6476dad0a 100644 --- a/crates/dap/src/types.rs +++ b/crates/dap/src/types.rs @@ -323,3 +323,11 @@ pub struct Module { #[serde(skip_serializing_if = "Option::is_none")] pub address_range: Option, } + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum SteppingGranularity { + Statement, + Line, + Instruction, +} From 331625e8761dafec2755f394ac3f6e961df26670 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 25 Jun 2024 21:07:01 +0200 Subject: [PATCH 025/650] Change hardcoded set breakpoint to `TODO` --- crates/project/src/project.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 765537842d6435..83dc090f0eb172 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1098,14 +1098,7 @@ impl Project { // initialize client.initialize().await.log_err()?; - // set break point - client - .set_breakpoints( - "/Users/remcosmits/Documents/code/symfony_demo/src/Kernel.php".into(), - 14, - ) - .await - .log_err(); + // TODO: fetch all old breakpoints and send them to the debug adapter // configuration done client.configuration_done().await.log_err()?; From d9e09c4a66390b036ee3939cc46c8e2505697d9a Mon Sep 17 00:00:00 2001 From: d1y Date: Tue, 25 Jun 2024 22:43:46 +0800 Subject: [PATCH 026/650] add debug icon --- assets/icons/debug-continue.svg | 1 + assets/icons/debug-restart.svg | 1 + assets/icons/debug-step-into.svg | 1 + assets/icons/debug-step-out.svg | 1 + assets/icons/debug-step-over.svg | 1 + assets/icons/debug-stop.svg | 1 + crates/debugger_ui/src/debugger_panel.rs | 12 ++++++------ crates/ui/src/components/icon.rs | 12 ++++++++++++ 8 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 assets/icons/debug-continue.svg create mode 100644 assets/icons/debug-restart.svg create mode 100644 assets/icons/debug-step-into.svg create mode 100644 assets/icons/debug-step-out.svg create mode 100644 assets/icons/debug-step-over.svg create mode 100644 assets/icons/debug-stop.svg diff --git a/assets/icons/debug-continue.svg b/assets/icons/debug-continue.svg new file mode 100644 index 00000000000000..a6d8eb74ab1fd6 --- /dev/null +++ b/assets/icons/debug-continue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/debug-restart.svg b/assets/icons/debug-restart.svg new file mode 100644 index 00000000000000..9eedf6702d95e6 --- /dev/null +++ b/assets/icons/debug-restart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/debug-step-into.svg b/assets/icons/debug-step-into.svg new file mode 100644 index 00000000000000..1a684d7a50266c --- /dev/null +++ b/assets/icons/debug-step-into.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/debug-step-out.svg b/assets/icons/debug-step-out.svg new file mode 100644 index 00000000000000..253190bccd7d41 --- /dev/null +++ b/assets/icons/debug-step-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/debug-step-over.svg b/assets/icons/debug-step-over.svg new file mode 100644 index 00000000000000..e7256555ab5023 --- /dev/null +++ b/assets/icons/debug-step-over.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/debug-stop.svg b/assets/icons/debug-stop.svg new file mode 100644 index 00000000000000..9a63e2191f5b1c --- /dev/null +++ b/assets/icons/debug-stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3c2a4b0cd30115..b3f4ca2a3512be 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -176,7 +176,7 @@ impl Render for DebugPanel { .p_2() .gap_2() .child( - IconButton::new("debug-continue", IconName::Play) + IconButton::new("debug-continue", IconName::DebugContinue) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); if let Some(thread_id) = view.thread_id { @@ -190,11 +190,11 @@ impl Render for DebugPanel { .tooltip(move |cx| Tooltip::text("Continue debug", cx)), ) .child( - IconButton::new("debug-step-over", IconName::Play) + IconButton::new("debug-step-over", IconName::DebugStepOver) .tooltip(move |cx| Tooltip::text("Step over", cx)), ) .child( - IconButton::new("debug-go-in", IconName::Play) + IconButton::new("debug-go-in", IconName::DebugStepInto) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); @@ -207,7 +207,7 @@ impl Render for DebugPanel { .tooltip(move |cx| Tooltip::text("Go in", cx)), ) .child( - IconButton::new("debug-go-out", IconName::Play) + IconButton::new("debug-go-out", IconName::DebugStepOut) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); if let Some(thread_id) = view.thread_id { @@ -219,11 +219,11 @@ impl Render for DebugPanel { .tooltip(move |cx| Tooltip::text("Go out", cx)), ) .child( - IconButton::new("debug-restart", IconName::Play) + IconButton::new("debug-restart", IconName::DebugRestart) .tooltip(move |cx| Tooltip::text("Restart", cx)), ) .child( - IconButton::new("debug-stop", IconName::Play) + IconButton::new("debug-stop", IconName::DebugStop) .tooltip(move |cx| Tooltip::text("Stop", cx)), ), ) diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index b752da75db6bef..9751a5d5fa5a9b 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -116,6 +116,12 @@ pub enum IconName { Copy, CountdownTimer, Dash, + DebugContinue, + DebugStepOver, + DebugStepInto, + DebugStepOut, + DebugRestart, + DebugStop, Delete, Disconnected, Download, @@ -250,6 +256,12 @@ impl IconName { IconName::Copy => "icons/copy.svg", IconName::CountdownTimer => "icons/countdown_timer.svg", IconName::Dash => "icons/dash.svg", + IconName::DebugContinue => "icons/debug-continue.svg", + IconName::DebugStepOver => "icons/debug-step-over.svg", + IconName::DebugStepInto => "icons/debug-step-into.svg", + IconName::DebugStepOut => "icons/debug-step-out.svg", + IconName::DebugRestart => "icons/debug-restart.svg", + IconName::DebugStop => "icons/debug-stop.svg", IconName::Delete => "icons/delete.svg", IconName::Disconnected => "icons/disconnected.svg", IconName::Download => "icons/download.svg", From d5dae425fcd732fd7f20e9e2d116380ddfdb39b1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 25 Jun 2024 21:35:47 +0200 Subject: [PATCH 027/650] Allow stepping to next line --- crates/dap/src/client.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index c69e1ee25f6160..7318922a8b2172 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,12 +1,12 @@ use crate::{ - events::{self}, + events, requests::{ ConfigurationDone, Continue, ContinueArguments, Initialize, InitializeArguments, Launch, LaunchRequestArguments, Next, NextArguments, SetBreakpoints, SetBreakpointsArguments, SetBreakpointsResponse, StepIn, StepInArguments, StepOut, StepOutArguments, }, transport::{self, Payload, Request, Transport}, - types::{DebuggerCapabilities, Source, SourceBreakpoint, ThreadId}, + types::{DebuggerCapabilities, Source, SourceBreakpoint, SteppingGranularity, ThreadId}, }; use anyhow::{anyhow, Context, Result}; use futures::{ @@ -246,18 +246,18 @@ impl DebugAdapterClient { .await } - pub async fn next_thread(&self, thread_id: ThreadId) { + pub async fn resume(&self, thread_id: ThreadId) { let _ = self - .request::(NextArguments { - thread_id, - granularity: None, - }) + .request::(ContinueArguments { thread_id }) .await; } - pub async fn continue_thread(&self, thread_id: ThreadId) { + pub async fn step_over(&self, thread_id: ThreadId) { let _ = self - .request::(ContinueArguments { thread_id }) + .request::(NextArguments { + thread_id, + granularity: Some(SteppingGranularity::Line), + }) .await; } From 79d23aa4fe0f21fa7c03f3ea3fa514cc8edb7073 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 25 Jun 2024 21:38:01 +0200 Subject: [PATCH 028/650] Start adding support for multiple threads + beginning of stack frame UI --- crates/debugger_ui/src/debugger_panel.rs | 141 +++++++++++++++++++---- 1 file changed, 121 insertions(+), 20 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3c2a4b0cd30115..a7d373f0a75706 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,20 +1,15 @@ use anyhow::Result; use dap::{ - client::{DebugAdapterClient, DebugAdapterClientId}, - types::ThreadId, + client::DebugAdapterClient, + requests::{StackTrace, StackTraceArguments}, + types::{StackFrame, ThreadId}, }; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, Subscription, Task, View, ViewContext, WeakView, }; -use project::Project; -use std::sync::Arc; -use ui::{ - div, h_flex, - prelude::{IntoElement, Pixels, WindowContext}, - px, ButtonCommon, Clickable, Element, IconButton, IconName, ParentElement, Render, Styled, - Tooltip, VisualContext, -}; +use std::{collections::HashMap, sync::Arc}; +use ui::{prelude::*, Tooltip}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, @@ -22,6 +17,11 @@ use workspace::{ actions!(debug, [TogglePanel]); +#[derive(Default)] +struct ThreadState { + pub stack_frames: Option>, +} + pub struct DebugPanel { pub position: DockPosition, pub zoomed: bool, @@ -31,6 +31,7 @@ pub struct DebugPanel { _subscriptions: Vec, pub thread_id: Option, pub workspace: WeakView, + thread_state: HashMap, } impl DebugPanel { @@ -49,12 +50,49 @@ impl DebugPanel { if let project::Event::DebugClientEvent { client_id, event } = event { match event { dap::events::Event::Initialized(_) => return, - dap::events::Event::Stopped(_) => todo!(), + dap::events::Event::Stopped(event) => { + if let Some(thread_id) = event.thread_id { + let client = this.debug_adapter(cx); + + cx.spawn(|this, mut cx| async move { + let res = client + .request::(StackTraceArguments { + thread_id, + start_frame: None, + levels: None, + format: None, + }) + .await?; + + this.update(&mut cx, |this, cx| { + if let Some(entry) = + this.thread_state.get_mut(&thread_id) + { + entry.stack_frames = Some(res.stack_frames); + + cx.notify(); + } + + anyhow::Ok(()) + }) + }) + .detach(); + }; + } dap::events::Event::Continued(_) => todo!(), dap::events::Event::Exited(_) => todo!(), dap::events::Event::Terminated(_) => todo!(), dap::events::Event::Thread(event) => { - this.thread_id = Some(event.thread_id); + if event.reason == "started" { + this.thread_state.insert( + event.thread_id, + ThreadState { stack_frames: None }, + ); + this.thread_id = Some(event.thread_id); + } else { + this.thread_id = None; + this.thread_state.remove(&event.thread_id); + } } dap::events::Event::Output(_) => todo!(), dap::events::Event::Breakpoint(_) => todo!(), @@ -77,17 +115,15 @@ impl DebugPanel { _subscriptions, thread_id: Some(ThreadId(1)), workspace: workspace.clone(), + thread_state: Default::default(), } }) } pub fn load( workspace: WeakView, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Task>> { - // workspace.project().update(cx, |project, cx| { - // project.start_debug_adapter_client(DebugAdapterClientId(1), cx); - // }); cx.spawn(|mut cx| async move { cx.update(|cx| DebugPanel::new(workspace, cx)) }) } @@ -102,6 +138,59 @@ impl DebugPanel { }) .unwrap() } + + fn render_stack_frames(&self, cx: &mut ViewContext) -> impl IntoElement { + let Some(thread_state) = self.thread_id.and_then(|t| self.thread_state.get(&t)) else { + return div().child("No information for this thread yet").into_any(); + }; + + let Some(stack_frames) = &thread_state.stack_frames else { + return div() + .child("No stack frames for this thread yet") + .into_any(); + }; + + div() + .gap_3() + .children( + stack_frames + .iter() + .map(|frame| self.render_stack_frame(frame, cx)), + ) + .into_any() + } + + fn render_stack_frame( + &self, + stack_frame: &StackFrame, + cx: &mut ViewContext, + ) -> impl IntoElement { + let source = stack_frame.source.clone(); + + div() + .id(("stack-frame", stack_frame.id)) + .p_1() + .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .child( + h_flex() + .gap_0p5() + .text_ui_sm(cx) + .child(stack_frame.name.clone()) + .child(format!( + "{}:{}", + source.clone().and_then(|s| s.name).unwrap_or_default(), + stack_frame.line, + )), + ) + .child( + div() + .text_ui_xs(cx) + .when_some(source.and_then(|s| s.path), |this, path| { + this.child(String::from(path.to_string_lossy())) + }), + ) + .into_any() + } } impl EventEmitter for DebugPanel {} @@ -176,14 +265,12 @@ impl Render for DebugPanel { .p_2() .gap_2() .child( - IconButton::new("debug-continue", IconName::Play) + IconButton::new("debug-resume", IconName::Play) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); if let Some(thread_id) = view.thread_id { cx.background_executor() - .spawn( - async move { client.continue_thread(thread_id).await }, - ) + .spawn(async move { client.resume(thread_id).await }) .detach(); } })) @@ -191,6 +278,14 @@ impl Render for DebugPanel { ) .child( IconButton::new("debug-step-over", IconName::Play) + .on_click(cx.listener(|view, _, cx| { + let client = view.debug_adapter(cx); + if let Some(thread_id) = view.thread_id { + cx.background_executor() + .spawn(async move { client.step_over(thread_id).await }) + .detach(); + } + })) .tooltip(move |cx| Tooltip::text("Step over", cx)), ) .child( @@ -227,6 +322,12 @@ impl Render for DebugPanel { .tooltip(move |cx| Tooltip::text("Stop", cx)), ), ) + .child( + h_flex() + .gap_4() + .child(self.render_stack_frames(cx)) + .child("Here see all the vars"), + ) .into_any() } } From 8015fb70e33ac96852603c12884028aecd1cb659 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 26 Jun 2024 11:44:43 +0200 Subject: [PATCH 029/650] Remove unused mut self --- crates/dap/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 7318922a8b2172..c537a32bf0149a 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -238,7 +238,7 @@ impl DebugAdapterClient { Ok(capabilities) } - pub async fn launch(&mut self) -> Result<()> { + pub async fn launch(&self) -> Result<()> { self.request::(LaunchRequestArguments { no_debug: Some(false), __restart: None, From 153efab377fda408897d3943650e5c3e688a2a5f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 26 Jun 2024 17:56:51 +0200 Subject: [PATCH 030/650] Migrate to `dap-types` crate --- Cargo.lock | 10 + crates/dap/Cargo.toml | 3 +- crates/dap/src/client.rs | 122 ++++--- crates/dap/src/events.rs | 150 -------- crates/dap/src/lib.rs | 5 +- crates/dap/src/requests.rs | 423 ----------------------- crates/dap/src/responses.rs | 16 - crates/dap/src/transport.rs | 47 ++- crates/dap/src/types.rs | 333 ------------------ crates/debugger_ui/src/debugger_panel.rs | 50 +-- crates/project/src/project.rs | 19 +- 11 files changed, 165 insertions(+), 1013 deletions(-) delete mode 100644 crates/dap/src/events.rs delete mode 100644 crates/dap/src/requests.rs delete mode 100644 crates/dap/src/responses.rs delete mode 100644 crates/dap/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 7c057cf84e9d10..03bdab2e7da947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3194,6 +3194,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-std", + "dap-types", "futures 0.3.28", "gpui", "log", @@ -3208,6 +3209,15 @@ dependencies = [ "util", ] +[[package]] +name = "dap-types" +version = "0.0.1" +source = "git+https://github.com/zed-industries/dap-types?rev=f9f436bf24a7b881d4ec2b0962d67d7d59f5eac7#f9f436bf24a7b881d4ec2b0962d67d7d59f5eac7" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "dashmap" version = "5.5.3" diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 300dcc27989101..dbff33fe7cd94e 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -10,6 +10,8 @@ workspace = true [dependencies] anyhow.workspace = true +async-std = "1.12.0" +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "f9f436bf24a7b881d4ec2b0962d67d7d59f5eac7" } futures.workspace = true gpui.workspace = true log.workspace = true @@ -22,4 +24,3 @@ serde_json.workspace = true serde_json_lenient.workspace = true smol.workspace = true util.workspace = true -async-std = "1.12.0" diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index c537a32bf0149a..0ead30b8bcfac0 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,19 +1,21 @@ -use crate::{ - events, +use crate::transport::{self, Events, Payload, Request, Transport}; +use anyhow::{anyhow, Context, Result}; + +use dap_types::{ requests::{ - ConfigurationDone, Continue, ContinueArguments, Initialize, InitializeArguments, Launch, - LaunchRequestArguments, Next, NextArguments, SetBreakpoints, SetBreakpointsArguments, - SetBreakpointsResponse, StepIn, StepInArguments, StepOut, StepOutArguments, + ConfigurationDone, Continue, Initialize, Launch, Next, SetBreakpoints, StepBack, StepIn, + StepOut, }, - transport::{self, Payload, Request, Transport}, - types::{DebuggerCapabilities, Source, SourceBreakpoint, SteppingGranularity, ThreadId}, + ConfigurationDoneArguments, ContinueArguments, InitializeRequestArgumentsPathFormat, + LaunchRequestArguments, NextArguments, SetBreakpointsArguments, SetBreakpointsResponse, Source, + SourceBreakpoint, StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, }; -use anyhow::{anyhow, Context, Result}; use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; use gpui::{AppContext, AsyncAppContext}; +use serde_json::json; use smol::{ io::BufReader, net::TcpStream, @@ -32,6 +34,7 @@ pub enum TransportType { TCP, STDIO, } + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct DebugAdapterClientId(pub usize); @@ -41,7 +44,7 @@ pub struct DebugAdapterClient { _process: Option, server_tx: UnboundedSender, request_count: AtomicU64, - capabilities: Option, + capabilities: Option, } impl DebugAdapterClient { @@ -55,7 +58,7 @@ impl DebugAdapterClient { event_handler: F, ) -> Result where - F: FnMut(events::Event, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { match transport_type { TransportType::TCP => { @@ -76,7 +79,7 @@ impl DebugAdapterClient { event_handler: F, ) -> Result where - F: FnMut(events::Event, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { let mut command = process::Command::new(command); command @@ -129,7 +132,7 @@ impl DebugAdapterClient { event_handler: F, ) -> Result where - F: FnMut(events::Event, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); @@ -156,7 +159,7 @@ impl DebugAdapterClient { cx: AsyncAppContext, ) -> Result<()> where - F: FnMut(events::Event, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { while let Some(payload) = client_rx.next().await { cx.update(|cx| match payload { @@ -182,10 +185,10 @@ impl DebugAdapterClient { } } - pub async fn request( + pub async fn request( &self, arguments: R::Arguments, - ) -> Result { + ) -> Result { let serialized_arguments = serde_json::to_value(arguments)?; let (callback_tx, mut callback_rx) = channel::>(1); @@ -214,21 +217,24 @@ impl DebugAdapterClient { self.request_count.fetch_add(1, Ordering::Relaxed) } - pub async fn initialize(&mut self) -> Result { - let args = InitializeArguments { + pub async fn initialize(&mut self) -> Result { + let args = dap_types::InitializeRequestArguments { client_id: Some("zed".to_owned()), client_name: Some("Zed".to_owned()), - adapter_id: "xdebug".into(), + adapter_id: "xdebug".into(), // TODO: read from config locale: Some("en-us".to_owned()), - lines_start_at_one: Some(true), - columns_start_at_one: Some(true), - path_format: Some("path".to_owned()), + path_format: Some(InitializeRequestArgumentsPathFormat::Path), supports_variable_type: Some(true), supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(false), + supports_run_in_terminal_request: Some(false), // TODO: we should support this supports_memory_references: Some(false), supports_progress_reporting: Some(false), supports_invalidated_event: Some(false), + lines_start_at1: Some(true), + columns_start_at1: Some(true), + supports_memory_event: None, + supports_args_can_be_interpreted_by_shell: None, + supports_start_debugging_request: None, }; let capabilities = self.request::(args).await?; @@ -240,52 +246,72 @@ impl DebugAdapterClient { pub async fn launch(&self) -> Result<()> { self.request::(LaunchRequestArguments { - no_debug: Some(false), - __restart: None, + raw: json!({"noDebug": false}), }) .await } - pub async fn resume(&self, thread_id: ThreadId) { + pub async fn resume(&self, thread_id: u64) { let _ = self - .request::(ContinueArguments { thread_id }) + .request::(ContinueArguments { + thread_id, + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), + }) .await; } - pub async fn step_over(&self, thread_id: ThreadId) { + pub async fn step_over(&self, thread_id: u64) { let _ = self .request::(NextArguments { thread_id, - granularity: Some(SteppingGranularity::Line), + granularity: Some(SteppingGranularity::Statement), + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), }) .await; } - pub async fn step_in(&self, thread_id: ThreadId) { + pub async fn step_in(&self, thread_id: u64) { let _ = self .request::(StepInArguments { thread_id, target_id: None, - granularity: None, + granularity: Some(SteppingGranularity::Statement), + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), }) .await; } - pub async fn step_out(&self, thread_id: ThreadId) { + pub async fn step_out(&self, thread_id: u64) { let _ = self .request::(StepOutArguments { thread_id, - granularity: None, + granularity: Some(SteppingGranularity::Statement), + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), }) .await; } - pub async fn step_back(&self, thread_id: ThreadId) { + pub async fn step_back(&self, thread_id: u64) { let _ = self - .request::(StepInArguments { + .request::(StepBackArguments { thread_id, - target_id: None, - granularity: None, + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), + granularity: Some(SteppingGranularity::Statement), }) .await; } @@ -293,26 +319,28 @@ impl DebugAdapterClient { pub async fn set_breakpoints( &self, path: PathBuf, - line: usize, + breakpoints: Option>, ) -> Result { self.request::(SetBreakpointsArguments { source: Source { - path: Some(path), - ..Default::default() + path: Some(String::from(path.to_string_lossy())), + name: None, + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, }, - breakpoints: Some(vec![SourceBreakpoint { - line, - column: None, - condition: None, - hit_condition: None, - log_message: None, - }]), + breakpoints, source_modified: None, + lines: None, }) .await } pub async fn configuration_done(&self) -> Result<()> { - self.request::(()).await + self.request::(ConfigurationDoneArguments) + .await } } diff --git a/crates/dap/src/events.rs b/crates/dap/src/events.rs deleted file mode 100644 index 052514098d468d..00000000000000 --- a/crates/dap/src/events.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::types::{DebuggerCapabilities, Source, ThreadId}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "event", content = "body")] -// seq is omitted as unused and is not sent by some implementations -pub enum Event { - Initialized(Option), - Stopped(Stopped), - Continued(Continued), - Exited(Exited), - Terminated(Option), - Thread(Thread), - Output(Output), - Breakpoint(Breakpoint), - Module(Module), - LoadedSource(LoadedSource), - Process(Process), - Capabilities(Capabilities), - // ProgressStart(), - // ProgressUpdate(), - // ProgressEnd(), - // Invalidated(), - Memory(Memory), -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Stopped { - pub reason: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub thread_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub preserve_focus_hint: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub text: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub all_threads_stopped: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub hit_breakpoint_ids: Option>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Continued { - pub thread_id: ThreadId, - #[serde(skip_serializing_if = "Option::is_none")] - pub all_threads_continued: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Exited { - pub exit_code: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Terminated { - #[serde(skip_serializing_if = "Option::is_none")] - pub restart: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Thread { - pub reason: String, - pub thread_id: ThreadId, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Output { - pub output: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub category: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub group: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub variables_reference: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Breakpoint { - pub reason: String, - pub breakpoint: crate::types::Breakpoint, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Module { - pub reason: String, - pub module: crate::types::Module, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LoadedSource { - pub reason: String, - pub source: crate::types::Source, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Process { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub system_process_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_local_process: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_method: Option, // TODO: use enum - #[serde(skip_serializing_if = "Option::is_none")] - pub pointer_size: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Capabilities { - pub capabilities: crate::types::DebuggerCapabilities, -} - -// #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -// #[serde(rename_all = "camelCase")] -// pub struct Invalidated { -// pub areas: Vec, -// pub thread_id: Option, -// pub stack_frame_id: Option, -// } - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Memory { - pub memory_reference: String, - pub offset: usize, - pub count: usize, -} diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index 423407bda4034c..44a69c12b50367 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,7 +1,4 @@ pub mod client; pub mod config_templates; -pub mod events; -pub mod requests; -pub mod responses; pub mod transport; -pub mod types; +pub use dap_types::*; diff --git a/crates/dap/src/requests.rs b/crates/dap/src/requests.rs deleted file mode 100644 index 9688a2e07fd604..00000000000000 --- a/crates/dap/src/requests.rs +++ /dev/null @@ -1,423 +0,0 @@ -use crate::types::{ - Breakpoint, DebuggerCapabilities, Scope, Source, SourceBreakpoint, StackFrame, - StackFrameFormat, SteppingGranularity, Thread, ThreadId, ValueFormat, Variable, - VariablePresentationHint, -}; -use ::serde::{Deserialize, Serialize}; -use serde::de::DeserializeOwned; -use serde_json::Value; -use std::collections::HashMap; - -pub trait Request { - type Arguments: DeserializeOwned + Serialize; - type Result: DeserializeOwned + Serialize; - const COMMAND: &'static str; -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct InitializeArguments { - #[serde(rename = "clientID", skip_serializing_if = "Option::is_none")] - pub client_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub client_name: Option, - #[serde(rename = "adapterID")] - pub adapter_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub locale: Option, - #[serde(rename = "linesStartAt1", skip_serializing_if = "Option::is_none")] - pub lines_start_at_one: Option, - #[serde(rename = "columnsStartAt1", skip_serializing_if = "Option::is_none")] - pub columns_start_at_one: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub path_format: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_variable_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_variable_paging: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_run_in_terminal_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_memory_references: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_progress_reporting: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_invalidated_event: Option, -} - -#[derive(Debug)] -pub enum Initialize {} - -impl Request for Initialize { - type Arguments = InitializeArguments; - type Result = DebuggerCapabilities; - const COMMAND: &'static str = "initialize"; -} - -#[derive(Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LaunchRequestArguments { - #[serde(skip_serializing_if = "Option::is_none")] - pub no_debug: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub __restart: Option, -} - -#[derive(Debug)] -pub enum Launch {} - -impl Request for Launch { - type Arguments = LaunchRequestArguments; - type Result = (); - const COMMAND: &'static str = "launch"; -} - -#[derive(Debug)] -pub enum Attach {} - -impl Request for Attach { - type Arguments = Value; - type Result = (); - const COMMAND: &'static str = "attach"; -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DisconnectArguments { - #[serde(skip_serializing_if = "Option::is_none")] - pub restart: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub terminate_debuggee: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub suspend_debuggee: Option, -} - -#[derive(Debug)] -pub enum Restart {} - -impl Request for Restart { - type Arguments = Value; - type Result = (); - const COMMAND: &'static str = "restart"; -} - -#[derive(Debug)] -pub enum Disconnect {} - -impl Request for Disconnect { - type Arguments = Option; - type Result = (); - const COMMAND: &'static str = "disconnect"; -} - -#[derive(Debug)] -pub enum ConfigurationDone {} - -impl Request for ConfigurationDone { - type Arguments = (); - type Result = (); - const COMMAND: &'static str = "configurationDone"; -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SetBreakpointsArguments { - pub source: Source, - #[serde(skip_serializing_if = "Option::is_none")] - pub breakpoints: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub source_modified: Option, -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SetBreakpointsResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub breakpoints: Option>, -} - -#[derive(Debug)] -pub enum SetBreakpoints {} - -impl Request for SetBreakpoints { - type Arguments = SetBreakpointsArguments; - type Result = SetBreakpointsResponse; - const COMMAND: &'static str = "setBreakpoints"; -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ContinueArguments { - pub thread_id: ThreadId, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ContinueResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub all_threads_continued: Option, -} - -#[derive(Debug)] -pub enum Continue {} - -impl Request for Continue { - type Arguments = ContinueArguments; - type Result = ContinueResponse; - const COMMAND: &'static str = "continue"; -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct StackTraceArguments { - pub thread_id: ThreadId, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_frame: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub levels: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct StackTraceResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub total_frames: Option, - pub stack_frames: Vec, -} - -#[derive(Debug)] -pub enum StackTrace {} - -impl Request for StackTrace { - type Arguments = StackTraceArguments; - type Result = StackTraceResponse; - const COMMAND: &'static str = "stackTrace"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ThreadsResponse { - pub threads: Vec, -} - -#[derive(Debug)] -pub enum Threads {} - -impl Request for Threads { - type Arguments = (); - type Result = ThreadsResponse; - const COMMAND: &'static str = "threads"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ScopesArguments { - pub frame_id: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ScopesResponse { - pub scopes: Vec, -} - -#[derive(Debug)] -pub enum Scopes {} - -impl Request for Scopes { - type Arguments = ScopesArguments; - type Result = ScopesResponse; - const COMMAND: &'static str = "scopes"; -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VariablesArguments { - pub variables_reference: usize, - #[serde(skip_serializing_if = "Option::is_none")] - pub filter: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub start: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub count: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VariablesResponse { - pub variables: Vec, -} - -#[derive(Debug)] -pub enum Variables {} - -impl Request for Variables { - type Arguments = VariablesArguments; - type Result = VariablesResponse; - const COMMAND: &'static str = "variables"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct StepInArguments { - pub thread_id: ThreadId, - #[serde(skip_serializing_if = "Option::is_none")] - pub target_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub granularity: Option, -} - -#[derive(Debug)] -pub enum StepIn {} - -impl Request for StepIn { - type Arguments = StepInArguments; - type Result = (); - const COMMAND: &'static str = "stepIn"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct StepOutArguments { - pub thread_id: ThreadId, - #[serde(skip_serializing_if = "Option::is_none")] - pub granularity: Option, -} - -#[derive(Debug)] -pub enum StepOut {} - -impl Request for StepOut { - type Arguments = StepOutArguments; - type Result = (); - const COMMAND: &'static str = "stepOut"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NextArguments { - pub thread_id: ThreadId, - #[serde(skip_serializing_if = "Option::is_none")] - pub granularity: Option, -} - -#[derive(Debug)] -pub enum Next {} - -impl Request for Next { - type Arguments = NextArguments; - type Result = (); - const COMMAND: &'static str = "next"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct PauseArguments { - pub thread_id: ThreadId, -} - -#[derive(Debug)] -pub enum Pause {} - -impl Request for Pause { - type Arguments = PauseArguments; - type Result = (); - const COMMAND: &'static str = "pause"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EvaluateArguments { - pub expression: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub frame_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EvaluateResponse { - pub result: String, - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub _type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub presentation_hint: Option, - pub variables_reference: usize, - #[serde(skip_serializing_if = "Option::is_none")] - pub named_variables: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub indexed_variables: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub memory_reference: Option, -} - -#[derive(Debug)] -pub enum Evaluate {} - -impl Request for Evaluate { - type Arguments = EvaluateArguments; - type Result = EvaluateResponse; - const COMMAND: &'static str = "evaluate"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SetExceptionBreakpointsArguments { - pub filters: Vec, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SetExceptionBreakpointsResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub breakpoints: Option>, -} - -#[derive(Debug)] -pub enum SetExceptionBreakpoints {} - -impl Request for SetExceptionBreakpoints { - type Arguments = SetExceptionBreakpointsArguments; - type Result = SetExceptionBreakpointsResponse; - const COMMAND: &'static str = "setExceptionBreakpoints"; -} - -// Reverse Requests - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RunInTerminalResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub process_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub shell_process_id: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RunInTerminalArguments { - #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - pub cwd: String, - pub args: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub env: Option>>, -} - -#[derive(Debug)] -pub enum RunInTerminal {} - -impl Request for RunInTerminal { - type Arguments = RunInTerminalArguments; - type Result = RunInTerminalResponse; - const COMMAND: &'static str = "runInTerminal"; -} diff --git a/crates/dap/src/responses.rs b/crates/dap/src/responses.rs deleted file mode 100644 index a97afcbc0889b4..00000000000000 --- a/crates/dap/src/responses.rs +++ /dev/null @@ -1,16 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Response { - _type: String, - success: bool, - arguments: Option, - request_seq: i32, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -enum ResponseArguments { - LaunchResponse, -} diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 0ca9d580e66f9d..4a2615dad15583 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -1,5 +1,9 @@ -use crate::events::Event; use anyhow::{anyhow, Context, Result}; +use dap_types::{ + BreakpointEvent, CapabilitiesEvent, ContinuedEvent, ExitedEvent, InvalidatedEvent, + LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent, StoppedEvent, + TerminatedEvent, ThreadEvent, +}; use futures::{ channel::mpsc::{unbounded, Sender, UnboundedReceiver, UnboundedSender}, AsyncBufRead, AsyncWrite, SinkExt as _, StreamExt, @@ -10,7 +14,38 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use smol::io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _}; use std::{collections::HashMap, sync::Arc}; -use util::ResultExt; +use util::ResultExt as _; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum Payload { + Event(Box), + Response(Response), + Request(Request), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(tag = "event", content = "body")] +#[serde(rename_all = "camelCase")] +pub enum Events { + Initialized, + Stopped(StoppedEvent), + Continued(ContinuedEvent), + Exited(ExitedEvent), + Terminated(TerminatedEvent), + Thread(ThreadEvent), + Output(OutputEvent), + Breakpoint(BreakpointEvent), + Module(ModuleEvent), + LoadedSource(LoadedSourceEvent), + Process(ProcessEvent), + Capabilities(CapabilitiesEvent), + ProgressStart, + ProgressUpdate, + ProgressEnd, + Invalidated(InvalidatedEvent), + Memory(MemoryEvent), +} #[derive(Debug, Deserialize, Serialize)] pub struct Request { @@ -30,14 +65,6 @@ pub struct Response { pub body: Option, } -#[derive(Debug, Deserialize, Serialize)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum Payload { - Event(Box), - Response(Response), - Request(Request), -} - #[derive(Debug)] pub struct Transport { pending_requests: Mutex>>>, diff --git a/crates/dap/src/types.rs b/crates/dap/src/types.rs deleted file mode 100644 index 39ddb6476dad0a..00000000000000 --- a/crates/dap/src/types.rs +++ /dev/null @@ -1,333 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::{collections::HashMap, fmt, path::PathBuf}; - -#[derive( - Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, -)] -pub struct ThreadId(pub isize); - -impl fmt::Display for ThreadId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -pub type ThreadStates = HashMap; - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ColumnDescriptor { - pub attribute_name: String, - pub label: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub ty: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub width: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ExceptionBreakpointsFilter { - pub filter: String, - pub label: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub default: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub condition_description: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DebuggerCapabilities { - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_configuration_done_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_function_breakpoints: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_conditional_breakpoints: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_hit_conditional_breakpoints: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_evaluate_for_hovers: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_step_back: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_set_variable: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_restart_frame: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_goto_targets_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_step_in_targets_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_completions_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_modules_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_restart_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_exception_options: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_value_formatting_options: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_exception_info_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub support_terminate_debuggee: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub support_suspend_debuggee: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_delayed_stack_trace_loading: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_loaded_sources_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_log_points: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_terminate_threads_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_set_expression: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_terminate_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_data_breakpoints: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_read_memory_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_write_memory_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_disassemble_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_cancel_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_breakpoint_locations_request: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_clipboard_context: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_stepping_granularity: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_instruction_breakpoints: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub supports_exception_filter_options: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub exception_breakpoint_filters: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub completion_trigger_characters: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub additional_module_columns: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub supported_checksum_algorithms: Option>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Checksum { - pub algorithm: String, - pub checksum: String, -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Source { - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub source_reference: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub presentation_hint: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub origin: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sources: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub adapter_data: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub checksums: Option>, -} - -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SourceBreakpoint { - pub line: usize, - #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub hit_condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub log_message: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Breakpoint { - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - pub verified: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub message: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_column: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub instruction_reference: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub offset: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct StackFrameFormat { - #[serde(skip_serializing_if = "Option::is_none")] - pub parameters: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub parameter_types: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub parameter_names: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub parameter_values: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub module: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub include_all: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct StackFrame { - pub id: usize, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - pub line: usize, - pub column: usize, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_column: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub can_restart: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub instruction_pointer_reference: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub module_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub presentation_hint: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Thread { - pub id: ThreadId, - pub name: String, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Scope { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub presentation_hint: Option, - pub variables_reference: usize, - #[serde(skip_serializing_if = "Option::is_none")] - pub named_variables: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub indexed_variables: Option, - pub expensive: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub end_column: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ValueFormat { - #[serde(skip_serializing_if = "Option::is_none")] - pub hex: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VariablePresentationHint { - #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub attributes: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub visibility: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Variable { - pub name: String, - pub value: String, - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub ty: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub presentation_hint: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub evaluate_name: Option, - pub variables_reference: usize, - #[serde(skip_serializing_if = "Option::is_none")] - pub named_variables: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub indexed_variables: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub memory_reference: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Module { - pub id: isize, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_optimized: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_user_code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub symbol_status: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub symbol_file_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub date_time_stamp: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub address_range: Option, -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum SteppingGranularity { - Statement, - Line, - Instruction, -} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a7d373f0a75706..16b36131932b79 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,9 +1,7 @@ use anyhow::Result; -use dap::{ - client::DebugAdapterClient, - requests::{StackTrace, StackTraceArguments}, - types::{StackFrame, ThreadId}, -}; +use dap::requests::StackTrace; +use dap::{client::DebugAdapterClient, transport::Events}; +use dap::{StackFrame, StackTraceArguments, ThreadEventReason}; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, Subscription, Task, View, ViewContext, WeakView, @@ -29,9 +27,9 @@ pub struct DebugPanel { pub focus_handle: FocusHandle, pub size: Pixels, _subscriptions: Vec, - pub thread_id: Option, + pub thread_id: Option, pub workspace: WeakView, - thread_state: HashMap, + thread_state: HashMap, } impl DebugPanel { @@ -49,8 +47,8 @@ impl DebugPanel { if let project::Event::DebugClientEvent { client_id, event } = event { match event { - dap::events::Event::Initialized(_) => return, - dap::events::Event::Stopped(event) => { + Events::Initialized => return, + Events::Stopped(event) => { if let Some(thread_id) = event.thread_id { let client = this.debug_adapter(cx); @@ -79,11 +77,11 @@ impl DebugPanel { .detach(); }; } - dap::events::Event::Continued(_) => todo!(), - dap::events::Event::Exited(_) => todo!(), - dap::events::Event::Terminated(_) => todo!(), - dap::events::Event::Thread(event) => { - if event.reason == "started" { + Events::Continued(_) => todo!(), + Events::Exited(_) => todo!(), + Events::Terminated(_) => todo!(), + Events::Thread(event) => { + if event.reason == ThreadEventReason::Started { this.thread_state.insert( event.thread_id, ThreadState { stack_frames: None }, @@ -94,13 +92,17 @@ impl DebugPanel { this.thread_state.remove(&event.thread_id); } } - dap::events::Event::Output(_) => todo!(), - dap::events::Event::Breakpoint(_) => todo!(), - dap::events::Event::Module(_) => todo!(), - dap::events::Event::LoadedSource(_) => todo!(), - dap::events::Event::Process(_) => todo!(), - dap::events::Event::Capabilities(_) => todo!(), - dap::events::Event::Memory(_) => todo!(), + Events::Output(_) => todo!(), + Events::Breakpoint(_) => todo!(), + Events::Module(_) => todo!(), + Events::LoadedSource(_) => todo!(), + Events::Capabilities(_) => todo!(), + Events::Memory(_) => todo!(), + Events::Process(_) => todo!(), + Events::ProgressEnd => todo!(), + Events::ProgressStart => todo!(), + Events::ProgressUpdate => todo!(), + Events::Invalidated(_) => todo!(), } } } @@ -113,7 +115,7 @@ impl DebugPanel { focus_handle: cx.focus_handle(), size: px(300.), _subscriptions, - thread_id: Some(ThreadId(1)), + thread_id: Some(1), workspace: workspace.clone(), thread_state: Default::default(), } @@ -185,9 +187,7 @@ impl DebugPanel { .child( div() .text_ui_xs(cx) - .when_some(source.and_then(|s| s.path), |this, path| { - this.child(String::from(path.to_string_lossy())) - }), + .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), ) .into_any() } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 83dc090f0eb172..4dd80200721335 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -23,7 +23,8 @@ use clock::ReplicaId; use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use dap::{ client::{DebugAdapterClient, DebugAdapterClientId, TransportType}, - config_templates::DebuggerConfigTemplate, + transport::Events, + SourceBreakpoint, }; use debounced_delay::DebouncedDelay; use debugger_inventory::{DebuggerConfigSourceKind, DebuggerInventory}; @@ -343,7 +344,7 @@ pub enum Event { DebugClientStarted(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, - event: dap::events::Event, + event: Events, }, ActiveEntryChanged(Option), ActivateProjectPanel, @@ -1089,7 +1090,7 @@ impl Project { client_id: id, event, }) - }); + }).log_err(); }, ) .await @@ -1170,7 +1171,17 @@ impl Project { .spawn(async move { for client in clients { client - .set_breakpoints(abs_path.clone(), row as usize) + .set_breakpoints( + abs_path.clone(), + Some(vec![SourceBreakpoint { + line: row as u64, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + }]), + ) .await?; } anyhow::Ok(()) From 0508df9e7bebec03a7201e664ff9fda244d9ab10 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 26 Jun 2024 18:20:18 +0200 Subject: [PATCH 031/650] Remove commit hash use repo url only --- Cargo.lock | 2 +- crates/dap/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03bdab2e7da947..e14a4720c231d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3212,7 +3212,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=f9f436bf24a7b881d4ec2b0962d67d7d59f5eac7#f9f436bf24a7b881d4ec2b0962d67d7d59f5eac7" +source = "git+https://github.com/zed-industries/dap-types#6e85834f9948cb5e5f085f7d5792235696ea78e2" dependencies = [ "serde", "serde_json", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index dbff33fe7cd94e..ea7428f01281d2 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -11,7 +11,7 @@ workspace = true [dependencies] anyhow.workspace = true async-std = "1.12.0" -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "f9f436bf24a7b881d4ec2b0962d67d7d59f5eac7" } +dap-types = { git = "https://github.com/zed-industries/dap-types" } futures.workspace = true gpui.workspace = true log.workspace = true From a67f28dba24b8ce3ed29fc32a57eb775752f8151 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 27 Jun 2024 11:00:33 -0400 Subject: [PATCH 032/650] Get Start Debugger action to open task menu --- .gitignore | 1 + Cargo.lock | 2 ++ crates/debugger_ui/Cargo.toml | 2 ++ crates/debugger_ui/src/lib.rs | 2 +- crates/paths/src/paths.rs | 2 +- crates/project/src/project.rs | 35 ++++++++++++++++---------------- crates/task/src/debug_format.rs | 24 ++++++++++++++++++++++ crates/task/src/lib.rs | 2 +- crates/task/src/task_template.rs | 16 ++++++++++++++- crates/tasks_ui/src/lib.rs | 13 ++++++++---- crates/tasks_ui/src/modal.rs | 9 ++++++-- 11 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 crates/task/src/debug_format.rs diff --git a/.gitignore b/.gitignore index e0dfc13d37b75b..3d5187ca4eb292 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ DerivedData/ .vscode .wrangler .flatpak-builder +.zed/debug.json diff --git a/Cargo.lock b/Cargo.lock index e14a4720c231d5..08809fe0569724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3282,6 +3282,8 @@ dependencies = [ "project", "serde", "serde_derive", + "task", + "tasks_ui", "ui", "workspace", ] diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 2ad433f813a1eb..0f333d0325dbd3 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -22,6 +22,8 @@ serde.workspace = true serde_derive.workspace = true ui.workspace = true workspace.workspace = true +tasks_ui.workspace = true +task.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index cb4a816845b2e1..cf77096378c064 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -17,7 +17,7 @@ pub fn init(cx: &mut AppContext) { |workspace: &mut Workspace, action: &StartDebugger, cx: &mut ViewContext<'_, Workspace>| { - select_debugger(workspace, action, cx).detach(); + tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach(); }, ); }, diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 08a4bfe91072fb..a832a2df2630fa 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -249,7 +249,7 @@ pub fn local_vscode_tasks_file_relative_path() -> &'static Path { } /// Returns the relative path to a `launch.json` file within a project. -pub fn local_launch_file_relative_path() -> &'static Path { +pub fn local_debug_file_relative_path() -> &'static Path { static LOCAL_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new(); LOCAL_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".zed/debug.json")) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4dd80200721335..edf6475533a928 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -74,7 +74,7 @@ use lsp_command::*; use node_runtime::NodeRuntime; use parking_lot::{Mutex, RwLock}; use paths::{ - local_launch_file_relative_path, local_settings_file_relative_path, + local_debug_file_relative_path, local_settings_file_relative_path, local_tasks_file_relative_path, local_vscode_launch_file_relative_path, local_vscode_tasks_file_relative_path, }; @@ -1085,12 +1085,14 @@ impl Project { "/Users/remcosmits/Documents/code/symfony_demo".into(), &mut cx, move |event, cx| { - this2.update(cx, |_, cx| { - cx.emit(Event::DebugClientEvent { - client_id: id, - event, + this2 + .update(cx, |_, cx| { + cx.emit(Event::DebugClientEvent { + client_id: id, + event, + }) }) - }).log_err(); + .log_err(); }, ) .await @@ -8425,28 +8427,24 @@ impl Project { ); } }) - } else if abs_path.ends_with(local_launch_file_relative_path()) { + } else if abs_path.ends_with(local_debug_file_relative_path()) { // TODO: handle local launch file (.zed/debug.json) - self.debugger_configs().update(cx, |debugger_configs, cx| { + self.task_inventory().update(cx, |task_inventory, cx| { if removed { - debugger_configs.remove_source(&abs_path); + task_inventory.remove_local_static_source(&abs_path); } else { let fs = self.fs.clone(); - let debugger_configs_file_rx = + let debug_task_file_rx = watch_config_file(&cx.background_executor(), fs, abs_path.clone()); - debugger_configs.add_source( - DebuggerConfigSourceKind::Worktree { - id: remote_worktree_id.to_usize(), + task_inventory.add_source( + TaskSourceKind::Worktree { + id: remote_worktree_id, abs_path, id_base: "local_debug_File_for_worktree".into(), }, |tx, cx| { - debugger_inventory::StaticSource::new(TrackedFile::new( - debugger_configs_file_rx, - tx, - cx, - )) + StaticSource::new(TrackedFile::new(debug_task_file_rx, tx, cx)) }, cx, ); @@ -11139,6 +11137,7 @@ impl Project { allow_concurrent_runs: proto_template.allow_concurrent_runs, reveal, tags: proto_template.tags, + ..Default::default() }; Some((task_source_kind, task_template)) }) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs new file mode 100644 index 00000000000000..7f3a49ac51a409 --- /dev/null +++ b/crates/task/src/debug_format.rs @@ -0,0 +1,24 @@ +use anyhow::bail; +use collections::HashMap; +use serde::Deserialize; +use util::ResultExt; + +use crate::{TaskTemplate, TaskTemplates, VariableName}; + +struct ZedDebugTaskFile {} + +impl ZedDebugTaskFile { + fn to_zed_format(self) -> anyhow::Result {} +} +impl TryFrom for TaskTemplates { + type Error = anyhow::Error; + + fn try_from(value: ZedDebugTaskFile) -> Result { + let templates = value + .tasks + .into_iter() + .filter_map(|debug_task_file| debug_task_file.to_zed_format(&replacer).log_err()) + .collect(); + Ok(Self(templates)) + } +} diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 10b9b050a48128..dcc252c2873fb5 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -12,7 +12,7 @@ use std::path::PathBuf; use std::str::FromStr; use std::{borrow::Cow, path::Path}; -pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates}; +pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates, TaskType}; pub use vscode_format::VsCodeTaskFile; /// Task identifier, unique within the application. diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index fda245b4c53061..831e8bebdb0c2a 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{default, path::PathBuf}; use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; @@ -45,12 +45,26 @@ pub struct TaskTemplate { /// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there #[serde(default)] pub reveal: RevealStrategy, + /// If this task should start a debugger or not + #[serde(default)] + pub task_type: TaskType, /// Represents the tags which this template attaches to. Adding this removes this task from other UI. #[serde(default)] pub tags: Vec, } +/// Represents the type of task that is being ran +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Copy, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TaskType { + /// Act like a typically task that runs commands + #[default] + Script, + /// This task starts the debugger for a language + Debug, +} + /// What to do with the terminal pane and tab, after the command was started. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 2106cbe831be44..d7dab3c19b5b0a 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -3,6 +3,7 @@ use editor::{tasks::task_context, Editor}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; use modal::TasksModal; use project::{Location, WorktreeId}; +use task::TaskType; use workspace::tasks::schedule_task; use workspace::{tasks::schedule_resolved_task, Workspace}; @@ -70,7 +71,7 @@ pub fn init(cx: &mut AppContext) { ); } } else { - toggle_modal(workspace, cx).detach(); + toggle_modal(workspace, cx, TaskType::Script).detach(); }; }); }, @@ -81,11 +82,15 @@ pub fn init(cx: &mut AppContext) { fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext) { match &action.task_name { Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx), - None => toggle_modal(workspace, cx).detach(), + None => toggle_modal(workspace, cx, task::TaskType::Script).detach(), } } -fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> { +pub fn toggle_modal( + workspace: &mut Workspace, + cx: &mut ViewContext<'_, Workspace>, + task_type: TaskType, +) -> AsyncTask<()> { let project = workspace.project().clone(); let workspace_handle = workspace.weak_handle(); let context_task = task_context(workspace, cx); @@ -97,7 +102,7 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) project.is_local() || project.ssh_connection_string(cx).is_some() }) { workspace.toggle_modal(cx, |cx| { - TasksModal::new(project, task_context, workspace_handle, cx) + TasksModal::new(project, task_context, workspace_handle, cx, task_type) }) } }) diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 34c1c127d65eb1..f3e93c277f786e 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -9,7 +9,7 @@ use gpui::{ }; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use project::{Project, TaskSourceKind}; -use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate}; +use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskType}; use ui::{ div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, @@ -73,6 +73,8 @@ pub(crate) struct TasksModalDelegate { prompt: String, task_context: TaskContext, placeholder_text: Arc, + /// If this delegate is responsible for running a scripting task or a debugger + task_type: TaskType, } impl TasksModalDelegate { @@ -80,6 +82,7 @@ impl TasksModalDelegate { project: Model, task_context: TaskContext, workspace: WeakView, + task_type: TaskType, ) -> Self { Self { project, @@ -92,6 +95,7 @@ impl TasksModalDelegate { prompt: String::default(), task_context, placeholder_text: Arc::from("Find a task, or run a command"), + task_type, } } @@ -143,10 +147,11 @@ impl TasksModal { task_context: TaskContext, workspace: WeakView, cx: &mut ViewContext, + task_type: TaskType, ) -> Self { let picker = cx.new_view(|cx| { Picker::uniform_list( - TasksModalDelegate::new(project, task_context, workspace), + TasksModalDelegate::new(project, task_context, workspace, task_type), cx, ) }); From b81065fe63b1b72de9dc4f0051a0ee1d7b781a3e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 27 Jun 2024 11:56:28 -0400 Subject: [PATCH 033/650] Get debug tasks to show in start debugger menu --- crates/task/src/lib.rs | 6 ++++++ crates/tasks_ui/src/modal.rs | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index dcc252c2873fb5..3b53aacde1b8c1 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -106,6 +106,12 @@ impl ResolvedTask { &self.original_task } + /// Get the task type that determines what this task is used for + /// And where is it shown in the UI + pub fn task_type(&self) -> TaskType { + self.original_task.task_type.clone() + } + /// Variables that were substituted during the task template resolution. pub fn substituted_variables(&self) -> &HashSet { &self.substituted_variables diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index f3e93c277f786e..8f8b8895cb0228 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -208,12 +208,13 @@ impl PickerDelegate for TasksModalDelegate { query: String, cx: &mut ViewContext>, ) -> Task<()> { + let task_type = self.task_type.clone(); cx.spawn(move |picker, mut cx| async move { let Some(candidates_task) = picker .update(&mut cx, |picker, cx| { match &mut picker.delegate.candidates { Some(candidates) => { - Task::ready(Ok(string_match_candidates(candidates.iter()))) + Task::ready(Ok(string_match_candidates(candidates.iter(), task_type))) } None => { let Ok((worktree, location)) = @@ -257,6 +258,7 @@ impl PickerDelegate for TasksModalDelegate { ) } }); + cx.spawn(|picker, mut cx| async move { let (used, current) = resolved_task.await; picker.update(&mut cx, |picker, _| { @@ -269,7 +271,7 @@ impl PickerDelegate for TasksModalDelegate { let mut new_candidates = used; new_candidates.extend(current); let match_candidates = - string_match_candidates(new_candidates.iter()); + string_match_candidates(new_candidates.iter(), task_type); let _ = picker.delegate.candidates.insert(new_candidates); match_candidates }) @@ -591,8 +593,10 @@ impl PickerDelegate for TasksModalDelegate { fn string_match_candidates<'a>( candidates: impl Iterator + 'a, + task_type: TaskType, ) -> Vec { candidates + .filter(|(_, candidate)| candidate.task_type() == task_type) .enumerate() .map(|(index, (_, candidate))| StringMatchCandidate { id: index, From 3a0b31137810f18f78c6a2db279201dc8b2a7f9b Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 27 Jun 2024 13:22:16 -0400 Subject: [PATCH 034/650] Get start debugger task to run DAP on confirm --- crates/debugger_ui/src/modal.rs | 4 +--- crates/project/src/project.rs | 24 +++++++++++++++++------- crates/tasks_ui/src/modal.rs | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/crates/debugger_ui/src/modal.rs b/crates/debugger_ui/src/modal.rs index 02e9b008fc090f..1a5140e0a2dc35 100644 --- a/crates/debugger_ui/src/modal.rs +++ b/crates/debugger_ui/src/modal.rs @@ -108,9 +108,7 @@ impl PickerDelegate for DebuggerModelDelegate { fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { self.workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client(cx); - }); + workspace.project().update(cx, |project, cx| {}); workspace.focus_panel::(cx); }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index edf6475533a928..f8ff61fd32ade2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1069,20 +1069,30 @@ impl Project { }) } - pub fn start_debug_adapter_client(&mut self, cx: &mut ModelContext) { + pub fn start_debug_adapter_client( + &mut self, + debug_task: task::ResolvedTask, + cx: &mut ModelContext, + ) { let id = DebugAdapterClientId(1); + let debug_template = debug_task.original_task(); + let command = debug_template.command.clone(); + let cwd = debug_template + .cwd + .clone() + .expect("Debug tasks need to know what directory to open"); + let mut args = debug_template.args.clone(); + + args.push("--server=8131".to_string().clone()); let task = cx.spawn(|this, mut cx| async move { let this2 = this.clone(); let mut client = DebugAdapterClient::new( TransportType::TCP, - "bun", - vec![ - "/Users/remcosmits/Documents/code/vscode-php-debug/out/phpDebug.js", - "--server=8131", - ], + &command, + args.iter().map(|ele| &ele[..]).collect(), 8131, - "/Users/remcosmits/Documents/code/symfony_demo".into(), + cwd.into(), &mut cx, move |event, cx| { this2 diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 8f8b8895cb0228..fba9bb450b2fb9 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -341,7 +341,20 @@ impl PickerDelegate for TasksModalDelegate { self.workspace .update(cx, |workspace, cx| { - schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx); + match task.task_type() { + TaskType::Script => schedule_resolved_task( + workspace, + task_source_kind, + task, + omit_history_entry, + cx, + ), + // TODO: Should create a schedule_resolved_debug_task function + // This would allow users to access to debug history and other issues + TaskType::Debug => workspace.project().update(cx, |project, cx| { + project.start_debug_adapter_client(task, cx) + }), + }; }) .ok(); cx.emit(DismissEvent); @@ -480,9 +493,23 @@ impl PickerDelegate for TasksModalDelegate { let Some((task_source_kind, task)) = self.spawn_oneshot() else { return; }; + self.workspace .update(cx, |workspace, cx| { - schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx); + match task.task_type() { + TaskType::Script => schedule_resolved_task( + workspace, + task_source_kind, + task, + omit_history_entry, + cx, + ), + // TODO: Should create a schedule_resolved_debug_task function + // This would allow users to access to debug history and other issues + TaskType::Debug => workspace.project().update(cx, |project, cx| { + project.start_debug_adapter_client(task, cx) + }), + }; }) .ok(); cx.emit(DismissEvent); @@ -596,8 +623,8 @@ fn string_match_candidates<'a>( task_type: TaskType, ) -> Vec { candidates - .filter(|(_, candidate)| candidate.task_type() == task_type) .enumerate() + .filter(|(_, (_, candidate))| candidate.task_type() == task_type) .map(|(index, (_, candidate))| StringMatchCandidate { id: index, char_bag: candidate.resolved_label.chars().collect(), From ab58d14559449123552791bc903b7e60b41be937 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 27 Jun 2024 20:05:02 +0200 Subject: [PATCH 035/650] Wip display scopes & variables --- crates/debugger_ui/src/debugger_panel.rs | 260 +++++++++++++++++++---- crates/project/src/project.rs | 14 +- 2 files changed, 225 insertions(+), 49 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c287791ed1cef9..c6645978827aa3 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,10 +1,13 @@ use anyhow::Result; -use dap::requests::StackTrace; +use dap::requests::{Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; -use dap::{StackFrame, StackTraceArguments, ThreadEventReason}; +use dap::{ + Scope, ScopesArguments, StackFrame, StackTraceArguments, ThreadEventReason, Variable, + VariablesArguments, +}; use gpui::{ - actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, - Subscription, Task, View, ViewContext, WeakView, + actions, list, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, + FocusableView, ListState, Subscription, Task, View, ViewContext, WeakView, }; use std::{collections::HashMap, sync::Arc}; use ui::{prelude::*, Tooltip}; @@ -17,7 +20,9 @@ actions!(debug, [TogglePanel]); #[derive(Default)] struct ThreadState { - pub stack_frames: Option>, + pub stack_frames: Vec, + pub scopes: HashMap>, // stack_frame_id -> scopes + pub variables: HashMap>, // scope.variable_reference -> variables } pub struct DebugPanel { @@ -27,9 +32,11 @@ pub struct DebugPanel { pub focus_handle: FocusHandle, pub size: Pixels, _subscriptions: Vec, - pub thread_id: Option, + pub current_thread_id: Option, + pub current_stack_frame_id: Option, pub workspace: WeakView, thread_state: HashMap, + pub stack_frame_list: ListState, } impl DebugPanel { @@ -62,11 +69,61 @@ impl DebugPanel { }) .await?; + let mut scopes: HashMap> = HashMap::new(); + let mut variables: HashMap> = + HashMap::new(); + + for stack_frame in res.stack_frames.clone().into_iter() { + let scope_response = client + .request::(ScopesArguments { + frame_id: stack_frame.id, + }) + .await?; + + scopes.insert( + stack_frame.id, + scope_response.scopes.clone(), + ); + + for scope in scope_response.scopes { + variables.insert( + scope.variables_reference, + client + .request::(VariablesArguments { + variables_reference: scope + .variables_reference, + filter: None, + start: None, + count: None, + format: None, + }) + .await? + .variables, + ); + } + } + this.update(&mut cx, |this, cx| { if let Some(entry) = this.thread_state.get_mut(&thread_id) { - entry.stack_frames = Some(res.stack_frames); + this.current_thread_id = Some(thread_id); + + this.current_stack_frame_id = + res.stack_frames.clone().first().map(|f| f.id); + + let mut stack_frames = Vec::new(); + + for stack_frame in res.stack_frames.clone() { + stack_frames.push(stack_frame); + } + + entry.stack_frames = stack_frames; + entry.scopes = scopes; + entry.variables = variables; + + this.stack_frame_list + .reset(entry.stack_frames.len()); cx.notify(); } @@ -84,13 +141,20 @@ impl DebugPanel { if event.reason == ThreadEventReason::Started { this.thread_state.insert( event.thread_id, - ThreadState { stack_frames: None }, + ThreadState { + ..Default::default() + }, ); - this.thread_id = Some(event.thread_id); + this.current_thread_id = Some(event.thread_id); } else { - this.thread_id = None; + if this.current_thread_id == Some(event.thread_id) { + this.current_thread_id = None; + } + this.stack_frame_list.reset(0); this.thread_state.remove(&event.thread_id); } + + cx.notify(); } Events::Output(_) => todo!(), Events::Breakpoint(_) => todo!(), @@ -108,6 +172,18 @@ impl DebugPanel { } })]; + let view = cx.view().downgrade(); + let stack_frame_list = + ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { + if let Some(view) = view.upgrade() { + view.update(cx, |view, cx| { + view.render_stack_frame(ix, cx).into_any_element() + }) + } else { + div().into_any() + } + }); + Self { position: DockPosition::Bottom, zoomed: false, @@ -115,9 +191,11 @@ impl DebugPanel { focus_handle: cx.focus_handle(), size: px(300.), _subscriptions, - thread_id: Some(1), + current_thread_id: None, + current_stack_frame_id: None, workspace: workspace.clone(), thread_state: Default::default(), + stack_frame_list, } }) } @@ -129,6 +207,17 @@ impl DebugPanel { cx.spawn(|mut cx| async move { cx.update(|cx| DebugPanel::new(workspace, cx)) }) } + fn stack_frame_for_index(&self, ix: usize) -> &StackFrame { + &self + .current_thread_id + .and_then(|id| { + self.thread_state + .get(&id) + .and_then(|state| state.stack_frames.get(ix)) + }) + .unwrap() + } + fn debug_adapter(&self, cx: &mut ViewContext) -> Arc { self.workspace .update(cx, |this, cx| { @@ -141,35 +230,25 @@ impl DebugPanel { .unwrap() } - fn render_stack_frames(&self, cx: &mut ViewContext) -> impl IntoElement { - let Some(thread_state) = self.thread_id.and_then(|t| self.thread_state.get(&t)) else { - return div().child("No information for this thread yet").into_any(); - }; - - let Some(stack_frames) = &thread_state.stack_frames else { - return div() - .child("No stack frames for this thread yet") - .into_any(); - }; - - div() + fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { + v_flex() + .w_full() .gap_3() - .children( - stack_frames - .iter() - .map(|frame| self.render_stack_frame(frame, cx)), - ) + .h_full() + .flex_grow() + .flex_shrink_0() + .child(list(self.stack_frame_list.clone()).size_full()) .into_any() } - fn render_stack_frame( - &self, - stack_frame: &StackFrame, - cx: &mut ViewContext, - ) -> impl IntoElement { + fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { + let stack_frame = self.stack_frame_for_index(ix); + let source = stack_frame.source.clone(); - div() + v_flex() + .rounded_md() + .group("") .id(("stack-frame", stack_frame.id)) .p_1() .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) @@ -185,12 +264,94 @@ impl DebugPanel { )), ) .child( - div() + h_flex() .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), ) .into_any() } + + fn render_scopes( + &self, + thread_state: &ThreadState, + cx: &mut ViewContext, + ) -> impl IntoElement { + let Some(scopes) = self + .current_stack_frame_id + .and_then(|id| thread_state.scopes.get(&id)) + else { + return div().child("No scopes for this thread yet").into_any(); + }; + + div() + .gap_3() + .text_ui_sm(cx) + .children( + scopes + .iter() + .map(|scope| self.render_scope(thread_state, scope, cx)), + ) + .into_any() + } + + fn render_scope( + &self, + thread_state: &ThreadState, + scope: &Scope, + cx: &mut ViewContext, + ) -> impl IntoElement { + div() + .id(("scope", scope.variables_reference)) + .p_1() + .text_ui_sm(cx) + .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .child(scope.name.clone()) + .child( + div() + .ml_2() + .child(self.render_variables(thread_state, scope, cx)), + ) + .into_any() + } + + fn render_variables( + &self, + thread_state: &ThreadState, + scope: &Scope, + cx: &mut ViewContext, + ) -> impl IntoElement { + let Some(variables) = thread_state.variables.get(&scope.variables_reference) else { + return div().child("No variables for this thread yet").into_any(); + }; + + div() + .gap_3() + .text_ui_sm(cx) + .children( + variables + .iter() + .map(|variable| self.render_variable(variable, cx)), + ) + .into_any() + } + + fn render_variable(&self, variable: &Variable, cx: &mut ViewContext) -> impl IntoElement { + h_flex() + .id(("variable", variable.variables_reference)) + .p_1() + .gap_1() + .text_ui_sm(cx) + .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .child(variable.name.clone()) + .child( + div() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .child(variable.value.clone()), + ) + .into_any() + } } impl EventEmitter for DebugPanel {} @@ -259,7 +420,8 @@ impl Panel for DebugPanel { impl Render for DebugPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() + v_flex() + .items_start() .child( h_flex() .p_2() @@ -268,7 +430,7 @@ impl Render for DebugPanel { IconButton::new("debug-continue", IconName::DebugContinue) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); - if let Some(thread_id) = view.thread_id { + if let Some(thread_id) = view.current_thread_id { cx.background_executor() .spawn(async move { client.resume(thread_id).await }) .detach(); @@ -278,6 +440,14 @@ impl Render for DebugPanel { ) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) + .on_click(cx.listener(|view, _, cx| { + let client = view.debug_adapter(cx); + if let Some(thread_id) = view.current_thread_id { + cx.background_executor() + .spawn(async move { client.step_over(thread_id).await }) + .detach(); + } + })) .tooltip(move |cx| Tooltip::text("Step over", cx)), ) .child( @@ -285,7 +455,7 @@ impl Render for DebugPanel { .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); - if let Some(thread_id) = view.thread_id { + if let Some(thread_id) = view.current_thread_id { cx.background_executor() .spawn(async move { client.step_in(thread_id).await }) .detach(); @@ -297,7 +467,7 @@ impl Render for DebugPanel { IconButton::new("debug-go-out", IconName::DebugStepOut) .on_click(cx.listener(|view, _, cx| { let client = view.debug_adapter(cx); - if let Some(thread_id) = view.thread_id { + if let Some(thread_id) = view.current_thread_id { cx.background_executor() .spawn(async move { client.step_out(thread_id).await }) .detach(); @@ -315,10 +485,14 @@ impl Render for DebugPanel { ), ) .child( - h_flex() - .gap_4() - .child(self.render_stack_frames(cx)) - .child("Here see all the vars"), + h_flex().size_full().items_start().p_1().gap_4().when_some( + self.current_thread_id + .and_then(|t| self.thread_state.get(&t)), + |this, thread_state| { + this.child(self.render_stack_frames(cx)) + .child(self.render_scopes(thread_state, cx)) + }, + ), ) .into_any() } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4dd80200721335..e8bf46cd77e774 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1085,12 +1085,14 @@ impl Project { "/Users/remcosmits/Documents/code/symfony_demo".into(), &mut cx, move |event, cx| { - this2.update(cx, |_, cx| { - cx.emit(Event::DebugClientEvent { - client_id: id, - event, + this2 + .update(cx, |_, cx| { + cx.emit(Event::DebugClientEvent { + client_id: id, + event, + }) }) - }).log_err(); + .log_err(); }, ) .await @@ -1155,7 +1157,7 @@ impl Project { { breakpoints_for_buffer.remove(ix); } else { - breakpoints_for_buffer.push(Breakpoint { row }); + breakpoints_for_buffer.push(Breakpoint { row: row + 1 }); } let clients = self From 73a68d560f4a12c088eff3572256857831fb67d2 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 27 Jun 2024 14:31:50 -0400 Subject: [PATCH 036/650] Change abs_path to path in project update worktree settings --- crates/project/src/project.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8cb22fa5603b87..5c4f68801f5900 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8437,7 +8437,7 @@ impl Project { ); } }) - } else if abs_path.ends_with(local_debug_file_relative_path()) { + } else if path.ends_with(local_debug_file_relative_path()) { // TODO: handle local launch file (.zed/debug.json) self.task_inventory().update(cx, |task_inventory, cx| { if removed { @@ -8460,7 +8460,7 @@ impl Project { ); } }); - } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) { + } else if path.ends_with(local_vscode_launch_file_relative_path()) { // TODO: handle vscode launch file (.vscode/launch.json) } } From 3d7cd5dac7ae836871469d36be2dfc3a9a8e6891 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 27 Jun 2024 22:00:01 +0200 Subject: [PATCH 037/650] Remove todo and change casing --- crates/project/src/project.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5c4f68801f5900..70ffb5bc70be2f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8438,7 +8438,6 @@ impl Project { } }) } else if path.ends_with(local_debug_file_relative_path()) { - // TODO: handle local launch file (.zed/debug.json) self.task_inventory().update(cx, |task_inventory, cx| { if removed { task_inventory.remove_local_static_source(&abs_path); @@ -8451,7 +8450,7 @@ impl Project { TaskSourceKind::Worktree { id: remote_worktree_id, abs_path, - id_base: "local_debug_File_for_worktree".into(), + id_base: "local_debug_file_for_worktree".into(), }, |tx, cx| { StaticSource::new(TrackedFile::new(debug_task_file_rx, tx, cx)) From ddd893a7952252cc57273e58506f57fd17e8ce52 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 28 Jun 2024 00:52:15 -0400 Subject: [PATCH 038/650] Clean up debugger inventory files/code --- crates/dap/src/config_templates.rs | 47 -------- crates/dap/src/lib.rs | 1 - crates/debugger_ui/src/lib.rs | 21 ---- crates/debugger_ui/src/modal.rs | 136 ----------------------- crates/project/src/debugger_inventory.rs | 115 ------------------- crates/project/src/project.rs | 11 -- 6 files changed, 331 deletions(-) delete mode 100644 crates/dap/src/config_templates.rs delete mode 100644 crates/debugger_ui/src/modal.rs delete mode 100644 crates/project/src/debugger_inventory.rs diff --git a/crates/dap/src/config_templates.rs b/crates/dap/src/config_templates.rs deleted file mode 100644 index 9297ae5c761b12..00000000000000 --- a/crates/dap/src/config_templates.rs +++ /dev/null @@ -1,47 +0,0 @@ -use schemars::{gen::SchemaSettings, JsonSchema}; -use serde::{Deserialize, Serialize}; - -/// A template definition of a Zed task to run. -/// May use the [`VariableName`] to get the corresponding substitutions into its fields. -/// -/// Template itself is not ready to spawn a task, it needs to be resolved with a [`TaskContext`] first, that -/// contains all relevant Zed state in task variables. -/// A single template may produce different tasks (or none) for different contexts. -#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct DebuggerConfigTemplate { - #[serde(rename = "type")] - pub _type: String, - pub request: String, - #[serde(default)] - pub args: Vec, -} - -impl DebuggerConfigTemplate { - /// Generates JSON schema of Tasks JSON template format. - pub fn generate_json_schema() -> serde_json_lenient::Value { - let schema = SchemaSettings::draft07() - .with(|settings| settings.option_add_null_type = false) - .into_generator() - .into_root_schema_for::(); - - serde_json_lenient::to_value(schema).unwrap() - } -} - -/// [`VsCodeTaskFile`] is a superset of Code's task definition format. -#[derive(Debug, Deserialize, PartialEq)] -pub struct ZedDebugConfigFile { - debugger_configs: Vec, -} - -// impl TryFrom for DebuggerConfigTemplate { -// type Error = anyhow::Error; - -// fn try_from(value: ZedDebugConfigFile) -> Result { - -// let templates = value -// . -// Ok(Self(templates)) -// } -// } diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index 44a69c12b50367..abf46905c0c3d2 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,4 +1,3 @@ pub mod client; -pub mod config_templates; pub mod transport; pub use dap_types::*; diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index cf77096378c064..da5739ea085e45 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,10 +1,8 @@ use debugger_panel::{DebugPanel, TogglePanel}; use gpui::{AppContext, Task, ViewContext}; -use modal::DebuggerSelectModal; use workspace::{StartDebugger, Workspace}; pub mod debugger_panel; -pub mod modal; pub fn init(cx: &mut AppContext) { cx.observe_new_views( @@ -24,22 +22,3 @@ pub fn init(cx: &mut AppContext) { ) .detach(); } - -fn select_debugger( - workspace: &mut Workspace, - _: &StartDebugger, - cx: &mut ViewContext, -) -> Task<()> { - let project = workspace.project().clone(); - let workspace_handle = workspace.weak_handle(); - - cx.spawn(|workspace, mut cx| async move { - workspace - .update(&mut cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx| { - DebuggerSelectModal::new(project, workspace_handle, cx) - }) - }) - .ok(); - }) -} diff --git a/crates/debugger_ui/src/modal.rs b/crates/debugger_ui/src/modal.rs deleted file mode 100644 index 1a5140e0a2dc35..00000000000000 --- a/crates/debugger_ui/src/modal.rs +++ /dev/null @@ -1,136 +0,0 @@ -use fuzzy::StringMatch; -use gpui::{ - rems, DismissEvent, EventEmitter, FocusableView, Model, Render, Subscription, - Task as AsyncTask, View, ViewContext, WeakView, -}; -use picker::{Picker, PickerDelegate}; -use project::Project; -use std::sync::Arc; -use ui::{prelude::*, ListItem, ListItemSpacing}; -use workspace::{ModalView, Workspace}; - -use crate::debugger_panel::DebugPanel; - -pub struct DebuggerSelectModal { - picker: View>, - _subscription: Subscription, -} - -impl DebuggerSelectModal { - pub fn new( - _project: Model, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let picker = - cx.new_view(|cx| Picker::uniform_list(DebuggerModelDelegate::new(workspace), cx)); - - let _subscription = cx.subscribe(&picker, |_, _, _, cx| { - cx.emit(DismissEvent); - }); - - Self { - picker, - _subscription, - } - } -} - -impl ModalView for DebuggerSelectModal {} - -impl EventEmitter for DebuggerSelectModal {} - -impl FocusableView for DebuggerSelectModal { - fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { - self.picker.read(cx).focus_handle(cx) - } -} - -impl Render for DebuggerSelectModal { - fn render(&mut self, _: &mut ViewContext) -> impl gpui::prelude::IntoElement { - v_flex() - .id("DebuggerSelectModel") - .key_context("DebuggerSelectModel") - .w(rems(34.)) - .child(self.picker.clone()) - } -} - -struct DebuggerModelDelegate { - matches: Vec, - selected_index: usize, - workspace: WeakView, - placeholder_text: Arc, -} - -impl DebuggerModelDelegate { - fn new(workspace: WeakView) -> Self { - Self { - workspace, - matches: vec![StringMatch { - candidate_id: 0, - score: 1.0, - positions: vec![0], - string: String::from("Mock debugger config"), - }], - selected_index: 0, - placeholder_text: Arc::from("Select & Start a debugger config"), - } - } -} - -impl PickerDelegate for DebuggerModelDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { - self.selected_index = ix; - } - - fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> std::sync::Arc { - self.placeholder_text.clone() - } - - fn update_matches( - &mut self, - query: String, - cx: &mut ViewContext>, - ) -> gpui::Task<()> { - AsyncTask::Ready(Some(())) - } - - fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { - self.workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| {}); - - workspace.focus_panel::(cx); - }); - - cx.emit(DismissEvent); - } - - fn dismissed(&mut self, cx: &mut ViewContext>) { - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - cx: &mut ViewContext>, - ) -> Option { - Some( - ListItem::new(SharedString::from("ajklsdf")) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .child(self.matches[ix].string.clone()), - ) - } -} diff --git a/crates/project/src/debugger_inventory.rs b/crates/project/src/debugger_inventory.rs deleted file mode 100644 index 0a79b53efc246a..00000000000000 --- a/crates/project/src/debugger_inventory.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::{ - borrow::Cow, - collections::VecDeque, - path::{Path, PathBuf}, - sync::Arc, -}; - -use dap::config_templates::DebuggerConfigTemplate; -use futures::{ - channel::mpsc::{unbounded, UnboundedSender}, - StreamExt, -}; -use gpui::{AppContext, Context, Model, ModelContext, Task}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use task::static_source::TrackedFile; - -pub struct DebuggerInventory { - sources: Vec, - update_sender: UnboundedSender<()>, - _update_pooler: Task>, -} - -impl DebuggerInventory { - pub fn new(cx: &mut AppContext) -> Model { - cx.new_model(|cx| { - let (update_sender, mut rx) = unbounded(); - let _update_pooler = cx.spawn(|this, mut cx| async move { - while let Some(()) = rx.next().await { - this.update(&mut cx, |_, cx| { - cx.notify(); - })?; - } - Ok(()) - }); - Self { - sources: Vec::new(), - update_sender, - _update_pooler, - } - }) - } - - pub fn remove_source(&mut self, abs_path: &PathBuf) { - todo!(); - } - - pub fn add_source( - &mut self, - kind: DebuggerConfigSourceKind, - create_source: impl FnOnce(UnboundedSender<()>, &mut AppContext) -> StaticSource, - cx: &mut ModelContext, - ) { - let abs_path = kind.abs_path(); - if abs_path.is_some() { - if let Some(a) = self.sources.iter().find(|s| s.kind.abs_path() == abs_path) { - log::debug!("Source for path {abs_path:?} already exists, not adding. Old kind: {OLD_KIND:?}, new kind: {kind:?}", OLD_KIND = a.kind); - return; - } - } - - let source = create_source(self.update_sender.clone(), cx); - let source = SourceInInventory { source, kind }; - self.sources.push(source); - cx.notify(); - } -} - -/// Kind of a source the tasks are fetched from, used to display more source information in the UI. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum DebuggerConfigSourceKind { - /// Tasks from the worktree's .zed/task.json - Worktree { - id: usize, - abs_path: PathBuf, - id_base: Cow<'static, str>, - }, - /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path - AbsPath { - id_base: Cow<'static, str>, - abs_path: PathBuf, - }, - /// Languages-specific tasks coming from extensions. - Language { name: Arc }, -} - -impl DebuggerConfigSourceKind { - fn abs_path(&self) -> Option<&Path> { - match self { - Self::AbsPath { abs_path, .. } | Self::Worktree { abs_path, .. } => Some(abs_path), - Self::Language { .. } => None, - } - } -} - -struct SourceInInventory { - source: StaticSource, - kind: DebuggerConfigSourceKind, -} - -pub struct StaticSource { - configs: TrackedFile, -} - -impl StaticSource { - pub fn new(debugger_configs: TrackedFile) -> Self { - Self { - configs: debugger_configs, - } - } -} - -/// A group of Tasks defined in a JSON file. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct DebuggerConfigTemplates(pub Vec); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 70ffb5bc70be2f..ef97aa1f8c5f59 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,6 +1,5 @@ pub mod connection_manager; pub mod debounced_delay; -mod debugger_inventory; pub mod lsp_command; pub mod lsp_ext_command; mod prettier_support; @@ -27,7 +26,6 @@ use dap::{ SourceBreakpoint, }; use debounced_delay::DebouncedDelay; -use debugger_inventory::{DebuggerConfigSourceKind, DebuggerInventory}; use futures::{ channel::{ mpsc::{self, UnboundedReceiver}, @@ -236,7 +234,6 @@ pub struct Project { prettiers_per_worktree: HashMap>>, prettier_instances: HashMap, tasks: Model, - debugger_configs: Model, hosted_project_id: Option, dev_server_project_id: Option, search_history: SearchHistory, @@ -744,7 +741,6 @@ impl Project { cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) .detach(); let tasks = Inventory::new(cx); - let debugger_configs = DebuggerInventory::new(cx); Self { worktrees: Vec::new(), @@ -802,7 +798,6 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), tasks, - debugger_configs, hosted_project_id: None, dev_server_project_id: None, search_history: Self::new_search_history(), @@ -870,7 +865,6 @@ impl Project { let this = cx.new_model(|cx| { let replica_id = response.payload.replica_id as ReplicaId; let tasks = Inventory::new(cx); - let debugger_configs = DebuggerInventory::new(cx); // BIG CAUTION NOTE: The order in which we initialize fields here matters and it should match what's done in Self::local. // Otherwise, you might run into issues where worktree id on remote is different than what's on local host. // That's because Worktree's identifier is entity id, which should probably be changed. @@ -965,7 +959,6 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), tasks, - debugger_configs, hosted_project_id: None, dev_server_project_id: response .payload @@ -1520,10 +1513,6 @@ impl Project { &self.tasks } - pub fn debugger_configs(&self) -> &Model { - &self.debugger_configs - } - pub fn search_history(&self) -> &SearchHistory { &self.search_history } From 01d384e676797a86dec1ef4bad78cfbb82aa8780 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 29 Jun 2024 15:29:08 +0200 Subject: [PATCH 039/650] Add missing event body mapping --- crates/dap/src/transport.rs | 11 +++++------ crates/debugger_ui/src/debugger_panel.rs | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 4a2615dad15583..ab938bfd76186c 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ BreakpointEvent, CapabilitiesEvent, ContinuedEvent, ExitedEvent, InvalidatedEvent, - LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent, StoppedEvent, - TerminatedEvent, ThreadEvent, + LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent, ProgressEndEvent, + ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent, ThreadEvent, }; use futures::{ channel::mpsc::{unbounded, Sender, UnboundedReceiver, UnboundedSender}, @@ -40,9 +40,9 @@ pub enum Events { LoadedSource(LoadedSourceEvent), Process(ProcessEvent), Capabilities(CapabilitiesEvent), - ProgressStart, - ProgressUpdate, - ProgressEnd, + ProgressStart(ProgressStartEvent), + ProgressUpdate(ProgressUpdateEvent), + ProgressEnd(ProgressEndEvent), Invalidated(InvalidatedEvent), Memory(MemoryEvent), } @@ -201,7 +201,6 @@ impl Transport { Err(_) => (), }, None => { - dbg!("Response to nonexistent request #{}", res.request_seq); client_tx.send(Payload::Response(res)).await.log_err(); } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c6645978827aa3..77f9542664c792 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -163,9 +163,9 @@ impl DebugPanel { Events::Capabilities(_) => todo!(), Events::Memory(_) => todo!(), Events::Process(_) => todo!(), - Events::ProgressEnd => todo!(), - Events::ProgressStart => todo!(), - Events::ProgressUpdate => todo!(), + Events::ProgressEnd(_) => todo!(), + Events::ProgressStart(_) => todo!(), + Events::ProgressUpdate(_) => todo!(), Events::Invalidated(_) => todo!(), } } From 854ff68bac10967a9b67b3605fd8d77e8033e089 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 29 Jun 2024 16:38:43 +0200 Subject: [PATCH 040/650] Clean up transport code --- crates/dap/src/transport.rs | 38 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index ab938bfd76186c..74f0abdc71cb37 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -80,20 +80,18 @@ impl Transport { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); - let transport = Self { + let transport = Arc::new(Self { pending_requests: Mutex::new(HashMap::default()), - }; - - let transport = Arc::new(transport); + }); let _ = cx.update(|cx| { - cx.spawn(|_| Self::recv(transport.clone(), server_stdout, client_tx)) + cx.spawn(|_| Self::receive(transport.clone(), server_stdout, client_tx)) .detach_and_log_err(cx); cx.spawn(|_| Self::send(transport, server_stdin, client_rx)) .detach_and_log_err(cx); if let Some(stderr) = server_stderr { - cx.spawn(|_| Self::err(stderr)).detach(); + cx.spawn(|_| Self::err(stderr)).detach_and_log_err(cx); } }); @@ -167,8 +165,6 @@ impl Transport { server_stdin: &mut Box, request: String, ) -> Result<()> { - dbg!("Request {}", &request); - server_stdin .write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes()) .await?; @@ -195,15 +191,11 @@ impl Transport { ) -> Result<()> { match msg { Payload::Response(res) => { - match self.pending_requests.lock().remove(&res.request_seq) { - Some(mut tx) => match tx.send(Self::process_response(res)).await { - Ok(_) => (), - Err(_) => (), - }, - None => { - client_tx.send(Payload::Response(res)).await.log_err(); - } - } + if let Some(mut tx) = self.pending_requests.lock().remove(&res.request_seq) { + tx.send(Self::process_response(res)).await?; + } else { + client_tx.send(Payload::Response(res)).await?; + }; Ok(()) } @@ -218,7 +210,7 @@ impl Transport { } } - async fn recv( + async fn receive( transport: Arc, mut server_stdout: Box, client_tx: UnboundedSender, @@ -248,16 +240,10 @@ impl Transport { Ok(()) } - async fn err(mut server_stderr: Box) { + async fn err(mut server_stderr: Box) -> Result<()> { let mut recv_buffer = String::new(); loop { - match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await { - Ok(_) => {} - Err(err) => { - dbg!("err: <- {:?}", err); - break; - } - } + Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await?; } } } From 47a5f0c62068c1bbeca8bfbfa11997160fc007a3 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 29 Jun 2024 16:47:27 +0200 Subject: [PATCH 041/650] Start wiring through custom launch data --- crates/dap/src/client.rs | 10 ++++------ crates/project/src/project.rs | 3 ++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 0ead30b8bcfac0..010a2f0154d222 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -15,7 +15,7 @@ use futures::{ AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; use gpui::{AppContext, AsyncAppContext}; -use serde_json::json; +use serde_json::Value; use smol::{ io::BufReader, net::TcpStream, @@ -244,11 +244,9 @@ impl DebugAdapterClient { Ok(capabilities) } - pub async fn launch(&self) -> Result<()> { - self.request::(LaunchRequestArguments { - raw: json!({"noDebug": false}), - }) - .await + pub async fn launch(&self, custom: Value) -> Result<()> { + self.request::(LaunchRequestArguments { raw: custom }) + .await } pub async fn resume(&self, thread_id: u64) { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 70ffb5bc70be2f..22ef2ab9c95a68 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -86,6 +86,7 @@ use rpc::{ErrorCode, ErrorExt as _}; use search::SearchQuery; use search_history::SearchHistory; use serde::Serialize; +use serde_json::json; use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; @@ -1117,7 +1118,7 @@ impl Project { client.configuration_done().await.log_err()?; // launch/attach request - client.launch().await.log_err()?; + client.launch(json!({"noDebug": false})).await.log_err()?; let client = Arc::new(client); From 8699dad0e3f5435f11e0bc5844ee6eccdbadf0a4 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 1 Jul 2024 12:43:57 +0200 Subject: [PATCH 042/650] Add restart & pause method & log request errors --- crates/dap/src/client.rs | 128 ++++++++++++++++++++++----------------- 1 file changed, 74 insertions(+), 54 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 010a2f0154d222..ea86ff02774db1 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -3,12 +3,13 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ - ConfigurationDone, Continue, Initialize, Launch, Next, SetBreakpoints, StepBack, StepIn, - StepOut, + ConfigurationDone, Continue, Initialize, Launch, Next, Pause, SetBreakpoints, StepBack, + StepIn, StepOut, }, ConfigurationDoneArguments, ContinueArguments, InitializeRequestArgumentsPathFormat, - LaunchRequestArguments, NextArguments, SetBreakpointsArguments, SetBreakpointsResponse, Source, - SourceBreakpoint, StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, + LaunchRequestArguments, NextArguments, PauseArguments, SetBreakpointsArguments, + SetBreakpointsResponse, Source, SourceBreakpoint, StepBackArguments, StepInArguments, + StepOutArguments, SteppingGranularity, }; use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, @@ -250,68 +251,87 @@ impl DebugAdapterClient { } pub async fn resume(&self, thread_id: u64) { - let _ = self - .request::(ContinueArguments { - thread_id, - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), - }) - .await; + self.request::(ContinueArguments { + thread_id, + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), + }) + .await + .log_err(); } pub async fn step_over(&self, thread_id: u64) { - let _ = self - .request::(NextArguments { - thread_id, - granularity: Some(SteppingGranularity::Statement), - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), - }) - .await; + self.request::(NextArguments { + thread_id, + granularity: Some(SteppingGranularity::Statement), + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), + }) + .await + .log_err(); } pub async fn step_in(&self, thread_id: u64) { - let _ = self - .request::(StepInArguments { - thread_id, - target_id: None, - granularity: Some(SteppingGranularity::Statement), - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), - }) - .await; + self.request::(StepInArguments { + thread_id, + target_id: None, + granularity: Some(SteppingGranularity::Statement), + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), + }) + .await + .log_err(); } pub async fn step_out(&self, thread_id: u64) { - let _ = self - .request::(StepOutArguments { - thread_id, - granularity: Some(SteppingGranularity::Statement), - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), - }) - .await; + self.request::(StepOutArguments { + thread_id, + granularity: Some(SteppingGranularity::Statement), + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), + }) + .await + .log_err(); } pub async fn step_back(&self, thread_id: u64) { - let _ = self - .request::(StepBackArguments { - thread_id, - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), - granularity: Some(SteppingGranularity::Statement), - }) - .await; + self.request::(StepBackArguments { + thread_id, + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), + granularity: Some(SteppingGranularity::Statement), + }) + .await + .log_err(); + } + + pub async fn restart(&self, thread_id: u64) { + self.request::(StepBackArguments { + thread_id, + single_thread: self + .capabilities + .clone() + .and_then(|c| c.supports_single_thread_execution_requests), + granularity: Some(SteppingGranularity::Statement), + }) + .await + .log_err(); + } + + pub async fn pause(&self, thread_id: u64) { + self.request::(PauseArguments { thread_id }) + .await + .log_err(); } pub async fn set_breakpoints( From 2ea1e4fa85f59652bfc8ef9c47e2459ced63b6bb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 1 Jul 2024 12:45:08 +0200 Subject: [PATCH 043/650] Add debug panel actions --- crates/debugger_ui/src/debugger_panel.rs | 134 ++++++++++++++++------- 1 file changed, 96 insertions(+), 38 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 77f9542664c792..4f6f6be6ac9bca 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -16,7 +16,18 @@ use workspace::{ Workspace, }; -actions!(debug, [TogglePanel]); +actions!( + debug_panel, + [ + TogglePanel, + Continue, + StepOver, + StepIn, + StepOut, + Restart, + Pause + ] +); #[derive(Default)] struct ThreadState { @@ -352,6 +363,60 @@ impl DebugPanel { ) .into_any() } + + fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { + let client = self.debug_adapter(cx); + if let Some(thread_id) = self.current_thread_id { + cx.background_executor() + .spawn(async move { client.resume(thread_id).await }) + .detach(); + } + } + + fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext) { + let client = self.debug_adapter(cx); + if let Some(thread_id) = self.current_thread_id { + cx.background_executor() + .spawn(async move { client.step_over(thread_id).await }) + .detach(); + } + } + + fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext) { + let client = self.debug_adapter(cx); + if let Some(thread_id) = self.current_thread_id { + cx.background_executor() + .spawn(async move { client.step_in(thread_id).await }) + .detach(); + } + } + + fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext) { + let client = self.debug_adapter(cx); + if let Some(thread_id) = self.current_thread_id { + cx.background_executor() + .spawn(async move { client.step_out(thread_id).await }) + .detach(); + } + } + + fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext) { + let client = self.debug_adapter(cx); + if let Some(thread_id) = self.current_thread_id { + cx.background_executor() + .spawn(async move { client.restart(thread_id).await }) + .detach(); + } + } + + fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext) { + let client = self.debug_adapter(cx); + if let Some(thread_id) = self.current_thread_id { + cx.background_executor() + .spawn(async move { client.pause(thread_id).await }) + .detach(); + } + } } impl EventEmitter for DebugPanel {} @@ -421,6 +486,14 @@ impl Panel for DebugPanel { impl Render for DebugPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() + .key_context("DebugPanel") + .track_focus(&self.focus_handle) + .capture_action(cx.listener(Self::handle_continue_action)) + .capture_action(cx.listener(Self::handle_step_over_action)) + .capture_action(cx.listener(Self::handle_step_in_action)) + .capture_action(cx.listener(Self::handle_step_out_action)) + .capture_action(cx.listener(Self::handle_restart_action)) + .capture_action(cx.listener(Self::handle_pause_action)) .items_start() .child( h_flex() @@ -428,60 +501,45 @@ impl Render for DebugPanel { .gap_2() .child( IconButton::new("debug-continue", IconName::DebugContinue) - .on_click(cx.listener(|view, _, cx| { - let client = view.debug_adapter(cx); - if let Some(thread_id) = view.current_thread_id { - cx.background_executor() - .spawn(async move { client.resume(thread_id).await }) - .detach(); - } - })) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(Continue.boxed_clone())), + ) .tooltip(move |cx| Tooltip::text("Continue debug", cx)), ) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) - .on_click(cx.listener(|view, _, cx| { - let client = view.debug_adapter(cx); - if let Some(thread_id) = view.current_thread_id { - cx.background_executor() - .spawn(async move { client.step_over(thread_id).await }) - .detach(); - } - })) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(StepOver.boxed_clone())), + ) .tooltip(move |cx| Tooltip::text("Step over", cx)), ) .child( - IconButton::new("debug-go-in", IconName::DebugStepInto) - .on_click(cx.listener(|view, _, cx| { - let client = view.debug_adapter(cx); - - if let Some(thread_id) = view.current_thread_id { - cx.background_executor() - .spawn(async move { client.step_in(thread_id).await }) - .detach(); - } - })) + IconButton::new("debug-step-in", IconName::DebugStepInto) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(StepIn.boxed_clone())), + ) .tooltip(move |cx| Tooltip::text("Go in", cx)), ) .child( - IconButton::new("debug-go-out", IconName::DebugStepOut) - .on_click(cx.listener(|view, _, cx| { - let client = view.debug_adapter(cx); - if let Some(thread_id) = view.current_thread_id { - cx.background_executor() - .spawn(async move { client.step_out(thread_id).await }) - .detach(); - } - })) + IconButton::new("debug-step-out", IconName::DebugStepOut) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(StepOut.boxed_clone())), + ) .tooltip(move |cx| Tooltip::text("Go out", cx)), ) .child( IconButton::new("debug-restart", IconName::DebugRestart) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(Restart.boxed_clone())), + ) .tooltip(move |cx| Tooltip::text("Restart", cx)), ) .child( - IconButton::new("debug-stop", IconName::DebugStop) - .tooltip(move |cx| Tooltip::text("Stop", cx)), + IconButton::new("debug-pause", IconName::DebugStop) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(Pause.boxed_clone())), + ) + .tooltip(move |cx| Tooltip::text("Pause", cx)), ), ) .child( From 09aabe481c444abb659aa5936826f866fa08b5b9 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 1 Jul 2024 16:09:06 +0200 Subject: [PATCH 044/650] Undo wrong request value --- crates/dap/src/client.rs | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index ea86ff02774db1..09ce52ba70f20a 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -253,10 +253,7 @@ impl DebugAdapterClient { pub async fn resume(&self, thread_id: u64) { self.request::(ContinueArguments { thread_id, - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), + single_thread: None, }) .await .log_err(); @@ -266,10 +263,7 @@ impl DebugAdapterClient { self.request::(NextArguments { thread_id, granularity: Some(SteppingGranularity::Statement), - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), + single_thread: None, }) .await .log_err(); @@ -280,10 +274,7 @@ impl DebugAdapterClient { thread_id, target_id: None, granularity: Some(SteppingGranularity::Statement), - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), + single_thread: None, }) .await .log_err(); @@ -293,10 +284,7 @@ impl DebugAdapterClient { self.request::(StepOutArguments { thread_id, granularity: Some(SteppingGranularity::Statement), - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), + single_thread: None, }) .await .log_err(); @@ -305,10 +293,7 @@ impl DebugAdapterClient { pub async fn step_back(&self, thread_id: u64) { self.request::(StepBackArguments { thread_id, - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), + single_thread: None, granularity: Some(SteppingGranularity::Statement), }) .await @@ -318,10 +303,7 @@ impl DebugAdapterClient { pub async fn restart(&self, thread_id: u64) { self.request::(StepBackArguments { thread_id, - single_thread: self - .capabilities - .clone() - .and_then(|c| c.supports_single_thread_execution_requests), + single_thread: None, granularity: Some(SteppingGranularity::Statement), }) .await From 7936a4bee39a4857cf122d147e2b5c684362fe8b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 10:45:41 +0200 Subject: [PATCH 045/650] Fix panic when denormalizing Initialized event with body --- crates/dap/src/transport.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 74f0abdc71cb37..8c7538e1825a62 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -1,8 +1,9 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ - BreakpointEvent, CapabilitiesEvent, ContinuedEvent, ExitedEvent, InvalidatedEvent, - LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent, ProgressEndEvent, - ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent, ThreadEvent, + BreakpointEvent, Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, + InvalidatedEvent, LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent, + ProgressEndEvent, ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent, + ThreadEvent, }; use futures::{ channel::mpsc::{unbounded, Sender, UnboundedReceiver, UnboundedSender}, @@ -28,7 +29,7 @@ pub enum Payload { #[serde(tag = "event", content = "body")] #[serde(rename_all = "camelCase")] pub enum Events { - Initialized, + Initialized(Option), Stopped(StoppedEvent), Continued(ContinuedEvent), Exited(ExitedEvent), From 3dd769be947499d1807e90cdf78f3ef750e6ac35 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 10:46:12 +0200 Subject: [PATCH 046/650] Fix panic denormalizing Terminated event without body --- crates/dap/src/transport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 8c7538e1825a62..6d8bcd60a64493 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -33,7 +33,7 @@ pub enum Events { Stopped(StoppedEvent), Continued(ContinuedEvent), Exited(ExitedEvent), - Terminated(TerminatedEvent), + Terminated(Option), Thread(ThreadEvent), Output(OutputEvent), Breakpoint(BreakpointEvent), From 3e022a5565f7954bc353637ed75438afd5216694 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 10:46:52 +0200 Subject: [PATCH 047/650] Change error message when non-success response came back --- crates/dap/src/transport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 6d8bcd60a64493..4cbf105c185f60 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -181,7 +181,7 @@ impl Transport { if response.success { Ok(response) } else { - Err(anyhow!("some error")) + Err(anyhow!("Received failed response")) } } From 1baa5aea94f9645dd0fc32bb34bd0858d2a79c33 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 10:57:49 +0200 Subject: [PATCH 048/650] Fix missing match arg for Initialized event --- crates/debugger_ui/src/debugger_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 4f6f6be6ac9bca..0b1ed2db3cdb63 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -65,7 +65,7 @@ impl DebugPanel { if let project::Event::DebugClientEvent { client_id, event } = event { match event { - Events::Initialized => return, + Events::Initialized(_) => return, Events::Stopped(event) => { if let Some(thread_id) = event.thread_id { let client = this.debug_adapter(cx); From c51206e980c2f14949c83c6b58a524c2f93ff6a5 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 10:59:02 +0200 Subject: [PATCH 049/650] Wire through launch config values to launch request --- Cargo.lock | 1 + crates/project/src/project.rs | 21 ++++++++++++++++----- crates/task/Cargo.toml | 1 + crates/task/src/lib.rs | 9 ++++++++- crates/task/src/task_template.rs | 25 ++++++++++++++++++++++++- 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c36e1831671db..afd537e97baae8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10703,6 +10703,7 @@ dependencies = [ "parking_lot", "schemars", "serde", + "serde_json", "serde_json_lenient", "sha2 0.10.7", "shellexpand 2.1.2", diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 22ef2ab9c95a68..168279ab89ac9c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1077,14 +1077,18 @@ impl Project { ) { let id = DebugAdapterClientId(1); let debug_template = debug_task.original_task(); - let command = debug_template.command.clone(); let cwd = debug_template .cwd .clone() .expect("Debug tasks need to know what directory to open"); - let mut args = debug_template.args.clone(); + let adapter_config = debug_task + .debug_adapter_config() + .expect("Debug taks need to specify adapter configuration"); - args.push("--server=8131".to_string().clone()); + let command = debug_template.command.clone(); + let args = debug_template.args.clone(); + let port = adapter_config.port.clone(); + let launch_config = adapter_config.launch_config.clone(); let task = cx.spawn(|this, mut cx| async move { let this2 = this.clone(); @@ -1092,7 +1096,7 @@ impl Project { TransportType::TCP, &command, args.iter().map(|ele| &ele[..]).collect(), - 8131, + port, cwd.into(), &mut cx, move |event, cx| { @@ -1118,7 +1122,14 @@ impl Project { client.configuration_done().await.log_err()?; // launch/attach request - client.launch(json!({"noDebug": false})).await.log_err()?; + client + .launch( + launch_config + .and_then(|c| Some(c.config)) + .unwrap_or(serde_json::Value::Null), + ) + .await + .log_err()?; let client = Arc::new(client); diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml index 43e3060a4e4855..7a22ea6157e4ea 100644 --- a/crates/task/Cargo.toml +++ b/crates/task/Cargo.toml @@ -17,6 +17,7 @@ hex.workspace = true parking_lot.workspace = true schemars.workspace = true serde.workspace = true +serde_json.workspace = true serde_json_lenient.workspace = true sha2.workspace = true shellexpand.workspace = true diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 3b53aacde1b8c1..7b3b6617f07918 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -12,7 +12,9 @@ use std::path::PathBuf; use std::str::FromStr; use std::{borrow::Cow, path::Path}; -pub use task_template::{RevealStrategy, TaskTemplate, TaskTemplates, TaskType}; +pub use task_template::{ + DebugAdapterConfig, RevealStrategy, TaskTemplate, TaskTemplates, TaskType, +}; pub use vscode_format::VsCodeTaskFile; /// Task identifier, unique within the application. @@ -112,6 +114,11 @@ impl ResolvedTask { self.original_task.task_type.clone() } + /// Get the configuration for the debug adapter that should be used for this task. + pub fn debug_adapter_config(&self) -> Option { + self.original_task.debug_adapter.clone() + } + /// Variables that were substituted during the task template resolution. pub fn substituted_variables(&self) -> &HashSet { &self.substituted_variables diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 831e8bebdb0c2a..58551578e281c7 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -4,6 +4,7 @@ use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; use schemars::{gen::SchemaSettings, JsonSchema}; use serde::{Deserialize, Serialize}; +use serde_json_lenient::Value; use sha2::{Digest, Sha256}; use util::{truncate_and_remove_front, ResultExt}; @@ -48,6 +49,10 @@ pub struct TaskTemplate { /// If this task should start a debugger or not #[serde(default)] pub task_type: TaskType, + /// Specific configuration for the debug adapter + /// This is only used if `task_type` is `Debug` + #[serde(default)] + pub debug_adapter: Option, /// Represents the tags which this template attaches to. Adding this removes this task from other UI. #[serde(default)] @@ -55,7 +60,7 @@ pub struct TaskTemplate { } /// Represents the type of task that is being ran -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Copy, Clone, Debug)] +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub enum TaskType { /// Act like a typically task that runs commands @@ -65,6 +70,24 @@ pub enum TaskType { Debug, } +/// Represents the configuration for the debug adapter +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub struct DebugAdapterConfig { + /// The port that the debug adapter is listening on + pub port: u16, + /// The configuration options that are send with the launch request + /// to the debug adapter + pub launch_config: Option, +} + +/// Represents the configuration for the debug adapter that is send with the launch request +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(transparent)] +pub struct DebugLaunchConfig { + pub config: serde_json::Value, +} + /// What to do with the terminal pane and tab, after the command was started. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] From cae295ff65c6335cc10b56bdb79265c5d3f2f2f6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 15:02:59 +0200 Subject: [PATCH 050/650] Fix send correct line to set breakpoint --- crates/project/src/project.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 168279ab89ac9c..74c507ee4953a9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1198,7 +1198,7 @@ impl Project { .set_breakpoints( abs_path.clone(), Some(vec![SourceBreakpoint { - line: row as u64, + line: (row + 1) as u64, condition: None, hit_condition: None, log_message: None, From 2debea81156514f24294222e751a037193743c39 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 15:06:09 +0200 Subject: [PATCH 051/650] Wire through transport type from config --- Cargo.lock | 1 + crates/dap/Cargo.toml | 1 + crates/dap/src/client.rs | 49 ++++++++++++++++++++------------ crates/project/src/project.rs | 2 +- crates/task/src/lib.rs | 2 +- crates/task/src/task_template.rs | 17 +++++++++-- 6 files changed, 50 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afd537e97baae8..fc241bb5f0d76a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3275,6 +3275,7 @@ dependencies = [ "serde_json", "serde_json_lenient", "smol", + "task", "util", ] diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index ea7428f01281d2..462cf97cc972cf 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -23,4 +23,5 @@ serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true smol.workspace = true +task.workspace = true util.workspace = true diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 09ce52ba70f20a..3bdfefcdb37dd9 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -16,7 +16,7 @@ use futures::{ AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; use gpui::{AppContext, AsyncAppContext}; -use serde_json::Value; +use serde_json::{json, Value}; use smol::{ io::BufReader, net::TcpStream, @@ -29,13 +29,9 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, time::Duration, }; +use task::{DebugAdapterConfig, TransportType}; use util::ResultExt; -pub enum TransportType { - TCP, - STDIO, -} - #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct DebugAdapterClientId(pub usize); @@ -46,14 +42,14 @@ pub struct DebugAdapterClient { server_tx: UnboundedSender, request_count: AtomicU64, capabilities: Option, + config: DebugAdapterConfig, } impl DebugAdapterClient { pub async fn new( - transport_type: TransportType, + config: DebugAdapterConfig, command: &str, args: Vec<&str>, - port: u16, project_path: PathBuf, cx: &mut AsyncAppContext, event_handler: F, @@ -61,20 +57,21 @@ impl DebugAdapterClient { where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { - match transport_type { + match config.transport { TransportType::TCP => { - Self::create_tcp_client(command, args, port, project_path, cx, event_handler).await + Self::create_tcp_client(config, command, args, project_path, cx, event_handler) + .await } TransportType::STDIO => { - Self::create_stdio_client(command, args, port, project_path, cx).await + Self::create_stdio_client(config, command, args, project_path, cx).await } } } async fn create_tcp_client( + config: DebugAdapterConfig, command: &str, args: Vec<&str>, - port: u16, project_path: PathBuf, cx: &mut AsyncAppContext, event_handler: F, @@ -100,11 +97,12 @@ impl DebugAdapterClient { .timer(Duration::from_millis(1000)) .await; - let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port); + let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), config.port); let (rx, tx) = TcpStream::connect(address).await?.split(); Self::handle_transport( + config, Box::new(BufReader::new(rx)), Box::new(tx), None, @@ -115,9 +113,9 @@ impl DebugAdapterClient { } async fn create_stdio_client( + config: DebugAdapterConfig, command: &str, args: Vec<&str>, - port: u16, project_path: PathBuf, cx: &mut AsyncAppContext, ) -> Result { @@ -125,6 +123,7 @@ impl DebugAdapterClient { } pub fn handle_transport( + config: DebugAdapterConfig, rx: Box, tx: Box, err: Option>, @@ -143,6 +142,7 @@ impl DebugAdapterClient { _process: process, request_count: AtomicU64::new(0), capabilities: None, + config, }; cx.spawn(move |cx| Self::handle_events(client_rx, event_handler, cx)) @@ -245,9 +245,16 @@ impl DebugAdapterClient { Ok(capabilities) } - pub async fn launch(&self, custom: Value) -> Result<()> { - self.request::(LaunchRequestArguments { raw: custom }) - .await + pub async fn launch(&self) -> Result<()> { + self.request::(LaunchRequestArguments { + raw: self + .config + .launch_config + .clone() + .and_then(|c| Some(c.config)) + .unwrap_or(Value::Null), + }) + .await } pub async fn resume(&self, thread_id: u64) { @@ -321,6 +328,12 @@ impl DebugAdapterClient { path: PathBuf, breakpoints: Option>, ) -> Result { + let adapter_data = self + .config + .launch_config + .clone() + .and_then(|c| Some(c.config)); + self.request::(SetBreakpointsArguments { source: Source { path: Some(String::from(path.to_string_lossy())), @@ -329,7 +342,7 @@ impl DebugAdapterClient { presentation_hint: None, origin: None, sources: None, - adapter_data: None, + adapter_data, checksums: None, }, breakpoints, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 74c507ee4953a9..95b47f2283f32d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -22,7 +22,7 @@ use client::{ use clock::ReplicaId; use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use dap::{ - client::{DebugAdapterClient, DebugAdapterClientId, TransportType}, + client::{DebugAdapterClient, DebugAdapterClientId}, transport::Events, SourceBreakpoint, }; diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 7b3b6617f07918..e02156a2f749b9 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -13,7 +13,7 @@ use std::str::FromStr; use std::{borrow::Cow, path::Path}; pub use task_template::{ - DebugAdapterConfig, RevealStrategy, TaskTemplate, TaskTemplates, TaskType, + DebugAdapterConfig, RevealStrategy, TaskTemplate, TaskTemplates, TaskType, TransportType, }; pub use vscode_format::VsCodeTaskFile; diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 58551578e281c7..1602c7642151e4 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,10 +1,9 @@ -use std::{default, path::PathBuf}; +use std::path::PathBuf; use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; use schemars::{gen::SchemaSettings, JsonSchema}; use serde::{Deserialize, Serialize}; -use serde_json_lenient::Value; use sha2::{Digest, Sha256}; use util::{truncate_and_remove_front, ResultExt}; @@ -70,12 +69,26 @@ pub enum TaskType { Debug, } +/// Represents the type of the debugger adapter connection +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TransportType { + /// Connect to the debug adapter via TCP + #[default] + TCP, + /// Connect to the debug adapter via STDIO + STDIO, +} + /// Represents the configuration for the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugAdapterConfig { /// The port that the debug adapter is listening on pub port: u16, + /// The type of connection the adapter should use + #[serde(default)] + pub transport: TransportType, /// The configuration options that are send with the launch request /// to the debug adapter pub launch_config: Option, From b869465f0008a8aa10d1886a129e1ec01ece0c73 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 15:09:26 +0200 Subject: [PATCH 052/650] Fix send configuration done request when initialized event was received. This fixes an issue that the debugger adapter was notified to early. So it had in correct breakpoint information and would always stop on the first line without given a stack trace --- crates/debugger_ui/src/debugger_panel.rs | 69 ++++++++++++------------ crates/project/src/project.rs | 25 ++------- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 0b1ed2db3cdb63..6804e124744c86 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,9 +1,9 @@ use anyhow::Result; -use dap::requests::{Scopes, StackTrace, Variables}; +use dap::requests::{BreakpointLocations, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - Scope, ScopesArguments, StackFrame, StackTraceArguments, ThreadEventReason, Variable, - VariablesArguments, + BreakpointLocationsArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, + ThreadEventReason, Variable, VariablesArguments, }; use gpui::{ actions, list, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, @@ -59,19 +59,22 @@ impl DebugPanel { let _subscriptions = vec![cx.subscribe(&project, { move |this: &mut Self, model, event, cx| { - if let project::Event::DebugClientStarted(client_id) = event { - dbg!(&event, &client_id); - } - if let project::Event::DebugClientEvent { client_id, event } = event { match event { - Events::Initialized(_) => return, + Events::Initialized(_) => { + let client = this.debug_adapter(cx); + cx.spawn(|_, _| async move { + // TODO: send all the current breakpoints + client.configuration_done().await + }) + .detach_and_log_err(cx); + } Events::Stopped(event) => { if let Some(thread_id) = event.thread_id { let client = this.debug_adapter(cx); cx.spawn(|this, mut cx| async move { - let res = client + let stack_trace_response = client .request::(StackTraceArguments { thread_id, start_frame: None, @@ -84,7 +87,9 @@ impl DebugPanel { let mut variables: HashMap> = HashMap::new(); - for stack_frame in res.stack_frames.clone().into_iter() { + for stack_frame in + stack_trace_response.stack_frames.clone().into_iter() + { let scope_response = client .request::(ScopesArguments { frame_id: stack_frame.id, @@ -120,16 +125,14 @@ impl DebugPanel { { this.current_thread_id = Some(thread_id); - this.current_stack_frame_id = - res.stack_frames.clone().first().map(|f| f.id); - - let mut stack_frames = Vec::new(); - - for stack_frame in res.stack_frames.clone() { - stack_frames.push(stack_frame); - } + this.current_stack_frame_id = stack_trace_response + .stack_frames + .clone() + .first() + .map(|f| f.id); - entry.stack_frames = stack_frames; + entry.stack_frames = + stack_trace_response.stack_frames.clone(); entry.scopes = scopes; entry.variables = variables; @@ -145,9 +148,9 @@ impl DebugPanel { .detach(); }; } - Events::Continued(_) => todo!(), - Events::Exited(_) => todo!(), - Events::Terminated(_) => todo!(), + Events::Continued(_) => {} + Events::Exited(_) => {} + Events::Terminated(_) => {} Events::Thread(event) => { if event.reason == ThreadEventReason::Started { this.thread_state.insert( @@ -167,17 +170,17 @@ impl DebugPanel { cx.notify(); } - Events::Output(_) => todo!(), - Events::Breakpoint(_) => todo!(), - Events::Module(_) => todo!(), - Events::LoadedSource(_) => todo!(), - Events::Capabilities(_) => todo!(), - Events::Memory(_) => todo!(), - Events::Process(_) => todo!(), - Events::ProgressEnd(_) => todo!(), - Events::ProgressStart(_) => todo!(), - Events::ProgressUpdate(_) => todo!(), - Events::Invalidated(_) => todo!(), + Events::Output(_) => {} + Events::Breakpoint(_) => {} + Events::Module(_) => {} + Events::LoadedSource(_) => {} + Events::Capabilities(_) => {} + Events::Memory(_) => {} + Events::Process(_) => {} + Events::ProgressEnd(_) => {} + Events::ProgressStart(_) => {} + Events::ProgressUpdate(_) => {} + Events::Invalidated(_) => {} } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 95b47f2283f32d..00bdca82075528 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -27,7 +27,7 @@ use dap::{ SourceBreakpoint, }; use debounced_delay::DebouncedDelay; -use debugger_inventory::{DebuggerConfigSourceKind, DebuggerInventory}; +use debugger_inventory::DebuggerInventory; use futures::{ channel::{ mpsc::{self, UnboundedReceiver}, @@ -1087,16 +1087,13 @@ impl Project { let command = debug_template.command.clone(); let args = debug_template.args.clone(); - let port = adapter_config.port.clone(); - let launch_config = adapter_config.launch_config.clone(); let task = cx.spawn(|this, mut cx| async move { let this2 = this.clone(); let mut client = DebugAdapterClient::new( - TransportType::TCP, + adapter_config, &command, args.iter().map(|ele| &ele[..]).collect(), - port, cwd.into(), &mut cx, move |event, cx| { @@ -1113,23 +1110,11 @@ impl Project { .await .log_err()?; - // initialize + // initialize request client.initialize().await.log_err()?; - // TODO: fetch all old breakpoints and send them to the debug adapter - - // configuration done - client.configuration_done().await.log_err()?; - - // launch/attach request - client - .launch( - launch_config - .and_then(|c| Some(c.config)) - .unwrap_or(serde_json::Value::Null), - ) - .await - .log_err()?; + // launch request + client.launch().await.log_err()?; let client = Arc::new(client); From 08afbc6b582b595236527fa799fe5c12339c92da Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 15:09:44 +0200 Subject: [PATCH 053/650] Update capabilities --- crates/dap/src/client.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 3bdfefcdb37dd9..d13763e917877b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -228,14 +228,14 @@ impl DebugAdapterClient { supports_variable_type: Some(true), supports_variable_paging: Some(false), supports_run_in_terminal_request: Some(false), // TODO: we should support this - supports_memory_references: Some(false), - supports_progress_reporting: Some(false), + supports_memory_references: Some(true), + supports_progress_reporting: Some(true), supports_invalidated_event: Some(false), lines_start_at1: Some(true), columns_start_at1: Some(true), - supports_memory_event: None, + supports_memory_event: Some(true), supports_args_can_be_interpreted_by_shell: None, - supports_start_debugging_request: None, + supports_start_debugging_request: Some(true), }; let capabilities = self.request::(args).await?; From a93913b9a38f8b83e1313380da7ca14a75890b73 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 16:51:30 +0200 Subject: [PATCH 054/650] Swap handle events with handle request This is a temporary fix, so we don't crash when race condition occurs and the event is send earlier than request was finished. The debug adapter was still `starting` meaning the task was still pending. --- crates/dap/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d13763e917877b..d9c721ae9a8d75 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -145,10 +145,10 @@ impl DebugAdapterClient { config, }; - cx.spawn(move |cx| Self::handle_events(client_rx, event_handler, cx)) + cx.spawn(move |_| Self::handle_recv(server_rx, server_tx, client_tx)) .detach(); - cx.spawn(move |_| Self::handle_recv(server_rx, server_tx, client_tx)) + cx.spawn(move |cx| Self::handle_events(client_rx, event_handler, cx)) .detach(); Ok(client) From 93f4775cf6b0c09adaca91ee7c90ad40bd9fc6b1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 18:59:50 +0200 Subject: [PATCH 055/650] Properly fix race condition of receive events and handling responses Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/dap/src/client.rs | 48 +++++++++++++++-------------------- crates/project/src/project.rs | 35 ++++++++++++++++--------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d9c721ae9a8d75..5fa6a2c1f66f5c 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -26,7 +26,10 @@ use std::{ net::{Ipv4Addr, SocketAddrV4}, path::PathBuf, process::Stdio, - sync::atomic::{AtomicU64, Ordering}, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, Mutex, + }, time::Duration, }; use task::{DebugAdapterConfig, TransportType}; @@ -43,24 +46,20 @@ pub struct DebugAdapterClient { request_count: AtomicU64, capabilities: Option, config: DebugAdapterConfig, + client_rx: Arc>>, } impl DebugAdapterClient { - pub async fn new( + pub async fn new( config: DebugAdapterConfig, command: &str, args: Vec<&str>, project_path: PathBuf, cx: &mut AsyncAppContext, - event_handler: F, - ) -> Result - where - F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, - { + ) -> Result { match config.transport { TransportType::TCP => { - Self::create_tcp_client(config, command, args, project_path, cx, event_handler) - .await + Self::create_tcp_client(config, command, args, project_path, cx).await } TransportType::STDIO => { Self::create_stdio_client(config, command, args, project_path, cx).await @@ -68,17 +67,13 @@ impl DebugAdapterClient { } } - async fn create_tcp_client( + async fn create_tcp_client( config: DebugAdapterConfig, command: &str, args: Vec<&str>, project_path: PathBuf, cx: &mut AsyncAppContext, - event_handler: F, - ) -> Result - where - F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, - { + ) -> Result { let mut command = process::Command::new(command); command .current_dir(project_path) @@ -108,7 +103,6 @@ impl DebugAdapterClient { None, Some(process), cx, - event_handler, ) } @@ -122,46 +116,46 @@ impl DebugAdapterClient { todo!("not implemented") } - pub fn handle_transport( + pub fn handle_transport( config: DebugAdapterConfig, rx: Box, tx: Box, err: Option>, process: Option, cx: &mut AsyncAppContext, - event_handler: F, - ) -> Result - where - F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, - { + ) -> Result { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); + let client_rx = Arc::new(Mutex::new(client_rx)); + let client = Self { server_tx: server_tx.clone(), _process: process, request_count: AtomicU64::new(0), capabilities: None, config, + client_rx, }; cx.spawn(move |_| Self::handle_recv(server_rx, server_tx, client_tx)) .detach(); - cx.spawn(move |cx| Self::handle_events(client_rx, event_handler, cx)) - .detach(); - Ok(client) } - async fn handle_events( - mut client_rx: UnboundedReceiver, + pub async fn handle_events( + client: Arc, mut event_handler: F, cx: AsyncAppContext, ) -> Result<()> where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { + let mut client_rx = client + .client_rx + .lock() + .expect("Client should not be locked by any other thread/task"); while let Some(payload) = client_rx.next().await { cx.update(|cx| match payload { Payload::Event(event) => event_handler(*event, cx), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 00bdca82075528..bc4ef0453fc1c1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -23,7 +23,7 @@ use clock::ReplicaId; use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, - transport::Events, + transport::{self, Events}, SourceBreakpoint, }; use debounced_delay::DebouncedDelay; @@ -1089,23 +1089,12 @@ impl Project { let args = debug_template.args.clone(); let task = cx.spawn(|this, mut cx| async move { - let this2 = this.clone(); let mut client = DebugAdapterClient::new( adapter_config, &command, args.iter().map(|ele| &ele[..]).collect(), cwd.into(), &mut cx, - move |event, cx| { - this2 - .update(cx, |_, cx| { - cx.emit(Event::DebugClientEvent { - client_id: id, - event, - }) - }) - .log_err(); - }, ) .await .log_err()?; @@ -1127,6 +1116,28 @@ impl Project { cx.emit(Event::DebugClientStarted(id)); + // call handle events + cx.spawn({ + let client = client.clone(); + move |project, cx| { + DebugAdapterClient::handle_events( + client, + move |event, cx| { + project + .update(cx, |_, cx| { + cx.emit(Event::DebugClientEvent { + client_id: id, + event, + }) + }) + .log_err(); + }, + cx, + ) + } + }) + .detach_and_log_err(cx); + anyhow::Ok(()) }) .log_err(); From 11d74ea4ecd8b6b0e9d9eecf72331e9a7508e716 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 2 Jul 2024 19:00:35 +0200 Subject: [PATCH 056/650] Add tooltip for stack trace debug items Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/debugger_ui/src/debugger_panel.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 6804e124744c86..8365577f8f579d 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -260,10 +260,20 @@ impl DebugPanel { let source = stack_frame.source.clone(); + let formatted_path = format!( + "{}:{}", + source.clone().and_then(|s| s.name).unwrap_or_default(), + stack_frame.line, + ); + v_flex() .rounded_md() .group("") .id(("stack-frame", stack_frame.id)) + .tooltip({ + let formatted_path = formatted_path.clone(); + move |cx| Tooltip::text(formatted_path.clone(), cx) + }) .p_1() .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) .child( @@ -271,11 +281,7 @@ impl DebugPanel { .gap_0p5() .text_ui_sm(cx) .child(stack_frame.name.clone()) - .child(format!( - "{}:{}", - source.clone().and_then(|s| s.name).unwrap_or_default(), - stack_frame.line, - )), + .child(formatted_path), ) .child( h_flex() From d6cafb83158922983c5ab66fee7d1d38692852fb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 3 Jul 2024 09:45:54 +0200 Subject: [PATCH 057/650] Fix typo --- crates/project/src/project.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1cf6443ff23586..03847f761708a4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1083,7 +1083,7 @@ impl Project { .expect("Debug tasks need to know what directory to open"); let adapter_config = debug_task .debug_adapter_config() - .expect("Debug taks need to specify adapter configuration"); + .expect("Debug tasks need to specify adapter configuration"); let command = debug_template.command.clone(); let args = debug_template.args.clone(); From 8c5f6a0be7d3e4be0b26d95642e886fc9dc3c2cf Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 3 Jul 2024 09:49:37 +0200 Subject: [PATCH 058/650] Make clippppy more happy --- crates/dap/src/client.rs | 10 +++------- crates/dap/src/transport.rs | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 5fa6a2c1f66f5c..0d79278367e3ab 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -16,7 +16,7 @@ use futures::{ AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; use gpui::{AppContext, AsyncAppContext}; -use serde_json::{json, Value}; +use serde_json::Value; use smol::{ io::BufReader, net::TcpStream, @@ -245,7 +245,7 @@ impl DebugAdapterClient { .config .launch_config .clone() - .and_then(|c| Some(c.config)) + .map(|c| c.config) .unwrap_or(Value::Null), }) .await @@ -322,11 +322,7 @@ impl DebugAdapterClient { path: PathBuf, breakpoints: Option>, ) -> Result { - let adapter_data = self - .config - .launch_config - .clone() - .and_then(|c| Some(c.config)); + let adapter_data = self.config.launch_config.clone().map(|c| c.config); self.request::(SetBreakpointsArguments { source: Source { diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 4cbf105c185f60..2f314ab4d3a991 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -15,7 +15,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use smol::io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _}; use std::{collections::HashMap, sync::Arc}; -use util::ResultExt as _; #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "camelCase")] @@ -188,27 +187,29 @@ impl Transport { async fn process_server_message( &self, mut client_tx: &UnboundedSender, - msg: Payload, + payload: Payload, ) -> Result<()> { - match msg { + match payload { Payload::Response(res) => { - if let Some(mut tx) = self.pending_requests.lock().remove(&res.request_seq) { + let pending_request = { + let mut pending_requests = self.pending_requests.lock(); + pending_requests.remove(&res.request_seq) + }; + + if let Some(mut tx) = pending_request { tx.send(Self::process_response(res)).await?; } else { client_tx.send(Payload::Response(res)).await?; }; - - Ok(()) } Payload::Request(_) => { - client_tx.send(msg).await.log_err(); - Ok(()) + client_tx.send(payload).await?; } Payload::Event(_) => { - client_tx.send(msg).await.log_err(); - Ok(()) + client_tx.send(payload).await?; } } + Ok(()) } async fn receive( From 93e0bbb833b021511cc0858c09bd0f3f6117492b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 3 Jul 2024 10:40:02 +0200 Subject: [PATCH 059/650] Use parking lot mutex instead of sync mutex --- crates/dap/src/client.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 0d79278367e3ab..f5b875457a0970 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -16,6 +16,7 @@ use futures::{ AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; use gpui::{AppContext, AsyncAppContext}; +use parking_lot::Mutex; use serde_json::Value; use smol::{ io::BufReader, @@ -28,7 +29,7 @@ use std::{ process::Stdio, sync::{ atomic::{AtomicU64, Ordering}, - Arc, Mutex, + Arc, }, time::Duration, }; @@ -152,10 +153,7 @@ impl DebugAdapterClient { where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { - let mut client_rx = client - .client_rx - .lock() - .expect("Client should not be locked by any other thread/task"); + let mut client_rx = client.client_rx.lock(); while let Some(payload) = client_rx.next().await { cx.update(|cx| match payload { Payload::Event(event) => event_handler(*event, cx), From 7d2f63ebbd445a862d179b4ef0d9e0a3edc34f0b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 3 Jul 2024 12:35:55 +0200 Subject: [PATCH 060/650] Split event handling code to separate functions --- crates/debugger_ui/src/debugger_panel.rs | 272 ++++++++++++----------- crates/project/src/project.rs | 7 +- 2 files changed, 150 insertions(+), 129 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 8365577f8f579d..414a09d6475cf6 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,16 +1,20 @@ use anyhow::Result; +use dap::client::DebugAdapterClientId; use dap::requests::{BreakpointLocations, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ BreakpointLocationsArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, - ThreadEventReason, Variable, VariablesArguments, + StoppedEvent, ThreadEventReason, Variable, VariablesArguments, }; use gpui::{ actions, list, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, ListState, Subscription, Task, View, ViewContext, WeakView, + FocusableView, ListState, Model, Subscription, Task, View, ViewContext, WeakView, }; +use project::Event::DebugClientEvent; +use project::Project; use std::{collections::HashMap, sync::Arc}; use ui::{prelude::*, Tooltip}; +use workspace::pane::Event; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, @@ -60,128 +64,7 @@ impl DebugPanel { let _subscriptions = vec![cx.subscribe(&project, { move |this: &mut Self, model, event, cx| { if let project::Event::DebugClientEvent { client_id, event } = event { - match event { - Events::Initialized(_) => { - let client = this.debug_adapter(cx); - cx.spawn(|_, _| async move { - // TODO: send all the current breakpoints - client.configuration_done().await - }) - .detach_and_log_err(cx); - } - Events::Stopped(event) => { - if let Some(thread_id) = event.thread_id { - let client = this.debug_adapter(cx); - - cx.spawn(|this, mut cx| async move { - let stack_trace_response = client - .request::(StackTraceArguments { - thread_id, - start_frame: None, - levels: None, - format: None, - }) - .await?; - - let mut scopes: HashMap> = HashMap::new(); - let mut variables: HashMap> = - HashMap::new(); - - for stack_frame in - stack_trace_response.stack_frames.clone().into_iter() - { - let scope_response = client - .request::(ScopesArguments { - frame_id: stack_frame.id, - }) - .await?; - - scopes.insert( - stack_frame.id, - scope_response.scopes.clone(), - ); - - for scope in scope_response.scopes { - variables.insert( - scope.variables_reference, - client - .request::(VariablesArguments { - variables_reference: scope - .variables_reference, - filter: None, - start: None, - count: None, - format: None, - }) - .await? - .variables, - ); - } - } - - this.update(&mut cx, |this, cx| { - if let Some(entry) = - this.thread_state.get_mut(&thread_id) - { - this.current_thread_id = Some(thread_id); - - this.current_stack_frame_id = stack_trace_response - .stack_frames - .clone() - .first() - .map(|f| f.id); - - entry.stack_frames = - stack_trace_response.stack_frames.clone(); - entry.scopes = scopes; - entry.variables = variables; - - this.stack_frame_list - .reset(entry.stack_frames.len()); - - cx.notify(); - } - - anyhow::Ok(()) - }) - }) - .detach(); - }; - } - Events::Continued(_) => {} - Events::Exited(_) => {} - Events::Terminated(_) => {} - Events::Thread(event) => { - if event.reason == ThreadEventReason::Started { - this.thread_state.insert( - event.thread_id, - ThreadState { - ..Default::default() - }, - ); - this.current_thread_id = Some(event.thread_id); - } else { - if this.current_thread_id == Some(event.thread_id) { - this.current_thread_id = None; - } - this.stack_frame_list.reset(0); - this.thread_state.remove(&event.thread_id); - } - - cx.notify(); - } - Events::Output(_) => {} - Events::Breakpoint(_) => {} - Events::Module(_) => {} - Events::LoadedSource(_) => {} - Events::Capabilities(_) => {} - Events::Memory(_) => {} - Events::Process(_) => {} - Events::ProgressEnd(_) => {} - Events::ProgressStart(_) => {} - Events::ProgressUpdate(_) => {} - Events::Invalidated(_) => {} - } + Self::handle_debug_client_events(this, model, client_id, event, cx); } } })]; @@ -244,6 +127,21 @@ impl DebugPanel { .unwrap() } + fn debug_adapter_by_id( + &self, + client_id: DebugAdapterClientId, + cx: &mut ViewContext, + ) -> Arc { + self.workspace + .update(cx, |this, cx| { + this.project() + .read(cx) + .debug_adapter_by_id(client_id) + .unwrap() + }) + .unwrap() + } + fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { v_flex() .w_full() @@ -426,6 +324,132 @@ impl DebugPanel { .detach(); } } + + fn handle_debug_client_events( + this: &mut Self, + model: Model, + client_id: &DebugAdapterClientId, + event: &Events, + cx: &mut ViewContext, + ) { + match event { + Events::Initialized(_) => { + let client = this.debug_adapter_by_id(*client_id, cx); + cx.spawn(|_, _| async move { + // TODO: send all the current breakpoints + client.configuration_done().await + }) + .detach_and_log_err(cx); + } + Events::Stopped(event) => Self::handle_stopped_event(this, model, client_id, event, cx), + Events::Continued(_) => {} + Events::Exited(_) => {} + Events::Terminated(_) => {} + Events::Thread(event) => { + if event.reason == ThreadEventReason::Started { + this.thread_state + .insert(event.thread_id, ThreadState::default()); + this.current_thread_id = Some(event.thread_id); + } else { + if this.current_thread_id == Some(event.thread_id) { + this.current_thread_id = None; + } + this.stack_frame_list.reset(0); + this.thread_state.remove(&event.thread_id); + } + + cx.notify(); + } + Events::Output(_) => {} + Events::Breakpoint(_) => {} + Events::Module(_) => {} + Events::LoadedSource(_) => {} + Events::Capabilities(_) => {} + Events::Memory(_) => {} + Events::Process(_) => {} + Events::ProgressEnd(_) => {} + Events::ProgressStart(_) => {} + Events::ProgressUpdate(_) => {} + Events::Invalidated(_) => {} + } + } + + fn handle_stopped_event( + this: &mut Self, + model: Model, + client_id: &DebugAdapterClientId, + event: &StoppedEvent, + cx: &mut ViewContext, + ) { + let Some(thread_id) = event.thread_id else { + return; + }; + + let client = this.debug_adapter_by_id(*client_id, cx); + + cx.spawn(|this, mut cx| async move { + let stack_trace_response = client + .request::(StackTraceArguments { + thread_id, + start_frame: None, + levels: None, + format: None, + }) + .await?; + + let mut scopes: HashMap> = HashMap::new(); + let mut variables: HashMap> = HashMap::new(); + + for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { + let scope_response = client + .request::(ScopesArguments { + frame_id: stack_frame.id, + }) + .await?; + + scopes.insert(stack_frame.id, scope_response.scopes.clone()); + + for scope in scope_response.scopes { + variables.insert( + scope.variables_reference, + client + .request::(VariablesArguments { + variables_reference: scope.variables_reference, + filter: None, + start: None, + count: None, + format: None, + }) + .await? + .variables, + ); + } + } + + this.update(&mut cx, |this, cx| { + if let Some(entry) = this.thread_state.get_mut(&thread_id) { + this.current_thread_id = Some(thread_id); + + this.current_stack_frame_id = stack_trace_response + .stack_frames + .clone() + .first() + .map(|f| f.id); + + entry.stack_frames = stack_trace_response.stack_frames.clone(); + entry.scopes = scopes; + entry.variables = variables; + + this.stack_frame_list.reset(entry.stack_frames.len()); + + cx.notify(); + } + + anyhow::Ok(()) + }) + }) + .detach(); + } } impl EventEmitter for DebugPanel {} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 03847f761708a4..b220bc8116a4e4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1060,13 +1060,10 @@ impl Project { }) } - pub fn debug_adapter_by_id( - &self, - id: DebugAdapterClientId, - ) -> Option<&Arc> { + pub fn debug_adapter_by_id(&self, id: DebugAdapterClientId) -> Option> { self.debug_adapters.get(&id).and_then(|state| match state { DebugAdapterClientState::Starting(_) => None, - DebugAdapterClientState::Running(client) => Some(client), + DebugAdapterClientState::Running(client) => Some(client.clone()), }) } From 003fb7c81ea3ebc88e2c09a39dfc4fe7c652082e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 3 Jul 2024 14:53:22 +0200 Subject: [PATCH 061/650] Move thread state to the client This allows us to show the current line and is closer for multi client/adapter support --- crates/dap/src/client.rs | 42 ++++- crates/debugger_ui/src/debugger_panel.rs | 197 +++++++++++------------ 2 files changed, 135 insertions(+), 104 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f5b875457a0970..e6bca37882e7b7 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -7,16 +7,16 @@ use dap_types::{ StepIn, StepOut, }, ConfigurationDoneArguments, ContinueArguments, InitializeRequestArgumentsPathFormat, - LaunchRequestArguments, NextArguments, PauseArguments, SetBreakpointsArguments, - SetBreakpointsResponse, Source, SourceBreakpoint, StepBackArguments, StepInArguments, - StepOutArguments, SteppingGranularity, + LaunchRequestArguments, NextArguments, PauseArguments, Scope, SetBreakpointsArguments, + SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, + StepInArguments, StepOutArguments, SteppingGranularity, Variable, }; use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, }; use gpui::{AppContext, AsyncAppContext}; -use parking_lot::Mutex; +use parking_lot::{Mutex, MutexGuard}; use serde_json::Value; use smol::{ io::BufReader, @@ -24,6 +24,7 @@ use smol::{ process::{self, Child}, }; use std::{ + collections::HashMap, net::{Ipv4Addr, SocketAddrV4}, path::PathBuf, process::Stdio, @@ -40,7 +41,14 @@ use util::ResultExt; #[repr(transparent)] pub struct DebugAdapterClientId(pub usize); -#[derive(Debug)] +#[derive(Debug, Default, Clone)] +pub struct ThreadState { + pub stack_frames: Vec, + pub scopes: HashMap>, // stack_frame_id -> scopes + pub variables: HashMap>, // scope.variable_reference -> variables + pub current_stack_frame_id: Option, +} + pub struct DebugAdapterClient { _process: Option, server_tx: UnboundedSender, @@ -48,6 +56,8 @@ pub struct DebugAdapterClient { capabilities: Option, config: DebugAdapterConfig, client_rx: Arc>>, + thread_state: Arc>>, // thread_id -> thread_state + current_thread_id: Arc>>, } impl DebugAdapterClient { @@ -137,6 +147,8 @@ impl DebugAdapterClient { capabilities: None, config, client_rx, + thread_state: Arc::new(Mutex::new(HashMap::new())), + current_thread_id: Arc::new(Mutex::new(None)), }; cx.spawn(move |_| Self::handle_recv(server_rx, server_tx, client_tx)) @@ -210,6 +222,26 @@ impl DebugAdapterClient { self.request_count.fetch_add(1, Ordering::Relaxed) } + pub fn current_thread_id(&self) -> Option { + self.current_thread_id.lock().clone() + } + + pub fn update_current_thread_id(&self, thread_id: Option) { + *self.current_thread_id.lock() = thread_id; + } + + pub fn thread_state(&self) -> MutexGuard> { + self.thread_state.lock() + } + + pub fn current_thread_state(&self) -> Option { + if let Some(id) = self.current_thread_id() { + self.thread_state().clone().get(&id).cloned() + } else { + None + } + } + pub async fn initialize(&mut self) -> Result { let args = dap_types::InitializeRequestArguments { client_id: Some("zed".to_owned()), diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 414a09d6475cf6..fd7acede896e02 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,20 +1,18 @@ use anyhow::Result; -use dap::client::DebugAdapterClientId; -use dap::requests::{BreakpointLocations, Scopes, StackTrace, Variables}; +use dap::client::{DebugAdapterClientId, ThreadState}; +use dap::requests::{Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - BreakpointLocationsArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, - StoppedEvent, ThreadEventReason, Variable, VariablesArguments, + Scope, ScopesArguments, StackFrame, StackTraceArguments, StoppedEvent, ThreadEvent, + ThreadEventReason, Variable, VariablesArguments, }; use gpui::{ actions, list, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, ListState, Model, Subscription, Task, View, ViewContext, WeakView, }; -use project::Event::DebugClientEvent; use project::Project; use std::{collections::HashMap, sync::Arc}; use ui::{prelude::*, Tooltip}; -use workspace::pane::Event; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, @@ -33,13 +31,6 @@ actions!( ] ); -#[derive(Default)] -struct ThreadState { - pub stack_frames: Vec, - pub scopes: HashMap>, // stack_frame_id -> scopes - pub variables: HashMap>, // scope.variable_reference -> variables -} - pub struct DebugPanel { pub position: DockPosition, pub zoomed: bool, @@ -47,10 +38,7 @@ pub struct DebugPanel { pub focus_handle: FocusHandle, pub size: Pixels, _subscriptions: Vec, - pub current_thread_id: Option, - pub current_stack_frame_id: Option, pub workspace: WeakView, - thread_state: HashMap, pub stack_frame_list: ListState, } @@ -88,10 +76,7 @@ impl DebugPanel { focus_handle: cx.focus_handle(), size: px(300.), _subscriptions, - current_thread_id: None, - current_stack_frame_id: None, workspace: workspace.clone(), - thread_state: Default::default(), stack_frame_list, } }) @@ -104,30 +89,23 @@ impl DebugPanel { cx.spawn(|mut cx| async move { cx.update(|cx| DebugPanel::new(workspace, cx)) }) } - fn stack_frame_for_index(&self, ix: usize) -> &StackFrame { - &self - .current_thread_id - .and_then(|id| { - self.thread_state - .get(&id) - .and_then(|state| state.stack_frames.get(ix)) - }) - .unwrap() + fn stack_frame_for_index(&self, ix: usize, cx: &mut ViewContext) -> Option { + self.debug_client(cx).and_then(|c| { + c.current_thread_state() + .and_then(|f| f.stack_frames.get(ix).cloned()) + }) } - fn debug_adapter(&self, cx: &mut ViewContext) -> Arc { + fn debug_client(&self, cx: &mut ViewContext) -> Option> { self.workspace .update(cx, |this, cx| { - this.project() - .read(cx) - .running_debug_adapters() - .next() - .unwrap() + this.project().read(cx).running_debug_adapters().next() }) - .unwrap() + .ok() + .flatten() } - fn debug_adapter_by_id( + fn debug_client_by_id( &self, client_id: DebugAdapterClientId, cx: &mut ViewContext, @@ -154,7 +132,7 @@ impl DebugPanel { } fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { - let stack_frame = self.stack_frame_for_index(ix); + let stack_frame = self.stack_frame_for_index(ix, cx).unwrap(); let source = stack_frame.source.clone(); @@ -195,7 +173,7 @@ impl DebugPanel { thread_state: &ThreadState, cx: &mut ViewContext, ) -> impl IntoElement { - let Some(scopes) = self + let Some(scopes) = thread_state .current_stack_frame_id .and_then(|id| thread_state.scopes.get(&id)) else { @@ -272,56 +250,62 @@ impl DebugPanel { } fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { - let client = self.debug_adapter(cx); - if let Some(thread_id) = self.current_thread_id { - cx.background_executor() - .spawn(async move { client.resume(thread_id).await }) - .detach(); + if let Some(client) = self.debug_client(cx) { + if let Some(thread_id) = client.current_thread_id() { + cx.background_executor() + .spawn(async move { client.resume(thread_id).await }) + .detach(); + } } } fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext) { - let client = self.debug_adapter(cx); - if let Some(thread_id) = self.current_thread_id { - cx.background_executor() - .spawn(async move { client.step_over(thread_id).await }) - .detach(); + if let Some(client) = self.debug_client(cx) { + if let Some(thread_id) = client.current_thread_id() { + cx.background_executor() + .spawn(async move { client.step_over(thread_id).await }) + .detach(); + } } } fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext) { - let client = self.debug_adapter(cx); - if let Some(thread_id) = self.current_thread_id { - cx.background_executor() - .spawn(async move { client.step_in(thread_id).await }) - .detach(); + if let Some(client) = self.debug_client(cx) { + if let Some(thread_id) = client.current_thread_id() { + cx.background_executor() + .spawn(async move { client.step_in(thread_id).await }) + .detach(); + } } } fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext) { - let client = self.debug_adapter(cx); - if let Some(thread_id) = self.current_thread_id { - cx.background_executor() - .spawn(async move { client.step_out(thread_id).await }) - .detach(); + if let Some(client) = self.debug_client(cx) { + if let Some(thread_id) = client.current_thread_id() { + cx.background_executor() + .spawn(async move { client.step_out(thread_id).await }) + .detach(); + } } } fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext) { - let client = self.debug_adapter(cx); - if let Some(thread_id) = self.current_thread_id { - cx.background_executor() - .spawn(async move { client.restart(thread_id).await }) - .detach(); + if let Some(client) = self.debug_client(cx) { + if let Some(thread_id) = client.current_thread_id() { + cx.background_executor() + .spawn(async move { client.restart(thread_id).await }) + .detach(); + } } } fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext) { - let client = self.debug_adapter(cx); - if let Some(thread_id) = self.current_thread_id { - cx.background_executor() - .spawn(async move { client.pause(thread_id).await }) - .detach(); + if let Some(client) = self.debug_client(cx) { + if let Some(thread_id) = client.current_thread_id() { + cx.background_executor() + .spawn(async move { client.pause(thread_id).await }) + .detach(); + } } } @@ -334,7 +318,7 @@ impl DebugPanel { ) { match event { Events::Initialized(_) => { - let client = this.debug_adapter_by_id(*client_id, cx); + let client = this.debug_client_by_id(*client_id, cx); cx.spawn(|_, _| async move { // TODO: send all the current breakpoints client.configuration_done().await @@ -345,21 +329,7 @@ impl DebugPanel { Events::Continued(_) => {} Events::Exited(_) => {} Events::Terminated(_) => {} - Events::Thread(event) => { - if event.reason == ThreadEventReason::Started { - this.thread_state - .insert(event.thread_id, ThreadState::default()); - this.current_thread_id = Some(event.thread_id); - } else { - if this.current_thread_id == Some(event.thread_id) { - this.current_thread_id = None; - } - this.stack_frame_list.reset(0); - this.thread_state.remove(&event.thread_id); - } - - cx.notify(); - } + Events::Thread(event) => Self::handle_thread_event(this, model, client_id, event, cx), Events::Output(_) => {} Events::Breakpoint(_) => {} Events::Module(_) => {} @@ -385,7 +355,7 @@ impl DebugPanel { return; }; - let client = this.debug_adapter_by_id(*client_id, cx); + let client = this.debug_client_by_id(*client_id, cx); cx.spawn(|this, mut cx| async move { let stack_trace_response = client @@ -427,19 +397,20 @@ impl DebugPanel { } this.update(&mut cx, |this, cx| { - if let Some(entry) = this.thread_state.get_mut(&thread_id) { - this.current_thread_id = Some(thread_id); + if let Some(entry) = client.thread_state().get_mut(&thread_id) { + client.update_current_thread_id(Some(thread_id)); - this.current_stack_frame_id = stack_trace_response + entry.current_stack_frame_id = stack_trace_response .stack_frames .clone() .first() .map(|f| f.id); - entry.stack_frames = stack_trace_response.stack_frames.clone(); entry.scopes = scopes; entry.variables = variables; + // TODO: check if the current client is the one that stopped + // if not we should not reset the stack frame list this.stack_frame_list.reset(entry.stack_frames.len()); cx.notify(); @@ -450,6 +421,37 @@ impl DebugPanel { }) .detach(); } + + fn handle_thread_event( + this: &mut Self, + model: Model, + client_id: &DebugAdapterClientId, + event: &ThreadEvent, + cx: &mut ViewContext, + ) { + let client = this.debug_client_by_id(*client_id, cx); + + let current_thread_id = client.current_thread_id(); + + if event.reason == ThreadEventReason::Started { + client + .thread_state() + .insert(event.thread_id, ThreadState::default()); + + if current_thread_id.is_none() { + client.update_current_thread_id(Some(event.thread_id)); + } + } else { + if current_thread_id == Some(event.thread_id) { + client.update_current_thread_id(None); + this.stack_frame_list.reset(0); // TODO: check based on the selected/current client + } + + client.thread_state().remove(&event.thread_id); + + cx.notify(); + } + } } impl EventEmitter for DebugPanel {} @@ -575,16 +577,13 @@ impl Render for DebugPanel { .tooltip(move |cx| Tooltip::text("Pause", cx)), ), ) - .child( - h_flex().size_full().items_start().p_1().gap_4().when_some( - self.current_thread_id - .and_then(|t| self.thread_state.get(&t)), - |this, thread_state| { - this.child(self.render_stack_frames(cx)) - .child(self.render_scopes(thread_state, cx)) - }, - ), - ) + .child(h_flex().size_full().items_start().p_1().gap_4().when_some( + self.debug_client(cx).and_then(|c| c.current_thread_state()), + |this, thread_state| { + this.child(self.render_stack_frames(cx)) + .child(self.render_scopes(&thread_state, cx)) + }, + )) .into_any() } } From f4eacca9874ba3b7e032ae2c0cdb04fea3eaebef Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 3 Jul 2024 15:48:47 +0200 Subject: [PATCH 062/650] Fix 2 TODO's for multiple client support --- crates/dap/src/client.rs | 25 +++++++++++++++++------- crates/debugger_ui/src/debugger_panel.rs | 10 ++++++---- crates/project/src/project.rs | 2 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index e6bca37882e7b7..68275b7805f9ce 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -50,6 +50,7 @@ pub struct ThreadState { } pub struct DebugAdapterClient { + id: DebugAdapterClientId, _process: Option, server_tx: UnboundedSender, request_count: AtomicU64, @@ -62,6 +63,7 @@ pub struct DebugAdapterClient { impl DebugAdapterClient { pub async fn new( + id: DebugAdapterClientId, config: DebugAdapterConfig, command: &str, args: Vec<&str>, @@ -70,15 +72,16 @@ impl DebugAdapterClient { ) -> Result { match config.transport { TransportType::TCP => { - Self::create_tcp_client(config, command, args, project_path, cx).await + Self::create_tcp_client(id, config, command, args, project_path, cx).await } TransportType::STDIO => { - Self::create_stdio_client(config, command, args, project_path, cx).await + Self::create_stdio_client(id, config, command, args, project_path, cx).await } } } async fn create_tcp_client( + id: DebugAdapterClientId, config: DebugAdapterConfig, command: &str, args: Vec<&str>, @@ -108,6 +111,7 @@ impl DebugAdapterClient { let (rx, tx) = TcpStream::connect(address).await?.split(); Self::handle_transport( + id, config, Box::new(BufReader::new(rx)), Box::new(tx), @@ -118,6 +122,7 @@ impl DebugAdapterClient { } async fn create_stdio_client( + id: DebugAdapterClientId, config: DebugAdapterConfig, command: &str, args: Vec<&str>, @@ -128,6 +133,7 @@ impl DebugAdapterClient { } pub fn handle_transport( + id: DebugAdapterClientId, config: DebugAdapterConfig, rx: Box, tx: Box, @@ -141,14 +147,15 @@ impl DebugAdapterClient { let client_rx = Arc::new(Mutex::new(client_rx)); let client = Self { - server_tx: server_tx.clone(), - _process: process, - request_count: AtomicU64::new(0), - capabilities: None, + id, config, client_rx, - thread_state: Arc::new(Mutex::new(HashMap::new())), + _process: process, + capabilities: None, + server_tx: server_tx.clone(), + request_count: AtomicU64::new(0), current_thread_id: Arc::new(Mutex::new(None)), + thread_state: Arc::new(Mutex::new(HashMap::new())), }; cx.spawn(move |_| Self::handle_recv(server_rx, server_tx, client_tx)) @@ -218,6 +225,10 @@ impl DebugAdapterClient { } } + pub fn id(&self) -> DebugAdapterClientId { + self.id + } + pub fn next_request_id(&self) -> u64 { self.request_count.fetch_add(1, Ordering::Relaxed) } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index fd7acede896e02..78be37d634ffe8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -409,9 +409,9 @@ impl DebugPanel { entry.scopes = scopes; entry.variables = variables; - // TODO: check if the current client is the one that stopped - // if not we should not reset the stack frame list - this.stack_frame_list.reset(entry.stack_frames.len()); + if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { + this.stack_frame_list.reset(entry.stack_frames.len()); + } cx.notify(); } @@ -444,7 +444,9 @@ impl DebugPanel { } else { if current_thread_id == Some(event.thread_id) { client.update_current_thread_id(None); - this.stack_frame_list.reset(0); // TODO: check based on the selected/current client + if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { + this.stack_frame_list.reset(0); + } } client.thread_state().remove(&event.thread_id); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b220bc8116a4e4..5f804c09875b1e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -86,7 +86,6 @@ use rpc::{ErrorCode, ErrorExt as _}; use search::SearchQuery; use search_history::SearchHistory; use serde::Serialize; -use serde_json::json; use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; @@ -1087,6 +1086,7 @@ impl Project { let task = cx.spawn(|this, mut cx| async move { let mut client = DebugAdapterClient::new( + id, adapter_config, &command, args.iter().map(|ele| &ele[..]).collect(), From 515122c54dd97784b51624d8b8694bd28abeff9b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 3 Jul 2024 18:20:05 +0200 Subject: [PATCH 063/650] Clean up debug panel code For now we only support debugger to be positioned on the bottom. --- crates/debugger_ui/src/debugger_panel.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 78be37d634ffe8..b2e4d3e6312d63 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -32,9 +32,6 @@ actions!( ); pub struct DebugPanel { - pub position: DockPosition, - pub zoomed: bool, - pub active: bool, pub focus_handle: FocusHandle, pub size: Pixels, _subscriptions: Vec, @@ -70,9 +67,6 @@ impl DebugPanel { }); Self { - position: DockPosition::Bottom, - zoomed: false, - active: false, focus_handle: cx.focus_handle(), size: px(300.), _subscriptions, @@ -470,18 +464,14 @@ impl Panel for DebugPanel { } fn position(&self, _cx: &WindowContext) -> DockPosition { - self.position + DockPosition::Bottom } - fn position_is_valid(&self, _position: DockPosition) -> bool { - true + fn position_is_valid(&self, position: DockPosition) -> bool { + position == DockPosition::Bottom } - fn set_position(&mut self, position: DockPosition, _cx: &mut ViewContext) { - self.position = position; - // TODO: - // cx.update_global::(f) - } + fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {} fn size(&self, _cx: &WindowContext) -> Pixels { self.size From 23a81d5d70adceb9fae4ce7bc666d031a472fdbb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 4 Jul 2024 13:55:17 +0200 Subject: [PATCH 064/650] Send all stack frame, scopes, and variable requests parallel This will improve performance for loading large stack frames. --- crates/debugger_ui/src/debugger_panel.rs | 63 +++++++++++++++--------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b2e4d3e6312d63..c4c9cc9cbe5e0c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -6,6 +6,7 @@ use dap::{ Scope, ScopesArguments, StackFrame, StackTraceArguments, StoppedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; +use futures::future::try_join_all; use gpui::{ actions, list, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, ListState, Model, Subscription, Task, View, ViewContext, WeakView, @@ -361,35 +362,51 @@ impl DebugPanel { }) .await?; - let mut scopes: HashMap> = HashMap::new(); - let mut variables: HashMap> = HashMap::new(); - + let mut scope_tasks = Vec::new(); for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { - let scope_response = client - .request::(ScopesArguments { - frame_id: stack_frame.id, - }) - .await?; + let frame_id = stack_frame.id.clone(); + let client = client.clone(); + scope_tasks.push(async move { + anyhow::Ok(( + frame_id.clone(), + client + .request::(ScopesArguments { frame_id }) + .await?, + )) + }); + } - scopes.insert(stack_frame.id, scope_response.scopes.clone()); + let mut scopes: HashMap> = HashMap::new(); + let mut variables: HashMap> = HashMap::new(); - for scope in scope_response.scopes { - variables.insert( - scope.variables_reference, - client - .request::(VariablesArguments { - variables_reference: scope.variables_reference, - filter: None, - start: None, - count: None, - format: None, - }) - .await? - .variables, - ); + let mut variable_tasks = Vec::new(); + for (thread_id, response) in try_join_all(scope_tasks).await? { + scopes.insert(thread_id, response.scopes.clone()); + + for scope in response.scopes { + let scope_reference = scope.variables_reference.clone(); + let client = client.clone(); + variable_tasks.push(async move { + anyhow::Ok(( + scope_reference.clone(), + client + .request::(VariablesArguments { + variables_reference: scope_reference, + filter: None, + start: None, + count: None, + format: None, + }) + .await?, + )) + }); } } + for (scope_reference, response) in try_join_all(variable_tasks).await? { + variables.insert(scope_reference, response.variables.clone()); + } + this.update(&mut cx, |this, cx| { if let Some(entry) = client.thread_state().get_mut(&thread_id) { client.update_current_thread_id(Some(thread_id)); From ef5990d42787bbb51a4bc730eb098ac2ea153085 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 4 Jul 2024 14:52:50 +0200 Subject: [PATCH 065/650] Move ToggleBreakpoint action to editor instead of workspace --- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 2 +- crates/workspace/src/workspace.rs | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 293d9da7199cca..6791aca699a6dc 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -292,6 +292,7 @@ gpui::actions!( SplitSelectionIntoLines, Tab, TabPrev, + ToggleBreakpoint, ToggleGitBlame, ToggleGitBlameInline, ToggleSelectionMenu, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aa4d1660643ae8..5472d17c40e4fc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -148,7 +148,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId}; use workspace::{ searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId, }; -use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast, ToggleBreakpoint}; +use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast}; use crate::hover_links::find_url; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 46d2ad003c6938..ea58954d4ff3ec 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -131,7 +131,6 @@ actions!( SaveAs, SaveWithoutFormat, StartDebugger, - ToggleBreakpoint, ToggleBottomDock, ToggleCenteredLayout, ToggleLeftDock, From ac3b9f7a4c2cd53433cfeadc7290c14e62df3e9f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 4 Jul 2024 23:57:30 +0200 Subject: [PATCH 066/650] Wip show current file & column --- crates/debugger_ui/src/debugger_panel.rs | 67 +++++++++++++++++------- crates/editor/src/editor.rs | 25 +++++++++ crates/project/src/project.rs | 2 +- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c4c9cc9cbe5e0c..da288007333343 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -6,12 +6,14 @@ use dap::{ Scope, ScopesArguments, StackFrame, StackTraceArguments, StoppedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; +use editor::Editor; use futures::future::try_join_all; use gpui::{ actions, list, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, ListState, Model, Subscription, Task, View, ViewContext, WeakView, }; use project::Project; +use std::path::Path; use std::{collections::HashMap, sync::Arc}; use ui::{prelude::*, Tooltip}; use workspace::{ @@ -19,6 +21,8 @@ use workspace::{ Workspace, }; +enum DebugCurrentRowHighlight {} + actions!( debug_panel, [ @@ -362,6 +366,7 @@ impl DebugPanel { }) .await?; + let current_stack_frame = stack_trace_response.stack_frames.first().unwrap().clone(); let mut scope_tasks = Vec::new(); for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { let frame_id = stack_frame.id.clone(); @@ -407,30 +412,54 @@ impl DebugPanel { variables.insert(scope_reference, response.variables.clone()); } - this.update(&mut cx, |this, cx| { - if let Some(entry) = client.thread_state().get_mut(&thread_id) { - client.update_current_thread_id(Some(thread_id)); - - entry.current_stack_frame_id = stack_trace_response - .stack_frames - .clone() - .first() - .map(|f| f.id); - entry.stack_frames = stack_trace_response.stack_frames.clone(); - entry.scopes = scopes; - entry.variables = variables; - - if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { - this.stack_frame_list.reset(entry.stack_frames.len()); - } + let task = this.update(&mut cx, |this, cx| { + { + if let Some(entry) = client.thread_state().get_mut(&thread_id) { + client.update_current_thread_id(Some(thread_id)); + + entry.current_stack_frame_id = Some(current_stack_frame.clone().id); + entry.stack_frames = stack_trace_response.stack_frames.clone(); + entry.scopes = scopes; + entry.variables = variables; + + if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { + this.stack_frame_list.reset(entry.stack_frames.len()); + } - cx.notify(); + cx.notify(); + } } - anyhow::Ok(()) + let path = current_stack_frame.clone().source.unwrap().path.unwrap(); + + this.workspace.update(cx, |workspace, cx| { + let project_path = workspace.project().read_with(cx, |project, cx| { + project.project_path_for_absolute_path(&Path::new(&path), cx) + }); + + if let Some(project_path) = project_path { + workspace.open_path_preview(project_path, None, false, true, cx) + } else { + Task::ready(Err(anyhow::anyhow!( + "No project path found for path: {}", + path + ))) + } + }) + })??; + + let editor = task.await?.downcast::().unwrap(); + + editor.update(&mut cx, |editor, cx| { + editor.go_to_line::( + (current_stack_frame.line - 1) as u32, + (current_stack_frame.column - 1) as u32, + Some(cx.theme().colors().editor_highlighted_line_background), + cx, + ); }) }) - .detach(); + .detach_and_log_err(cx); } fn handle_thread_event( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5472d17c40e4fc..8895bf8e0e4b4e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8883,6 +8883,31 @@ impl Editor { } } + pub fn go_to_line( + &mut self, + row: u32, + column: u32, + highlight_color: Option, + cx: &mut ViewContext, + ) { + let snapshot = self.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(Point::new(row, column), Bias::Left); + let anchor = snapshot.buffer_snapshot.anchor_before(point); + self.clear_row_highlights::(); + self.highlight_rows::( + anchor..=anchor, + Some( + highlight_color + .unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background), + ), + true, + cx, + ); + self.request_autoscroll(Autoscroll::center(), cx); + } + fn seek_in_direction( &mut self, snapshot: &DisplaySnapshot, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5f804c09875b1e..fb20d70e3c0bae 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -23,7 +23,7 @@ use clock::ReplicaId; use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, - transport::{self, Events}, + transport::Events, SourceBreakpoint, }; use debounced_delay::DebouncedDelay; From 953a2b376ca04e222dbdba30f52f736057ee703d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 00:08:54 +0200 Subject: [PATCH 067/650] Make sure we don't resume other threads with action for one thread --- crates/dap/src/client.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 68275b7805f9ce..e97be528a414d8 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -295,7 +295,7 @@ impl DebugAdapterClient { pub async fn resume(&self, thread_id: u64) { self.request::(ContinueArguments { thread_id, - single_thread: None, + single_thread: Some(true), }) .await .log_err(); @@ -305,7 +305,7 @@ impl DebugAdapterClient { self.request::(NextArguments { thread_id, granularity: Some(SteppingGranularity::Statement), - single_thread: None, + single_thread: Some(true), }) .await .log_err(); @@ -316,7 +316,7 @@ impl DebugAdapterClient { thread_id, target_id: None, granularity: Some(SteppingGranularity::Statement), - single_thread: None, + single_thread: Some(true), }) .await .log_err(); @@ -326,7 +326,7 @@ impl DebugAdapterClient { self.request::(StepOutArguments { thread_id, granularity: Some(SteppingGranularity::Statement), - single_thread: None, + single_thread: Some(true), }) .await .log_err(); @@ -335,7 +335,7 @@ impl DebugAdapterClient { pub async fn step_back(&self, thread_id: u64) { self.request::(StepBackArguments { thread_id, - single_thread: None, + single_thread: Some(true), granularity: Some(SteppingGranularity::Statement), }) .await @@ -345,7 +345,7 @@ impl DebugAdapterClient { pub async fn restart(&self, thread_id: u64) { self.request::(StepBackArguments { thread_id, - single_thread: None, + single_thread: Some(true), granularity: Some(SteppingGranularity::Statement), }) .await From 12bef0830a38d7029d5b59abda2052d7999cfe08 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 14:25:01 +0200 Subject: [PATCH 068/650] Allow clicking on stack frames --- crates/dap/src/client.rs | 16 ++++ crates/debugger_ui/src/debugger_panel.rs | 116 +++++++++++++++-------- 2 files changed, 94 insertions(+), 38 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index e97be528a414d8..6b3fc60777c1ae 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -253,6 +253,22 @@ impl DebugAdapterClient { } } + pub fn update_current_stack_frame_id(&self, stack_frame_id: u64) { + if let Some(id) = self.current_thread_id() { + if let Some(thread_state) = self.thread_state().get_mut(&id) { + thread_state.current_stack_frame_id = Some(stack_frame_id); + }; + } + } + + pub fn current_stack_frame_id(&self) -> Option { + if let Some(thread_state) = self.current_thread_state() { + thread_state.current_stack_frame_id + } else { + None + } + } + pub async fn initialize(&mut self) -> Result { let args = dap_types::InitializeRequestArguments { client_id: Some("zed".to_owned()), diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index da288007333343..e3784ea24e2b64 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -134,6 +134,10 @@ impl DebugPanel { let stack_frame = self.stack_frame_for_index(ix, cx).unwrap(); let source = stack_frame.source.clone(); + let selected_frame_id = self + .debug_client(cx) + .and_then(|c| c.current_stack_frame_id()); + let is_selected_frame = Some(stack_frame.id) == selected_frame_id; let formatted_path = format!( "{}:{}", @@ -150,6 +154,21 @@ impl DebugPanel { move |cx| Tooltip::text(formatted_path.clone(), cx) }) .p_1() + .when(is_selected_frame, |this| { + this.bg(cx.theme().colors().element_hover) + }) + .on_click(cx.listener({ + let stack_frame = stack_frame.clone(); + move |this, _, cx| { + if let Some(client) = this.debug_client(cx) { + client.update_current_stack_frame_id(stack_frame.id); + this.go_to_stack_frame(&stack_frame, cx) + .detach_and_log_err(cx); + + cx.notify(); + }; + } + })) .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) .child( h_flex() @@ -343,6 +362,51 @@ impl DebugPanel { } } + fn go_to_stack_frame( + &self, + stack_frame: &StackFrame, + cx: &mut ViewContext, + ) -> Task> { + let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); + let row = (stack_frame.line - 1) as u32; + let column = (stack_frame.column - 1) as u32; + cx.spawn(move |this, mut cx| async move { + let task = this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |workspace, cx| { + let project_path = workspace.project().read_with(cx, |project, cx| { + project.project_path_for_absolute_path(&Path::new(&path), cx) + }); + + if let Some(project_path) = project_path { + workspace.open_path_preview(project_path, None, false, true, cx) + } else { + Task::ready(Err(anyhow::anyhow!( + "No project path found for path: {}", + path + ))) + } + }) + })??; + + let editor = task.await?.downcast::().unwrap(); + + this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + editor.go_to_line::( + row, + column, + Some(cx.theme().colors().editor_highlighted_line_background), + cx, + ); + }) + }) + })??; + + anyhow::Ok(()) + }) + } + fn handle_stopped_event( this: &mut Self, model: Model, @@ -413,51 +477,27 @@ impl DebugPanel { } let task = this.update(&mut cx, |this, cx| { - { - if let Some(entry) = client.thread_state().get_mut(&thread_id) { - client.update_current_thread_id(Some(thread_id)); - - entry.current_stack_frame_id = Some(current_stack_frame.clone().id); - entry.stack_frames = stack_trace_response.stack_frames.clone(); - entry.scopes = scopes; - entry.variables = variables; + if let Some(entry) = client.thread_state().get_mut(&thread_id) { + client.update_current_thread_id(Some(thread_id)); - if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { - this.stack_frame_list.reset(entry.stack_frames.len()); - } + entry.current_stack_frame_id = Some(current_stack_frame.clone().id); + entry.stack_frames = stack_trace_response.stack_frames.clone(); + entry.scopes = scopes; + entry.variables = variables; - cx.notify(); + if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { + this.stack_frame_list.reset(entry.stack_frames.len()); } - } - - let path = current_stack_frame.clone().source.unwrap().path.unwrap(); - this.workspace.update(cx, |workspace, cx| { - let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_absolute_path(&Path::new(&path), cx) - }); + cx.notify(); + } - if let Some(project_path) = project_path { - workspace.open_path_preview(project_path, None, false, true, cx) - } else { - Task::ready(Err(anyhow::anyhow!( - "No project path found for path: {}", - path - ))) - } - }) - })??; + this.go_to_stack_frame(¤t_stack_frame, cx) + })?; - let editor = task.await?.downcast::().unwrap(); + task.await?; - editor.update(&mut cx, |editor, cx| { - editor.go_to_line::( - (current_stack_frame.line - 1) as u32, - (current_stack_frame.column - 1) as u32, - Some(cx.theme().colors().editor_highlighted_line_background), - cx, - ); - }) + anyhow::Ok(()) }) .detach_and_log_err(cx); } From f4606bd9513c4a8cbff911216a8ce417a7874292 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 15:09:42 +0200 Subject: [PATCH 069/650] Don't go to stack frame if event is not ment for current client --- crates/debugger_ui/src/debugger_panel.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index e3784ea24e2b64..fd20fe5b05e0b0 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -487,12 +487,13 @@ impl DebugPanel { if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { this.stack_frame_list.reset(entry.stack_frames.len()); - } + cx.notify(); - cx.notify(); + return this.go_to_stack_frame(¤t_stack_frame, cx); + } } - this.go_to_stack_frame(¤t_stack_frame, cx) + Task::ready(anyhow::Ok(())) })?; task.await?; From dcf6f6ca3078613b152b85522233928a25efb9ec Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 15:50:11 +0200 Subject: [PATCH 070/650] Implement handle terminated event --- crates/debugger_ui/src/debugger_panel.rs | 40 +++++++++++++++++------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index fd20fe5b05e0b0..04def9935ad731 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,10 +1,10 @@ use anyhow::Result; use dap::client::{DebugAdapterClientId, ThreadState}; -use dap::requests::{Scopes, StackTrace, Variables}; +use dap::requests::{Disconnect, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - Scope, ScopesArguments, StackFrame, StackTraceArguments, StoppedEvent, ThreadEvent, - ThreadEventReason, Variable, VariablesArguments, + DisconnectArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, StoppedEvent, + TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; use editor::Editor; use futures::future::try_join_all; @@ -52,9 +52,9 @@ impl DebugPanel { .unwrap(); let _subscriptions = vec![cx.subscribe(&project, { - move |this: &mut Self, model, event, cx| { + move |this: &mut Self, _, event, cx| { if let project::Event::DebugClientEvent { client_id, event } = event { - Self::handle_debug_client_events(this, model, client_id, event, cx); + Self::handle_debug_client_events(this, client_id, event, cx); } } })]; @@ -329,7 +329,6 @@ impl DebugPanel { fn handle_debug_client_events( this: &mut Self, - model: Model, client_id: &DebugAdapterClientId, event: &Events, cx: &mut ViewContext, @@ -343,11 +342,11 @@ impl DebugPanel { }) .detach_and_log_err(cx); } - Events::Stopped(event) => Self::handle_stopped_event(this, model, client_id, event, cx), + Events::Stopped(event) => Self::handle_stopped_event(this, client_id, event, cx), Events::Continued(_) => {} Events::Exited(_) => {} - Events::Terminated(_) => {} - Events::Thread(event) => Self::handle_thread_event(this, model, client_id, event, cx), + Events::Terminated(event) => Self::handle_terminated_event(this, client_id, event, cx), + Events::Thread(event) => Self::handle_thread_event(this, client_id, event, cx), Events::Output(_) => {} Events::Breakpoint(_) => {} Events::Module(_) => {} @@ -409,7 +408,6 @@ impl DebugPanel { fn handle_stopped_event( this: &mut Self, - model: Model, client_id: &DebugAdapterClientId, event: &StoppedEvent, cx: &mut ViewContext, @@ -505,7 +503,6 @@ impl DebugPanel { fn handle_thread_event( this: &mut Self, - model: Model, client_id: &DebugAdapterClientId, event: &ThreadEvent, cx: &mut ViewContext, @@ -535,6 +532,27 @@ impl DebugPanel { cx.notify(); } } + + fn handle_terminated_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + event: &Option, + cx: &mut ViewContext, + ) { + let restart = event.as_ref().is_some_and(|e| e.restart.is_some()); + let client = this.debug_client_by_id(*client_id, cx); + + cx.spawn(|_, _| async move { + client + .request::(DisconnectArguments { + restart: Some(restart), + terminate_debuggee: None, + suspend_debuggee: None, + }) + .await + }) + .detach_and_log_err(cx); + } } impl EventEmitter for DebugPanel {} From d238675c1a5d0521532fdec9ae9101e4dc6a8fb4 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 16:31:07 +0200 Subject: [PATCH 071/650] Wip support for determing to use `launch` or `attach` request --- crates/dap/src/client.rs | 38 +++++++++++++++++--------------- crates/project/src/project.rs | 12 ++++++---- crates/task/src/lib.rs | 3 ++- crates/task/src/task_template.rs | 28 +++++++++++++++++------ 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 6b3fc60777c1ae..787d833e4aeaa1 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -3,13 +3,13 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ - ConfigurationDone, Continue, Initialize, Launch, Next, Pause, SetBreakpoints, StepBack, - StepIn, StepOut, + Attach, ConfigurationDone, Continue, Initialize, Launch, Next, Pause, SetBreakpoints, + StepBack, StepIn, StepOut, }, - ConfigurationDoneArguments, ContinueArguments, InitializeRequestArgumentsPathFormat, - LaunchRequestArguments, NextArguments, PauseArguments, Scope, SetBreakpointsArguments, - SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, - StepInArguments, StepOutArguments, SteppingGranularity, Variable, + AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, + InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, + Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, + StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, Variable, }; use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, @@ -34,7 +34,7 @@ use std::{ }, time::Duration, }; -use task::{DebugAdapterConfig, TransportType}; +use task::{DebugAdapterConfig, DebugConnectionType}; use util::ResultExt; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -70,11 +70,11 @@ impl DebugAdapterClient { project_path: PathBuf, cx: &mut AsyncAppContext, ) -> Result { - match config.transport { - TransportType::TCP => { + match config.connection { + DebugConnectionType::TCP => { Self::create_tcp_client(id, config, command, args, project_path, cx).await } - TransportType::STDIO => { + DebugConnectionType::STDIO => { Self::create_stdio_client(id, config, command, args, project_path, cx).await } } @@ -296,14 +296,16 @@ impl DebugAdapterClient { Ok(capabilities) } - pub async fn launch(&self) -> Result<()> { + pub async fn launch(&self, args: Option) -> Result<()> { self.request::(LaunchRequestArguments { - raw: self - .config - .launch_config - .clone() - .map(|c| c.config) - .unwrap_or(Value::Null), + raw: args.unwrap_or(Value::Null), + }) + .await + } + + pub async fn attach(&self, args: Option) -> Result<()> { + self.request::(AttachRequestArguments { + raw: args.unwrap_or(Value::Null), }) .await } @@ -379,7 +381,7 @@ impl DebugAdapterClient { path: PathBuf, breakpoints: Option>, ) -> Result { - let adapter_data = self.config.launch_config.clone().map(|c| c.config); + let adapter_data = self.config.request_args.clone().map(|c| c.args); self.request::(SetBreakpointsArguments { source: Source { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fb20d70e3c0bae..794450ccd02814 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -113,7 +113,7 @@ use std::{ }; use task::{ static_source::{StaticSource, TrackedFile}, - RevealStrategy, TaskContext, TaskTemplate, TaskVariables, VariableName, + DebugRequestType, RevealStrategy, TaskContext, TaskTemplate, TaskVariables, VariableName, }; use terminals::Terminals; use text::{Anchor, BufferId, LineEnding}; @@ -1083,11 +1083,12 @@ impl Project { let command = debug_template.command.clone(); let args = debug_template.args.clone(); + let request_args = adapter_config.clone().request_args.map(|a| a.args); let task = cx.spawn(|this, mut cx| async move { let mut client = DebugAdapterClient::new( id, - adapter_config, + adapter_config.clone(), &command, args.iter().map(|ele| &ele[..]).collect(), cwd.into(), @@ -1099,8 +1100,11 @@ impl Project { // initialize request client.initialize().await.log_err()?; - // launch request - client.launch().await.log_err()?; + // send correct request based on adapter config + match adapter_config.request { + DebugRequestType::Launch => client.launch(request_args).await.log_err()?, + DebugRequestType::Attach => client.attach(request_args).await.log_err()?, + }; let client = Arc::new(client); diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index e02156a2f749b9..ce4cd106dee856 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -13,7 +13,8 @@ use std::str::FromStr; use std::{borrow::Cow, path::Path}; pub use task_template::{ - DebugAdapterConfig, RevealStrategy, TaskTemplate, TaskTemplates, TaskType, TransportType, + DebugAdapterConfig, DebugConnectionType, RevealStrategy, TaskTemplate, TaskTemplates, TaskType, + DebugRequestType }; pub use vscode_format::VsCodeTaskFile; diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 1602c7642151e4..ab8e60ebadf489 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -72,7 +72,7 @@ pub enum TaskType { /// Represents the type of the debugger adapter connection #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] -pub enum TransportType { +pub enum DebugConnectionType { /// Connect to the debug adapter via TCP #[default] TCP, @@ -80,25 +80,39 @@ pub enum TransportType { STDIO, } +/// Represents the type that will determine which request to call on the debug adapter +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum DebugRequestType { + /// Call the `launch` request on the debug adapter + #[default] + Launch, + /// Call the `attach` request on the debug adapter + Attach, +} + /// Represents the configuration for the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugAdapterConfig { + /// The type of connection the adapter should use + #[serde(default)] + pub connection: DebugConnectionType, /// The port that the debug adapter is listening on pub port: u16, - /// The type of connection the adapter should use + /// The type of request that should be called on the debug adapter #[serde(default)] - pub transport: TransportType, - /// The configuration options that are send with the launch request + pub request: DebugRequestType, + /// The configuration options that are send with the `launch` or `attach` request /// to the debug adapter - pub launch_config: Option, + pub request_args: Option, } /// Represents the configuration for the debug adapter that is send with the launch request #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(transparent)] -pub struct DebugLaunchConfig { - pub config: serde_json::Value, +pub struct DebugRequestArgs { + pub args: serde_json::Value, } /// What to do with the terminal pane and tab, after the command was started. From 4aedc1cd0b6b408795a3a92cc2c812802788e0ef Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 16:44:27 +0200 Subject: [PATCH 072/650] Save request type on client --- crates/dap/src/client.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 787d833e4aeaa1..41b604c16efe05 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -34,7 +34,7 @@ use std::{ }, time::Duration, }; -use task::{DebugAdapterConfig, DebugConnectionType}; +use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType}; use util::ResultExt; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -59,6 +59,7 @@ pub struct DebugAdapterClient { client_rx: Arc>>, thread_state: Arc>>, // thread_id -> thread_state current_thread_id: Arc>>, + request_type: DebugRequestType, } impl DebugAdapterClient { @@ -146,10 +147,12 @@ impl DebugAdapterClient { let client_rx = Arc::new(Mutex::new(client_rx)); + let request_type = config.clone().request; let client = Self { id, config, client_rx, + request_type, _process: process, capabilities: None, server_tx: server_tx.clone(), @@ -229,6 +232,10 @@ impl DebugAdapterClient { self.id } + pub fn request_type(&self) -> DebugRequestType { + self.request_type.clone() + } + pub fn next_request_id(&self) -> u64 { self.request_count.fetch_add(1, Ordering::Relaxed) } From 00c5b83384f63e8e8e1a2c3a3ea14de5967cb82a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 16:50:43 +0200 Subject: [PATCH 073/650] Update handle terminated event to restart debugger --- crates/debugger_ui/src/debugger_panel.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 04def9935ad731..8b7a78561089ca 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -15,6 +15,7 @@ use gpui::{ use project::Project; use std::path::Path; use std::{collections::HashMap, sync::Arc}; +use task::DebugRequestType; use ui::{prelude::*, Tooltip}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -539,17 +540,28 @@ impl DebugPanel { event: &Option, cx: &mut ViewContext, ) { - let restart = event.as_ref().is_some_and(|e| e.restart.is_some()); + let restart_args = event.clone().and_then(|e| e.restart); let client = this.debug_client_by_id(*client_id, cx); cx.spawn(|_, _| async move { + let should_restart = restart_args.is_some(); + client .request::(DisconnectArguments { - restart: Some(restart), + restart: Some(should_restart), terminate_debuggee: None, suspend_debuggee: None, }) - .await + .await?; + + if should_restart { + match client.request_type() { + DebugRequestType::Launch => client.launch(restart_args).await, + DebugRequestType::Attach => client.attach(restart_args).await, + } + } else { + anyhow::Ok(()) + } }) .detach_and_log_err(cx); } From 9678cc9bc3973957b2757d803c125242514d6598 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 19:15:16 +0200 Subject: [PATCH 074/650] Remove editor highlights --- crates/debugger_ui/src/debugger_panel.rs | 89 +++++++++++++++++++++--- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 8b7a78561089ca..73fcdc63372e03 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -10,9 +10,8 @@ use editor::Editor; use futures::future::try_join_all; use gpui::{ actions, list, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, ListState, Model, Subscription, Task, View, ViewContext, WeakView, + FocusableView, ListState, Subscription, Task, View, ViewContext, WeakView, }; -use project::Project; use std::path::Path; use std::{collections::HashMap, sync::Arc}; use task::DebugRequestType; @@ -163,9 +162,8 @@ impl DebugPanel { move |this, _, cx| { if let Some(client) = this.debug_client(cx) { client.update_current_stack_frame_id(stack_frame.id); - this.go_to_stack_frame(&stack_frame, cx) + this.go_to_stack_frame(&stack_frame, client.clone(), false, cx) .detach_and_log_err(cx); - cx.notify(); }; } @@ -362,15 +360,75 @@ impl DebugPanel { } } + fn remove_highlights( + &self, + client: Arc, + cx: &mut ViewContext, + ) -> Task> { + let mut tasks = Vec::new(); + for (_, thread_state) in client.thread_state().clone() { + for stack_frame in thread_state.stack_frames { + tasks.push(self.remove_editor_highlight(&stack_frame, cx)); + } + } + + cx.spawn(|_, _| async move { + try_join_all(tasks).await?; + + anyhow::Ok(()) + }) + } + + fn remove_editor_highlight( + &self, + stack_frame: &StackFrame, + cx: &mut ViewContext, + ) -> Task> { + let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); + + cx.spawn(|this, mut cx| async move { + let task = this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |workspace, cx| { + let project_path = workspace.project().read_with(cx, |project, cx| { + project.project_path_for_absolute_path(&Path::new(&path), cx) + }); + + if let Some(project_path) = project_path { + workspace.open_path(project_path, None, false, cx) + } else { + Task::ready(Err(anyhow::anyhow!( + "No project path found for path: {}", + path + ))) + } + }) + })??; + + let editor = task.await?.downcast::().unwrap(); + + editor.update(&mut cx, |editor, _| { + editor.clear_row_highlights::(); + }) + }) + } + fn go_to_stack_frame( &self, stack_frame: &StackFrame, + client: Arc, + clear_highlights: bool, cx: &mut ViewContext, ) -> Task> { let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); let row = (stack_frame.line - 1) as u32; let column = (stack_frame.column - 1) as u32; + cx.spawn(move |this, mut cx| async move { + if clear_highlights { + this.update(&mut cx, |this, cx| this.remove_highlights(client, cx))? + .await?; + } + let task = this.update(&mut cx, |this, cx| { this.workspace.update(cx, |workspace, cx| { let project_path = workspace.project().read_with(cx, |project, cx| { @@ -420,6 +478,11 @@ impl DebugPanel { let client = this.debug_client_by_id(*client_id, cx); cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |this, cx| { + this.remove_highlights(client.clone(), cx) + })? + .await?; + let stack_trace_response = client .request::(StackTraceArguments { thread_id, @@ -488,7 +551,12 @@ impl DebugPanel { this.stack_frame_list.reset(entry.stack_frames.len()); cx.notify(); - return this.go_to_stack_frame(¤t_stack_frame, cx); + return this.go_to_stack_frame( + ¤t_stack_frame, + client.clone(), + true, + cx, + ); } } @@ -524,12 +592,17 @@ impl DebugPanel { if current_thread_id == Some(event.thread_id) { client.update_current_thread_id(None); if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { - this.stack_frame_list.reset(0); + cx.spawn({ + let client = client.clone(); + |this, mut cx| async move { + this.update(&mut cx, |this, cx| this.remove_highlights(client, cx))? + .await + } + }) + .detach_and_log_err(cx); } } - client.thread_state().remove(&event.thread_id); - cx.notify(); } } From ce8ec033f442fe9c39266439d8c04c35c04bd24d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 19:16:38 +0200 Subject: [PATCH 075/650] Fix clippy --- crates/task/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index ce4cd106dee856..fd0f3b25e2898a 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -13,8 +13,8 @@ use std::str::FromStr; use std::{borrow::Cow, path::Path}; pub use task_template::{ - DebugAdapterConfig, DebugConnectionType, RevealStrategy, TaskTemplate, TaskTemplates, TaskType, - DebugRequestType + DebugAdapterConfig, DebugConnectionType, DebugRequestType, RevealStrategy, TaskTemplate, + TaskTemplates, TaskType, }; pub use vscode_format::VsCodeTaskFile; From 9006e8fdffbb0226b635a796c7e70b8fe4357426 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 19:19:00 +0200 Subject: [PATCH 076/650] Make clippy's day --- crates/dap/src/client.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 41b604c16efe05..96c59704a7249b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -123,12 +123,12 @@ impl DebugAdapterClient { } async fn create_stdio_client( - id: DebugAdapterClientId, - config: DebugAdapterConfig, - command: &str, - args: Vec<&str>, - project_path: PathBuf, - cx: &mut AsyncAppContext, + _id: DebugAdapterClientId, + _config: DebugAdapterConfig, + _command: &str, + _args: Vec<&str>, + _project_path: PathBuf, + _cx: &mut AsyncAppContext, ) -> Result { todo!("not implemented") } @@ -241,7 +241,7 @@ impl DebugAdapterClient { } pub fn current_thread_id(&self) -> Option { - self.current_thread_id.lock().clone() + *self.current_thread_id.lock() } pub fn update_current_thread_id(&self, thread_id: Option) { From b827a35e44fa8c81e62bb3b5ff005396955614be Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 5 Jul 2024 19:26:12 +0200 Subject: [PATCH 077/650] Make clippy's day again with using async mutex --- crates/dap/src/client.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 96c59704a7249b..a4c90f16e78c74 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -56,7 +56,7 @@ pub struct DebugAdapterClient { request_count: AtomicU64, capabilities: Option, config: DebugAdapterConfig, - client_rx: Arc>>, + client_rx: Arc>>, thread_state: Arc>>, // thread_id -> thread_state current_thread_id: Arc>>, request_type: DebugRequestType, @@ -145,7 +145,7 @@ impl DebugAdapterClient { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); - let client_rx = Arc::new(Mutex::new(client_rx)); + let client_rx = Arc::new(smol::lock::Mutex::new(client_rx)); let request_type = config.clone().request; let client = Self { @@ -175,8 +175,7 @@ impl DebugAdapterClient { where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { - let mut client_rx = client.client_rx.lock(); - while let Some(payload) = client_rx.next().await { + while let Some(payload) = client.client_rx.lock().await.next().await { cx.update(|cx| match payload { Payload::Event(event) => event_handler(*event, cx), _ => unreachable!(), From 13e56010c1d4e4238e157a435ee9f2cf66d2fe6e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 7 Jul 2024 17:17:26 +0200 Subject: [PATCH 078/650] Add status types for thread state so we can disable buttons on this status --- crates/dap/src/client.rs | 15 ++++++ crates/debugger_ui/src/debugger_panel.rs | 64 ++++++++++++++---------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index a4c90f16e78c74..81f9b67263820d 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -37,12 +37,21 @@ use std::{ use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType}; use util::ResultExt; +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ThreadStatus { + #[default] + Running, + Stopped, + Ended, +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct DebugAdapterClientId(pub usize); #[derive(Debug, Default, Clone)] pub struct ThreadState { + pub status: ThreadStatus, pub stack_frames: Vec, pub scopes: HashMap>, // stack_frame_id -> scopes pub variables: HashMap>, // scope.variable_reference -> variables @@ -247,6 +256,12 @@ impl DebugAdapterClient { *self.current_thread_id.lock() = thread_id; } + pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) { + if let Some(thread_state) = self.thread_state().get_mut(&thread_id) { + thread_state.status = status; + }; + } + pub fn thread_state(&self) -> MutexGuard> { self.thread_state.lock() } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 73fcdc63372e03..097fb15f03ec7c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use dap::client::{DebugAdapterClientId, ThreadState}; +use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::requests::{Disconnect, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ @@ -538,17 +538,18 @@ impl DebugPanel { variables.insert(scope_reference, response.variables.clone()); } - let task = this.update(&mut cx, |this, cx| { - if let Some(entry) = client.thread_state().get_mut(&thread_id) { + this.update(&mut cx, |this, cx| { + if let Some(thread_state) = client.thread_state().get_mut(&thread_id) { client.update_current_thread_id(Some(thread_id)); - entry.current_stack_frame_id = Some(current_stack_frame.clone().id); - entry.stack_frames = stack_trace_response.stack_frames.clone(); - entry.scopes = scopes; - entry.variables = variables; + thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); + thread_state.stack_frames = stack_trace_response.stack_frames.clone(); + thread_state.scopes = scopes; + thread_state.variables = variables; + thread_state.status = ThreadStatus::Stopped; if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { - this.stack_frame_list.reset(entry.stack_frames.len()); + this.stack_frame_list.reset(thread_state.stack_frames.len()); cx.notify(); return this.go_to_stack_frame( @@ -561,11 +562,8 @@ impl DebugPanel { } Task::ready(anyhow::Ok(())) - })?; - - task.await?; - - anyhow::Ok(()) + })? + .await }) .detach_and_log_err(cx); } @@ -590,21 +588,20 @@ impl DebugPanel { } } else { if current_thread_id == Some(event.thread_id) { - client.update_current_thread_id(None); - if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { - cx.spawn({ - let client = client.clone(); - |this, mut cx| async move { - this.update(&mut cx, |this, cx| this.remove_highlights(client, cx))? - .await - } - }) - .detach_and_log_err(cx); - } - } + client.update_thread_state_status(event.thread_id, ThreadStatus::Ended); - cx.notify(); + cx.spawn({ + let client = client.clone(); + |this, mut cx| async move { + this.update(&mut cx, |this, cx| this.remove_highlights(client, cx))? + .await + } + }) + .detach_and_log_err(cx); + } } + + cx.notify(); } fn handle_terminated_event( @@ -638,6 +635,13 @@ impl DebugPanel { }) .detach_and_log_err(cx); } + + fn disable_button(&self, cx: &mut ViewContext) -> bool { + let thread_state = self.debug_client(cx).and_then(|c| c.current_thread_state()); + thread_state + .and_then(|s| Some(s.status != ThreadStatus::Stopped)) + .unwrap_or(true) + } } impl EventEmitter for DebugPanel {} @@ -702,6 +706,8 @@ impl Panel for DebugPanel { impl Render for DebugPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let disable_button = self.disable_button(cx); + v_flex() .key_context("DebugPanel") .track_focus(&self.focus_handle) @@ -721,6 +727,7 @@ impl Render for DebugPanel { .on_click( cx.listener(|_, _, cx| cx.dispatch_action(Continue.boxed_clone())), ) + .disabled(disable_button) .tooltip(move |cx| Tooltip::text("Continue debug", cx)), ) .child( @@ -728,6 +735,7 @@ impl Render for DebugPanel { .on_click( cx.listener(|_, _, cx| cx.dispatch_action(StepOver.boxed_clone())), ) + .disabled(disable_button) .tooltip(move |cx| Tooltip::text("Step over", cx)), ) .child( @@ -735,6 +743,7 @@ impl Render for DebugPanel { .on_click( cx.listener(|_, _, cx| cx.dispatch_action(StepIn.boxed_clone())), ) + .disabled(disable_button) .tooltip(move |cx| Tooltip::text("Go in", cx)), ) .child( @@ -742,6 +751,7 @@ impl Render for DebugPanel { .on_click( cx.listener(|_, _, cx| cx.dispatch_action(StepOut.boxed_clone())), ) + .disabled(disable_button) .tooltip(move |cx| Tooltip::text("Go out", cx)), ) .child( @@ -749,6 +759,7 @@ impl Render for DebugPanel { .on_click( cx.listener(|_, _, cx| cx.dispatch_action(Restart.boxed_clone())), ) + .disabled(disable_button) .tooltip(move |cx| Tooltip::text("Restart", cx)), ) .child( @@ -756,6 +767,7 @@ impl Render for DebugPanel { .on_click( cx.listener(|_, _, cx| cx.dispatch_action(Pause.boxed_clone())), ) + .disabled(disable_button) .tooltip(move |cx| Tooltip::text("Pause", cx)), ), ) From 817760688a48ef204db9c37761ea0942cc62dc4c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 7 Jul 2024 17:32:29 +0200 Subject: [PATCH 079/650] Don't support removing current thread_id --- crates/dap/src/client.rs | 4 ++-- crates/debugger_ui/src/debugger_panel.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 81f9b67263820d..287b968e3de032 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -252,8 +252,8 @@ impl DebugAdapterClient { *self.current_thread_id.lock() } - pub fn update_current_thread_id(&self, thread_id: Option) { - *self.current_thread_id.lock() = thread_id; + pub fn update_current_thread_id(&self, thread_id: u64) { + *self.current_thread_id.lock() = Some(thread_id); } pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 097fb15f03ec7c..e7ab9e53f19acd 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -540,7 +540,7 @@ impl DebugPanel { this.update(&mut cx, |this, cx| { if let Some(thread_state) = client.thread_state().get_mut(&thread_id) { - client.update_current_thread_id(Some(thread_id)); + client.update_current_thread_id(thread_id); thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); thread_state.stack_frames = stack_trace_response.stack_frames.clone(); @@ -584,7 +584,7 @@ impl DebugPanel { .insert(event.thread_id, ThreadState::default()); if current_thread_id.is_none() { - client.update_current_thread_id(Some(event.thread_id)); + client.update_current_thread_id(event.thread_id); } } else { if current_thread_id == Some(event.thread_id) { From da84aa1ac21ed24627e3968a2b034ab68bffe4ad Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 7 Jul 2024 20:21:38 +0200 Subject: [PATCH 080/650] Fix panic with `line - 1` and `column - 1` --- crates/debugger_ui/src/debugger_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index e7ab9e53f19acd..a161425b72e099 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -420,8 +420,8 @@ impl DebugPanel { cx: &mut ViewContext, ) -> Task> { let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); - let row = (stack_frame.line - 1) as u32; - let column = (stack_frame.column - 1) as u32; + let row = (stack_frame.line.saturating_sub(1)) as u32; + let column = (stack_frame.column.saturating_sub(1)) as u32; cx.spawn(move |this, mut cx| async move { if clear_highlights { From a87409813cc7f4137f8c398e5da8aeca9749b826 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 7 Jul 2024 20:24:33 +0200 Subject: [PATCH 081/650] Fix that thread is not always known in stopped event This fixes an issue that not all debuggers send the `thread` event and we assumed that we always know the thread inside the handling code of the `stopped` event. --- crates/debugger_ui/src/debugger_panel.rs | 32 +++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a161425b72e099..0e5fd102cf1bf4 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -539,26 +539,24 @@ impl DebugPanel { } this.update(&mut cx, |this, cx| { - if let Some(thread_state) = client.thread_state().get_mut(&thread_id) { - client.update_current_thread_id(thread_id); + let mut thread_state = client.thread_state(); + let thread_state = thread_state + .entry(thread_id) + .or_insert(ThreadState::default()); - thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); - thread_state.stack_frames = stack_trace_response.stack_frames.clone(); - thread_state.scopes = scopes; - thread_state.variables = variables; - thread_state.status = ThreadStatus::Stopped; + client.update_current_thread_id(thread_id); - if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { - this.stack_frame_list.reset(thread_state.stack_frames.len()); - cx.notify(); + thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); + thread_state.stack_frames = stack_trace_response.stack_frames.clone(); + thread_state.scopes = scopes; + thread_state.variables = variables; + thread_state.status = ThreadStatus::Stopped; - return this.go_to_stack_frame( - ¤t_stack_frame, - client.clone(), - true, - cx, - ); - } + if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { + this.stack_frame_list.reset(thread_state.stack_frames.len()); + cx.notify(); + + return this.go_to_stack_frame(¤t_stack_frame, client.clone(), true, cx); } Task::ready(anyhow::Ok(())) From 361bbec3a01913513953bc5a8e876ae4fc8e0536 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 7 Jul 2024 20:37:33 +0200 Subject: [PATCH 082/650] Make some more room to show stack trace --- crates/debugger_ui/src/debugger_panel.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 0e5fd102cf1bf4..aba4d713d4a2d4 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -121,11 +121,9 @@ impl DebugPanel { fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { v_flex() - .w_full() + .w_1_3() .gap_3() .h_full() - .flex_grow() - .flex_shrink_0() .child(list(self.stack_frame_list.clone()).size_full()) .into_any() } @@ -198,6 +196,7 @@ impl DebugPanel { }; div() + .w_3_4() .gap_3() .text_ui_sm(cx) .children( @@ -715,6 +714,7 @@ impl Render for DebugPanel { .capture_action(cx.listener(Self::handle_step_out_action)) .capture_action(cx.listener(Self::handle_restart_action)) .capture_action(cx.listener(Self::handle_pause_action)) + .size_full() .items_start() .child( h_flex() From cce58570dc19511ab3a38216a554364d31eeb375 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 7 Jul 2024 20:51:32 +0200 Subject: [PATCH 083/650] Remove unneeded clone --- crates/debugger_ui/src/debugger_panel.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index aba4d713d4a2d4..638c8fba596916 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -37,11 +37,11 @@ actions!( ); pub struct DebugPanel { - pub focus_handle: FocusHandle, - pub size: Pixels, + focus_handle: FocusHandle, + size: Pixels, _subscriptions: Vec, - pub workspace: WeakView, - pub stack_frame_list: ListState, + workspace: WeakView, + stack_frame_list: ListState, } impl DebugPanel { @@ -75,7 +75,7 @@ impl DebugPanel { focus_handle: cx.focus_handle(), size: px(300.), _subscriptions, - workspace: workspace.clone(), + workspace, stack_frame_list, } }) From 648daa32372ce0924dfc8f01b2e488497cc6ac61 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 10 Jul 2024 22:31:49 +0200 Subject: [PATCH 084/650] WIP start adding support for debugging multiple threads at the same time Start showing all the threads as tabs so you can debug multiple threads at the same time. --- crates/dap/src/client.rs | 34 +- crates/debugger_ui/Cargo.toml | 4 +- crates/debugger_ui/src/debugger_panel.rs | 603 ++++++------------ crates/debugger_ui/src/debugger_panel_item.rs | 423 ++++++++++++ crates/debugger_ui/src/lib.rs | 5 +- 5 files changed, 618 insertions(+), 451 deletions(-) create mode 100644 crates/debugger_ui/src/debugger_panel_item.rs diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 287b968e3de032..052d1ab350bdf8 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -67,7 +67,6 @@ pub struct DebugAdapterClient { config: DebugAdapterConfig, client_rx: Arc>>, thread_state: Arc>>, // thread_id -> thread_state - current_thread_id: Arc>>, request_type: DebugRequestType, } @@ -166,7 +165,6 @@ impl DebugAdapterClient { capabilities: None, server_tx: server_tx.clone(), request_count: AtomicU64::new(0), - current_thread_id: Arc::new(Mutex::new(None)), thread_state: Arc::new(Mutex::new(HashMap::new())), }; @@ -248,14 +246,6 @@ impl DebugAdapterClient { self.request_count.fetch_add(1, Ordering::Relaxed) } - pub fn current_thread_id(&self) -> Option { - *self.current_thread_id.lock() - } - - pub fn update_current_thread_id(&self, thread_id: u64) { - *self.current_thread_id.lock() = Some(thread_id); - } - pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) { if let Some(thread_state) = self.thread_state().get_mut(&thread_id) { thread_state.status = status; @@ -266,28 +256,8 @@ impl DebugAdapterClient { self.thread_state.lock() } - pub fn current_thread_state(&self) -> Option { - if let Some(id) = self.current_thread_id() { - self.thread_state().clone().get(&id).cloned() - } else { - None - } - } - - pub fn update_current_stack_frame_id(&self, stack_frame_id: u64) { - if let Some(id) = self.current_thread_id() { - if let Some(thread_state) = self.thread_state().get_mut(&id) { - thread_state.current_stack_frame_id = Some(stack_frame_id); - }; - } - } - - pub fn current_stack_frame_id(&self) -> Option { - if let Some(thread_state) = self.current_thread_state() { - thread_state.current_stack_frame_id - } else { - None - } + pub fn thread_state_by_id(&self, thread_id: u64) -> ThreadState { + self.thread_state.lock().get(&thread_id).cloned().unwrap() } pub async fn initialize(&mut self) -> Result { diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 0f333d0325dbd3..9477c45ed7c32d 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -20,10 +20,10 @@ picker.workspace = true project.workspace = true serde.workspace = true serde_derive.workspace = true +task.workspace = true +tasks_ui.workspace = true ui.workspace = true workspace.workspace = true -tasks_ui.workspace = true -task.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 638c8fba596916..f7b2d5c0715c7e 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -9,47 +9,60 @@ use dap::{ use editor::Editor; use futures::future::try_join_all; use gpui::{ - actions, list, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, ListState, Subscription, Task, View, ViewContext, WeakView, + actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, + Subscription, Task, View, ViewContext, WeakView, }; use std::path::Path; use std::{collections::HashMap, sync::Arc}; use task::DebugRequestType; -use ui::{prelude::*, Tooltip}; +use ui::prelude::*; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; +use workspace::{NewFile, Pane}; + +use crate::debugger_panel_item::DebugPanelItem; enum DebugCurrentRowHighlight {} -actions!( - debug_panel, - [ - TogglePanel, - Continue, - StepOver, - StepIn, - StepOut, - Restart, - Pause - ] -); +#[derive(Debug)] +pub enum DebugPanelEvent { + Stopped((DebugAdapterClientId, StoppedEvent)), + Thread((DebugAdapterClientId, ThreadEvent)), +} + +actions!(debug_panel, [TogglePanel]); pub struct DebugPanel { - focus_handle: FocusHandle, size: Pixels, - _subscriptions: Vec, + pane: View, + focus_handle: FocusHandle, workspace: WeakView, - stack_frame_list: ListState, + _subscriptions: Vec, } impl DebugPanel { - pub fn new(workspace: WeakView, cx: &mut WindowContext) -> View { - cx.new_view(|cx: &mut ViewContext| { - let project = workspace - .update(cx, |workspace, _| workspace.project().clone()) - .unwrap(); + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> View { + cx.new_view(|cx| { + let pane = cx.new_view(|cx| { + let mut pane = Pane::new( + workspace.weak_handle(), + workspace.project().clone(), + Default::default(), + None, + NewFile.boxed_clone(), + cx, + ); + pane.set_can_split(false, cx); + pane.set_can_navigate(true, cx); + pane.display_nav_history_buttons(None); + pane.set_should_display_tab_bar(|_| true); + + pane + }); + + let project = workspace.project().clone(); let _subscriptions = vec![cx.subscribe(&project, { move |this: &mut Self, _, event, cx| { @@ -59,24 +72,12 @@ impl DebugPanel { } })]; - let view = cx.view().downgrade(); - let stack_frame_list = - ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { - if let Some(view) = view.upgrade() { - view.update(cx, |view, cx| { - view.render_stack_frame(ix, cx).into_any_element() - }) - } else { - div().into_any() - } - }); - Self { - focus_handle: cx.focus_handle(), + pane, size: px(300.), _subscriptions, - workspace, - stack_frame_list, + focus_handle: cx.focus_handle(), + workspace: workspace.weak_handle(), } }) } @@ -85,13 +86,8 @@ impl DebugPanel { workspace: WeakView, cx: AsyncWindowContext, ) -> Task>> { - cx.spawn(|mut cx| async move { cx.update(|cx| DebugPanel::new(workspace, cx)) }) - } - - fn stack_frame_for_index(&self, ix: usize, cx: &mut ViewContext) -> Option { - self.debug_client(cx).and_then(|c| { - c.current_thread_state() - .and_then(|f| f.stack_frames.get(ix).cloned()) + cx.spawn(|mut cx| async move { + workspace.update(&mut cx, |workspace, cx| DebugPanel::new(workspace, cx)) }) } @@ -119,212 +115,6 @@ impl DebugPanel { .unwrap() } - fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { - v_flex() - .w_1_3() - .gap_3() - .h_full() - .child(list(self.stack_frame_list.clone()).size_full()) - .into_any() - } - - fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { - let stack_frame = self.stack_frame_for_index(ix, cx).unwrap(); - - let source = stack_frame.source.clone(); - let selected_frame_id = self - .debug_client(cx) - .and_then(|c| c.current_stack_frame_id()); - let is_selected_frame = Some(stack_frame.id) == selected_frame_id; - - let formatted_path = format!( - "{}:{}", - source.clone().and_then(|s| s.name).unwrap_or_default(), - stack_frame.line, - ); - - v_flex() - .rounded_md() - .group("") - .id(("stack-frame", stack_frame.id)) - .tooltip({ - let formatted_path = formatted_path.clone(); - move |cx| Tooltip::text(formatted_path.clone(), cx) - }) - .p_1() - .when(is_selected_frame, |this| { - this.bg(cx.theme().colors().element_hover) - }) - .on_click(cx.listener({ - let stack_frame = stack_frame.clone(); - move |this, _, cx| { - if let Some(client) = this.debug_client(cx) { - client.update_current_stack_frame_id(stack_frame.id); - this.go_to_stack_frame(&stack_frame, client.clone(), false, cx) - .detach_and_log_err(cx); - cx.notify(); - }; - } - })) - .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) - .child( - h_flex() - .gap_0p5() - .text_ui_sm(cx) - .child(stack_frame.name.clone()) - .child(formatted_path), - ) - .child( - h_flex() - .text_ui_xs(cx) - .text_color(cx.theme().colors().text_muted) - .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), - ) - .into_any() - } - - fn render_scopes( - &self, - thread_state: &ThreadState, - cx: &mut ViewContext, - ) -> impl IntoElement { - let Some(scopes) = thread_state - .current_stack_frame_id - .and_then(|id| thread_state.scopes.get(&id)) - else { - return div().child("No scopes for this thread yet").into_any(); - }; - - div() - .w_3_4() - .gap_3() - .text_ui_sm(cx) - .children( - scopes - .iter() - .map(|scope| self.render_scope(thread_state, scope, cx)), - ) - .into_any() - } - - fn render_scope( - &self, - thread_state: &ThreadState, - scope: &Scope, - cx: &mut ViewContext, - ) -> impl IntoElement { - div() - .id(("scope", scope.variables_reference)) - .p_1() - .text_ui_sm(cx) - .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) - .child(scope.name.clone()) - .child( - div() - .ml_2() - .child(self.render_variables(thread_state, scope, cx)), - ) - .into_any() - } - - fn render_variables( - &self, - thread_state: &ThreadState, - scope: &Scope, - cx: &mut ViewContext, - ) -> impl IntoElement { - let Some(variables) = thread_state.variables.get(&scope.variables_reference) else { - return div().child("No variables for this thread yet").into_any(); - }; - - div() - .gap_3() - .text_ui_sm(cx) - .children( - variables - .iter() - .map(|variable| self.render_variable(variable, cx)), - ) - .into_any() - } - - fn render_variable(&self, variable: &Variable, cx: &mut ViewContext) -> impl IntoElement { - h_flex() - .id(("variable", variable.variables_reference)) - .p_1() - .gap_1() - .text_ui_sm(cx) - .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) - .child(variable.name.clone()) - .child( - div() - .text_ui_xs(cx) - .text_color(cx.theme().colors().text_muted) - .child(variable.value.clone()), - ) - .into_any() - } - - fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { - if let Some(client) = self.debug_client(cx) { - if let Some(thread_id) = client.current_thread_id() { - cx.background_executor() - .spawn(async move { client.resume(thread_id).await }) - .detach(); - } - } - } - - fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext) { - if let Some(client) = self.debug_client(cx) { - if let Some(thread_id) = client.current_thread_id() { - cx.background_executor() - .spawn(async move { client.step_over(thread_id).await }) - .detach(); - } - } - } - - fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext) { - if let Some(client) = self.debug_client(cx) { - if let Some(thread_id) = client.current_thread_id() { - cx.background_executor() - .spawn(async move { client.step_in(thread_id).await }) - .detach(); - } - } - } - - fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext) { - if let Some(client) = self.debug_client(cx) { - if let Some(thread_id) = client.current_thread_id() { - cx.background_executor() - .spawn(async move { client.step_out(thread_id).await }) - .detach(); - } - } - } - - fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext) { - if let Some(client) = self.debug_client(cx) { - if let Some(thread_id) = client.current_thread_id() { - cx.background_executor() - .spawn(async move { client.restart(thread_id).await }) - .detach(); - } - } - } - - fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext) { - if let Some(client) = self.debug_client(cx) { - if let Some(thread_id) = client.current_thread_id() { - cx.background_executor() - .spawn(async move { client.pause(thread_id).await }) - .detach(); - } - } - } - fn handle_debug_client_events( this: &mut Self, client_id: &DebugAdapterClientId, @@ -378,6 +168,31 @@ impl DebugPanel { }) } + fn remove_highlights_for_thread( + &self, + client: Arc, + thread_id: u64, + cx: &mut ViewContext, + ) -> Task> { + let mut tasks = Vec::new(); + + if let Some(thread_state) = client.thread_state().get(&thread_id) { + for stack_frame in thread_state.stack_frames.clone() { + tasks.push(self.remove_editor_highlight(&stack_frame, cx)); + } + } + + if tasks.is_empty() { + return Task::ready(Ok(())); + } + + cx.spawn(|_, _| async move { + try_join_all(tasks).await?; + + anyhow::Ok(()) + }) + } + fn remove_editor_highlight( &self, stack_frame: &StackFrame, @@ -474,93 +289,123 @@ impl DebugPanel { return; }; - let client = this.debug_client_by_id(*client_id, cx); + let client_id = client_id.clone(); + let client = this.debug_client_by_id(client_id.clone(), cx); - cx.spawn(|this, mut cx| async move { - this.update(&mut cx, |this, cx| { - this.remove_highlights(client.clone(), cx) - })? - .await?; - - let stack_trace_response = client - .request::(StackTraceArguments { - thread_id, - start_frame: None, - levels: None, - format: None, - }) + let client_id = client_id.clone(); + cx.spawn({ + let event = event.clone(); + |this, mut cx| async move { + this.update(&mut cx, |this, cx| { + this.remove_highlights_for_thread(client.clone(), thread_id, cx) + })? .await?; - let current_stack_frame = stack_trace_response.stack_frames.first().unwrap().clone(); - let mut scope_tasks = Vec::new(); - for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { - let frame_id = stack_frame.id.clone(); - let client = client.clone(); - scope_tasks.push(async move { - anyhow::Ok(( - frame_id.clone(), - client - .request::(ScopesArguments { frame_id }) - .await?, - )) - }); - } - - let mut scopes: HashMap> = HashMap::new(); - let mut variables: HashMap> = HashMap::new(); - - let mut variable_tasks = Vec::new(); - for (thread_id, response) in try_join_all(scope_tasks).await? { - scopes.insert(thread_id, response.scopes.clone()); + let stack_trace_response = client + .request::(StackTraceArguments { + thread_id, + start_frame: None, + levels: None, + format: None, + }) + .await?; - for scope in response.scopes { - let scope_reference = scope.variables_reference.clone(); + let current_stack_frame = + stack_trace_response.stack_frames.first().unwrap().clone(); + let mut scope_tasks = Vec::new(); + for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { + let frame_id = stack_frame.id.clone(); let client = client.clone(); - variable_tasks.push(async move { + scope_tasks.push(async move { anyhow::Ok(( - scope_reference.clone(), + frame_id.clone(), client - .request::(VariablesArguments { - variables_reference: scope_reference, - filter: None, - start: None, - count: None, - format: None, - }) + .request::(ScopesArguments { frame_id }) .await?, )) }); } - } - for (scope_reference, response) in try_join_all(variable_tasks).await? { - variables.insert(scope_reference, response.variables.clone()); - } + let mut scopes: HashMap> = HashMap::new(); + let mut variables: HashMap> = HashMap::new(); + + let mut variable_tasks = Vec::new(); + for (thread_id, response) in try_join_all(scope_tasks).await? { + scopes.insert(thread_id, response.scopes.clone()); + + for scope in response.scopes { + let scope_reference = scope.variables_reference.clone(); + let client = client.clone(); + variable_tasks.push(async move { + anyhow::Ok(( + scope_reference.clone(), + client + .request::(VariablesArguments { + variables_reference: scope_reference, + filter: None, + start: None, + count: None, + format: None, + }) + .await?, + )) + }); + } + } - this.update(&mut cx, |this, cx| { - let mut thread_state = client.thread_state(); - let thread_state = thread_state - .entry(thread_id) - .or_insert(ThreadState::default()); + for (scope_reference, response) in try_join_all(variable_tasks).await? { + variables.insert(scope_reference, response.variables.clone()); + } - client.update_current_thread_id(thread_id); + this.update(&mut cx, |this, cx| { + let mut thread_state = client.thread_state(); + let thread_state = thread_state + .entry(thread_id) + .or_insert(ThreadState::default()); + + thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); + thread_state.stack_frames = stack_trace_response.stack_frames.clone(); + thread_state.scopes = scopes; + thread_state.variables = variables; + thread_state.status = ThreadStatus::Stopped; + + let focus = this.focus_handle(cx).contains_focused(cx); + + let existing_item = this + .pane + .read(cx) + .items() + .filter_map(|item| item.downcast::()) + .find(|item| { + let item = item.read(cx); + + item.client().id() == client_id && item.thread_id() == thread_id + }); + + if let None = existing_item { + let debug_panel = cx.view().clone(); + this.pane.update(cx, |this, cx| { + let tab = cx.new_view(|cx| { + DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx) + }); + + this.add_item(Box::new(tab.clone()), focus, focus, None, cx) + }); + } - thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); - thread_state.stack_frames = stack_trace_response.stack_frames.clone(); - thread_state.scopes = scopes; - thread_state.variables = variables; - thread_state.status = ThreadStatus::Stopped; + cx.emit(DebugPanelEvent::Stopped((client_id, event))); - if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { - this.stack_frame_list.reset(thread_state.stack_frames.len()); - cx.notify(); + // if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { + // // this.stack_frame_list.reset(thread_state.stack_frames.len()); + // // cx.notify(); - return this.go_to_stack_frame(¤t_stack_frame, client.clone(), true, cx); - } + // return this.go_to_stack_frame(¤t_stack_frame, client.clone(), true, cx); + // } - Task::ready(anyhow::Ok(())) - })? - .await + Task::ready(anyhow::Ok(())) + })? + .await + } }) .detach_and_log_err(cx); } @@ -572,33 +417,28 @@ impl DebugPanel { cx: &mut ViewContext, ) { let client = this.debug_client_by_id(*client_id, cx); - - let current_thread_id = client.current_thread_id(); + let thread_id = event.thread_id; if event.reason == ThreadEventReason::Started { client .thread_state() - .insert(event.thread_id, ThreadState::default()); - - if current_thread_id.is_none() { - client.update_current_thread_id(event.thread_id); - } + .insert(thread_id, ThreadState::default()); } else { - if current_thread_id == Some(event.thread_id) { - client.update_thread_state_status(event.thread_id, ThreadStatus::Ended); + client.update_thread_state_status(thread_id.clone(), ThreadStatus::Ended); - cx.spawn({ - let client = client.clone(); - |this, mut cx| async move { - this.update(&mut cx, |this, cx| this.remove_highlights(client, cx))? - .await - } - }) - .detach_and_log_err(cx); - } + cx.spawn({ + let client = client.clone(); + |this, mut cx| async move { + this.update(&mut cx, |this, cx| { + this.remove_highlights_for_thread(client, thread_id, cx) + })? + .await + } + }) + .detach_and_log_err(cx); } - cx.notify(); + cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); } fn handle_terminated_event( @@ -632,16 +472,11 @@ impl DebugPanel { }) .detach_and_log_err(cx); } - - fn disable_button(&self, cx: &mut ViewContext) -> bool { - let thread_state = self.debug_client(cx).and_then(|c| c.current_thread_state()); - thread_state - .and_then(|s| Some(s.status != ThreadStatus::Stopped)) - .unwrap_or(true) - } } impl EventEmitter for DebugPanel {} +impl EventEmitter for DebugPanel {} +impl EventEmitter for DebugPanel {} impl FocusableView for DebugPanel { fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { @@ -703,79 +538,17 @@ impl Panel for DebugPanel { impl Render for DebugPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let disable_button = self.disable_button(cx); - v_flex() .key_context("DebugPanel") .track_focus(&self.focus_handle) - .capture_action(cx.listener(Self::handle_continue_action)) - .capture_action(cx.listener(Self::handle_step_over_action)) - .capture_action(cx.listener(Self::handle_step_in_action)) - .capture_action(cx.listener(Self::handle_step_out_action)) - .capture_action(cx.listener(Self::handle_restart_action)) - .capture_action(cx.listener(Self::handle_pause_action)) + // .capture_action(cx.listener(Self::handle_continue_action)) + // .capture_action(cx.listener(Self::handle_step_over_action)) + // .capture_action(cx.listener(Self::handle_step_in_action)) + // .capture_action(cx.listener(Self::handle_step_out_action)) + // .capture_action(cx.listener(Self::handle_restart_action)) + // .capture_action(cx.listener(Self::handle_pause_action)) .size_full() - .items_start() - .child( - h_flex() - .p_2() - .gap_2() - .child( - IconButton::new("debug-continue", IconName::DebugContinue) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(Continue.boxed_clone())), - ) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Continue debug", cx)), - ) - .child( - IconButton::new("debug-step-over", IconName::DebugStepOver) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(StepOver.boxed_clone())), - ) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Step over", cx)), - ) - .child( - IconButton::new("debug-step-in", IconName::DebugStepInto) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(StepIn.boxed_clone())), - ) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Go in", cx)), - ) - .child( - IconButton::new("debug-step-out", IconName::DebugStepOut) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(StepOut.boxed_clone())), - ) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Go out", cx)), - ) - .child( - IconButton::new("debug-restart", IconName::DebugRestart) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(Restart.boxed_clone())), - ) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Restart", cx)), - ) - .child( - IconButton::new("debug-pause", IconName::DebugStop) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(Pause.boxed_clone())), - ) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Pause", cx)), - ), - ) - .child(h_flex().size_full().items_start().p_1().gap_4().when_some( - self.debug_client(cx).and_then(|c| c.current_thread_state()), - |this, thread_state| { - this.child(self.render_stack_frames(cx)) - .child(self.render_scopes(&thread_state, cx)) - }, - )) + .child(self.pane.clone()) .into_any() } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs new file mode 100644 index 00000000000000..c85b97e92bcdc0 --- /dev/null +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -0,0 +1,423 @@ +use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, ThreadEventReason, Variable}; +use gpui::{ + actions, list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListState, + Subscription, View, +}; +use std::sync::Arc; +use ui::WindowContext; +use ui::{prelude::*, Tooltip}; +use workspace::item::{Item, ItemEvent}; + +use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; + +pub struct DebugPanelItem { + thread_id: u64, + focus_handle: FocusHandle, + stack_frame_list: ListState, + client: Arc, + _subscriptions: Vec, + current_stack_frame_id: Option, +} + +actions!( + debug_panel_item, + [Continue, StepOver, StepIn, StepOut, Restart, Pause] +); + +impl DebugPanelItem { + pub fn new( + debug_panel: View, + client: Arc, + thread_id: u64, + cx: &mut ViewContext, + ) -> Self { + let focus_handle = cx.focus_handle(); + let weakview = cx.view().downgrade(); + let stack_frame_list = + ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { + if let Some(view) = weakview.upgrade() { + view.update(cx, |view, cx| { + view.render_stack_frame(ix, cx).into_any_element() + }) + } else { + div().into_any() + } + }); + + let _subscriptions = vec![cx.subscribe(&debug_panel, { + move |this: &mut Self, _, event: &DebugPanelEvent, cx| { + match event { + DebugPanelEvent::Stopped((client_id, event)) => { + Self::handle_stopped_event(this, client_id, event) + } + DebugPanelEvent::Thread((client_id, event)) => { + Self::handle_thread_event(this, client_id, event, cx) + } + }; + } + })]; + + Self { + client, + thread_id, + focus_handle, + _subscriptions, + stack_frame_list, + current_stack_frame_id: None, + } + } + + fn handle_stopped_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + event: &StoppedEvent, + ) { + if *client_id != this.client.id() { + return; + } + + if event.thread_id != Some(this.thread_id) { + return; + } + + if let Some(thread_state) = this.current_thread_state() { + this.stack_frame_list.reset(thread_state.stack_frames.len()); + }; + } + + fn handle_thread_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + event: &ThreadEvent, + cx: &mut ViewContext, + ) { + if *client_id != this.client.id() { + return; + } + + if event.thread_id != this.thread_id { + return; + } + + // dbg!(event); + } +} + +impl EventEmitter for DebugPanelItem {} + +impl FocusableView for DebugPanelItem { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for DebugPanelItem { + type Event = ItemEvent; + + fn tab_content( + &self, + params: workspace::item::TabContentParams, + _: &WindowContext, + ) -> AnyElement { + Label::new(format!("Thread {}", self.thread_id)) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() + } +} + +impl DebugPanelItem { + pub fn client(&self) -> Arc { + self.client.clone() + } + + pub fn thread_id(&self) -> u64 { + self.thread_id + } + + fn stack_frame_for_index(&self, ix: usize) -> StackFrame { + self.client + .thread_state_by_id(self.thread_id) + .stack_frames + .get(ix) + .cloned() + .unwrap() + } + + fn current_thread_state(&self) -> Option { + self.client.thread_state().get(&self.thread_id).cloned() + } + + fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { + v_flex() + .w_1_3() + .gap_3() + .h_full() + .child(list(self.stack_frame_list.clone()).size_full()) + .into_any() + } + + fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { + let stack_frame = self.stack_frame_for_index(ix); + + let source = stack_frame.source.clone(); + let selected_frame_id = self.current_stack_frame_id; + let is_selected_frame = Some(stack_frame.id) == selected_frame_id; + + let formatted_path = format!( + "{}:{}", + source.clone().and_then(|s| s.name).unwrap_or_default(), + stack_frame.line, + ); + + v_flex() + .rounded_md() + .group("") + .id(("stack-frame", stack_frame.id)) + .tooltip({ + let formatted_path = formatted_path.clone(); + move |cx| Tooltip::text(formatted_path.clone(), cx) + }) + .p_1() + .when(is_selected_frame, |this| { + this.bg(cx.theme().colors().element_hover) + }) + .on_click(cx.listener({ + let stack_frame = stack_frame.clone(); + move |this, _, cx| { + this.current_stack_frame_id = Some(stack_frame.id); + // TODO: + // this.go_to_stack_frame(&stack_frame, this.client.clone(), false, cx) + // .detach_and_log_err(cx); + cx.notify(); + } + })) + .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .child( + h_flex() + .gap_0p5() + .text_ui_sm(cx) + .child(stack_frame.name.clone()) + .child(formatted_path), + ) + .child( + h_flex() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), + ) + .into_any() + } + + fn render_scopes( + &self, + thread_state: &ThreadState, + cx: &mut ViewContext, + ) -> impl IntoElement { + let Some(scopes) = thread_state + .current_stack_frame_id + .and_then(|id| thread_state.scopes.get(&id)) + else { + return div().child("No scopes for this thread yet").into_any(); + }; + + div() + .w_3_4() + .gap_3() + .text_ui_sm(cx) + .children( + scopes + .iter() + .map(|scope| self.render_scope(thread_state, scope, cx)), + ) + .into_any() + } + + fn render_scope( + &self, + thread_state: &ThreadState, + scope: &Scope, + cx: &mut ViewContext, + ) -> impl IntoElement { + div() + .id(("scope", scope.variables_reference)) + .p_1() + .text_ui_sm(cx) + .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .child(scope.name.clone()) + .child( + div() + .ml_2() + .child(self.render_variables(thread_state, scope, cx)), + ) + .into_any() + } + + fn render_variables( + &self, + thread_state: &ThreadState, + scope: &Scope, + cx: &mut ViewContext, + ) -> impl IntoElement { + let Some(variables) = thread_state.variables.get(&scope.variables_reference) else { + return div().child("No variables for this thread yet").into_any(); + }; + + div() + .gap_3() + .text_ui_sm(cx) + .children( + variables + .iter() + .map(|variable| self.render_variable(variable, cx)), + ) + .into_any() + } + + fn render_variable(&self, variable: &Variable, cx: &mut ViewContext) -> impl IntoElement { + h_flex() + .id(("variable", variable.variables_reference)) + .p_1() + .gap_1() + .text_ui_sm(cx) + .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .child(variable.name.clone()) + .child( + div() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .child(variable.value.clone()), + ) + .into_any() + } + + fn disable_button(&self) -> bool { + let thread_state = self.current_thread_state(); + thread_state + .and_then(|s| Some(s.status != ThreadStatus::Stopped)) + .unwrap_or(true) + } + + fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { + let client = self.client.clone(); + let thread_id = self.thread_id; + cx.background_executor() + .spawn(async move { client.resume(thread_id).await }) + .detach(); + } + + fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext) { + let client = self.client.clone(); + let thread_id = self.thread_id; + cx.background_executor() + .spawn(async move { client.step_over(thread_id).await }) + .detach(); + } + + fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext) { + let client = self.client.clone(); + let thread_id = self.thread_id; + cx.background_executor() + .spawn(async move { client.step_in(thread_id).await }) + .detach(); + } + + fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext) { + let client = self.client.clone(); + let thread_id = self.thread_id; + cx.background_executor() + .spawn(async move { client.step_out(thread_id).await }) + .detach(); + } + + fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext) { + let client = self.client.clone(); + let thread_id = self.thread_id; + cx.background_executor() + .spawn(async move { client.restart(thread_id).await }) + .detach(); + } + + fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext) { + let client = self.client.clone(); + let thread_id = self.thread_id; + cx.background_executor() + .spawn(async move { client.pause(thread_id).await }) + .detach(); + } +} + +impl Render for DebugPanelItem { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let disable_button = self.disable_button(); + + v_flex() + .key_context("DebugPanelItem") + .track_focus(&self.focus_handle) + .capture_action(cx.listener(Self::handle_continue_action)) + .capture_action(cx.listener(Self::handle_step_over_action)) + .capture_action(cx.listener(Self::handle_step_in_action)) + .capture_action(cx.listener(Self::handle_step_out_action)) + .capture_action(cx.listener(Self::handle_restart_action)) + .capture_action(cx.listener(Self::handle_pause_action)) + .p_2() + .size_full() + .items_start() + .child( + h_flex() + .gap_2() + .child( + IconButton::new("debug-continue", IconName::DebugContinue) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Continue))), + ) + .disabled(disable_button) + .tooltip(move |cx| Tooltip::text("Continue debug", cx)), + ) + .child( + IconButton::new("debug-step-over", IconName::DebugStepOver) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOver))), + ) + .disabled(disable_button) + .tooltip(move |cx| Tooltip::text("Step over", cx)), + ) + .child( + IconButton::new("debug-step-in", IconName::DebugStepInto) + .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepIn)))) + .disabled(disable_button) + .tooltip(move |cx| Tooltip::text("Go in", cx)), + ) + .child( + IconButton::new("debug-step-out", IconName::DebugStepOut) + .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOut)))) + .disabled(disable_button) + .tooltip(move |cx| Tooltip::text("Go out", cx)), + ) + .child( + IconButton::new("debug-restart", IconName::DebugRestart) + .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Restart)))) + .disabled(disable_button) + .tooltip(move |cx| Tooltip::text("Restart", cx)), + ) + .child( + IconButton::new("debug-pause", IconName::DebugStop) + .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Pause)))) + .disabled(disable_button) + .tooltip(move |cx| Tooltip::text("Pause", cx)), + ), + ) + .child(h_flex().size_full().items_start().p_1().gap_4().when_some( + self.current_thread_state(), + |this, thread_state| { + this.child(self.render_stack_frames(cx)) + .child(self.render_scopes(&thread_state, cx)) + }, + )) + .into_any() + } +} diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index da5739ea085e45..aead9f36120229 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,8 +1,9 @@ use debugger_panel::{DebugPanel, TogglePanel}; -use gpui::{AppContext, Task, ViewContext}; +use gpui::{AppContext, ViewContext}; use workspace::{StartDebugger, Workspace}; pub mod debugger_panel; +mod debugger_panel_item; pub fn init(cx: &mut AppContext) { cx.observe_new_views( @@ -13,7 +14,7 @@ pub fn init(cx: &mut AppContext) { }) .register_action( |workspace: &mut Workspace, - action: &StartDebugger, + _: &StartDebugger, cx: &mut ViewContext<'_, Workspace>| { tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach(); }, From d15ff2d06f36119f733e89ad3d1e50ca29f552ea Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 11 Jul 2024 19:13:26 +0200 Subject: [PATCH 085/650] Don't spawn task for empty tasks vec --- crates/debugger_ui/src/debugger_panel.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index f7b2d5c0715c7e..816a90a6b56ee5 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -155,12 +155,16 @@ impl DebugPanel { cx: &mut ViewContext, ) -> Task> { let mut tasks = Vec::new(); - for (_, thread_state) in client.thread_state().clone() { - for stack_frame in thread_state.stack_frames { + for thread_state in client.thread_state().values() { + for stack_frame in thread_state.stack_frames.clone() { tasks.push(self.remove_editor_highlight(&stack_frame, cx)); } } + if tasks.is_empty() { + return Task::ready(Ok(())); + } + cx.spawn(|_, _| async move { try_join_all(tasks).await?; @@ -175,7 +179,6 @@ impl DebugPanel { cx: &mut ViewContext, ) -> Task> { let mut tasks = Vec::new(); - if let Some(thread_state) = client.thread_state().get(&thread_id) { for stack_frame in thread_state.stack_frames.clone() { tasks.push(self.remove_editor_highlight(&stack_frame, cx)); From f287c897a4b57520e738c56d3981e9222b261757 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 11 Jul 2024 19:13:48 +0200 Subject: [PATCH 086/650] Remove commented out code --- crates/debugger_ui/src/debugger_panel.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 816a90a6b56ee5..26a91dce181aa9 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -540,16 +540,10 @@ impl Panel for DebugPanel { } impl Render for DebugPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { v_flex() .key_context("DebugPanel") .track_focus(&self.focus_handle) - // .capture_action(cx.listener(Self::handle_continue_action)) - // .capture_action(cx.listener(Self::handle_step_over_action)) - // .capture_action(cx.listener(Self::handle_step_in_action)) - // .capture_action(cx.listener(Self::handle_step_out_action)) - // .capture_action(cx.listener(Self::handle_restart_action)) - // .capture_action(cx.listener(Self::handle_pause_action)) .size_full() .child(self.pane.clone()) .into_any() From 49da08ffa4464b2151e7aeebf6821539b1b864a0 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 11 Jul 2024 20:44:55 +0200 Subject: [PATCH 087/650] Make goto stack trace frame work again after stopped event --- crates/debugger_ui/src/debugger_panel.rs | 161 +++++++++--------- crates/debugger_ui/src/debugger_panel_item.rs | 35 ++-- 2 files changed, 101 insertions(+), 95 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 26a91dce181aa9..05dced80524552 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -149,15 +149,67 @@ impl DebugPanel { } } + pub fn go_to_stack_frame( + stack_frame: &StackFrame, + client: Arc, + clear_highlights: bool, + cx: &mut ViewContext, + ) -> Task> { + let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); + let row = (stack_frame.line.saturating_sub(1)) as u32; + let column = (stack_frame.column.saturating_sub(1)) as u32; + + cx.spawn(move |this, mut cx| async move { + if clear_highlights { + // TODO: + // this.update(&mut cx, |this, cx| this.remove_highlights(client, cx))? + // .await?; + } + + let task = this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |workspace, cx| { + let project_path = workspace.project().read_with(cx, |project, cx| { + project.project_path_for_absolute_path(&Path::new(&path), cx) + }); + + if let Some(project_path) = project_path { + workspace.open_path_preview(project_path, None, false, true, cx) + } else { + Task::ready(Err(anyhow::anyhow!( + "No project path found for path: {}", + path + ))) + } + }) + })??; + + let editor = task.await?.downcast::().unwrap(); + + this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + editor.go_to_line::( + row, + column, + Some(cx.theme().colors().editor_highlighted_line_background), + cx, + ); + }) + }) + })??; + + anyhow::Ok(()) + }) + } + fn remove_highlights( - &self, client: Arc, cx: &mut ViewContext, ) -> Task> { let mut tasks = Vec::new(); for thread_state in client.thread_state().values() { for stack_frame in thread_state.stack_frames.clone() { - tasks.push(self.remove_editor_highlight(&stack_frame, cx)); + tasks.push(Self::remove_editor_highlight(&stack_frame, cx)); } } @@ -173,7 +225,6 @@ impl DebugPanel { } fn remove_highlights_for_thread( - &self, client: Arc, thread_id: u64, cx: &mut ViewContext, @@ -181,7 +232,7 @@ impl DebugPanel { let mut tasks = Vec::new(); if let Some(thread_state) = client.thread_state().get(&thread_id) { for stack_frame in thread_state.stack_frames.clone() { - tasks.push(self.remove_editor_highlight(&stack_frame, cx)); + tasks.push(Self::remove_editor_highlight(&stack_frame, cx)); } } @@ -197,7 +248,6 @@ impl DebugPanel { } fn remove_editor_highlight( - &self, stack_frame: &StackFrame, cx: &mut ViewContext, ) -> Task> { @@ -229,59 +279,6 @@ impl DebugPanel { }) } - fn go_to_stack_frame( - &self, - stack_frame: &StackFrame, - client: Arc, - clear_highlights: bool, - cx: &mut ViewContext, - ) -> Task> { - let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); - let row = (stack_frame.line.saturating_sub(1)) as u32; - let column = (stack_frame.column.saturating_sub(1)) as u32; - - cx.spawn(move |this, mut cx| async move { - if clear_highlights { - this.update(&mut cx, |this, cx| this.remove_highlights(client, cx))? - .await?; - } - - let task = this.update(&mut cx, |this, cx| { - this.workspace.update(cx, |workspace, cx| { - let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_absolute_path(&Path::new(&path), cx) - }); - - if let Some(project_path) = project_path { - workspace.open_path_preview(project_path, None, false, true, cx) - } else { - Task::ready(Err(anyhow::anyhow!( - "No project path found for path: {}", - path - ))) - } - }) - })??; - - let editor = task.await?.downcast::().unwrap(); - - this.update(&mut cx, |this, cx| { - this.workspace.update(cx, |_, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_line::( - row, - column, - Some(cx.theme().colors().editor_highlighted_line_background), - cx, - ); - }) - }) - })??; - - anyhow::Ok(()) - }) - } - fn handle_stopped_event( this: &mut Self, client_id: &DebugAdapterClientId, @@ -299,11 +296,6 @@ impl DebugPanel { cx.spawn({ let event = event.clone(); |this, mut cx| async move { - this.update(&mut cx, |this, cx| { - this.remove_highlights_for_thread(client.clone(), thread_id, cx) - })? - .await?; - let stack_trace_response = client .request::(StackTraceArguments { thread_id, @@ -372,8 +364,6 @@ impl DebugPanel { thread_state.variables = variables; thread_state.status = ThreadStatus::Stopped; - let focus = this.focus_handle(cx).contains_focused(cx); - let existing_item = this .pane .read(cx) @@ -392,18 +382,24 @@ impl DebugPanel { DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx) }); - this.add_item(Box::new(tab.clone()), focus, focus, None, cx) + this.add_item(Box::new(tab.clone()), false, false, None, cx) }); } cx.emit(DebugPanelEvent::Stopped((client_id, event))); - // if Some(client.id()) == this.debug_client(cx).map(|c| c.id()) { - // // this.stack_frame_list.reset(thread_state.stack_frames.len()); - // // cx.notify(); - - // return this.go_to_stack_frame(¤t_stack_frame, client.clone(), true, cx); - // } + if let Some(item) = this.pane.read(cx).active_item() { + if let Some(pane) = item.downcast::() { + if pane.read(cx).thread_id() == thread_id { + return Self::go_to_stack_frame( + ¤t_stack_frame, + client.clone(), + true, + cx, + ); + } + } + } Task::ready(anyhow::Ok(())) })? @@ -429,16 +425,17 @@ impl DebugPanel { } else { client.update_thread_state_status(thread_id.clone(), ThreadStatus::Ended); - cx.spawn({ - let client = client.clone(); - |this, mut cx| async move { - this.update(&mut cx, |this, cx| { - this.remove_highlights_for_thread(client, thread_id, cx) - })? - .await - } - }) - .detach_and_log_err(cx); + // TODO: we want to figure out for witch clients/threads we should remove the highlights + // cx.spawn({ + // let client = client.clone(); + // |this, mut cx| async move { + // this.update(&mut cx, |_, cx| { + // Self::remove_highlights_for_thread(client, thread_id, cx) + // })? + // .await + // } + // }) + // .detach_and_log_err(cx); } cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index c85b97e92bcdc0..f9f61032d6dd4c 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,9 +1,13 @@ +use anyhow::Result; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; -use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, ThreadEventReason, Variable}; +use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable}; +use editor::Editor; +use futures::future::try_join_all; use gpui::{ actions, list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListState, - Subscription, View, + Subscription, Task, View, WeakView, }; +use std::path::Path; use std::sync::Arc; use ui::WindowContext; use ui::{prelude::*, Tooltip}; @@ -68,16 +72,20 @@ impl DebugPanelItem { } } + fn should_skip_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + thread_id: u64, + ) -> bool { + thread_id != this.thread_id || *client_id != this.client.id() + } + fn handle_stopped_event( this: &mut Self, client_id: &DebugAdapterClientId, event: &StoppedEvent, ) { - if *client_id != this.client.id() { - return; - } - - if event.thread_id != Some(this.thread_id) { + if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or_default()) { return; } @@ -92,11 +100,7 @@ impl DebugPanelItem { event: &ThreadEvent, cx: &mut ViewContext, ) { - if *client_id != this.client.id() { - return; - } - - if event.thread_id != this.thread_id { + if Self::should_skip_event(this, client_id, event.thread_id) { return; } @@ -190,10 +194,15 @@ impl DebugPanelItem { let stack_frame = stack_frame.clone(); move |this, _, cx| { this.current_stack_frame_id = Some(stack_frame.id); + + // let client = this.client(); + // DebugPanel::go_to_stack_frame(&stack_frame, client, true, cx) + // .detach_and_log_err(cx); + // TODO: // this.go_to_stack_frame(&stack_frame, this.client.clone(), false, cx) // .detach_and_log_err(cx); - cx.notify(); + // cx.notify(); } })) .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) From 5ab95f1e1a8c16db4a504b49948925121fa6c992 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 11 Jul 2024 20:51:56 +0200 Subject: [PATCH 088/650] Remove unused method --- crates/debugger_ui/src/debugger_panel.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 05dced80524552..71288d3f44b87c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -91,15 +91,6 @@ impl DebugPanel { }) } - fn debug_client(&self, cx: &mut ViewContext) -> Option> { - self.workspace - .update(cx, |this, cx| { - this.project().read(cx).running_debug_adapters().next() - }) - .ok() - .flatten() - } - fn debug_client_by_id( &self, client_id: DebugAdapterClientId, From f108d4c705ca38faff82c431ec1aee5ad4d779d7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 11 Jul 2024 20:54:41 +0200 Subject: [PATCH 089/650] Replace find method with any Because we don't use the result of the find so any would be better in this case --- crates/debugger_ui/src/debugger_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 71288d3f44b87c..9930e67ba4b546 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -360,13 +360,13 @@ impl DebugPanel { .read(cx) .items() .filter_map(|item| item.downcast::()) - .find(|item| { + .any(|item| { let item = item.read(cx); item.client().id() == client_id && item.thread_id() == thread_id }); - if let None = existing_item { + if existing_item { let debug_panel = cx.view().clone(); this.pane.update(cx, |this, cx| { let tab = cx.new_view(|cx| { From 4a6f6151f04c5c634b75fb85eb1b85be3cd3cda2 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 11 Jul 2024 20:59:02 +0200 Subject: [PATCH 090/650] Remove editor highlights when debugger terminated event was received --- crates/debugger_ui/src/debugger_panel.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9930e67ba4b546..518cd5963d26a8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -441,9 +441,12 @@ impl DebugPanel { let restart_args = event.clone().and_then(|e| e.restart); let client = this.debug_client_by_id(*client_id, cx); - cx.spawn(|_, _| async move { + cx.spawn(|this, mut cx| async move { let should_restart = restart_args.is_some(); + this.update(&mut cx, |_, cx| Self::remove_highlights(client.clone(), cx))? + .await?; + client .request::(DisconnectArguments { restart: Some(should_restart), From 68dd3c90c2a5e70a3d4ba07105e282daaebd5d8c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 12 Jul 2024 21:10:39 +0200 Subject: [PATCH 091/650] Fix we never created the first pane OOPS Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/debugger_ui/src/debugger_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 518cd5963d26a8..a56110cb4780c9 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -366,7 +366,7 @@ impl DebugPanel { item.client().id() == client_id && item.thread_id() == thread_id }); - if existing_item { + if !existing_item { let debug_panel = cx.view().clone(); this.pane.update(cx, |this, cx| { let tab = cx.new_view(|cx| { From 014ffbce2e21f091b6eb202d268fb1a4bc3d47f4 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 12 Jul 2024 21:11:38 +0200 Subject: [PATCH 092/650] Send the initial breakpoints from the project We now send the initial breakpoints that we created before the debug adapter was running. Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/debugger_ui/src/debugger_panel.rs | 16 +++++++- crates/project/src/project.rs | 51 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a56110cb4780c9..5e47c5ee85d6ff 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -115,8 +115,20 @@ impl DebugPanel { match event { Events::Initialized(_) => { let client = this.debug_client_by_id(*client_id, cx); - cx.spawn(|_, _| async move { - // TODO: send all the current breakpoints + + cx.spawn(|this, mut cx| async move { + let task = this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + let client = client.clone(); + + project.send_breakpoints(client, cx) + }) + }) + })??; + + task.await?; + client.configuration_done().await }) .detach_and_log_err(cx); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 77dc8afdd4d706..455e7ae3e55b80 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1059,6 +1059,57 @@ impl Project { }) } + pub fn send_breakpoints( + &self, + client: Arc, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(|project, mut cx| async move { + let task = project.update(&mut cx, |project, cx| { + let mut tasks = Vec::new(); + + for (buffer_id, breakpoints) in project.breakpoints.iter() { + let res = maybe!({ + let buffer = project.buffer_for_id(*buffer_id)?; + + let project_path = buffer.read(cx).project_path(cx)?; + let worktree = project.worktree_for_id(project_path.worktree_id, cx)?; + let path = worktree.read(cx).absolutize(&project_path.path).ok()?; + + Some((path, breakpoints)) + }); + + if let Some((path, breakpoints)) = res { + tasks.push( + client.set_breakpoints( + path, + Some( + breakpoints + .iter() + .map(|b| SourceBreakpoint { + line: b.row as u64, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + }) + .collect::>(), + ), + ), + ); + } + } + + try_join_all(tasks) + })?; + + task.await?; + + Ok(()) + }) + } + pub fn start_debug_adapter_client( &mut self, debug_task: task::ResolvedTask, From 8d99f9b7d2bc8b61693471dc35861c41b7595b61 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 12 Jul 2024 21:58:18 +0200 Subject: [PATCH 093/650] Fix send all breakpoints when toggling breakpoints We now send all the breakpoints for the current buffer when toggling one breakpoint. Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/editor/src/editor.rs | 4 ++-- crates/project/src/project.rs | 35 +++++++++-------------------------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8895bf8e0e4b4e..9907a8d41326ed 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5898,9 +5898,9 @@ impl Editor { }, ); } - + project.update(cx, |project, cx| { - project.update_breakpoint(buffer, row, cx); + project.update_breakpoint(buffer, row + 1, cx); }); cx.notify(); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 455e7ae3e55b80..25f02fe3b8f9d7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1200,18 +1200,9 @@ impl Project { row: BufferRow, cx: &mut ModelContext, ) { - let buffer = buffer.read(cx); - let Some(abs_path) = maybe!({ - let project_path = buffer.project_path(cx)?; - let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; - worktree.read(cx).absolutize(&project_path.path).ok() - }) else { - return; - }; - let breakpoints_for_buffer = self .breakpoints - .entry(buffer.remote_id()) + .entry(buffer.read(cx).remote_id()) .or_insert(Vec::new()); if let Some(ix) = breakpoints_for_buffer @@ -1220,7 +1211,7 @@ impl Project { { breakpoints_for_buffer.remove(ix); } else { - breakpoints_for_buffer.push(Breakpoint { row: row + 1 }); + breakpoints_for_buffer.push(Breakpoint { row }); } let clients = self @@ -1232,23 +1223,15 @@ impl Project { }) .collect::>(); + let mut tasks = Vec::new(); + for client in clients { + tasks.push(self.send_breakpoints(client, cx)); + } + cx.background_executor() .spawn(async move { - for client in clients { - client - .set_breakpoints( - abs_path.clone(), - Some(vec![SourceBreakpoint { - line: (row + 1) as u64, - condition: None, - hit_condition: None, - log_message: None, - column: None, - mode: None, - }]), - ) - .await?; - } + try_join_all(tasks).await?; + anyhow::Ok(()) }) .detach_and_log_err(cx) From 9ea9b41e7348ae85991bdec0dbc946dc54715635 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 12 Jul 2024 22:18:01 +0200 Subject: [PATCH 094/650] Merge main into debugger Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- Cargo.lock | 88 +++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf3e2f5a5c99ec..32fb3e8b86532b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,7 +87,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f" dependencies = [ "base64 0.22.0", - "bitflags 2.4.2", + "bitflags 2.6.0", "home", "libc", "log", @@ -110,7 +110,7 @@ version = "0.24.1-dev" source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f" dependencies = [ "base64 0.22.0", - "bitflags 2.4.2", + "bitflags 2.6.0", "home", "libc", "log", @@ -1605,9 +1605,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -1637,7 +1637,7 @@ source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b598 dependencies = [ "ash", "ash-window", - "bitflags 2.4.2", + "bitflags 2.6.0", "block", "bytemuck", "codespan-reporting", @@ -1921,7 +1921,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "log", "polling 3.3.2", "rustix 0.38.32", @@ -2856,7 +2856,7 @@ name = "cosmic-text" version = "0.11.2" source = "git+https://github.com/pop-os/cosmic-text?rev=542b20c#542b20ca4376a3b5de5fa629db1a4ace44e18e0c" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "fontdb", "log", "rangemap", @@ -3277,7 +3277,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types#6e85834f9948cb5e5f085f7d5792235696ea78e2" +source = "git+https://github.com/zed-industries/dap-types#1d4d7676d9b82e7915278982b8a4c2de46d7dbd2" dependencies = [ "serde", "serde_json", @@ -4117,7 +4117,7 @@ name = "feedback" version = "0.1.0" dependencies = [ "anyhow", - "bitflags 2.4.2", + "bitflags 2.6.0", "client", "db", "editor", @@ -4451,7 +4451,7 @@ dependencies = [ name = "fsevent" version = "0.1.0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "core-foundation", "fsevent-sys 3.1.0", "parking_lot", @@ -4762,7 +4762,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "libc", "libgit2-sys", "log", @@ -4873,7 +4873,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "gpu-alloc-types", ] @@ -4894,7 +4894,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", ] [[package]] @@ -5153,7 +5153,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f7acb9683d7c7068aa46d47557bfa4e35a277964b350d9504a87b03610163fd" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "byteorder", "heed-traits", "heed-types", @@ -6753,7 +6753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e" dependencies = [ "bit-set", - "bitflags 2.4.2", + "bitflags 2.6.0", "codespan-reporting", "hexf-parse", "indexmap 2.2.6", @@ -6854,7 +6854,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "libc", "memoffset", @@ -6866,7 +6866,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -6935,7 +6935,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "crossbeam-channel", "filetime", "fsevent-sys 4.1.0", @@ -7279,9 +7279,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "5.1.2" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32" +checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" dependencies = [ "is-wsl", "libc", @@ -7308,7 +7308,7 @@ version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -8321,7 +8321,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "memchr", "unicase", ] @@ -9132,7 +9132,7 @@ version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "errno 0.3.8", "itoa", "libc", @@ -9207,7 +9207,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "bytemuck", "libm", "smallvec", @@ -9430,7 +9430,7 @@ version = "0.1.0" dependencies = [ "any_vec", "anyhow", - "bitflags 2.4.2", + "bitflags 2.6.0", "client", "collections", "editor", @@ -10182,7 +10182,7 @@ dependencies = [ "atoi", "base64 0.21.7", "bigdecimal", - "bitflags 2.4.2", + "bitflags 2.6.0", "byteorder", "bytes 1.5.0", "chrono", @@ -10229,7 +10229,7 @@ dependencies = [ "atoi", "base64 0.21.7", "bigdecimal", - "bitflags 2.4.2", + "bitflags 2.6.0", "byteorder", "chrono", "crc", @@ -10650,7 +10650,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aef1f9d4c1dbdd1cb3a63be9efd2f04d8ddbc919d46112982c76818ffc2f1a7" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cap-fs-ext", "cap-std", "fd-lock", @@ -10954,18 +10954,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -11375,7 +11375,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "bytes 1.5.0", "futures-core", "futures-util", @@ -12146,7 +12146,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cursor-icon", "log", "serde", @@ -12298,7 +12298,7 @@ version = "0.201.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "indexmap 2.2.6", "semver", ] @@ -12565,7 +12565,7 @@ checksum = "371d828b6849ea06d598ae7dd1c316e8dd9e99b76f77d93d5886cb25c7f8e188" dependencies = [ "anyhow", "async-trait", - "bitflags 2.4.2", + "bitflags 2.6.0", "bytes 1.5.0", "cap-fs-ext", "cap-net-ext", @@ -12652,7 +12652,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "rustix 0.38.32", "wayland-backend", "wayland-scanner", @@ -12675,7 +12675,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -12687,7 +12687,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -12806,7 +12806,7 @@ checksum = "ae1136a209614ace00b0c11f04dc7cf42540773be3b22eff6ad165110aba29c1" dependencies = [ "anyhow", "async-trait", - "bitflags 2.4.2", + "bitflags 2.6.0", "thiserror", "tracing", "wasmtime", @@ -13236,7 +13236,7 @@ version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "windows-sys 0.52.0", ] @@ -13255,7 +13255,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "wit-bindgen-rt", "wit-bindgen-rust-macro", ] @@ -13311,7 +13311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" dependencies = [ "anyhow", - "bitflags 2.4.2", + "bitflags 2.6.0", "indexmap 2.2.6", "log", "serde", @@ -13517,7 +13517,7 @@ name = "xim-parser" version = "0.2.1" source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", ] [[package]] From a583efd9b9983643a88631b7335abd27293a96c7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 12 Jul 2024 22:34:42 +0200 Subject: [PATCH 095/650] Give clippy the best day of his life --- crates/debugger_ui/src/debugger_panel_item.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index f9f61032d6dd4c..8d593d5ec20bf4 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,13 +1,10 @@ -use anyhow::Result; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable}; -use editor::Editor; -use futures::future::try_join_all; + use gpui::{ actions, list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListState, - Subscription, Task, View, WeakView, + Subscription, View, }; -use std::path::Path; use std::sync::Arc; use ui::WindowContext; use ui::{prelude::*, Tooltip}; @@ -98,13 +95,13 @@ impl DebugPanelItem { this: &mut Self, client_id: &DebugAdapterClientId, event: &ThreadEvent, - cx: &mut ViewContext, + _: &mut ViewContext, ) { if Self::should_skip_event(this, client_id, event.thread_id) { return; } - // dbg!(event); + // TODO: handle thread event } } @@ -192,7 +189,7 @@ impl DebugPanelItem { }) .on_click(cx.listener({ let stack_frame = stack_frame.clone(); - move |this, _, cx| { + move |this, _, _| { this.current_stack_frame_id = Some(stack_frame.id); // let client = this.client(); From 4e2d0351cc3b5ae6a13db7511551ec3e76ce8595 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 14 Jul 2024 15:17:55 +0200 Subject: [PATCH 096/650] Rename thread state -> thread states --- crates/dap/src/client.rs | 16 ++++++++-------- crates/debugger_ui/src/debugger_panel.rs | 8 ++++---- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 052d1ab350bdf8..f45b1dc97a6a8b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -66,8 +66,8 @@ pub struct DebugAdapterClient { capabilities: Option, config: DebugAdapterConfig, client_rx: Arc>>, - thread_state: Arc>>, // thread_id -> thread_state request_type: DebugRequestType, + thread_states: Arc>>, // thread_id -> thread_state } impl DebugAdapterClient { @@ -102,8 +102,8 @@ impl DebugAdapterClient { .current_dir(project_path) .args(args) .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) .kill_on_drop(true); let process = command @@ -165,7 +165,7 @@ impl DebugAdapterClient { capabilities: None, server_tx: server_tx.clone(), request_count: AtomicU64::new(0), - thread_state: Arc::new(Mutex::new(HashMap::new())), + thread_states: Arc::new(Mutex::new(HashMap::new())), }; cx.spawn(move |_| Self::handle_recv(server_rx, server_tx, client_tx)) @@ -247,17 +247,17 @@ impl DebugAdapterClient { } pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) { - if let Some(thread_state) = self.thread_state().get_mut(&thread_id) { + if let Some(thread_state) = self.thread_states().get_mut(&thread_id) { thread_state.status = status; }; } - pub fn thread_state(&self) -> MutexGuard> { - self.thread_state.lock() + pub fn thread_states(&self) -> MutexGuard> { + self.thread_states.lock() } pub fn thread_state_by_id(&self, thread_id: u64) -> ThreadState { - self.thread_state.lock().get(&thread_id).cloned().unwrap() + self.thread_states.lock().get(&thread_id).cloned().unwrap() } pub async fn initialize(&mut self) -> Result { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 5e47c5ee85d6ff..d557f506d99646 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -210,7 +210,7 @@ impl DebugPanel { cx: &mut ViewContext, ) -> Task> { let mut tasks = Vec::new(); - for thread_state in client.thread_state().values() { + for thread_state in client.thread_states().values() { for stack_frame in thread_state.stack_frames.clone() { tasks.push(Self::remove_editor_highlight(&stack_frame, cx)); } @@ -233,7 +233,7 @@ impl DebugPanel { cx: &mut ViewContext, ) -> Task> { let mut tasks = Vec::new(); - if let Some(thread_state) = client.thread_state().get(&thread_id) { + if let Some(thread_state) = client.thread_states().get(&thread_id) { for stack_frame in thread_state.stack_frames.clone() { tasks.push(Self::remove_editor_highlight(&stack_frame, cx)); } @@ -356,7 +356,7 @@ impl DebugPanel { } this.update(&mut cx, |this, cx| { - let mut thread_state = client.thread_state(); + let mut thread_state = client.thread_states(); let thread_state = thread_state .entry(thread_id) .or_insert(ThreadState::default()); @@ -423,7 +423,7 @@ impl DebugPanel { if event.reason == ThreadEventReason::Started { client - .thread_state() + .thread_states() .insert(thread_id, ThreadState::default()); } else { client.update_thread_state_status(thread_id.clone(), ThreadStatus::Ended); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 8d593d5ec20bf4..96d26887d4a067 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -150,7 +150,7 @@ impl DebugPanelItem { } fn current_thread_state(&self) -> Option { - self.client.thread_state().get(&self.thread_id).cloned() + self.client.thread_states().get(&self.thread_id).cloned() } fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { From 00379280f3281910017028594899961d521f3b8b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 14 Jul 2024 15:18:37 +0200 Subject: [PATCH 097/650] Don't store request type on client(its already in the config) --- crates/dap/src/client.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f45b1dc97a6a8b..7a4c0ef574c11b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -66,7 +66,6 @@ pub struct DebugAdapterClient { capabilities: Option, config: DebugAdapterConfig, client_rx: Arc>>, - request_type: DebugRequestType, thread_states: Arc>>, // thread_id -> thread_state } @@ -155,12 +154,10 @@ impl DebugAdapterClient { let client_rx = Arc::new(smol::lock::Mutex::new(client_rx)); - let request_type = config.clone().request; let client = Self { id, config, client_rx, - request_type, _process: process, capabilities: None, server_tx: server_tx.clone(), @@ -239,7 +236,7 @@ impl DebugAdapterClient { } pub fn request_type(&self) -> DebugRequestType { - self.request_type.clone() + self.config.request.clone() } pub fn next_request_id(&self) -> u64 { From c9074b1c250c807dfba7defaa105c4575132040d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 14 Jul 2024 15:19:05 +0200 Subject: [PATCH 098/650] Wire through debugger id for initialize request --- crates/dap/src/client.rs | 2 +- crates/task/src/task_template.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 7a4c0ef574c11b..c257163fd13781 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -261,7 +261,7 @@ impl DebugAdapterClient { let args = dap_types::InitializeRequestArguments { client_id: Some("zed".to_owned()), client_name: Some("Zed".to_owned()), - adapter_id: "xdebug".into(), // TODO: read from config + adapter_id: self.config.id.clone(), locale: Some("en-us".to_owned()), path_format: Some(InitializeRequestArgumentsPathFormat::Path), supports_variable_type: Some(true), diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index ab8e60ebadf489..a2a807c5216379 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -95,6 +95,9 @@ pub enum DebugRequestType { #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugAdapterConfig { + /// Unique id of for the debug adapter, + /// that will be send with the `initialize` request + pub id: String, /// The type of connection the adapter should use #[serde(default)] pub connection: DebugConnectionType, From 1b42dd5865a46567b3cce17c6d92582535c5cd98 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 14 Jul 2024 15:48:43 +0200 Subject: [PATCH 099/650] Add client id to thread tab title --- crates/dap/src/client.rs | 4 ++++ crates/debugger_ui/src/debugger_panel_item.rs | 18 +++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index c257163fd13781..d8ae216d061036 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -235,6 +235,10 @@ impl DebugAdapterClient { self.id } + pub fn config(&self) -> DebugAdapterConfig { + self.config.clone() + } + pub fn request_type(&self) -> DebugRequestType { self.config.request.clone() } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 96d26887d4a067..4ec6f3c4cb69b0 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -121,13 +121,17 @@ impl Item for DebugPanelItem { params: workspace::item::TabContentParams, _: &WindowContext, ) -> AnyElement { - Label::new(format!("Thread {}", self.thread_id)) - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() + Label::new(format!( + "{} - Thread {}", + self.client.config().id, + self.thread_id + )) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } } From 737b03c928c5618aeb96d849adf0866e36dcaa3c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 14 Jul 2024 16:44:47 +0200 Subject: [PATCH 100/650] Refactor go_to_stack_frame & clear_hightlights --- crates/debugger_ui/src/debugger_panel.rs | 209 +++++++++++------------ 1 file changed, 103 insertions(+), 106 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index d557f506d99646..203f58b1596180 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -152,133 +152,123 @@ impl DebugPanel { } } - pub fn go_to_stack_frame( - stack_frame: &StackFrame, + pub async fn go_to_stack_frame( + workspace: WeakView, + stack_frame: StackFrame, client: Arc, clear_highlights: bool, - cx: &mut ViewContext, - ) -> Task> { + mut cx: AsyncWindowContext, + ) -> Result<()> { let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); let row = (stack_frame.line.saturating_sub(1)) as u32; let column = (stack_frame.column.saturating_sub(1)) as u32; - cx.spawn(move |this, mut cx| async move { - if clear_highlights { - // TODO: - // this.update(&mut cx, |this, cx| this.remove_highlights(client, cx))? - // .await?; - } + if clear_highlights { + Self::remove_highlights(workspace.clone(), client, cx.clone()).await?; + } - let task = this.update(&mut cx, |this, cx| { - this.workspace.update(cx, |workspace, cx| { - let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_absolute_path(&Path::new(&path), cx) - }); + let task = workspace.update(&mut cx, |workspace, cx| { + let project_path = workspace.project().read_with(cx, |project, cx| { + project.project_path_for_absolute_path(&Path::new(&path), cx) + }); - if let Some(project_path) = project_path { - workspace.open_path_preview(project_path, None, false, true, cx) - } else { - Task::ready(Err(anyhow::anyhow!( - "No project path found for path: {}", - path - ))) - } - }) - })??; - - let editor = task.await?.downcast::().unwrap(); - - this.update(&mut cx, |this, cx| { - this.workspace.update(cx, |_, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_line::( - row, - column, - Some(cx.theme().colors().editor_highlighted_line_background), - cx, - ); - }) - }) - })??; + if let Some(project_path) = project_path { + workspace.open_path_preview(project_path, None, false, true, cx) + } else { + Task::ready(Err(anyhow::anyhow!( + "No project path found for path: {}", + path + ))) + } + })?; - anyhow::Ok(()) + let editor = task.await?.downcast::().unwrap(); + + workspace.update(&mut cx, |_, cx| { + editor.update(cx, |editor, cx| { + editor.go_to_line::( + row, + column, + Some(cx.theme().colors().editor_highlighted_line_background), + cx, + ); + }) }) } - fn remove_highlights( + async fn remove_highlights( + workspace: WeakView, client: Arc, - cx: &mut ViewContext, - ) -> Task> { + cx: AsyncWindowContext, + ) -> Result<()> { let mut tasks = Vec::new(); for thread_state in client.thread_states().values() { for stack_frame in thread_state.stack_frames.clone() { - tasks.push(Self::remove_editor_highlight(&stack_frame, cx)); + tasks.push(Self::remove_editor_highlight( + workspace.clone(), + stack_frame, + cx.clone(), + )); } } - if tasks.is_empty() { - return Task::ready(Ok(())); - } - - cx.spawn(|_, _| async move { + if !tasks.is_empty() { try_join_all(tasks).await?; + } - anyhow::Ok(()) - }) + anyhow::Ok(()) } - fn remove_highlights_for_thread( + async fn remove_highlights_for_thread( + workspace: WeakView, client: Arc, thread_id: u64, - cx: &mut ViewContext, - ) -> Task> { + cx: AsyncWindowContext, + ) -> Result<()> { let mut tasks = Vec::new(); if let Some(thread_state) = client.thread_states().get(&thread_id) { for stack_frame in thread_state.stack_frames.clone() { - tasks.push(Self::remove_editor_highlight(&stack_frame, cx)); + tasks.push(Self::remove_editor_highlight( + workspace.clone(), + stack_frame.clone(), + cx.clone(), + )); } } - if tasks.is_empty() { - return Task::ready(Ok(())); - } - - cx.spawn(|_, _| async move { + if !tasks.is_empty() { try_join_all(tasks).await?; + } - anyhow::Ok(()) - }) + anyhow::Ok(()) } - fn remove_editor_highlight( - stack_frame: &StackFrame, - cx: &mut ViewContext, - ) -> Task> { + async fn remove_editor_highlight( + workspace: WeakView, + stack_frame: StackFrame, + mut cx: AsyncWindowContext, + ) -> Result<()> { let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); - cx.spawn(|this, mut cx| async move { - let task = this.update(&mut cx, |this, cx| { - this.workspace.update(cx, |workspace, cx| { - let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_absolute_path(&Path::new(&path), cx) - }); + let task = workspace.update(&mut cx, |workspace, cx| { + let project_path = workspace.project().read_with(cx, |project, cx| { + project.project_path_for_absolute_path(&Path::new(&path), cx) + }); - if let Some(project_path) = project_path { - workspace.open_path(project_path, None, false, cx) - } else { - Task::ready(Err(anyhow::anyhow!( - "No project path found for path: {}", - path - ))) - } - }) - })??; + if let Some(project_path) = project_path { + workspace.open_path(project_path, None, false, cx) + } else { + Task::ready(Err(anyhow::anyhow!( + "No project path found for path: {}", + path + ))) + } + })?; - let editor = task.await?.downcast::().unwrap(); + let editor = task.await?.downcast::().unwrap(); - editor.update(&mut cx, |editor, _| { - editor.clear_row_highlights::(); - }) + editor.update(&mut cx, |editor, _| { + editor.clear_row_highlights::(); }) } @@ -394,12 +384,18 @@ impl DebugPanel { if let Some(item) = this.pane.read(cx).active_item() { if let Some(pane) = item.downcast::() { if pane.read(cx).thread_id() == thread_id { - return Self::go_to_stack_frame( - ¤t_stack_frame, - client.clone(), - true, - cx, - ); + let workspace = this.workspace.clone(); + let client = client.clone(); + return cx.spawn(|_, cx| async move { + Self::go_to_stack_frame( + workspace, + current_stack_frame.clone(), + client, + true, + cx, + ) + .await + }); } } } @@ -429,16 +425,17 @@ impl DebugPanel { client.update_thread_state_status(thread_id.clone(), ThreadStatus::Ended); // TODO: we want to figure out for witch clients/threads we should remove the highlights - // cx.spawn({ - // let client = client.clone(); - // |this, mut cx| async move { - // this.update(&mut cx, |_, cx| { - // Self::remove_highlights_for_thread(client, thread_id, cx) - // })? - // .await - // } - // }) - // .detach_and_log_err(cx); + cx.spawn({ + let client = client.clone(); + |this, mut cx| async move { + let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?; + + Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?; + + anyhow::Ok(()) + } + }) + .detach_and_log_err(cx); } cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); @@ -452,12 +449,12 @@ impl DebugPanel { ) { let restart_args = event.clone().and_then(|e| e.restart); let client = this.debug_client_by_id(*client_id, cx); + let workspace = this.workspace.clone(); - cx.spawn(|this, mut cx| async move { + cx.spawn(|_, cx| async move { let should_restart = restart_args.is_some(); - this.update(&mut cx, |_, cx| Self::remove_highlights(client.clone(), cx))? - .await?; + Self::remove_highlights(workspace, client.clone(), cx).await?; client .request::(DisconnectArguments { From 92b93850bb19f8d0f08aa4fbb15a922cf9a552ef Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 14 Jul 2024 15:35:37 -0400 Subject: [PATCH 101/650] Add support for DAP to use std for communication --- crates/dap/src/client.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d8ae216d061036..caaa11b126f675 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -2,6 +2,7 @@ use crate::transport::{self, Events, Payload, Request, Transport}; use anyhow::{anyhow, Context, Result}; use dap_types::{ + events::Process, requests::{ Attach, ConfigurationDone, Continue, Initialize, Launch, Next, Pause, SetBreakpoints, StepBack, StepIn, StepOut, @@ -130,14 +131,31 @@ impl DebugAdapterClient { } async fn create_stdio_client( - _id: DebugAdapterClientId, - _config: DebugAdapterConfig, - _command: &str, - _args: Vec<&str>, - _project_path: PathBuf, - _cx: &mut AsyncAppContext, + id: DebugAdapterClientId, + config: DebugAdapterConfig, + command: &str, + args: Vec<&str>, + project_path: PathBuf, + cx: &mut AsyncAppContext, ) -> Result { - todo!("not implemented") + let mut command = process::Command::new(command); + command + .current_dir(project_path) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true); + + let mut process = command + .spawn() + .with_context(|| "failed to spawn command.")?; + + let stdin = Box::new(process.stdin.take().unwrap()); + let stdout = Box::new(BufReader::new(process.stdout.take().unwrap())); + let stderr = Box::new(BufReader::new(process.stderr.take().unwrap())); + + Self::handle_transport(id, config, stdout, stdin, Some(stderr), Some(process), cx) } pub fn handle_transport( From 3e1aa65b20aecdfc84b9be21944fe1305d185422 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 14 Jul 2024 19:16:08 -0400 Subject: [PATCH 102/650] Add more descriptive error logs for DAP --- crates/dap/src/client.rs | 25 +++++++++++++++++++++---- crates/dap/src/transport.rs | 4 ++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index caaa11b126f675..c5d223443008b1 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -2,7 +2,6 @@ use crate::transport::{self, Events, Payload, Request, Transport}; use anyhow::{anyhow, Context, Result}; use dap_types::{ - events::Process, requests::{ Attach, ConfigurationDone, Continue, Initialize, Launch, Next, Pause, SetBreakpoints, StepBack, StepIn, StepOut, @@ -151,9 +150,27 @@ impl DebugAdapterClient { .spawn() .with_context(|| "failed to spawn command.")?; - let stdin = Box::new(process.stdin.take().unwrap()); - let stdout = Box::new(BufReader::new(process.stdout.take().unwrap())); - let stderr = Box::new(BufReader::new(process.stderr.take().unwrap())); + // give the adapter some time to start std + cx.background_executor() + .timer(Duration::from_millis(1000)) + .await; + + let stdin = process + .stdin + .take() + .ok_or_else(|| anyhow!("Failed to open stdin"))?; + let stdout = process + .stdout + .take() + .ok_or_else(|| anyhow!("Failed to open stdout"))?; + let stderr = process + .stderr + .take() + .ok_or_else(|| anyhow!("Failed to open stderr"))?; + + let stdin = Box::new(stdin); + let stdout = Box::new(BufReader::new(stdout)); + let stderr = Box::new(BufReader::new(stderr)); Self::handle_transport(id, config, stdout, stdin, Some(stderr), Some(process), cx) } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 2f314ab4d3a991..24fa67bfbcd9ba 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -106,7 +106,7 @@ impl Transport { loop { buffer.truncate(0); if reader.read_line(buffer).await? == 0 { - return Err(anyhow!("stream closed")); + return Err(anyhow!("reader stream closed")); }; if buffer == "\r\n" { @@ -140,7 +140,7 @@ impl Transport { ) -> Result<()> { buffer.truncate(0); if err.read_line(buffer).await? == 0 { - return Err(anyhow!("stream closed")); + return Err(anyhow!("error stream closed")); }; Ok(()) From 73ef7718750425c50b6390e3610b345e6b024002 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 14 Jul 2024 22:36:54 -0400 Subject: [PATCH 103/650] Implement handler for continued event --- crates/debugger_ui/src/debugger_panel.rs | 26 ++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 203f58b1596180..b1d03b7ec813d8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,10 +1,10 @@ use anyhow::Result; -use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::client::{self, DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::requests::{Disconnect, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - DisconnectArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, StoppedEvent, - TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, + ContinuedEvent, DisconnectArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, + StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; use editor::Editor; use futures::future::try_join_all; @@ -134,7 +134,7 @@ impl DebugPanel { .detach_and_log_err(cx); } Events::Stopped(event) => Self::handle_stopped_event(this, client_id, event, cx), - Events::Continued(_) => {} + Events::Continued(event) => Self::handle_continued_event(this, client_id, event, cx), Events::Exited(_) => {} Events::Terminated(event) => Self::handle_terminated_event(this, client_id, event, cx), Events::Thread(event) => Self::handle_thread_event(this, client_id, event, cx), @@ -272,6 +272,24 @@ impl DebugPanel { }) } + fn handle_continued_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + event: &ContinuedEvent, + cx: &mut ViewContext, + ) { + let all_threads = event.all_threads_continued.unwrap_or(false); + let client = this.debug_client_by_id(client_id.clone(), cx); + + if all_threads { + for thread in client.thread_states().values_mut() { + thread.status = ThreadStatus::Running; + } + } else { + client.update_thread_state_status(event.thread_id, ThreadStatus::Running); + } + } + fn handle_stopped_event( this: &mut Self, client_id: &DebugAdapterClientId, From 8d7ec33183da53f2aae24a6259328cfc209092f7 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 15 Jul 2024 11:43:10 -0400 Subject: [PATCH 104/650] Add PR feedback to handle_continued_event function --- crates/debugger_ui/src/debugger_panel.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b1d03b7ec813d8..31e9e3f4ed7f7f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -279,7 +279,7 @@ impl DebugPanel { cx: &mut ViewContext, ) { let all_threads = event.all_threads_continued.unwrap_or(false); - let client = this.debug_client_by_id(client_id.clone(), cx); + let client = this.debug_client_by_id(*client_id, cx); if all_threads { for thread in client.thread_states().values_mut() { @@ -288,6 +288,8 @@ impl DebugPanel { } else { client.update_thread_state_status(event.thread_id, ThreadStatus::Running); } + + cx.notify(); } fn handle_stopped_event( From 77a314350f25638b07ba2c1cf256172056c88370 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 15 Jul 2024 18:03:30 +0200 Subject: [PATCH 105/650] Fix autocompletion for connection type before `s_t_d_i_o` after `stdio` Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/task/src/task_template.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index a2a807c5216379..a4cecf936ec7f5 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -71,7 +71,7 @@ pub enum TaskType { /// Represents the type of the debugger adapter connection #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "lowercase")] pub enum DebugConnectionType { /// Connect to the debug adapter via TCP #[default] From ffa0609f8d3624d1a32d50419b60114bed9430e1 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:46:20 -0400 Subject: [PATCH 106/650] Implement stdio debugger connection type (#5) * Add support for DAP to use std for communication * Add more descriptive error logs for DAP * Implement handler for continued event * Add PR feedback to handle_continued_event function --- crates/dap/src/client.rs | 49 ++++++++++++++++++++---- crates/dap/src/transport.rs | 4 +- crates/debugger_ui/src/debugger_panel.rs | 28 ++++++++++++-- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d8ae216d061036..c5d223443008b1 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -130,14 +130,49 @@ impl DebugAdapterClient { } async fn create_stdio_client( - _id: DebugAdapterClientId, - _config: DebugAdapterConfig, - _command: &str, - _args: Vec<&str>, - _project_path: PathBuf, - _cx: &mut AsyncAppContext, + id: DebugAdapterClientId, + config: DebugAdapterConfig, + command: &str, + args: Vec<&str>, + project_path: PathBuf, + cx: &mut AsyncAppContext, ) -> Result { - todo!("not implemented") + let mut command = process::Command::new(command); + command + .current_dir(project_path) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true); + + let mut process = command + .spawn() + .with_context(|| "failed to spawn command.")?; + + // give the adapter some time to start std + cx.background_executor() + .timer(Duration::from_millis(1000)) + .await; + + let stdin = process + .stdin + .take() + .ok_or_else(|| anyhow!("Failed to open stdin"))?; + let stdout = process + .stdout + .take() + .ok_or_else(|| anyhow!("Failed to open stdout"))?; + let stderr = process + .stderr + .take() + .ok_or_else(|| anyhow!("Failed to open stderr"))?; + + let stdin = Box::new(stdin); + let stdout = Box::new(BufReader::new(stdout)); + let stderr = Box::new(BufReader::new(stderr)); + + Self::handle_transport(id, config, stdout, stdin, Some(stderr), Some(process), cx) } pub fn handle_transport( diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 2f314ab4d3a991..24fa67bfbcd9ba 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -106,7 +106,7 @@ impl Transport { loop { buffer.truncate(0); if reader.read_line(buffer).await? == 0 { - return Err(anyhow!("stream closed")); + return Err(anyhow!("reader stream closed")); }; if buffer == "\r\n" { @@ -140,7 +140,7 @@ impl Transport { ) -> Result<()> { buffer.truncate(0); if err.read_line(buffer).await? == 0 { - return Err(anyhow!("stream closed")); + return Err(anyhow!("error stream closed")); }; Ok(()) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 203f58b1596180..31e9e3f4ed7f7f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,10 +1,10 @@ use anyhow::Result; -use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::client::{self, DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::requests::{Disconnect, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - DisconnectArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, StoppedEvent, - TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, + ContinuedEvent, DisconnectArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, + StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; use editor::Editor; use futures::future::try_join_all; @@ -134,7 +134,7 @@ impl DebugPanel { .detach_and_log_err(cx); } Events::Stopped(event) => Self::handle_stopped_event(this, client_id, event, cx), - Events::Continued(_) => {} + Events::Continued(event) => Self::handle_continued_event(this, client_id, event, cx), Events::Exited(_) => {} Events::Terminated(event) => Self::handle_terminated_event(this, client_id, event, cx), Events::Thread(event) => Self::handle_thread_event(this, client_id, event, cx), @@ -272,6 +272,26 @@ impl DebugPanel { }) } + fn handle_continued_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + event: &ContinuedEvent, + cx: &mut ViewContext, + ) { + let all_threads = event.all_threads_continued.unwrap_or(false); + let client = this.debug_client_by_id(*client_id, cx); + + if all_threads { + for thread in client.thread_states().values_mut() { + thread.status = ThreadStatus::Running; + } + } else { + client.update_thread_state_status(event.thread_id, ThreadStatus::Running); + } + + cx.notify(); + } + fn handle_stopped_event( this: &mut Self, client_id: &DebugAdapterClientId, From b6e677eb069151ac23e4a04d23def81cdfc76841 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 15 Jul 2024 18:39:43 +0200 Subject: [PATCH 107/650] Wip configure host for tcp debug adapter Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/dap/src/client.rs | 37 +++++++++++++++++------- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/task/src/lib.rs | 4 +-- crates/task/src/task_template.rs | 28 +++++++++++++----- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index c5d223443008b1..0c95e4aa8e6bcc 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -20,7 +20,7 @@ use parking_lot::{Mutex, MutexGuard}; use serde_json::Value; use smol::{ io::BufReader, - net::TcpStream, + net::{TcpListener, TcpStream}, process::{self, Child}, }; use std::{ @@ -34,7 +34,7 @@ use std::{ }, time::Duration, }; -use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType}; +use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType, TCPHost}; use util::ResultExt; #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -78,9 +78,9 @@ impl DebugAdapterClient { project_path: PathBuf, cx: &mut AsyncAppContext, ) -> Result { - match config.connection { - DebugConnectionType::TCP => { - Self::create_tcp_client(id, config, command, args, project_path, cx).await + match config.connection.clone() { + DebugConnectionType::TCP(host) => { + Self::create_tcp_client(id, config, host, command, args, project_path, cx).await } DebugConnectionType::STDIO => { Self::create_stdio_client(id, config, command, args, project_path, cx).await @@ -91,11 +91,17 @@ impl DebugAdapterClient { async fn create_tcp_client( id: DebugAdapterClientId, config: DebugAdapterConfig, + host: TCPHost, command: &str, args: Vec<&str>, project_path: PathBuf, cx: &mut AsyncAppContext, ) -> Result { + let mut port = host.port; + if port.is_none() { + port = Self::get_port().await; + } + let mut command = process::Command::new(command); command .current_dir(project_path) @@ -114,7 +120,10 @@ impl DebugAdapterClient { .timer(Duration::from_millis(1000)) .await; - let address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), config.port); + let address = SocketAddrV4::new( + host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)), + port.unwrap(), + ); let (rx, tx) = TcpStream::connect(address).await?.split(); @@ -129,6 +138,17 @@ impl DebugAdapterClient { ) } + async fn get_port() -> Option { + Some( + TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0)) + .await + .ok()? + .local_addr() + .ok()? + .port(), + ) + } + async fn create_stdio_client( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -150,11 +170,6 @@ impl DebugAdapterClient { .spawn() .with_context(|| "failed to spawn command.")?; - // give the adapter some time to start std - cx.background_executor() - .timer(Duration::from_millis(1000)) - .await; - let stdin = process .stdin .take() diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 31e9e3f4ed7f7f..e34e665a7efba6 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use dap::client::{self, DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::requests::{Disconnect, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index fd0f3b25e2898a..a1a9c36d4d1c49 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -13,8 +13,8 @@ use std::str::FromStr; use std::{borrow::Cow, path::Path}; pub use task_template::{ - DebugAdapterConfig, DebugConnectionType, DebugRequestType, RevealStrategy, TaskTemplate, - TaskTemplates, TaskType, + DebugAdapterConfig, DebugConnectionType, DebugRequestType, RevealStrategy, TCPHost, + TaskTemplate, TaskTemplates, TaskType, }; pub use vscode_format::VsCodeTaskFile; diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index a4cecf936ec7f5..3b9ffe9531f3ac 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{net::Ipv4Addr, path::PathBuf}; use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; @@ -70,16 +70,30 @@ pub enum TaskType { } /// Represents the type of the debugger adapter connection -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase")] +#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "lowercase", tag = "connection")] pub enum DebugConnectionType { /// Connect to the debug adapter via TCP - #[default] - TCP, + TCP(TCPHost), /// Connect to the debug adapter via STDIO STDIO, } +impl Default for DebugConnectionType { + fn default() -> Self { + DebugConnectionType::TCP(TCPHost::default()) + } +} + +/// Represents the host information of the debug adapter +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +pub struct TCPHost { + /// The port that the debug adapter is listening on + pub port: Option, + /// The host that the debug adapter is listening too + pub host: Option, +} + /// Represents the type that will determine which request to call on the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] @@ -99,10 +113,8 @@ pub struct DebugAdapterConfig { /// that will be send with the `initialize` request pub id: String, /// The type of connection the adapter should use - #[serde(default)] + #[serde(default, flatten)] pub connection: DebugConnectionType, - /// The port that the debug adapter is listening on - pub port: u16, /// The type of request that should be called on the debug adapter #[serde(default)] pub request: DebugRequestType, From 65a790e4ca1a26c184b40bc84210bdac8b081294 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 20:18:16 +0200 Subject: [PATCH 108/650] Make double click on pane action optional --- crates/assistant/src/assistant_panel.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 4 ++-- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/workspace/src/pane.rs | 8 +++++--- crates/workspace/src/workspace.rs | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 85021081d4270d..960f7421431910 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -294,7 +294,7 @@ impl AssistantPanel { workspace.project().clone(), Default::default(), None, - NewFile.boxed_clone(), + Some(NewFile.boxed_clone()), cx, ); pane.set_can_split(false, cx); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index e34e665a7efba6..4752043aed01a3 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -16,11 +16,11 @@ use std::path::Path; use std::{collections::HashMap, sync::Arc}; use task::DebugRequestType; use ui::prelude::*; +use workspace::Pane; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; -use workspace::{NewFile, Pane}; use crate::debugger_panel_item::DebugPanelItem; @@ -51,7 +51,7 @@ impl DebugPanel { workspace.project().clone(), Default::default(), None, - NewFile.boxed_clone(), + None, cx, ); pane.set_can_split(false, cx); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 064adfaaf4e871..c2bb3e9d888a7f 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -78,7 +78,7 @@ impl TerminalPanel { workspace.project().clone(), Default::default(), None, - NewTerminal.boxed_clone(), + Some(NewTerminal.boxed_clone()), cx, ); pane.set_can_split(false, cx); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 087cfd6bef662c..f492553411a504 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -242,7 +242,7 @@ pub struct Pane { /// Is None if navigation buttons are permanently turned off (and should not react to setting changes). /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed. display_nav_history_buttons: Option, - double_click_dispatch_action: Box, + double_click_dispatch_action: Option>, save_modals_spawned: HashSet, } @@ -310,7 +310,7 @@ impl Pane { project: Model, next_timestamp: Arc, can_drop_predicate: Option bool + 'static>>, - double_click_dispatch_action: Box, + double_click_dispatch_action: Option>, cx: &mut ViewContext, ) -> Self { let focus_handle = cx.focus_handle(); @@ -1881,7 +1881,9 @@ impl Pane { })) .on_click(cx.listener(move |this, event: &ClickEvent, cx| { if event.up.click_count == 2 { - cx.dispatch_action(this.double_click_dispatch_action.boxed_clone()) + if let Some(action) = &this.double_click_dispatch_action { + cx.dispatch_action(action.boxed_clone()); + } } })), ) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 527db7116327a5..4e97328a31e2d3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -726,7 +726,7 @@ impl Workspace { project.clone(), pane_history_timestamp.clone(), None, - NewFile.boxed_clone(), + Some(NewFile.boxed_clone()), cx, ) }); @@ -2089,7 +2089,7 @@ impl Workspace { self.project.clone(), self.pane_history_timestamp.clone(), None, - NewFile.boxed_clone(), + Some(NewFile.boxed_clone()), cx, ) }); From ba4a70d7ae3584d15b92f07f5e76b2889256b744 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 20:56:24 +0200 Subject: [PATCH 109/650] Make TCP connect delay configurable --- crates/dap/src/client.rs | 15 +++++++++------ crates/task/src/task_template.rs | 4 +++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 0c95e4aa8e6bcc..d22ed0026bd859 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -113,12 +113,15 @@ impl DebugAdapterClient { let process = command .spawn() - .with_context(|| "failed to spawn command.")?; - - // give the adapter some time to spin up the tcp server - cx.background_executor() - .timer(Duration::from_millis(1000)) - .await; + .with_context(|| "failed to start debug adapter.")?; + + if let Some(delay) = host.delay { + // some debug adapters need some time to start the TCP server + // so we have to wait few milliseconds before we can connect to it + cx.background_executor() + .timer(Duration::from_millis(delay)) + .await; + } let address = SocketAddrV4::new( host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)), diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 3b9ffe9531f3ac..26c47304504b36 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,4 +1,4 @@ -use std::{net::Ipv4Addr, path::PathBuf}; +use std::{net::Ipv4Addr, path::PathBuf, time::Duration}; use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; @@ -92,6 +92,8 @@ pub struct TCPHost { pub port: Option, /// The host that the debug adapter is listening too pub host: Option, + /// The delay in ms between starting and connecting to the debug adapter + pub delay: Option, } /// Represents the type that will determine which request to call on the debug adapter From 0deb3cc6061ad7f0675858c397bd387ee3826c7b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 21:17:05 +0200 Subject: [PATCH 110/650] Move handle initialized event to own method --- crates/debugger_ui/src/debugger_panel.rs | 47 ++++++++++++++---------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 4752043aed01a3..c60c90a60d4997 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -113,25 +113,8 @@ impl DebugPanel { cx: &mut ViewContext, ) { match event { - Events::Initialized(_) => { - let client = this.debug_client_by_id(*client_id, cx); - - cx.spawn(|this, mut cx| async move { - let task = this.update(&mut cx, |this, cx| { - this.workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - let client = client.clone(); - - project.send_breakpoints(client, cx) - }) - }) - })??; - - task.await?; - - client.configuration_done().await - }) - .detach_and_log_err(cx); + Events::Initialized(event) => { + Self::handle_initialized_event(this, client_id, event, cx) } Events::Stopped(event) => Self::handle_stopped_event(this, client_id, event, cx), Events::Continued(event) => Self::handle_continued_event(this, client_id, event, cx), @@ -272,6 +255,32 @@ impl DebugPanel { }) } + fn handle_initialized_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + _: &Option, + cx: &mut ViewContext, + ) { + let client = this.debug_client_by_id(*client_id, cx); + + cx.spawn(|this, mut cx| async move { + let task = this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + let client = client.clone(); + + project.send_breakpoints(client, cx) + }) + }) + })??; + + task.await?; + + client.configuration_done().await + }) + .detach_and_log_err(cx); + } + fn handle_continued_event( this: &mut Self, client_id: &DebugAdapterClientId, From dc5928374e29f3b2e7ae9d36137444fc4a789828 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 21:18:41 +0200 Subject: [PATCH 111/650] Fix we did not notify after stack frame list reset was doen This fixes an issue if you step debug the same thread, the stack frame list was not up to date. --- crates/debugger_ui/src/debugger_panel_item.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 4ec6f3c4cb69b0..7b44bb748ac6c8 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -50,7 +50,7 @@ impl DebugPanelItem { move |this: &mut Self, _, event: &DebugPanelEvent, cx| { match event { DebugPanelEvent::Stopped((client_id, event)) => { - Self::handle_stopped_event(this, client_id, event) + Self::handle_stopped_event(this, client_id, event, cx) } DebugPanelEvent::Thread((client_id, event)) => { Self::handle_thread_event(this, client_id, event, cx) @@ -81,6 +81,7 @@ impl DebugPanelItem { this: &mut Self, client_id: &DebugAdapterClientId, event: &StoppedEvent, + cx: &mut ViewContext, ) { if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or_default()) { return; @@ -88,6 +89,7 @@ impl DebugPanelItem { if let Some(thread_state) = this.current_thread_state() { this.stack_frame_list.reset(thread_state.stack_frames.len()); + cx.notify(); }; } @@ -95,7 +97,7 @@ impl DebugPanelItem { this: &mut Self, client_id: &DebugAdapterClientId, event: &ThreadEvent, - _: &mut ViewContext, + _: &mut ViewContext, ) { if Self::should_skip_event(this, client_id, event.thread_id) { return; From f8b9937e51ebd02c2a477be506a39acdcec09c26 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 21:19:20 +0200 Subject: [PATCH 112/650] Handle exited event --- crates/dap/src/client.rs | 1 + crates/debugger_ui/src/debugger_panel.rs | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d22ed0026bd859..97dbf374dd928e 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -42,6 +42,7 @@ pub enum ThreadStatus { #[default] Running, Stopped, + Exited, Ended, } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c60c90a60d4997..6021956a8a080c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -3,8 +3,9 @@ use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::requests::{Disconnect, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - ContinuedEvent, DisconnectArguments, Scope, ScopesArguments, StackFrame, StackTraceArguments, - StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, + Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, Scope, ScopesArguments, + StackFrame, StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, + Variable, VariablesArguments, }; use editor::Editor; use futures::future::try_join_all; @@ -118,7 +119,7 @@ impl DebugPanel { } Events::Stopped(event) => Self::handle_stopped_event(this, client_id, event, cx), Events::Continued(event) => Self::handle_continued_event(this, client_id, event, cx), - Events::Exited(_) => {} + Events::Exited(event) => Self::handle_exited_event(this, client_id, event, cx), Events::Terminated(event) => Self::handle_terminated_event(this, client_id, event, cx), Events::Thread(event) => Self::handle_thread_event(this, client_id, event, cx), Events::Output(_) => {} @@ -470,6 +471,21 @@ impl DebugPanel { cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); } + fn handle_exited_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + _: &ExitedEvent, + cx: &mut ViewContext, + ) { + let client = this.debug_client_by_id(*client_id, cx); + cx.spawn(|_, _| async move { + for thread_state in client.thread_states().values_mut() { + thread_state.status = ThreadStatus::Exited; + } + }) + .detach(); + } + fn handle_terminated_event( this: &mut Self, client_id: &DebugAdapterClientId, From fe899c9164861d1bb107dfa6f0e24c7a65f888d3 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 21:28:36 +0200 Subject: [PATCH 113/650] Change that current thread state does not return option --- crates/debugger_ui/src/debugger_panel_item.rs | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 7b44bb748ac6c8..2e3a1c7bc1c238 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -87,10 +87,9 @@ impl DebugPanelItem { return; } - if let Some(thread_state) = this.current_thread_state() { - this.stack_frame_list.reset(thread_state.stack_frames.len()); - cx.notify(); - }; + this.stack_frame_list + .reset(this.current_thread_state().stack_frames.len()); + cx.notify(); } fn handle_thread_event( @@ -155,8 +154,12 @@ impl DebugPanelItem { .unwrap() } - fn current_thread_state(&self) -> Option { - self.client.thread_states().get(&self.thread_id).cloned() + fn current_thread_state(&self) -> ThreadState { + self.client + .thread_states() + .get(&self.thread_id) + .cloned() + .unwrap() } fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { @@ -225,11 +228,8 @@ impl DebugPanelItem { .into_any() } - fn render_scopes( - &self, - thread_state: &ThreadState, - cx: &mut ViewContext, - ) -> impl IntoElement { + fn render_scopes(&self, cx: &mut ViewContext) -> impl IntoElement { + let thread_state = self.current_thread_state(); let Some(scopes) = thread_state .current_stack_frame_id .and_then(|id| thread_state.scopes.get(&id)) @@ -244,7 +244,7 @@ impl DebugPanelItem { .children( scopes .iter() - .map(|scope| self.render_scope(thread_state, scope, cx)), + .map(|scope| self.render_scope(&thread_state, scope, cx)), ) .into_any() } @@ -308,10 +308,7 @@ impl DebugPanelItem { } fn disable_button(&self) -> bool { - let thread_state = self.current_thread_state(); - thread_state - .and_then(|s| Some(s.status != ThreadStatus::Stopped)) - .unwrap_or(true) + self.current_thread_state().status != ThreadStatus::Stopped } fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { @@ -423,13 +420,15 @@ impl Render for DebugPanelItem { .tooltip(move |cx| Tooltip::text("Pause", cx)), ), ) - .child(h_flex().size_full().items_start().p_1().gap_4().when_some( - self.current_thread_state(), - |this, thread_state| { - this.child(self.render_stack_frames(cx)) - .child(self.render_scopes(&thread_state, cx)) - }, - )) + .child( + h_flex() + .size_full() + .items_start() + .p_1() + .gap_4() + .child(self.render_stack_frames(cx)) + .child(self.render_scopes(cx)), + ) .into_any() } } From 99e01fc608f02f87243ee0f68724167ae698945f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 21:28:52 +0200 Subject: [PATCH 114/650] Clippyyyyy --- crates/task/src/task_template.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 26c47304504b36..85795184bf98ab 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,4 +1,4 @@ -use std::{net::Ipv4Addr, path::PathBuf, time::Duration}; +use std::{net::Ipv4Addr, path::PathBuf}; use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; From 6aced1b3aabb7ce13cfbdeb89e3eb272af3bef77 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 21:29:47 +0200 Subject: [PATCH 115/650] Add tooltip for tab content to indicate the thread status --- crates/debugger_ui/src/debugger_panel_item.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 2e3a1c7bc1c238..aeea5f03186dc5 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -134,6 +134,15 @@ impl Item for DebugPanelItem { }) .into_any_element() } + + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(SharedString::from(format!( + "{} Thread {} - {:?}", + self.client.config().id, + self.thread_id, + self.current_thread_state().status + ))) + } } impl DebugPanelItem { From ea9e0755df1cd58ce3210598f8e67c45214c356c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 21:51:46 +0200 Subject: [PATCH 116/650] Implement stop action --- crates/dap/src/client.rs | 16 +++++++++++++--- crates/debugger_ui/src/debugger_panel_item.rs | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 97dbf374dd928e..6ea5a7d8ee4841 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -3,10 +3,10 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ - Attach, ConfigurationDone, Continue, Initialize, Launch, Next, Pause, SetBreakpoints, - StepBack, StepIn, StepOut, + Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, + SetBreakpoints, StepBack, StepIn, StepOut, }, - AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, + AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, Variable, @@ -422,6 +422,16 @@ impl DebugAdapterClient { .log_err(); } + pub async fn stop(&self) { + self.request::(DisconnectArguments { + restart: Some(false), + terminate_debuggee: Some(false), + suspend_debuggee: Some(false), + }) + .await + .log_err(); + } + pub async fn set_breakpoints( &self, path: PathBuf, diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index aeea5f03186dc5..c7f8394684d244 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -23,7 +23,7 @@ pub struct DebugPanelItem { actions!( debug_panel_item, - [Continue, StepOver, StepIn, StepOut, Restart, Pause] + [Continue, StepOver, StepIn, StepOut, Restart, Pause, Stop] ); impl DebugPanelItem { @@ -367,6 +367,13 @@ impl DebugPanelItem { .spawn(async move { client.pause(thread_id).await }) .detach(); } + + fn handle_stop_action(&mut self, _: &Stop, cx: &mut ViewContext) { + let client = self.client.clone(); + cx.background_executor() + .spawn(async move { client.stop().await }) + .detach(); + } } impl Render for DebugPanelItem { @@ -382,6 +389,7 @@ impl Render for DebugPanelItem { .capture_action(cx.listener(Self::handle_step_out_action)) .capture_action(cx.listener(Self::handle_restart_action)) .capture_action(cx.listener(Self::handle_pause_action)) + .capture_action(cx.listener(Self::handle_stop_action)) .p_2() .size_full() .items_start() @@ -423,10 +431,10 @@ impl Render for DebugPanelItem { .tooltip(move |cx| Tooltip::text("Restart", cx)), ) .child( - IconButton::new("debug-pause", IconName::DebugStop) - .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Pause)))) + IconButton::new("debug-stop", IconName::DebugStop) + .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop)))) .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Pause", cx)), + .tooltip(move |cx| Tooltip::text("Stop", cx)), ), ) .child( From e350417a3378d591d09fe8e6d33f2d33962b84c8 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 22:55:37 +0200 Subject: [PATCH 117/650] Add debug pause icon --- assets/icons/debug-pause.svg | 1 + crates/ui/src/components/icon.rs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 assets/icons/debug-pause.svg diff --git a/assets/icons/debug-pause.svg b/assets/icons/debug-pause.svg new file mode 100644 index 00000000000000..099a2ccdb9ab1f --- /dev/null +++ b/assets/icons/debug-pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 94d3bfba27c2d0..b639854d4176df 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -135,6 +135,7 @@ pub enum IconName { Copy, CountdownTimer, Dash, + DebugPause, DebugContinue, DebugStepOver, DebugStepInto, @@ -293,6 +294,7 @@ impl IconName { IconName::Copy => "icons/copy.svg", IconName::CountdownTimer => "icons/countdown_timer.svg", IconName::Dash => "icons/dash.svg", + IconName::DebugPause => "icons/debug-pause.svg", IconName::DebugContinue => "icons/debug-continue.svg", IconName::DebugStepOver => "icons/debug-step-over.svg", IconName::DebugStepInto => "icons/debug-step-into.svg", From 7ce1b8dc76a73f682de1ee118869a970d84f710d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 23:06:39 +0200 Subject: [PATCH 118/650] Update the thread state status after continue request if continue event was not send --- crates/dap/src/client.rs | 12 ++++---- crates/debugger_ui/src/debugger_panel_item.rs | 29 +++++++++++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 6ea5a7d8ee4841..f377f4dd25df24 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -6,10 +6,11 @@ use dap_types::{ Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, SetBreakpoints, StepBack, StepIn, StepOut, }, - AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, - InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, - Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, - StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, Variable, + AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, + DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, + NextArguments, PauseArguments, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, + SourceBreakpoint, StackFrame, StepBackArguments, StepInArguments, StepOutArguments, + SteppingGranularity, Variable, }; use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, @@ -356,13 +357,12 @@ impl DebugAdapterClient { .await } - pub async fn resume(&self, thread_id: u64) { + pub async fn resume(&self, thread_id: u64) -> Result { self.request::(ContinueArguments { thread_id, single_thread: Some(true), }) .await - .log_err(); } pub async fn step_over(&self, thread_id: u64) { diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index c7f8394684d244..1fd4dea9271b4a 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,6 +1,6 @@ +use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable}; - use gpui::{ actions, list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListState, Subscription, View, @@ -10,8 +10,6 @@ use ui::WindowContext; use ui::{prelude::*, Tooltip}; use workspace::item::{Item, ItemEvent}; -use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; - pub struct DebugPanelItem { thread_id: u64, focus_handle: FocusHandle, @@ -323,9 +321,28 @@ impl DebugPanelItem { fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; - cx.background_executor() - .spawn(async move { client.resume(thread_id).await }) - .detach(); + let status = self.current_thread_state().status; + + cx.spawn(|this, mut cx| async move { + let response = client.resume(thread_id).await?; + + this.update(&mut cx, |this, cx| { + // if the debug adapter does not send the continued event, + // and the status of the thread did not change we haven to assume the thread is running + if status == this.current_thread_state().status { + if response.all_threads_continued.unwrap_or(false) { + for thread in client.thread_states().values_mut() { + thread.status = ThreadStatus::Running; + } + } else { + client.update_thread_state_status(thread_id, ThreadStatus::Running); + } + + cx.notify(); + } + }) + }) + .detach_and_log_err(cx); } fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext) { From ef098c028befb6bc140bf510730595d3d080355f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 23:07:29 +0200 Subject: [PATCH 119/650] Show/disable actions buttons based on thread state status --- crates/debugger_ui/src/debugger_panel_item.rs | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 1fd4dea9271b4a..8ae387371c20e7 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -314,10 +314,6 @@ impl DebugPanelItem { .into_any() } - fn disable_button(&self) -> bool { - self.current_thread_state().status != ThreadStatus::Stopped - } - fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; @@ -395,7 +391,7 @@ impl DebugPanelItem { impl Render for DebugPanelItem { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let disable_button = self.disable_button(); + let thread_status = self.current_thread_state().status; v_flex() .key_context("DebugPanelItem") @@ -413,44 +409,67 @@ impl Render for DebugPanelItem { .child( h_flex() .gap_2() - .child( - IconButton::new("debug-continue", IconName::DebugContinue) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Continue))), + .when( + self.current_thread_state().status != ThreadStatus::Running, + |this| { + this.child( + IconButton::new("debug-continue", IconName::DebugContinue) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(Continue)) + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |cx| Tooltip::text("Continue program", cx)), ) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Continue debug", cx)), + }, + ) + .when( + self.current_thread_state().status == ThreadStatus::Running, + |this| { + this.child( + IconButton::new("debug-pause", IconName::DebugPause) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Pause))), + ) + .tooltip(move |cx| Tooltip::text("Pause program", cx)), + ) + }, ) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) .on_click( cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOver))), ) - .disabled(disable_button) + .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Step over", cx)), ) .child( IconButton::new("debug-step-in", IconName::DebugStepInto) .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepIn)))) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Go in", cx)), + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |cx| Tooltip::text("Step in", cx)), ) .child( IconButton::new("debug-step-out", IconName::DebugStepOut) .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOut)))) - .disabled(disable_button) - .tooltip(move |cx| Tooltip::text("Go out", cx)), + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |cx| Tooltip::text("Step out", cx)), ) .child( IconButton::new("debug-restart", IconName::DebugRestart) .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Restart)))) - .disabled(disable_button) + .disabled( + thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, + ) .tooltip(move |cx| Tooltip::text("Restart", cx)), ) .child( IconButton::new("debug-stop", IconName::DebugStop) .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop)))) - .disabled(disable_button) + .disabled( + thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, + ) .tooltip(move |cx| Tooltip::text("Stop", cx)), ), ) From a48166e5a0af619dfb8511cb670e2fa778d648bb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 16 Jul 2024 23:07:43 +0200 Subject: [PATCH 120/650] Add method to get the capabilities from the client --- crates/dap/src/client.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f377f4dd25df24..3ffaa52ed1a2d2 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -298,6 +298,10 @@ impl DebugAdapterClient { self.config.request.clone() } + pub fn capabilities(&self) -> Option { + self.capabilities.clone() + } + pub fn next_request_id(&self) -> u64 { self.request_count.fetch_add(1, Ordering::Relaxed) } From 923ae5473ac3f7a6e3b730e75210595e21dc6669 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 17 Jul 2024 09:22:29 +0200 Subject: [PATCH 121/650] Fix that we change the thread state status to running Not all the debug adapters send the continue event so we couldn't determine that the thread state status was `running`. So we have to determine it our selfs if the thread state status did not change, if so we change the status to `running`. This allows us to show the pause button when the thread state status is running, so you can pause the programs execution. --- crates/dap/src/client.rs | 12 +-- crates/debugger_ui/src/debugger_panel_item.rs | 84 ++++++++++++------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 3ffaa52ed1a2d2..0d4e2157712b40 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -369,17 +369,16 @@ impl DebugAdapterClient { .await } - pub async fn step_over(&self, thread_id: u64) { + pub async fn step_over(&self, thread_id: u64) -> Result<()> { self.request::(NextArguments { thread_id, granularity: Some(SteppingGranularity::Statement), single_thread: Some(true), }) .await - .log_err(); } - pub async fn step_in(&self, thread_id: u64) { + pub async fn step_in(&self, thread_id: u64) -> Result<()> { self.request::(StepInArguments { thread_id, target_id: None, @@ -387,27 +386,24 @@ impl DebugAdapterClient { single_thread: Some(true), }) .await - .log_err(); } - pub async fn step_out(&self, thread_id: u64) { + pub async fn step_out(&self, thread_id: u64) -> Result<()> { self.request::(StepOutArguments { thread_id, granularity: Some(SteppingGranularity::Statement), single_thread: Some(true), }) .await - .log_err(); } - pub async fn step_back(&self, thread_id: u64) { + pub async fn step_back(&self, thread_id: u64) -> Result<()> { self.request::(StepBackArguments { thread_id, single_thread: Some(true), granularity: Some(SteppingGranularity::Statement), }) .await - .log_err(); } pub async fn restart(&self, thread_id: u64) { diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 8ae387371c20e7..d381d6695ff253 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,9 +1,10 @@ use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; +use anyhow::Result; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable}; use gpui::{ - actions, list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListState, - Subscription, View, + actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, + FocusableView, ListState, Subscription, View, WeakView, }; use std::sync::Arc; use ui::WindowContext; @@ -314,29 +315,40 @@ impl DebugPanelItem { .into_any() } + // if the debug adapter does not send the continued event, + // and the status of the thread did not change we have to assume the thread is running + // so we have to update the thread state status to running + fn update_thread_state( + this: WeakView, + previous_status: ThreadStatus, + all_threads_continued: Option, + mut cx: AsyncWindowContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + if previous_status == this.current_thread_state().status { + if all_threads_continued.unwrap_or(false) { + for thread in this.client.thread_states().values_mut() { + thread.status = ThreadStatus::Running; + } + } else { + this.client + .update_thread_state_status(this.thread_id, ThreadStatus::Running); + } + + cx.notify(); + } + }) + } + fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; - let status = self.current_thread_state().status; + let previous_status = self.current_thread_state().status; - cx.spawn(|this, mut cx| async move { + cx.spawn(|this, cx| async move { let response = client.resume(thread_id).await?; - this.update(&mut cx, |this, cx| { - // if the debug adapter does not send the continued event, - // and the status of the thread did not change we haven to assume the thread is running - if status == this.current_thread_state().status { - if response.all_threads_continued.unwrap_or(false) { - for thread in client.thread_states().values_mut() { - thread.status = ThreadStatus::Running; - } - } else { - client.update_thread_state_status(thread_id, ThreadStatus::Running); - } - - cx.notify(); - } - }) + Self::update_thread_state(this, previous_status, response.all_threads_continued, cx) }) .detach_and_log_err(cx); } @@ -344,30 +356,46 @@ impl DebugPanelItem { fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; - cx.background_executor() - .spawn(async move { client.step_over(thread_id).await }) - .detach(); + let previous_status = self.current_thread_state().status; + + cx.spawn(|this, cx| async move { + client.step_over(thread_id).await?; + + Self::update_thread_state(this, previous_status, None, cx) + }) + .detach_and_log_err(cx); } fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; - cx.background_executor() - .spawn(async move { client.step_in(thread_id).await }) - .detach(); + let previous_status = self.current_thread_state().status; + + cx.spawn(|this, cx| async move { + client.step_in(thread_id).await?; + + Self::update_thread_state(this, previous_status, None, cx) + }) + .detach_and_log_err(cx); } fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; - cx.background_executor() - .spawn(async move { client.step_out(thread_id).await }) - .detach(); + let previous_status = self.current_thread_state().status; + + cx.spawn(|this, cx| async move { + client.step_out(thread_id).await?; + + Self::update_thread_state(this, previous_status, None, cx) + }) + .detach_and_log_err(cx); } fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; + cx.background_executor() .spawn(async move { client.restart(thread_id).await }) .detach(); From 4c777ad14078d19efae460251f36ce795554e1fe Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 17 Jul 2024 14:09:56 +0200 Subject: [PATCH 122/650] Fix deserialize error when debug adapter send empty `{}` body but expected type `()` --- crates/dap/src/transport.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 24fa67bfbcd9ba..1fcbc2978be34a 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -11,7 +11,7 @@ use futures::{ }; use gpui::AsyncAppContext; use parking_lot::Mutex; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use smol::io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _}; use std::{collections::HashMap, sync::Arc}; @@ -62,9 +62,22 @@ pub struct Response { pub success: bool, pub command: String, pub message: Option, + #[serde(default, deserialize_with = "deserialize_empty_object")] pub body: Option, } +fn deserialize_empty_object<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + if value == Value::Object(serde_json::Map::new()) { + Ok(None) + } else { + Ok(Some(value)) + } +} + #[derive(Debug)] pub struct Transport { pending_requests: Mutex>>>, From ef67321ff2ab18051b7e25c83c87c687b92d3367 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 17 Jul 2024 14:41:22 +0200 Subject: [PATCH 123/650] Implement restart program --- crates/dap/src/client.rs | 25 +++++++++++-------- crates/debugger_ui/src/debugger_panel_item.rs | 12 ++++++--- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 0d4e2157712b40..1fd4ada7cf634b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -3,14 +3,14 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ - Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, + Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Restart, SetBreakpoints, StepBack, StepIn, StepOut, }, AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, - NextArguments, PauseArguments, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, - SourceBreakpoint, StackFrame, StepBackArguments, StepInArguments, StepOutArguments, - SteppingGranularity, Variable, + NextArguments, PauseArguments, RestartArguments, Scope, SetBreakpointsArguments, + SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, + StepInArguments, StepOutArguments, SteppingGranularity, Variable, }; use futures::{ channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, @@ -298,8 +298,8 @@ impl DebugAdapterClient { self.config.request.clone() } - pub fn capabilities(&self) -> Option { - self.capabilities.clone() + pub fn capabilities(&self) -> dap_types::Capabilities { + self.capabilities.clone().unwrap() } pub fn next_request_id(&self) -> u64 { @@ -406,11 +406,14 @@ impl DebugAdapterClient { .await } - pub async fn restart(&self, thread_id: u64) { - self.request::(StepBackArguments { - thread_id, - single_thread: Some(true), - granularity: Some(SteppingGranularity::Statement), + pub async fn restart(&self) { + self.request::(RestartArguments { + raw: self + .config + .request_args + .as_ref() + .map(|v| v.args.clone()) + .unwrap_or(Value::Null), }) .await .log_err(); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index d381d6695ff253..e512e868c0fafe 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -394,10 +394,9 @@ impl DebugPanelItem { fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext) { let client = self.client.clone(); - let thread_id = self.thread_id; cx.background_executor() - .spawn(async move { client.restart(thread_id).await }) + .spawn(async move { client.restart().await }) .detach(); } @@ -486,8 +485,13 @@ impl Render for DebugPanelItem { IconButton::new("debug-restart", IconName::DebugRestart) .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Restart)))) .disabled( - thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, + !self + .client + .capabilities() + .supports_restart_request + .unwrap_or_default() + || thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, ) .tooltip(move |cx| Tooltip::text("Restart", cx)), ) From d08e28f4e003e55960d81331c73f45bb30ea9668 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 17 Jul 2024 15:43:16 +0200 Subject: [PATCH 124/650] Clean up show continue/pause buttons --- crates/debugger_ui/src/debugger_panel_item.rs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index e512e868c0fafe..0a6c11c03912e6 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -436,20 +436,7 @@ impl Render for DebugPanelItem { .child( h_flex() .gap_2() - .when( - self.current_thread_state().status != ThreadStatus::Running, - |this| { - this.child( - IconButton::new("debug-continue", IconName::DebugContinue) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(Continue)) - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Continue program", cx)), - ) - }, - ) - .when( + .when_else( self.current_thread_state().status == ThreadStatus::Running, |this| { this.child( @@ -460,6 +447,16 @@ impl Render for DebugPanelItem { .tooltip(move |cx| Tooltip::text("Pause program", cx)), ) }, + |this| { + this.child( + IconButton::new("debug-continue", IconName::DebugContinue) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(Continue)) + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |cx| Tooltip::text("Continue program", cx)), + ) + }, ) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) From 7c9771b8d72dea94f2d1665196a276d5136e24c0 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 18 Jul 2024 14:20:01 -0400 Subject: [PATCH 125/650] Updated debugger --- crates/dap/src/transport.rs | 2 ++ crates/debugger_ui/src/debugger_panel.rs | 16 ++++++++++++++-- crates/project/src/project.rs | 8 ++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 24fa67bfbcd9ba..49a18f6117fd95 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -45,6 +45,8 @@ pub enum Events { ProgressEnd(ProgressEndEvent), Invalidated(InvalidatedEvent), Memory(MemoryEvent), + #[serde(untagged)] + Other(HashMap) } #[derive(Debug, Deserialize, Serialize)] diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 31e9e3f4ed7f7f..b4024f1fa7e310 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -129,7 +129,18 @@ impl DebugPanel { task.await?; - client.configuration_done().await + // client.is_server_ready = true; + client.configuration_done().await?; + + let request_args = client.config().request_args.map(|a| a.args); + + // send correct request based on adapter config + match client.config().request { + DebugRequestType::Launch => client.launch(request_args).await?, + DebugRequestType::Attach => client.attach(request_args).await?, + }; + + anyhow::Ok(()) }) .detach_and_log_err(cx); } @@ -148,7 +159,8 @@ impl DebugPanel { Events::ProgressEnd(_) => {} Events::ProgressStart(_) => {} Events::ProgressUpdate(_) => {} - Events::Invalidated(_) => {} + Events::Invalidated(_) => {}, + Events::Other(_) => {}, } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4d3638409387f4..1c3d90d8b72b92 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1166,10 +1166,10 @@ impl Project { client.initialize().await.log_err()?; // send correct request based on adapter config - match adapter_config.request { - DebugRequestType::Launch => client.launch(request_args).await.log_err()?, - DebugRequestType::Attach => client.attach(request_args).await.log_err()?, - }; + // match adapter_config.request { + // DebugRequestType::Launch => client.launch(request_args).await.log_err()?, + // DebugRequestType::Attach => client.attach(request_args).await.log_err()?, + // }; let client = Arc::new(client); From 15d518639902b395a8fed214ba98aad142588565 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 23 Jul 2024 01:56:54 -0400 Subject: [PATCH 126/650] Fix race condition when using late case debug adapters The main thing this commit does is fix race conditions between a dap client's event handler and sending a launch/attach request. Some adapters would only respond to a starting request after the client handled an init event, which could never happen because the client used to start it's handler after it sent a launch request. This commit also ignores undefined errors instead of crashing. This allows the client to work with adapters that send custom events. Finially, I added some more descriptive error messages and change client's starting request seq from 0 to 1 to be more in line with the specs. --- crates/dap/src/client.rs | 2 +- crates/dap/src/transport.rs | 32 ++++++++++++++++++------ crates/debugger_ui/src/debugger_panel.rs | 6 ++--- crates/editor/src/editor.rs | 6 ++--- crates/project/src/project.rs | 13 +++++----- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 1fd4ada7cf634b..741f840f9fe02d 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -216,7 +216,7 @@ impl DebugAdapterClient { _process: process, capabilities: None, server_tx: server_tx.clone(), - request_count: AtomicU64::new(0), + request_count: AtomicU64::new(1), thread_states: Arc::new(Mutex::new(HashMap::new())), }; diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 3f2c58291c230b..cff57c496fb015 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -46,7 +46,7 @@ pub enum Events { Invalidated(InvalidatedEvent), Memory(MemoryEvent), #[serde(untagged)] - Other(HashMap) + Other(HashMap), } #[derive(Debug, Deserialize, Serialize)] @@ -120,7 +120,13 @@ impl Transport { let mut content_length = None; loop { buffer.truncate(0); - if reader.read_line(buffer).await? == 0 { + + if reader + .read_line(buffer) + .await + .with_context(|| "reading a message from server")? + == 0 + { return Err(anyhow!("reader stream closed")); }; @@ -141,10 +147,12 @@ impl Transport { let content_length = content_length.context("missing content length")?; let mut content = vec![0; content_length]; - reader.read_exact(&mut content).await?; - let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; + reader + .read_exact(&mut content) + .await + .with_context(|| "reading after a loop")?; - dbg!(msg); + let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; Ok(serde_json::from_str::(msg)?) } @@ -187,7 +195,6 @@ impl Transport { server_stdin.write_all(request.as_bytes()).await?; server_stdin.flush().await?; - Ok(()) } @@ -212,11 +219,19 @@ impl Transport { }; if let Some(mut tx) = pending_request { - tx.send(Self::process_response(res)).await?; + if !tx.is_closed() { + tx.send(Self::process_response(res)).await?; + } else { + log::warn!( + "Response stream associated with request seq: {} is closed", + &res.request_seq + ); // TODO: Fix this case so it never happens + } } else { client_tx.send(Payload::Response(res)).await?; }; } + Payload::Request(_) => { client_tx.send(payload).await?; } @@ -239,7 +254,8 @@ impl Transport { &client_tx, Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?, ) - .await?; + .await + .context("Process server message failed in transport::receive")?; } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index d8290912c2e338..0c80576c67310e 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use dap::client::{self, DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::requests::{Disconnect, Scopes, StackTrace, Variables}; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ @@ -132,8 +132,8 @@ impl DebugPanel { Events::ProgressEnd(_) => {} Events::ProgressStart(_) => {} Events::ProgressUpdate(_) => {} - Events::Invalidated(_) => {}, - Events::Other(_) => {}, + Events::Invalidated(_) => {} + Events::Other(_) => {} } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a1427cc8ec6cd1..538736a256ffd9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -450,7 +450,7 @@ struct ResolvedTasks { #[derive(Clone, Debug)] struct Breakpoint { row: MultiBufferRow, - line: BufferRow, + _line: BufferRow, } #[derive(Copy, Clone, Debug)] @@ -5942,11 +5942,11 @@ impl Editor { key, Breakpoint { row: MultiBufferRow(row), - line: row, + _line: row, }, ); } - + project.update(cx, |project, cx| { project.update_breakpoint(buffer, row + 1, cx); }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1c3d90d8b72b92..9206c308b695ad 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1163,13 +1163,7 @@ impl Project { .log_err()?; // initialize request - client.initialize().await.log_err()?; - - // send correct request based on adapter config - // match adapter_config.request { - // DebugRequestType::Launch => client.launch(request_args).await.log_err()?, - // DebugRequestType::Attach => client.attach(request_args).await.log_err()?, - // }; + let _capabilities = client.initialize().await.log_err()?; let client = Arc::new(client); @@ -1208,6 +1202,11 @@ impl Project { }) .log_err(); + match adapter_config.request { + DebugRequestType::Launch => client.launch(request_args).await.log_err()?, + DebugRequestType::Attach => client.attach(request_args).await.log_err()?, + }; + Some(client) }); From 0b8c4ded9ee6afdd9fbe262aae87150dc0dfd9bf Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 23 Jul 2024 02:12:09 -0400 Subject: [PATCH 127/650] Get clippy to run successfully --- crates/debugger_ui/src/debugger_panel.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 0c80576c67310e..2afe7495482348 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -313,10 +313,9 @@ impl DebugPanel { return; }; - let client_id = client_id.clone(); - let client = this.debug_client_by_id(client_id.clone(), cx); + let client_id = *client_id; + let client = this.debug_client_by_id(client_id, cx); - let client_id = client_id.clone(); cx.spawn({ let event = event.clone(); |this, mut cx| async move { @@ -333,11 +332,11 @@ impl DebugPanel { stack_trace_response.stack_frames.first().unwrap().clone(); let mut scope_tasks = Vec::new(); for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { - let frame_id = stack_frame.id.clone(); + let frame_id = stack_frame.id; let client = client.clone(); scope_tasks.push(async move { anyhow::Ok(( - frame_id.clone(), + frame_id, client .request::(ScopesArguments { frame_id }) .await?, @@ -353,11 +352,11 @@ impl DebugPanel { scopes.insert(thread_id, response.scopes.clone()); for scope in response.scopes { - let scope_reference = scope.variables_reference.clone(); + let scope_reference = scope.variables_reference; let client = client.clone(); variable_tasks.push(async move { anyhow::Ok(( - scope_reference.clone(), + scope_reference, client .request::(VariablesArguments { variables_reference: scope_reference, @@ -383,7 +382,7 @@ impl DebugPanel { .or_insert(ThreadState::default()); thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); - thread_state.stack_frames = stack_trace_response.stack_frames.clone(); + thread_state.stack_frames = stack_trace_response.stack_frames; thread_state.scopes = scopes; thread_state.variables = variables; thread_state.status = ThreadStatus::Stopped; @@ -453,7 +452,7 @@ impl DebugPanel { .thread_states() .insert(thread_id, ThreadState::default()); } else { - client.update_thread_state_status(thread_id.clone(), ThreadStatus::Ended); + client.update_thread_state_status(thread_id, ThreadStatus::Ended); // TODO: we want to figure out for witch clients/threads we should remove the highlights cx.spawn({ From 780bfafedf7a1f775602a6d4642d9d4d4ac18f57 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 23 Jul 2024 02:56:20 -0400 Subject: [PATCH 128/650] Add some function docs to dap client --- crates/dap/src/client.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 741f840f9fe02d..e8607c317b8bf2 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -72,6 +72,15 @@ pub struct DebugAdapterClient { } impl DebugAdapterClient { + /// Creates & returns a new debug adapter client + /// + /// # Parameters + /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients + /// - `config`: The adapter specific configurations from debugger task that is starting + /// - `command`: The command that starts the debugger + /// - `args`: Arguments of the command that starts the debugger + /// - `project_path`: The absolute path of the project that is being debugged + /// - `cx`: The context that the new client belongs too pub async fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -90,6 +99,17 @@ impl DebugAdapterClient { } } + /// Creates a debug client that connects to an adapter through tcp + /// + /// TCP clients don't have an error communication stream with an adapter + /// + /// # Parameters + /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients + /// - `config`: The adapter specific configurations from debugger task that is starting + /// - `command`: The command that starts the debugger + /// - `args`: Arguments of the command that starts the debugger + /// - `project_path`: The absolute path of the project that is being debugged + /// - `cx`: The context that the new client belongs too async fn create_tcp_client( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -143,6 +163,7 @@ impl DebugAdapterClient { ) } + /// Get an open port to use with the tcp client when not supplied by debug config async fn get_port() -> Option { Some( TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0)) @@ -154,6 +175,15 @@ impl DebugAdapterClient { ) } + /// Creates a debug client that connects to an adapter through std input/output + /// + /// # Parameters + /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients + /// - `config`: The adapter specific configurations from debugger task that is starting + /// - `command`: The command that starts the debugger + /// - `args`: Arguments of the command that starts the debugger + /// - `project_path`: The absolute path of the project that is being debugged + /// - `cx`: The context that the new client belongs too async fn create_stdio_client( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -226,6 +256,14 @@ impl DebugAdapterClient { Ok(client) } + /// Set's up a client's event handler. + /// + /// This function should only be called once or else errors will arise + /// # Parameters + /// `client`: A pointer to the client to pass the event handler too + /// `event_handler`: The function that is called to handle events + /// should be DebugPanel::handle_debug_client_events + /// `cx`: The context that this task will run in pub async fn handle_events( client: Arc, mut event_handler: F, @@ -258,6 +296,8 @@ impl DebugAdapterClient { } } + /// Send a request to an adapter and get a response back + /// Note: This function will block until a response is sent back from the adapter pub async fn request( &self, arguments: R::Arguments, From 2e10853b34021809f14e6094492faec96345b953 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 23 Jul 2024 14:45:04 +0200 Subject: [PATCH 129/650] Fix sending incorrect start request id --- crates/dap/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 1fd4ada7cf634b..741f840f9fe02d 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -216,7 +216,7 @@ impl DebugAdapterClient { _process: process, capabilities: None, server_tx: server_tx.clone(), - request_count: AtomicU64::new(0), + request_count: AtomicU64::new(1), thread_states: Arc::new(Mutex::new(HashMap::new())), }; From b88fd3e0c55beb49294c080ef3d730540098c824 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 24 Jul 2024 11:50:28 +0200 Subject: [PATCH 130/650] Make sure we read events as early as possible We have to read them as early as possible to make sure we support more debug adapters. Some of them wait before you handled the initialized event. That is also why I moved the launch/attach request to be directly after the initialize request. For example the xdebug adapter sends the initialized event only when you send the launch request, so before if we send the breakpoints and configuration is done requests the adapter would stop and all channels were closed. --- crates/dap/src/client.rs | 140 +++++++++++++++-------- crates/dap/src/transport.rs | 48 ++++---- crates/debugger_ui/src/debugger_panel.rs | 69 ++++++----- crates/editor/src/editor.rs | 6 +- crates/project/src/project.rs | 49 +++----- 5 files changed, 170 insertions(+), 142 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 741f840f9fe02d..207ef6c006d8b2 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,4 +1,4 @@ -use crate::transport::{self, Events, Payload, Request, Transport}; +use crate::transport::{Events, Payload, Request, Response, Transport}; use anyhow::{anyhow, Context, Result}; use dap_types::{ @@ -12,14 +12,12 @@ use dap_types::{ SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, Variable, }; -use futures::{ - channel::mpsc::{channel, unbounded, UnboundedReceiver, UnboundedSender}, - AsyncBufRead, AsyncReadExt, AsyncWrite, SinkExt as _, StreamExt, -}; +use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; use parking_lot::{Mutex, MutexGuard}; use serde_json::Value; use smol::{ + channel::{bounded, unbounded, Receiver, Sender}, io::BufReader, net::{TcpListener, TcpStream}, process::{self, Child}, @@ -63,42 +61,68 @@ pub struct ThreadState { pub struct DebugAdapterClient { id: DebugAdapterClientId, _process: Option, - server_tx: UnboundedSender, + server_tx: Sender, request_count: AtomicU64, - capabilities: Option, + capabilities: Arc>>, config: DebugAdapterConfig, - client_rx: Arc>>, thread_states: Arc>>, // thread_id -> thread_state } impl DebugAdapterClient { - pub async fn new( + pub async fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, command: &str, args: Vec<&str>, project_path: PathBuf, + event_handler: F, cx: &mut AsyncAppContext, - ) -> Result { + ) -> Result> + where + F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, + { match config.connection.clone() { DebugConnectionType::TCP(host) => { - Self::create_tcp_client(id, config, host, command, args, project_path, cx).await + Self::create_tcp_client( + id, + config, + host, + command, + args, + project_path, + event_handler, + cx, + ) + .await } DebugConnectionType::STDIO => { - Self::create_stdio_client(id, config, command, args, project_path, cx).await + Self::create_stdio_client( + id, + config, + command, + args, + project_path, + event_handler, + cx, + ) + .await } } } - async fn create_tcp_client( + async fn create_tcp_client( id: DebugAdapterClientId, config: DebugAdapterConfig, host: TCPHost, command: &str, args: Vec<&str>, project_path: PathBuf, + event_handler: F, cx: &mut AsyncAppContext, - ) -> Result { + ) -> Result> + where + F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, + { let mut port = host.port; if port.is_none() { port = Self::get_port().await; @@ -139,6 +163,7 @@ impl DebugAdapterClient { Box::new(tx), None, Some(process), + event_handler, cx, ) } @@ -154,14 +179,18 @@ impl DebugAdapterClient { ) } - async fn create_stdio_client( + async fn create_stdio_client( id: DebugAdapterClientId, config: DebugAdapterConfig, command: &str, args: Vec<&str>, project_path: PathBuf, + event_handler: F, cx: &mut AsyncAppContext, - ) -> Result { + ) -> Result> + where + F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, + { let mut command = process::Command::new(command); command .current_dir(project_path) @@ -192,49 +221,67 @@ impl DebugAdapterClient { let stdout = Box::new(BufReader::new(stdout)); let stderr = Box::new(BufReader::new(stderr)); - Self::handle_transport(id, config, stdout, stdin, Some(stderr), Some(process), cx) + Self::handle_transport( + id, + config, + stdout, + stdin, + Some(stderr), + Some(process), + event_handler, + cx, + ) } - pub fn handle_transport( + pub fn handle_transport( id: DebugAdapterClientId, config: DebugAdapterConfig, rx: Box, tx: Box, err: Option>, process: Option, + event_handler: F, cx: &mut AsyncAppContext, - ) -> Result { + ) -> Result> + where + F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, + { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); - let client_rx = Arc::new(smol::lock::Mutex::new(client_rx)); - - let client = Self { + let client = Arc::new(Self { id, config, - client_rx, + server_tx, _process: process, - capabilities: None, - server_tx: server_tx.clone(), request_count: AtomicU64::new(1), + capabilities: Default::default(), thread_states: Arc::new(Mutex::new(HashMap::new())), - }; + }); + + cx.update(|cx| { + cx.background_executor() + .spawn(Self::handle_recv(server_rx, client_tx)) + .detach_and_log_err(cx); - cx.spawn(move |_| Self::handle_recv(server_rx, server_tx, client_tx)) - .detach(); + cx.spawn(|mut cx| async move { + Self::handle_events(client_rx, event_handler, &mut cx).await + }) + .detach_and_log_err(cx); + })?; Ok(client) } pub async fn handle_events( - client: Arc, + client_rx: Receiver, mut event_handler: F, - cx: AsyncAppContext, + cx: &mut AsyncAppContext, ) -> Result<()> where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { - while let Some(payload) = client.client_rx.lock().await.next().await { + while let Ok(payload) = client_rx.recv().await { cx.update(|cx| match payload { Payload::Event(event) => event_handler(*event, cx), _ => unreachable!(), @@ -244,18 +291,16 @@ impl DebugAdapterClient { anyhow::Ok(()) } - async fn handle_recv( - mut server_rx: UnboundedReceiver, - mut server_tx: UnboundedSender, - mut client_tx: UnboundedSender, - ) { - while let Some(payload) = server_rx.next().await { + async fn handle_recv(server_rx: Receiver, client_tx: Sender) -> Result<()> { + while let Ok(payload) = server_rx.recv().await { match payload { - Payload::Event(ev) => client_tx.send(Payload::Event(ev)).await.log_err(), - Payload::Response(res) => server_tx.send(Payload::Response(res)).await.log_err(), - Payload::Request(req) => client_tx.send(Payload::Request(req)).await.log_err(), + Payload::Event(ev) => client_tx.send(Payload::Event(ev)).await?, + Payload::Response(_) => unreachable!(), + Payload::Request(req) => client_tx.send(Payload::Request(req)).await?, }; } + + anyhow::Ok(()) } pub async fn request( @@ -264,7 +309,7 @@ impl DebugAdapterClient { ) -> Result { let serialized_arguments = serde_json::to_value(arguments)?; - let (callback_tx, mut callback_rx) = channel::>(1); + let (callback_tx, callback_rx) = bounded::>(1); let request = Request { back_ch: Some(callback_tx), @@ -273,12 +318,9 @@ impl DebugAdapterClient { arguments: Some(serialized_arguments), }; - self.server_tx - .clone() - .send(Payload::Request(request)) - .await?; + self.server_tx.send(Payload::Request(request)).await?; - let response = callback_rx.next().await.ok_or(anyhow!("no response"))??; + let response = callback_rx.recv().await??; match response.success { true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), @@ -299,7 +341,7 @@ impl DebugAdapterClient { } pub fn capabilities(&self) -> dap_types::Capabilities { - self.capabilities.clone().unwrap() + self.capabilities.lock().clone().unwrap_or_default() } pub fn next_request_id(&self) -> u64 { @@ -320,7 +362,7 @@ impl DebugAdapterClient { self.thread_states.lock().get(&thread_id).cloned().unwrap() } - pub async fn initialize(&mut self) -> Result { + pub async fn initialize(&self) -> Result { let args = dap_types::InitializeRequestArguments { client_id: Some("zed".to_owned()), client_name: Some("Zed".to_owned()), @@ -342,7 +384,7 @@ impl DebugAdapterClient { let capabilities = self.request::(args).await?; - self.capabilities = Some(capabilities.clone()); + *self.capabilities.lock() = Some(capabilities.clone()); Ok(capabilities) } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 1fcbc2978be34a..3b46ab14405880 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -5,15 +5,15 @@ use dap_types::{ ProgressEndEvent, ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent, ThreadEvent, }; -use futures::{ - channel::mpsc::{unbounded, Sender, UnboundedReceiver, UnboundedSender}, - AsyncBufRead, AsyncWrite, SinkExt as _, StreamExt, -}; +use futures::{AsyncBufRead, AsyncWrite}; use gpui::AsyncAppContext; -use parking_lot::Mutex; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; -use smol::io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _}; +use smol::{ + channel::{unbounded, Receiver, Sender}, + io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt}, + lock::Mutex, +}; use std::{collections::HashMap, sync::Arc}; #[derive(Debug, Deserialize, Serialize)] @@ -89,7 +89,7 @@ impl Transport { server_stdin: Box, server_stderr: Option>, cx: &mut AsyncAppContext, - ) -> (UnboundedReceiver, UnboundedSender) { + ) -> (Receiver, Sender) { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); @@ -98,13 +98,20 @@ impl Transport { }); let _ = cx.update(|cx| { - cx.spawn(|_| Self::receive(transport.clone(), server_stdout, client_tx)) + let transport = transport.clone(); + + cx.background_executor() + .spawn(Self::receive(transport.clone(), server_stdout, client_tx)) .detach_and_log_err(cx); - cx.spawn(|_| Self::send(transport, server_stdin, client_rx)) + + cx.background_executor() + .spawn(Self::send(transport.clone(), server_stdin, client_rx)) .detach_and_log_err(cx); if let Some(stderr) = server_stderr { - cx.spawn(|_| Self::err(stderr)).detach_and_log_err(cx); + cx.background_executor() + .spawn(Self::err(stderr)) + .detach_and_log_err(cx); } }); @@ -166,7 +173,7 @@ impl Transport { ) -> Result<()> { if let Payload::Request(request) = &mut payload { if let Some(back) = request.back_ch.take() { - self.pending_requests.lock().insert(request.seq, back); + self.pending_requests.lock().await.insert(request.seq, back); } } self.send_string_to_server(server_stdin, serde_json::to_string(&payload)?) @@ -179,11 +186,9 @@ impl Transport { request: String, ) -> Result<()> { server_stdin - .write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes()) + .write_all(format!("Content-Length: {}\r\n\r\n{}", request.len(), request).as_bytes()) .await?; - server_stdin.write_all(request.as_bytes()).await?; - server_stdin.flush().await?; Ok(()) @@ -199,17 +204,12 @@ impl Transport { async fn process_server_message( &self, - mut client_tx: &UnboundedSender, + client_tx: &Sender, payload: Payload, ) -> Result<()> { match payload { Payload::Response(res) => { - let pending_request = { - let mut pending_requests = self.pending_requests.lock(); - pending_requests.remove(&res.request_seq) - }; - - if let Some(mut tx) = pending_request { + if let Some(tx) = self.pending_requests.lock().await.remove(&res.request_seq) { tx.send(Self::process_response(res)).await?; } else { client_tx.send(Payload::Response(res)).await?; @@ -228,7 +228,7 @@ impl Transport { async fn receive( transport: Arc, mut server_stdout: Box, - client_tx: UnboundedSender, + client_tx: Sender, ) -> Result<()> { let mut recv_buffer = String::new(); loop { @@ -244,9 +244,9 @@ impl Transport { async fn send( transport: Arc, mut server_stdin: Box, - mut client_rx: UnboundedReceiver, + client_rx: Receiver, ) -> Result<()> { - while let Some(payload) = client_rx.next().await { + while let Ok(payload) = client_rx.recv().await { transport .send_payload_to_server(&mut server_stdin, payload) .await?; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 6021956a8a080c..48c031ef16859e 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -66,10 +66,31 @@ impl DebugPanel { let project = workspace.project().clone(); let _subscriptions = vec![cx.subscribe(&project, { - move |this: &mut Self, _, event, cx| { - if let project::Event::DebugClientEvent { client_id, event } = event { - Self::handle_debug_client_events(this, client_id, event, cx); + move |this: &mut Self, _, event, cx| match event { + project::Event::DebugClientEvent { event, client_id } => { + Self::handle_debug_client_events( + this, + this.debug_client_by_id(*client_id, cx), + event, + cx, + ); } + project::Event::DebugClientStarted(client_id) => { + let client = this.debug_client_by_id(*client_id, cx); + cx.spawn(|_, _| async move { + client.initialize().await?; + + let request_args = client.config().request_args.map(|a| a.args); + + // send correct request based on adapter config + match client.config().request { + DebugRequestType::Launch => client.launch(request_args).await, + DebugRequestType::Attach => client.attach(request_args).await, + } + }) + .detach_and_log_err(cx); + } + _ => {} } })]; @@ -109,19 +130,17 @@ impl DebugPanel { fn handle_debug_client_events( this: &mut Self, - client_id: &DebugAdapterClientId, + client: Arc, event: &Events, cx: &mut ViewContext, ) { match event { - Events::Initialized(event) => { - Self::handle_initialized_event(this, client_id, event, cx) - } - Events::Stopped(event) => Self::handle_stopped_event(this, client_id, event, cx), - Events::Continued(event) => Self::handle_continued_event(this, client_id, event, cx), - Events::Exited(event) => Self::handle_exited_event(this, client_id, event, cx), - Events::Terminated(event) => Self::handle_terminated_event(this, client_id, event, cx), - Events::Thread(event) => Self::handle_thread_event(this, client_id, event, cx), + Events::Initialized(event) => Self::handle_initialized_event(client, event, cx), + Events::Stopped(event) => Self::handle_stopped_event(this, client, event, cx), + Events::Continued(event) => Self::handle_continued_event(this, client, event, cx), + Events::Exited(event) => Self::handle_exited_event(this, client, event, cx), + Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx), + Events::Thread(event) => Self::handle_thread_event(this, client, event, cx), Events::Output(_) => {} Events::Breakpoint(_) => {} Events::Module(_) => {} @@ -257,13 +276,10 @@ impl DebugPanel { } fn handle_initialized_event( - this: &mut Self, - client_id: &DebugAdapterClientId, + client: Arc, _: &Option, cx: &mut ViewContext, ) { - let client = this.debug_client_by_id(*client_id, cx); - cx.spawn(|this, mut cx| async move { let task = this.update(&mut cx, |this, cx| { this.workspace.update(cx, |workspace, cx| { @@ -284,12 +300,11 @@ impl DebugPanel { fn handle_continued_event( this: &mut Self, - client_id: &DebugAdapterClientId, + client: Arc, event: &ContinuedEvent, cx: &mut ViewContext, ) { let all_threads = event.all_threads_continued.unwrap_or(false); - let client = this.debug_client_by_id(*client_id, cx); if all_threads { for thread in client.thread_states().values_mut() { @@ -304,7 +319,7 @@ impl DebugPanel { fn handle_stopped_event( this: &mut Self, - client_id: &DebugAdapterClientId, + client: Arc, event: &StoppedEvent, cx: &mut ViewContext, ) { @@ -312,10 +327,7 @@ impl DebugPanel { return; }; - let client_id = client_id.clone(); - let client = this.debug_client_by_id(client_id.clone(), cx); - - let client_id = client_id.clone(); + let client_id = client.id().clone(); cx.spawn({ let event = event.clone(); |this, mut cx| async move { @@ -440,11 +452,10 @@ impl DebugPanel { fn handle_thread_event( this: &mut Self, - client_id: &DebugAdapterClientId, + client: Arc, event: &ThreadEvent, cx: &mut ViewContext, ) { - let client = this.debug_client_by_id(*client_id, cx); let thread_id = event.thread_id; if event.reason == ThreadEventReason::Started { @@ -468,16 +479,15 @@ impl DebugPanel { .detach_and_log_err(cx); } - cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); + cx.emit(DebugPanelEvent::Thread((client.id(), event.clone()))); } fn handle_exited_event( this: &mut Self, - client_id: &DebugAdapterClientId, + client: Arc, _: &ExitedEvent, cx: &mut ViewContext, ) { - let client = this.debug_client_by_id(*client_id, cx); cx.spawn(|_, _| async move { for thread_state in client.thread_states().values_mut() { thread_state.status = ThreadStatus::Exited; @@ -488,12 +498,11 @@ impl DebugPanel { fn handle_terminated_event( this: &mut Self, - client_id: &DebugAdapterClientId, + client: Arc, event: &Option, cx: &mut ViewContext, ) { let restart_args = event.clone().and_then(|e| e.restart); - let client = this.debug_client_by_id(*client_id, cx); let workspace = this.workspace.clone(); cx.spawn(|_, cx| async move { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a1427cc8ec6cd1..538736a256ffd9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -450,7 +450,7 @@ struct ResolvedTasks { #[derive(Clone, Debug)] struct Breakpoint { row: MultiBufferRow, - line: BufferRow, + _line: BufferRow, } #[derive(Copy, Clone, Debug)] @@ -5942,11 +5942,11 @@ impl Editor { key, Breakpoint { row: MultiBufferRow(row), - line: row, + _line: row, }, ); } - + project.update(cx, |project, cx| { project.update_breakpoint(buffer, row + 1, cx); }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4d3638409387f4..1cf31e562ad3e6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -113,7 +113,7 @@ use std::{ }; use task::{ static_source::{StaticSource, TrackedFile}, - DebugRequestType, RevealStrategy, TaskContext, TaskTemplate, TaskVariables, VariableName, + RevealStrategy, TaskContext, TaskTemplate, TaskVariables, VariableName, }; use terminals::Terminals; use text::{Anchor, BufferId, LineEnding}; @@ -1148,31 +1148,30 @@ impl Project { let command = debug_template.command.clone(); let args = debug_template.args.clone(); - let request_args = adapter_config.clone().request_args.map(|a| a.args); let task = cx.spawn(|this, mut cx| async move { - let mut client = DebugAdapterClient::new( + let project = this.clone(); + let client = DebugAdapterClient::new( id, adapter_config.clone(), &command, args.iter().map(|ele| &ele[..]).collect(), cwd.into(), + move |event, cx| { + project + .update(cx, |_, cx| { + cx.emit(Event::DebugClientEvent { + client_id: id, + event, + }) + }) + .log_err(); + }, &mut cx, ) .await .log_err()?; - // initialize request - client.initialize().await.log_err()?; - - // send correct request based on adapter config - match adapter_config.request { - DebugRequestType::Launch => client.launch(request_args).await.log_err()?, - DebugRequestType::Attach => client.attach(request_args).await.log_err()?, - }; - - let client = Arc::new(client); - this.update(&mut cx, |this, cx| { let handle = this .debug_adapters @@ -1182,28 +1181,6 @@ impl Project { cx.emit(Event::DebugClientStarted(id)); - // call handle events - cx.spawn({ - let client = client.clone(); - move |project, cx| { - DebugAdapterClient::handle_events( - client, - move |event, cx| { - project - .update(cx, |_, cx| { - cx.emit(Event::DebugClientEvent { - client_id: id, - event, - }) - }) - .log_err(); - }, - cx, - ) - } - }) - .detach_and_log_err(cx); - anyhow::Ok(()) }) .log_err(); From ee3323d12a56a4d4b59b0c2f6da2ec1df8660e3c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 24 Jul 2024 12:14:18 +0200 Subject: [PATCH 131/650] Make clippy happy again --- crates/debugger_ui/src/debugger_panel.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 48c031ef16859e..5c216824bb6c18 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -136,11 +136,11 @@ impl DebugPanel { ) { match event { Events::Initialized(event) => Self::handle_initialized_event(client, event, cx), - Events::Stopped(event) => Self::handle_stopped_event(this, client, event, cx), - Events::Continued(event) => Self::handle_continued_event(this, client, event, cx), - Events::Exited(event) => Self::handle_exited_event(this, client, event, cx), + Events::Stopped(event) => Self::handle_stopped_event(client, event, cx), + Events::Continued(event) => Self::handle_continued_event(client, event, cx), + Events::Exited(event) => Self::handle_exited_event(client, event, cx), Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx), - Events::Thread(event) => Self::handle_thread_event(this, client, event, cx), + Events::Thread(event) => Self::handle_thread_event(client, event, cx), Events::Output(_) => {} Events::Breakpoint(_) => {} Events::Module(_) => {} @@ -299,7 +299,6 @@ impl DebugPanel { } fn handle_continued_event( - this: &mut Self, client: Arc, event: &ContinuedEvent, cx: &mut ViewContext, @@ -318,7 +317,6 @@ impl DebugPanel { } fn handle_stopped_event( - this: &mut Self, client: Arc, event: &StoppedEvent, cx: &mut ViewContext, @@ -451,7 +449,6 @@ impl DebugPanel { } fn handle_thread_event( - this: &mut Self, client: Arc, event: &ThreadEvent, cx: &mut ViewContext, @@ -483,7 +480,6 @@ impl DebugPanel { } fn handle_exited_event( - this: &mut Self, client: Arc, _: &ExitedEvent, cx: &mut ViewContext, From 1a421db92d208301521cb5cfd764f0783bbc2b2c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 24 Jul 2024 13:05:14 +0200 Subject: [PATCH 132/650] Make clippy happier than ever --- crates/dap/src/client.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 207ef6c006d8b2..56f2a3cc5f2eec 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -110,6 +110,7 @@ impl DebugAdapterClient { } } + #[allow(clippy::too_many_arguments)] async fn create_tcp_client( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -233,6 +234,7 @@ impl DebugAdapterClient { ) } + #[allow(clippy::too_many_arguments)] pub fn handle_transport( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -284,7 +286,9 @@ impl DebugAdapterClient { while let Ok(payload) = client_rx.recv().await { cx.update(|cx| match payload { Payload::Event(event) => event_handler(*event, cx), - _ => unreachable!(), + e => { + dbg!(&e); + } })?; } From c7f7d18681a11d62f778a77b727fc865f6f81aea Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 25 Jul 2024 00:06:51 -0400 Subject: [PATCH 133/650] Fix debugger's highlight when stepping through code Merging with the main zed branch removed a function that the debugger panel relied on. I added the function back and changed it to work with the updated internals of zed. --- crates/debugger_ui/src/debugger_panel.rs | 4 +-- crates/project/src/project.rs | 31 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 14734c88a0f2e1..174b1bba67f537 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -172,7 +172,7 @@ impl DebugPanel { let task = workspace.update(&mut cx, |workspace, cx| { let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_full_path(&Path::new(&path), cx) + project.project_path_for_absolute_path(&Path::new(&path), cx) }); if let Some(project_path) = project_path { @@ -255,7 +255,7 @@ impl DebugPanel { let task = workspace.update(&mut cx, |workspace, cx| { let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_full_path(&Path::new(&path), cx) + project.project_path_for_absolute_path(&Path::new(&path), cx) }); if let Some(project_path) = project_path { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2f2f4878f3b596..ca4a5fc12c0acf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8609,6 +8609,37 @@ impl Project { }) } + pub fn project_path_for_absolute_path( + &self, + abs_path: &Path, + cx: &AppContext, + ) -> Option { + self.find_local_worktree(abs_path, cx) + .map(|(worktree, relative_path)| ProjectPath { + worktree_id: worktree.read(cx).id(), + path: relative_path.into(), + }) + } + + pub fn find_local_worktree( + &self, + abs_path: &Path, + cx: &AppContext, + ) -> Option<(Model, PathBuf)> { + let trees = self.worktrees(cx); + + for tree in trees { + if let Some(relative_path) = tree + .read(cx) + .as_local() + .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok()) + { + return Some((tree.clone(), relative_path.into())); + } + } + None + } + pub fn get_workspace_root( &self, project_path: &ProjectPath, From 7f1bd3b1d92b35846473ae016b8d66e217f6e67f Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 25 Jul 2024 00:20:29 -0400 Subject: [PATCH 134/650] Get clippy to pass & add an error log instead of using dbg!() --- crates/dap/src/client.rs | 5 +++-- crates/debugger_ui/src/debugger_panel.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 29c880e241edd5..95751a46d351a1 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -130,6 +130,7 @@ impl DebugAdapterClient { /// - `args`: Arguments of the command that starts the debugger /// - `project_path`: The absolute path of the project that is being debugged /// - `cx`: The context that the new client belongs too + #[allow(clippy::too_many_arguments)] async fn create_tcp_client( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -323,8 +324,8 @@ impl DebugAdapterClient { while let Ok(payload) = client_rx.recv().await { cx.update(|cx| match payload { Payload::Event(event) => event_handler(*event, cx), - e => { - dbg!(&e); + err => { + log::error!("Invalid Event: {:#?}", err); } })?; } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 174b1bba67f537..3cb8b84398afcb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -325,7 +325,7 @@ impl DebugPanel { return; }; - let client_id = client.id().clone(); + let client_id = client.id(); cx.spawn({ let event = event.clone(); |this, mut cx| async move { From 0975ca844cf2498e15bd4b70ee36ab5b927beb67 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 25 Jul 2024 01:13:05 -0400 Subject: [PATCH 135/650] Get sequence id to be incremented after getting respond and event --- crates/dap/src/client.rs | 14 +++++++++----- crates/debugger_ui/src/debugger_panel.rs | 3 +++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 95751a46d351a1..57d664b3f259cf 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -62,7 +62,7 @@ pub struct DebugAdapterClient { id: DebugAdapterClientId, _process: Option, server_tx: Sender, - request_count: AtomicU64, + sequence_count: AtomicU64, capabilities: Arc>>, config: DebugAdapterConfig, thread_states: Arc>>, // thread_id -> thread_state @@ -286,7 +286,7 @@ impl DebugAdapterClient { config, server_tx, _process: process, - request_count: AtomicU64::new(1), + sequence_count: AtomicU64::new(1), capabilities: Default::default(), thread_states: Arc::new(Mutex::new(HashMap::new())), }; @@ -357,7 +357,7 @@ impl DebugAdapterClient { let request = Request { back_ch: Some(callback_tx), - seq: self.next_request_id(), + seq: self.next_sequence_id(), command: R::COMMAND.to_string(), arguments: Some(serialized_arguments), }; @@ -365,6 +365,7 @@ impl DebugAdapterClient { self.server_tx.send(Payload::Request(request)).await?; let response = callback_rx.recv().await??; + let _ = self.next_sequence_id(); match response.success { true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), @@ -388,8 +389,11 @@ impl DebugAdapterClient { self.capabilities.lock().clone().unwrap_or_default() } - pub fn next_request_id(&self) -> u64 { - self.request_count.fetch_add(1, Ordering::Relaxed) + /// Get the next sequence id to be used in a request + /// # Side Effect + /// This function also increment's client's sequence count by one + pub fn next_sequence_id(&self) -> u64 { + self.sequence_count.fetch_add(1, Ordering::Relaxed) } pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3cb8b84398afcb..1e6218c8e126ec 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -133,6 +133,9 @@ impl DebugPanel { event: &Events, cx: &mut ViewContext, ) { + // Increment the sequence id because an event is being processed + let _ = client.next_sequence_id(); + match event { Events::Initialized(event) => Self::handle_initialized_event(client, event, cx), Events::Stopped(event) => Self::handle_stopped_event(client, event, cx), From 916150a8e0e6787211ac1348b276ba0b6b515b05 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 25 Jul 2024 11:00:28 -0400 Subject: [PATCH 136/650] Get breakpoints to use anchors instead Warning: Project is not being sent breakpoints right now --- crates/editor/src/editor.rs | 44 +++++++++++++++++++++++++----------- crates/editor/src/element.rs | 16 ++++++++----- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9ca552937bcb88..e51865ab93f781 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -452,8 +452,7 @@ struct ResolvedTasks { #[derive(Clone, Debug)] struct Breakpoint { - row: MultiBufferRow, - _line: BufferRow, + position: Anchor, } #[derive(Copy, Clone, Debug)] @@ -575,7 +574,7 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - breakpoints: BTreeMap<(BufferId, BufferRow), Breakpoint>, + breakpoints: BTreeMap<(BufferId, ExcerptId), Breakpoint>, previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, @@ -5141,14 +5140,19 @@ impl Editor { } } - fn render_breakpoint(&self, row: DisplayRow, cx: &mut ViewContext) -> IconButton { + fn render_breakpoint( + &self, + position: Anchor, + row: DisplayRow, + cx: &mut ViewContext, + ) -> IconButton { IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play) .icon_size(IconSize::XSmall) .size(ui::ButtonSize::None) .icon_color(Color::Error) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); - editor.toggle_breakpoint_at_row(row.0, cx) //TODO handle folded + editor.toggle_breakpoint_at_row(position, cx) //TODO handle folded })) } @@ -5972,10 +5976,21 @@ impl Editor { pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { let cursor_position: Point = self.selections.newest(cx).head(); - self.toggle_breakpoint_at_row(cursor_position.row, cx); + + let breakpoint_position = self + .snapshot(cx) + .display_snapshot + .buffer_snapshot + .anchor_before(cursor_position); + + self.toggle_breakpoint_at_row(breakpoint_position, cx); } - pub fn toggle_breakpoint_at_row(&mut self, row: u32, cx: &mut ViewContext) { + pub fn toggle_breakpoint_at_row( + &mut self, + breakpoint_position: Anchor, + cx: &mut ViewContext, + ) { let Some(project) = &self.project else { return; }; @@ -5984,21 +5999,24 @@ impl Editor { }; let buffer_id = buffer.read(cx).remote_id(); - let key = (buffer_id, row); + let key = (buffer_id, breakpoint_position.excerpt_id); if self.breakpoints.remove(&key).is_none() { self.breakpoints.insert( key, Breakpoint { - row: MultiBufferRow(row), - _line: row, + position: breakpoint_position, }, ); } + // let row = breakpoint_position + // .to_point(&(self.snapshot(cx).display_snapshot.buffer_snapshot)) + // .row + // + 1; - project.update(cx, |project, cx| { - project.update_breakpoint(buffer, row + 1, cx); - }); + // project.update(cx, |project, cx| { + // project.update_breakpoint(buffer, row, cx); + // }); cx.notify(); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 73ae70abdb13b3..498aee55c21b97 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1570,17 +1570,21 @@ impl EditorElement { .breakpoints .iter() .filter_map(|(_, breakpoint)| { - if snapshot.is_line_folded(breakpoint.row) { + let point = breakpoint + .position + .to_display_point(&snapshot.display_snapshot); + + let row = MultiBufferRow { 0: point.row().0 }; + + if snapshot.is_line_folded(row) { return None; } - let display_row = Point::new(breakpoint.row.0, 0) - .to_display_point(snapshot) - .row(); - let button = editor.render_breakpoint(display_row, cx); + + let button = editor.render_breakpoint(breakpoint.position, point.row(), cx); let button = prepaint_gutter_button( button, - display_row, + point.row(), line_height, gutter_dimensions, scroll_pixel_position, From f0a5775204acc79308d4b49c6b4031771dc1d5b5 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 25 Jul 2024 13:33:05 -0400 Subject: [PATCH 137/650] Fix toggle breakpoints having a max of one bp per buffer Breakpoints are now stored in a BTreeMap> I did this becauase we need to constant check if a breakpoint exists in a buffer whenever we toggle one. --- crates/editor/src/editor.rs | 20 ++++++++++---------- crates/editor/src/element.rs | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e51865ab93f781..2f75d770d51309 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -450,7 +450,7 @@ struct ResolvedTasks { position: Anchor, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] struct Breakpoint { position: Anchor, } @@ -574,7 +574,7 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - breakpoints: BTreeMap<(BufferId, ExcerptId), Breakpoint>, + breakpoints: BTreeMap>, previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, @@ -5999,15 +5999,15 @@ impl Editor { }; let buffer_id = buffer.read(cx).remote_id(); - let key = (buffer_id, breakpoint_position.excerpt_id); + // let key = (buffer_id, breakpoint_position); + let breakpoint = Breakpoint { + position: breakpoint_position, + }; - if self.breakpoints.remove(&key).is_none() { - self.breakpoints.insert( - key, - Breakpoint { - position: breakpoint_position, - }, - ); + let breakpoint_set = self.breakpoints.entry(buffer_id).or_default(); + + if !breakpoint_set.remove(&breakpoint) { + breakpoint_set.insert(breakpoint); } // let row = breakpoint_position // .to_point(&(self.snapshot(cx).display_snapshot.buffer_snapshot)) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 498aee55c21b97..7f3b23ce9fb0a9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1569,7 +1569,8 @@ impl EditorElement { editor .breakpoints .iter() - .filter_map(|(_, breakpoint)| { + .flat_map(|(_buffer_id, breakpoint_set)| breakpoint_set.iter()) + .filter_map(|breakpoint| { let point = breakpoint .position .to_display_point(&snapshot.display_snapshot); From 9a8a54109eeccfa7a5b5f35a9944b68c2ea63045 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:24:19 -0400 Subject: [PATCH 138/650] Fix race condition in debugger (#6) * Add support for DAP to use std for communication * Add more descriptive error logs for DAP * Implement handler for continued event * Add PR feedback to handle_continued_event function * Updated debugger * Fix race condition when using late case debug adapters The main thing this commit does is fix race conditions between a dap client's event handler and sending a launch/attach request. Some adapters would only respond to a starting request after the client handled an init event, which could never happen because the client used to start it's handler after it sent a launch request. This commit also ignores undefined errors instead of crashing. This allows the client to work with adapters that send custom events. Finially, I added some more descriptive error messages and change client's starting request seq from 0 to 1 to be more in line with the specs. * Get clippy to run successfully * Add some function docs to dap client * Fix debugger's highlight when stepping through code Merging with the main zed branch removed a function that the debugger panel relied on. I added the function back and changed it to work with the updated internals of zed. * Get clippy to pass & add an error log instead of using dbg!() * Get sequence id to be incremented after getting respond and event --------- Co-authored-by: Remco Smits --- crates/dap/src/client.rs | 70 ++++++++++++++---- crates/dap/src/transport.rs | 33 +++++++-- crates/debugger_ui/src/debugger_panel.rs | 23 +++--- crates/debugger_ui/src/debugger_panel_item.rs | 12 ++-- crates/project/src/project.rs | 71 ++++++++++++++----- 5 files changed, 153 insertions(+), 56 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 56f2a3cc5f2eec..57d664b3f259cf 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -62,13 +62,22 @@ pub struct DebugAdapterClient { id: DebugAdapterClientId, _process: Option, server_tx: Sender, - request_count: AtomicU64, + sequence_count: AtomicU64, capabilities: Arc>>, config: DebugAdapterConfig, thread_states: Arc>>, // thread_id -> thread_state } impl DebugAdapterClient { + /// Creates & returns a new debug adapter client + /// + /// # Parameters + /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients + /// - `config`: The adapter specific configurations from debugger task that is starting + /// - `command`: The command that starts the debugger + /// - `args`: Arguments of the command that starts the debugger + /// - `project_path`: The absolute path of the project that is being debugged + /// - `cx`: The context that the new client belongs too pub async fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -77,7 +86,7 @@ impl DebugAdapterClient { project_path: PathBuf, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result> + ) -> Result where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { @@ -110,6 +119,17 @@ impl DebugAdapterClient { } } + /// Creates a debug client that connects to an adapter through tcp + /// + /// TCP clients don't have an error communication stream with an adapter + /// + /// # Parameters + /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients + /// - `config`: The adapter specific configurations from debugger task that is starting + /// - `command`: The command that starts the debugger + /// - `args`: Arguments of the command that starts the debugger + /// - `project_path`: The absolute path of the project that is being debugged + /// - `cx`: The context that the new client belongs too #[allow(clippy::too_many_arguments)] async fn create_tcp_client( id: DebugAdapterClientId, @@ -120,7 +140,7 @@ impl DebugAdapterClient { project_path: PathBuf, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result> + ) -> Result where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { @@ -169,6 +189,7 @@ impl DebugAdapterClient { ) } + /// Get an open port to use with the tcp client when not supplied by debug config async fn get_port() -> Option { Some( TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0)) @@ -180,6 +201,15 @@ impl DebugAdapterClient { ) } + /// Creates a debug client that connects to an adapter through std input/output + /// + /// # Parameters + /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients + /// - `config`: The adapter specific configurations from debugger task that is starting + /// - `command`: The command that starts the debugger + /// - `args`: Arguments of the command that starts the debugger + /// - `project_path`: The absolute path of the project that is being debugged + /// - `cx`: The context that the new client belongs too async fn create_stdio_client( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -188,7 +218,7 @@ impl DebugAdapterClient { project_path: PathBuf, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result> + ) -> Result where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { @@ -244,22 +274,22 @@ impl DebugAdapterClient { process: Option, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result> + ) -> Result where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); - let client = Arc::new(Self { + let client = Self { id, config, server_tx, _process: process, - request_count: AtomicU64::new(1), + sequence_count: AtomicU64::new(1), capabilities: Default::default(), thread_states: Arc::new(Mutex::new(HashMap::new())), - }); + }; cx.update(|cx| { cx.background_executor() @@ -275,6 +305,14 @@ impl DebugAdapterClient { Ok(client) } + /// Set's up a client's event handler. + /// + /// This function should only be called once or else errors will arise + /// # Parameters + /// `client`: A pointer to the client to pass the event handler too + /// `event_handler`: The function that is called to handle events + /// should be DebugPanel::handle_debug_client_events + /// `cx`: The context that this task will run in pub async fn handle_events( client_rx: Receiver, mut event_handler: F, @@ -286,8 +324,8 @@ impl DebugAdapterClient { while let Ok(payload) = client_rx.recv().await { cx.update(|cx| match payload { Payload::Event(event) => event_handler(*event, cx), - e => { - dbg!(&e); + err => { + log::error!("Invalid Event: {:#?}", err); } })?; } @@ -307,6 +345,8 @@ impl DebugAdapterClient { anyhow::Ok(()) } + /// Send a request to an adapter and get a response back + /// Note: This function will block until a response is sent back from the adapter pub async fn request( &self, arguments: R::Arguments, @@ -317,7 +357,7 @@ impl DebugAdapterClient { let request = Request { back_ch: Some(callback_tx), - seq: self.next_request_id(), + seq: self.next_sequence_id(), command: R::COMMAND.to_string(), arguments: Some(serialized_arguments), }; @@ -325,6 +365,7 @@ impl DebugAdapterClient { self.server_tx.send(Payload::Request(request)).await?; let response = callback_rx.recv().await??; + let _ = self.next_sequence_id(); match response.success { true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), @@ -348,8 +389,11 @@ impl DebugAdapterClient { self.capabilities.lock().clone().unwrap_or_default() } - pub fn next_request_id(&self) -> u64 { - self.request_count.fetch_add(1, Ordering::Relaxed) + /// Get the next sequence id to be used in a request + /// # Side Effect + /// This function also increment's client's sequence count by one + pub fn next_sequence_id(&self) -> u64 { + self.sequence_count.fetch_add(1, Ordering::Relaxed) } pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) { diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 3b46ab14405880..949a38ddeab505 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -45,6 +45,8 @@ pub enum Events { ProgressEnd(ProgressEndEvent), Invalidated(InvalidatedEvent), Memory(MemoryEvent), + #[serde(untagged)] + Other(HashMap), } #[derive(Debug, Deserialize, Serialize)] @@ -125,7 +127,13 @@ impl Transport { let mut content_length = None; loop { buffer.truncate(0); - if reader.read_line(buffer).await? == 0 { + + if reader + .read_line(buffer) + .await + .with_context(|| "reading a message from server")? + == 0 + { return Err(anyhow!("reader stream closed")); }; @@ -146,10 +154,12 @@ impl Transport { let content_length = content_length.context("missing content length")?; let mut content = vec![0; content_length]; - reader.read_exact(&mut content).await?; - let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; + reader + .read_exact(&mut content) + .await + .with_context(|| "reading after a loop")?; - dbg!(msg); + let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; Ok(serde_json::from_str::(msg)?) } @@ -190,7 +200,6 @@ impl Transport { .await?; server_stdin.flush().await?; - Ok(()) } @@ -210,11 +219,20 @@ impl Transport { match payload { Payload::Response(res) => { if let Some(tx) = self.pending_requests.lock().await.remove(&res.request_seq) { - tx.send(Self::process_response(res)).await?; + + if !tx.is_closed() { + tx.send(Self::process_response(res)).await?; + } else { + log::warn!( + "Response stream associated with request seq: {} is closed", + &res.request_seq + ); // TODO: Fix this case so it never happens + } } else { client_tx.send(Payload::Response(res)).await?; }; } + Payload::Request(_) => { client_tx.send(payload).await?; } @@ -237,7 +255,8 @@ impl Transport { &client_tx, Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?, ) - .await?; + .await + .context("Process server message failed in transport::receive")?; } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 44813bd8e355da..1e6218c8e126ec 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -79,7 +79,6 @@ impl DebugPanel { let client = this.debug_client_by_id(*client_id, cx); cx.spawn(|_, _| async move { client.initialize().await?; - let request_args = client.config().request_args.map(|a| a.args); // send correct request based on adapter config @@ -134,6 +133,9 @@ impl DebugPanel { event: &Events, cx: &mut ViewContext, ) { + // Increment the sequence id because an event is being processed + let _ = client.next_sequence_id(); + match event { Events::Initialized(event) => Self::handle_initialized_event(client, event, cx), Events::Stopped(event) => Self::handle_stopped_event(client, event, cx), @@ -152,6 +154,7 @@ impl DebugPanel { Events::ProgressStart(_) => {} Events::ProgressUpdate(_) => {} Events::Invalidated(_) => {} + Events::Other(_) => {} } } @@ -172,7 +175,7 @@ impl DebugPanel { let task = workspace.update(&mut cx, |workspace, cx| { let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_full_path(&Path::new(&path), cx) + project.project_path_for_absolute_path(&Path::new(&path), cx) }); if let Some(project_path) = project_path { @@ -255,7 +258,7 @@ impl DebugPanel { let task = workspace.update(&mut cx, |workspace, cx| { let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_full_path(&Path::new(&path), cx) + project.project_path_for_absolute_path(&Path::new(&path), cx) }); if let Some(project_path) = project_path { @@ -325,7 +328,7 @@ impl DebugPanel { return; }; - let client_id = client.id().clone(); + let client_id = client.id(); cx.spawn({ let event = event.clone(); |this, mut cx| async move { @@ -342,11 +345,11 @@ impl DebugPanel { stack_trace_response.stack_frames.first().unwrap().clone(); let mut scope_tasks = Vec::new(); for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { - let frame_id = stack_frame.id.clone(); + let frame_id = stack_frame.id; let client = client.clone(); scope_tasks.push(async move { anyhow::Ok(( - frame_id.clone(), + frame_id, client .request::(ScopesArguments { frame_id }) .await?, @@ -362,11 +365,11 @@ impl DebugPanel { scopes.insert(thread_id, response.scopes.clone()); for scope in response.scopes { - let scope_reference = scope.variables_reference.clone(); + let scope_reference = scope.variables_reference; let client = client.clone(); variable_tasks.push(async move { anyhow::Ok(( - scope_reference.clone(), + scope_reference, client .request::(VariablesArguments { variables_reference: scope_reference, @@ -392,7 +395,7 @@ impl DebugPanel { .or_insert(ThreadState::default()); thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); - thread_state.stack_frames = stack_trace_response.stack_frames.clone(); + thread_state.stack_frames = stack_trace_response.stack_frames; thread_state.scopes = scopes; thread_state.variables = variables; thread_state.status = ThreadStatus::Stopped; @@ -460,7 +463,7 @@ impl DebugPanel { .thread_states() .insert(thread_id, ThreadState::default()); } else { - client.update_thread_state_status(thread_id.clone(), ThreadStatus::Ended); + client.update_thread_state_status(thread_id, ThreadStatus::Ended); // TODO: we want to figure out for witch clients/threads we should remove the highlights cx.spawn({ diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 0a6c11c03912e6..b6dd8339769bdb 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -436,9 +436,8 @@ impl Render for DebugPanelItem { .child( h_flex() .gap_2() - .when_else( - self.current_thread_state().status == ThreadStatus::Running, - |this| { + .map(|this| { + if self.current_thread_state().status == ThreadStatus::Running { this.child( IconButton::new("debug-pause", IconName::DebugPause) .on_click( @@ -446,8 +445,7 @@ impl Render for DebugPanelItem { ) .tooltip(move |cx| Tooltip::text("Pause program", cx)), ) - }, - |this| { + } else { this.child( IconButton::new("debug-continue", IconName::DebugContinue) .on_click(cx.listener(|_, _, cx| { @@ -456,8 +454,8 @@ impl Render for DebugPanelItem { .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Continue program", cx)), ) - }, - ) + } + }) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) .on_click( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 92cccd7c10eedd..dfca5defbb7a9f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1219,26 +1219,28 @@ impl Project { let task = cx.spawn(|this, mut cx| async move { let project = this.clone(); - let client = DebugAdapterClient::new( - id, - adapter_config.clone(), - &command, - args.iter().map(|ele| &ele[..]).collect(), - cwd.into(), - move |event, cx| { - project - .update(cx, |_, cx| { - cx.emit(Event::DebugClientEvent { - client_id: id, - event, + let client = Arc::new( + DebugAdapterClient::new( + id, + adapter_config.clone(), + &command, + args.iter().map(|ele| &ele[..]).collect(), + cwd.into(), + move |event, cx| { + project + .update(cx, |_, cx| { + cx.emit(Event::DebugClientEvent { + client_id: id, + event, + }) }) - }) - .log_err(); - }, - &mut cx, - ) - .await - .log_err()?; + .log_err(); + }, + &mut cx, + ) + .await + .log_err()?, + ); this.update(&mut cx, |this, cx| { let handle = this @@ -8572,6 +8574,37 @@ impl Project { }) } + pub fn project_path_for_absolute_path( + &self, + abs_path: &Path, + cx: &AppContext, + ) -> Option { + self.find_local_worktree(abs_path, cx) + .map(|(worktree, relative_path)| ProjectPath { + worktree_id: worktree.read(cx).id(), + path: relative_path.into(), + }) + } + + pub fn find_local_worktree( + &self, + abs_path: &Path, + cx: &AppContext, + ) -> Option<(Model, PathBuf)> { + let trees = self.worktrees(cx); + + for tree in trees { + if let Some(relative_path) = tree + .read(cx) + .as_local() + .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok()) + { + return Some((tree.clone(), relative_path.into())); + } + } + None + } + pub fn get_workspace_root( &self, project_path: &ProjectPath, From 403ae10087f86f289bc498134972b07649b8a52b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 25 Jul 2024 21:22:51 +0200 Subject: [PATCH 139/650] Fix infinite loop of threads, because sequence id was wrong This was added to make sure we had the same sequence id as python debugger. But resulting in breaking xdebug(php) debug adapter --- crates/dap/src/client.rs | 1 - crates/debugger_ui/src/debugger_panel.rs | 3 --- 2 files changed, 4 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 57d664b3f259cf..ff1baa003cc4ef 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -365,7 +365,6 @@ impl DebugAdapterClient { self.server_tx.send(Payload::Request(request)).await?; let response = callback_rx.recv().await??; - let _ = self.next_sequence_id(); match response.success { true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1e6218c8e126ec..3cb8b84398afcb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -133,9 +133,6 @@ impl DebugPanel { event: &Events, cx: &mut ViewContext, ) { - // Increment the sequence id because an event is being processed - let _ = client.next_sequence_id(); - match event { Events::Initialized(event) => Self::handle_initialized_event(client, event, cx), Events::Stopped(event) => Self::handle_stopped_event(client, event, cx), From b70acdfa4ae2628d5e871b7d741dc695b3203311 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 25 Jul 2024 21:30:49 +0200 Subject: [PATCH 140/650] Wip run in terminal request --- crates/dap/src/client.rs | 78 ++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index ff1baa003cc4ef..d0fa16c0ee8e86 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,16 +1,17 @@ -use crate::transport::{Events, Payload, Request, Response, Transport}; +use crate::transport::{Events, Payload, Response, Transport}; use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ - Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Restart, - SetBreakpoints, StepBack, StepIn, StepOut, + Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, + Restart, RunInTerminal, SetBreakpoints, StepBack, StepIn, StepOut, }, AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, - NextArguments, PauseArguments, RestartArguments, Scope, SetBreakpointsArguments, - SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, - StepInArguments, StepOutArguments, SteppingGranularity, Variable, + NextArguments, PauseArguments, RestartArguments, RunInTerminalRequestArguments, + RunInTerminalResponse, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, + SourceBreakpoint, StackFrame, StepBackArguments, StepInArguments, StepOutArguments, + SteppingGranularity, Variable, }; use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; @@ -284,7 +285,7 @@ impl DebugAdapterClient { let client = Self { id, config, - server_tx, + server_tx: server_tx.clone(), _process: process, sequence_count: AtomicU64::new(1), capabilities: Default::default(), @@ -297,7 +298,7 @@ impl DebugAdapterClient { .detach_and_log_err(cx); cx.spawn(|mut cx| async move { - Self::handle_events(client_rx, event_handler, &mut cx).await + Self::handle_events(client_rx, server_tx, event_handler, &mut cx).await }) .detach_and_log_err(cx); })?; @@ -315,6 +316,7 @@ impl DebugAdapterClient { /// `cx`: The context that this task will run in pub async fn handle_events( client_rx: Receiver, + server_tx: Sender, mut event_handler: F, cx: &mut AsyncAppContext, ) -> Result<()> @@ -324,9 +326,58 @@ impl DebugAdapterClient { while let Ok(payload) = client_rx.recv().await { cx.update(|cx| match payload { Payload::Event(event) => event_handler(*event, cx), - err => { - log::error!("Invalid Event: {:#?}", err); + Payload::Request(request) => { + if RunInTerminal::COMMAND == request.command { + let server_tx = server_tx.clone(); + cx.spawn(|_| async move { + let arguments: RunInTerminalRequestArguments = + serde_json::from_value(request.arguments.unwrap_or_default())?; + + dbg!(&arguments); + + let envs: HashMap = arguments + .env + .unwrap() + .as_object() + .unwrap() + .iter() + .map(|(key, value)| (key.clone(), value.clone().to_string())) + .collect(); + + let mut args = arguments.args.clone(); + + let mut command = process::Command::new(args.remove(0)); + command.current_dir(arguments.cwd).envs(envs).args(args); + + let process = command + .spawn() + .with_context(|| "failed to spawn run in terminal command.")?; + + let json = serde_json::to_value(RunInTerminalResponse { + process_id: Some(process.id() as u64), + shell_process_id: None, + })?; + + dbg!(&json); + + server_tx + .send(Payload::Response(Response { + request_seq: request.seq, + success: true, + command: RunInTerminal::COMMAND.into(), + message: None, + body: Some(json), + })) + .await?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } else { + log::error!("Unhandled Event: {:#?}", request.command); + } } + _ => unreachable!(), })?; } @@ -347,15 +398,12 @@ impl DebugAdapterClient { /// Send a request to an adapter and get a response back /// Note: This function will block until a response is sent back from the adapter - pub async fn request( - &self, - arguments: R::Arguments, - ) -> Result { + pub async fn request(&self, arguments: R::Arguments) -> Result { let serialized_arguments = serde_json::to_value(arguments)?; let (callback_tx, callback_rx) = bounded::>(1); - let request = Request { + let request = crate::transport::Request { back_ch: Some(callback_tx), seq: self.next_sequence_id(), command: R::COMMAND.to_string(), From 6ff5e0074060250cf2964884159d66bc80189fb4 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 25 Jul 2024 22:21:27 +0200 Subject: [PATCH 141/650] Fix don't go to stack frame if only the thread id matches of the stopped event This makes sure if you have multiple debug adapters running we don't match a thread id that belongs to a different client --- crates/debugger_ui/src/debugger_panel.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3cb8b84398afcb..94d42ae809b083 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -423,7 +423,8 @@ impl DebugPanel { if let Some(item) = this.pane.read(cx).active_item() { if let Some(pane) = item.downcast::() { - if pane.read(cx).thread_id() == thread_id { + let pane = pane.read(cx); + if pane.thread_id() == thread_id && pane.client().id() == client_id { let workspace = this.workspace.clone(); let client = client.clone(); return cx.spawn(|_, cx| async move { From d28950c6338b14c3be23e03cb6ba7ca001fce87f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 26 Jul 2024 23:29:08 +0200 Subject: [PATCH 142/650] Line up toggle debug panel focus with other panels action names --- crates/debugger_ui/src/debugger_panel.rs | 4 ++-- crates/debugger_ui/src/lib.rs | 27 +++++++++--------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 94d42ae809b083..edc653b8d291c1 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -33,7 +33,7 @@ pub enum DebugPanelEvent { Thread((DebugAdapterClientId, ThreadEvent)), } -actions!(debug_panel, [TogglePanel]); +actions!(debug_panel, [ToggleFocus]); pub struct DebugPanel { size: Pixels, @@ -570,7 +570,7 @@ impl Panel for DebugPanel { } fn toggle_action(&self) -> Box { - Box::new(TogglePanel) + Box::new(ToggleFocus) } fn icon_label(&self, _: &WindowContext) -> Option { diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index aead9f36120229..3f0dd73e891eaa 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,25 +1,18 @@ -use debugger_panel::{DebugPanel, TogglePanel}; -use gpui::{AppContext, ViewContext}; +use debugger_panel::{DebugPanel, ToggleFocus}; +use gpui::AppContext; use workspace::{StartDebugger, Workspace}; pub mod debugger_panel; mod debugger_panel_item; pub fn init(cx: &mut AppContext) { - cx.observe_new_views( - |workspace: &mut Workspace, _: &mut ViewContext| { - workspace - .register_action(|workspace, _action: &TogglePanel, cx| { - workspace.focus_panel::(cx); - }) - .register_action( - |workspace: &mut Workspace, - _: &StartDebugger, - cx: &mut ViewContext<'_, Workspace>| { - tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach(); - }, - ); - }, - ) + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + workspace.register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| { + tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach(); + }); + }) .detach(); } From 1ff23477de159081a196e6a6d0d397266c5277c6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 27 Jul 2024 10:43:35 +0200 Subject: [PATCH 143/650] Clean up run in terminal code --- crates/dap/src/client.rs | 96 ++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d0fa16c0ee8e86..ea638e164f5327 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -324,63 +324,61 @@ impl DebugAdapterClient { F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { while let Ok(payload) = client_rx.recv().await { - cx.update(|cx| match payload { - Payload::Event(event) => event_handler(*event, cx), + match payload { + Payload::Event(event) => cx.update(|cx| event_handler(*event, cx))?, Payload::Request(request) => { if RunInTerminal::COMMAND == request.command { - let server_tx = server_tx.clone(); - cx.spawn(|_| async move { - let arguments: RunInTerminalRequestArguments = - serde_json::from_value(request.arguments.unwrap_or_default())?; - - dbg!(&arguments); - - let envs: HashMap = arguments - .env - .unwrap() - .as_object() - .unwrap() - .iter() - .map(|(key, value)| (key.clone(), value.clone().to_string())) - .collect(); - - let mut args = arguments.args.clone(); - - let mut command = process::Command::new(args.remove(0)); - command.current_dir(arguments.cwd).envs(envs).args(args); - - let process = command - .spawn() - .with_context(|| "failed to spawn run in terminal command.")?; - - let json = serde_json::to_value(RunInTerminalResponse { - process_id: Some(process.id() as u64), - shell_process_id: None, - })?; - - dbg!(&json); - - server_tx - .send(Payload::Response(Response { - request_seq: request.seq, - success: true, - command: RunInTerminal::COMMAND.into(), - message: None, - body: Some(json), - })) - .await?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + Self::handle_run_in_terminal_request(request, &server_tx).await?; } else { - log::error!("Unhandled Event: {:#?}", request.command); + unreachable!("Unknown reverse request {}", request.command); } } _ => unreachable!(), - })?; + } + } + + anyhow::Ok(()) + } + + async fn handle_run_in_terminal_request( + request: crate::transport::Request, + server_tx: &Sender, + ) -> Result<()> { + let arguments: RunInTerminalRequestArguments = + serde_json::from_value(request.arguments.unwrap_or_default())?; + + let mut args = arguments.args.clone(); + let mut command = process::Command::new(args.remove(0)); + + let envs = arguments.env.as_ref().and_then(|e| e.as_object()).map(|e| { + e.iter() + .map(|(key, value)| (key.clone(), value.clone().to_string())) + .collect::>() + }); + + if let Some(envs) = envs { + command.envs(envs); } + let process = command + .current_dir(arguments.cwd) + .args(args) + .spawn() + .with_context(|| "failed to spawn run in terminal command.")?; + + server_tx + .send(Payload::Response(Response { + request_seq: request.seq, + success: true, + command: RunInTerminal::COMMAND.into(), + message: None, + body: Some(serde_json::to_value(RunInTerminalResponse { + process_id: Some(process.id() as u64), + shell_process_id: None, + })?), + })) + .await?; + anyhow::Ok(()) } From 7f8c28877f0d3f7d0df8a8a509979edd3e2c3d5f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 27 Jul 2024 10:46:55 +0200 Subject: [PATCH 144/650] Wip start debugging request --- crates/dap/src/client.rs | 72 ++++++++++++++++++++++++++++------- crates/project/src/project.rs | 40 +++++++++---------- 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index ea638e164f5327..f0a910e687f1d5 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -4,14 +4,14 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, - Restart, RunInTerminal, SetBreakpoints, StepBack, StepIn, StepOut, + Restart, RunInTerminal, SetBreakpoints, StartDebugging, StepBack, StepIn, StepOut, }, AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, RestartArguments, RunInTerminalRequestArguments, RunInTerminalResponse, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, - SourceBreakpoint, StackFrame, StepBackArguments, StepInArguments, StepOutArguments, - SteppingGranularity, Variable, + SourceBreakpoint, StackFrame, StartDebuggingRequestArguments, StepBackArguments, + StepInArguments, StepOutArguments, SteppingGranularity, Variable, }; use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; @@ -67,6 +67,7 @@ pub struct DebugAdapterClient { capabilities: Arc>>, config: DebugAdapterConfig, thread_states: Arc>>, // thread_id -> thread_state + sub_client: Arc>>>, } impl DebugAdapterClient { @@ -87,7 +88,7 @@ impl DebugAdapterClient { project_path: PathBuf, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result + ) -> Result> where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { @@ -141,7 +142,7 @@ impl DebugAdapterClient { project_path: PathBuf, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result + ) -> Result> where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { @@ -219,7 +220,7 @@ impl DebugAdapterClient { project_path: PathBuf, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result + ) -> Result> where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { @@ -275,30 +276,34 @@ impl DebugAdapterClient { process: Option, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result + ) -> Result> where F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); - let client = Self { + let client = Arc::new(Self { id, config, - server_tx: server_tx.clone(), _process: process, - sequence_count: AtomicU64::new(1), + sub_client: Default::default(), + server_tx: server_tx.clone(), capabilities: Default::default(), - thread_states: Arc::new(Mutex::new(HashMap::new())), - }; + thread_states: Default::default(), + sequence_count: AtomicU64::new(1), + }); cx.update(|cx| { cx.background_executor() .spawn(Self::handle_recv(server_rx, client_tx)) .detach_and_log_err(cx); - cx.spawn(|mut cx| async move { - Self::handle_events(client_rx, server_tx, event_handler, &mut cx).await + cx.spawn({ + let client = client.clone(); + |mut cx| async move { + Self::handle_events(client, client_rx, server_tx, event_handler, &mut cx).await + } }) .detach_and_log_err(cx); })?; @@ -315,6 +320,7 @@ impl DebugAdapterClient { /// should be DebugPanel::handle_debug_client_events /// `cx`: The context that this task will run in pub async fn handle_events( + this: Arc, client_rx: Receiver, server_tx: Sender, mut event_handler: F, @@ -329,6 +335,8 @@ impl DebugAdapterClient { Payload::Request(request) => { if RunInTerminal::COMMAND == request.command { Self::handle_run_in_terminal_request(request, &server_tx).await?; + } else if StartDebugging::COMMAND == request.command { + Self::handle_start_debugging_request(&this, request, cx).await?; } else { unreachable!("Unknown reverse request {}", request.command); } @@ -382,6 +390,42 @@ impl DebugAdapterClient { anyhow::Ok(()) } + async fn handle_start_debugging_request( + this: &Arc, + request: crate::transport::Request, + cx: &mut AsyncAppContext, + ) -> Result<()> { + dbg!(&request); + let arguments: StartDebuggingRequestArguments = + serde_json::from_value(request.arguments.clone().unwrap_or_default())?; + + let sub_client = DebugAdapterClient::new( + DebugAdapterClientId(1), + this.config.clone(), + "node", + vec![ + "/Users/remcosmits/Downloads/js-debug/src/dapDebugServer.js", + "8134", + "127.0.0.1", + ], + PathBuf::from("/Users/remcosmits/Documents/code/prettier-test"), + |event, cx| { + dbg!(event); + }, + cx, + ) + .await?; + + dbg!(&arguments); + + let res = sub_client.launch(request.arguments).await?; + dbg!(res); + + *this.sub_client.lock() = Some(sub_client); + + anyhow::Ok(()) + } + async fn handle_recv(server_rx: Receiver, client_tx: Sender) -> Result<()> { while let Ok(payload) = server_rx.recv().await { match payload { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index dfca5defbb7a9f..b0c5c84e37566b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1219,28 +1219,26 @@ impl Project { let task = cx.spawn(|this, mut cx| async move { let project = this.clone(); - let client = Arc::new( - DebugAdapterClient::new( - id, - adapter_config.clone(), - &command, - args.iter().map(|ele| &ele[..]).collect(), - cwd.into(), - move |event, cx| { - project - .update(cx, |_, cx| { - cx.emit(Event::DebugClientEvent { - client_id: id, - event, - }) + let client = DebugAdapterClient::new( + id, + adapter_config.clone(), + &command, + args.iter().map(|ele| &ele[..]).collect(), + cwd.into(), + move |event, cx| { + project + .update(cx, |_, cx| { + cx.emit(Event::DebugClientEvent { + client_id: id, + event, }) - .log_err(); - }, - &mut cx, - ) - .await - .log_err()?, - ); + }) + .log_err(); + }, + &mut cx, + ) + .await + .log_err()?; this.update(&mut cx, |this, cx| { let handle = this From 4373e479f773fab281e5565e5b88430297e9abee Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 27 Jul 2024 13:46:11 -0400 Subject: [PATCH 145/650] Get Editor & Project to share breakpoints through RWLock Previously editor would send breakpoints to project whenever a new breakpoint was deleted or added. Now editor shares a shared pointer to a breakpoint datastructure. This behavior is more efficient because it doesn't have to repeatly send breakpoints everytime one is updated. Still have to handle cases when a breakpoint is added/removed during a debugging phase. I also have to figure out how to share the breakpoints pointer only once per project and editor. --- Cargo.lock | 2 + crates/dap/Cargo.toml | 1 + crates/dap/src/client.rs | 5 ++ crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 29 ++++++--- crates/editor/src/element.rs | 1 + crates/project/src/project.rs | 117 +++++++++++++++++++--------------- crates/text/src/text.rs | 2 +- 8 files changed, 97 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d22c2dbf47afc..aa960cb70c2f6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3234,6 +3234,7 @@ dependencies = [ "futures 0.3.28", "gpui", "log", + "multi_buffer", "parking_lot", "postage", "release_channel", @@ -3593,6 +3594,7 @@ dependencies = [ "collections", "convert_case 0.6.0", "ctor", + "dap", "db", "emojis", "env_logger", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 462cf97cc972cf..107c118cdde9e2 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -14,6 +14,7 @@ async-std = "1.12.0" dap-types = { git = "https://github.com/zed-industries/dap-types" } futures.workspace = true gpui.workspace = true +multi_buffer.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 57d664b3f259cf..707c857f4c527c 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -555,3 +555,8 @@ impl DebugAdapterClient { .await } } + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct Breakpoint { + pub position: multi_buffer::Anchor, +} diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 8fb4e3be325591..b0761667509cf4 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -37,6 +37,7 @@ clock.workspace = true collections.workspace = true convert_case = "0.6.0" db.workspace = true +dap.workspace = true emojis.workspace = true file_icons.workspace = true futures.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2f75d770d51309..1a2f4689ee59c2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -97,6 +97,7 @@ use language::{point_to_lsp, BufferRow, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; use task::{ResolvedTask, TaskTemplate, TaskVariables}; +use dap::client::Breakpoint; use hover_links::{HoverLink, HoveredLinkState, InlayHighlight}; pub use lsp::CompletionContext; use lsp::{ @@ -450,11 +451,6 @@ struct ResolvedTasks { position: Anchor, } -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -struct Breakpoint { - position: Anchor, -} - #[derive(Copy, Clone, Debug)] struct MultiBufferOffset(usize); #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] @@ -574,7 +570,7 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - breakpoints: BTreeMap>, + breakpoints: Arc>>>, previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, @@ -1788,6 +1784,15 @@ impl Editor { None }; + let breakpoints: Arc>>> = Default::default(); + // TODO: Figure out why the code below doesn't work + // if let Some(project) = project.as_ref() { + // dbg!("Setting breakpoints from editor to project"); + // project.update(cx, |project, _cx| { + // project.breakpoints = breakpoints.clone(); + // }) + // } + let mut this = Self { focus_handle, show_cursor_when_unfocused: false, @@ -1890,7 +1895,7 @@ impl Editor { blame_subscription: None, file_header_size, tasks: Default::default(), - breakpoints: Default::default(), + breakpoints, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), @@ -5994,6 +5999,12 @@ impl Editor { let Some(project) = &self.project else { return; }; + + // TODO: Figure out how to only clone breakpoints pointer once per editor + project.update(cx, |project, _cx| { + project.breakpoints = self.breakpoints.clone(); + }); + let Some(buffer) = self.buffer.read(cx).as_singleton() else { return; }; @@ -6004,7 +6015,9 @@ impl Editor { position: breakpoint_position, }; - let breakpoint_set = self.breakpoints.entry(buffer_id).or_default(); + let mut write_guard = self.breakpoints.write(); + + let breakpoint_set = write_guard.entry(buffer_id).or_default(); if !breakpoint_set.remove(&breakpoint) { breakpoint_set.insert(breakpoint); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7f3b23ce9fb0a9..6a08b2df07c00c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1568,6 +1568,7 @@ impl EditorElement { self.editor.update(cx, |editor, cx| { editor .breakpoints + .read() .iter() .flat_map(|(_buffer_id, breakpoint_set)| breakpoint_set.iter()) .filter_map(|breakpoint| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index dfca5defbb7a9f..a0f1bf2c54aaef 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -25,7 +25,7 @@ use client::{ use clock::ReplicaId; use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use dap::{ - client::{DebugAdapterClient, DebugAdapterClientId}, + client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId}, transport::Events, SourceBreakpoint, }; @@ -119,7 +119,7 @@ use task::{ HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, TaskVariables, VariableName, }; use terminals::Terminals; -use text::{Anchor, BufferId, LineEnding}; +use text::{Anchor, BufferId, LineEnding, Point}; use unicase::UniCase; use util::{ debug_panic, defer, maybe, merge_json_value_into, parse_env_output, post_inc, @@ -182,7 +182,7 @@ pub struct Project { language_servers: HashMap, language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, debug_adapters: HashMap, - breakpoints: HashMap>, + pub breakpoints: Arc>>>, language_server_statuses: BTreeMap, last_formatting_failure: Option, last_workspace_edits_by_language_server: HashMap, @@ -314,10 +314,6 @@ impl PartialEq for LanguageServerPromptRequest { } } -struct Breakpoint { - row: BufferRow, -} - #[derive(Clone, Debug, PartialEq)] pub enum Event { LanguageServerAdded(LanguageServerId), @@ -1157,11 +1153,19 @@ impl Project { let task = project.update(&mut cx, |project, cx| { let mut tasks = Vec::new(); - for (buffer_id, breakpoints) in project.breakpoints.iter() { - let res = maybe!({ + for (buffer_id, breakpoints) in project.breakpoints.read().iter() { + let buffer = maybe!({ let buffer = project.buffer_for_id(*buffer_id, cx)?; + Some(buffer.read(cx)) + }); + + if buffer.is_none() { + continue; + } + let buffer = buffer.as_ref().unwrap(); - let project_path = buffer.read(cx).project_path(cx)?; + let res = maybe!({ + let project_path = buffer.project_path(cx)?; let worktree = project.worktree_for_id(project_path.worktree_id, cx)?; let path = worktree.read(cx).absolutize(&project_path.path).ok()?; @@ -1175,13 +1179,21 @@ impl Project { Some( breakpoints .iter() - .map(|b| SourceBreakpoint { - line: b.row as u64, - condition: None, - hit_condition: None, - log_message: None, - column: None, - mode: None, + .map(|b| { + dbg!(SourceBreakpoint { + line: (buffer + .summary_for_anchor::( + &b.position.text_anchor, + ) + .row + + 1) + as u64, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + }) }) .collect::>(), ), @@ -1204,6 +1216,7 @@ impl Project { debug_task: task::ResolvedTask, cx: &mut ModelContext, ) { + dbg!(&self.breakpoints); let id = DebugAdapterClientId(1); let debug_template = debug_task.original_task(); let cwd = debug_template @@ -1268,41 +1281,41 @@ impl Project { row: BufferRow, cx: &mut ModelContext, ) { - let breakpoints_for_buffer = self - .breakpoints - .entry(buffer.read(cx).remote_id()) - .or_insert(Vec::new()); - - if let Some(ix) = breakpoints_for_buffer - .iter() - .position(|breakpoint| breakpoint.row == row) - { - breakpoints_for_buffer.remove(ix); - } else { - breakpoints_for_buffer.push(Breakpoint { row }); - } - - let clients = self - .debug_adapters - .iter() - .filter_map(|(_, state)| match state { - DebugAdapterClientState::Starting(_) => None, - DebugAdapterClientState::Running(client) => Some(client.clone()), - }) - .collect::>(); - - let mut tasks = Vec::new(); - for client in clients { - tasks.push(self.send_breakpoints(client, cx)); - } - - cx.background_executor() - .spawn(async move { - try_join_all(tasks).await?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx) + // let breakpoints_for_buffer = self + // .breakpoints + // .entry(buffer.read(cx).remote_id()) + // .or_insert(Vec::new()); + + // if let Some(ix) = breakpoints_for_buffer + // .iter() + // .position(|breakpoint| breakpoint.row == row) + // { + // breakpoints_for_buffer.remove(ix); + // } else { + // breakpoints_for_buffer.push(Breakpoint { row }); + // } + + // let clients = self + // .debug_adapters + // .iter() + // .filter_map(|(_, state)| match state { + // DebugAdapterClientState::Starting(_) => None, + // DebugAdapterClientState::Running(client) => Some(client.clone()), + // }) + // .collect::>(); + + // let mut tasks = Vec::new(); + // for client in clients { + // tasks.push(self.send_breakpoints(client, cx)); + // } + + // cx.background_executor() + // .spawn(async move { + // try_join_all(tasks).await?; + + // anyhow::Ok(()) + // }) + // .detach_and_log_err(cx) } fn shutdown_language_servers( diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index a70d1b769238fb..a40c0dedf28ee8 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2155,7 +2155,7 @@ impl BufferSnapshot { }) } - fn summary_for_anchor(&self, anchor: &Anchor) -> D + pub fn summary_for_anchor(&self, anchor: &Anchor) -> D where D: TextDimension, { From c39c0a55f5394956cebdf31f0c12a821c457ed5b Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 27 Jul 2024 15:44:46 -0400 Subject: [PATCH 146/650] Have project share breakpoints pointer with editor Instead of editor sharing with project each time a breakpoint is toggled. An editor will clone project's breakpoints if a project is passed into the editors new function --- crates/editor/src/editor.rs | 32 ++++++++++---------------------- crates/editor/src/element.rs | 7 +++++-- crates/project/src/project.rs | 29 +++++++++++++---------------- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1a2f4689ee59c2..b07060f38e875e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -570,7 +570,7 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - breakpoints: Arc>>>, + breakpoints: Option>>>>, previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, @@ -1784,14 +1784,11 @@ impl Editor { None }; - let breakpoints: Arc>>> = Default::default(); - // TODO: Figure out why the code below doesn't work - // if let Some(project) = project.as_ref() { - // dbg!("Setting breakpoints from editor to project"); - // project.update(cx, |project, _cx| { - // project.breakpoints = breakpoints.clone(); - // }) - // } + let breakpoints = if let Some(project) = project.as_ref() { + Some(project.update(cx, |project, _cx| project.breakpoints.clone())) + } else { + None + }; let mut this = Self { focus_handle, @@ -6000,10 +5997,9 @@ impl Editor { return; }; - // TODO: Figure out how to only clone breakpoints pointer once per editor - project.update(cx, |project, _cx| { - project.breakpoints = self.breakpoints.clone(); - }); + let Some(breakpoints) = &self.breakpoints else { + return; + }; let Some(buffer) = self.buffer.read(cx).as_singleton() else { return; @@ -6015,21 +6011,13 @@ impl Editor { position: breakpoint_position, }; - let mut write_guard = self.breakpoints.write(); + let mut write_guard = breakpoints.write(); let breakpoint_set = write_guard.entry(buffer_id).or_default(); if !breakpoint_set.remove(&breakpoint) { breakpoint_set.insert(breakpoint); } - // let row = breakpoint_position - // .to_point(&(self.snapshot(cx).display_snapshot.buffer_snapshot)) - // .row - // + 1; - - // project.update(cx, |project, cx| { - // project.update_breakpoint(buffer, row, cx); - // }); cx.notify(); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6a08b2df07c00c..4c5ca7c680251d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1566,8 +1566,11 @@ impl EditorElement { cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { - editor - .breakpoints + let Some(breakpoints) = &editor.breakpoints else { + return vec![]; + }; + + breakpoints .read() .iter() .flat_map(|(_buffer_id, breakpoint_set)| breakpoint_set.iter()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a0f1bf2c54aaef..10009b95440258 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1179,21 +1179,19 @@ impl Project { Some( breakpoints .iter() - .map(|b| { - dbg!(SourceBreakpoint { - line: (buffer - .summary_for_anchor::( - &b.position.text_anchor, - ) - .row - + 1) - as u64, - condition: None, - hit_condition: None, - log_message: None, - column: None, - mode: None, - }) + .map(|b| SourceBreakpoint { + line: (buffer + .summary_for_anchor::( + &b.position.text_anchor, + ) + .row + + 1) + as u64, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, }) .collect::>(), ), @@ -1216,7 +1214,6 @@ impl Project { debug_task: task::ResolvedTask, cx: &mut ModelContext, ) { - dbg!(&self.breakpoints); let id = DebugAdapterClientId(1); let debug_template = debug_task.original_task(); let cwd = debug_template From 08dbf365bbf11020976fc2f123a31a4a9db525c1 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 27 Jul 2024 17:53:44 -0400 Subject: [PATCH 147/650] Start work on getting breakpoint indicator to show in gutter The end goal of this commit is to allow a user to set a breakpoint by hovering over a line in the editor's gutter. Currently breakpoints only show on line 6 when the mouse is on a gutter. --- crates/editor/src/editor.rs | 7 ++++++ crates/editor/src/element.rs | 43 ++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b07060f38e875e..04dc10c4a859a3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -570,7 +570,13 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, + /// All the breakpoints that are active within a project + /// Is shared with editor's active project breakpoints: Option>>>>, + /// Allow's a user to create a breakpoint by selecting this indicator + /// It should be None while a user is not hovering over the gutter + /// Otherwise it represents the point that the breakpoint will be shown + pub gutter_breakpoint_indicator: Option, previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, @@ -1893,6 +1899,7 @@ impl Editor { file_header_size, tasks: Default::default(), breakpoints, + gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4c5ca7c680251d..cce39ebf92f614 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -702,6 +702,13 @@ impl EditorElement { let gutter_hovered = gutter_hitbox.is_hovered(cx); editor.set_gutter_hovered(gutter_hovered, cx); + if gutter_hovered { + // TODO: Show breakpoint indicator on line closest to mouse + editor.gutter_breakpoint_indicator = Some(Point::new(5, 0)); + } else { + editor.gutter_breakpoint_indicator = None; + } + // Don't trigger hover popover if mouse is hovering over context menu if text_hitbox.is_hovered(cx) { let point_for_position = @@ -1570,7 +1577,7 @@ impl EditorElement { return vec![]; }; - breakpoints + let mut breakpoints_to_render = breakpoints .read() .iter() .flat_map(|(_buffer_id, breakpoint_set)| breakpoint_set.iter()) @@ -1599,7 +1606,39 @@ impl EditorElement { ); Some(button) }) - .collect_vec() + .collect_vec(); + + // See if a user is hovered over a gutter line & if they are display + // a breakpoint indicator that they can click to add a breakpoint + if let Some(gutter_breakpoint) = &editor.gutter_breakpoint_indicator { + let button = IconButton::new( + ( + "gutter_breakpoint_indicator", + gutter_breakpoint.row as usize, + ), + ui::IconName::Play, + ) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) + .icon_color(Color::Hint); + + let button = prepaint_gutter_button( + button, + gutter_breakpoint + .to_display_point(&snapshot.display_snapshot) + .row(), + line_height, + gutter_dimensions, + scroll_pixel_position, + gutter_hitbox, + rows_with_hunk_bounds, + cx, + ); + + breakpoints_to_render.push(button); + } + + breakpoints_to_render }) } From 4bb8ec96fd69226a0d171393a125ddc3025c26d5 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 27 Jul 2024 18:10:12 -0400 Subject: [PATCH 148/650] Get gutter breakpoint hint to display at correct point and set breakpoint The breakpoints that are added through clicking on the gutter can't be affect by the toggle breakpoint command. This is a bug --- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 33 ++++++++++++++++++--------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 04dc10c4a859a3..8227ee7b007cea 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -576,7 +576,7 @@ pub struct Editor { /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown - pub gutter_breakpoint_indicator: Option, + pub gutter_breakpoint_indicator: Option, previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index cce39ebf92f614..2c5c233d346291 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -704,7 +704,10 @@ impl EditorElement { if gutter_hovered { // TODO: Show breakpoint indicator on line closest to mouse - editor.gutter_breakpoint_indicator = Some(Point::new(5, 0)); + let point_for_position = + position_map.point_for_position(text_hitbox.bounds, event.position); + let position = point_for_position.previous_valid; + editor.gutter_breakpoint_indicator = Some(position); } else { editor.gutter_breakpoint_indicator = None; } @@ -1610,23 +1613,23 @@ impl EditorElement { // See if a user is hovered over a gutter line & if they are display // a breakpoint indicator that they can click to add a breakpoint - if let Some(gutter_breakpoint) = &editor.gutter_breakpoint_indicator { - let button = IconButton::new( - ( - "gutter_breakpoint_indicator", - gutter_breakpoint.row as usize, - ), - ui::IconName::Play, - ) - .icon_size(IconSize::XSmall) - .size(ui::ButtonSize::None) - .icon_color(Color::Hint); + // TODO: We should figure out a way to display this side by side with + // the code action button. They currently overlap + if let Some(gutter_breakpoint) = editor.gutter_breakpoint_indicator { + let gutter_anchor = snapshot.display_point_to_anchor(gutter_breakpoint, Bias::Left); + + let button = IconButton::new("gutter_breakpoint_indicator", ui::IconName::Play) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) + .icon_color(Color::Hint) + .on_click(cx.listener(move |editor, _e, cx| { + editor.focus(cx); + editor.toggle_breakpoint_at_row(gutter_anchor, cx) //TODO handle folded + })); let button = prepaint_gutter_button( button, - gutter_breakpoint - .to_display_point(&snapshot.display_snapshot) - .row(), + gutter_breakpoint.row(), line_height, gutter_dimensions, scroll_pixel_position, From a54540053494177de31fdded3cd61c2b5837101b Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 28 Jul 2024 13:51:28 -0400 Subject: [PATCH 149/650] Set up breakpoint toggle to send breakpoints to dap when necessary --- Cargo.lock | 2 + crates/dap/Cargo.toml | 2 + crates/dap/src/client.rs | 18 +++++ crates/editor/src/editor.rs | 9 +++ crates/project/src/project.rs | 119 +++++++++++++++++----------------- 5 files changed, 90 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa960cb70c2f6a..4bb714aa3b9708 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3233,6 +3233,7 @@ dependencies = [ "dap-types", "futures 0.3.28", "gpui", + "language", "log", "multi_buffer", "parking_lot", @@ -3244,6 +3245,7 @@ dependencies = [ "serde_json_lenient", "smol", "task", + "text", "util", ] diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 107c118cdde9e2..c6a3aa0894bca8 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -15,6 +15,7 @@ dap-types = { git = "https://github.com/zed-industries/dap-types" } futures.workspace = true gpui.workspace = true multi_buffer.workspace = true +language.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true @@ -25,4 +26,5 @@ serde_json.workspace = true serde_json_lenient.workspace = true smol.workspace = true task.workspace = true +text.workspace = true util.workspace = true diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index e8bf1f87386110..91b5981b33c5cf 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -15,6 +15,7 @@ use dap_types::{ }; use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; +use language::Buffer; use parking_lot::{Mutex, MutexGuard}; use serde_json::Value; use smol::{ @@ -35,6 +36,7 @@ use std::{ time::Duration, }; use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType, TCPHost}; +use text::Point; use util::ResultExt; #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -652,3 +654,19 @@ impl DebugAdapterClient { pub struct Breakpoint { pub position: multi_buffer::Anchor, } + +impl Breakpoint { + pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { + SourceBreakpoint { + line: (buffer + .summary_for_anchor::(&self.position.text_anchor) + .row + + 1) as u64, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + } + } +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8227ee7b007cea..3ad8c003b2d1f0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -578,6 +578,7 @@ pub struct Editor { /// Otherwise it represents the point that the breakpoint will be shown pub gutter_breakpoint_indicator: Option, previous_search_ranges: Option]>>, + active_debuggers: bool, file_header_size: u8, breadcrumb_header: Option, focused_block: Option, @@ -1922,6 +1923,7 @@ impl Editor { tasks_update_task: None, linked_edit_ranges: Default::default(), previous_search_ranges: None, + active_debuggers: false, breadcrumb_header: None, focused_block: None, }; @@ -6025,6 +6027,13 @@ impl Editor { if !breakpoint_set.remove(&breakpoint) { breakpoint_set.insert(breakpoint); } + + if self.active_debuggers { + project.update(cx, |project, cx| { + project.update_file_breakpoints(buffer_id, cx) + }); + } + cx.notify(); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4e47282193a553..e068665a8dd179 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -27,7 +27,6 @@ use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use dap::{ client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId}, transport::Events, - SourceBreakpoint, }; use debounced_delay::DebouncedDelay; use futures::{ @@ -56,8 +55,8 @@ use language::{ deserialize_anchor, deserialize_version, serialize_anchor, serialize_line_ending, serialize_version, split_operations, }, - range_from_lsp, Bias, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, Capability, - CodeLabel, ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, + range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel, + ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, @@ -119,7 +118,7 @@ use task::{ HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, TaskVariables, VariableName, }; use terminals::Terminals; -use text::{Anchor, BufferId, LineEnding, Point}; +use text::{Anchor, BufferId, LineEnding}; use unicase::UniCase; use util::{ debug_panic, defer, maybe, merge_json_value_into, parse_env_output, post_inc, @@ -1175,24 +1174,11 @@ impl Project { if let Some((path, breakpoints)) = res { tasks.push( client.set_breakpoints( - path, + path.clone(), Some( breakpoints .iter() - .map(|b| SourceBreakpoint { - line: (buffer - .summary_for_anchor::( - &b.position.text_anchor, - ) - .row - + 1) - as u64, - condition: None, - hit_condition: None, - log_message: None, - column: None, - mode: None, - }) + .map(|b| b.to_source_breakpoint(buffer)) .collect::>(), ), ), @@ -1270,47 +1256,60 @@ impl Project { .insert(id, DebugAdapterClientState::Starting(task)); } - pub fn _update_breakpoint( - &mut self, - _buffer: Model, - _row: BufferRow, - _cx: &mut ModelContext, - ) { - // let breakpoints_for_buffer = self - // .breakpoints - // .entry(buffer.read(cx).remote_id()) - // .or_insert(Vec::new()); - - // if let Some(ix) = breakpoints_for_buffer - // .iter() - // .position(|breakpoint| breakpoint.row == row) - // { - // breakpoints_for_buffer.remove(ix); - // } else { - // breakpoints_for_buffer.push(Breakpoint { row }); - // } - - // let clients = self - // .debug_adapters - // .iter() - // .filter_map(|(_, state)| match state { - // DebugAdapterClientState::Starting(_) => None, - // DebugAdapterClientState::Running(client) => Some(client.clone()), - // }) - // .collect::>(); - - // let mut tasks = Vec::new(); - // for client in clients { - // tasks.push(self.send_breakpoints(client, cx)); - // } - - // cx.background_executor() - // .spawn(async move { - // try_join_all(tasks).await?; - - // anyhow::Ok(()) - // }) - // .detach_and_log_err(cx) + pub fn update_file_breakpoints(&self, buffer_id: BufferId, cx: &mut ModelContext) { + let clients = self + .debug_adapters + .iter() + .filter_map(|(_, state)| match state { + DebugAdapterClientState::Starting(_) => None, + DebugAdapterClientState::Running(client) => Some(client.clone()), + }) + .collect::>(); + + let Some(buffer) = self.buffer_for_id(buffer_id, cx) else { + return; + }; + + let buffer = buffer.read(cx); + + let file_path = maybe!({ + let project_path = buffer.project_path(cx)?; + let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + let path = worktree.read(cx).absolutize(&project_path.path).ok()?; + + Some(path) + }); + + let Some(file_path) = file_path else { + return; + }; + + let read_guard = self.breakpoints.read(); + + let breakpoints_locations = read_guard.get(&buffer_id); + + if let Some(breakpoints_locations) = breakpoints_locations { + let breakpoints_locations = Some( + breakpoints_locations + .iter() + .map(|bp| bp.to_source_breakpoint(&buffer)) + .collect(), + ); + + // TODO: Send correct value for sourceModified + for client in clients { + let bps = breakpoints_locations.clone(); + let file_path = file_path.clone(); + + cx.background_executor() + .spawn(async move { + client.set_breakpoints(file_path, bps).await?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + } } fn shutdown_language_servers( From 4c5deb0b4ee091c116ca87e525f7ac8a440b6e60 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 28 Jul 2024 18:13:14 -0400 Subject: [PATCH 150/650] Finalize refactoring breakpoints to use RWLocks --- crates/editor/src/editor.rs | 25 ++++++++++++++----------- crates/project/src/project.rs | 5 ++++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3ad8c003b2d1f0..d875451b597f9e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -578,7 +578,6 @@ pub struct Editor { /// Otherwise it represents the point that the breakpoint will be shown pub gutter_breakpoint_indicator: Option, previous_search_ranges: Option]>>, - active_debuggers: bool, file_header_size: u8, breadcrumb_header: Option, focused_block: Option, @@ -1923,7 +1922,6 @@ impl Editor { tasks_update_task: None, linked_edit_ranges: Default::default(), previous_search_ranges: None, - active_debuggers: false, breadcrumb_header: None, focused_block: None, }; @@ -6020,19 +6018,24 @@ impl Editor { position: breakpoint_position, }; - let mut write_guard = breakpoints.write(); + // Putting the write guard within it's own scope so it's dropped + // before project updates it's breakpoints. This is done to prevent + // a data race condition where project waits to get a read lock + { + let mut write_guard = breakpoints.write(); - let breakpoint_set = write_guard.entry(buffer_id).or_default(); + let breakpoint_set = write_guard.entry(buffer_id).or_default(); - if !breakpoint_set.remove(&breakpoint) { - breakpoint_set.insert(breakpoint); + if !breakpoint_set.remove(&breakpoint) { + breakpoint_set.insert(breakpoint); + } } - if self.active_debuggers { - project.update(cx, |project, cx| { - project.update_file_breakpoints(buffer_id, cx) - }); - } + project.update(cx, |project, cx| { + if project.has_active_debugger() { + project.update_file_breakpoints(buffer_id, cx); + } + }); cx.notify(); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e068665a8dd179..7df11f362017d7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1195,6 +1195,10 @@ impl Project { }) } + pub fn has_active_debugger(&self) -> bool { + self.debug_adapters.len() > 0 + } + pub fn start_debug_adapter_client( &mut self, debug_task: task::ResolvedTask, @@ -1300,7 +1304,6 @@ impl Project { for client in clients { let bps = breakpoints_locations.clone(); let file_path = file_path.clone(); - cx.background_executor() .spawn(async move { client.set_breakpoints(file_path, bps).await?; From 74931bd4721aa0e53d8660be58c7b08165415a37 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:27:07 -0400 Subject: [PATCH 151/650] Make active debug line stand out more & set unique DAP client ids (#8) --- crates/dap/src/client.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/project/src/project.rs | 9 ++++++++- crates/theme/src/default_colors.rs | 2 ++ crates/theme/src/one_themes.rs | 6 ++++++ crates/theme/src/schema.rs | 8 ++++++++ crates/theme/src/styles/colors.rs | 2 ++ 7 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f0a910e687f1d5..b6d5ec94676d5b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -409,7 +409,7 @@ impl DebugAdapterClient { "127.0.0.1", ], PathBuf::from("/Users/remcosmits/Documents/code/prettier-test"), - |event, cx| { + |event, _cx| { dbg!(event); }, cx, diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index edc653b8d291c1..34b3425c949be3 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -192,7 +192,7 @@ impl DebugPanel { editor.go_to_line::( row, column, - Some(cx.theme().colors().editor_highlighted_line_background), + Some(cx.theme().colors().editor_debugger_active_line_background), cx, ); }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b0c5c84e37566b..575b7be08e6968 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -191,6 +191,7 @@ pub struct Project { HashMap>>, client: Arc, next_entry_id: Arc, + next_debugger_id: AtomicUsize, join_project_response_message_id: u32, next_diagnostic_group_id: usize, diagnostic_summaries: @@ -822,6 +823,7 @@ impl Project { fs, ssh_session: None, next_entry_id: Default::default(), + next_debugger_id: Default::default(), next_diagnostic_group_id: Default::default(), diagnostics: Default::default(), diagnostic_summaries: Default::default(), @@ -986,6 +988,7 @@ impl Project { fs, ssh_session: None, next_entry_id: Default::default(), + next_debugger_id: Default::default(), next_diagnostic_group_id: Default::default(), diagnostic_summaries: Default::default(), diagnostics: Default::default(), @@ -1204,7 +1207,7 @@ impl Project { debug_task: task::ResolvedTask, cx: &mut ModelContext, ) { - let id = DebugAdapterClientId(1); + let id = DebugAdapterClientId(self.next_debugger_id()); let debug_template = debug_task.original_task(); let cwd = debug_template .cwd @@ -10499,6 +10502,10 @@ impl Project { } } + fn next_debugger_id(&mut self) -> usize { + self.next_debugger_id.fetch_add(1, SeqCst) + } + pub fn task_context_for_location( &self, captured_variables: TaskVariables, diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index de69c3294e3e26..ae5a9b96e8c9c9 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -71,6 +71,7 @@ impl ThemeColors { editor_subheader_background: neutral().light().step_2(), editor_active_line_background: neutral().light_alpha().step_3(), editor_highlighted_line_background: neutral().light_alpha().step_3(), + editor_debugger_active_line_background: neutral().light().step_8(), editor_line_number: neutral().light().step_10(), editor_active_line_number: neutral().light().step_11(), editor_invisible: neutral().light().step_10(), @@ -169,6 +170,7 @@ impl ThemeColors { editor_subheader_background: neutral().dark().step_3(), editor_active_line_background: neutral().dark_alpha().step_3(), editor_highlighted_line_background: neutral().dark_alpha().step_4(), + editor_debugger_active_line_background: neutral().light_alpha().step_5(), editor_line_number: neutral().dark_alpha().step_10(), editor_active_line_number: neutral().dark_alpha().step_12(), editor_invisible: neutral().dark_alpha().step_4(), diff --git a/crates/theme/src/one_themes.rs b/crates/theme/src/one_themes.rs index fdc266b5d2608a..fd9924abdd0188 100644 --- a/crates/theme/src/one_themes.rs +++ b/crates/theme/src/one_themes.rs @@ -88,6 +88,12 @@ pub(crate) fn one_dark() -> Theme { editor_subheader_background: bg, editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0), editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1), + editor_debugger_active_line_background: hsla( + 207.8 / 360., + 81. / 100., + 66. / 100., + 0.2, + ), editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0), editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index 30548b05496262..edb06dd08520fb 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -375,6 +375,10 @@ pub struct ThemeColorsContent { #[serde(rename = "editor.highlighted_line.background")] pub editor_highlighted_line_background: Option, + /// Background of active line of debugger + #[serde(rename = "editor.debugger_active_line.background")] + pub editor_debugger_active_line_background: Option, + /// Text Color. Used for the text of the line number in the editor gutter. #[serde(rename = "editor.line_number")] pub editor_line_number: Option, @@ -756,6 +760,10 @@ impl ThemeColorsContent { .editor_highlighted_line_background .as_ref() .and_then(|color| try_parse_color(color).ok()), + editor_debugger_active_line_background: self + .editor_debugger_active_line_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), editor_line_number: self .editor_line_number .as_ref() diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 349a79ba662794..0ffbcd686514bf 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -147,6 +147,8 @@ pub struct ThemeColors { pub editor_subheader_background: Hsla, pub editor_active_line_background: Hsla, pub editor_highlighted_line_background: Hsla, + /// Line color of the line a debugger is currently stopped at + pub editor_debugger_active_line_background: Hsla, /// Text Color. Used for the text of the line number in the editor gutter. pub editor_line_number: Hsla, /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted. From d222fbe84c102244584dcce2c7acd9e12d0b7577 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 29 Jul 2024 16:42:19 -0400 Subject: [PATCH 152/650] Fix breakpoint indicator lagging behind mouse in gutter --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2c5c233d346291..d964d65841c982 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -703,11 +703,11 @@ impl EditorElement { editor.set_gutter_hovered(gutter_hovered, cx); if gutter_hovered { - // TODO: Show breakpoint indicator on line closest to mouse let point_for_position = position_map.point_for_position(text_hitbox.bounds, event.position); let position = point_for_position.previous_valid; editor.gutter_breakpoint_indicator = Some(position); + cx.notify(); } else { editor.gutter_breakpoint_indicator = None; } From 620e65411b9a0d57964d920796e055414793d271 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 29 Jul 2024 17:58:52 -0400 Subject: [PATCH 153/650] Fix crash that can happen after placing breakpoint in file and changing files The crash was happening because editor was trying to render all breakpoints even if they didn't belong to the current buffer. Breakpoint don't persistent after closing a tab, will need to change the breakpoint DS key from buffer_id to something more permament --- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 70 ++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d875451b597f9e..264c484b1c0294 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6013,7 +6013,7 @@ impl Editor { }; let buffer_id = buffer.read(cx).remote_id(); - // let key = (buffer_id, breakpoint_position); + let breakpoint = Breakpoint { position: breakpoint_position, }; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d964d65841c982..fcc77fae501367 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -67,7 +67,10 @@ use ui::prelude::*; use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip}; use util::RangeExt; use util::ResultExt; -use workspace::{item::Item, Workspace}; +use workspace::{ + item::{FollowableItem, Item}, + Workspace, +}; struct SelectionLayout { head: DisplayPoint, @@ -1580,36 +1583,49 @@ impl EditorElement { return vec![]; }; - let mut breakpoints_to_render = breakpoints - .read() - .iter() - .flat_map(|(_buffer_id, breakpoint_set)| breakpoint_set.iter()) - .filter_map(|breakpoint| { - let point = breakpoint - .position - .to_display_point(&snapshot.display_snapshot); + let Some(active_buffer) = editor.buffer().read(cx).as_singleton() else { + return vec![]; + }; - let row = MultiBufferRow { 0: point.row().0 }; + let active_buffer_id = active_buffer.read(cx).remote_id(); + let read_guard = breakpoints.read(); - if snapshot.is_line_folded(row) { - return None; - } + let mut breakpoints_to_render = if let Some(breakpoint_set) = + read_guard.get(&active_buffer_id) + { + breakpoint_set + .iter() + .filter_map(|breakpoint| { + let point = breakpoint + .position + .to_display_point(&snapshot.display_snapshot); - let button = editor.render_breakpoint(breakpoint.position, point.row(), cx); + let row = MultiBufferRow { 0: point.row().0 }; - let button = prepaint_gutter_button( - button, - point.row(), - line_height, - gutter_dimensions, - scroll_pixel_position, - gutter_hitbox, - rows_with_hunk_bounds, - cx, - ); - Some(button) - }) - .collect_vec(); + if snapshot.is_line_folded(row) { + return None; + } + + let button = editor.render_breakpoint(breakpoint.position, point.row(), cx); + + let button = prepaint_gutter_button( + button, + point.row(), + line_height, + gutter_dimensions, + scroll_pixel_position, + gutter_hitbox, + rows_with_hunk_bounds, + cx, + ); + Some(button) + }) + .collect_vec() + } else { + vec![] + }; + + drop(read_guard); // See if a user is hovered over a gutter line & if they are display // a breakpoint indicator that they can click to add a breakpoint From 655b23c63558be29275cba22c837586da7436088 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 29 Jul 2024 18:48:27 -0400 Subject: [PATCH 154/650] Get clippy to pass --- .zed/tasks.json | 5 +++++ crates/dap/src/client.rs | 12 +++--------- crates/editor/src/element.rs | 8 +++----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.zed/tasks.json b/.zed/tasks.json index 259ab07f3e0656..ca84769a0716d3 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -3,5 +3,10 @@ "label": "clippy", "command": "./script/clippy", "args": [] + }, + { + "label": "Run Zed Tests", + "command": "cargo nextest run --workspace --no-fail-fast", + "args": [] } ] diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 91b5981b33c5cf..3fa0c26d9b150b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -399,8 +399,7 @@ impl DebugAdapterClient { request: crate::transport::Request, cx: &mut AsyncAppContext, ) -> Result<()> { - dbg!(&request); - let arguments: StartDebuggingRequestArguments = + let _arguments: StartDebuggingRequestArguments = serde_json::from_value(request.arguments.clone().unwrap_or_default())?; let sub_client = DebugAdapterClient::new( @@ -413,17 +412,12 @@ impl DebugAdapterClient { "127.0.0.1", ], PathBuf::from("/Users/remcosmits/Documents/code/prettier-test"), - |event, _cx| { - dbg!(event); - }, + |_event, _cx| {}, cx, ) .await?; - dbg!(&arguments); - - let res = sub_client.launch(request.arguments).await?; - dbg!(res); + let _res = sub_client.launch(request.arguments).await?; *this.sub_client.lock() = Some(sub_client); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fcc77fae501367..ecbced834d47d8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -67,10 +67,7 @@ use ui::prelude::*; use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip}; use util::RangeExt; use util::ResultExt; -use workspace::{ - item::{FollowableItem, Item}, - Workspace, -}; +use workspace::{item::Item, Workspace}; struct SelectionLayout { head: DisplayPoint, @@ -1630,7 +1627,7 @@ impl EditorElement { // See if a user is hovered over a gutter line & if they are display // a breakpoint indicator that they can click to add a breakpoint // TODO: We should figure out a way to display this side by side with - // the code action button. They currently overlap + // the code action button. They currently overlap if let Some(gutter_breakpoint) = editor.gutter_breakpoint_indicator { let gutter_anchor = snapshot.display_point_to_anchor(gutter_breakpoint, Bias::Left); @@ -1661,6 +1658,7 @@ impl EditorElement { }) } + #[allow(clippy::too_many_arguments)] fn layout_run_indicators( &self, line_height: Pixels, From 2ab7f834b24f03a7e1f546758b0f912273a56038 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 30 Jul 2024 18:07:33 +0200 Subject: [PATCH 155/650] Implement `startDebugging` reverse request (#9) * Wip move handle reverse requests on the debug panel * Remove todo for startDebugging * Remove unused code * Make clippy happy * Log error instead of ignoring it * Remove debug code --- Cargo.lock | 2 + crates/dap/src/client.rs | 345 +++++++++-------------- crates/dap/src/transport.rs | 11 +- crates/debugger_ui/Cargo.toml | 2 + crates/debugger_ui/src/debugger_panel.rs | 99 +++++-- crates/project/src/project.rs | 45 ++- crates/tasks_ui/src/modal.rs | 4 +- 7 files changed, 258 insertions(+), 250 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4bb714aa3b9708..a07a19a93ad688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3322,9 +3322,11 @@ dependencies = [ "project", "serde", "serde_derive", + "serde_json", "task", "tasks_ui", "ui", + "util", "workspace", ] diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 8b2e7fcae2956c..f7d3b431539345 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,16 +1,15 @@ -use crate::transport::{Events, Payload, Response, Transport}; +use crate::transport::{Payload, Response, Transport}; use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, - Restart, RunInTerminal, SetBreakpoints, StartDebugging, StepBack, StepIn, StepOut, + Restart, SetBreakpoints, StepBack, StepIn, StepOut, }, AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, - NextArguments, PauseArguments, RestartArguments, RunInTerminalRequestArguments, - RunInTerminalResponse, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, - SourceBreakpoint, StackFrame, StartDebuggingRequestArguments, StepBackArguments, + NextArguments, PauseArguments, RestartArguments, Scope, SetBreakpointsArguments, + SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, Variable, }; use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; @@ -63,13 +62,23 @@ pub struct ThreadState { pub struct DebugAdapterClient { id: DebugAdapterClientId, + pub args: Vec, + pub command: String, + pub cwd: PathBuf, + pub request_args: Option, _process: Option, server_tx: Sender, sequence_count: AtomicU64, - capabilities: Arc>>, config: DebugAdapterConfig, thread_states: Arc>>, // thread_id -> thread_state - sub_client: Arc>>>, + capabilities: Arc>>, +} + +pub struct TransportParams { + rx: Box, + tx: Box, + err: Option>, + process: Option, } impl DebugAdapterClient { @@ -80,47 +89,49 @@ impl DebugAdapterClient { /// - `config`: The adapter specific configurations from debugger task that is starting /// - `command`: The command that starts the debugger /// - `args`: Arguments of the command that starts the debugger - /// - `project_path`: The absolute path of the project that is being debugged + /// - `cwd`: The absolute path of the project that is being debugged /// - `cx`: The context that the new client belongs too pub async fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, - command: &str, - args: Vec<&str>, - project_path: PathBuf, + command: &String, + args: &Vec, + cwd: &PathBuf, + request_args: Option, event_handler: F, cx: &mut AsyncAppContext, ) -> Result> where - F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, { - match config.connection.clone() { + let transport_params = match config.connection.clone() { DebugConnectionType::TCP(host) => { - Self::create_tcp_client( - id, - config, - host, - command, - args, - project_path, - event_handler, - cx, - ) - .await + Self::create_tcp_client(host, command, args, cwd, cx).await? } - DebugConnectionType::STDIO => { - Self::create_stdio_client( - id, - config, - command, - args, - project_path, - event_handler, - cx, - ) - .await - } - } + DebugConnectionType::STDIO => Self::create_stdio_client(command, args, cwd).await?, + }; + + let server_tx = Self::handle_transport( + transport_params.rx, + transport_params.tx, + transport_params.err, + event_handler, + cx, + )?; + + Ok(Arc::new(Self { + id, + config, + server_tx, + request_args, + cwd: cwd.clone(), + args: args.clone(), + command: command.clone(), + capabilities: Default::default(), + thread_states: Default::default(), + sequence_count: AtomicU64::new(1), + _process: transport_params.process, + })) } /// Creates a debug client that connects to an adapter through tcp @@ -128,26 +139,17 @@ impl DebugAdapterClient { /// TCP clients don't have an error communication stream with an adapter /// /// # Parameters - /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients - /// - `config`: The adapter specific configurations from debugger task that is starting /// - `command`: The command that starts the debugger /// - `args`: Arguments of the command that starts the debugger - /// - `project_path`: The absolute path of the project that is being debugged + /// - `cwd`: The absolute path of the project that is being debugged /// - `cx`: The context that the new client belongs too - #[allow(clippy::too_many_arguments)] - async fn create_tcp_client( - id: DebugAdapterClientId, - config: DebugAdapterConfig, + async fn create_tcp_client( host: TCPHost, - command: &str, - args: Vec<&str>, - project_path: PathBuf, - event_handler: F, + command: &String, + args: &Vec, + cwd: &PathBuf, cx: &mut AsyncAppContext, - ) -> Result> - where - F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, - { + ) -> Result { let mut port = host.port; if port.is_none() { port = Self::get_port().await; @@ -155,7 +157,7 @@ impl DebugAdapterClient { let mut command = process::Command::new(command); command - .current_dir(project_path) + .current_dir(cwd) .args(args) .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -181,16 +183,12 @@ impl DebugAdapterClient { let (rx, tx) = TcpStream::connect(address).await?.split(); - Self::handle_transport( - id, - config, - Box::new(BufReader::new(rx)), - Box::new(tx), - None, - Some(process), - event_handler, - cx, - ) + Ok(TransportParams { + rx: Box::new(BufReader::new(rx)), + tx: Box::new(tx), + err: None, + process: Some(process), + }) } /// Get an open port to use with the tcp client when not supplied by debug config @@ -208,27 +206,17 @@ impl DebugAdapterClient { /// Creates a debug client that connects to an adapter through std input/output /// /// # Parameters - /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients - /// - `config`: The adapter specific configurations from debugger task that is starting /// - `command`: The command that starts the debugger /// - `args`: Arguments of the command that starts the debugger - /// - `project_path`: The absolute path of the project that is being debugged - /// - `cx`: The context that the new client belongs too - async fn create_stdio_client( - id: DebugAdapterClientId, - config: DebugAdapterConfig, - command: &str, - args: Vec<&str>, - project_path: PathBuf, - event_handler: F, - cx: &mut AsyncAppContext, - ) -> Result> - where - F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, - { + /// - `cwd`: The absolute path of the project that is being debugged + async fn create_stdio_client( + command: &String, + args: &Vec, + cwd: &PathBuf, + ) -> Result { let mut command = process::Command::new(command); command - .current_dir(project_path) + .current_dir(cwd) .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -252,65 +240,39 @@ impl DebugAdapterClient { .take() .ok_or_else(|| anyhow!("Failed to open stderr"))?; - let stdin = Box::new(stdin); - let stdout = Box::new(BufReader::new(stdout)); - let stderr = Box::new(BufReader::new(stderr)); - - Self::handle_transport( - id, - config, - stdout, - stdin, - Some(stderr), - Some(process), - event_handler, - cx, - ) + Ok(TransportParams { + rx: Box::new(BufReader::new(stdout)), + tx: Box::new(stdin), + err: Some(Box::new(BufReader::new(stderr))), + process: Some(process), + }) } - #[allow(clippy::too_many_arguments)] pub fn handle_transport( - id: DebugAdapterClientId, - config: DebugAdapterConfig, rx: Box, tx: Box, err: Option>, - process: Option, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result> + ) -> Result> where - F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, { let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); let (client_tx, client_rx) = unbounded::(); - let client = Arc::new(Self { - id, - config, - _process: process, - sub_client: Default::default(), - server_tx: server_tx.clone(), - capabilities: Default::default(), - thread_states: Default::default(), - sequence_count: AtomicU64::new(1), - }); - cx.update(|cx| { cx.background_executor() - .spawn(Self::handle_recv(server_rx, client_tx)) + .spawn(Self::handle_recv(server_rx, client_tx.clone())) .detach_and_log_err(cx); cx.spawn({ - let client = client.clone(); - |mut cx| async move { - Self::handle_events(client, client_rx, server_tx, event_handler, &mut cx).await - } + |mut cx| async move { Self::handle_events(client_rx, event_handler, &mut cx).await } }) .detach_and_log_err(cx); - })?; - Ok(client) + server_tx + }) } /// Set's up a client's event handler. @@ -322,109 +284,62 @@ impl DebugAdapterClient { /// should be DebugPanel::handle_debug_client_events /// `cx`: The context that this task will run in pub async fn handle_events( - this: Arc, client_rx: Receiver, - server_tx: Sender, mut event_handler: F, cx: &mut AsyncAppContext, ) -> Result<()> where - F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, { while let Ok(payload) = client_rx.recv().await { - match payload { - Payload::Event(event) => cx - .update(|cx| event_handler(*event, cx)) - .context("Event handler failed")?, - Payload::Request(request) => { - if RunInTerminal::COMMAND == request.command { - Self::handle_run_in_terminal_request(request, &server_tx).await?; - } else if StartDebugging::COMMAND == request.command { - Self::handle_start_debugging_request(&this, request, cx).await?; - } else { - unreachable!("Unknown reverse request {}", request.command); - } - } - _ => unreachable!(), - } - } - - anyhow::Ok(()) - } - - async fn handle_run_in_terminal_request( - request: crate::transport::Request, - server_tx: &Sender, - ) -> Result<()> { - let arguments: RunInTerminalRequestArguments = - serde_json::from_value(request.arguments.unwrap_or_default())?; - - let mut args = arguments.args.clone(); - let mut command = process::Command::new(args.remove(0)); - - let envs = arguments.env.as_ref().and_then(|e| e.as_object()).map(|e| { - e.iter() - .map(|(key, value)| (key.clone(), value.clone().to_string())) - .collect::>() - }); - - if let Some(envs) = envs { - command.envs(envs); + cx.update(|cx| event_handler(payload, cx))?; } - let process = command - .current_dir(arguments.cwd) - .args(args) - .spawn() - .with_context(|| "failed to spawn run in terminal command.")?; - - server_tx - .send(Payload::Response(Response { - request_seq: request.seq, - success: true, - command: RunInTerminal::COMMAND.into(), - message: None, - body: Some(serde_json::to_value(RunInTerminalResponse { - process_id: Some(process.id() as u64), - shell_process_id: None, - })?), - })) - .await?; - anyhow::Ok(()) } - async fn handle_start_debugging_request( - this: &Arc, - request: crate::transport::Request, - cx: &mut AsyncAppContext, - ) -> Result<()> { - let _arguments: StartDebuggingRequestArguments = - serde_json::from_value(request.arguments.clone().unwrap_or_default())?; - - let sub_client = DebugAdapterClient::new( - DebugAdapterClientId(1), - this.config.clone(), - "node", - vec![ - "/Users/remcosmits/Downloads/js-debug/src/dapDebugServer.js", - "8134", - "127.0.0.1", - ], - PathBuf::from("/Users/remcosmits/Documents/code/prettier-test"), - |event, _cx| { - dbg!(event); - }, - cx, - ) - .await?; - - let _res = sub_client.launch(request.arguments).await?; - - *this.sub_client.lock() = Some(sub_client); - - anyhow::Ok(()) - } + // async fn handle_run_in_terminal_request( + // this: &Arc, + // request: crate::transport::Request, + // cx: &mut AsyncAppContext, + // ) -> Result<()> { + // let arguments: RunInTerminalRequestArguments = + // serde_json::from_value(request.arguments.unwrap_or_default())?; + + // let mut args = arguments.args.clone(); + // let mut command = process::Command::new(args.remove(0)); + + // let envs = arguments.env.as_ref().and_then(|e| e.as_object()).map(|e| { + // e.iter() + // .map(|(key, value)| ((key.clone(), value.clone().to_string()))) + // .collect::>() + // }); + + // if let Some(envs) = envs { + // command.envs(envs); + // } + + // let process = command + // .current_dir(arguments.cwd) + // .args(args) + // .spawn() + // .with_context(|| "failed to spawn run in terminal command.")?; + + // this.server_tx + // .send(Payload::Response(Response { + // request_seq: request.seq, + // success: true, + // command: RunInTerminal::COMMAND.into(), + // message: None, + // body: Some(serde_json::to_value(RunInTerminalResponse { + // process_id: Some(process.id() as u64), + // shell_process_id: None, + // })?), + // })) + // .await?; + + // anyhow::Ok(()) + // } async fn handle_recv(server_rx: Receiver, client_tx: Sender) -> Result<()> { while let Ok(payload) = server_rx.recv().await { @@ -471,6 +386,10 @@ impl DebugAdapterClient { self.config.clone() } + pub fn request_args(&self) -> Option { + self.request_args.clone() + } + pub fn request_type(&self) -> DebugRequestType { self.config.request.clone() } @@ -480,8 +399,6 @@ impl DebugAdapterClient { } /// Get the next sequence id to be used in a request - /// # Side Effect - /// This function also increment's client's sequence count by one pub fn next_sequence_id(&self) -> u64 { self.sequence_count.fetch_add(1, Ordering::Relaxed) } @@ -509,14 +426,14 @@ impl DebugAdapterClient { path_format: Some(InitializeRequestArgumentsPathFormat::Path), supports_variable_type: Some(true), supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(false), // TODO: we should support this + supports_run_in_terminal_request: Some(true), supports_memory_references: Some(true), supports_progress_reporting: Some(true), - supports_invalidated_event: Some(false), + supports_invalidated_event: Some(true), lines_start_at1: Some(true), columns_start_at1: Some(true), supports_memory_event: Some(true), - supports_args_can_be_interpreted_by_shell: None, + supports_args_can_be_interpreted_by_shell: Some(true), supports_start_debugging_request: Some(true), }; diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 949a38ddeab505..c420a23ba0aae7 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -16,7 +16,7 @@ use smol::{ }; use std::{collections::HashMap, sync::Arc}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Payload { Event(Box), @@ -49,7 +49,7 @@ pub enum Events { Other(HashMap), } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct Request { #[serde(skip)] pub back_ch: Option>>, @@ -58,6 +58,12 @@ pub struct Request { pub arguments: Option, } +impl PartialEq for Request { + fn eq(&self, other: &Self) -> bool { + self.seq == other.seq && self.command == other.command && self.arguments == other.arguments + } +} + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct Response { pub request_seq: u64, @@ -219,7 +225,6 @@ impl Transport { match payload { Payload::Response(res) => { if let Some(tx) = self.pending_requests.lock().await.remove(&res.request_seq) { - if !tx.is_closed() { tx.send(Self::process_response(res)).await?; } else { diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 9477c45ed7c32d..0cd438bfce49ea 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -20,9 +20,11 @@ picker.workspace = true project.workspace = true serde.workspace = true serde_derive.workspace = true +serde_json.workspace = true task.workspace = true tasks_ui.workspace = true ui.workspace = true +util.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c0cef385645620..8559ced9be551b 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,11 +1,13 @@ +use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; -use dap::requests::{Disconnect, Scopes, StackTrace, Variables}; +use dap::requests::{Disconnect, Request, Scopes, StackTrace, StartDebugging, Variables}; +use dap::transport::Payload; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, Scope, ScopesArguments, - StackFrame, StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, - Variable, VariablesArguments, + StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, TerminatedEvent, + ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; use editor::Editor; use futures::future::try_join_all; @@ -13,18 +15,18 @@ use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, Subscription, Task, View, ViewContext, WeakView, }; +use serde_json::json; use std::path::Path; use std::{collections::HashMap, sync::Arc}; use task::DebugRequestType; use ui::prelude::*; +use util::{merge_json_value_into, ResultExt}; use workspace::Pane; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; -use crate::debugger_panel_item::DebugPanelItem; - enum DebugCurrentRowHighlight {} #[derive(Debug)] @@ -67,24 +69,35 @@ impl DebugPanel { let _subscriptions = vec![cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { - project::Event::DebugClientEvent { event, client_id } => { - Self::handle_debug_client_events( - this, - this.debug_client_by_id(*client_id, cx), - event, - cx, - ); + project::Event::DebugClientEvent { payload, client_id } => { + let client = this.debug_client_by_id(*client_id, cx); + + match payload { + Payload::Event(event) => { + Self::handle_debug_client_events(this, client, event, cx); + } + Payload::Request(request) => { + if StartDebugging::COMMAND == request.command { + Self::handle_start_debugging_request(this, client, request, cx) + .log_err(); + } + } + _ => unreachable!(), + } } project::Event::DebugClientStarted(client_id) => { let client = this.debug_client_by_id(*client_id, cx); cx.spawn(|_, _| async move { client.initialize().await?; - let request_args = client.config().request_args.map(|a| a.args); // send correct request based on adapter config match client.config().request { - DebugRequestType::Launch => client.launch(request_args).await, - DebugRequestType::Attach => client.attach(request_args).await, + DebugRequestType::Launch => { + client.launch(client.request_args()).await + } + DebugRequestType::Attach => { + client.attach(client.request_args()).await + } } }) .detach_and_log_err(cx); @@ -127,6 +140,40 @@ impl DebugPanel { .unwrap() } + fn handle_start_debugging_request( + this: &mut Self, + client: Arc, + request: &dap::transport::Request, + cx: &mut ViewContext, + ) -> Result<()> { + let arguments: StartDebuggingRequestArguments = + serde_json::from_value(request.arguments.clone().unwrap_or_default())?; + + let mut json = json!({}); + if let Some(args) = client + .config() + .request_args + .as_ref() + .map(|a| a.args.clone()) + { + merge_json_value_into(args, &mut json); + } + merge_json_value_into(arguments.configuration, &mut json); + + this.workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.start_debug_adapter_client( + client.config(), + client.command.clone(), + client.args.clone(), + client.cwd.clone(), + Some(json), + cx, + ); + }) + }) + } + fn handle_debug_client_events( this: &mut Self, client: Arc, @@ -413,13 +460,23 @@ impl DebugPanel { if !existing_item { let debug_panel = cx.view().clone(); - this.pane.update(cx, |this, cx| { - let tab = cx.new_view(|cx| { - DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx) - }); - this.add_item(Box::new(tab.clone()), false, false, None, cx) - }); + this.workspace + .update(cx, |_, cx| { + this.pane.update(cx, |this, cx| { + let tab = cx.new_view(|cx| { + DebugPanelItem::new( + debug_panel, + client.clone(), + thread_id, + cx, + ) + }); + + this.add_item(Box::new(tab.clone()), false, false, None, cx) + }) + }) + .log_err(); } cx.emit(DebugPanelEvent::Stopped((client_id, event))); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6c7f5b1f017c5a..ed8fc0fa8f9489 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,7 +26,7 @@ use clock::ReplicaId; use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use dap::{ client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId}, - transport::Events, + transport::Payload, }; use debounced_delay::DebouncedDelay; use futures::{ @@ -115,7 +115,8 @@ use std::{ }; use task::{ static_source::{StaticSource, TrackedFile}, - HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, TaskVariables, VariableName, + DebugAdapterConfig, HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, + TaskVariables, VariableName, }; use terminals::Terminals; use text::{Anchor, BufferId, LineEnding}; @@ -325,7 +326,7 @@ pub enum Event { DebugClientStarted(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, - event: Events, + payload: Payload, }, ActiveEntryChanged(Option), ActivateProjectPanel, @@ -1199,15 +1200,16 @@ impl Project { } pub fn has_active_debugger(&self) -> bool { - self.debug_adapters.len() > 0 + self.debug_adapters + .values() + .any(|c| matches!(c, DebugAdapterClientState::Starting(_))) } - pub fn start_debug_adapter_client( + pub fn start_debug_adapter_client_from_task( &mut self, debug_task: task::ResolvedTask, cx: &mut ModelContext, ) { - let id = DebugAdapterClientId(self.next_debugger_id()); let debug_template = debug_task.original_task(); let cwd = debug_template .cwd @@ -1217,23 +1219,46 @@ impl Project { .debug_adapter_config() .expect("Debug tasks need to specify adapter configuration"); + let debug_template = debug_template.clone(); let command = debug_template.command.clone(); let args = debug_template.args.clone(); + let request_args = adapter_config.request_args.as_ref().map(|a| a.args.clone()); + + self.start_debug_adapter_client( + adapter_config, + command, + args, + cwd.into(), + request_args, + cx, + ); + } + pub fn start_debug_adapter_client( + &mut self, + config: DebugAdapterConfig, + command: String, + args: Vec, + cwd: PathBuf, + request_args: Option, + cx: &mut ModelContext, + ) { + let id = DebugAdapterClientId(self.next_debugger_id()); let task = cx.spawn(|this, mut cx| async move { let project = this.clone(); let client = DebugAdapterClient::new( id, - adapter_config.clone(), + config, &command, - args.iter().map(|ele| &ele[..]).collect(), - cwd.into(), + &args, + &cwd, + request_args, move |event, cx| { project .update(cx, |_, cx| { cx.emit(Event::DebugClientEvent { client_id: id, - event, + payload: event, }) }) .log_err(); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index d0674a8d212dbb..c2ca1551ba5efd 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -352,7 +352,7 @@ impl PickerDelegate for TasksModalDelegate { // TODO: Should create a schedule_resolved_debug_task function // This would allow users to access to debug history and other issues TaskType::Debug => workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client(task, cx) + project.start_debug_adapter_client_from_task(task, cx) }), }; }) @@ -507,7 +507,7 @@ impl PickerDelegate for TasksModalDelegate { // TODO: Should create a schedule_resolved_debug_task function // This would allow users to access to debug history and other issues TaskType::Debug => workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client(task, cx) + project.start_debug_adapter_client_from_task(task, cx) }), }; }) From b9c1e511a9662ab62c79fdf689d50b22dc5e63c4 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 31 Jul 2024 10:28:31 +0200 Subject: [PATCH 156/650] Fix auto pick port didn't use the configure host (#10) * Fix use the configured host to determine the port that we can use. * Make clippy happy --- crates/dap/src/client.rs | 13 ++++++++----- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f7d3b431539345..f395a84729b4ae 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -91,6 +91,7 @@ impl DebugAdapterClient { /// - `args`: Arguments of the command that starts the debugger /// - `cwd`: The absolute path of the project that is being debugged /// - `cx`: The context that the new client belongs too + #[allow(clippy::too_many_arguments)] pub async fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -150,9 +151,11 @@ impl DebugAdapterClient { cwd: &PathBuf, cx: &mut AsyncAppContext, ) -> Result { + let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); + let mut port = host.port; if port.is_none() { - port = Self::get_port().await; + port = Self::get_port(host_address).await; } let mut command = process::Command::new(command); @@ -177,8 +180,8 @@ impl DebugAdapterClient { } let address = SocketAddrV4::new( - host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)), - port.unwrap(), + host_address, + port.ok_or(anyhow!("Port is required to connect to TCP server"))?, ); let (rx, tx) = TcpStream::connect(address).await?.split(); @@ -192,9 +195,9 @@ impl DebugAdapterClient { } /// Get an open port to use with the tcp client when not supplied by debug config - async fn get_port() -> Option { + async fn get_port(host: Ipv4Addr) -> Option { Some( - TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0)) + TcpListener::bind(SocketAddrV4::new(host, 0)) .await .ok()? .local_addr() diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index b6dd8339769bdb..73430678f3fe9e 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -437,7 +437,7 @@ impl Render for DebugPanelItem { h_flex() .gap_2() .map(|this| { - if self.current_thread_state().status == ThreadStatus::Running { + if thread_status == ThreadStatus::Running { this.child( IconButton::new("debug-pause", IconName::DebugPause) .on_click( From aa257ece8627e07b3d7da4624ec659fcf689f079 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 31 Jul 2024 11:24:59 +0200 Subject: [PATCH 157/650] Fix use correct request to stop thread (#11) * Fix clippy * Remove increment request sequence (wrong while merging) * Send correct request to stop thread based on capabilities --- crates/dap/src/client.rs | 13 ++++++++----- crates/debugger_ui/src/debugger_panel.rs | 3 --- crates/debugger_ui/src/debugger_panel_item.rs | 15 ++++++++++++++- crates/task/src/lib.rs | 4 ++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f395a84729b4ae..00ae8817a01264 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -4,13 +4,13 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, - Restart, SetBreakpoints, StepBack, StepIn, StepOut, + Restart, SetBreakpoints, StepBack, StepIn, StepOut, TerminateThreads, }, AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, RestartArguments, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, - StepInArguments, StepOutArguments, SteppingGranularity, Variable, + StepInArguments, StepOutArguments, SteppingGranularity, TerminateThreadsArguments, Variable, }; use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; @@ -373,7 +373,6 @@ impl DebugAdapterClient { self.server_tx.send(Payload::Request(request)).await?; let response = callback_rx.recv().await??; - let _ = self.next_sequence_id(); match response.success { true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), @@ -525,14 +524,13 @@ impl DebugAdapterClient { .log_err(); } - pub async fn stop(&self) { + pub async fn stop(&self) -> Result<()> { self.request::(DisconnectArguments { restart: Some(false), terminate_debuggee: Some(false), suspend_debuggee: Some(false), }) .await - .log_err(); } pub async fn set_breakpoints( @@ -564,6 +562,11 @@ impl DebugAdapterClient { self.request::(ConfigurationDoneArguments) .await } + + pub async fn terminate_threads(&self, thread_ids: Option>) -> Result<()> { + self.request::(TerminateThreadsArguments { thread_ids }) + .await + } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 8559ced9be551b..71b072f9bb076f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -180,9 +180,6 @@ impl DebugPanel { event: &Events, cx: &mut ViewContext, ) { - // Increment the sequence id because an event is being processed - let _ = client.next_sequence_id(); - match event { Events::Initialized(event) => Self::handle_initialized_event(client, event, cx), Events::Stopped(event) => Self::handle_stopped_event(client, event, cx), diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 73430678f3fe9e..c869185b567051 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -410,8 +410,21 @@ impl DebugPanelItem { fn handle_stop_action(&mut self, _: &Stop, cx: &mut ViewContext) { let client = self.client.clone(); + let thread_ids = vec![self.thread_id; 1]; + + let support_terminate_threads = client + .capabilities() + .supports_terminate_threads_request + .unwrap_or_default(); + cx.background_executor() - .spawn(async move { client.stop().await }) + .spawn(async move { + if support_terminate_threads { + client.terminate_threads(Some(thread_ids)).await + } else { + client.stop().await + } + }) .detach(); } } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 6030abbf3beda2..c99f4789855bf6 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -14,8 +14,8 @@ use std::str::FromStr; use std::{borrow::Cow, path::Path}; pub use task_template::{ - DebugAdapterConfig, DebugConnectionType, DebugRequestType, RevealStrategy, TCPHost, - TaskTemplate, TaskTemplates, TaskType, HideStrategy, + DebugAdapterConfig, DebugConnectionType, DebugRequestType, HideStrategy, RevealStrategy, + TCPHost, TaskTemplate, TaskTemplates, TaskType, }; pub use vscode_format::VsCodeTaskFile; From 1c98c1c302422df83683df52ee23c437394aa0fb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 31 Jul 2024 15:09:27 +0200 Subject: [PATCH 158/650] Act on capabilities when sending requests (#12) * Fix used wrong request args in set breakpoints request Some debug adapters depend on getting the exact data that you passed in `launch` or `attach` request. * Send correct request for stopping debug adapter I changed the name to be more in line with the request name. We now also send the correct request values on the `support_terminate_debuggee` and `support_terminate_debuggee` capabilities. * Send disconnect request for terminate threads if it does not support it * Only send configuration done request if its supported * Add disconnect icon * Only send step over request params when adapter supports it * Only send resume(continue) request params if adapter supports it * Step in only send request args if adapter supports it * Step out only send request args if adapter supports it * Step back only send request args if adapter supports it * Log error using `detach_and_log_err` instead of manually --- assets/icons/debug-disconnect.svg | 1 + crates/dap/src/client.rs | 163 +++++++++++++++--- crates/debugger_ui/src/debugger_panel_item.rs | 38 ++-- crates/ui/src/components/icon.rs | 2 + 4 files changed, 166 insertions(+), 38 deletions(-) create mode 100644 assets/icons/debug-disconnect.svg diff --git a/assets/icons/debug-disconnect.svg b/assets/icons/debug-disconnect.svg new file mode 100644 index 00000000000000..3435eebaa07c2e --- /dev/null +++ b/assets/icons/debug-disconnect.svg @@ -0,0 +1 @@ + diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 00ae8817a01264..f1ff845df9d590 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -461,51 +461,128 @@ impl DebugAdapterClient { } pub async fn resume(&self, thread_id: u64) -> Result { + let supports_single_thread_execution_requests = self + .capabilities() + .supports_single_thread_execution_requests + .unwrap_or_default(); + self.request::(ContinueArguments { thread_id, - single_thread: Some(true), + single_thread: if supports_single_thread_execution_requests { + Some(true) + } else { + None + }, }) .await } pub async fn step_over(&self, thread_id: u64) -> Result<()> { + let capabilities = self.capabilities(); + + let supports_single_thread_execution_requests = capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(); + let supports_stepping_granularity = capabilities + .supports_stepping_granularity + .unwrap_or_default(); + self.request::(NextArguments { thread_id, - granularity: Some(SteppingGranularity::Statement), - single_thread: Some(true), + granularity: if supports_stepping_granularity { + Some(SteppingGranularity::Statement) + } else { + None + }, + single_thread: if supports_single_thread_execution_requests { + Some(true) + } else { + None + }, }) .await } pub async fn step_in(&self, thread_id: u64) -> Result<()> { + let capabilities = self.capabilities(); + + let supports_single_thread_execution_requests = capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(); + let supports_stepping_granularity = capabilities + .supports_stepping_granularity + .unwrap_or_default(); + self.request::(StepInArguments { thread_id, target_id: None, - granularity: Some(SteppingGranularity::Statement), - single_thread: Some(true), + granularity: if supports_stepping_granularity { + Some(SteppingGranularity::Statement) + } else { + None + }, + single_thread: if supports_single_thread_execution_requests { + Some(true) + } else { + None + }, }) .await } pub async fn step_out(&self, thread_id: u64) -> Result<()> { + let capabilities = self.capabilities(); + + let supports_single_thread_execution_requests = capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(); + let supports_stepping_granularity = capabilities + .supports_stepping_granularity + .unwrap_or_default(); + self.request::(StepOutArguments { thread_id, - granularity: Some(SteppingGranularity::Statement), - single_thread: Some(true), + granularity: if supports_stepping_granularity { + Some(SteppingGranularity::Statement) + } else { + None + }, + single_thread: if supports_single_thread_execution_requests { + Some(true) + } else { + None + }, }) .await } pub async fn step_back(&self, thread_id: u64) -> Result<()> { + let capabilities = self.capabilities(); + + let supports_single_thread_execution_requests = capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(); + let supports_stepping_granularity = capabilities + .supports_stepping_granularity + .unwrap_or_default(); + self.request::(StepBackArguments { thread_id, - single_thread: Some(true), - granularity: Some(SteppingGranularity::Statement), + granularity: if supports_stepping_granularity { + Some(SteppingGranularity::Statement) + } else { + None + }, + single_thread: if supports_single_thread_execution_requests { + Some(true) + } else { + None + }, }) .await } - pub async fn restart(&self) { + pub async fn restart(&self) -> Result<()> { self.request::(RestartArguments { raw: self .config @@ -515,20 +592,40 @@ impl DebugAdapterClient { .unwrap_or(Value::Null), }) .await - .log_err(); } - pub async fn pause(&self, thread_id: u64) { - self.request::(PauseArguments { thread_id }) - .await - .log_err(); + pub async fn pause(&self, thread_id: u64) -> Result<()> { + self.request::(PauseArguments { thread_id }).await } - pub async fn stop(&self) -> Result<()> { + pub async fn disconnect( + &self, + restart: Option, + terminate: Option, + suspend: Option, + ) -> Result<()> { + let supports_terminate_debuggee = self + .capabilities() + .support_terminate_debuggee + .unwrap_or_default(); + + let supports_suspend_debuggee = self + .capabilities() + .support_terminate_debuggee + .unwrap_or_default(); + self.request::(DisconnectArguments { - restart: Some(false), - terminate_debuggee: Some(false), - suspend_debuggee: Some(false), + restart, + terminate_debuggee: if supports_terminate_debuggee { + terminate + } else { + None + }, + suspend_debuggee: if supports_suspend_debuggee { + suspend + } else { + None + }, }) .await } @@ -538,7 +635,7 @@ impl DebugAdapterClient { path: PathBuf, breakpoints: Option>, ) -> Result { - let adapter_data = self.config.request_args.clone().map(|c| c.args); + let adapter_data = self.request_args.clone(); self.request::(SetBreakpointsArguments { source: Source { @@ -559,13 +656,33 @@ impl DebugAdapterClient { } pub async fn configuration_done(&self) -> Result<()> { - self.request::(ConfigurationDoneArguments) + let support_configuration_done_request = self + .capabilities() + .supports_configuration_done_request + .unwrap_or_default(); + + if support_configuration_done_request { + self.request::(ConfigurationDoneArguments) + .await + } else { + Ok(()) + } + } .await } pub async fn terminate_threads(&self, thread_ids: Option>) -> Result<()> { - self.request::(TerminateThreadsArguments { thread_ids }) - .await + let support_terminate_threads = self + .capabilities() + .supports_terminate_threads_request + .unwrap_or_default(); + + if support_terminate_threads { + self.request::(TerminateThreadsArguments { thread_ids }) + .await + } else { + self.disconnect(None, Some(true), None).await + } } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index c869185b567051..8b3d68348e6eb9 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -22,7 +22,7 @@ pub struct DebugPanelItem { actions!( debug_panel_item, - [Continue, StepOver, StepIn, StepOut, Restart, Pause, Stop] + [Continue, StepOver, StepIn, StepOut, Restart, Pause, Stop, Disconnect] ); impl DebugPanelItem { @@ -397,7 +397,7 @@ impl DebugPanelItem { cx.background_executor() .spawn(async move { client.restart().await }) - .detach(); + .detach_and_log_err(cx); } fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext) { @@ -405,27 +405,23 @@ impl DebugPanelItem { let thread_id = self.thread_id; cx.background_executor() .spawn(async move { client.pause(thread_id).await }) - .detach(); + .detach_and_log_err(cx); } fn handle_stop_action(&mut self, _: &Stop, cx: &mut ViewContext) { let client = self.client.clone(); let thread_ids = vec![self.thread_id; 1]; - let support_terminate_threads = client - .capabilities() - .supports_terminate_threads_request - .unwrap_or_default(); + cx.background_executor() + .spawn(async move { client.terminate_threads(Some(thread_ids)).await }) + .detach_and_log_err(cx); + } + fn handle_disconnect_action(&mut self, _: &Disconnect, cx: &mut ViewContext) { + let client = self.client.clone(); cx.background_executor() - .spawn(async move { - if support_terminate_threads { - client.terminate_threads(Some(thread_ids)).await - } else { - client.stop().await - } - }) - .detach(); + .spawn(async move { client.disconnect(None, Some(true), None).await }) + .detach_and_log_err(cx); } } @@ -443,6 +439,7 @@ impl Render for DebugPanelItem { .capture_action(cx.listener(Self::handle_restart_action)) .capture_action(cx.listener(Self::handle_pause_action)) .capture_action(cx.listener(Self::handle_stop_action)) + .capture_action(cx.listener(Self::handle_disconnect_action)) .p_2() .size_full() .items_start() @@ -511,6 +508,17 @@ impl Render for DebugPanelItem { && thread_status != ThreadStatus::Running, ) .tooltip(move |cx| Tooltip::text("Stop", cx)), + ) + .child( + IconButton::new("debug-disconnect", IconName::DebugDisconnect) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Disconnect))), + ) + .disabled( + thread_status == ThreadStatus::Exited + || thread_status == ThreadStatus::Ended, + ) + .tooltip(move |cx| Tooltip::text("Disconnect", cx)), ), ) .child( diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index f5d29c7ab8a963..90326ab8f56328 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -155,6 +155,7 @@ pub enum IconName { DebugStepOut, DebugRestart, DebugStop, + DebugDisconnect, Delete, Disconnected, Download, @@ -313,6 +314,7 @@ impl IconName { IconName::DebugStepOut => "icons/debug-step-out.svg", IconName::DebugRestart => "icons/debug-restart.svg", IconName::DebugStop => "icons/debug-stop.svg", + IconName::DebugDisconnect => "icons/debug-disconnect.svg", IconName::Delete => "icons/delete.svg", IconName::Disconnected => "icons/disconnected.svg", IconName::Download => "icons/download.svg", From fc4d46ec22e74f5c4399dbc749412b099cd71a6c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 31 Jul 2024 15:11:54 +0200 Subject: [PATCH 159/650] Add terminate request --- crates/dap/src/client.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f1ff845df9d590..468d9b798c0af5 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -4,13 +4,14 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, - Restart, SetBreakpoints, StepBack, StepIn, StepOut, TerminateThreads, + Restart, SetBreakpoints, StepBack, StepIn, StepOut, Terminate, TerminateThreads, }, AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, RestartArguments, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, - StepInArguments, StepOutArguments, SteppingGranularity, TerminateThreadsArguments, Variable, + StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, + TerminateThreadsArguments, Variable, }; use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; @@ -668,7 +669,21 @@ impl DebugAdapterClient { Ok(()) } } + + pub async fn terminate(&self) -> Result<()> { + let support_terminate_request = self + .capabilities() + .supports_terminate_request + .unwrap_or_default(); + + if support_terminate_request { + self.request::(TerminateArguments { + restart: Some(false), + }) .await + } else { + self.disconnect(None, Some(true), None).await + } } pub async fn terminate_threads(&self, thread_ids: Option>) -> Result<()> { From c99865b853746fd35d41af016fd1b4cdd2327f6d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 31 Jul 2024 15:17:45 +0200 Subject: [PATCH 160/650] Make clippy pass --- crates/dap/src/client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 468d9b798c0af5..86cbfccf394d67 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -37,7 +37,6 @@ use std::{ }; use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType, TCPHost}; use text::Point; -use util::ResultExt; #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ThreadStatus { From 7ff1a0835636c87c1821305d4eb53d6b8fc27bd2 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 31 Jul 2024 15:29:38 +0200 Subject: [PATCH 161/650] Remove unused crates --- Cargo.lock | 10 ---------- crates/dap/Cargo.toml | 6 ------ crates/dap/LICENSE-GPL | 1 + crates/debugger_ui/Cargo.toml | 4 ---- crates/debugger_ui/LICENSE-GPL | 1 + 5 files changed, 2 insertions(+), 20 deletions(-) create mode 120000 crates/dap/LICENSE-GPL create mode 120000 crates/debugger_ui/LICENSE-GPL diff --git a/Cargo.lock b/Cargo.lock index a07a19a93ad688..c582bff0935e60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3229,7 +3229,6 @@ name = "dap" version = "0.1.0" dependencies = [ "anyhow", - "async-std", "dap-types", "futures 0.3.28", "gpui", @@ -3237,16 +3236,11 @@ dependencies = [ "log", "multi_buffer", "parking_lot", - "postage", - "release_channel", - "schemars", "serde", "serde_json", - "serde_json_lenient", "smol", "task", "text", - "util", ] [[package]] @@ -3313,15 +3307,11 @@ version = "0.1.0" dependencies = [ "anyhow", "dap", - "db", "editor", "futures 0.3.28", - "fuzzy", "gpui", - "picker", "project", "serde", - "serde_derive", "serde_json", "task", "tasks_ui", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index c6a3aa0894bca8..a961c1285dac9f 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -10,7 +10,6 @@ workspace = true [dependencies] anyhow.workspace = true -async-std = "1.12.0" dap-types = { git = "https://github.com/zed-industries/dap-types" } futures.workspace = true gpui.workspace = true @@ -18,13 +17,8 @@ multi_buffer.workspace = true language.workspace = true log.workspace = true parking_lot.workspace = true -postage.workspace = true -release_channel.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true -serde_json_lenient.workspace = true smol.workspace = true task.workspace = true text.workspace = true -util.workspace = true diff --git a/crates/dap/LICENSE-GPL b/crates/dap/LICENSE-GPL new file mode 120000 index 00000000000000..e0f9dbd5d63fef --- /dev/null +++ b/crates/dap/LICENSE-GPL @@ -0,0 +1 @@ +LICENSE-GPL \ No newline at end of file diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 0cd438bfce49ea..ab5933f2da5f99 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -11,15 +11,11 @@ workspace = true [dependencies] anyhow.workspace = true dap.workspace = true -db.workspace = true editor.workspace = true futures.workspace = true -fuzzy.workspace = true gpui.workspace = true -picker.workspace = true project.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true task.workspace = true tasks_ui.workspace = true diff --git a/crates/debugger_ui/LICENSE-GPL b/crates/debugger_ui/LICENSE-GPL new file mode 120000 index 00000000000000..e0f9dbd5d63fef --- /dev/null +++ b/crates/debugger_ui/LICENSE-GPL @@ -0,0 +1 @@ +LICENSE-GPL \ No newline at end of file From e974ddedced22ba25e302543a54110c6bef9d38f Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 1 Aug 2024 11:22:28 -0400 Subject: [PATCH 162/650] Start work on setting up workplace serialization for breakpoints --- Cargo.lock | 1 + crates/editor/src/editor.rs | 2 + crates/editor/src/items.rs | 1 + crates/sqlez/src/bindable.rs | 2 + crates/sqlez/src/statement.rs | 6 +++ crates/workspace/Cargo.toml | 1 + crates/workspace/src/persistence.rs | 62 +++++++++++++++++++++++++++++ 7 files changed, 75 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a07a19a93ad688..2ffabd1f9eadad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13370,6 +13370,7 @@ dependencies = [ "client", "clock", "collections", + "dap-types", "db", "derive_more", "dev_server_projects", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 264c484b1c0294..8b955514310877 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6012,6 +6012,8 @@ impl Editor { return; }; + dbg!(buffer.read(cx).project_path(cx)); + let buffer_id = buffer.read(cx).remote_id(); let breakpoint = Breakpoint { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 50b91f4465fcd9..34a1b501679505 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1124,6 +1124,7 @@ impl ProjectItem for Editor { buffer: Model, cx: &mut ViewContext, ) -> Self { + dbg!("Opening a buffer as a project item"); Self::for_buffer(buffer, Some(project), cx) } } diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index e8b9679936d742..15812b8f694b34 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -8,12 +8,14 @@ use util::paths::PathExt; use crate::statement::{SqlType, Statement}; +/// Define the number of columns that a type occupies in a query/database pub trait StaticColumnCount { fn column_count() -> usize { 1 } } +/// Bind values of different types to placeholders in a prepared SQL statement. pub trait Bind { fn bind(&self, statement: &Statement, start_index: i32) -> Result; } diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index 462f902239f324..5bebfa84a6a908 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -9,9 +9,15 @@ use crate::bindable::{Bind, Column}; use crate::connection::Connection; pub struct Statement<'a> { + /// vector of pointers to the raw SQLite statement objects. + /// it holds the actual prepared statements that will be executed. raw_statements: Vec<*mut sqlite3_stmt>, + /// Index of the current statement being executed from the `raw_statements` vector. current_statement: usize, + /// A reference to the database connection. + /// This is used to execute the statements and check for errors. connection: &'a Connection, + ///Indicates that the `Statement` struct is tied to the lifetime of the SQLite statement phantom: PhantomData, } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index c410846416a4aa..49c14648d35a1c 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -35,6 +35,7 @@ call.workspace = true client.workspace = true clock.workspace = true collections.workspace = true +dap-types = { git = "https://github.com/zed-industries/dap-types" } db.workspace = true derive_more.workspace = true fs.workspace = true diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index fa21f8102efb9f..d3e7396b17d766 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -135,6 +135,31 @@ impl Column for SerializedWindowBounds { } } +struct Breakpoint { + position: u64, +} + +impl sqlez::bindable::StaticColumnCount for Breakpoint {} +impl sqlez::bindable::Bind for Breakpoint { + fn bind( + &self, + statement: &sqlez::statement::Statement, + start_index: i32, + ) -> anyhow::Result { + statement.bind(&self.position, start_index) + } +} + +impl Column for Breakpoint { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let position = statement + .column_int64(start_index) + .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? + as u64; + Ok((Breakpoint { position }, start_index + 1)) + } +} + #[derive(Clone, Debug, PartialEq)] struct SerializedPixels(gpui::Pixels); impl sqlez::bindable::StaticColumnCount for SerializedPixels {} @@ -203,6 +228,13 @@ define_connection! { // active: bool, // Indicates if this item is the active one in the pane // preview: bool // Indicates if this item is a preview item // ) + // + // Anthony is testing database configs below + // CREATE TABLE breakpoints( + // workspace_id: usize Foreign Key, // References workspace table + // local_path: PathBuf, // References the file that the breakpoints belong too + // breakpoint_location: Vec, // A list of the locations of breakpoints + // ) pub static ref DB: WorkspaceDb<()> = &[sql!( CREATE TABLE workspaces( @@ -348,6 +380,15 @@ define_connection! { sql!( ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; ), + sql!(CREATE TABLE breakpoints ( + workspace_id INTEGER NOT NULL, + file_path BLOB NOT NULL, + breakpoint_location INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; + ), ]; } @@ -692,6 +733,27 @@ impl WorkspaceDb { } } + query! { + pub fn breakpoints(id: WorkspaceId, file_path: &Path) -> Result> { + SELECT breakpoint_location + FROM breakpoints + WHERE workspace_id = ?1 AND file_path = ?2 + } + } + + query! { + pub fn clear_breakpoints(id: WorkspaceId, file_path: &Path) -> Result<()> { + DELETE FROM breakpoints + WHERE workspace_id = ?1 AND file_path = ?2 + } + } + + query! { + pub fn insert_breakpoint(id: WorkspaceId, file_path: &Path, breakpoint_location: Breakpoint) -> Result<()> { + INSERT INTO breakpoints (workspace_id, file_path, breakpoint_location) VALUES (?1, ?2, ?3) + } + } + query! { fn dev_server_projects() -> Result> { SELECT id, path, dev_server_name From 42aefb4034c3aa95797264c753c7d88b8dd9f9f4 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 1 Aug 2024 14:50:59 -0400 Subject: [PATCH 163/650] Change breakpoints DS to use project path as key instead of bufferID --- crates/dap/src/client.rs | 16 +++++++++++- crates/editor/src/editor.rs | 23 +++++++++++++---- crates/editor/src/element.rs | 7 ++++-- crates/editor/src/items.rs | 23 ++++++++++++++++- crates/project/src/buffer_store.rs | 4 +++ crates/project/src/project.rs | 40 ++++++++++++++---------------- 6 files changed, 83 insertions(+), 30 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index f7d3b431539345..3e250f367c6100 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -14,7 +14,7 @@ use dap_types::{ }; use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; -use language::Buffer; +use language::{Buffer, BufferSnapshot}; use parking_lot::{Mutex, MutexGuard}; use serde_json::Value; use smol::{ @@ -582,4 +582,18 @@ impl Breakpoint { mode: None, } } + + pub fn source_for_snapshot(&self, snapshot: &BufferSnapshot) -> SourceBreakpoint { + SourceBreakpoint { + line: (snapshot + .summary_for_anchor::(&self.position.text_anchor) + .row + + 1) as u64, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + } + } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8b955514310877..f9470d0b7ccc6a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -572,7 +572,7 @@ pub struct Editor { tasks_update_task: Option>, /// All the breakpoints that are active within a project /// Is shared with editor's active project - breakpoints: Option>>>>, + breakpoints: Option>>>>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown @@ -1640,6 +1640,15 @@ impl Editor { cx: &mut ViewContext, ) -> Self { let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let path = format!( + "Opening a buffer at {:?}", + buffer + .read(cx) + .as_singleton() + .and_then(|this| this.read(cx).project_path(cx)) + ); + dbg!(path); + Self::new(EditorMode::Full, buffer, project, false, cx) } @@ -6012,9 +6021,13 @@ impl Editor { return; }; - dbg!(buffer.read(cx).project_path(cx)); + let buffer = buffer.read(cx); + + let Some(file_path) = buffer.project_path(cx) else { + return; + }; - let buffer_id = buffer.read(cx).remote_id(); + let snapshot = buffer.snapshot(); let breakpoint = Breakpoint { position: breakpoint_position, @@ -6026,7 +6039,7 @@ impl Editor { { let mut write_guard = breakpoints.write(); - let breakpoint_set = write_guard.entry(buffer_id).or_default(); + let breakpoint_set = write_guard.entry(file_path.clone()).or_default(); if !breakpoint_set.remove(&breakpoint) { breakpoint_set.insert(breakpoint); @@ -6035,7 +6048,7 @@ impl Editor { project.update(cx, |project, cx| { if project.has_active_debugger() { - project.update_file_breakpoints(buffer_id, cx); + project.update_file_breakpoints(snapshot, file_path, cx); } }); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ecbced834d47d8..81b837b82bf1c7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1584,11 +1584,14 @@ impl EditorElement { return vec![]; }; - let active_buffer_id = active_buffer.read(cx).remote_id(); + let Some(project_path) = project::Item::project_path(active_buffer.read(cx), cx) else { + return vec![]; + }; + let read_guard = breakpoints.read(); let mut breakpoints_to_render = if let Some(breakpoint_set) = - read_guard.get(&active_buffer_id) + read_guard.get(&project_path) { breakpoint_set .iter() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 34a1b501679505..8ddaad845f897f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -7,6 +7,7 @@ use crate::{ }; use anyhow::{anyhow, Context as _, Result}; use collections::HashSet; +use dap::client::Breakpoint; use file_icons::FileIcons; use futures::future::try_join_all; use git::repository::GitFileStatus; @@ -25,7 +26,10 @@ use project::{ }; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; -use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams}; +use workspace::{ + item::{Dedup, ItemSettings, SerializableItem, TabContentParams}, + WorkspaceDb, +}; use std::{ any::TypeId, @@ -1039,7 +1043,24 @@ impl SerializableItem for Editor { pane.update(&mut cx, |_, cx| { cx.new_view(|cx| { + let _buffer_path = buffer.read(cx).project_path(cx); + let mut editor = Editor::for_buffer(buffer, Some(project), cx); + let anchor = &editor + .snapshot(cx) + .display_snapshot + .buffer_snapshot + .anchor_before(Point::new(0, 0)); + + if let Some(_buffer_path) = _buffer_path { + if let Some(breakpoints) = editor.breakpoints.clone() { + let mut breakpoints = breakpoints.write(); + let vec = breakpoints.entry(_buffer_path).or_default(); + vec.insert(Breakpoint { + position: anchor.clone(), + }); + } + } editor.read_scroll_position_from_db(item_id, workspace_id, cx); editor diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 2d5ed7ed84eb70..3877ebbc041a18 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -288,6 +288,10 @@ impl BufferStore { .detach_and_log_err(cx); } + pub fn buffer_id_for_project_path(&self, project_path: &ProjectPath) -> Option<&BufferId> { + self.local_buffer_ids_by_path.get(project_path) + } + fn open_local_buffer_internal( &mut self, path: Arc, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ed8fc0fa8f9489..5ad02cf15f091d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -182,7 +182,7 @@ pub struct Project { language_servers: HashMap, language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, debug_adapters: HashMap, - pub breakpoints: Arc>>>, + pub breakpoints: Arc>>>, language_server_statuses: BTreeMap, last_formatting_failure: Option, last_workspace_edits_by_language_server: HashMap, @@ -1155,20 +1155,16 @@ impl Project { cx.spawn(|project, mut cx| async move { let task = project.update(&mut cx, |project, cx| { let mut tasks = Vec::new(); + let buffer_store = project.buffer_store.read(cx); - for (buffer_id, breakpoints) in project.breakpoints.read().iter() { - let buffer = maybe!({ - let buffer = project.buffer_for_id(*buffer_id, cx)?; - Some(buffer.read(cx)) - }); + for (project_path, breakpoints) in project.breakpoints.read().iter() { + let buffer_id = buffer_store.buffer_id_for_project_path(project_path); - if buffer.is_none() { + let Some(buffer_id) = buffer_id else { continue; - } - let buffer = buffer.as_ref().unwrap(); + }; let res = maybe!({ - let project_path = buffer.project_path(cx)?; let worktree = project.worktree_for_id(project_path.worktree_id, cx)?; let path = worktree.read(cx).absolutize(&project_path.path).ok()?; @@ -1176,13 +1172,17 @@ impl Project { }); if let Some((path, breakpoints)) = res { + let Some(buffer) = &project.buffer_for_id(*buffer_id, cx) else { + continue; + }; + tasks.push( client.set_breakpoints( path.clone(), Some( breakpoints .iter() - .map(|b| b.to_source_breakpoint(buffer)) + .map(|b| b.source_for_snapshot(&buffer.read(cx).snapshot())) .collect::>(), ), ), @@ -1288,7 +1288,12 @@ impl Project { .insert(id, DebugAdapterClientState::Starting(task)); } - pub fn update_file_breakpoints(&self, buffer_id: BufferId, cx: &mut ModelContext) { + pub fn update_file_breakpoints( + &self, + snapshot: BufferSnapshot, + project_path: ProjectPath, + cx: &ModelContext, + ) { let clients = self .debug_adapters .iter() @@ -1298,14 +1303,7 @@ impl Project { }) .collect::>(); - let Some(buffer) = self.buffer_for_id(buffer_id, cx) else { - return; - }; - - let buffer = buffer.read(cx); - let file_path = maybe!({ - let project_path = buffer.project_path(cx)?; let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let path = worktree.read(cx).absolutize(&project_path.path).ok()?; @@ -1318,13 +1316,13 @@ impl Project { let read_guard = self.breakpoints.read(); - let breakpoints_locations = read_guard.get(&buffer_id); + let breakpoints_locations = read_guard.get(&project_path); if let Some(breakpoints_locations) = breakpoints_locations { let breakpoints_locations = Some( breakpoints_locations .iter() - .map(|bp| bp.to_source_breakpoint(&buffer)) + .map(|bp| bp.source_for_snapshot(&snapshot)) .collect(), ); From ca844637f7f9f5bf7b569163a1bbfb32a43079d5 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 1 Aug 2024 16:53:54 -0400 Subject: [PATCH 164/650] Get workplace to serialize breakpoints --- crates/editor/src/editor.rs | 9 ---- crates/editor/src/items.rs | 23 +--------- crates/project/src/project.rs | 44 ++++++++++++++++++- crates/workspace/src/persistence.rs | 52 +++++++++++++++++++++-- crates/workspace/src/persistence/model.rs | 1 + crates/workspace/src/workspace.rs | 5 +++ 6 files changed, 99 insertions(+), 35 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f9470d0b7ccc6a..94277d4a2bd922 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1640,15 +1640,6 @@ impl Editor { cx: &mut ViewContext, ) -> Self { let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let path = format!( - "Opening a buffer at {:?}", - buffer - .read(cx) - .as_singleton() - .and_then(|this| this.read(cx).project_path(cx)) - ); - dbg!(path); - Self::new(EditorMode::Full, buffer, project, false, cx) } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8ddaad845f897f..b9785f6872a4d0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -26,10 +26,7 @@ use project::{ }; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; -use workspace::{ - item::{Dedup, ItemSettings, SerializableItem, TabContentParams}, - WorkspaceDb, -}; +use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams}; use std::{ any::TypeId, @@ -1043,25 +1040,7 @@ impl SerializableItem for Editor { pane.update(&mut cx, |_, cx| { cx.new_view(|cx| { - let _buffer_path = buffer.read(cx).project_path(cx); - let mut editor = Editor::for_buffer(buffer, Some(project), cx); - let anchor = &editor - .snapshot(cx) - .display_snapshot - .buffer_snapshot - .anchor_before(Point::new(0, 0)); - - if let Some(_buffer_path) = _buffer_path { - if let Some(breakpoints) = editor.breakpoints.clone() { - let mut breakpoints = breakpoints.write(); - let vec = breakpoints.entry(_buffer_path).or_default(); - vec.insert(Breakpoint { - position: anchor.clone(), - }); - } - } - editor.read_scroll_position_from_db(item_id, workspace_id, cx); editor }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5ad02cf15f091d..abf020e531f589 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -119,7 +119,7 @@ use task::{ TaskVariables, VariableName, }; use terminals::Terminals; -use text::{Anchor, BufferId, LineEnding}; +use text::{Anchor, BufferId, LineEnding, Point}; use unicase::UniCase; use util::{ debug_panic, defer, maybe, merge_json_value_into, parse_env_output, post_inc, @@ -1288,6 +1288,48 @@ impl Project { .insert(id, DebugAdapterClientState::Starting(task)); } + pub fn breakpoint_lines_for_project_path( + &self, + project_path: &ProjectPath, + cx: &ModelContext, + ) -> Option> { + let buffer_id = self + .buffer_store + .read(cx) + .buffer_id_for_project_path(project_path)?; + + let buffer = self.buffer_for_id(*buffer_id, cx)?.read(cx); + + let bp_read_guard = self.breakpoints.read(); + + Some( + bp_read_guard + .get(project_path)? + .iter() + .map(|breakpoint| { + buffer + .summary_for_anchor::(&breakpoint.position.text_anchor) + .row as u64 + }) + .collect(), + ) + } + + pub fn seralize_breakpoints(&self, cx: &ModelContext) -> Vec<(PathBuf, Vec)> { + let breakpoint_read_guard = self.breakpoints.read(); + let mut result = Vec::new(); + + for project_path in breakpoint_read_guard.keys() { + if let Some(breakpoint_lines) = + self.breakpoint_lines_for_project_path(&project_path, cx) + { + result.push((project_path.path.to_path_buf(), breakpoint_lines)) + } + } + + result + } + pub fn update_file_breakpoints( &self, snapshot: BufferSnapshot, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index d3e7396b17d766..d8daa7fc535044 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -135,8 +135,9 @@ impl Column for SerializedWindowBounds { } } -struct Breakpoint { - position: u64, +#[derive(Debug)] +pub struct Breakpoint { + pub position: u64, } impl sqlez::bindable::StaticColumnCount for Breakpoint {} @@ -489,6 +490,7 @@ impl WorkspaceDb { display, docks, session_id: None, + breakpoints: None, }) } @@ -583,6 +585,7 @@ impl WorkspaceDb { display, docks, session_id: None, + breakpoints: None, }) } @@ -591,12 +594,47 @@ impl WorkspaceDb { pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { self.write(move |conn| { conn.with_savepoint("update_worktrees", || { - // Clear out panes and pane_groups + // Clear out panes, pane_groups, and breakpoints conn.exec_bound(sql!( DELETE FROM pane_groups WHERE workspace_id = ?1; DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; + if let Some(breakpoints) = workspace.breakpoints { + for (file_path, rows) in breakpoints { + let path = file_path.as_path(); + + match conn.exec_bound(sql!( + DELETE FROM breakpoints + WHERE workspace_id = ?1 AND file_path = ?2;))?((workspace.id, path)) { + Err(err) => { + log::error!("{err}"); + // continue; + } + Ok(_) => {} + } + + for row in rows { + match conn.exec_bound(sql!( + INSERT INTO breakpoints (workspace_id, file_path, breakpoint_location) + VALUES (?1, ?2, ?3);))? + (( + workspace.id, + path, + Breakpoint { position: row }, + )) { + Err(err) => { + log::error!("{err}"); + continue; + } + Ok(_) => {} + } + } + } + + // dbg!(persistence::DB.all_breakpoints(database_id)); + } + match workspace.location { SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => { conn.exec_bound(sql!( @@ -733,6 +771,14 @@ impl WorkspaceDb { } } + query! { + pub fn all_breakpoints(id: WorkspaceId) -> Result> { + SELECT breakpoint_location + FROM breakpoints + WHERE workspace_id = ? + } + } + query! { pub fn breakpoints(id: WorkspaceId, file_path: &Path) -> Result> { SELECT breakpoint_location diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 4795d76cfcefa4..fcd888004bf310 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -216,6 +216,7 @@ pub(crate) struct SerializedWorkspace { pub(crate) display: Option, pub(crate) docks: DockStructure, pub(crate) session_id: Option, + pub(crate) breakpoints: Option)>>, } #[derive(Debug, PartialEq, Clone, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2ec967587169a0..8fb318a9f893d5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4019,6 +4019,10 @@ impl Workspace { }; if let Some(location) = location { + let breakpoint_lines = self + .project + .update(cx, |project, cx| project.seralize_breakpoints(cx)); + let center_group = build_serialized_pane_group(&self.center.root, cx); let docks = build_serialized_docks(self, cx); let window_bounds = Some(SerializedWindowBounds(cx.window_bounds())); @@ -4031,6 +4035,7 @@ impl Workspace { docks, centered_layout: self.centered_layout, session_id: self.session_id.clone(), + breakpoints: Some(breakpoint_lines), }; return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)); } From 9dfd2f5bdd81c42e97f193e29bd936778025bd22 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 1 Aug 2024 18:29:46 -0400 Subject: [PATCH 165/650] Get load workspace to get breakpoint data --- crates/editor/src/items.rs | 1 - crates/workspace/src/persistence.rs | 95 +++++++++++++++++++++++++---- crates/workspace/src/workspace.rs | 2 +- 3 files changed, 83 insertions(+), 15 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b9785f6872a4d0..4d1f2dd2a5943f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -7,7 +7,6 @@ use crate::{ }; use anyhow::{anyhow, Context as _, Result}; use collections::HashSet; -use dap::client::Breakpoint; use file_icons::FileIcons; use futures::future::try_join_all; use git::repository::GitFileStatus; diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index d8daa7fc535044..55e8ae29bfed81 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1,15 +1,16 @@ pub mod model; -use std::path::Path; +use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail, Context, Result}; use client::DevServerProjectId; +use collections::HashMap; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds, WindowBounds}; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, + statement::{SqlType, Statement}, }; use ui::px; @@ -140,6 +141,9 @@ pub struct Breakpoint { pub position: u64, } +#[derive(Debug)] +struct Breakpoints(Vec); + impl sqlez::bindable::StaticColumnCount for Breakpoint {} impl sqlez::bindable::Bind for Breakpoint { fn bind( @@ -161,6 +165,28 @@ impl Column for Breakpoint { } } +impl Column for Breakpoints { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let mut breakpoints = Vec::new(); + let mut index = start_index; + + loop { + match statement.column_type(index) { + Ok(SqlType::Null) => break, + _ => { + let position = statement + .column_int64(index) + .with_context(|| format!("Failed to read BreakPoint at index {index}"))? + as u64; + breakpoints.push(Breakpoint { position }); + index += 1; + } + } + } + Ok((Breakpoints(breakpoints), index)) + } +} + #[derive(Clone, Debug, PartialEq)] struct SerializedPixels(gpui::Pixels); impl sqlez::bindable::StaticColumnCount for SerializedPixels {} @@ -454,6 +480,49 @@ impl WorkspaceDb { .warn_on_err() .flatten()?; + dbg!("About to query breakpoints"); + + // Figure out why the below query didn't work + // let breakpoints: Result> = self + // .select_bound(sql! { + // SELECT file_path, GROUP_CONCAT(breakpoint_location) as breakpoint_locations + // FROM breakpoints + // WHERE workspace_id = ? + // GROUP BY file_path}) + // .and_then(|mut prepared_statement| (prepared_statement)(workspace_id)); + + let breakpoints: Result> = self + .select_bound(sql! { + SELECT file_path, breakpoint_location + FROM breakpoints + WHERE workspace_id = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(workspace_id)); + + let breakpoints: Option)>> = match breakpoints { + Ok(bp) => { + if bp.is_empty() { + log::error!("Breakpoints are empty"); + } + + let mut map: HashMap> = HashMap::new(); + + for (file_path, breakpoint) in bp { + map.entry(file_path).or_default().push(breakpoint.position); + } + + Some( + map.into_iter() + .map(|(file_path, breakpoints)| (file_path, breakpoints)) + .collect(), + ) + } + Err(msg) => { + log::error!("{msg}"); + None + } + }; + let location = if let Some(dev_server_project_id) = dev_server_project_id { let dev_server_project: SerializedDevServerProject = self .select_row_bound(sql! { @@ -490,7 +559,7 @@ impl WorkspaceDb { display, docks, session_id: None, - breakpoints: None, + breakpoints, }) } @@ -631,8 +700,6 @@ impl WorkspaceDb { } } } - - // dbg!(persistence::DB.all_breakpoints(database_id)); } match workspace.location { @@ -771,16 +838,18 @@ impl WorkspaceDb { } } - query! { - pub fn all_breakpoints(id: WorkspaceId) -> Result> { - SELECT breakpoint_location - FROM breakpoints - WHERE workspace_id = ? - } - } + // TODO: Fix this query + // query! { + // pub fn all_breakpoints(id: WorkspaceId) -> Result)>> { + // SELECT local_path, GROUP_CONCAT(breakpoint_location) as breakpoint_locations + // FROM breakpoints + // WHERE workspace_id = ? + // GROUP BY local_path; + // } + // } query! { - pub fn breakpoints(id: WorkspaceId, file_path: &Path) -> Result> { + pub fn breakpoints_for_file(id: WorkspaceId, file_path: &Path) -> Result> { SELECT breakpoint_location FROM breakpoints WHERE workspace_id = ?1 AND file_path = ?2 diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8fb318a9f893d5..e0e15af72e16ff 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,7 +43,7 @@ use item::{ ProjectItem, SerializableItem, SerializableItemHandle, }; use itertools::Itertools; -use language::{LanguageRegistry, Rope}; +use language::{proto::deserialize_selection, LanguageRegistry, Rope}; use lazy_static::lazy_static; pub use modal_layer::*; use node_runtime::NodeRuntime; From 8b63c1ab6fe29991d6064cee4519f040ad914646 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 1 Aug 2024 19:17:25 -0400 Subject: [PATCH 166/650] Change breakpoint DS back so it uses buffer_id as a key I also made a new DS specifically for breakpoints that aren't part of any open buffers. It should be easier to serialize and deserialize breakpoints now. --- crates/editor/src/editor.rs | 25 ++++----- crates/editor/src/element.rs | 10 +--- crates/project/src/project.rs | 86 +++++++++++++++-------------- crates/workspace/src/persistence.rs | 13 ++++- crates/workspace/src/workspace.rs | 4 +- 5 files changed, 74 insertions(+), 64 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 94277d4a2bd922..41d7bb8fbfb57b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -570,9 +570,10 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - /// All the breakpoints that are active within a project - /// Is shared with editor's active project - breakpoints: Option>>>>, + /// All the breakpoints that are contained within open buffers in the editor + opened_breakpoints: Option>>>>, + /// All breakpoints that belong to this project but are in closed files + closed_breakpoints: Option>>>>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown @@ -1791,7 +1792,7 @@ impl Editor { }; let breakpoints = if let Some(project) = project.as_ref() { - Some(project.update(cx, |project, _cx| project.breakpoints.clone())) + Some(project.update(cx, |project, _cx| project.open_breakpoints.clone())) } else { None }; @@ -1898,7 +1899,8 @@ impl Editor { blame_subscription: None, file_header_size, tasks: Default::default(), - breakpoints, + opened_breakpoints: breakpoints, + closed_breakpoints: None, gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -6004,7 +6006,7 @@ impl Editor { return; }; - let Some(breakpoints) = &self.breakpoints else { + let Some(breakpoints) = &self.opened_breakpoints else { return; }; @@ -6013,12 +6015,7 @@ impl Editor { }; let buffer = buffer.read(cx); - - let Some(file_path) = buffer.project_path(cx) else { - return; - }; - - let snapshot = buffer.snapshot(); + let buffer_id = buffer.remote_id(); let breakpoint = Breakpoint { position: breakpoint_position, @@ -6030,7 +6027,7 @@ impl Editor { { let mut write_guard = breakpoints.write(); - let breakpoint_set = write_guard.entry(file_path.clone()).or_default(); + let breakpoint_set = write_guard.entry(buffer_id).or_default(); if !breakpoint_set.remove(&breakpoint) { breakpoint_set.insert(breakpoint); @@ -6039,7 +6036,7 @@ impl Editor { project.update(cx, |project, cx| { if project.has_active_debugger() { - project.update_file_breakpoints(snapshot, file_path, cx); + project.update_file_breakpoints(buffer_id, cx); } }); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 81b837b82bf1c7..a4d19eee29ad71 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1576,7 +1576,7 @@ impl EditorElement { cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { - let Some(breakpoints) = &editor.breakpoints else { + let Some(breakpoints) = &editor.opened_breakpoints else { return vec![]; }; @@ -1584,14 +1584,10 @@ impl EditorElement { return vec![]; }; - let Some(project_path) = project::Item::project_path(active_buffer.read(cx), cx) else { - return vec![]; - }; - + let buffer_id = active_buffer.read(cx).remote_id(); let read_guard = breakpoints.read(); - let mut breakpoints_to_render = if let Some(breakpoint_set) = - read_guard.get(&project_path) + let mut breakpoints_to_render = if let Some(breakpoint_set) = read_guard.get(&buffer_id) { breakpoint_set .iter() diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index abf020e531f589..ce701e1cfad82c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -182,7 +182,8 @@ pub struct Project { language_servers: HashMap, language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, debug_adapters: HashMap, - pub breakpoints: Arc>>>, + pub open_breakpoints: Arc>>>, + closed_breakpoints: Option>>>>, language_server_statuses: BTreeMap, last_formatting_failure: Option, last_workspace_edits_by_language_server: HashMap, @@ -826,7 +827,8 @@ impl Project { supplementary_language_servers: HashMap::default(), language_servers: Default::default(), debug_adapters: Default::default(), - breakpoints: Default::default(), + open_breakpoints: Default::default(), + closed_breakpoints: Default::default(), language_server_ids: HashMap::default(), language_server_statuses: Default::default(), last_formatting_failure: None, @@ -1021,7 +1023,8 @@ impl Project { }) .collect(), debug_adapters: Default::default(), - breakpoints: Default::default(), + open_breakpoints: Default::default(), + closed_breakpoints: Default::default(), last_formatting_failure: None, last_workspace_edits_by_language_server: Default::default(), language_server_watched_paths: HashMap::default(), @@ -1153,18 +1156,20 @@ impl Project { cx: &mut ModelContext, ) -> Task> { cx.spawn(|project, mut cx| async move { + // TODO: Send breakpoints from unopened files as well let task = project.update(&mut cx, |project, cx| { let mut tasks = Vec::new(); - let buffer_store = project.buffer_store.read(cx); - for (project_path, breakpoints) in project.breakpoints.read().iter() { - let buffer_id = buffer_store.buffer_id_for_project_path(project_path); - - let Some(buffer_id) = buffer_id else { + for (buffer_id, breakpoints) in project.open_breakpoints.read().iter() { + let Some(buffer) = maybe!({ + let buffer = project.buffer_for_id(*buffer_id, cx)?; + Some(buffer.read(cx)) + }) else { continue; }; let res = maybe!({ + let project_path = buffer.project_path(cx)?; let worktree = project.worktree_for_id(project_path.worktree_id, cx)?; let path = worktree.read(cx).absolutize(&project_path.path).ok()?; @@ -1172,17 +1177,13 @@ impl Project { }); if let Some((path, breakpoints)) = res { - let Some(buffer) = &project.buffer_for_id(*buffer_id, cx) else { - continue; - }; - tasks.push( client.set_breakpoints( path.clone(), Some( breakpoints .iter() - .map(|b| b.source_for_snapshot(&buffer.read(cx).snapshot())) + .map(|b| b.source_for_snapshot(&buffer.snapshot())) .collect::>(), ), ), @@ -1288,23 +1289,19 @@ impl Project { .insert(id, DebugAdapterClientState::Starting(task)); } - pub fn breakpoint_lines_for_project_path( + pub fn serialize_breakpoint_for_buffer_id( &self, - project_path: &ProjectPath, + buffer_id: &BufferId, cx: &ModelContext, - ) -> Option> { - let buffer_id = self - .buffer_store - .read(cx) - .buffer_id_for_project_path(project_path)?; - + ) -> Option<(PathBuf, Vec)> { let buffer = self.buffer_for_id(*buffer_id, cx)?.read(cx); + let path = buffer.project_path(cx)?; + let bp_read_guard = self.open_breakpoints.read(); - let bp_read_guard = self.breakpoints.read(); - - Some( + Some(( + path.path.to_path_buf(), bp_read_guard - .get(project_path)? + .get(buffer_id)? .iter() .map(|breakpoint| { buffer @@ -1312,30 +1309,27 @@ impl Project { .row as u64 }) .collect(), - ) + )) } - pub fn seralize_breakpoints(&self, cx: &ModelContext) -> Vec<(PathBuf, Vec)> { - let breakpoint_read_guard = self.breakpoints.read(); + pub fn serialize_breakpoints(&self, cx: &ModelContext) -> Vec<(PathBuf, Vec)> { + let breakpoint_read_guard = self.open_breakpoints.read(); let mut result = Vec::new(); - for project_path in breakpoint_read_guard.keys() { - if let Some(breakpoint_lines) = - self.breakpoint_lines_for_project_path(&project_path, cx) + for buffer_id in breakpoint_read_guard.keys() { + if let Some(serialize_breakpoints) = + self.serialize_breakpoint_for_buffer_id(&buffer_id, cx) { - result.push((project_path.path.to_path_buf(), breakpoint_lines)) + result.push(serialize_breakpoints) } } + // TODO Add breakpoints that are in closed buffers result } - pub fn update_file_breakpoints( - &self, - snapshot: BufferSnapshot, - project_path: ProjectPath, - cx: &ModelContext, - ) { + // We don't need to send breakpoints from closed files because ... TODO: Fill in reasoning + pub fn update_file_breakpoints(&self, buffer_id: BufferId, cx: &ModelContext) { let clients = self .debug_adapters .iter() @@ -1345,7 +1339,18 @@ impl Project { }) .collect::>(); + if clients.is_empty() { + return; + } + + let Some(buffer) = self.buffer_for_id(buffer_id, cx) else { + return; + }; + + let buffer = buffer.read(cx); + let file_path = maybe!({ + let project_path = buffer.project_path(cx)?; let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let path = worktree.read(cx).absolutize(&project_path.path).ok()?; @@ -1356,9 +1361,10 @@ impl Project { return; }; - let read_guard = self.breakpoints.read(); + let read_guard = self.open_breakpoints.read(); - let breakpoints_locations = read_guard.get(&project_path); + let breakpoints_locations = read_guard.get(&buffer_id); + let snapshot = buffer.snapshot(); if let Some(breakpoints_locations) = breakpoints_locations { let breakpoints_locations = Some( diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 55e8ae29bfed81..34dfedb2865a9f 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -505,7 +505,7 @@ impl WorkspaceDb { log::error!("Breakpoints are empty"); } - let mut map: HashMap> = HashMap::new(); + let mut map: HashMap> = Default::default(); for (file_path, breakpoint) in bp { map.entry(file_path).or_default().push(breakpoint.position); @@ -1278,6 +1278,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + breakpoints: None, }; let workspace_2 = SerializedWorkspace { @@ -1289,6 +1290,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + breakpoints: None, }; db.save_workspace(workspace_1.clone()).await; @@ -1392,6 +1394,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + breakpoints: None, }; db.save_workspace(workspace.clone()).await; @@ -1425,6 +1428,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + breakpoints: None, }; let mut workspace_2 = SerializedWorkspace { @@ -1436,6 +1440,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + breakpoints: None, }; db.save_workspace(workspace_1.clone()).await; @@ -1477,6 +1482,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + breakpoints: None, }; db.save_workspace(workspace_3.clone()).await; @@ -1512,6 +1518,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-1".to_owned()), + breakpoints: None, }; let workspace_2 = SerializedWorkspace { @@ -1523,6 +1530,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-1".to_owned()), + breakpoints: None, }; let workspace_3 = SerializedWorkspace { @@ -1534,6 +1542,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-2".to_owned()), + breakpoints: None, }; let workspace_4 = SerializedWorkspace { @@ -1545,6 +1554,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + breakpoints: None, }; db.save_workspace(workspace_1.clone()).await; @@ -1582,6 +1592,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + breakpoints: None, } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e0e15af72e16ff..cb28d72c6c7215 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,7 +43,7 @@ use item::{ ProjectItem, SerializableItem, SerializableItemHandle, }; use itertools::Itertools; -use language::{proto::deserialize_selection, LanguageRegistry, Rope}; +use language::{LanguageRegistry, Rope}; use lazy_static::lazy_static; pub use modal_layer::*; use node_runtime::NodeRuntime; @@ -4021,7 +4021,7 @@ impl Workspace { if let Some(location) = location { let breakpoint_lines = self .project - .update(cx, |project, cx| project.seralize_breakpoints(cx)); + .update(cx, |project, cx| project.serialize_breakpoints(cx)); let center_group = build_serialized_pane_group(&self.center.root, cx); let docks = build_serialized_docks(self, cx); From d4904f97bb66df9edf1914101e4be66e72d1dd8a Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 3 Aug 2024 13:42:01 -0400 Subject: [PATCH 167/650] Start work on deserializing Workspace --- Cargo.lock | 1 + crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 45 ++++++++++--- crates/workspace/src/persistence.rs | 80 +++++++++++------------ crates/workspace/src/persistence/model.rs | 3 +- crates/workspace/src/workspace.rs | 25 ++++++- 6 files changed, 101 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ffabd1f9eadad..ce211b32d337f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8035,6 +8035,7 @@ dependencies = [ "language", "log", "lsp", + "multi_buffer", "node_runtime", "parking_lot", "pathdiff", diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 1819cf879faed1..03bf0dd70def4e 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -72,6 +72,7 @@ text.workspace = true util.workspace = true unicase.workspace = true which.workspace = true +multi_buffer.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ce701e1cfad82c..abb01d6501cfb0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -142,6 +142,8 @@ pub use worktree::{ FS_WATCH_LATENCY, }; +pub use multi_buffer::MultiBuffer; + const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4; const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); @@ -183,7 +185,7 @@ pub struct Project { language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, debug_adapters: HashMap, pub open_breakpoints: Arc>>>, - closed_breakpoints: Option>>>>, + pub closed_breakpoints: Arc>>>, language_server_statuses: BTreeMap, last_formatting_failure: Option, last_workspace_edits_by_language_server: HashMap, @@ -1312,15 +1314,13 @@ impl Project { )) } - pub fn serialize_breakpoints(&self, cx: &ModelContext) -> Vec<(PathBuf, Vec)> { + pub fn serialize_breakpoints(&self, cx: &ModelContext) -> HashMap> { let breakpoint_read_guard = self.open_breakpoints.read(); - let mut result = Vec::new(); + let mut result: HashMap> = Default::default(); for buffer_id in breakpoint_read_guard.keys() { - if let Some(serialize_breakpoints) = - self.serialize_breakpoint_for_buffer_id(&buffer_id, cx) - { - result.push(serialize_breakpoints) + if let Some((key, value)) = self.serialize_breakpoint_for_buffer_id(&buffer_id, cx) { + result.insert(key, value); } } @@ -2222,12 +2222,37 @@ impl Project { cx: &mut ModelContext, ) -> Task, AnyModel)>> { let task = self.open_buffer(path.clone(), cx); - cx.spawn(move |_, cx| async move { + cx.spawn(move |project, mut cx| async move { let buffer = task.await?; - let project_entry_id = buffer.read_with(&cx, |buffer, cx| { - File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) + let (project_entry_id, buffer_id) = buffer.read_with(&cx, |buffer, cx| { + ( + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)), + buffer.remote_id(), + ) + })?; + + let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx))?; + dbg!("Opening a path", path.path.clone()); + project.update(&mut cx, |project, cx| { + let breakpoint_rows = { project.closed_breakpoints.write().remove(&path) }; + dbg!(&project.closed_breakpoints.read()); + let snapshot = multi_buffer.read(cx).snapshot(cx); + dbg!(&breakpoint_rows); + + if let Some(breakpoint_row) = breakpoint_rows { + let mut write_guard = project.open_breakpoints.write(); + let buffer_breakpoints = write_guard.entry(buffer_id).or_default(); + dbg!(&buffer_breakpoints); + + for row in breakpoint_row { + let position = snapshot.anchor_at(Point::new(row as u32, 0), Bias::Left); + + buffer_breakpoints.insert(Breakpoint { position }); + } + } })?; + dbg!("Path open successfully"); let buffer: &AnyModel = &buffer; Ok((project_entry_id, buffer.clone())) }) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 34dfedb2865a9f..d6d8360964ce65 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -499,7 +499,7 @@ impl WorkspaceDb { }) .and_then(|mut prepared_statement| (prepared_statement)(workspace_id)); - let breakpoints: Option)>> = match breakpoints { + let breakpoints: HashMap> = match breakpoints { Ok(bp) => { if bp.is_empty() { log::error!("Breakpoints are empty"); @@ -511,15 +511,11 @@ impl WorkspaceDb { map.entry(file_path).or_default().push(breakpoint.position); } - Some( - map.into_iter() - .map(|(file_path, breakpoints)| (file_path, breakpoints)) - .collect(), - ) + map } Err(msg) => { log::error!("{msg}"); - None + Default::default() } }; @@ -654,7 +650,7 @@ impl WorkspaceDb { display, docks, session_id: None, - breakpoints: None, + breakpoints: Default::default(), }) } @@ -669,39 +665,39 @@ impl WorkspaceDb { DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; - if let Some(breakpoints) = workspace.breakpoints { - for (file_path, rows) in breakpoints { - let path = file_path.as_path(); + for (file_path, rows) in workspace.breakpoints { + let path = file_path.as_path(); + + match conn.exec_bound(sql!( + DELETE FROM breakpoints + WHERE workspace_id = ?1 AND file_path = ?2;))?((workspace.id, path)) { + Err(err) => { + log::error!("{err}"); + // continue; + } + Ok(_) => {} + } + + for row in rows { match conn.exec_bound(sql!( - DELETE FROM breakpoints - WHERE workspace_id = ?1 AND file_path = ?2;))?((workspace.id, path)) { + INSERT INTO breakpoints (workspace_id, file_path, breakpoint_location) + VALUES (?1, ?2, ?3);))? + (( + workspace.id, + path, + Breakpoint { position: row }, + )) { Err(err) => { log::error!("{err}"); - // continue; + continue; } Ok(_) => {} } - - for row in rows { - match conn.exec_bound(sql!( - INSERT INTO breakpoints (workspace_id, file_path, breakpoint_location) - VALUES (?1, ?2, ?3);))? - (( - workspace.id, - path, - Breakpoint { position: row }, - )) { - Err(err) => { - log::error!("{err}"); - continue; - } - Ok(_) => {} - } - } } } + match workspace.location { SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => { conn.exec_bound(sql!( @@ -1278,7 +1274,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: None, + breakpoints: Default::deafult(), }; let workspace_2 = SerializedWorkspace { @@ -1290,7 +1286,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: None, + breakpoints: Default::deafult(), }; db.save_workspace(workspace_1.clone()).await; @@ -1394,7 +1390,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: None, + breakpoints: Default::deafult(), }; db.save_workspace(workspace.clone()).await; @@ -1428,7 +1424,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: None, + breakpoints: Default::deafult(), }; let mut workspace_2 = SerializedWorkspace { @@ -1440,7 +1436,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: None, + breakpoints: Default::deafult(), }; db.save_workspace(workspace_1.clone()).await; @@ -1482,7 +1478,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: None, + breakpoints: Default::deafult(), }; db.save_workspace(workspace_3.clone()).await; @@ -1518,7 +1514,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-1".to_owned()), - breakpoints: None, + breakpoints: Default::deafult(), }; let workspace_2 = SerializedWorkspace { @@ -1530,7 +1526,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-1".to_owned()), - breakpoints: None, + breakpoints: Default::deafult(), }; let workspace_3 = SerializedWorkspace { @@ -1542,7 +1538,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-2".to_owned()), - breakpoints: None, + breakpoints: Default::deafult(), }; let workspace_4 = SerializedWorkspace { @@ -1554,7 +1550,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: None, + breakpoints: Default::deafult(), }; db.save_workspace(workspace_1.clone()).await; @@ -1592,7 +1588,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: None, + breakpoints: Default::deafult(), } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index fcd888004bf310..1bec39b11271ac 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -5,6 +5,7 @@ use crate::{ use anyhow::{Context, Result}; use async_recursion::async_recursion; use client::DevServerProjectId; +use collections::HashMap; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, @@ -216,7 +217,7 @@ pub(crate) struct SerializedWorkspace { pub(crate) display: Option, pub(crate) docks: DockStructure, pub(crate) session_id: Option, - pub(crate) breakpoints: Option)>>, + pub(crate) breakpoints: HashMap>, } #[derive(Debug, PartialEq, Clone, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index cb28d72c6c7215..7ba3a7328bdef2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1089,6 +1089,7 @@ impl Workspace { let mut worktree_roots: HashSet> = Default::default(); let mut project_paths: Vec<(PathBuf, Option)> = Vec::with_capacity(paths_to_open.len()); + for path in paths_to_open.into_iter() { if let Some((worktree, project_entry)) = cx .update(|cx| { @@ -4035,7 +4036,7 @@ impl Workspace { docks, centered_layout: self.centered_layout, session_id: self.session_id.clone(), - breakpoints: Some(breakpoint_lines), + breakpoints: breakpoint_lines, }; return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)); } @@ -4100,6 +4101,8 @@ impl Workspace { let mut center_items = None; // Traverse the splits tree and add to things + // Add unopened breakpoints to project before opening any + // buffer if let Some((group, active_pane, items)) = serialized_workspace .center_group .deserialize( @@ -4133,6 +4136,26 @@ impl Workspace { } })?; + dbg!(&serialized_workspace.breakpoints); + workspace.update(&mut cx, |workspace, cx| { + workspace.project().update(cx, |project, _cx| { + let mut write_guard = project.closed_breakpoints.write(); + + for project_path in items_by_project_path.keys() { + write_guard.insert( + project_path.clone(), + serialized_workspace + .breakpoints + .get(&project_path.path.to_path_buf()) + .unwrap_or(&vec![1]) + .clone(), + ); + } + + dbg!(&write_guard); + }) + })?; + let opened_items = paths_to_open .into_iter() .map(|path_to_open| { From 0247fd6087b76959dbe40f0c16683eaf8fdcb837 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 3 Aug 2024 15:22:26 -0400 Subject: [PATCH 168/650] Get deserialization to work for file's that open when initializing Zed --- crates/editor/src/editor.rs | 2 +- crates/project/src/project.rs | 11 +++------ crates/workspace/src/persistence.rs | 4 ++-- crates/workspace/src/workspace.rs | 37 ++++++++++++----------------- 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 41d7bb8fbfb57b..c14e1b0bb3a4d5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -573,7 +573,7 @@ pub struct Editor { /// All the breakpoints that are contained within open buffers in the editor opened_breakpoints: Option>>>>, /// All breakpoints that belong to this project but are in closed files - closed_breakpoints: Option>>>>, + closed_breakpoints: Option, Vec>>>>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index abb01d6501cfb0..5f9c77ca63b90c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -185,7 +185,7 @@ pub struct Project { language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, debug_adapters: HashMap, pub open_breakpoints: Arc>>>, - pub closed_breakpoints: Arc>>>, + pub closed_breakpoints: Arc, Vec>>>, language_server_statuses: BTreeMap, last_formatting_failure: Option, last_workspace_edits_by_language_server: HashMap, @@ -216,7 +216,7 @@ pub struct Project { collaborators: HashMap, client_subscriptions: Vec, worktree_store: Model, - buffer_store: Model, + pub buffer_store: Model, _subscriptions: Vec, shared_buffers: HashMap>, #[allow(clippy::type_complexity)] @@ -2232,17 +2232,13 @@ impl Project { })?; let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx))?; - dbg!("Opening a path", path.path.clone()); project.update(&mut cx, |project, cx| { - let breakpoint_rows = { project.closed_breakpoints.write().remove(&path) }; - dbg!(&project.closed_breakpoints.read()); + let breakpoint_rows = { project.closed_breakpoints.write().remove(&path.path) }; let snapshot = multi_buffer.read(cx).snapshot(cx); - dbg!(&breakpoint_rows); if let Some(breakpoint_row) = breakpoint_rows { let mut write_guard = project.open_breakpoints.write(); let buffer_breakpoints = write_guard.entry(buffer_id).or_default(); - dbg!(&buffer_breakpoints); for row in breakpoint_row { let position = snapshot.anchor_at(Point::new(row as u32, 0), Bias::Left); @@ -2252,7 +2248,6 @@ impl Project { } })?; - dbg!("Path open successfully"); let buffer: &AnyModel = &buffer; Ok((project_entry_id, buffer.clone())) }) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index d6d8360964ce65..bf44e6c3475d50 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -480,8 +480,7 @@ impl WorkspaceDb { .warn_on_err() .flatten()?; - dbg!("About to query breakpoints"); - + // dbg! // Figure out why the below query didn't work // let breakpoints: Result> = self // .select_bound(sql! { @@ -509,6 +508,7 @@ impl WorkspaceDb { for (file_path, breakpoint) in bp { map.entry(file_path).or_default().push(breakpoint.position); + // We shift breakpoint's position by one because they are zero indexed } map diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7ba3a7328bdef2..dc4ec4295959c0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4101,8 +4101,21 @@ impl Workspace { let mut center_items = None; // Traverse the splits tree and add to things - // Add unopened breakpoints to project before opening any - // buffer + // Add unopened breakpoints to project before opening any items + + workspace.update(&mut cx, |workspace, cx| { + workspace.project().update(cx, |project, _cx| { + let mut write_guard = project.closed_breakpoints.write(); + + for (file_path, mut breakpoint_rows) in serialized_workspace.breakpoints { + write_guard + .entry(Arc::from(file_path)) + .or_default() + .append(&mut breakpoint_rows); + } + }) + })?; + if let Some((group, active_pane, items)) = serialized_workspace .center_group .deserialize( @@ -4136,26 +4149,6 @@ impl Workspace { } })?; - dbg!(&serialized_workspace.breakpoints); - workspace.update(&mut cx, |workspace, cx| { - workspace.project().update(cx, |project, _cx| { - let mut write_guard = project.closed_breakpoints.write(); - - for project_path in items_by_project_path.keys() { - write_guard.insert( - project_path.clone(), - serialized_workspace - .breakpoints - .get(&project_path.path.to_path_buf()) - .unwrap_or(&vec![1]) - .clone(), - ); - } - - dbg!(&write_guard); - }) - })?; - let opened_items = paths_to_open .into_iter() .map(|path_to_open| { From 68158d3fc501a386c7f29ae537749ba0d9c55dc8 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 3 Aug 2024 16:14:00 -0400 Subject: [PATCH 169/650] Transfer breakpoints from closed DS -> open DS when a buffer is open --- crates/editor/src/editor.rs | 28 +++++++++++++++++++++++----- crates/project/src/project.rs | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c14e1b0bb3a4d5..cc4ce4690b799d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1791,10 +1791,15 @@ impl Editor { None }; - let breakpoints = if let Some(project) = project.as_ref() { - Some(project.update(cx, |project, _cx| project.open_breakpoints.clone())) + let (opened_breakpoints, closed_breakpoints) = if let Some(project) = project.as_ref() { + project.update(cx, |project, _cx| { + ( + Some(project.open_breakpoints.clone()), + Some(project.closed_breakpoints.clone()), + ) + }) } else { - None + (None, None) }; let mut this = Self { @@ -1899,8 +1904,8 @@ impl Editor { blame_subscription: None, file_header_size, tasks: Default::default(), - opened_breakpoints: breakpoints, - closed_breakpoints: None, + opened_breakpoints, + closed_breakpoints, gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -1941,6 +1946,19 @@ impl Editor { this.git_blame_inline_enabled = true; this.start_git_blame_inline(false, cx); } + + // Check if this buffer should have breakpoints added too it + if let Some((path, buffer_id, snapshot)) = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let buffer = buffer.as_singleton()?.read(cx); + Some((buffer.project_path(cx)?.path, buffer.remote_id(), snapshot)) + }) { + if let Some(project) = this.project.as_ref() { + project.update(cx, |project, _cx| { + project.write_breakpoints(path, buffer_id, snapshot) + }); + } + } } this.report_editor_event("open", None, cx); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5f9c77ca63b90c..3be1570137db5b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -69,6 +69,7 @@ use lsp::{ ServerHealthStatus, ServerStatus, TextEdit, WorkDoneProgressCancelParams, }; use lsp_command::*; +use multi_buffer::MultiBufferSnapshot; use node_runtime::NodeRuntime; use parking_lot::{Mutex, RwLock}; use paths::{ @@ -1314,6 +1315,26 @@ impl Project { )) } + pub fn write_breakpoints( + &mut self, + path: Arc, + buffer_id: BufferId, + snapshot: MultiBufferSnapshot, + ) { + if let Some(breakpoint_rows) = { self.closed_breakpoints.write().remove(&path) } { + let mut write_guard = self.open_breakpoints.write(); + + for row in breakpoint_rows { + write_guard + .entry(buffer_id) + .or_default() + .insert(Breakpoint { + position: snapshot.anchor_at(Point::new(row as u32, 0), Bias::Left), + }); + } + } + } + pub fn serialize_breakpoints(&self, cx: &ModelContext) -> HashMap> { let breakpoint_read_guard = self.open_breakpoints.read(); let mut result: HashMap> = Default::default(); From e24e5f7e89b4cd355ce52220cf9d17299fd88615 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 3 Aug 2024 20:15:16 -0400 Subject: [PATCH 170/650] Transfer breakpoints from open -> close when buffer maintaining them is released --- crates/project/src/project.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3be1570137db5b..ab008893e7ceb2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2447,6 +2447,7 @@ impl Project { buffer: &Model, cx: &mut ModelContext, ) -> Result<()> { + println!("Registering a buffer right now"); self.request_buffer_diff_recalculation(buffer, cx); buffer.update(cx, |buffer, _| { buffer.set_language_registry(self.languages.clone()) @@ -2460,6 +2461,23 @@ impl Project { self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_servers(buffer, cx); cx.observe_release(buffer, |this, buffer, cx| { + // Serialize the breakpoints of this buffer and send them + // Unopened breakpoints to maintain correct state + if let Some(breakpoints) = this.open_breakpoints.write().remove(&buffer.remote_id()) { + if let Some(file_path) = buffer.project_path(cx) { + let file_path = file_path.path; + this.closed_breakpoints + .write() + .entry(file_path) + .or_default() + .extend(breakpoints.into_iter().map(|bp| { + buffer + .summary_for_anchor::(&bp.position.text_anchor) + .row as u64 + })); + } + } + if let Some(file) = File::from_dyn(buffer.file()) { if file.is_local() { let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); From b1d830cb09216da9e77ca10033acbd163882e24c Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 3 Aug 2024 20:18:35 -0400 Subject: [PATCH 171/650] Editor doesn't need to know about closed breakpoint --- crates/editor/src/editor.rs | 14 +++----------- crates/project/src/project.rs | 1 + 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cc4ce4690b799d..d0aceea346f4d0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -572,8 +572,6 @@ pub struct Editor { tasks_update_task: Option>, /// All the breakpoints that are contained within open buffers in the editor opened_breakpoints: Option>>>>, - /// All breakpoints that belong to this project but are in closed files - closed_breakpoints: Option, Vec>>>>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown @@ -1791,15 +1789,10 @@ impl Editor { None }; - let (opened_breakpoints, closed_breakpoints) = if let Some(project) = project.as_ref() { - project.update(cx, |project, _cx| { - ( - Some(project.open_breakpoints.clone()), - Some(project.closed_breakpoints.clone()), - ) - }) + let opened_breakpoints = if let Some(project) = project.as_ref() { + project.update(cx, |project, _cx| Some(project.open_breakpoints.clone())) } else { - (None, None) + None }; let mut this = Self { @@ -1905,7 +1898,6 @@ impl Editor { file_header_size, tasks: Default::default(), opened_breakpoints, - closed_breakpoints, gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ab008893e7ceb2..1dcdcde0657e3f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -186,6 +186,7 @@ pub struct Project { language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, debug_adapters: HashMap, pub open_breakpoints: Arc>>>, + /// All breakpoints that belong to this project but are in closed files pub closed_breakpoints: Arc, Vec>>>, language_server_statuses: BTreeMap, last_formatting_failure: Option, From 5f4affdefe614c98cc2260b455cf92d56b5d94a4 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 3 Aug 2024 23:26:12 -0400 Subject: [PATCH 172/650] Introduce serialized breakpoint type & refactor code base to use it --- Cargo.lock | 1 + crates/dap/src/client.rs | 18 ++- crates/editor/src/editor.rs | 6 +- crates/editor/src/items.rs | 1 - crates/project/src/project.rs | 142 +++++++++++++++++----- crates/workspace/Cargo.toml | 1 + crates/workspace/src/persistence.rs | 102 +++++++++------- crates/workspace/src/persistence/model.rs | 4 +- crates/workspace/src/workspace.rs | 29 +++-- 9 files changed, 211 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce211b32d337f1..2b12d0c4a9d008 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13371,6 +13371,7 @@ dependencies = [ "client", "clock", "collections", + "dap", "dap-types", "db", "derive_more", diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 3e250f367c6100..d336fc25edd715 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -26,7 +26,7 @@ use smol::{ use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddrV4}, - path::PathBuf, + path::{Path, PathBuf}, process::Stdio, sync::{ atomic::{AtomicU64, Ordering}, @@ -596,4 +596,20 @@ impl Breakpoint { mode: None, } } + + pub fn to_serialized(&self, buffer: &Buffer, path: Arc) -> SerializedBreakpoint { + SerializedBreakpoint { + position: buffer + .summary_for_anchor::(&self.position.text_anchor) + .row + + 1, + path, + } + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct SerializedBreakpoint { + pub position: u32, + pub path: Arc, } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d0aceea346f4d0..9b92f7f097db62 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1940,14 +1940,14 @@ impl Editor { } // Check if this buffer should have breakpoints added too it - if let Some((path, buffer_id, snapshot)) = buffer.read_with(cx, |buffer, cx| { + if let Some((project_path, buffer_id, snapshot)) = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); let buffer = buffer.as_singleton()?.read(cx); - Some((buffer.project_path(cx)?.path, buffer.remote_id(), snapshot)) + Some((buffer.project_path(cx)?, buffer.remote_id(), snapshot)) }) { if let Some(project) = this.project.as_ref() { project.update(cx, |project, _cx| { - project.write_breakpoints(path, buffer_id, snapshot) + project.convert_to_open_breakpoints(&project_path, buffer_id, snapshot) }); } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 4d1f2dd2a5943f..d457f0a89a751a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1123,7 +1123,6 @@ impl ProjectItem for Editor { buffer: Model, cx: &mut ViewContext, ) -> Self { - dbg!("Opening a buffer as a project item"); Self::for_buffer(buffer, Some(project), cx) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1dcdcde0657e3f..767d2b430f75e2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -25,7 +25,7 @@ use client::{ use clock::ReplicaId; use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use dap::{ - client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId}, + client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId, SerializedBreakpoint}, transport::Payload, }; use debounced_delay::DebouncedDelay; @@ -187,7 +187,7 @@ pub struct Project { debug_adapters: HashMap, pub open_breakpoints: Arc>>>, /// All breakpoints that belong to this project but are in closed files - pub closed_breakpoints: Arc, Vec>>>, + pub closed_breakpoints: Arc>>>, language_server_statuses: BTreeMap, last_formatting_failure: Option, last_workspace_edits_by_language_server: HashMap, @@ -1154,11 +1154,75 @@ impl Project { }) } + pub fn all_breakpoints( + &self, + as_abs_path: bool, + cx: &mut ModelContext, + ) -> HashMap, Vec> { + let mut all_breakpoints: HashMap, Vec> = Default::default(); + + for (buffer_id, breakpoints) in self.open_breakpoints.read().iter() { + let Some(buffer) = maybe!({ + let buffer = self.buffer_for_id(*buffer_id, cx)?; + Some(buffer.read(cx)) + }) else { + continue; + }; + + let Some(path) = maybe!({ + let project_path = buffer.project_path(cx)?; + + if as_abs_path { + let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + Some(Arc::from( + worktree + .read(cx) + .absolutize(&project_path.path) + .ok()? + .as_path(), + )) + } else { + Some(project_path.path) + } + }) else { + continue; + }; + + all_breakpoints.entry(path.clone()).or_default().extend( + breakpoints + .into_iter() + .map(|bp| bp.to_serialized(buffer, path.clone())), + ); + } + + for (project_path, serialized_breakpoints) in self.closed_breakpoints.read().iter() { + let file_path = maybe!({ + if as_abs_path { + Some(Arc::from(self.absolute_path(project_path, cx)?)) + } else { + Some(project_path.path.clone()) + } + }); + + if let Some(file_path) = file_path { + all_breakpoints + .entry(file_path) + .or_default() + .extend(serialized_breakpoints.iter().map(|bp| bp.clone())); + } + } + + all_breakpoints + } + pub fn send_breakpoints( &self, client: Arc, cx: &mut ModelContext, ) -> Task> { + println!("All breakpoints {:?}", self.all_breakpoints(true, cx)); + println!("All breakpoints {:?}", self.all_breakpoints(false, cx)); + cx.spawn(|project, mut cx| async move { // TODO: Send breakpoints from unopened files as well let task = project.update(&mut cx, |project, cx| { @@ -1293,60 +1357,72 @@ impl Project { .insert(id, DebugAdapterClientState::Starting(task)); } + // TODO Anth: Convert this function to use a serialized breakpoint and return one too pub fn serialize_breakpoint_for_buffer_id( &self, buffer_id: &BufferId, cx: &ModelContext, - ) -> Option<(PathBuf, Vec)> { + ) -> Option<(Arc, Vec)> { let buffer = self.buffer_for_id(*buffer_id, cx)?.read(cx); - let path = buffer.project_path(cx)?; + let project_path = buffer.project_path(cx)?; + let worktree_path = self + .worktree_for_id(project_path.worktree_id, cx)? + .read(cx) + .abs_path(); let bp_read_guard = self.open_breakpoints.read(); Some(( - path.path.to_path_buf(), + worktree_path, bp_read_guard .get(buffer_id)? .iter() - .map(|breakpoint| { - buffer - .summary_for_anchor::(&breakpoint.position.text_anchor) - .row as u64 - }) + .map(|bp| bp.to_serialized(buffer, project_path.path.clone())) .collect(), )) } - pub fn write_breakpoints( + // TODO Anth: Add docs about how this is used when a new editor with breakpoints is init + pub fn convert_to_open_breakpoints( &mut self, - path: Arc, + project_path: &ProjectPath, buffer_id: BufferId, snapshot: MultiBufferSnapshot, ) { - if let Some(breakpoint_rows) = { self.closed_breakpoints.write().remove(&path) } { + if let Some(serialized_breakpoints) = + { self.closed_breakpoints.write().remove(project_path) } + { let mut write_guard = self.open_breakpoints.write(); - for row in breakpoint_rows { + for serialized_bp in serialized_breakpoints { write_guard .entry(buffer_id) .or_default() .insert(Breakpoint { - position: snapshot.anchor_at(Point::new(row as u32, 0), Bias::Left), + position: snapshot + .anchor_at(Point::new(serialized_bp.position, 0), Bias::Left), }); } } } - pub fn serialize_breakpoints(&self, cx: &ModelContext) -> HashMap> { + pub fn serialize_breakpoints( + &self, + cx: &ModelContext, + ) -> HashMap, Vec> { let breakpoint_read_guard = self.open_breakpoints.read(); - let mut result: HashMap> = Default::default(); + let mut result: HashMap, Vec> = Default::default(); for buffer_id in breakpoint_read_guard.keys() { - if let Some((key, value)) = self.serialize_breakpoint_for_buffer_id(&buffer_id, cx) { - result.insert(key, value); + if let Some((worktree_path, serialized_breakpoint)) = + self.serialize_breakpoint_for_buffer_id(&buffer_id, cx) + { + result.insert(worktree_path, serialized_breakpoint); } } - // TODO Add breakpoints that are in closed buffers + // TODO Anth: Add breakpoints that are in closed buffers + // TODO Anth: Add documentation + // TODO Anth: use SerializeBreakpoint Type as value in result hashmap result } @@ -2255,15 +2331,16 @@ impl Project { let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx))?; project.update(&mut cx, |project, cx| { - let breakpoint_rows = { project.closed_breakpoints.write().remove(&path.path) }; + let serialized_breakpoints = { project.closed_breakpoints.write().remove(&path) }; let snapshot = multi_buffer.read(cx).snapshot(cx); - if let Some(breakpoint_row) = breakpoint_rows { + if let Some(serialized_breakpoints) = serialized_breakpoints { let mut write_guard = project.open_breakpoints.write(); let buffer_breakpoints = write_guard.entry(buffer_id).or_default(); - for row in breakpoint_row { - let position = snapshot.anchor_at(Point::new(row as u32, 0), Bias::Left); + for serialized_bp in serialized_breakpoints { + let position = + snapshot.anchor_at(Point::new(serialized_bp.position, 0), Bias::Left); buffer_breakpoints.insert(Breakpoint { position }); } @@ -2448,7 +2525,6 @@ impl Project { buffer: &Model, cx: &mut ModelContext, ) -> Result<()> { - println!("Registering a buffer right now"); self.request_buffer_diff_recalculation(buffer, cx); buffer.update(cx, |buffer, _| { buffer.set_language_registry(self.languages.clone()) @@ -2464,18 +2540,18 @@ impl Project { cx.observe_release(buffer, |this, buffer, cx| { // Serialize the breakpoints of this buffer and send them // Unopened breakpoints to maintain correct state + // TODO Anth: Almost done yayyy if let Some(breakpoints) = this.open_breakpoints.write().remove(&buffer.remote_id()) { - if let Some(file_path) = buffer.project_path(cx) { - let file_path = file_path.path; + if let Some(project_path) = buffer.project_path(cx) { this.closed_breakpoints .write() - .entry(file_path) + .entry(project_path.clone()) .or_default() - .extend(breakpoints.into_iter().map(|bp| { - buffer - .summary_for_anchor::(&bp.position.text_anchor) - .row as u64 - })); + .extend( + breakpoints + .into_iter() + .map(|bp| bp.to_serialized(buffer, project_path.path.clone())), + ); } } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 49c14648d35a1c..b58300cfbdcbb4 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -36,6 +36,7 @@ client.workspace = true clock.workspace = true collections.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types" } +dap.workspace = true db.workspace = true derive_more.workspace = true fs.workspace = true diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index bf44e6c3475d50..88af082eb04b41 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1,10 +1,14 @@ pub mod model; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use anyhow::{anyhow, bail, Context, Result}; use client::DevServerProjectId; use collections::HashMap; +use dap::client::SerializedBreakpoint; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds, WindowBounds}; @@ -138,7 +142,7 @@ impl Column for SerializedWindowBounds { #[derive(Debug)] pub struct Breakpoint { - pub position: u64, + pub position: u32, } #[derive(Debug)] @@ -158,9 +162,9 @@ impl sqlez::bindable::Bind for Breakpoint { impl Column for Breakpoint { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { let position = statement - .column_int64(start_index) + .column_int(start_index) .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? - as u64; + as u32; Ok((Breakpoint { position }, start_index + 1)) } } @@ -175,9 +179,9 @@ impl Column for Breakpoints { Ok(SqlType::Null) => break, _ => { let position = statement - .column_int64(index) + .column_int(index) .with_context(|| format!("Failed to read BreakPoint at index {index}"))? - as u64; + as u32; breakpoints.push(Breakpoint { position }); index += 1; } @@ -257,9 +261,11 @@ define_connection! { // ) // // Anthony is testing database configs below + // TODO Anth: Update docs below // CREATE TABLE breakpoints( // workspace_id: usize Foreign Key, // References workspace table - // local_path: PathBuf, // References the file that the breakpoints belong too + // worktree_path: PathBuf, // Path of worktree that this breakpoint belong's too. Used to determine the absolute path of a breakpoint + // local_path: PathBuf, // References the file that the breakpoints belong too TODO Anth: rename to rel_path // breakpoint_location: Vec, // A list of the locations of breakpoints // ) pub static ref DB: WorkspaceDb<()> = @@ -409,7 +415,8 @@ define_connection! { ), sql!(CREATE TABLE breakpoints ( workspace_id INTEGER NOT NULL, - file_path BLOB NOT NULL, + worktree_path BLOB NOT NULL, + relative_path BLOB NOT NULL, breakpoint_location INTEGER NOT NULL, FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE @@ -490,34 +497,39 @@ impl WorkspaceDb { // GROUP BY file_path}) // .and_then(|mut prepared_statement| (prepared_statement)(workspace_id)); - let breakpoints: Result> = self + let breakpoints: Result> = self .select_bound(sql! { - SELECT file_path, breakpoint_location + SELECT worktree_path, relative_path, breakpoint_location FROM breakpoints WHERE workspace_id = ? }) .and_then(|mut prepared_statement| (prepared_statement)(workspace_id)); - let breakpoints: HashMap> = match breakpoints { - Ok(bp) => { - if bp.is_empty() { - log::error!("Breakpoints are empty"); - } + let serialized_breakpoints: HashMap, Vec> = + match breakpoints { + Ok(bp) => { + if bp.is_empty() { + log::error!("Breakpoints are empty"); + } - let mut map: HashMap> = Default::default(); + let mut map: HashMap, Vec> = Default::default(); - for (file_path, breakpoint) in bp { - map.entry(file_path).or_default().push(breakpoint.position); - // We shift breakpoint's position by one because they are zero indexed - } + for (worktree_path, file_path, breakpoint) in bp { + map.entry(Arc::from(worktree_path.as_path())) + .or_default() + .push(SerializedBreakpoint { + position: breakpoint.position, + path: Arc::from(file_path.as_path()), + }); + } - map - } - Err(msg) => { - log::error!("{msg}"); - Default::default() - } - }; + map + } + Err(msg) => { + log::error!("{msg}"); + Default::default() + } + }; let location = if let Some(dev_server_project_id) = dev_server_project_id { let dev_server_project: SerializedDevServerProject = self @@ -555,7 +567,7 @@ impl WorkspaceDb { display, docks, session_id: None, - breakpoints, + breakpoints: serialized_breakpoints, }) } @@ -665,28 +677,28 @@ impl WorkspaceDb { DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; - - for (file_path, rows) in workspace.breakpoints { - let path = file_path.as_path(); - - match conn.exec_bound(sql!( - DELETE FROM breakpoints - WHERE workspace_id = ?1 AND file_path = ?2;))?((workspace.id, path)) { - Err(err) => { - log::error!("{err}"); - // continue; - } - Ok(_) => {} + // Clear out breakpoints associated with this workspace + match conn.exec_bound(sql!( + DELETE FROM breakpoints + WHERE workspace_id = ?1;))?(workspace.id,) { + Err(err) => { + log::error!("Breakpoints failed to clear with error: {err}"); } + Ok(_) => {} + } + + for (worktree_path, serialized_breakpoints) in workspace.breakpoints { + for serialized_breakpoint in serialized_breakpoints { + let relative_path = serialized_breakpoint.path; - for row in rows { match conn.exec_bound(sql!( - INSERT INTO breakpoints (workspace_id, file_path, breakpoint_location) - VALUES (?1, ?2, ?3);))? + INSERT INTO breakpoints (workspace_id, relative_path, worktree_path, breakpoint_location) + VALUES (?1, ?2, ?3, ?4);))? (( workspace.id, - path, - Breakpoint { position: row }, + relative_path, + worktree_path.clone(), + Breakpoint { position: serialized_breakpoint.position }, )) { Err(err) => { log::error!("{err}"); diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 1bec39b11271ac..7ed75c2dc7ab2d 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -6,6 +6,7 @@ use anyhow::{Context, Result}; use async_recursion::async_recursion; use client::DevServerProjectId; use collections::HashMap; +use dap::client::SerializedBreakpoint; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, @@ -217,7 +218,8 @@ pub(crate) struct SerializedWorkspace { pub(crate) display: Option, pub(crate) docks: DockStructure, pub(crate) session_id: Option, - pub(crate) breakpoints: HashMap>, + /// The key of this hashmap is an absolute worktree path that owns the breakpoint + pub(crate) breakpoints: HashMap, Vec>, } #[derive(Debug, PartialEq, Clone, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index dc4ec4295959c0..4e48df70f6ff80 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4090,7 +4090,7 @@ impl Workspace { } pub(crate) fn load_workspace( - serialized_workspace: SerializedWorkspace, + mut serialized_workspace: SerializedWorkspace, paths_to_open: Vec>, cx: &mut ViewContext, ) -> Task>>>> { @@ -4100,22 +4100,33 @@ impl Workspace { let mut center_group = None; let mut center_items = None; - // Traverse the splits tree and add to things // Add unopened breakpoints to project before opening any items - workspace.update(&mut cx, |workspace, cx| { - workspace.project().update(cx, |project, _cx| { + workspace.project().update(cx, |project, cx| { let mut write_guard = project.closed_breakpoints.write(); - for (file_path, mut breakpoint_rows) in serialized_workspace.breakpoints { - write_guard - .entry(Arc::from(file_path)) - .or_default() - .append(&mut breakpoint_rows); + for worktree in project.worktrees(cx) { + let (worktree_id, worktree_path) = + worktree.read_with(cx, |tree, _cx| (tree.id(), tree.abs_path())); + + if let Some(serialized_breakpoints) = + serialized_workspace.breakpoints.remove(&worktree_path) + { + for serialized_bp in serialized_breakpoints { + write_guard + .entry(ProjectPath { + worktree_id, + path: serialized_bp.path.clone(), + }) + .or_default() + .push(serialized_bp); + } + } } }) })?; + // Traverse the splits tree and add to things if let Some((group, active_pane, items)) = serialized_workspace .center_group .deserialize( From f3cb4a467b980d56e00765a71866fc425586f926 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 4 Aug 2024 00:42:18 -0400 Subject: [PATCH 173/650] Send breakpoints from unopened files to adapters & fix workspace serialization --- crates/dap/src/client.rs | 21 +++++-- crates/project/src/project.rs | 98 +++++++++++++---------------- crates/workspace/src/persistence.rs | 2 +- crates/workspace/src/workspace.rs | 2 + 4 files changed, 65 insertions(+), 58 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d336fc25edd715..348de5b425c857 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -534,14 +534,14 @@ impl DebugAdapterClient { pub async fn set_breakpoints( &self, - path: PathBuf, - breakpoints: Option>, + absolute_file_path: Arc, + breakpoints: Vec, ) -> Result { let adapter_data = self.config.request_args.clone().map(|c| c.args); self.request::(SetBreakpointsArguments { source: Source { - path: Some(String::from(path.to_string_lossy())), + path: Some(String::from(absolute_file_path.to_string_lossy())), name: None, source_reference: None, presentation_hint: None, @@ -550,7 +550,7 @@ impl DebugAdapterClient { adapter_data, checksums: None, }, - breakpoints, + breakpoints: Some(breakpoints), source_modified: None, lines: None, }) @@ -613,3 +613,16 @@ pub struct SerializedBreakpoint { pub position: u32, pub path: Arc, } + +impl SerializedBreakpoint { + pub fn to_source_breakpoint(&self) -> SourceBreakpoint { + SourceBreakpoint { + line: self.position as u64, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + } + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 767d2b430f75e2..f5e31931e3c538 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1188,10 +1188,14 @@ impl Project { continue; }; - all_breakpoints.entry(path.clone()).or_default().extend( + let Some(relative_path) = maybe!({ Some(buffer.project_path(cx)?.path) }) else { + continue; + }; + + all_breakpoints.entry(path).or_default().extend( breakpoints .into_iter() - .map(|bp| bp.to_serialized(buffer, path.clone())), + .map(|bp| bp.to_serialized(buffer, relative_path.clone())), ); } @@ -1220,43 +1224,18 @@ impl Project { client: Arc, cx: &mut ModelContext, ) -> Task> { - println!("All breakpoints {:?}", self.all_breakpoints(true, cx)); - println!("All breakpoints {:?}", self.all_breakpoints(false, cx)); - cx.spawn(|project, mut cx| async move { - // TODO: Send breakpoints from unopened files as well let task = project.update(&mut cx, |project, cx| { let mut tasks = Vec::new(); - for (buffer_id, breakpoints) in project.open_breakpoints.read().iter() { - let Some(buffer) = maybe!({ - let buffer = project.buffer_for_id(*buffer_id, cx)?; - Some(buffer.read(cx)) - }) else { - continue; - }; - - let res = maybe!({ - let project_path = buffer.project_path(cx)?; - let worktree = project.worktree_for_id(project_path.worktree_id, cx)?; - let path = worktree.read(cx).absolutize(&project_path.path).ok()?; - - Some((path, breakpoints)) - }); + for (abs_path, serialized_breakpoints) in project.all_breakpoints(true, cx) { + let source_breakpoints = serialized_breakpoints + .iter() + .map(|bp| bp.to_source_breakpoint()) + .collect::>(); - if let Some((path, breakpoints)) = res { - tasks.push( - client.set_breakpoints( - path.clone(), - Some( - breakpoints - .iter() - .map(|b| b.source_for_snapshot(&buffer.snapshot())) - .collect::>(), - ), - ), - ); - } + tasks + .push(client.set_breakpoints(abs_path.clone(), source_breakpoints.clone())); } try_join_all(tasks) @@ -1399,8 +1378,8 @@ impl Project { .or_default() .insert(Breakpoint { position: snapshot - .anchor_at(Point::new(serialized_bp.position, 0), Bias::Left), - }); + .anchor_at(Point::new(serialized_bp.position - 1, 0), Bias::Left), + }); // Serialized breakpoints start at index one so we shift when converting to open breakpoints } } } @@ -1420,9 +1399,20 @@ impl Project { } } - // TODO Anth: Add breakpoints that are in closed buffers + for (project_path, serialized_bp) in self.closed_breakpoints.read().iter() { + let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else { + continue; + }; + + let worktree_path = worktree.read(cx).abs_path(); + + result + .entry(worktree_path) + .or_default() + .extend(serialized_bp.iter().map(|bp| bp.clone())); + } + // TODO Anth: Add documentation - // TODO Anth: use SerializeBreakpoint Type as value in result hashmap result } @@ -1447,7 +1437,7 @@ impl Project { let buffer = buffer.read(cx); - let file_path = maybe!({ + let abs_file_path = maybe!({ let project_path = buffer.project_path(cx)?; let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let path = worktree.read(cx).absolutize(&project_path.path).ok()?; @@ -1455,30 +1445,30 @@ impl Project { Some(path) }); - let Some(file_path) = file_path else { + let Some(file_path) = abs_file_path else { return; }; let read_guard = self.open_breakpoints.read(); - let breakpoints_locations = read_guard.get(&buffer_id); + let breakpoints = read_guard.get(&buffer_id); let snapshot = buffer.snapshot(); - if let Some(breakpoints_locations) = breakpoints_locations { - let breakpoints_locations = Some( - breakpoints_locations - .iter() - .map(|bp| bp.source_for_snapshot(&snapshot)) - .collect(), - ); - + if let Some(breakpoints) = breakpoints { // TODO: Send correct value for sourceModified + for client in clients { - let bps = breakpoints_locations.clone(); let file_path = file_path.clone(); + let source_breakpoints = breakpoints + .iter() + .map(|bp| bp.source_for_snapshot(&snapshot)) + .collect::>(); + cx.background_executor() .spawn(async move { - client.set_breakpoints(file_path, bps).await?; + client + .set_breakpoints(Arc::from(file_path), source_breakpoints) + .await?; anyhow::Ok(()) }) @@ -2339,8 +2329,10 @@ impl Project { let buffer_breakpoints = write_guard.entry(buffer_id).or_default(); for serialized_bp in serialized_breakpoints { - let position = - snapshot.anchor_at(Point::new(serialized_bp.position, 0), Bias::Left); + // serialized breakpoints are start at index one and need to converted + // to index zero in order to display/work properly with open breakpoints + let position = snapshot + .anchor_at(Point::new(serialized_bp.position - 1, 0), Bias::Left); buffer_breakpoints.insert(Breakpoint { position }); } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 88af082eb04b41..f005dd3a9d1680 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -265,7 +265,7 @@ define_connection! { // CREATE TABLE breakpoints( // workspace_id: usize Foreign Key, // References workspace table // worktree_path: PathBuf, // Path of worktree that this breakpoint belong's too. Used to determine the absolute path of a breakpoint - // local_path: PathBuf, // References the file that the breakpoints belong too TODO Anth: rename to rel_path + // relative_path: PathBuf, // References the file that the breakpoints belong too // breakpoint_location: Vec, // A list of the locations of breakpoints // ) pub static ref DB: WorkspaceDb<()> = diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4e48df70f6ff80..d57cff8ec74290 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4024,6 +4024,8 @@ impl Workspace { .project .update(cx, |project, cx| project.serialize_breakpoints(cx)); + println!("{breakpoint_lines:?}"); + let center_group = build_serialized_pane_group(&self.center.root, cx); let docks = build_serialized_docks(self, cx); let window_bounds = Some(SerializedWindowBounds(cx.window_bounds())); From 4181c39224a64e76fe6f9f1fad800528051c6b16 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 4 Aug 2024 01:21:45 -0400 Subject: [PATCH 174/650] Delete println that was used for debugging --- crates/workspace/src/persistence.rs | 2 +- crates/workspace/src/workspace.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index f005dd3a9d1680..62fb9214583341 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -526,7 +526,7 @@ impl WorkspaceDb { map } Err(msg) => { - log::error!("{msg}"); + log::error!("Breakpoints query failed with msg: {msg}"); Default::default() } }; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d57cff8ec74290..4e48df70f6ff80 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4024,8 +4024,6 @@ impl Workspace { .project .update(cx, |project, cx| project.serialize_breakpoints(cx)); - println!("{breakpoint_lines:?}"); - let center_group = build_serialized_pane_group(&self.center.root, cx); let docks = build_serialized_docks(self, cx); let window_bounds = Some(SerializedWindowBounds(cx.window_bounds())); From 96bdeca2acf40db05cdddcda37e915d6bf0fcda6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 5 Aug 2024 21:24:01 +0200 Subject: [PATCH 175/650] Terminate thread when pane is closed/removed (#14) --- crates/debugger_ui/src/debugger_panel.rs | 89 +++++++++++++++--------- crates/workspace/src/pane.rs | 14 ++-- crates/workspace/src/workspace.rs | 5 +- 3 files changed, 63 insertions(+), 45 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 71b072f9bb076f..7c8cbb76a7d366 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -21,11 +21,11 @@ use std::{collections::HashMap, sync::Arc}; use task::DebugRequestType; use ui::prelude::*; use util::{merge_json_value_into, ResultExt}; -use workspace::Pane; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; +use workspace::{pane, Pane}; enum DebugCurrentRowHighlight {} @@ -67,44 +67,49 @@ impl DebugPanel { let project = workspace.project().clone(); - let _subscriptions = vec![cx.subscribe(&project, { - move |this: &mut Self, _, event, cx| match event { - project::Event::DebugClientEvent { payload, client_id } => { - let client = this.debug_client_by_id(*client_id, cx); + let _subscriptions = vec![ + cx.subscribe(&pane, Self::handle_pane_event), + cx.subscribe(&project, { + move |this: &mut Self, _, event, cx| match event { + project::Event::DebugClientEvent { payload, client_id } => { + let client = this.debug_client_by_id(*client_id, cx); - match payload { - Payload::Event(event) => { - Self::handle_debug_client_events(this, client, event, cx); - } - Payload::Request(request) => { - if StartDebugging::COMMAND == request.command { - Self::handle_start_debugging_request(this, client, request, cx) + match payload { + Payload::Event(event) => { + Self::handle_debug_client_events(this, client, event, cx); + } + Payload::Request(request) => { + if StartDebugging::COMMAND == request.command { + Self::handle_start_debugging_request( + this, client, request, cx, + ) .log_err(); + } } + _ => unreachable!(), } - _ => unreachable!(), } - } - project::Event::DebugClientStarted(client_id) => { - let client = this.debug_client_by_id(*client_id, cx); - cx.spawn(|_, _| async move { - client.initialize().await?; - - // send correct request based on adapter config - match client.config().request { - DebugRequestType::Launch => { - client.launch(client.request_args()).await + project::Event::DebugClientStarted(client_id) => { + let client = this.debug_client_by_id(*client_id, cx); + cx.spawn(|_, _| async move { + client.initialize().await?; + + // send correct request based on adapter config + match client.config().request { + DebugRequestType::Launch => { + client.launch(client.request_args()).await + } + DebugRequestType::Attach => { + client.attach(client.request_args()).await + } } - DebugRequestType::Attach => { - client.attach(client.request_args()).await - } - } - }) - .detach_and_log_err(cx); + }) + .detach_and_log_err(cx); + } + _ => {} } - _ => {} - } - })]; + }), + ]; Self { pane, @@ -140,6 +145,26 @@ impl DebugPanel { .unwrap() } + fn handle_pane_event( + &mut self, + _: View, + event: &pane::Event, + cx: &mut ViewContext, + ) { + if let pane::Event::RemovedItem { item } = event { + let thread_panel = item.downcast::().unwrap(); + + thread_panel.update(cx, |pane, cx| { + let thread_id = pane.thread_id(); + let client = pane.client().clone(); + cx.spawn( + |_, _| async move { client.terminate_threads(Some(vec![thread_id; 1])).await }, + ) + .detach_and_log_err(cx); + }); + }; + } + fn handle_start_debugging_request( this: &mut Self, client: Arc, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index b506709a801320..a204f76a2cdce4 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -169,8 +169,7 @@ pub enum Event { AddItem { item: Box }, ActivateItem { local: bool }, Remove, - RemoveItem { idx: usize }, - RemovedItem { item_id: EntityId }, + RemovedItem { item: Box }, Split(SplitDirection), ChangeItemTitle, Focus, @@ -190,10 +189,9 @@ impl fmt::Debug for Event { .field("local", local) .finish(), Event::Remove => f.write_str("Remove"), - Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(), - Event::RemovedItem { item_id } => f + Event::RemovedItem { item } => f .debug_struct("RemovedItem") - .field("item_id", item_id) + .field("item", &item.item_id()) .finish(), Event::Split(direction) => f .debug_struct("Split") @@ -1308,13 +1306,9 @@ impl Pane { } } - cx.emit(Event::RemoveItem { idx: item_index }); - let item = self.items.remove(item_index); - cx.emit(Event::RemovedItem { - item_id: item.item_id(), - }); + cx.emit(Event::RemovedItem { item: item.clone() }); if self.items.is_empty() { item.deactivated(cx); if close_pane_if_empty { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 367d3733e11696..65c24e816cf785 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2904,11 +2904,10 @@ impl Workspace { } self.update_window_edited(cx); } - pane::Event::RemoveItem { .. } => {} - pane::Event::RemovedItem { item_id } => { + pane::Event::RemovedItem { item } => { cx.emit(Event::ActiveItemChanged); self.update_window_edited(cx); - if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { + if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) { if entry.get().entity_id() == pane.entity_id() { entry.remove(); } From 49dd57d1f9b2df494485191bfdb4ae98b1e06850 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 5 Aug 2024 21:38:30 +0200 Subject: [PATCH 176/650] Add output for each thread (#15) --- crates/debugger_ui/src/debugger_panel.rs | 28 +- crates/debugger_ui/src/debugger_panel_item.rs | 288 +++++++++++++----- 2 files changed, 235 insertions(+), 81 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 7c8cbb76a7d366..c3d25691f57ae6 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -5,9 +5,9 @@ use dap::requests::{Disconnect, Request, Scopes, StackTrace, StartDebugging, Var use dap::transport::Payload; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, Scope, ScopesArguments, - StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, TerminatedEvent, - ThreadEvent, ThreadEventReason, Variable, VariablesArguments, + Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, OutputEvent, Scope, + ScopesArguments, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, + TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; use editor::Editor; use futures::future::try_join_all; @@ -29,10 +29,10 @@ use workspace::{pane, Pane}; enum DebugCurrentRowHighlight {} -#[derive(Debug)] pub enum DebugPanelEvent { Stopped((DebugAdapterClientId, StoppedEvent)), Thread((DebugAdapterClientId, ThreadEvent)), + Output((DebugAdapterClientId, OutputEvent)), } actions!(debug_panel, [ToggleFocus]); @@ -212,7 +212,7 @@ impl DebugPanel { Events::Exited(event) => Self::handle_exited_event(client, event, cx), Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx), Events::Thread(event) => Self::handle_thread_event(client, event, cx), - Events::Output(_) => {} + Events::Output(event) => Self::handle_output_event(client, event, cx), Events::Breakpoint(_) => {} Events::Module(_) => {} Events::LoadedSource(_) => {} @@ -503,6 +503,8 @@ impl DebugPanel { cx.emit(DebugPanelEvent::Stopped((client_id, event))); + cx.notify(); + if let Some(item) = this.pane.read(cx).active_item() { if let Some(pane) = item.downcast::() { let pane = pane.read(cx); @@ -545,6 +547,8 @@ impl DebugPanel { } else { client.update_thread_state_status(thread_id, ThreadStatus::Ended); + cx.notify(); + // TODO: we want to figure out for witch clients/threads we should remove the highlights cx.spawn({ let client = client.clone(); @@ -567,12 +571,14 @@ impl DebugPanel { _: &ExitedEvent, cx: &mut ViewContext, ) { - cx.spawn(|_, _| async move { + cx.spawn(|this, mut cx| async move { for thread_state in client.thread_states().values_mut() { thread_state.status = ThreadStatus::Exited; } + + this.update(&mut cx, |_, cx| cx.notify()) }) - .detach(); + .detach_and_log_err(cx); } fn handle_terminated_event( @@ -608,6 +614,14 @@ impl DebugPanel { }) .detach_and_log_err(cx); } + + fn handle_output_event( + client: Arc, + event: &OutputEvent, + cx: &mut ViewContext, + ) { + cx.emit(DebugPanelEvent::Output((client.id(), event.clone()))); + } } impl EventEmitter for DebugPanel {} diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 8b3d68348e6eb9..65cfa6f8c6dc17 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,7 +1,10 @@ use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; use anyhow::Result; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; -use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable}; +use dap::{ + OutputEvent, OutputEventCategory, Scope, StackFrame, StoppedEvent, ThreadEvent, Variable, +}; +use editor::Editor; use gpui::{ actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, ListState, Subscription, View, WeakView, @@ -11,6 +14,13 @@ use ui::WindowContext; use ui::{prelude::*, Tooltip}; use workspace::item::{Item, ItemEvent}; +#[derive(PartialEq, Eq)] +enum ThreadItem { + Variables, + Console, + Output, +} + pub struct DebugPanelItem { thread_id: u64, focus_handle: FocusHandle, @@ -18,6 +28,8 @@ pub struct DebugPanelItem { client: Arc, _subscriptions: Vec, current_stack_frame_id: Option, + active_thread_item: ThreadItem, + output_editor: View, } actions!( @@ -54,17 +66,36 @@ impl DebugPanelItem { DebugPanelEvent::Thread((client_id, event)) => { Self::handle_thread_event(this, client_id, event, cx) } + DebugPanelEvent::Output((client_id, event)) => { + Self::handle_output_event(this, client_id, event, cx) + } }; } })]; + let output_editor = cx.new_view(|cx| { + let mut editor = Editor::multi_line(cx); + editor.set_placeholder_text("Debug adapter and script output", cx); + editor.set_read_only(true); + editor.set_show_inline_completions(false); + editor.set_searchable(true); + editor.set_auto_replace_emoji_shortcode(false); + editor.set_show_indent_guides(false, cx); + editor.set_autoindent(false); + editor.set_show_gutter(false, cx); + editor.set_show_line_numbers(false, cx); + editor + }); + Self { client, thread_id, focus_handle, + output_editor, _subscriptions, stack_frame_list, current_stack_frame_id: None, + active_thread_item: ThreadItem::Variables, } } @@ -103,6 +134,35 @@ impl DebugPanelItem { // TODO: handle thread event } + + fn handle_output_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + event: &OutputEvent, + cx: &mut ViewContext, + ) { + if Self::should_skip_event(this, client_id, this.thread_id) { + return; + } + + if event + .category + .as_ref() + .map(|c| *c == OutputEventCategory::Telemetry) + .unwrap_or(false) + { + return; + } + + this.output_editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.move_to_end(&editor::actions::MoveToEnd, cx); + editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); + editor.set_read_only(true); + + cx.notify(); + }); + } } impl EventEmitter for DebugPanelItem {} @@ -172,9 +232,8 @@ impl DebugPanelItem { fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { v_flex() - .w_1_3() .gap_3() - .h_full() + .size_full() .child(list(self.stack_frame_list.clone()).size_full()) .into_any() } @@ -428,8 +487,9 @@ impl DebugPanelItem { impl Render for DebugPanelItem { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let thread_status = self.current_thread_state().status; + let active_thread_item = &self.active_thread_item; - v_flex() + h_flex() .key_context("DebugPanelItem") .track_focus(&self.focus_handle) .capture_action(cx.listener(Self::handle_continue_action)) @@ -444,91 +504,171 @@ impl Render for DebugPanelItem { .size_full() .items_start() .child( - h_flex() - .gap_2() - .map(|this| { - if thread_status == ThreadStatus::Running { - this.child( - IconButton::new("debug-pause", IconName::DebugPause) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Pause))), + v_flex() + .size_full() + .items_start() + .child( + h_flex() + .py_1() + .gap_2() + .map(|this| { + if thread_status == ThreadStatus::Running { + this.child( + IconButton::new("debug-pause", IconName::DebugPause) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(Pause)) + })) + .tooltip(move |cx| Tooltip::text("Pause program", cx)), ) - .tooltip(move |cx| Tooltip::text("Pause program", cx)), - ) - } else { - this.child( - IconButton::new("debug-continue", IconName::DebugContinue) + } else { + this.child( + IconButton::new("debug-continue", IconName::DebugContinue) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(Continue)) + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |cx| { + Tooltip::text("Continue program", cx) + }), + ) + } + }) + .child( + IconButton::new("debug-step-over", IconName::DebugStepOver) .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(Continue)) + cx.dispatch_action(Box::new(StepOver)) })) .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Continue program", cx)), + .tooltip(move |cx| Tooltip::text("Step over", cx)), ) - } - }) - .child( - IconButton::new("debug-step-over", IconName::DebugStepOver) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOver))), + .child( + IconButton::new("debug-step-in", IconName::DebugStepInto) + .on_click( + cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(StepIn)) + }), + ) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |cx| Tooltip::text("Step in", cx)), ) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Step over", cx)), - ) - .child( - IconButton::new("debug-step-in", IconName::DebugStepInto) - .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepIn)))) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Step in", cx)), - ) - .child( - IconButton::new("debug-step-out", IconName::DebugStepOut) - .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOut)))) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Step out", cx)), - ) - .child( - IconButton::new("debug-restart", IconName::DebugRestart) - .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Restart)))) - .disabled( - !self - .client - .capabilities() - .supports_restart_request - .unwrap_or_default() - || thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, + .child( + IconButton::new("debug-step-out", IconName::DebugStepOut) + .on_click( + cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(StepOut)) + }), + ) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |cx| Tooltip::text("Step out", cx)), ) - .tooltip(move |cx| Tooltip::text("Restart", cx)), - ) - .child( - IconButton::new("debug-stop", IconName::DebugStop) - .on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop)))) - .disabled( - thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, + .child( + IconButton::new("debug-restart", IconName::DebugRestart) + .on_click( + cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(Restart)) + }), + ) + .disabled( + !self + .client + .capabilities() + .supports_restart_request + .unwrap_or_default() + || thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, + ) + .tooltip(move |cx| Tooltip::text("Restart", cx)), ) - .tooltip(move |cx| Tooltip::text("Stop", cx)), + .child( + IconButton::new("debug-stop", IconName::DebugStop) + .on_click( + cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop))), + ) + .disabled( + thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, + ) + .tooltip(move |cx| Tooltip::text("Stop", cx)), + ) + .child( + IconButton::new("debug-disconnect", IconName::DebugDisconnect) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(Disconnect)) + })) + .disabled( + thread_status == ThreadStatus::Exited + || thread_status == ThreadStatus::Ended, + ) + .tooltip(move |cx| Tooltip::text("Disconnect", cx)), + ), ) .child( - IconButton::new("debug-disconnect", IconName::DebugDisconnect) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Disconnect))), - ) - .disabled( - thread_status == ThreadStatus::Exited - || thread_status == ThreadStatus::Ended, - ) - .tooltip(move |cx| Tooltip::text("Disconnect", cx)), + h_flex() + .size_full() + .items_start() + .p_1() + .gap_4() + .child(self.render_stack_frames(cx)), ), ) .child( - h_flex() + v_flex() .size_full() .items_start() - .p_1() - .gap_4() - .child(self.render_stack_frames(cx)) - .child(self.render_scopes(cx)), + .child( + h_flex() + .child( + div() + .id("variables") + .px_2() + .py_1() + .cursor_pointer() + .border_b_2() + .when(*active_thread_item == ThreadItem::Variables, |this| { + this.border_color(cx.theme().colors().border) + }) + .child(Label::new("Variables")) + .on_click(cx.listener(|this, _, _| { + this.active_thread_item = ThreadItem::Variables; + })), + ) + .child( + div() + .id("console") + .px_2() + .py_1() + .cursor_pointer() + .border_b_2() + .when(*active_thread_item == ThreadItem::Console, |this| { + this.border_color(cx.theme().colors().border) + }) + .child(Label::new("Console")) + .on_click(cx.listener(|this, _, _| { + this.active_thread_item = ThreadItem::Console; + })), + ) + .child( + div() + .id("output") + .px_2() + .py_1() + .cursor_pointer() + .border_b_2() + .when(*active_thread_item == ThreadItem::Output, |this| { + this.border_color(cx.theme().colors().border) + }) + .child(Label::new("Output")) + .on_click(cx.listener(|this, _, _| { + this.active_thread_item = ThreadItem::Output; + })), + ), + ) + .when(*active_thread_item == ThreadItem::Variables, |this| { + this.child(self.render_scopes(cx)) + }) + .when(*active_thread_item == ThreadItem::Output, |this| { + this.child(self.output_editor.clone()) + }), ) .into_any() } From f4af5afe62c61543f43a1d51a71a465977f34b5a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 5 Aug 2024 22:50:11 +0200 Subject: [PATCH 177/650] Fix merge conflicts --- crates/debugger_ui/src/debugger_panel.rs | 1 + crates/workspace/src/pane.rs | 2 ++ crates/workspace/src/workspace.rs | 1 + 3 files changed, 4 insertions(+) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c3d25691f57ae6..9acd3641e6d7f5 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -68,6 +68,7 @@ impl DebugPanel { let project = workspace.project().clone(); let _subscriptions = vec![ + cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe(&pane, Self::handle_pane_event), cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a204f76a2cdce4..d8b6895d5a5d5b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -169,6 +169,7 @@ pub enum Event { AddItem { item: Box }, ActivateItem { local: bool }, Remove, + RemoveItem { idx: usize }, RemovedItem { item: Box }, Split(SplitDirection), ChangeItemTitle, @@ -189,6 +190,7 @@ impl fmt::Debug for Event { .field("local", local) .finish(), Event::Remove => f.write_str("Remove"), + Event::RemoveItem { idx } => f.debug_struct("RemoveItem").field("idx", idx).finish(), Event::RemovedItem { item } => f .debug_struct("RemovedItem") .field("item", &item.item_id()) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 65c24e816cf785..888ee7d3723330 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2904,6 +2904,7 @@ impl Workspace { } self.update_window_edited(cx); } + pane::Event::RemoveItem { .. } => {} pane::Event::RemovedItem { item } => { cx.emit(Event::ActiveItemChanged); self.update_window_edited(cx); From a4cc28f48096c66534cf4d5f89151ee9a5a537e9 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 5 Aug 2024 17:31:23 -0400 Subject: [PATCH 178/650] Fix bug where toggle breakpoint action wouldn't untoggle --- crates/editor/src/editor.rs | 6 +++++- crates/editor/src/element.rs | 31 +++++++++++++++++++------------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9b92f7f097db62..bff0ed28e31df3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5998,11 +5998,15 @@ impl Editor { pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { let cursor_position: Point = self.selections.newest(cx).head(); + // We Set the column position to zero so this function interacts correctly + // between calls by clicking on the gutter & using an action to toggle a + // breakpoint. Otherwise, toggling a breakpoint through an action wouldn't + // untoggle a breakpoint that was added through clicking on the gutter let breakpoint_position = self .snapshot(cx) .display_snapshot .buffer_snapshot - .anchor_before(cursor_position); + .anchor_before(Point::new(cursor_position.row, 0)); self.toggle_breakpoint_at_row(breakpoint_position, cx); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a4d19eee29ad71..ff72be6215939a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1586,6 +1586,7 @@ impl EditorElement { let buffer_id = active_buffer.read(cx).remote_id(); let read_guard = breakpoints.read(); + let mut has_placed_cursor_breakpoint = false; let mut breakpoints_to_render = if let Some(breakpoint_set) = read_guard.get(&buffer_id) { @@ -1602,7 +1603,17 @@ impl EditorElement { return None; } - let button = editor.render_breakpoint(breakpoint.position, point.row(), cx); + let mut button = + editor.render_breakpoint(breakpoint.position, point.row(), cx); + + if editor + .gutter_breakpoint_indicator + .and_then(|p| Some(p.row())) + == Some(point.row()) + { + button = button.icon_color(Color::Conflict); + has_placed_cursor_breakpoint = true; + } let button = prepaint_gutter_button( button, @@ -1625,22 +1636,15 @@ impl EditorElement { // See if a user is hovered over a gutter line & if they are display // a breakpoint indicator that they can click to add a breakpoint - // TODO: We should figure out a way to display this side by side with + // TODO Anth: We should figure out a way to display this side by side with // the code action button. They currently overlap if let Some(gutter_breakpoint) = editor.gutter_breakpoint_indicator { let gutter_anchor = snapshot.display_point_to_anchor(gutter_breakpoint, Bias::Left); - let button = IconButton::new("gutter_breakpoint_indicator", ui::IconName::Play) - .icon_size(IconSize::XSmall) - .size(ui::ButtonSize::None) - .icon_color(Color::Hint) - .on_click(cx.listener(move |editor, _e, cx| { - editor.focus(cx); - editor.toggle_breakpoint_at_row(gutter_anchor, cx) //TODO handle folded - })); + let button = editor.render_breakpoint(gutter_anchor, gutter_breakpoint.row(), cx); let button = prepaint_gutter_button( - button, + button.icon_color(Color::Hint), gutter_breakpoint.row(), line_height, gutter_dimensions, @@ -1650,7 +1654,9 @@ impl EditorElement { cx, ); - breakpoints_to_render.push(button); + if !has_placed_cursor_breakpoint { + breakpoints_to_render.push(button); + } } breakpoints_to_render @@ -4206,6 +4212,7 @@ fn prepaint_gutter_button( cx: &mut WindowContext<'_>, ) -> AnyElement { let mut button = button.into_any_element(); + let available_space = size( AvailableSpace::MinContent, AvailableSpace::Definite(line_height), From c92ecc690fb6502b589f8cfb491591f59088b4e2 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 6 Aug 2024 16:34:35 -0400 Subject: [PATCH 179/650] Handle case where Code action indicator interlaps with breakpoint Code action's that overlap with breakpoints will now show as red. Still need to add a code action to toggle a breakpoint & handle overlaps with code runners --- crates/dap/src/client.rs | 4 + crates/editor/src/display_map.rs | 2 +- crates/editor/src/display_map/block_map.rs | 2 +- crates/editor/src/editor.rs | 47 +++++++- crates/editor/src/element.rs | 125 ++++++++------------- 5 files changed, 97 insertions(+), 83 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 348de5b425c857..45d8005f582d27 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -583,6 +583,10 @@ impl Breakpoint { } } + pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { + buffer.summary_for_anchor::(&self.position.text_anchor) + } + pub fn source_for_snapshot(&self, snapshot: &BufferSnapshot) -> SourceBreakpoint { SourceBreakpoint { line: (snapshot diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 3b0d3b89ebe483..fc26fa21dba554 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1047,7 +1047,7 @@ impl DisplaySnapshot { } } -#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] +#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)] pub struct DisplayPoint(BlockPoint); impl Debug for DisplayPoint { diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 5302bfa73b6061..94346156695eb2 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -62,7 +62,7 @@ impl Into for CustomBlockId { } } -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash)] pub struct BlockPoint(pub Point); #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bff0ed28e31df3..d37214e36384e9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5126,14 +5126,21 @@ impl Editor { _style: &EditorStyle, row: DisplayRow, is_active: bool, + overlaps_breakpoint: bool, cx: &mut ViewContext, ) -> Option { if self.available_code_actions.is_some() { + let color = if overlaps_breakpoint { + Color::Error + } else { + Color::Muted + }; + Some( IconButton::new("code_actions_indicator", ui::IconName::Bolt) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) + .icon_color(color) .selected(is_active) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); @@ -5161,16 +5168,52 @@ impl Editor { } } + fn active_breakpoint_points(&mut self, cx: &mut ViewContext) -> HashSet { + let mut breakpoint_display_points = HashSet::default(); + + let Some(buffer) = self.buffer.read(cx).as_singleton() else { + return breakpoint_display_points; + }; + + let Some(opened_breakpoints) = self.opened_breakpoints.clone() else { + return breakpoint_display_points; + }; + + let snapshot = self.snapshot(cx); + let buffer = buffer.read(cx); + + if let Some(breakpoints) = opened_breakpoints.read().get(&buffer.remote_id()) { + for breakpoint in breakpoints { + breakpoint_display_points.insert(breakpoint.position.to_display_point(&snapshot)); + } + } + + if let Some(gutter_point) = self.gutter_breakpoint_indicator.clone() { + breakpoint_display_points.insert(gutter_point); + } + + breakpoint_display_points + } + fn render_breakpoint( &self, position: Anchor, row: DisplayRow, cx: &mut ViewContext, ) -> IconButton { + let color = if self + .gutter_breakpoint_indicator + .is_some_and(|gutter_bp| gutter_bp.row() == row) + { + Color::Hint + } else { + Color::Error + }; + IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play) .icon_size(IconSize::XSmall) .size(ui::ButtonSize::None) - .icon_color(Color::Error) + .icon_color(color) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); editor.toggle_breakpoint_at_row(position, cx) //TODO handle folded diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ff72be6215939a..fa0fa96d7c9b06 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -27,7 +27,7 @@ use crate::{ RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, }; use client::ParticipantIndex; -use collections::{BTreeMap, HashMap}; +use collections::{BTreeMap, HashMap, HashSet}; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; use gpui::Subscription; use gpui::{ @@ -1573,93 +1573,42 @@ impl EditorElement { gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, snapshot: &EditorSnapshot, + breakpoints: HashSet, cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { - let Some(breakpoints) = &editor.opened_breakpoints else { + if editor.opened_breakpoints.is_none() { return vec![]; }; - let Some(active_buffer) = editor.buffer().read(cx).as_singleton() else { - return vec![]; - }; - - let buffer_id = active_buffer.read(cx).remote_id(); - let read_guard = breakpoints.read(); - let mut has_placed_cursor_breakpoint = false; - - let mut breakpoints_to_render = if let Some(breakpoint_set) = read_guard.get(&buffer_id) - { - breakpoint_set - .iter() - .filter_map(|breakpoint| { - let point = breakpoint - .position - .to_display_point(&snapshot.display_snapshot); - - let row = MultiBufferRow { 0: point.row().0 }; - - if snapshot.is_line_folded(row) { - return None; - } + breakpoints + .iter() + .filter_map(|point| { + let row = MultiBufferRow { 0: point.row().0 }; - let mut button = - editor.render_breakpoint(breakpoint.position, point.row(), cx); + if snapshot.is_line_folded(row) { + return None; + } - if editor - .gutter_breakpoint_indicator - .and_then(|p| Some(p.row())) - == Some(point.row()) - { - button = button.icon_color(Color::Conflict); - has_placed_cursor_breakpoint = true; - } + let position = snapshot + .display_snapshot + .display_point_to_anchor(point.clone(), Bias::Left); - let button = prepaint_gutter_button( - button, - point.row(), - line_height, - gutter_dimensions, - scroll_pixel_position, - gutter_hitbox, - rows_with_hunk_bounds, - cx, - ); - Some(button) - }) - .collect_vec() - } else { - vec![] - }; + let button = editor.render_breakpoint(position, point.row(), cx); - drop(read_guard); - - // See if a user is hovered over a gutter line & if they are display - // a breakpoint indicator that they can click to add a breakpoint - // TODO Anth: We should figure out a way to display this side by side with - // the code action button. They currently overlap - if let Some(gutter_breakpoint) = editor.gutter_breakpoint_indicator { - let gutter_anchor = snapshot.display_point_to_anchor(gutter_breakpoint, Bias::Left); - - let button = editor.render_breakpoint(gutter_anchor, gutter_breakpoint.row(), cx); - - let button = prepaint_gutter_button( - button.icon_color(Color::Hint), - gutter_breakpoint.row(), - line_height, - gutter_dimensions, - scroll_pixel_position, - gutter_hitbox, - rows_with_hunk_bounds, - cx, - ); - - if !has_placed_cursor_breakpoint { - breakpoints_to_render.push(button); - } - } - - breakpoints_to_render + let button = prepaint_gutter_button( + button, + point.row(), + line_height, + gutter_dimensions, + scroll_pixel_position, + gutter_hitbox, + rows_with_hunk_bounds, + cx, + ); + Some(button) + }) + .collect_vec() }) } @@ -1732,6 +1681,7 @@ impl EditorElement { gutter_dimensions: &GutterDimensions, gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, + overlaps_breakpoint: bool, cx: &mut WindowContext, ) -> Option { let mut active = false; @@ -1745,7 +1695,13 @@ impl EditorElement { { active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row); }; - button = editor.render_code_actions_indicator(&self.style, row, active, cx); + button = editor.render_code_actions_indicator( + &self.style, + row, + active, + overlaps_breakpoint, + cx, + ); }); let button = prepaint_gutter_button( @@ -5006,6 +4962,10 @@ impl Element for EditorElement { cx.set_view_id(self.editor.entity_id()); cx.set_focus_handle(&focus_handle); + let mut breakpoint_lines = self + .editor + .update(cx, |editor, cx| editor.active_breakpoint_points(cx)); + let rem_size = self.rem_size(cx); cx.with_rem_size(rem_size, |cx| { cx.with_text_style(Some(text_style), |cx| { @@ -5489,6 +5449,11 @@ impl Element for EditorElement { .contains_key(&(buffer_id, row)); if !has_test_indicator { + let breakpoint_point = + DisplayPoint::new(newest_selection_head.row(), 0); + let overlaps_breakpoint = + breakpoint_lines.remove(&breakpoint_point); + code_actions_indicator = self .layout_code_actions_indicator( line_height, @@ -5497,6 +5462,7 @@ impl Element for EditorElement { &gutter_dimensions, &gutter_hitbox, &rows_with_hunk_bounds, + overlaps_breakpoint, cx, ); } @@ -5513,6 +5479,7 @@ impl Element for EditorElement { &gutter_hitbox, &rows_with_hunk_bounds, &snapshot, + breakpoint_lines, cx, ); From 1ad8ed77e4dda920d0fefcf96d2007d19ba88c05 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 6 Aug 2024 20:39:36 -0400 Subject: [PATCH 180/650] Show a breakpoint on line with code action while code action symbol is loading I also handle overlaps with code test buttons as well --- crates/editor/src/editor.rs | 24 +++++------ crates/editor/src/element.rs | 80 ++++++++++++++++++++++++------------ 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d37214e36384e9..19ce206fa04b85 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5126,21 +5126,14 @@ impl Editor { _style: &EditorStyle, row: DisplayRow, is_active: bool, - overlaps_breakpoint: bool, cx: &mut ViewContext, ) -> Option { if self.available_code_actions.is_some() { - let color = if overlaps_breakpoint { - Color::Error - } else { - Color::Muted - }; - Some( IconButton::new("code_actions_indicator", ui::IconName::Bolt) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) - .icon_color(color) + .icon_color(Color::Muted) .selected(is_active) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); @@ -5216,7 +5209,7 @@ impl Editor { .icon_color(color) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); - editor.toggle_breakpoint_at_row(position, cx) //TODO handle folded + editor.toggle_breakpoint_at_anchor(position, cx) //TODO handle folded })) } @@ -5225,12 +5218,19 @@ impl Editor { _style: &EditorStyle, is_active: bool, row: DisplayRow, + overlaps_breakpoint: bool, cx: &mut ViewContext, ) -> IconButton { + let color = if overlaps_breakpoint { + Color::Error + } else { + Color::Muted + }; + IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) + .icon_color(color) .selected(is_active) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); @@ -6051,10 +6051,10 @@ impl Editor { .buffer_snapshot .anchor_before(Point::new(cursor_position.row, 0)); - self.toggle_breakpoint_at_row(breakpoint_position, cx); + self.toggle_breakpoint_at_anchor(breakpoint_position, cx); } - pub fn toggle_breakpoint_at_row( + pub fn toggle_breakpoint_at_anchor( &mut self, breakpoint_position: Anchor, cx: &mut ViewContext, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fa0fa96d7c9b06..60f01351f62769 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1621,6 +1621,7 @@ impl EditorElement { gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, snapshot: &EditorSnapshot, + breakpoints: &mut HashSet, cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { @@ -1649,10 +1650,13 @@ impl EditorElement { return None; } let display_row = multibuffer_point.to_display_point(snapshot).row(); + let display_point = DisplayPoint::new(display_row, 0); + let button = editor.render_run_indicator( &self.style, Some(display_row) == active_task_indicator_row, display_row, + breakpoints.remove(&display_point), cx, ); @@ -1681,7 +1685,7 @@ impl EditorElement { gutter_dimensions: &GutterDimensions, gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, - overlaps_breakpoint: bool, + breakpoint_points: &mut HashSet, cx: &mut WindowContext, ) -> Option { let mut active = false; @@ -1695,17 +1699,21 @@ impl EditorElement { { active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row); }; - button = editor.render_code_actions_indicator( - &self.style, - row, - active, - overlaps_breakpoint, - cx, - ); + button = editor.render_code_actions_indicator(&self.style, row, active, cx); }); + let Some(button) = button else { + return None; + }; + + let button = if breakpoint_points.remove(&DisplayPoint::new(row, 0)) { + button.icon_color(Color::Error) + } else { + button + }; + let button = prepaint_gutter_button( - button?, + button, row, line_height, gutter_dimensions, @@ -1786,6 +1794,7 @@ impl EditorElement { active_rows: &BTreeMap, newest_selection_head: Option, snapshot: &EditorSnapshot, + breakpoint_rows: HashSet, cx: &mut WindowContext, ) -> Vec> { let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| { @@ -1825,7 +1834,9 @@ impl EditorElement { .map(|(ix, multibuffer_row)| { let multibuffer_row = multibuffer_row?; let display_row = DisplayRow(rows.start.0 + ix as u32); - let color = if active_rows.contains_key(&display_row) { + let color = if breakpoint_rows.contains(&display_row) { + Hsla::red() + } else if active_rows.contains_key(&display_row) { cx.theme().colors().editor_active_line_number } else { cx.theme().colors().editor_line_number @@ -5137,12 +5148,29 @@ impl Element for EditorElement { cx, ); + let gutter_breakpoint_position = + self.editor.read(cx).gutter_breakpoint_indicator; + + let breakpoint_rows = breakpoint_lines + .iter() + .filter_map(|display_point| { + if gutter_breakpoint_position + .is_some_and(|point| &point == display_point) + { + None + } else { + Some(display_point.row()) + } + }) + .collect(); + let line_numbers = self.layout_line_numbers( start_row..end_row, buffer_rows.iter().copied(), &active_rows, newest_selection_head, &snapshot, + breakpoint_rows, cx, ); @@ -5449,11 +5477,6 @@ impl Element for EditorElement { .contains_key(&(buffer_id, row)); if !has_test_indicator { - let breakpoint_point = - DisplayPoint::new(newest_selection_head.row(), 0); - let overlaps_breakpoint = - breakpoint_lines.remove(&breakpoint_point); - code_actions_indicator = self .layout_code_actions_indicator( line_height, @@ -5462,7 +5485,7 @@ impl Element for EditorElement { &gutter_dimensions, &gutter_hitbox, &rows_with_hunk_bounds, - overlaps_breakpoint, + &mut breakpoint_lines, cx, ); } @@ -5472,17 +5495,6 @@ impl Element for EditorElement { } } - let breakpoints = self.layout_breakpoints( - line_height, - scroll_pixel_position, - &gutter_dimensions, - &gutter_hitbox, - &rows_with_hunk_bounds, - &snapshot, - breakpoint_lines, - cx, - ); - let test_indicators = if gutter_settings.runnables { self.layout_run_indicators( line_height, @@ -5491,12 +5503,24 @@ impl Element for EditorElement { &gutter_hitbox, &rows_with_hunk_bounds, &snapshot, + &mut breakpoint_lines, cx, ) } else { Vec::new() }; + let breakpoints = self.layout_breakpoints( + line_height, + scroll_pixel_position, + &gutter_dimensions, + &gutter_hitbox, + &rows_with_hunk_bounds, + &snapshot, + breakpoint_lines, + cx, + ); + let close_indicators = self.layout_hunk_diff_close_indicators( line_height, scroll_pixel_position, @@ -6284,6 +6308,7 @@ mod tests { let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); let element = EditorElement::new(&editor, style); let snapshot = window.update(cx, |editor, cx| editor.snapshot(cx)).unwrap(); + let breakpoint_rows = HashSet::default(); let layouts = cx .update_window(*window, |_, cx| { @@ -6293,6 +6318,7 @@ mod tests { &Default::default(), Some(DisplayPoint::new(DisplayRow(0), 0)), &snapshot, + breakpoint_rows, cx, ) }) From 111a0dc53901509ae17c4e83c40a9d5573837aaa Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 6 Aug 2024 22:24:01 -0400 Subject: [PATCH 181/650] Get project::has_active_debuggers to return true if a debugger is running instead of starting We shouldn't consider a debugger active until it's running because it may fail during it's starting phase. Also, this check is used determine if we should update a file's breakpoints when one is toggled. --- crates/project/src/project.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f5e31931e3c538..33f39a03fc3602 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1250,7 +1250,7 @@ impl Project { pub fn has_active_debugger(&self) -> bool { self.debug_adapters .values() - .any(|c| matches!(c, DebugAdapterClientState::Starting(_))) + .any(|c| matches!(c, DebugAdapterClientState::Running(_))) } pub fn start_debug_adapter_client_from_task( @@ -1455,7 +1455,7 @@ impl Project { let snapshot = buffer.snapshot(); if let Some(breakpoints) = breakpoints { - // TODO: Send correct value for sourceModified + // TODO debugger: Send correct value for sourceModified for client in clients { let file_path = file_path.clone(); From 5a08fc2033065bf627b644417ea09f1519b2b5ce Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 6 Aug 2024 22:48:28 -0400 Subject: [PATCH 182/650] Fix breakpoint line number not being red when cursor is on it & clippy warnings --- crates/dap/src/client.rs | 1 + crates/editor/src/editor.rs | 4 ---- crates/editor/src/element.rs | 24 +++++++++++++----------- crates/workspace/src/persistence.rs | 24 +++++++++++++----------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 45d8005f582d27..aa6a0335572cc7 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -91,6 +91,7 @@ impl DebugAdapterClient { /// - `args`: Arguments of the command that starts the debugger /// - `cwd`: The absolute path of the project that is being debugged /// - `cx`: The context that the new client belongs too + #[allow(clippy::too_many_arguments)] pub async fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 19ce206fa04b85..e1204071b4849c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5181,10 +5181,6 @@ impl Editor { } } - if let Some(gutter_point) = self.gutter_breakpoint_indicator.clone() { - breakpoint_display_points.insert(gutter_point); - } - breakpoint_display_points } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 60f01351f62769..f8c06faaa86447 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1592,7 +1592,7 @@ impl EditorElement { let position = snapshot .display_snapshot - .display_point_to_anchor(point.clone(), Bias::Left); + .display_point_to_anchor(*point, Bias::Left); let button = editor.render_breakpoint(position, point.row(), cx); @@ -1787,6 +1787,7 @@ impl EditorElement { relative_rows } + #[allow(clippy::too_many_arguments)] fn layout_line_numbers( &self, rows: Range, @@ -5148,22 +5149,23 @@ impl Element for EditorElement { cx, ); - let gutter_breakpoint_position = + let gutter_breakpoint_indicator = self.editor.read(cx).gutter_breakpoint_indicator; let breakpoint_rows = breakpoint_lines .iter() - .filter_map(|display_point| { - if gutter_breakpoint_position - .is_some_and(|point| &point == display_point) - { - None - } else { - Some(display_point.row()) - } - }) + .map(|display_point| display_point.row()) .collect(); + // We want all lines with breakpoint's to have their number's painted + // red & we still want to render a grey breakpoint for the gutter + // indicator so we add that in after creating breakpoint_rows for layout line nums + // Otherwise, when a cursor is on a line number it will always be white even + // if that line has a breakpoint + if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator { + breakpoint_lines.insert(gutter_breakpoint_point); + } + let line_numbers = self.layout_line_numbers( start_row..end_row, buffer_rows.iter().copied(), diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 62fb9214583341..584bb497e3415d 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -145,7 +145,9 @@ pub struct Breakpoint { pub position: u32, } +/// This struct is used to implment traits on Vec #[derive(Debug)] +#[allow(dead_code)] struct Breakpoints(Vec); impl sqlez::bindable::StaticColumnCount for Breakpoint {} @@ -1286,7 +1288,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; let workspace_2 = SerializedWorkspace { @@ -1298,7 +1300,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -1402,7 +1404,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; db.save_workspace(workspace.clone()).await; @@ -1436,7 +1438,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; let mut workspace_2 = SerializedWorkspace { @@ -1448,7 +1450,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -1490,7 +1492,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; db.save_workspace(workspace_3.clone()).await; @@ -1526,7 +1528,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-1".to_owned()), - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; let workspace_2 = SerializedWorkspace { @@ -1538,7 +1540,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-1".to_owned()), - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; let workspace_3 = SerializedWorkspace { @@ -1550,7 +1552,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-2".to_owned()), - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; let workspace_4 = SerializedWorkspace { @@ -1562,7 +1564,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: Default::deafult(), + breakpoints: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -1600,7 +1602,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, - breakpoints: Default::deafult(), + breakpoints: Default::default(), } } From 7d243ac274a804753b26f7dbb43881dc491bb9f5 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 7 Aug 2024 15:26:34 -0400 Subject: [PATCH 183/650] Get breakpoints to display in the correct positions within multi buffers Remco Smits --- crates/editor/src/editor.rs | 59 ++++++++++++++++++++++++----- crates/workspace/src/persistence.rs | 2 +- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e1204071b4849c..63db06da6d53ba 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5163,22 +5163,63 @@ impl Editor { fn active_breakpoint_points(&mut self, cx: &mut ViewContext) -> HashSet { let mut breakpoint_display_points = HashSet::default(); + let snapshot = self.snapshot(cx); - let Some(buffer) = self.buffer.read(cx).as_singleton() else { + let Some(opened_breakpoints) = self.opened_breakpoints.clone() else { return breakpoint_display_points; }; - let Some(opened_breakpoints) = self.opened_breakpoints.clone() else { + let opened_breakpoints = opened_breakpoints.read(); + + if let Some(buffer) = self.buffer.read(cx).as_singleton() { + let buffer = buffer.read(cx); + + if let Some(breakpoints) = opened_breakpoints.get(&buffer.remote_id()) { + for breakpoint in breakpoints { + breakpoint_display_points + .insert(breakpoint.position.to_display_point(&snapshot)); + } + }; + return breakpoint_display_points; - }; + } - let snapshot = self.snapshot(cx); - let buffer = buffer.read(cx); + let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; - if let Some(breakpoints) = opened_breakpoints.read().get(&buffer.remote_id()) { - for breakpoint in breakpoints { - breakpoint_display_points.insert(breakpoint.position.to_display_point(&snapshot)); - } + for excerpt_boundery in + multi_buffer_snapshot.excerpt_boundaries_in_range(Point::new(0, 0)..) + { + let info = excerpt_boundery.next.as_ref(); + + if let Some(info) = info { + let Some(range) = multi_buffer_snapshot.range_for_excerpt::(info.id) else { + continue; + }; + + let excerpt_head = range.start.to_display_point(&snapshot.display_snapshot); + let buffer_range = info + .buffer + .summary_for_anchor::(&info.range.context.start) + ..info + .buffer + .summary_for_anchor::(&info.range.context.end); + + if let Some(breakpoints) = opened_breakpoints.get(&info.buffer_id) { + for breakpoint in breakpoints { + let breakpoint_position = info + .buffer + .summary_for_anchor::(&breakpoint.position.text_anchor); + + if buffer_range.contains(&breakpoint_position) { + let delta = breakpoint_position.row - buffer_range.start.row; + + let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0); + + breakpoint_display_points.insert(position); + } + } + }; + }; } breakpoint_display_points diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 584bb497e3415d..0818ca7e515929 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -689,7 +689,7 @@ impl WorkspaceDb { Ok(_) => {} } - for (worktree_path, serialized_breakpoints) in workspace.breakpoints { + for (worktree_path, serialized_breakpoints) in workspace.breakpoints { for serialized_breakpoint in serialized_breakpoints { let relative_path = serialized_breakpoint.path; From d3fe698c23c6117843ca774afbbcd4cd8689ee3e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 7 Aug 2024 19:08:28 -0400 Subject: [PATCH 184/650] Fix bug that caused a breakpoints to only remain persistent in one open file when booting zed up --- crates/project/src/project.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 28770236faed7a..cdf7a32bf92074 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1395,10 +1395,13 @@ impl Project { let mut result: HashMap, Vec> = Default::default(); for buffer_id in breakpoint_read_guard.keys() { - if let Some((worktree_path, serialized_breakpoint)) = + if let Some((worktree_path, mut serialized_breakpoint)) = self.serialize_breakpoint_for_buffer_id(&buffer_id, cx) { - result.insert(worktree_path, serialized_breakpoint); + result + .entry(worktree_path.clone()) + .or_default() + .append(&mut serialized_breakpoint) } } From 0f1738c8cc47b41d71d261f92a84aa23b14507a8 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 7 Aug 2024 19:17:49 -0400 Subject: [PATCH 185/650] Allow toggling breakpoints to work from within multibuffer gutter --- crates/editor/src/editor.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c1fa6a726d4040..75a1eedbb575fe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6091,13 +6091,10 @@ impl Editor { return; }; - let Some(buffer) = self.buffer.read(cx).as_singleton() else { + let Some(buffer_id) = breakpoint_position.buffer_id else { return; }; - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); - let breakpoint = Breakpoint { position: breakpoint_position, }; From 30b59a6c92a4cb1f86607162ea209938c3396a9e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 7 Aug 2024 22:32:01 -0400 Subject: [PATCH 186/650] Add breakpoint documentation --- crates/dap/docs/breakpoints.md | 9 ++++++ crates/editor/src/editor.rs | 29 +++++++++++++------ crates/editor/src/element.rs | 2 +- crates/project/src/project.rs | 43 +++++++++++++++++++++++------ crates/workspace/src/persistence.rs | 2 -- 5 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 crates/dap/docs/breakpoints.md diff --git a/crates/dap/docs/breakpoints.md b/crates/dap/docs/breakpoints.md new file mode 100644 index 00000000000000..53d6fef0285f0c --- /dev/null +++ b/crates/dap/docs/breakpoints.md @@ -0,0 +1,9 @@ +# Overview + +The active `Project` is responsible for maintain opened and closed breakpoints +as well as serializing breakpoints to save. At a high level project serializes +the positions of breakpoints that don't belong to any active buffers and handles +converting breakpoints from serializing to active whenever a buffer is opened/closed. + +`Project` also handles sending all relavent breakpoint information to debug adapter's +during debugging or when starting a debugger. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 75a1eedbb575fe..897771ad3c73f3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -560,7 +560,7 @@ pub struct Editor { tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, /// All the breakpoints that are contained within open buffers in the editor - opened_breakpoints: Option>>>>, + breakpoints: Option>>>>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown @@ -1885,7 +1885,7 @@ impl Editor { blame_subscription: None, file_header_size, tasks: Default::default(), - opened_breakpoints, + breakpoints: opened_breakpoints, gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -5148,11 +5148,16 @@ impl Editor { } } + /// Get all display points of breakpoints that will be rendered within editor + /// + /// This function is used to handle overlaps between breakpoints and Code action/runner symbol. + /// It's also used to set the color of line numbers with breakpoints to the breakpoint color. + /// TODO Debugger: Use this function to color toggle symbols that house nested breakpoints fn active_breakpoint_points(&mut self, cx: &mut ViewContext) -> HashSet { let mut breakpoint_display_points = HashSet::default(); let snapshot = self.snapshot(cx); - let Some(opened_breakpoints) = self.opened_breakpoints.clone() else { + let Some(opened_breakpoints) = self.breakpoints.clone() else { return breakpoint_display_points; }; @@ -5179,12 +5184,19 @@ impl Editor { let info = excerpt_boundery.next.as_ref(); if let Some(info) = info { - let Some(range) = multi_buffer_snapshot.range_for_excerpt::(info.id) else { + let Some(excerpt_ranges) = + multi_buffer_snapshot.range_for_excerpt::(info.id) + else { continue; }; - let excerpt_head = range.start.to_display_point(&snapshot.display_snapshot); - let buffer_range = info + // To translate a breakpoint's position within a singular buffer to a multi buffer + // position we need to know it's excerpt starting location, it's position within + // the singular buffer, and if that position is within the excerpt's range. + let excerpt_head = excerpt_ranges + .start + .to_display_point(&snapshot.display_snapshot); + let buffer_range = info // Buffer lines being shown within the excerpt .buffer .summary_for_anchor::(&info.range.context.start) ..info @@ -5193,11 +5205,12 @@ impl Editor { if let Some(breakpoints) = opened_breakpoints.get(&info.buffer_id) { for breakpoint in breakpoints { - let breakpoint_position = info + let breakpoint_position = info // Breakpoint's position within the singular buffer .buffer .summary_for_anchor::(&breakpoint.position.text_anchor); if buffer_range.contains(&breakpoint_position) { + // Translated breakpoint position from singular buffer to multi buffer let delta = breakpoint_position.row - buffer_range.start.row; let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0); @@ -6087,7 +6100,7 @@ impl Editor { return; }; - let Some(breakpoints) = &self.opened_breakpoints else { + let Some(breakpoints) = &self.breakpoints else { return; }; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e28f6a74b92d32..a929124457ccc0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1578,7 +1578,7 @@ impl EditorElement { cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { - if editor.opened_breakpoints.is_none() { + if editor.breakpoints.is_none() { return vec![]; }; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cdf7a32bf92074..c081683d7ff484 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1339,8 +1339,19 @@ impl Project { .insert(id, DebugAdapterClientState::Starting(task)); } - // TODO Anth: Convert this function to use a serialized breakpoint and return one too - pub fn serialize_breakpoint_for_buffer_id( + /// Get all serialized breakpoints that belong to a buffer + /// + /// # Parameters + /// `buffer_id`: The buffer id to get serialized breakpoints of + /// `cx`: The context of the editor + /// + /// # Return + /// `None`: If the buffer associated with buffer id doesn't exist or this editor + /// doesn't belong to a project + /// + /// `(Path, Vec, @@ -1363,7 +1374,11 @@ impl Project { )) } - // TODO Anth: Add docs about how this is used when a new editor with breakpoints is init + // Convert serialize breakpoints to active buffer breakpoints + // + // When a new buffer is opened, project converts any serialize + // breakpoints to active breakpoints that the buffer is aware + // of. pub fn convert_to_open_breakpoints( &mut self, project_path: &ProjectPath, @@ -1387,6 +1402,12 @@ impl Project { } } + /// Serialize all breakpoints to save within workspace's database + /// + /// # Return + /// HashMap: + /// Key: A valid worktree path + /// Value: All serialized breakpoints that belong to a worktree pub fn serialize_breakpoints( &self, cx: &ModelContext, @@ -1396,7 +1417,7 @@ impl Project { for buffer_id in breakpoint_read_guard.keys() { if let Some((worktree_path, mut serialized_breakpoint)) = - self.serialize_breakpoint_for_buffer_id(&buffer_id, cx) + self.serialize_breakpoints_for_buffer_id(&buffer_id, cx) { result .entry(worktree_path.clone()) @@ -1418,11 +1439,14 @@ impl Project { .extend(serialized_bp.iter().map(|bp| bp.clone())); } - // TODO Anth: Add documentation result } - // We don't need to send breakpoints from closed files because ... TODO: Fill in reasoning + /// Sends updated breakpoint information of one file to all active debug adapters + /// + /// This function is called whenever a breakpoint is toggled, and it doesn't need + /// to send breakpoints from closed files because those breakpoints can't change + /// without opening a buffer. pub fn update_file_breakpoints(&self, buffer_id: BufferId, cx: &ModelContext) { let clients = self .debug_adapters @@ -2552,9 +2576,10 @@ impl Project { self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_servers(buffer, cx); cx.observe_release(buffer, |this, buffer, cx| { - // Serialize the breakpoints of this buffer and send them - // Unopened breakpoints to maintain correct state - // TODO Anth: Almost done yayyy + // Serialize the breakpoints of this buffer and set them + // as unopened breakpoints to maintain correct state. + // Otherwise, project wouldn't allow breakpoints within + // closed files. if let Some(breakpoints) = this.open_breakpoints.write().remove(&buffer.remote_id()) { if let Some(project_path) = buffer.project_path(cx) { this.closed_breakpoints diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index cc83589848d081..c12a63adf91c8e 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -263,8 +263,6 @@ define_connection! { // preview: bool // Indicates if this item is a preview item // ) // - // Anthony is testing database configs below - // TODO Anth: Update docs below // CREATE TABLE breakpoints( // workspace_id: usize Foreign Key, // References workspace table // worktree_path: PathBuf, // Path of worktree that this breakpoint belong's too. Used to determine the absolute path of a breakpoint From fe58a702a016fbf615b1d3c396b30b246a74b34c Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 7 Aug 2024 23:05:29 -0400 Subject: [PATCH 187/650] Add debugger.accent as a configurable theme --- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/element.rs | 4 ++-- crates/theme/src/default_colors.rs | 2 ++ crates/theme/src/one_themes.rs | 1 + crates/theme/src/schema.rs | 9 +++++++++ crates/theme/src/styles/colors.rs | 3 +++ crates/ui/src/styles/color.rs | 2 ++ 7 files changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 897771ad3c73f3..f875b5d732b53a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5237,7 +5237,7 @@ impl Editor { { Color::Hint } else { - Color::Error + Color::Debugger }; IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play) @@ -5259,7 +5259,7 @@ impl Editor { cx: &mut ViewContext, ) -> IconButton { let color = if overlaps_breakpoint { - Color::Error + Color::Debugger } else { Color::Muted }; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a929124457ccc0..9ec10568f9fc82 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1708,7 +1708,7 @@ impl EditorElement { }; let button = if breakpoint_points.remove(&DisplayPoint::new(row, 0)) { - button.icon_color(Color::Error) + button.icon_color(Color::Debugger) } else { button }; @@ -1837,7 +1837,7 @@ impl EditorElement { let multibuffer_row = multibuffer_row?; let display_row = DisplayRow(rows.start.0 + ix as u32); let color = if breakpoint_rows.contains(&display_row) { - Hsla::red() + cx.theme().colors().debugger_accent } else if active_rows.contains_key(&display_row) { cx.theme().colors().editor_active_line_number } else { diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index ae5a9b96e8c9c9..4951cbbaa55196 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -48,6 +48,7 @@ impl ThemeColors { icon_disabled: neutral().light().step_9(), icon_placeholder: neutral().light().step_10(), icon_accent: blue().light().step_11(), + debugger_accent: red().dark().step_3(), status_bar_background: neutral().light().step_2(), title_bar_background: neutral().light().step_2(), title_bar_inactive_background: neutral().light_alpha().step_3(), @@ -147,6 +148,7 @@ impl ThemeColors { icon_disabled: neutral().dark().step_9(), icon_placeholder: neutral().dark().step_10(), icon_accent: blue().dark().step_11(), + debugger_accent: red().dark().step_3(), status_bar_background: neutral().dark().step_2(), title_bar_background: neutral().dark().step_2(), title_bar_inactive_background: neutral().dark_alpha().step_3(), diff --git a/crates/theme/src/one_themes.rs b/crates/theme/src/one_themes.rs index bb8c8752b5db76..7489d504971b07 100644 --- a/crates/theme/src/one_themes.rs +++ b/crates/theme/src/one_themes.rs @@ -74,6 +74,7 @@ pub(crate) fn one_dark() -> Theme { icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), icon_accent: blue, + debugger_accent: red, status_bar_background: bg, title_bar_background: bg, title_bar_inactive_background: bg, diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index edb06dd08520fb..8fd0a0f60490cb 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -294,6 +294,11 @@ pub struct ThemeColorsContent { #[serde(rename = "icon.accent")] pub icon_accent: Option, + /// Color used to accent some of the debuggers elements + /// Only accent breakpoint & breakpoint related symbols right now + #[serde(rename = "debugger.accent")] + pub debugger_accent: Option, + #[serde(rename = "status_bar.background")] pub status_bar_background: Option, @@ -662,6 +667,10 @@ impl ThemeColorsContent { .icon_accent .as_ref() .and_then(|color| try_parse_color(color).ok()), + debugger_accent: self + .debugger_accent + .as_ref() + .and_then(|color| try_parse_color(color).ok()), status_bar_background: self .status_bar_background .as_ref() diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 0ffbcd686514bf..27280dd05b0048 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -107,6 +107,9 @@ pub struct ThemeColors { /// /// This might be used to show when a toggleable icon button is selected. pub icon_accent: Hsla, + /// Color used to accent some debugger elements + /// Is used by breakpoints + pub debugger_accent: Hsla, // === // UI Elements diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs index b35728e478fb5e..452a51d8c7f468 100644 --- a/crates/ui/src/styles/color.rs +++ b/crates/ui/src/styles/color.rs @@ -8,6 +8,7 @@ pub enum Color { Default, Accent, Created, + Debugger, Deleted, Disabled, Error, @@ -35,6 +36,7 @@ impl Color { Color::Modified => cx.theme().status().modified, Color::Conflict => cx.theme().status().conflict, Color::Ignored => cx.theme().status().ignored, + Color::Debugger => cx.theme().colors().debugger_accent, Color::Deleted => cx.theme().status().deleted, Color::Disabled => cx.theme().colors().text_disabled, Color::Hidden => cx.theme().status().hidden, From 5678469f9db2c875a164cd17485285995c765bd8 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 12 Aug 2024 22:12:25 +0200 Subject: [PATCH 188/650] Show variables recursively (#16) * WIP Show variables in toggable list items * Use more unique element id so we can group variables by name and reference * Fix correct pass in listItem toggle value * Fix we did not set the current stack frame id * WIP start fetching variables recursively * Remove duplicated code for current stack frame id * Fetch all variables for each stack frame parallel * Fetch all variable on the same level parallel * Rename vars so its more clear * Refactor how we store thread data We now store the information in a better way so we could filter on it easier and faster. * Remove unused code * Use stack frame id as top level key instead of struct itself * Add has_children to scope thread entry * Fix allow switching current stack frame Also fixed building list entries twice right after each other * Show message when scope does not have variables * Remove scope if it does not have variables * Add allow collapsing scopes * Add allow collapsing variables * Add docs to determine collapsed variables * Allow collapsing variables and always show first scope variables * Correctly fix collapse variables * Fix clippy --- Cargo.lock | 2 +- crates/dap/src/client.rs | 13 +- crates/debugger_ui/src/debugger_panel.rs | 130 +++++--- crates/debugger_ui/src/debugger_panel_item.rs | 307 +++++++++++++----- 4 files changed, 321 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1113e3c69a193..e3c3a03b5af911 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3242,7 +3242,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types#e715437f1193d5da6d7de6a71abdd1ac1fbf4c9d" +source = "git+https://github.com/zed-industries/dap-types#04f7b33db5f825cd874b5be73919609f4c5d8169" dependencies = [ "serde", "serde_json", diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 86cbfccf394d67..477685e100b5d7 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -25,7 +25,7 @@ use smol::{ process::{self, Child}, }; use std::{ - collections::HashMap, + collections::{BTreeMap, HashMap}, net::{Ipv4Addr, SocketAddrV4}, path::PathBuf, process::Stdio, @@ -55,9 +55,8 @@ pub struct DebugAdapterClientId(pub usize); pub struct ThreadState { pub status: ThreadStatus, pub stack_frames: Vec, - pub scopes: HashMap>, // stack_frame_id -> scopes - pub variables: HashMap>, // scope.variable_reference -> variables - pub current_stack_frame_id: Option, + pub variables: HashMap>>, + pub current_stack_frame_id: u64, } pub struct DebugAdapterClient { @@ -411,6 +410,12 @@ impl DebugAdapterClient { }; } + pub fn update_current_stack_frame(&self, thread_id: u64, stack_frame_id: u64) { + if let Some(thread_state) = self.thread_states().get_mut(&thread_id) { + thread_state.current_stack_frame_id = stack_frame_id; + }; + } + pub fn thread_states(&self) -> MutexGuard> { self.thread_states.lock() } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9acd3641e6d7f5..1d383bb42ddcf8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -5,9 +5,9 @@ use dap::requests::{Disconnect, Request, Scopes, StackTrace, StartDebugging, Var use dap::transport::Payload; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, OutputEvent, Scope, - ScopesArguments, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, - TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, + Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, OutputEvent, ScopesArguments, + StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, TerminatedEvent, + ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; use editor::Editor; use futures::future::try_join_all; @@ -16,8 +16,9 @@ use gpui::{ Subscription, Task, View, ViewContext, WeakView, }; use serde_json::json; +use std::collections::BTreeMap; use std::path::Path; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; use task::DebugRequestType; use ui::prelude::*; use util::{merge_json_value_into, ResultExt}; @@ -389,6 +390,51 @@ impl DebugPanel { cx.notify(); } + async fn fetch_variables( + client: Arc, + variables_reference: u64, + depth: usize, + ) -> Result> { + let response = client + .request::(VariablesArguments { + variables_reference, + filter: None, + start: None, + count: None, + format: None, + }) + .await?; + + let mut tasks = Vec::new(); + for variable in response.variables { + let client = client.clone(); + tasks.push(async move { + let mut variables = vec![(depth, variable.clone())]; + + if variable.variables_reference > 0 { + let mut nested_variables = Box::pin(Self::fetch_variables( + client, + variable.variables_reference, + depth + 1, + )) + .await?; + + variables.append(&mut nested_variables); + } + + anyhow::Ok(variables) + }); + } + + let mut variables = Vec::new(); + + for mut variable_entries in try_join_all(tasks).await? { + variables.append(&mut variable_entries); + } + + anyhow::Ok(variables) + } + fn handle_stopped_event( client: Arc, event: &StoppedEvent, @@ -411,65 +457,65 @@ impl DebugPanel { }) .await?; + let mut thread_state = ThreadState::default(); + let current_stack_frame = stack_trace_response.stack_frames.first().unwrap().clone(); let mut scope_tasks = Vec::new(); for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { - let frame_id = stack_frame.id; let client = client.clone(); scope_tasks.push(async move { anyhow::Ok(( - frame_id, + stack_frame.id, client - .request::(ScopesArguments { frame_id }) + .request::(ScopesArguments { + frame_id: stack_frame.id, + }) .await?, )) }); } - let mut scopes: HashMap> = HashMap::new(); - let mut variables: HashMap> = HashMap::new(); - - let mut variable_tasks = Vec::new(); - for (thread_id, response) in try_join_all(scope_tasks).await? { - scopes.insert(thread_id, response.scopes.clone()); + let mut stack_frame_tasks = Vec::new(); + for (stack_frame_id, response) in try_join_all(scope_tasks).await? { + let client = client.clone(); + stack_frame_tasks.push(async move { + let mut variable_tasks = Vec::new(); + + for scope in response.scopes { + let scope_reference = scope.variables_reference; + + let client = client.clone(); + variable_tasks.push(async move { + anyhow::Ok(( + scope, + Self::fetch_variables(client, scope_reference, 1).await?, + )) + }); + } - for scope in response.scopes { - let scope_reference = scope.variables_reference; - let client = client.clone(); - variable_tasks.push(async move { - anyhow::Ok(( - scope_reference, - client - .request::(VariablesArguments { - variables_reference: scope_reference, - filter: None, - start: None, - count: None, - format: None, - }) - .await?, - )) - }); - } + anyhow::Ok((stack_frame_id, try_join_all(variable_tasks).await?)) + }); } - for (scope_reference, response) in try_join_all(variable_tasks).await? { - variables.insert(scope_reference, response.variables.clone()); + for (stack_frame_id, scopes) in try_join_all(stack_frame_tasks).await? { + let stack_frame_state = thread_state + .variables + .entry(stack_frame_id) + .or_insert_with(BTreeMap::default); + + for (scope, variables) in scopes { + stack_frame_state.insert(scope, variables); + } } this.update(&mut cx, |this, cx| { - let mut thread_state = client.thread_states(); - let thread_state = thread_state - .entry(thread_id) - .or_insert(ThreadState::default()); - - thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id); + thread_state.current_stack_frame_id = current_stack_frame.clone().id; thread_state.stack_frames = stack_trace_response.stack_frames; - thread_state.scopes = scopes; - thread_state.variables = variables; thread_state.status = ThreadStatus::Stopped; + client.thread_states().insert(thread_id, thread_state); + let existing_item = this .pane .read(cx) @@ -496,7 +542,7 @@ impl DebugPanel { ) }); - this.add_item(Box::new(tab.clone()), false, false, None, cx) + this.add_item(Box::new(tab), false, false, None, cx) }) }) .log_err(); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 65cfa6f8c6dc17..b13bcbd6b0a94a 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -9,9 +9,10 @@ use gpui::{ actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, ListState, Subscription, View, WeakView, }; +use std::collections::HashMap; use std::sync::Arc; -use ui::WindowContext; use ui::{prelude::*, Tooltip}; +use ui::{ListItem, WindowContext}; use workspace::item::{Item, ItemEvent}; #[derive(PartialEq, Eq)] @@ -21,15 +22,27 @@ enum ThreadItem { Output, } +#[derive(Debug, Clone)] +pub enum ThreadEntry { + Scope(Scope), + Variable { + depth: usize, + variable: Arc, + has_children: bool, + }, +} + pub struct DebugPanelItem { thread_id: u64, + variable_list: ListState, focus_handle: FocusHandle, stack_frame_list: ListState, + output_editor: View, + open_entries: Vec, + stack_frame_entries: HashMap>, + active_thread_item: ThreadItem, client: Arc, _subscriptions: Vec, - current_stack_frame_id: Option, - active_thread_item: ThreadItem, - output_editor: View, } actions!( @@ -45,6 +58,17 @@ impl DebugPanelItem { cx: &mut ViewContext, ) -> Self { let focus_handle = cx.focus_handle(); + + let weakview = cx.view().downgrade(); + let variable_list = + ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { + if let Some(view) = weakview.upgrade() { + view.update(cx, |view, cx| view.render_variable_list_entry(ix, cx)) + } else { + div().into_any() + } + }); + let weakview = cx.view().downgrade(); let stack_frame_list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { @@ -78,7 +102,7 @@ impl DebugPanelItem { editor.set_placeholder_text("Debug adapter and script output", cx); editor.set_read_only(true); editor.set_show_inline_completions(false); - editor.set_searchable(true); + editor.set_searchable(false); editor.set_auto_replace_emoji_shortcode(false); editor.set_show_indent_guides(false, cx); editor.set_autoindent(false); @@ -91,10 +115,12 @@ impl DebugPanelItem { client, thread_id, focus_handle, + variable_list, output_editor, _subscriptions, stack_frame_list, - current_stack_frame_id: None, + open_entries: Default::default(), + stack_frame_entries: Default::default(), active_thread_item: ThreadItem::Variables, } } @@ -117,8 +143,13 @@ impl DebugPanelItem { return; } - this.stack_frame_list - .reset(this.current_thread_state().stack_frames.len()); + let thread_state = this.current_thread_state(); + + this.stack_frame_list.reset(thread_state.stack_frames.len()); + if let Some(stack_frame) = thread_state.stack_frames.first() { + this.update_stack_frame_id(stack_frame.id); + }; + cx.notify(); } @@ -230,6 +261,113 @@ impl DebugPanelItem { .unwrap() } + fn update_stack_frame_id(&mut self, stack_frame_id: u64) { + self.client + .update_current_stack_frame(self.thread_id, stack_frame_id); + + self.open_entries.clear(); + + self.build_variable_list_entries(stack_frame_id, true); + } + + pub fn render_variable_list_entry( + &mut self, + ix: usize, + cx: &mut ViewContext, + ) -> AnyElement { + let Some(entries) = self + .stack_frame_entries + .get(&self.current_thread_state().current_stack_frame_id) + else { + return div().into_any_element(); + }; + + match &entries[ix] { + ThreadEntry::Scope(scope) => self.render_scope(scope, cx), + ThreadEntry::Variable { + depth, + variable, + has_children, + .. + } => self.render_variable(variable, *depth, *has_children, cx), + } + } + + fn scope_entry_id(scope: &Scope) -> SharedString { + SharedString::from(format!("scope-{}", scope.variables_reference)) + } + + fn variable_entry_id(variable: &Variable, depth: usize) -> SharedString { + SharedString::from(format!("variable-{}-{}", depth, variable.name)) + } + + fn render_scope(&self, scope: &Scope, cx: &mut ViewContext) -> AnyElement { + let element_id = scope.variables_reference; + + let scope_id = Self::scope_entry_id(scope); + let disclosed = self.open_entries.binary_search(&scope_id).is_ok(); + + div() + .id(element_id as usize) + .group("") + .flex() + .w_full() + .h_full() + .child( + ListItem::new(scope_id.clone()) + .indent_level(1) + .indent_step_size(px(20.)) + .toggle(disclosed) + .on_toggle( + cx.listener(move |this, _, cx| this.toggle_entry_collapsed(&scope_id, cx)), + ) + .child(div().text_ui(cx).w_full().child(scope.name.clone())), + ) + .into_any() + } + + fn render_variable( + &self, + variable: &Variable, + depth: usize, + has_children: bool, + cx: &mut ViewContext, + ) -> AnyElement { + let variable_id = Self::variable_entry_id(variable, depth); + + let disclosed = has_children.then(|| self.open_entries.binary_search(&variable_id).is_ok()); + + div() + .id(variable_id.clone()) + .group("") + .h_4() + .size_full() + .child( + ListItem::new(variable_id.clone()) + .indent_level(depth + 1) + .indent_step_size(px(20.)) + .toggle(disclosed) + .on_toggle( + cx.listener(move |this, _, cx| { + this.toggle_entry_collapsed(&variable_id, cx) + }), + ) + .child( + h_flex() + .gap_1() + .text_ui_sm(cx) + .child(variable.name.clone()) + .child( + div() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .child(variable.value.clone()), + ), + ), + ) + .into_any() + } + fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { v_flex() .gap_3() @@ -242,8 +380,8 @@ impl DebugPanelItem { let stack_frame = self.stack_frame_for_index(ix); let source = stack_frame.source.clone(); - let selected_frame_id = self.current_stack_frame_id; - let is_selected_frame = Some(stack_frame.id) == selected_frame_id; + let is_selected_frame = + stack_frame.id == self.current_thread_state().current_stack_frame_id; let formatted_path = format!( "{}:{}", @@ -264,9 +402,11 @@ impl DebugPanelItem { this.bg(cx.theme().colors().element_hover) }) .on_click(cx.listener({ - let stack_frame = stack_frame.clone(); - move |this, _, _| { - this.current_stack_frame_id = Some(stack_frame.id); + let stack_frame_id = stack_frame.id; + move |this, _, cx| { + this.update_stack_frame_id(stack_frame_id); + + cx.notify(); // let client = this.client(); // DebugPanel::go_to_stack_frame(&stack_frame, client, true, cx) @@ -295,83 +435,66 @@ impl DebugPanelItem { .into_any() } - fn render_scopes(&self, cx: &mut ViewContext) -> impl IntoElement { + pub fn build_variable_list_entries(&mut self, stack_frame_id: u64, open_first_scope: bool) { let thread_state = self.current_thread_state(); - let Some(scopes) = thread_state - .current_stack_frame_id - .and_then(|id| thread_state.scopes.get(&id)) - else { - return div().child("No scopes for this thread yet").into_any(); + let Some(scopes_and_vars) = thread_state.variables.get(&stack_frame_id) else { + return; }; - div() - .w_3_4() - .gap_3() - .text_ui_sm(cx) - .children( - scopes - .iter() - .map(|scope| self.render_scope(&thread_state, scope, cx)), - ) - .into_any() - } + let mut entries: Vec = Vec::default(); + for (scope, variables) in scopes_and_vars { + if variables.is_empty() { + continue; + } - fn render_scope( - &self, - thread_state: &ThreadState, - scope: &Scope, - cx: &mut ViewContext, - ) -> impl IntoElement { - div() - .id(("scope", scope.variables_reference)) - .p_1() - .text_ui_sm(cx) - .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) - .child(scope.name.clone()) - .child( - div() - .ml_2() - .child(self.render_variables(thread_state, scope, cx)), - ) - .into_any() - } + if open_first_scope && self.open_entries.is_empty() { + self.open_entries.push(Self::scope_entry_id(scope)); + } - fn render_variables( - &self, - thread_state: &ThreadState, - scope: &Scope, - cx: &mut ViewContext, - ) -> impl IntoElement { - let Some(variables) = thread_state.variables.get(&scope.variables_reference) else { - return div().child("No variables for this thread yet").into_any(); - }; + entries.push(ThreadEntry::Scope(scope.clone())); - div() - .gap_3() - .text_ui_sm(cx) - .children( - variables - .iter() - .map(|variable| self.render_variable(variable, cx)), - ) - .into_any() - } + if self + .open_entries + .binary_search(&Self::scope_entry_id(scope)) + .is_err() + { + continue; + } - fn render_variable(&self, variable: &Variable, cx: &mut ViewContext) -> impl IntoElement { - h_flex() - .id(("variable", variable.variables_reference)) - .p_1() - .gap_1() - .text_ui_sm(cx) - .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) - .child(variable.name.clone()) - .child( - div() - .text_ui_xs(cx) - .text_color(cx.theme().colors().text_muted) - .child(variable.value.clone()), - ) - .into_any() + let mut depth_check: Option = None; + + for (depth, variable) in variables { + if depth_check.is_some_and(|d| *depth > d) { + continue; + } + + if depth_check.is_some_and(|d| d >= *depth) { + depth_check = None; + } + + let has_children = variable.variables_reference > 0; + + if self + .open_entries + .binary_search(&Self::variable_entry_id(variable, *depth)) + .is_err() + { + if depth_check.is_none() || depth_check.is_some_and(|d| d > *depth) { + depth_check = Some(*depth); + } + } + + entries.push(ThreadEntry::Variable { + depth: *depth, + variable: Arc::new(variable.clone()), + has_children, + }); + } + } + + let len = entries.len(); + self.stack_frame_entries.insert(stack_frame_id, entries); + self.variable_list.reset(len); } // if the debug adapter does not send the continued event, @@ -482,6 +605,21 @@ impl DebugPanelItem { .spawn(async move { client.disconnect(None, Some(true), None).await }) .detach_and_log_err(cx); } + + fn toggle_entry_collapsed(&mut self, entry_id: &SharedString, cx: &mut ViewContext) { + match self.open_entries.binary_search(&entry_id) { + Ok(ix) => { + self.open_entries.remove(ix); + } + Err(ix) => { + self.open_entries.insert(ix, entry_id.clone()); + } + }; + + self.build_variable_list_entries(self.current_thread_state().current_stack_frame_id, false); + + cx.notify(); + } } impl Render for DebugPanelItem { @@ -664,7 +802,8 @@ impl Render for DebugPanelItem { ), ) .when(*active_thread_item == ThreadItem::Variables, |this| { - this.child(self.render_scopes(cx)) + this.size_full() + .child(list(self.variable_list.clone()).gap_1_5().size_full()) }) .when(*active_thread_item == ThreadItem::Output, |this| { this.child(self.output_editor.clone()) From b561c68d28332b0a6c5fd98238a09388287fc019 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 14 Aug 2024 10:46:07 +0200 Subject: [PATCH 189/650] Always show disclosure icon for variables and scopes (#17) --- crates/debugger_ui/src/debugger_panel_item.rs | 2 ++ crates/ui/src/components/list/list_item.rs | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index b13bcbd6b0a94a..7b72eccfb40c5d 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -317,6 +317,7 @@ impl DebugPanelItem { ListItem::new(scope_id.clone()) .indent_level(1) .indent_step_size(px(20.)) + .always_show_disclosure_icon(false) .toggle(disclosed) .on_toggle( cx.listener(move |this, _, cx| this.toggle_entry_collapsed(&scope_id, cx)), @@ -346,6 +347,7 @@ impl DebugPanelItem { ListItem::new(variable_id.clone()) .indent_level(depth + 1) .indent_step_size(px(20.)) + .always_show_disclosure_icon(false) .toggle(disclosed) .on_toggle( cx.listener(move |this, _, cx| { diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 6b38b7f963fee3..2f7a932d73613b 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -36,6 +36,7 @@ pub struct ListItem { on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, selectable: bool, + always_show_disclosure_icon: bool, } impl ListItem { @@ -58,6 +59,7 @@ impl ListItem { tooltip: None, children: SmallVec::new(), selectable: true, + always_show_disclosure_icon: false, } } @@ -71,6 +73,11 @@ impl ListItem { self } + pub fn always_show_disclosure_icon(mut self, show: bool) -> Self { + self.always_show_disclosure_icon = show; + self + } + pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { self.on_click = Some(Box::new(handler)); self @@ -230,7 +237,9 @@ impl RenderOnce for ListItem { .flex() .absolute() .left(rems(-1.)) - .when(is_open, |this| this.visible_on_hover("")) + .when(is_open && !self.always_show_disclosure_icon, |this| { + this.visible_on_hover("") + }) .child(Disclosure::new("toggle", is_open).on_toggle(self.on_toggle)) })) .child( From 9ea23b0a62abdbd537aacad1d870cf853448611f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 14 Aug 2024 15:28:39 +0200 Subject: [PATCH 190/650] Fix wrong value for always showing disclosure icon Oopss, after testing i did commit the wrong value. --- crates/debugger_ui/src/debugger_panel_item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 7b72eccfb40c5d..5641810e442813 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -317,7 +317,7 @@ impl DebugPanelItem { ListItem::new(scope_id.clone()) .indent_level(1) .indent_step_size(px(20.)) - .always_show_disclosure_icon(false) + .always_show_disclosure_icon(true) .toggle(disclosed) .on_toggle( cx.listener(move |this, _, cx| this.toggle_entry_collapsed(&scope_id, cx)), @@ -347,7 +347,7 @@ impl DebugPanelItem { ListItem::new(variable_id.clone()) .indent_level(depth + 1) .indent_step_size(px(20.)) - .always_show_disclosure_icon(false) + .always_show_disclosure_icon(true) .toggle(disclosed) .on_toggle( cx.listener(move |this, _, cx| { From 9643e71947f5ba379f998eb16182d75aff868370 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 14 Aug 2024 17:00:18 +0200 Subject: [PATCH 191/650] Remove debugger client when its terminated (#18) * Only terminate thread if it did not already end * Remove debug client when its terminated --- crates/dap/src/client.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 46 +++++++++++++----------- crates/project/src/project.rs | 29 ++++++++++----- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 477685e100b5d7..6d34143db30f18 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -700,7 +700,7 @@ impl DebugAdapterClient { self.request::(TerminateThreadsArguments { thread_ids }) .await } else { - self.disconnect(None, Some(true), None).await + self.terminate().await } } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1d383bb42ddcf8..336654704eafa3 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,12 +1,12 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; -use dap::requests::{Disconnect, Request, Scopes, StackTrace, StartDebugging, Variables}; +use dap::requests::{Request, Scopes, StackTrace, StartDebugging, Variables}; use dap::transport::Payload; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ - Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, OutputEvent, ScopesArguments, - StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, TerminatedEvent, + Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, ScopesArguments, StackFrame, + StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, VariablesArguments, }; use editor::Editor; @@ -158,11 +158,17 @@ impl DebugPanel { thread_panel.update(cx, |pane, cx| { let thread_id = pane.thread_id(); - let client = pane.client().clone(); - cx.spawn( - |_, _| async move { client.terminate_threads(Some(vec![thread_id; 1])).await }, - ) - .detach_and_log_err(cx); + let client = pane.client(); + let thread_status = client.thread_state_by_id(thread_id).status; + + // only terminate thread if the thread has not yet ended + if thread_status != ThreadStatus::Ended && thread_status != ThreadStatus::Exited { + let client = client.clone(); + cx.spawn(|_, _| async move { + client.terminate_threads(Some(vec![thread_id; 1])).await + }) + .detach_and_log_err(cx); + } }); }; } @@ -637,26 +643,24 @@ impl DebugPanel { let restart_args = event.clone().and_then(|e| e.restart); let workspace = this.workspace.clone(); - cx.spawn(|_, cx| async move { - let should_restart = restart_args.is_some(); + cx.spawn(|_, mut cx| async move { + Self::remove_highlights(workspace.clone(), client.clone(), cx.clone()).await?; - Self::remove_highlights(workspace, client.clone(), cx).await?; + if restart_args.is_some() { + client.disconnect(Some(true), None, None).await?; - client - .request::(DisconnectArguments { - restart: Some(should_restart), - terminate_debuggee: None, - suspend_debuggee: None, - }) - .await?; - - if should_restart { match client.request_type() { DebugRequestType::Launch => client.launch(restart_args).await, DebugRequestType::Attach => client.attach(restart_args).await, } } else { - anyhow::Ok(()) + cx.update(|cx| { + workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.stop_debug_adapter_client(client.id(), false, cx) + }) + }) + })? } }) .detach_and_log_err(cx); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bd41955d5bdf48..f9e5a2553d39d9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1292,15 +1292,28 @@ impl Project { .insert(id, DebugAdapterClientState::Starting(task)); } + pub fn stop_debug_adapter_client( + &mut self, + client_id: DebugAdapterClientId, + should_terminate: bool, + cx: &mut ModelContext, + ) { + let Some(debug_client) = self.debug_adapters.remove(&client_id) else { + return; + }; + + if !should_terminate { + return; + } + + if let DebugAdapterClientState::Running(client) = debug_client { + cx.spawn(|_, _| async move { client.terminate().await }) + .detach_and_log_err(cx) + } + } + pub fn update_file_breakpoints(&self, buffer_id: BufferId, cx: &mut ModelContext) { - let clients = self - .debug_adapters - .iter() - .filter_map(|(_, state)| match state { - DebugAdapterClientState::Starting(_) => None, - DebugAdapterClientState::Running(client) => Some(client.clone()), - }) - .collect::>(); + let clients = self.running_debug_adapters().collect::>(); let Some(buffer) = self.buffer_for_id(buffer_id, cx) else { return; From 3a3f4990bada96c02bca94d922c48114d53ae232 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 15 Aug 2024 17:13:04 -0400 Subject: [PATCH 192/650] Breakpoint PR review edits Co-authored-by: Remco Smits --- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/element.rs | 3 ++- crates/project/src/project.rs | 23 ++++++++++------------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 00db8b340079bf..3b9318ad168a9c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1780,7 +1780,7 @@ impl Editor { }; let opened_breakpoints = if let Some(project) = project.as_ref() { - project.update(cx, |project, _cx| Some(project.open_breakpoints.clone())) + project.read_with(cx, |project, _cx| Some(project.open_breakpoints.clone())) } else { None }; @@ -5247,7 +5247,7 @@ impl Editor { .icon_color(color) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); - editor.toggle_breakpoint_at_anchor(position, cx) //TODO handle folded + editor.toggle_breakpoint_at_anchor(position, cx) })) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 36033ba1b846f8..055c23e7164497 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -714,11 +714,12 @@ impl EditorElement { position_map.point_for_position(text_hitbox.bounds, event.position); let position = point_for_position.previous_valid; editor.gutter_breakpoint_indicator = Some(position); - cx.notify(); } else { editor.gutter_breakpoint_indicator = None; } + cx.notify(); + // Don't trigger hover popover if mouse is hovering over context menu if text_hitbox.is_hovered(cx) { let point_for_position = diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ea2ef58adf94de..637c38918aab4e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1396,8 +1396,10 @@ impl Project { .entry(buffer_id) .or_default() .insert(Breakpoint { - position: snapshot - .anchor_at(Point::new(serialized_bp.position - 1, 0), Bias::Left), + position: snapshot.anchor_at( + Point::new(serialized_bp.position.saturating_sub(1), 0), + Bias::Left, + ), }); // Serialized breakpoints start at index one so we shift when converting to open breakpoints } } @@ -1449,14 +1451,7 @@ impl Project { /// to send breakpoints from closed files because those breakpoints can't change /// without opening a buffer. pub fn update_file_breakpoints(&self, buffer_id: BufferId, cx: &ModelContext) { - let clients = self - .debug_adapters - .iter() - .filter_map(|(_, state)| match state { - DebugAdapterClientState::Starting(_) => None, - DebugAdapterClientState::Running(client) => Some(client.clone()), - }) - .collect::>(); + let clients = self.running_debug_adapters().collect::>(); if clients.is_empty() { return; @@ -2376,10 +2371,12 @@ impl Project { let buffer_breakpoints = write_guard.entry(buffer_id).or_default(); for serialized_bp in serialized_breakpoints { - // serialized breakpoints are start at index one and need to converted + // serialized breakpoints start at index one and need to converted // to index zero in order to display/work properly with open breakpoints - let position = snapshot - .anchor_at(Point::new(serialized_bp.position - 1, 0), Bias::Left); + let position = snapshot.anchor_at( + Point::new(serialized_bp.position.saturating_sub(1), 0), + Bias::Left, + ); buffer_breakpoints.insert(Breakpoint { position }); } From e9f0e936ea82492cb9beeb36ece1d6ce13435b56 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 15 Aug 2024 19:40:40 -0400 Subject: [PATCH 193/650] Set up ground work for debugger settings & save breakpoints setting Co-authored-by: Remco Smits --- Cargo.lock | 4 +++- assets/settings/default.json | 7 ++++++- crates/dap/Cargo.toml | 4 +++- crates/dap/src/debugger_settings.rs | 32 +++++++++++++++++++++++++++++ crates/dap/src/lib.rs | 1 + crates/dap/src/transport.rs | 1 - crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/lib.rs | 4 ++++ crates/project/src/project.rs | 8 +++++++- crates/workspace/Cargo.toml | 1 - 10 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 crates/dap/src/debugger_settings.rs diff --git a/Cargo.lock b/Cargo.lock index f5aaa245bac7d1..44c41c3e3c2565 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3232,8 +3232,10 @@ dependencies = [ "log", "multi_buffer", "parking_lot", + "schemars", "serde", "serde_json", + "settings", "smol", "task", "text", @@ -3323,6 +3325,7 @@ dependencies = [ "project", "serde", "serde_json", + "settings", "task", "tasks_ui", "ui", @@ -13532,7 +13535,6 @@ dependencies = [ "clock", "collections", "dap", - "dap-types", "db", "derive_more", "dev_server_projects", diff --git a/assets/settings/default.json b/assets/settings/default.json index 007eba7d175cd5..accaa423841eb3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -997,5 +997,10 @@ // ] // } // ] - "ssh_connections": null + "ssh_connections": null, + + "debugger": { + // Save breakpoints across different Zed sessions + "save_breakpoints": true + } } diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index a961c1285dac9f..c8b688acfd73b9 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -13,12 +13,14 @@ anyhow.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types" } futures.workspace = true gpui.workspace = true -multi_buffer.workspace = true language.workspace = true log.workspace = true +multi_buffer.workspace = true parking_lot.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true +settings.workspace = true smol.workspace = true task.workspace = true text.workspace = true diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs new file mode 100644 index 00000000000000..fb7647e0132874 --- /dev/null +++ b/crates/dap/src/debugger_settings.rs @@ -0,0 +1,32 @@ +use gpui::{AppContext, Global}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources}; + +#[derive(Deserialize, Clone, Debug)] +pub struct DebuggerSettings { + pub save_breakpoints: bool, +} + +#[derive(Default, Serialize, Deserialize, JsonSchema, Clone)] +pub struct DebuggerSettingsContent { + /// Whether the breakpoints should be reused across Zed sessions. + /// + /// Default: true + pub save_breakpoints: Option, +} + +impl Settings for DebuggerSettings { + const KEY: Option<&'static str> = Some("debugger"); + + type FileContent = DebuggerSettingsContent; + + fn load( + sources: SettingsSources, + _: &mut AppContext, + ) -> anyhow::Result { + sources.json_merge() + } +} + +impl Global for DebuggerSettings {} diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index abf46905c0c3d2..da1f4793e7aad1 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,3 +1,4 @@ pub mod client; pub mod transport; pub use dap_types::*; +pub mod debugger_settings; diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index c420a23ba0aae7..0144c6f094520f 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -166,7 +166,6 @@ impl Transport { .with_context(|| "reading after a loop")?; let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; - Ok(serde_json::from_str::(msg)?) } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index ab5933f2da5f99..3f3fb99c79fbc5 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -17,6 +17,7 @@ gpui.workspace = true project.workspace = true serde.workspace = true serde_json.workspace = true +settings.workspace = true task.workspace = true tasks_ui.workspace = true ui.workspace = true diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 3f0dd73e891eaa..11c06e3feeaf9c 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,11 +1,15 @@ +use dap::debugger_settings::DebuggerSettings; use debugger_panel::{DebugPanel, ToggleFocus}; use gpui::AppContext; +use settings::Settings; use workspace::{StartDebugger, Workspace}; pub mod debugger_panel; mod debugger_panel_item; pub fn init(cx: &mut AppContext) { + DebuggerSettings::register(cx); + cx.observe_new_views(|workspace: &mut Workspace, _| { workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::(cx); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 637c38918aab4e..59c6a1ffaad5af 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,6 +26,7 @@ use clock::ReplicaId; use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use dap::{ client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId, SerializedBreakpoint}, + debugger_settings::DebuggerSettings, transport::Payload, }; use debounced_delay::DebouncedDelay; @@ -1415,9 +1416,14 @@ impl Project { &self, cx: &ModelContext, ) -> HashMap, Vec> { - let breakpoint_read_guard = self.open_breakpoints.read(); let mut result: HashMap, Vec> = Default::default(); + if !DebuggerSettings::get_global(cx).save_breakpoints { + return result; + } + + let breakpoint_read_guard = self.open_breakpoints.read(); + for buffer_id in breakpoint_read_guard.keys() { if let Some((worktree_path, mut serialized_breakpoint)) = self.serialize_breakpoints_for_buffer_id(&buffer_id, cx) diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 64cc1ff6a20be1..a130b1db6ad4bf 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -35,7 +35,6 @@ call.workspace = true client.workspace = true clock.workspace = true collections.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types" } dap.workspace = true db.workspace = true derive_more.workspace = true From 11b2bc1ffc32e71f783b16cae8339fcd91939a82 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 18 Aug 2024 16:44:53 +0200 Subject: [PATCH 194/650] Lazy fetch variables (#19) * Lazy fetch * Remove unused code * Clean up fetch nested variable * Include scope id for variable id to be more unique * Clean up not needed get_mut function calls --- crates/dap/src/client.rs | 21 +++- crates/debugger_ui/src/debugger_panel.rs | 103 ++++++++---------- crates/debugger_ui/src/debugger_panel_item.rs | 99 +++++++++++++++-- 3 files changed, 155 insertions(+), 68 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 6d34143db30f18..0ce31a35713e83 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -4,14 +4,14 @@ use anyhow::{anyhow, Context, Result}; use dap_types::{ requests::{ Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, - Restart, SetBreakpoints, StepBack, StepIn, StepOut, Terminate, TerminateThreads, + Restart, SetBreakpoints, StepBack, StepIn, StepOut, Terminate, TerminateThreads, Variables, }, AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, RestartArguments, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, - TerminateThreadsArguments, Variable, + TerminateThreadsArguments, Variable, VariablesArguments, }; use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; @@ -55,6 +55,9 @@ pub struct DebugAdapterClientId(pub usize); pub struct ThreadState { pub status: ThreadStatus, pub stack_frames: Vec, + // HashMap> + pub vars: HashMap>, + // HashMap>> pub variables: HashMap>>, pub current_stack_frame_id: u64, } @@ -703,6 +706,20 @@ impl DebugAdapterClient { self.terminate().await } } + + pub async fn variables(&self, variables_reference: u64) -> Result> { + anyhow::Ok( + self.request::(VariablesArguments { + variables_reference, + filter: None, + start: None, + count: None, + format: None, + }) + .await? + .variables, + ) + } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 336654704eafa3..2be624f1842494 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,13 +1,13 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; -use dap::requests::{Request, Scopes, StackTrace, StartDebugging, Variables}; +use dap::requests::{Request, Scopes, StackTrace, StartDebugging}; use dap::transport::Payload; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, ScopesArguments, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, TerminatedEvent, - ThreadEvent, ThreadEventReason, Variable, VariablesArguments, + ThreadEvent, ThreadEventReason, Variable, }; use editor::Editor; use futures::future::try_join_all; @@ -26,7 +26,7 @@ use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; -use workspace::{pane, Pane}; +use workspace::{pane, Pane, StartDebugger}; enum DebugCurrentRowHighlight {} @@ -396,51 +396,6 @@ impl DebugPanel { cx.notify(); } - async fn fetch_variables( - client: Arc, - variables_reference: u64, - depth: usize, - ) -> Result> { - let response = client - .request::(VariablesArguments { - variables_reference, - filter: None, - start: None, - count: None, - format: None, - }) - .await?; - - let mut tasks = Vec::new(); - for variable in response.variables { - let client = client.clone(); - tasks.push(async move { - let mut variables = vec![(depth, variable.clone())]; - - if variable.variables_reference > 0 { - let mut nested_variables = Box::pin(Self::fetch_variables( - client, - variable.variables_reference, - depth + 1, - )) - .await?; - - variables.append(&mut nested_variables); - } - - anyhow::Ok(variables) - }); - } - - let mut variables = Vec::new(); - - for mut variable_entries in try_join_all(tasks).await? { - variables.append(&mut variable_entries); - } - - anyhow::Ok(variables) - } - fn handle_stopped_event( client: Arc, event: &StoppedEvent, @@ -493,10 +448,7 @@ impl DebugPanel { let client = client.clone(); variable_tasks.push(async move { - anyhow::Ok(( - scope, - Self::fetch_variables(client, scope_reference, 1).await?, - )) + anyhow::Ok((scope, client.variables(scope_reference).await?)) }); } @@ -511,7 +463,17 @@ impl DebugPanel { .or_insert_with(BTreeMap::default); for (scope, variables) in scopes { - stack_frame_state.insert(scope, variables); + thread_state + .vars + .insert(scope.variables_reference, variables.clone()); + + stack_frame_state.insert( + scope, + variables + .into_iter() + .map(|v| (1, v)) + .collect::>(), + ); } } @@ -738,12 +700,43 @@ impl Panel for DebugPanel { } impl Render for DebugPanel { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() .key_context("DebugPanel") .track_focus(&self.focus_handle) .size_full() - .child(self.pane.clone()) + .map(|this| { + if self.pane.read(cx).items_len() == 0 { + this.child( + h_flex().size_full().items_center().justify_center().child( + v_flex() + .gap_2() + .rounded_md() + .max_w_64() + .items_start() + .child( + Label::new("You can create a debug task by creating a new task and setting the `type` key to `debug`") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + h_flex().w_full().justify_end().child( + Button::new( + "start-debugger", + "Choose a debugger", + ) + .label_size(LabelSize::Small) + .on_click(move |_, cx| { + cx.dispatch_action(StartDebugger.boxed_clone()); + }) + ), + ), + ), + ) + } else { + this.child(self.pane.clone()) + } + }) .into_any() } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 5641810e442813..56d39a5b93a459 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -27,6 +27,7 @@ pub enum ThreadEntry { Scope(Scope), Variable { depth: usize, + scope: Scope, variable: Arc, has_children: bool, }, @@ -286,10 +287,11 @@ impl DebugPanelItem { ThreadEntry::Scope(scope) => self.render_scope(scope, cx), ThreadEntry::Variable { depth, + scope, variable, has_children, .. - } => self.render_variable(variable, *depth, *has_children, cx), + } => self.render_variable(ix, variable, scope, *depth, *has_children, cx), } } @@ -297,8 +299,11 @@ impl DebugPanelItem { SharedString::from(format!("scope-{}", scope.variables_reference)) } - fn variable_entry_id(variable: &Variable, depth: usize) -> SharedString { - SharedString::from(format!("variable-{}-{}", depth, variable.name)) + fn variable_entry_id(variable: &Variable, scope: &Scope, depth: usize) -> SharedString { + SharedString::from(format!( + "variable-{}-{}-{}", + depth, scope.variables_reference, variable.name + )) } fn render_scope(&self, scope: &Scope, cx: &mut ViewContext) -> AnyElement { @@ -329,12 +334,15 @@ impl DebugPanelItem { fn render_variable( &self, + ix: usize, variable: &Variable, + scope: &Scope, depth: usize, has_children: bool, cx: &mut ViewContext, ) -> AnyElement { - let variable_id = Self::variable_entry_id(variable, depth); + let variable_reference = variable.variables_reference; + let variable_id = Self::variable_entry_id(variable, scope, depth); let disclosed = has_children.then(|| self.open_entries.binary_search(&variable_id).is_ok()); @@ -349,11 +357,79 @@ impl DebugPanelItem { .indent_step_size(px(20.)) .always_show_disclosure_icon(true) .toggle(disclosed) - .on_toggle( - cx.listener(move |this, _, cx| { - this.toggle_entry_collapsed(&variable_id, cx) - }), - ) + .on_toggle(cx.listener(move |this, _, cx| { + if !has_children { + return; + } + + // if we already opend the variable/we already fetched it + // we can just toggle it because we already have the nested variable + if disclosed.unwrap_or(true) + || this + .current_thread_state() + .vars + .contains_key(&variable_reference) + { + return this.toggle_entry_collapsed(&variable_id, cx); + } + + let Some(entries) = this + .stack_frame_entries + .get(&this.current_thread_state().current_stack_frame_id) + else { + return; + }; + + let Some(entry) = entries.get(ix) else { + return; + }; + + if let ThreadEntry::Variable { scope, depth, .. } = entry { + let variable_id = variable_id.clone(); + let client = this.client.clone(); + let scope = scope.clone(); + let depth = *depth; + cx.spawn(|this, mut cx| async move { + let variables = client.variables(variable_reference).await?; + + this.update(&mut cx, |this, cx| { + let client = this.client.clone(); + let mut thread_states = client.thread_states(); + let Some(thread_state) = thread_states.get_mut(&this.thread_id) + else { + return; + }; + + if let Some(state) = thread_state + .variables + .get_mut(&thread_state.current_stack_frame_id) + .and_then(|s| s.get_mut(&scope)) + { + let position = state.iter().position(|(d, v)| { + Self::variable_entry_id(v, &scope, *d) == variable_id + }); + + if let Some(position) = position { + state.splice( + position + 1..position + 1, + variables + .clone() + .into_iter() + .map(|v| (depth + 1, v)), + ); + } + + thread_state.vars.insert(variable_reference, variables); + } + + drop(thread_states); + + this.toggle_entry_collapsed(&variable_id, cx); + }) + }) + .detach_and_log_err(cx); + } + })) .child( h_flex() .gap_1() @@ -478,7 +554,7 @@ impl DebugPanelItem { if self .open_entries - .binary_search(&Self::variable_entry_id(variable, *depth)) + .binary_search(&Self::variable_entry_id(&variable, &scope, *depth)) .is_err() { if depth_check.is_none() || depth_check.is_some_and(|d| d > *depth) { @@ -487,9 +563,10 @@ impl DebugPanelItem { } entries.push(ThreadEntry::Variable { + has_children, depth: *depth, + scope: scope.clone(), variable: Arc::new(variable.clone()), - has_children, }); } } From db8b8beb70ae2e099fdb7072413a205c574ecf3d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 21 Aug 2024 13:53:55 +0200 Subject: [PATCH 195/650] Fix typo --- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 56d39a5b93a459..bb743e0358d770 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -362,7 +362,7 @@ impl DebugPanelItem { return; } - // if we already opend the variable/we already fetched it + // if we already opened the variable/we already fetched it // we can just toggle it because we already have the nested variable if disclosed.unwrap_or(true) || this From fb169af400909b9e77be7f3492c66b0b5e87c4cb Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 21 Aug 2024 11:23:17 -0400 Subject: [PATCH 196/650] Add todo to fix bug & bug information Co-authored-by: Remco Smits --- crates/editor/src/editor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3861de8effe300..759f2431a4ef15 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5196,12 +5196,13 @@ impl Editor { /// TODO Debugger: Use this function to color toggle symbols that house nested breakpoints fn active_breakpoint_points(&mut self, cx: &mut ViewContext) -> HashSet { let mut breakpoint_display_points = HashSet::default(); - let snapshot = self.snapshot(cx); let Some(opened_breakpoints) = self.breakpoints.clone() else { return breakpoint_display_points; }; + let snapshot = self.snapshot(cx); + let opened_breakpoints = opened_breakpoints.read(); if let Some(buffer) = self.buffer.read(cx).as_singleton() { @@ -5211,6 +5212,8 @@ impl Editor { for breakpoint in breakpoints { breakpoint_display_points .insert(breakpoint.position.to_display_point(&snapshot)); + // Breakpoints TODO: Multibuffer bp toggle failing here + // dued to invalid excerpt id. Multibuffer excerpt id isn't the same as a singular buffer id } }; From 7885da1a2c4db3b1b73b2cb2c40c12f26b412995 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 21 Aug 2024 21:24:12 +0200 Subject: [PATCH 197/650] Fix typos --- crates/dap/docs/breakpoints.md | 2 +- crates/editor/src/editor.rs | 4 ++-- crates/workspace/src/persistence.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/dap/docs/breakpoints.md b/crates/dap/docs/breakpoints.md index 53d6fef0285f0c..8b819b089bf8c4 100644 --- a/crates/dap/docs/breakpoints.md +++ b/crates/dap/docs/breakpoints.md @@ -5,5 +5,5 @@ as well as serializing breakpoints to save. At a high level project serializes the positions of breakpoints that don't belong to any active buffers and handles converting breakpoints from serializing to active whenever a buffer is opened/closed. -`Project` also handles sending all relavent breakpoint information to debug adapter's +`Project` also handles sending all relevant breakpoint information to debug adapter's during debugging or when starting a debugger. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1c012d8e29b23e..785e7657e11c62 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5242,10 +5242,10 @@ impl Editor { let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; - for excerpt_boundery in + for excerpt_boundary in multi_buffer_snapshot.excerpt_boundaries_in_range(Point::new(0, 0)..) { - let info = excerpt_boundery.next.as_ref(); + let info = excerpt_boundary.next.as_ref(); if let Some(info) = info { let Some(excerpt_ranges) = diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c12a63adf91c8e..0d55a6b3ece2ae 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -145,7 +145,7 @@ pub struct Breakpoint { pub position: u32, } -/// This struct is used to implment traits on Vec +/// This struct is used to implement traits on Vec #[derive(Debug)] #[allow(dead_code)] struct Breakpoints(Vec); From 3c98e8986fd1f322efe6225749a8713d867607ed Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 21 Aug 2024 22:04:39 +0200 Subject: [PATCH 198/650] Fix failing test --- Cargo.lock | 1 + crates/recent_projects/Cargo.toml | 3 ++- crates/recent_projects/src/recent_projects.rs | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 44798e4593243e..c92ce68d8b556a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8618,6 +8618,7 @@ dependencies = [ "anyhow", "auto_update", "client", + "dap", "dev_server_projects", "editor", "futures 0.3.30", diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index da4ee210e1acfe..3198cc6686b225 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -42,9 +42,10 @@ util.workspace = true workspace.workspace = true [dev-dependencies] +dap = { workspace = true } editor = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } serde_json.workspace = true +settings = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index fcb6ffef423591..71806125df65d8 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -5,6 +5,8 @@ mod ssh_remotes; pub use ssh_connections::open_ssh_project; use client::{DevServerProjectId, ProjectId}; +#[cfg(test)] +use dap::debugger_settings::DebuggerSettings; use dev_servers::reconnect_to_dev_server_project; pub use dev_servers::DevServerProjects; use disconnected_overlay::DisconnectedOverlay; @@ -857,6 +859,7 @@ mod tests { crate::init(cx); editor::init(cx); workspace::init_settings(cx); + DebuggerSettings::register(cx); Project::init_settings(cx); state }) From 651c31c338f0600a7b28fbbf310d062cb79f2ac9 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 21 Aug 2024 22:33:00 +0200 Subject: [PATCH 199/650] Change icons from vscode to lucide (#21) --- assets/icons/debug-continue.svg | 2 +- assets/icons/debug-disconnect.svg | 2 +- assets/icons/debug-pause.svg | 2 +- assets/icons/debug-restart.svg | 2 +- assets/icons/debug-step-into.svg | 6 +++++- assets/icons/debug-step-out.svg | 6 +++++- assets/icons/debug-step-over.svg | 6 +++++- assets/icons/debug-stop.svg | 2 +- assets/icons/debug.svg | 1 + 9 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 assets/icons/debug.svg diff --git a/assets/icons/debug-continue.svg b/assets/icons/debug-continue.svg index a6d8eb74ab1fd6..e2a99c38d032fc 100644 --- a/assets/icons/debug-continue.svg +++ b/assets/icons/debug-continue.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/debug-disconnect.svg b/assets/icons/debug-disconnect.svg index 3435eebaa07c2e..0eb253715288fc 100644 --- a/assets/icons/debug-disconnect.svg +++ b/assets/icons/debug-disconnect.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug-pause.svg b/assets/icons/debug-pause.svg index 099a2ccdb9ab1f..bea531bc5a755b 100644 --- a/assets/icons/debug-pause.svg +++ b/assets/icons/debug-pause.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/debug-restart.svg b/assets/icons/debug-restart.svg index 9eedf6702d95e6..4eff13b94b698d 100644 --- a/assets/icons/debug-restart.svg +++ b/assets/icons/debug-restart.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/debug-step-into.svg b/assets/icons/debug-step-into.svg index 1a684d7a50266c..69e5cff3f176ca 100644 --- a/assets/icons/debug-step-into.svg +++ b/assets/icons/debug-step-into.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/assets/icons/debug-step-out.svg b/assets/icons/debug-step-out.svg index 253190bccd7d41..680e13e65e0414 100644 --- a/assets/icons/debug-step-out.svg +++ b/assets/icons/debug-step-out.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/assets/icons/debug-step-over.svg b/assets/icons/debug-step-over.svg index e7256555ab5023..005b901da3c49b 100644 --- a/assets/icons/debug-step-over.svg +++ b/assets/icons/debug-step-over.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/assets/icons/debug-stop.svg b/assets/icons/debug-stop.svg index 9a63e2191f5b1c..fef651c5864a15 100644 --- a/assets/icons/debug-stop.svg +++ b/assets/icons/debug-stop.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg new file mode 100644 index 00000000000000..8cea0c460402fb --- /dev/null +++ b/assets/icons/debug.svg @@ -0,0 +1 @@ + From bbb449c1b7fe5a3bd68d0c24a39db0b46ea3d59f Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 21 Aug 2024 17:08:45 -0400 Subject: [PATCH 200/650] Get debugger panel buttons to trigger while panel isn't focused --- Cargo.lock | 1 + crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/debugger_panel.rs | 4 + crates/debugger_ui/src/debugger_panel_item.rs | 150 +++++++++++++----- crates/debugger_ui/src/lib.rs | 22 ++- crates/recent_projects/src/recent_projects.rs | 3 +- 6 files changed, 128 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c92ce68d8b556a..e843bcecfcb732 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3310,6 +3310,7 @@ dependencies = [ "editor", "futures 0.3.30", "gpui", + "log", "project", "serde", "serde_json", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 3f3fb99c79fbc5..4fec15fae220f0 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -20,6 +20,7 @@ serde_json.workspace = true settings.workspace = true task.workspace = true tasks_ui.workspace = true +log.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 2be624f1842494..f0c53b87821b79 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -648,6 +648,10 @@ impl FocusableView for DebugPanel { } impl Panel for DebugPanel { + fn pane(&self) -> Option> { + Some(self.pane.clone()) + } + fn persistent_name() -> &'static str { "DebugPanel" } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index bb743e0358d770..1a057fcbd75649 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -6,14 +6,17 @@ use dap::{ }; use editor::Editor; use gpui::{ - actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, + impl_actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, ListState, Subscription, View, WeakView, }; +use serde::Deserialize; use std::collections::HashMap; use std::sync::Arc; use ui::{prelude::*, Tooltip}; use ui::{ListItem, WindowContext}; +use workspace::dock::Panel; use workspace::item::{Item, ItemEvent}; +use workspace::Workspace; #[derive(PartialEq, Eq)] enum ThreadItem { @@ -46,10 +49,30 @@ pub struct DebugPanelItem { _subscriptions: Vec, } -actions!( - debug_panel_item, - [Continue, StepOver, StepIn, StepOut, Restart, Pause, Stop, Disconnect] -); +impl_actions!(debug_panel_item, [DebugItemAction]); + +/// This struct is for actions that should be triggered even when +/// the debug pane is not in focus. This is done by setting workspace +/// as the action listener then having workspace call `handle_workspace_action` +#[derive(Clone, Deserialize, PartialEq, Default)] +pub struct DebugItemAction { + kind: DebugPanelItemActionKind, +} + +/// Actions that can be sent to workspace +/// currently all of these are button toggles +#[derive(Deserialize, PartialEq, Default, Clone, Debug)] +enum DebugPanelItemActionKind { + #[default] + Continue, + StepOver, + StepIn, + StepOut, + Restart, + Pause, + Stop, + Disconnect, +} impl DebugPanelItem { pub fn new( @@ -601,7 +624,46 @@ impl DebugPanelItem { }) } - fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext) { + /// Actions that should be handled even when Debug Panel is not in focus + pub fn workspace_action_handler( + workspace: &mut Workspace, + action: &DebugItemAction, + cx: &mut ViewContext, + ) { + let Some(pane) = workspace + .panel::(cx) + .and_then(|panel| panel.read(cx).pane()) + else { + log::error!( + "Can't get Debug panel to handle Debug action: {:?} + This shouldn't happen because there has to be an Debug panel to click a button and trigger this action", + action.kind + ); + return; + }; + + pane.update(cx, |this, cx| { + let Some(active_item) = this + .active_item() + .and_then(|item| item.downcast::()) + else { + return; + }; + + active_item.update(cx, |item, cx| match action.kind { + DebugPanelItemActionKind::Stop => item.handle_stop_action(cx), + DebugPanelItemActionKind::Continue => item.handle_continue_action(cx), + DebugPanelItemActionKind::StepIn => item.handle_step_in_action(cx), + DebugPanelItemActionKind::StepOut => item.handle_step_out_action(cx), + DebugPanelItemActionKind::StepOver => item.handle_step_over_action(cx), + DebugPanelItemActionKind::Pause => item.handle_pause_action(cx), + DebugPanelItemActionKind::Disconnect => item.handle_disconnect_action(cx), + DebugPanelItemActionKind::Restart => item.handle_restart_action(cx), + }); + }); + } + + fn handle_continue_action(&mut self, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; let previous_status = self.current_thread_state().status; @@ -614,7 +676,7 @@ impl DebugPanelItem { .detach_and_log_err(cx); } - fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext) { + fn handle_step_over_action(&mut self, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; let previous_status = self.current_thread_state().status; @@ -627,7 +689,7 @@ impl DebugPanelItem { .detach_and_log_err(cx); } - fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext) { + fn handle_step_in_action(&mut self, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; let previous_status = self.current_thread_state().status; @@ -640,7 +702,7 @@ impl DebugPanelItem { .detach_and_log_err(cx); } - fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext) { + fn handle_step_out_action(&mut self, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; let previous_status = self.current_thread_state().status; @@ -653,7 +715,7 @@ impl DebugPanelItem { .detach_and_log_err(cx); } - fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext) { + fn handle_restart_action(&mut self, cx: &mut ViewContext) { let client = self.client.clone(); cx.background_executor() @@ -661,7 +723,7 @@ impl DebugPanelItem { .detach_and_log_err(cx); } - fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext) { + fn handle_pause_action(&mut self, cx: &mut ViewContext) { let client = self.client.clone(); let thread_id = self.thread_id; cx.background_executor() @@ -669,7 +731,7 @@ impl DebugPanelItem { .detach_and_log_err(cx); } - fn handle_stop_action(&mut self, _: &Stop, cx: &mut ViewContext) { + fn handle_stop_action(&mut self, cx: &mut ViewContext) { let client = self.client.clone(); let thread_ids = vec![self.thread_id; 1]; @@ -678,7 +740,7 @@ impl DebugPanelItem { .detach_and_log_err(cx); } - fn handle_disconnect_action(&mut self, _: &Disconnect, cx: &mut ViewContext) { + fn handle_disconnect_action(&mut self, cx: &mut ViewContext) { let client = self.client.clone(); cx.background_executor() .spawn(async move { client.disconnect(None, Some(true), None).await }) @@ -709,14 +771,6 @@ impl Render for DebugPanelItem { h_flex() .key_context("DebugPanelItem") .track_focus(&self.focus_handle) - .capture_action(cx.listener(Self::handle_continue_action)) - .capture_action(cx.listener(Self::handle_step_over_action)) - .capture_action(cx.listener(Self::handle_step_in_action)) - .capture_action(cx.listener(Self::handle_step_out_action)) - .capture_action(cx.listener(Self::handle_restart_action)) - .capture_action(cx.listener(Self::handle_pause_action)) - .capture_action(cx.listener(Self::handle_stop_action)) - .capture_action(cx.listener(Self::handle_disconnect_action)) .p_2() .size_full() .items_start() @@ -733,7 +787,9 @@ impl Render for DebugPanelItem { this.child( IconButton::new("debug-pause", IconName::DebugPause) .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(Pause)) + cx.dispatch_action(Box::new(DebugItemAction { + kind: DebugPanelItemActionKind::Pause, + })) })) .tooltip(move |cx| Tooltip::text("Pause program", cx)), ) @@ -741,7 +797,9 @@ impl Render for DebugPanelItem { this.child( IconButton::new("debug-continue", IconName::DebugContinue) .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(Continue)) + cx.dispatch_action(Box::new(DebugItemAction { + kind: DebugPanelItemActionKind::Continue, + })) })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| { @@ -753,38 +811,40 @@ impl Render for DebugPanelItem { .child( IconButton::new("debug-step-over", IconName::DebugStepOver) .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(StepOver)) + cx.dispatch_action(Box::new(DebugItemAction { + kind: DebugPanelItemActionKind::StepOver, + })) })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Step over", cx)), ) .child( IconButton::new("debug-step-in", IconName::DebugStepInto) - .on_click( - cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(StepIn)) - }), - ) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(DebugItemAction { + kind: DebugPanelItemActionKind::StepIn, + })) + })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Step in", cx)), ) .child( IconButton::new("debug-step-out", IconName::DebugStepOut) - .on_click( - cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(StepOut)) - }), - ) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(DebugItemAction { + kind: DebugPanelItemActionKind::StepOut, + })) + })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Step out", cx)), ) .child( IconButton::new("debug-restart", IconName::DebugRestart) - .on_click( - cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(Restart)) - }), - ) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(DebugItemAction { + kind: DebugPanelItemActionKind::Restart, + })) + })) .disabled( !self .client @@ -798,9 +858,11 @@ impl Render for DebugPanelItem { ) .child( IconButton::new("debug-stop", IconName::DebugStop) - .on_click( - cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop))), - ) + .on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(Box::new(DebugItemAction { + kind: DebugPanelItemActionKind::Stop, + })) + })) .disabled( thread_status != ThreadStatus::Stopped && thread_status != ThreadStatus::Running, @@ -810,7 +872,9 @@ impl Render for DebugPanelItem { .child( IconButton::new("debug-disconnect", IconName::DebugDisconnect) .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(Disconnect)) + cx.dispatch_action(Box::new(DebugItemAction { + kind: DebugPanelItemActionKind::Disconnect, + })) })) .disabled( thread_status == ThreadStatus::Exited diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 11c06e3feeaf9c..ccfb4a3c0e85cd 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,7 +1,9 @@ use dap::debugger_settings::DebuggerSettings; use debugger_panel::{DebugPanel, ToggleFocus}; +use debugger_panel_item::DebugPanelItem; use gpui::AppContext; use settings::Settings; +use ui::ViewContext; use workspace::{StartDebugger, Workspace}; pub mod debugger_panel; @@ -10,13 +12,17 @@ mod debugger_panel_item; pub fn init(cx: &mut AppContext) { DebuggerSettings::register(cx); - cx.observe_new_views(|workspace: &mut Workspace, _| { - workspace.register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); - }); - workspace.register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| { - tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach(); - }); - }) + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace + .register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }) + .register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| { + tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach(); + }) + .register_action(DebugPanelItem::workspace_action_handler); + }, + ) .detach(); } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 71806125df65d8..c1e4bdc8b06eeb 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -5,8 +5,6 @@ mod ssh_remotes; pub use ssh_connections::open_ssh_project; use client::{DevServerProjectId, ProjectId}; -#[cfg(test)] -use dap::debugger_settings::DebuggerSettings; use dev_servers::reconnect_to_dev_server_project; pub use dev_servers::DevServerProjects; use disconnected_overlay::DisconnectedOverlay; @@ -711,6 +709,7 @@ impl Render for MatchTooltip { mod tests { use std::path::PathBuf; + use dap::debugger_settings::DebuggerSettings; use editor::Editor; use gpui::{TestAppContext, UpdateGlobal, WindowHandle}; use project::{project_settings::ProjectSettings, Project}; From 149116e76fe4a54fae94e3bd4e316a8502515634 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 22 Aug 2024 07:17:50 +0200 Subject: [PATCH 201/650] Close debug panel tabs when client stopped (#20) --- crates/debugger_ui/src/debugger_panel.rs | 98 +++++++++++-------- crates/debugger_ui/src/debugger_panel_item.rs | 15 +++ crates/project/src/project.rs | 13 ++- 3 files changed, 83 insertions(+), 43 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index f0c53b87821b79..ff152ee6182f81 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -34,6 +34,7 @@ pub enum DebugPanelEvent { Stopped((DebugAdapterClientId, StoppedEvent)), Thread((DebugAdapterClientId, ThreadEvent)), Output((DebugAdapterClientId, OutputEvent)), + ClientStopped(DebugAdapterClientId), } actions!(debug_panel, [ToggleFocus]); @@ -74,7 +75,9 @@ impl DebugPanel { cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { project::Event::DebugClientEvent { payload, client_id } => { - let client = this.debug_client_by_id(*client_id, cx); + let Some(client) = this.debug_client_by_id(*client_id, cx) else { + return cx.emit(DebugPanelEvent::ClientStopped(*client_id)); + }; match payload { Payload::Event(event) => { @@ -92,21 +95,28 @@ impl DebugPanel { } } project::Event::DebugClientStarted(client_id) => { - let client = this.debug_client_by_id(*client_id, cx); - cx.spawn(|_, _| async move { - client.initialize().await?; - - // send correct request based on adapter config - match client.config().request { - DebugRequestType::Launch => { - client.launch(client.request_args()).await + let Some(client) = this.debug_client_by_id(*client_id, cx) else { + return cx.emit(DebugPanelEvent::ClientStopped(*client_id)); + }; + + cx.background_executor() + .spawn(async move { + client.initialize().await?; + + // send correct request based on adapter config + match client.config().request { + DebugRequestType::Launch => { + client.launch(client.request_args()).await + } + DebugRequestType::Attach => { + client.attach(client.request_args()).await + } } - DebugRequestType::Attach => { - client.attach(client.request_args()).await - } - } - }) - .detach_and_log_err(cx); + }) + .detach_and_log_err(cx); + } + project::Event::DebugClientStopped(client_id) => { + cx.emit(DebugPanelEvent::ClientStopped(*client_id)); } _ => {} } @@ -136,15 +146,13 @@ impl DebugPanel { &self, client_id: DebugAdapterClientId, cx: &mut ViewContext, - ) -> Arc { + ) -> Option> { self.workspace .update(cx, |this, cx| { - this.project() - .read(cx) - .debug_adapter_by_id(client_id) - .unwrap() + this.project().read(cx).debug_adapter_by_id(client_id) }) - .unwrap() + .ok() + .flatten() } fn handle_pane_event( @@ -153,24 +161,32 @@ impl DebugPanel { event: &pane::Event, cx: &mut ViewContext, ) { - if let pane::Event::RemovedItem { item } = event { - let thread_panel = item.downcast::().unwrap(); + match event { + pane::Event::RemovedItem { item } => { + let thread_panel = item.downcast::().unwrap(); - thread_panel.update(cx, |pane, cx| { - let thread_id = pane.thread_id(); - let client = pane.client(); - let thread_status = client.thread_state_by_id(thread_id).status; + thread_panel.update(cx, |pane, cx| { + let thread_id = pane.thread_id(); + let client = pane.client(); + let thread_status = client.thread_state_by_id(thread_id).status; - // only terminate thread if the thread has not yet ended - if thread_status != ThreadStatus::Ended && thread_status != ThreadStatus::Exited { - let client = client.clone(); - cx.spawn(|_, _| async move { - client.terminate_threads(Some(vec![thread_id; 1])).await - }) - .detach_and_log_err(cx); - } - }); - }; + // only terminate thread if the thread has not yet ended + if thread_status != ThreadStatus::Ended && thread_status != ThreadStatus::Exited + { + let client = client.clone(); + cx.background_executor() + .spawn(async move { + client.terminate_threads(Some(vec![thread_id; 1])).await + }) + .detach_and_log_err(cx); + } + }); + } + pane::Event::Remove => cx.emit(PanelEvent::Close), + pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), + pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), + _ => {} + } } fn handle_start_debugging_request( @@ -500,7 +516,7 @@ impl DebugPanel { this.workspace .update(cx, |_, cx| { - this.pane.update(cx, |this, cx| { + this.pane.update(cx, |_, cx| { let tab = cx.new_view(|cx| { DebugPanelItem::new( debug_panel, @@ -510,8 +526,10 @@ impl DebugPanel { ) }); - this.add_item(Box::new(tab), false, false, None, cx) - }) + cx.emit(pane::Event::AddItem { + item: Box::new(tab), + }); + }); }) .log_err(); } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 1a057fcbd75649..7ba2689bf0c5d2 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -117,6 +117,9 @@ impl DebugPanelItem { DebugPanelEvent::Output((client_id, event)) => { Self::handle_output_event(this, client_id, event, cx) } + DebugPanelEvent::ClientStopped(client_id) => { + Self::handle_client_stopped_event(this, client_id, cx) + } }; } })]; @@ -218,6 +221,18 @@ impl DebugPanelItem { cx.notify(); }); } + + fn handle_client_stopped_event( + this: &mut Self, + client_id: &DebugAdapterClientId, + cx: &mut ViewContext, + ) { + if Self::should_skip_event(this, client_id, this.thread_id) { + return; + } + + cx.emit(ItemEvent::CloseItem); + } } impl EventEmitter for DebugPanelItem {} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b35fc8adea6330..3001807f098da0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -332,6 +332,7 @@ pub enum Event { LanguageServerPrompt(LanguageServerPromptRequest), LanguageNotFound(Model), DebugClientStarted(DebugAdapterClientId), + DebugClientStopped(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, payload: Payload, @@ -1544,12 +1545,18 @@ impl Project { }; if !should_terminate { - return; + return cx.emit(Event::DebugClientStopped(client_id)); } if let DebugAdapterClientState::Running(client) = debug_client { - cx.spawn(|_, _| async move { client.terminate().await }) - .detach_and_log_err(cx) + cx.spawn(|project, mut cx| async move { + client.terminate().await.log_err(); + + project.update(&mut cx, |_, cx| { + cx.emit(Event::DebugClientStopped(client.id())) + }) + }) + .detach_and_log_err(cx) } } From f3e7129479d3f87fb1a668498742e00a7ef5f44b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 23 Aug 2024 20:39:58 +0200 Subject: [PATCH 202/650] Add debug icon to toggle debug panel (#23) * Add debug icon to toggle debug panel * Add setting to wether to show the debug button * Fix clippy --- assets/settings/default.json | 3 ++- crates/dap/src/debugger_settings.rs | 5 +++++ crates/debugger_ui/src/debugger_panel.rs | 12 +++++++++--- crates/ui/src/components/icon.rs | 2 ++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f428f5e88b4323..8997323387c4d3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1016,7 +1016,8 @@ "debugger": { // Save breakpoints across different Zed sessions - "save_breakpoints": true + "save_breakpoints": true, + "button": true }, // Configures the Context Server Protocol binaries diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index fb7647e0132874..031dc14cb5b9c4 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -6,6 +6,7 @@ use settings::{Settings, SettingsSources}; #[derive(Deserialize, Clone, Debug)] pub struct DebuggerSettings { pub save_breakpoints: bool, + pub button: bool, } #[derive(Default, Serialize, Deserialize, JsonSchema, Clone)] @@ -14,6 +15,10 @@ pub struct DebuggerSettingsContent { /// /// Default: true pub save_breakpoints: Option, + /// Whether to show the debug button in the status bar. + /// + /// Default: true + pub button: Option, } impl Settings for DebuggerSettings { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index ff152ee6182f81..fe1554bbb0e9f2 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,6 +1,7 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::debugger_settings::DebuggerSettings; use dap::requests::{Request, Scopes, StackTrace, StartDebugging}; use dap::transport::Payload; use dap::{client::DebugAdapterClient, transport::Events}; @@ -16,6 +17,7 @@ use gpui::{ Subscription, Task, View, ViewContext, WeakView, }; use serde_json::json; +use settings::Settings; use std::collections::BTreeMap; use std::path::Path; use std::sync::Arc; @@ -693,11 +695,15 @@ impl Panel for DebugPanel { } fn icon(&self, _cx: &WindowContext) -> Option { - None + Some(IconName::Debug) } - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { - None + fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str> { + if DebuggerSettings::get_global(cx).button { + Some("Debug Panel") + } else { + None + } } fn toggle_action(&self) -> Box { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 0e5fe237207ac4..570af8a2b051df 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -160,6 +160,7 @@ pub enum IconName { DebugStepInto, DebugStepOut, DebugRestart, + Debug, DebugStop, DebugDisconnect, DatabaseZap, @@ -331,6 +332,7 @@ impl IconName { IconName::Copy => "icons/copy.svg", IconName::CountdownTimer => "icons/countdown_timer.svg", IconName::Dash => "icons/dash.svg", + IconName::Debug => "icons/debug.svg", IconName::DebugPause => "icons/debug-pause.svg", IconName::DebugContinue => "icons/debug-continue.svg", IconName::DebugStepOver => "icons/debug-step-over.svg", From 9bcd03b755fe79244d11ad81fb55fdcaad56b3a7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 23 Aug 2024 20:55:49 +0200 Subject: [PATCH 203/650] Remove default methods --- crates/debugger_ui/src/debugger_panel.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index fe1554bbb0e9f2..bd35115a1469fb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -709,22 +709,6 @@ impl Panel for DebugPanel { fn toggle_action(&self) -> Box { Box::new(ToggleFocus) } - - fn icon_label(&self, _: &WindowContext) -> Option { - None - } - - fn is_zoomed(&self, _cx: &WindowContext) -> bool { - false - } - - fn starts_open(&self, _cx: &WindowContext) -> bool { - false - } - - fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} - - fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} } impl Render for DebugPanel { From 618d81a3de7d93ceef33df3e282d411c973df543 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 24 Aug 2024 15:25:00 +0200 Subject: [PATCH 204/650] Fix that debug tab was not showing anymore (#24) --- crates/debugger_ui/src/debugger_panel.rs | 31 ++++++++----------- crates/debugger_ui/src/debugger_panel_item.rs | 16 ++++++++-- crates/project/src/project.rs | 14 +++------ 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index bd35115a1469fb..04371342f4587b 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -187,6 +187,13 @@ impl DebugPanel { pane::Event::Remove => cx.emit(PanelEvent::Close), pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), + pane::Event::AddItem { item } => { + self.workspace + .update(cx, |workspace, cx| { + item.added_to_pane(workspace, self.pane.clone(), cx) + }) + .ok(); + } _ => {} } } @@ -515,25 +522,13 @@ impl DebugPanel { if !existing_item { let debug_panel = cx.view().clone(); + this.pane.update(cx, |pane, cx| { + let tab = cx.new_view(|cx| { + DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx) + }); - this.workspace - .update(cx, |_, cx| { - this.pane.update(cx, |_, cx| { - let tab = cx.new_view(|cx| { - DebugPanelItem::new( - debug_panel, - client.clone(), - thread_id, - cx, - ) - }); - - cx.emit(pane::Event::AddItem { - item: Box::new(tab), - }); - }); - }) - .log_err(); + pane.add_item(Box::new(tab), true, true, None, cx); + }); } cx.emit(DebugPanelEvent::Stopped((client_id, event))); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 7ba2689bf0c5d2..692aa575588319 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -18,6 +18,10 @@ use workspace::dock::Panel; use workspace::item::{Item, ItemEvent}; use workspace::Workspace; +pub enum Event { + Close, +} + #[derive(PartialEq, Eq)] enum ThreadItem { Variables, @@ -231,11 +235,11 @@ impl DebugPanelItem { return; } - cx.emit(ItemEvent::CloseItem); + cx.emit(Event::Close); } } -impl EventEmitter for DebugPanelItem {} +impl EventEmitter for DebugPanelItem {} impl FocusableView for DebugPanelItem { fn focus_handle(&self, _: &AppContext) -> FocusHandle { @@ -244,7 +248,7 @@ impl FocusableView for DebugPanelItem { } impl Item for DebugPanelItem { - type Event = ItemEvent; + type Event = Event; fn tab_content( &self, @@ -272,6 +276,12 @@ impl Item for DebugPanelItem { self.current_thread_state().status ))) } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } + } } impl DebugPanelItem { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3001807f098da0..3fe9a1c3f85700 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1544,19 +1544,15 @@ impl Project { return; }; + cx.emit(Event::DebugClientStopped(client_id)); + if !should_terminate { - return cx.emit(Event::DebugClientStopped(client_id)); + return; } if let DebugAdapterClientState::Running(client) = debug_client { - cx.spawn(|project, mut cx| async move { - client.terminate().await.log_err(); - - project.update(&mut cx, |_, cx| { - cx.emit(Event::DebugClientStopped(client.id())) - }) - }) - .detach_and_log_err(cx) + cx.spawn(|_, _| async move { client.terminate().await }) + .detach_and_log_err(cx) } } From 5a301fc83aa1adb31babdd367a1b28b25cdc1257 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 25 Aug 2024 12:31:03 +0200 Subject: [PATCH 205/650] Improve clear highlights performance (#25) * Only clear highlights op open tabs This much better so we don't have to open each path of each stack frame in each thread. * Don't open the same file twice for clearing thread highlights --- crates/debugger_ui/src/debugger_panel.rs | 70 +++++++++++------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 04371342f4587b..00adccc1f38e06 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -263,7 +263,6 @@ impl DebugPanel { pub async fn go_to_stack_frame( workspace: WeakView, stack_frame: StackFrame, - client: Arc, clear_highlights: bool, mut cx: AsyncWindowContext, ) -> Result<()> { @@ -272,7 +271,7 @@ impl DebugPanel { let column = (stack_frame.column.saturating_sub(1)) as u32; if clear_highlights { - Self::remove_highlights(workspace.clone(), client, cx.clone()).await?; + Self::remove_highlights(workspace.clone(), cx.clone())?; } let task = workspace.update(&mut cx, |workspace, cx| { @@ -304,27 +303,18 @@ impl DebugPanel { }) } - async fn remove_highlights( - workspace: WeakView, - client: Arc, - cx: AsyncWindowContext, - ) -> Result<()> { - let mut tasks = Vec::new(); - for thread_state in client.thread_states().values() { - for stack_frame in thread_state.stack_frames.clone() { - tasks.push(Self::remove_editor_highlight( - workspace.clone(), - stack_frame, - cx.clone(), - )); - } - } - - if !tasks.is_empty() { - try_join_all(tasks).await?; - } + fn remove_highlights(workspace: WeakView, mut cx: AsyncWindowContext) -> Result<()> { + workspace.update(&mut cx, |workspace, cx| { + let editor_views = workspace + .items_of_type::(cx) + .collect::>>(); - anyhow::Ok(()) + for editor_view in editor_views { + editor_view.update(cx, |editor, _| { + editor.clear_row_highlights::(); + }); + } + }) } async fn remove_highlights_for_thread( @@ -334,14 +324,24 @@ impl DebugPanel { cx: AsyncWindowContext, ) -> Result<()> { let mut tasks = Vec::new(); - if let Some(thread_state) = client.thread_states().get(&thread_id) { - for stack_frame in thread_state.stack_frames.clone() { - tasks.push(Self::remove_editor_highlight( - workspace.clone(), - stack_frame.clone(), - cx.clone(), - )); + let mut paths: Vec = Vec::new(); + let thread_state = client.thread_state_by_id(thread_id); + + for stack_frame in thread_state.stack_frames.into_iter() { + let Some(path) = stack_frame.source.clone().and_then(|s| s.path.clone()) else { + continue; + }; + + if paths.contains(&path) { + continue; } + + paths.push(path.clone()); + tasks.push(Self::remove_editor_highlight( + workspace.clone(), + path, + cx.clone(), + )); } if !tasks.is_empty() { @@ -353,11 +353,9 @@ impl DebugPanel { async fn remove_editor_highlight( workspace: WeakView, - stack_frame: StackFrame, + path: String, mut cx: AsyncWindowContext, ) -> Result<()> { - let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); - let task = workspace.update(&mut cx, |workspace, cx| { let project_path = workspace.project().read_with(cx, |project, cx| { project.project_path_for_absolute_path(&Path::new(&path), cx) @@ -389,9 +387,7 @@ impl DebugPanel { let task = this.update(&mut cx, |this, cx| { this.workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - let client = client.clone(); - - project.send_breakpoints(client, cx) + project.send_breakpoints(client.clone(), cx) }) }) })??; @@ -540,12 +536,10 @@ impl DebugPanel { let pane = pane.read(cx); if pane.thread_id() == thread_id && pane.client().id() == client_id { let workspace = this.workspace.clone(); - let client = client.clone(); return cx.spawn(|_, cx| async move { Self::go_to_stack_frame( workspace, current_stack_frame.clone(), - client, true, cx, ) @@ -621,7 +615,7 @@ impl DebugPanel { let workspace = this.workspace.clone(); cx.spawn(|_, mut cx| async move { - Self::remove_highlights(workspace.clone(), client.clone(), cx.clone()).await?; + Self::remove_highlights(workspace.clone(), cx.clone())?; if restart_args.is_some() { client.disconnect(Some(true), None, None).await?; From 31b27e1e85b1b37ab9dbf508495aec10eaed2453 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 25 Aug 2024 15:31:56 +0200 Subject: [PATCH 206/650] Allow clicking on stack frame again (#26) --- crates/debugger_ui/src/debugger_panel.rs | 15 ++++++++++++-- crates/debugger_ui/src/debugger_panel_item.rs | 20 ++++++++++--------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 00adccc1f38e06..de9096bae2ef1c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -266,7 +266,12 @@ impl DebugPanel { clear_highlights: bool, mut cx: AsyncWindowContext, ) -> Result<()> { - let path = stack_frame.clone().source.unwrap().path.unwrap().clone(); + let Some(path) = &stack_frame.source.and_then(|s| s.path) else { + return Err(anyhow::anyhow!( + "Cannot go to stack frame, path doesn't exist" + )); + }; + let row = (stack_frame.line.saturating_sub(1)) as u32; let column = (stack_frame.column.saturating_sub(1)) as u32; @@ -520,7 +525,13 @@ impl DebugPanel { let debug_panel = cx.view().clone(); this.pane.update(cx, |pane, cx| { let tab = cx.new_view(|cx| { - DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx) + DebugPanelItem::new( + debug_panel, + this.workspace.clone(), + client.clone(), + thread_id, + cx, + ) }); pane.add_item(Box::new(tab), true, true, None, cx); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 692aa575588319..0ed0f7d566d2a0 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -51,6 +51,7 @@ pub struct DebugPanelItem { active_thread_item: ThreadItem, client: Arc, _subscriptions: Vec, + workspace: WeakView, } impl_actions!(debug_panel_item, [DebugItemAction]); @@ -81,6 +82,7 @@ enum DebugPanelItemActionKind { impl DebugPanelItem { pub fn new( debug_panel: View, + workspace: WeakView, client: Arc, thread_id: u64, cx: &mut ViewContext, @@ -145,6 +147,7 @@ impl DebugPanelItem { Self { client, thread_id, + workspace, focus_handle, variable_list, output_editor, @@ -529,19 +532,18 @@ impl DebugPanelItem { }) .on_click(cx.listener({ let stack_frame_id = stack_frame.id; + let stack_frame = stack_frame.clone(); move |this, _, cx| { this.update_stack_frame_id(stack_frame_id); - cx.notify(); - - // let client = this.client(); - // DebugPanel::go_to_stack_frame(&stack_frame, client, true, cx) - // .detach_and_log_err(cx); + let workspace = this.workspace.clone(); + let stack_frame = stack_frame.clone(); + cx.spawn(|_, cx| async move { + DebugPanel::go_to_stack_frame(workspace, stack_frame, true, cx).await + }) + .detach_and_log_err(cx); - // TODO: - // this.go_to_stack_frame(&stack_frame, this.client.clone(), false, cx) - // .detach_and_log_err(cx); - // cx.notify(); + cx.notify(); } })) .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) From 199b6657c6a1a2a4273e738b9e5817399f119f32 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 25 Aug 2024 19:29:59 +0200 Subject: [PATCH 207/650] Remove duplicated content code for debugger settings (#27) Refactored this because its being refactored inside: https://github.com/zed-industries/zed/pull/16744 --- crates/dap/src/debugger_settings.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index 031dc14cb5b9c4..b75c9dbc084698 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -3,28 +3,32 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; -#[derive(Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)] +#[serde(default)] pub struct DebuggerSettings { - pub save_breakpoints: bool, - pub button: bool, -} - -#[derive(Default, Serialize, Deserialize, JsonSchema, Clone)] -pub struct DebuggerSettingsContent { /// Whether the breakpoints should be reused across Zed sessions. /// /// Default: true - pub save_breakpoints: Option, + pub save_breakpoints: bool, /// Whether to show the debug button in the status bar. /// /// Default: true - pub button: Option, + pub button: bool, +} + +impl Default for DebuggerSettings { + fn default() -> Self { + Self { + button: true, + save_breakpoints: true, + } + } } impl Settings for DebuggerSettings { const KEY: Option<&'static str> = Some("debugger"); - type FileContent = DebuggerSettingsContent; + type FileContent = Self; fn load( sources: SettingsSources, From 045f927b4c27da6dae2d13267b79531eea161468 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 25 Aug 2024 19:44:01 +0200 Subject: [PATCH 208/650] Don't close the debug panel when debug session ends (#28) --- crates/debugger_ui/src/debugger_panel.rs | 1 + crates/workspace/src/pane.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index de9096bae2ef1c..243fb485f09d8d 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -65,6 +65,7 @@ impl DebugPanel { pane.set_can_navigate(true, cx); pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_| true); + pane.set_close_pane_if_empty(false, cx); pane }); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 6b6f346444ca2a..7925d9b81f09bc 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -264,6 +264,7 @@ pub struct Pane { display_nav_history_buttons: Option, double_click_dispatch_action: Option>, save_modals_spawned: HashSet, + close_pane_if_empty: bool, } pub struct ActivationHistoryEntry { @@ -469,6 +470,7 @@ impl Pane { _subscriptions: subscriptions, double_click_dispatch_action, save_modals_spawned: HashSet::default(), + close_pane_if_empty: true, } } @@ -597,6 +599,15 @@ impl Pane { cx.notify(); } + pub fn set_close_pane_if_empty( + &mut self, + close_pane_if_empty: bool, + cx: &mut ViewContext, + ) { + self.close_pane_if_empty = close_pane_if_empty; + cx.notify(); + } + pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext) { self.toolbar.update(cx, |toolbar, cx| { toolbar.set_can_navigate(can_navigate, cx); @@ -1308,7 +1319,7 @@ impl Pane { .iter() .position(|i| i.item_id() == item.item_id()) { - pane.remove_item(item_ix, false, true, cx); + pane.remove_item(item_ix, false, pane.close_pane_if_empty, cx); } }) .ok(); From 2b504b3248b53aab432ee08c1cb69adedc5999a4 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:36:46 -0400 Subject: [PATCH 209/650] Debugger elements (#29) --- crates/debugger_ui/src/debugger_panel_item.rs | 386 +++--------------- crates/debugger_ui/src/lib.rs | 1 + crates/debugger_ui/src/variable_list.rs | 313 ++++++++++++++ 3 files changed, 376 insertions(+), 324 deletions(-) create mode 100644 crates/debugger_ui/src/variable_list.rs diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 0ed0f7d566d2a0..25d84aad37bc41 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,4 +1,5 @@ use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; +use crate::variable_list::VariableList; use anyhow::Result; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::{ @@ -10,10 +11,9 @@ use gpui::{ FocusableView, ListState, Subscription, View, WeakView, }; use serde::Deserialize; -use std::collections::HashMap; use std::sync::Arc; +use ui::WindowContext; use ui::{prelude::*, Tooltip}; -use ui::{ListItem, WindowContext}; use workspace::dock::Panel; use workspace::item::{Item, ItemEvent}; use workspace::Workspace; @@ -42,18 +42,18 @@ pub enum ThreadEntry { pub struct DebugPanelItem { thread_id: u64, - variable_list: ListState, + variable_list: View, focus_handle: FocusHandle, stack_frame_list: ListState, output_editor: View, - open_entries: Vec, - stack_frame_entries: HashMap>, active_thread_item: ThreadItem, client: Arc, _subscriptions: Vec, workspace: WeakView, } +pub enum DebugPanelItemEvent {} + impl_actions!(debug_panel_item, [DebugItemAction]); /// This struct is for actions that should be triggered even when @@ -89,15 +89,8 @@ impl DebugPanelItem { ) -> Self { let focus_handle = cx.focus_handle(); - let weakview = cx.view().downgrade(); - let variable_list = - ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { - if let Some(view) = weakview.upgrade() { - view.update(cx, |view, cx| view.render_variable_list_entry(ix, cx)) - } else { - div().into_any() - } - }); + let model = cx.model().clone(); + let variable_list = cx.new_view(|cx| VariableList::new(model, cx)); let weakview = cx.view().downgrade(); let stack_frame_list = @@ -153,8 +146,6 @@ impl DebugPanelItem { output_editor, _subscriptions, stack_frame_list, - open_entries: Default::default(), - stack_frame_entries: Default::default(), active_thread_item: ThreadItem::Variables, } } @@ -181,7 +172,7 @@ impl DebugPanelItem { this.stack_frame_list.reset(thread_state.stack_frames.len()); if let Some(stack_frame) = thread_state.stack_frames.first() { - this.update_stack_frame_id(stack_frame.id); + this.update_stack_frame_id(stack_frame.id, cx); }; cx.notify(); @@ -240,54 +231,7 @@ impl DebugPanelItem { cx.emit(Event::Close); } -} - -impl EventEmitter for DebugPanelItem {} - -impl FocusableView for DebugPanelItem { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} -impl Item for DebugPanelItem { - type Event = Event; - - fn tab_content( - &self, - params: workspace::item::TabContentParams, - _: &WindowContext, - ) -> AnyElement { - Label::new(format!( - "{} - Thread {}", - self.client.config().id, - self.thread_id - )) - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() - } - - fn tab_tooltip_text(&self, _: &AppContext) -> Option { - Some(SharedString::from(format!( - "{} Thread {} - {:?}", - self.client.config().id, - self.thread_id, - self.current_thread_state().status - ))) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - Event::Close => f(ItemEvent::CloseItem), - } - } -} - -impl DebugPanelItem { pub fn client(&self) -> Arc { self.client.clone() } @@ -305,7 +249,7 @@ impl DebugPanelItem { .unwrap() } - fn current_thread_state(&self) -> ThreadState { + pub fn current_thread_state(&self) -> ThreadState { self.client .thread_states() .get(&self.thread_id) @@ -313,188 +257,15 @@ impl DebugPanelItem { .unwrap() } - fn update_stack_frame_id(&mut self, stack_frame_id: u64) { + fn update_stack_frame_id(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { self.client .update_current_stack_frame(self.thread_id, stack_frame_id); - self.open_entries.clear(); - - self.build_variable_list_entries(stack_frame_id, true); - } - - pub fn render_variable_list_entry( - &mut self, - ix: usize, - cx: &mut ViewContext, - ) -> AnyElement { - let Some(entries) = self - .stack_frame_entries - .get(&self.current_thread_state().current_stack_frame_id) - else { - return div().into_any_element(); - }; - - match &entries[ix] { - ThreadEntry::Scope(scope) => self.render_scope(scope, cx), - ThreadEntry::Variable { - depth, - scope, - variable, - has_children, - .. - } => self.render_variable(ix, variable, scope, *depth, *has_children, cx), - } - } - - fn scope_entry_id(scope: &Scope) -> SharedString { - SharedString::from(format!("scope-{}", scope.variables_reference)) - } - - fn variable_entry_id(variable: &Variable, scope: &Scope, depth: usize) -> SharedString { - SharedString::from(format!( - "variable-{}-{}-{}", - depth, scope.variables_reference, variable.name - )) - } - - fn render_scope(&self, scope: &Scope, cx: &mut ViewContext) -> AnyElement { - let element_id = scope.variables_reference; - - let scope_id = Self::scope_entry_id(scope); - let disclosed = self.open_entries.binary_search(&scope_id).is_ok(); - - div() - .id(element_id as usize) - .group("") - .flex() - .w_full() - .h_full() - .child( - ListItem::new(scope_id.clone()) - .indent_level(1) - .indent_step_size(px(20.)) - .always_show_disclosure_icon(true) - .toggle(disclosed) - .on_toggle( - cx.listener(move |this, _, cx| this.toggle_entry_collapsed(&scope_id, cx)), - ) - .child(div().text_ui(cx).w_full().child(scope.name.clone())), - ) - .into_any() - } - - fn render_variable( - &self, - ix: usize, - variable: &Variable, - scope: &Scope, - depth: usize, - has_children: bool, - cx: &mut ViewContext, - ) -> AnyElement { - let variable_reference = variable.variables_reference; - let variable_id = Self::variable_entry_id(variable, scope, depth); - - let disclosed = has_children.then(|| self.open_entries.binary_search(&variable_id).is_ok()); + let thread_state = self.current_thread_state(); - div() - .id(variable_id.clone()) - .group("") - .h_4() - .size_full() - .child( - ListItem::new(variable_id.clone()) - .indent_level(depth + 1) - .indent_step_size(px(20.)) - .always_show_disclosure_icon(true) - .toggle(disclosed) - .on_toggle(cx.listener(move |this, _, cx| { - if !has_children { - return; - } - - // if we already opened the variable/we already fetched it - // we can just toggle it because we already have the nested variable - if disclosed.unwrap_or(true) - || this - .current_thread_state() - .vars - .contains_key(&variable_reference) - { - return this.toggle_entry_collapsed(&variable_id, cx); - } - - let Some(entries) = this - .stack_frame_entries - .get(&this.current_thread_state().current_stack_frame_id) - else { - return; - }; - - let Some(entry) = entries.get(ix) else { - return; - }; - - if let ThreadEntry::Variable { scope, depth, .. } = entry { - let variable_id = variable_id.clone(); - let client = this.client.clone(); - let scope = scope.clone(); - let depth = *depth; - cx.spawn(|this, mut cx| async move { - let variables = client.variables(variable_reference).await?; - - this.update(&mut cx, |this, cx| { - let client = this.client.clone(); - let mut thread_states = client.thread_states(); - let Some(thread_state) = thread_states.get_mut(&this.thread_id) - else { - return; - }; - - if let Some(state) = thread_state - .variables - .get_mut(&thread_state.current_stack_frame_id) - .and_then(|s| s.get_mut(&scope)) - { - let position = state.iter().position(|(d, v)| { - Self::variable_entry_id(v, &scope, *d) == variable_id - }); - - if let Some(position) = position { - state.splice( - position + 1..position + 1, - variables - .clone() - .into_iter() - .map(|v| (depth + 1, v)), - ); - } - - thread_state.vars.insert(variable_reference, variables); - } - - drop(thread_states); - - this.toggle_entry_collapsed(&variable_id, cx); - }) - }) - .detach_and_log_err(cx); - } - })) - .child( - h_flex() - .gap_1() - .text_ui_sm(cx) - .child(variable.name.clone()) - .child( - div() - .text_ui_xs(cx) - .text_color(cx.theme().colors().text_muted) - .child(variable.value.clone()), - ), - ), - ) - .into_any() + self.variable_list.update(cx, |variable_list, cx| { + variable_list.build_entries(thread_state, true, cx) + }); } fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { @@ -534,7 +305,7 @@ impl DebugPanelItem { let stack_frame_id = stack_frame.id; let stack_frame = stack_frame.clone(); move |this, _, cx| { - this.update_stack_frame_id(stack_frame_id); + this.update_stack_frame_id(stack_frame_id, cx); let workspace = this.workspace.clone(); let stack_frame = stack_frame.clone(); @@ -563,69 +334,6 @@ impl DebugPanelItem { .into_any() } - pub fn build_variable_list_entries(&mut self, stack_frame_id: u64, open_first_scope: bool) { - let thread_state = self.current_thread_state(); - let Some(scopes_and_vars) = thread_state.variables.get(&stack_frame_id) else { - return; - }; - - let mut entries: Vec = Vec::default(); - for (scope, variables) in scopes_and_vars { - if variables.is_empty() { - continue; - } - - if open_first_scope && self.open_entries.is_empty() { - self.open_entries.push(Self::scope_entry_id(scope)); - } - - entries.push(ThreadEntry::Scope(scope.clone())); - - if self - .open_entries - .binary_search(&Self::scope_entry_id(scope)) - .is_err() - { - continue; - } - - let mut depth_check: Option = None; - - for (depth, variable) in variables { - if depth_check.is_some_and(|d| *depth > d) { - continue; - } - - if depth_check.is_some_and(|d| d >= *depth) { - depth_check = None; - } - - let has_children = variable.variables_reference > 0; - - if self - .open_entries - .binary_search(&Self::variable_entry_id(&variable, &scope, *depth)) - .is_err() - { - if depth_check.is_none() || depth_check.is_some_and(|d| d > *depth) { - depth_check = Some(*depth); - } - } - - entries.push(ThreadEntry::Variable { - has_children, - depth: *depth, - scope: scope.clone(), - variable: Arc::new(variable.clone()), - }); - } - } - - let len = entries.len(); - self.stack_frame_entries.insert(stack_frame_id, entries); - self.variable_list.reset(len); - } - // if the debug adapter does not send the continued event, // and the status of the thread did not change we have to assume the thread is running // so we have to update the thread state status to running @@ -662,10 +370,10 @@ impl DebugPanelItem { .and_then(|panel| panel.read(cx).pane()) else { log::error!( - "Can't get Debug panel to handle Debug action: {:?} - This shouldn't happen because there has to be an Debug panel to click a button and trigger this action", - action.kind - ); + "Can't get Debug panel to handle Debug action: {:?} + This shouldn't happen because there has to be an Debug panel to click a button and trigger this action", + action.kind + ); return; }; @@ -773,20 +481,51 @@ impl DebugPanelItem { .spawn(async move { client.disconnect(None, Some(true), None).await }) .detach_and_log_err(cx); } +} - fn toggle_entry_collapsed(&mut self, entry_id: &SharedString, cx: &mut ViewContext) { - match self.open_entries.binary_search(&entry_id) { - Ok(ix) => { - self.open_entries.remove(ix); - } - Err(ix) => { - self.open_entries.insert(ix, entry_id.clone()); - } - }; +impl EventEmitter for DebugPanelItem {} +impl EventEmitter for DebugPanelItem {} - self.build_variable_list_entries(self.current_thread_state().current_stack_frame_id, false); +impl FocusableView for DebugPanelItem { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} - cx.notify(); +impl Item for DebugPanelItem { + type Event = Event; + + fn tab_content( + &self, + params: workspace::item::TabContentParams, + _: &WindowContext, + ) -> AnyElement { + Label::new(format!( + "{} - Thread {}", + self.client.config().id, + self.thread_id + )) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() + } + + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(SharedString::from(format!( + "{} Thread {} - {:?}", + self.client.config().id, + self.thread_id, + self.current_thread_state().status + ))) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } } } @@ -972,8 +711,7 @@ impl Render for DebugPanelItem { ), ) .when(*active_thread_item == ThreadItem::Variables, |this| { - this.size_full() - .child(list(self.variable_list.clone()).gap_1_5().size_full()) + this.size_full().child(self.variable_list.clone()) }) .when(*active_thread_item == ThreadItem::Output, |this| { this.child(self.output_editor.clone()) diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index ccfb4a3c0e85cd..69145be5aa069b 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -8,6 +8,7 @@ use workspace::{StartDebugger, Workspace}; pub mod debugger_panel; mod debugger_panel_item; +mod variable_list; pub fn init(cx: &mut AppContext) { DebuggerSettings::register(cx); diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs new file mode 100644 index 00000000000000..ac73a324fd9b0a --- /dev/null +++ b/crates/debugger_ui/src/variable_list.rs @@ -0,0 +1,313 @@ +use crate::debugger_panel_item::{DebugPanelItem, DebugPanelItemEvent, ThreadEntry}; +use dap::{client::ThreadState, Scope, Variable}; + +use gpui::{list, AnyElement, ListState, Model, Subscription}; +use ui::{prelude::*, ListItem}; + +use std::{collections::HashMap, sync::Arc}; + +pub struct VariableList { + pub list: ListState, + debug_panel_item: Model, + open_entries: Vec, + stack_frame_entries: HashMap>, + _subscriptions: Vec, +} + +impl VariableList { + pub fn new(debug_panel_item: Model, cx: &mut ViewContext) -> Self { + let weakview = cx.view().downgrade(); + + let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { + weakview + .upgrade() + .map(|view| view.update(cx, |this, cx| this.render_entry(ix, cx))) + .unwrap_or(div().into_any()) + }); + + let _subscriptions = vec![cx.subscribe(&debug_panel_item, Self::handle_events)]; + + Self { + list, + debug_panel_item, + open_entries: Default::default(), + stack_frame_entries: Default::default(), + _subscriptions, + } + } + + fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let debug_item = self.debug_panel_item.read(cx); + let Some(entries) = self + .stack_frame_entries + .get(&debug_item.current_thread_state().current_stack_frame_id) + else { + return div().into_any_element(); + }; + + match &entries[ix] { + ThreadEntry::Scope(scope) => self.render_scope(scope, cx), + ThreadEntry::Variable { + depth, + scope, + variable, + has_children, + .. + } => self.render_variable(ix, variable, scope, *depth, *has_children, cx), + } + } + + fn handle_events( + &mut self, + _debug_panel_item: Model, + _event: &DebugPanelItemEvent, + _cx: &mut ViewContext, + ) { + } + + pub fn toggle_entry_collapsed(&mut self, entry_id: &SharedString, cx: &mut ViewContext) { + match self.open_entries.binary_search(&entry_id) { + Ok(ix) => { + self.open_entries.remove(ix); + } + Err(ix) => { + self.open_entries.insert(ix, entry_id.clone()); + } + }; + + let thread_state = self + .debug_panel_item + .read_with(cx, |panel, _cx| panel.current_thread_state()); + + self.build_entries(thread_state, false, cx); + cx.notify(); + } + + pub fn build_entries( + &mut self, + thread_state: ThreadState, + open_first_scope: bool, + _cx: &mut ViewContext, + ) { + let stack_frame_id = thread_state.current_stack_frame_id; + let Some(scopes_and_vars) = thread_state.variables.get(&stack_frame_id) else { + return; + }; + + let mut entries: Vec = Vec::default(); + for (scope, variables) in scopes_and_vars { + if variables.is_empty() { + continue; + } + + if open_first_scope && self.open_entries.is_empty() { + self.open_entries.push(scope_entry_id(scope)); + } + + entries.push(ThreadEntry::Scope(scope.clone())); + + if self + .open_entries + .binary_search(&scope_entry_id(scope)) + .is_err() + { + continue; + } + + let mut depth_check: Option = None; + + for (depth, variable) in variables { + if depth_check.is_some_and(|d| *depth > d) { + continue; + } + + if depth_check.is_some_and(|d| d >= *depth) { + depth_check = None; + } + + let has_children = variable.variables_reference > 0; + + if self + .open_entries + .binary_search(&variable_entry_id(&variable, &scope, *depth)) + .is_err() + { + if depth_check.is_none() || depth_check.is_some_and(|d| d > *depth) { + depth_check = Some(*depth); + } + } + + entries.push(ThreadEntry::Variable { + has_children, + depth: *depth, + scope: scope.clone(), + variable: Arc::new(variable.clone()), + }); + } + } + + let len = entries.len(); + self.stack_frame_entries.insert(stack_frame_id, entries); + self.list.reset(len); + } + + pub fn render_variable( + &self, + ix: usize, + variable: &Variable, + scope: &Scope, + depth: usize, + has_children: bool, + cx: &mut ViewContext, + ) -> AnyElement { + let variable_reference = variable.variables_reference; + let variable_id = variable_entry_id(variable, scope, depth); + + let disclosed = has_children.then(|| self.open_entries.binary_search(&variable_id).is_ok()); + + div() + .id(variable_id.clone()) + .group("") + .h_4() + .size_full() + .child( + ListItem::new(variable_id.clone()) + .indent_level(depth + 1) + .indent_step_size(px(20.)) + .always_show_disclosure_icon(true) + .toggle(disclosed) + .on_toggle(cx.listener(move |this, _, cx| { + if !has_children { + return; + } + + let debug_item = this.debug_panel_item.read(cx); + + // if we already opend the variable/we already fetched it + // we can just toggle it because we already have the nested variable + if disclosed.unwrap_or(true) + || debug_item + .current_thread_state() + .vars + .contains_key(&variable_reference) + { + return this.toggle_entry_collapsed(&variable_id, cx); + } + + let Some(entries) = this + .stack_frame_entries + .get(&debug_item.current_thread_state().current_stack_frame_id) + else { + return; + }; + + let Some(entry) = entries.get(ix) else { + return; + }; + + if let ThreadEntry::Variable { scope, depth, .. } = entry { + let variable_id = variable_id.clone(); + let client = debug_item.client(); + let scope = scope.clone(); + let depth = *depth; + + cx.spawn(|this, mut cx| async move { + let variables = client.variables(variable_reference).await?; + + this.update(&mut cx, |this, cx| { + let client = client.clone(); + let mut thread_states = client.thread_states(); + let Some(thread_state) = thread_states + .get_mut(&this.debug_panel_item.read(cx).thread_id()) + else { + return; + }; + + if let Some(state) = thread_state + .variables + .get_mut(&thread_state.current_stack_frame_id) + .and_then(|s| s.get_mut(&scope)) + { + let position = state.iter().position(|(d, v)| { + variable_entry_id(v, &scope, *d) == variable_id + }); + + if let Some(position) = position { + state.splice( + position + 1..position + 1, + variables + .clone() + .into_iter() + .map(|v| (depth + 1, v)), + ); + } + + thread_state.vars.insert(variable_reference, variables); + } + + drop(thread_states); + this.toggle_entry_collapsed(&variable_id, cx); + }) + }) + .detach_and_log_err(cx); + } + })) + .child( + h_flex() + .gap_1() + .text_ui_sm(cx) + .child(variable.name.clone()) + .child( + div() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .child(variable.value.clone()), + ), + ), + ) + .into_any() + } + + fn render_scope(&self, scope: &Scope, cx: &mut ViewContext) -> AnyElement { + let element_id = scope.variables_reference; + + let scope_id = scope_entry_id(scope); + let disclosed = self.open_entries.binary_search(&scope_id).is_ok(); + + div() + .id(element_id as usize) + .group("") + .flex() + .w_full() + .h_full() + .child( + ListItem::new(scope_id.clone()) + .indent_level(1) + .indent_step_size(px(20.)) + .always_show_disclosure_icon(true) + .toggle(disclosed) + .on_toggle( + cx.listener(move |this, _, cx| this.toggle_entry_collapsed(&scope_id, cx)), + ) + .child(div().text_ui(cx).w_full().child(scope.name.clone())), + ) + .into_any() + } +} + +impl Render for VariableList { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + list(self.list.clone()).gap_1_5().size_full() + } +} + +pub fn variable_entry_id(variable: &Variable, scope: &Scope, depth: usize) -> SharedString { + SharedString::from(format!( + "variable-{}-{}-{}", + depth, scope.variables_reference, variable.name + )) +} + +fn scope_entry_id(scope: &Scope) -> SharedString { + SharedString::from(format!("scope-{}", scope.variables_reference)) +} From 008b6b591b7dd10c4361ac7ba469e75146c857ab Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:46:37 -0400 Subject: [PATCH 210/650] Debugger console (#30) * Create debugger console * Get console to output console messages during output events * Use match expression in handle_output_event * Move debug console code to it's own file console --- Cargo.lock | 1 + crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/console.rs | 114 ++++++++++++++++++ crates/debugger_ui/src/debugger_panel_item.rs | 55 +++++++-- crates/debugger_ui/src/lib.rs | 1 + 5 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 crates/debugger_ui/src/console.rs diff --git a/Cargo.lock b/Cargo.lock index e843bcecfcb732..303fbf626bd967 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3317,6 +3317,7 @@ dependencies = [ "settings", "task", "tasks_ui", + "theme", "ui", "util", "workspace", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 4fec15fae220f0..17206635beb576 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -20,6 +20,7 @@ serde_json.workspace = true settings.workspace = true task.workspace = true tasks_ui.workspace = true +theme.workspace = true log.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs new file mode 100644 index 00000000000000..7f9ca4b5a2d04d --- /dev/null +++ b/crates/debugger_ui/src/console.rs @@ -0,0 +1,114 @@ +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{Render, Subscription, TextStyle, View, ViewContext}; +use settings::Settings; +use theme::ThemeSettings; +use ui::prelude::*; + +pub struct Console { + console: View, + query_bar: View, + _subscriptions: Vec, +} + +impl Console { + pub fn new(cx: &mut ViewContext) -> Self { + let console = cx.new_view(|cx| { + let mut editor = Editor::multi_line(cx); + editor.move_to_end(&editor::actions::MoveToEnd, cx); + editor.set_read_only(true); + editor.set_show_gutter(false, cx); + editor.set_show_inline_completions(false); + editor + }); + + let query_bar = cx.new_view(|cx| Editor::single_line(cx)); + + let _subscriptions = vec![]; + + Self { + console, + query_bar, + _subscriptions, + } + } + + pub fn add_message(&mut self, message: &str, cx: &mut ViewContext) { + self.console.update(cx, |console, cx| { + console.set_read_only(false); + console.move_to_end(&editor::actions::MoveToEnd, cx); + console.insert(format!("{}\n", message).as_str(), cx); + console.set_read_only(true); + }); + + cx.notify(); + } + + fn render_console(&self, cx: &ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.console.read(cx).read_only(cx) { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_size: rems(0.875).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(1.3), + ..Default::default() + }; + + EditorElement::new( + &self.console, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } + + fn render_query_bar(&self, cx: &ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.console.read(cx).read_only(cx) { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_size: rems(0.875).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(1.3), + ..Default::default() + }; + + EditorElement::new( + &self.query_bar, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } +} + +impl Render for Console { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + v_flex() + .size_full() + .id("Debugger Console") + .child(self.render_console(cx)) + .child( + div() + .child(self.render_query_bar(cx)) + .pt(Spacing::XSmall.rems(cx)), + ) + .border_2() + } +} diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 25d84aad37bc41..8f2d89817d1183 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,5 +1,7 @@ +use crate::console::Console; use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; use crate::variable_list::VariableList; + use anyhow::Result; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::{ @@ -43,6 +45,7 @@ pub enum ThreadEntry { pub struct DebugPanelItem { thread_id: u64, variable_list: View, + console: View, focus_handle: FocusHandle, stack_frame_list: ListState, output_editor: View, @@ -91,6 +94,7 @@ impl DebugPanelItem { let model = cx.model().clone(); let variable_list = cx.new_view(|cx| VariableList::new(model, cx)); + let console = cx.new_view(|cx| Console::new(cx)); let weakview = cx.view().downgrade(); let stack_frame_list = @@ -143,6 +147,7 @@ impl DebugPanelItem { workspace, focus_handle, variable_list, + console, output_editor, _subscriptions, stack_frame_list, @@ -201,23 +206,44 @@ impl DebugPanelItem { return; } - if event + // The default value of an event category is console + // so we assume that is the output type if it doesn't exist + let output_category = event .category .as_ref() - .map(|c| *c == OutputEventCategory::Telemetry) - .unwrap_or(false) - { - return; - } + .unwrap_or(&OutputEventCategory::Console); - this.output_editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.move_to_end(&editor::actions::MoveToEnd, cx); - editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); - editor.set_read_only(true); + match output_category { + OutputEventCategory::Console => { + this.console.update(cx, |console, cx| { + console.add_message(&event.output, cx); + }); + } + // OutputEventCategory::Stderr => {} + OutputEventCategory::Stdout => { + this.output_editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.move_to_end(&editor::actions::MoveToEnd, cx); + editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); + editor.set_read_only(true); - cx.notify(); - }); + cx.notify(); + }); + } + // OutputEventCategory::Unknown => {} + // OutputEventCategory::Important => {} + OutputEventCategory::Telemetry => {} + _ => { + this.output_editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.move_to_end(&editor::actions::MoveToEnd, cx); + editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); + editor.set_read_only(true); + + cx.notify(); + }); + } + } } fn handle_client_stopped_event( @@ -715,6 +741,9 @@ impl Render for DebugPanelItem { }) .when(*active_thread_item == ThreadItem::Output, |this| { this.child(self.output_editor.clone()) + }) + .when(*active_thread_item == ThreadItem::Console, |this| { + this.child(self.console.clone()) }), ) .into_any() diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 69145be5aa069b..aacf47f8510e4d 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -6,6 +6,7 @@ use settings::Settings; use ui::ViewContext; use workspace::{StartDebugger, Workspace}; +mod console; pub mod debugger_panel; mod debugger_panel_item; mod variable_list; From 3ac4d1eaacf27278414c1ee51ed49172d0eeaae4 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 26 Aug 2024 23:24:07 -0400 Subject: [PATCH 211/650] Fix CICD spell check error --- crates/debugger_ui/src/variable_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index ac73a324fd9b0a..6875681a052c8f 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -183,7 +183,7 @@ impl VariableList { let debug_item = this.debug_panel_item.read(cx); - // if we already opend the variable/we already fetched it + // if we already opened the variable/we already fetched it // we can just toggle it because we already have the nested variable if disclosed.unwrap_or(true) || debug_item From fc4078f8e8eef977f04d596626591aa0127d7227 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 28 Aug 2024 10:22:39 +0200 Subject: [PATCH 212/650] Fix we did not open the first scope after stopping for second time --- crates/debugger_ui/src/variable_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 6875681a052c8f..16b55bbfecc7f0 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -100,7 +100,7 @@ impl VariableList { continue; } - if open_first_scope && self.open_entries.is_empty() { + if open_first_scope && entries.is_empty() { self.open_entries.push(scope_entry_id(scope)); } From 4cf735bd93a59d16bbd836b1fa9b15d04ab7de08 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 31 Aug 2024 14:50:57 +0200 Subject: [PATCH 213/650] Add right click menu to copy variable name & value (#32) --- crates/debugger_ui/src/variable_list.rs | 85 +++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 7 deletions(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 16b55bbfecc7f0..c104d87341f139 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,22 +1,28 @@ use crate::debugger_panel_item::{DebugPanelItem, DebugPanelItemEvent, ThreadEntry}; use dap::{client::ThreadState, Scope, Variable}; -use gpui::{list, AnyElement, ListState, Model, Subscription}; -use ui::{prelude::*, ListItem}; +use gpui::{ + anchored, deferred, list, AnyElement, ClipboardItem, DismissEvent, FocusHandle, FocusableView, + ListState, Model, MouseDownEvent, Point, Subscription, View, +}; +use ui::{prelude::*, ContextMenu, ListItem}; use std::{collections::HashMap, sync::Arc}; pub struct VariableList { pub list: ListState, - debug_panel_item: Model, + focus_handle: FocusHandle, open_entries: Vec, - stack_frame_entries: HashMap>, _subscriptions: Vec, + debug_panel_item: Model, + stack_frame_entries: HashMap>, + open_context_menu: Option<(View, Point, Subscription)>, } impl VariableList { pub fn new(debug_panel_item: Model, cx: &mut ViewContext) -> Self { let weakview = cx.view().downgrade(); + let focus_handle = cx.focus_handle(); let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { weakview @@ -29,10 +35,12 @@ impl VariableList { Self { list, + focus_handle, + _subscriptions, debug_panel_item, + open_context_menu: None, open_entries: Default::default(), stack_frame_entries: Default::default(), - _subscriptions, } } @@ -151,6 +159,46 @@ impl VariableList { self.list.reset(len); } + fn deploy_variable_context_menu( + &mut self, + variable: Variable, + position: Point, + cx: &mut ViewContext, + ) { + let this = cx.view().clone(); + + let context_menu = ContextMenu::build(cx, |menu, cx| { + menu.entry( + "Copy name", + None, + cx.handler_for(&this, move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(variable.name.clone())) + }), + ) + .entry( + "Copy value", + None, + cx.handler_for(&this, move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(variable.value.clone())) + }), + ) + }); + + cx.focus_view(&context_menu); + let subscription = + cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + if this.open_context_menu.as_ref().is_some_and(|context_menu| { + context_menu.0.focus_handle(cx).contains_focused(cx) + }) { + cx.focus_self(); + } + this.open_context_menu.take(); + cx.notify(); + }); + + self.open_context_menu = Some((context_menu, position, subscription)); + } + pub fn render_variable( &self, ix: usize, @@ -252,6 +300,12 @@ impl VariableList { .detach_and_log_err(cx); } })) + .on_secondary_mouse_down(cx.listener({ + let variable = variable.clone(); + move |this, event: &MouseDownEvent, cx| { + this.deploy_variable_context_menu(variable.clone(), event.position, cx) + } + })) .child( h_flex() .gap_1() @@ -295,9 +349,26 @@ impl VariableList { } } +impl FocusableView for VariableList { + fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + impl Render for VariableList { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - list(self.list.clone()).gap_1_5().size_full() + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + div() + .size_full() + .child(list(self.list.clone()).gap_1_5().size_full()) + .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { + deferred( + anchored() + .position(*position) + .anchor(gpui::AnchorCorner::TopLeft) + .child(menu.clone()), + ) + .with_priority(1) + })) } } From 83cc4524659cd352d65ad730f360a9d9e428c2bd Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 1 Sep 2024 11:44:32 +0200 Subject: [PATCH 214/650] Make stepping granularity configurable (#33) This commit also changes the default granularity to line, before this was statement but most editors have line as the default. --- assets/settings/default.json | 2 +- crates/dap/src/client.rs | 62 ++++--------------- crates/dap/src/debugger_settings.rs | 28 +++++++++ crates/debugger_ui/src/debugger_panel_item.rs | 11 +++- 4 files changed, 50 insertions(+), 53 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 399cdd0043a72c..77da4180af29f2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1031,7 +1031,7 @@ "ssh_connections": null, "debugger": { - // Save breakpoints across different Zed sessions + "stepping_granularity": "line", "save_breakpoints": true, "button": true }, diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index b98fd08775d02d..3b7864979b64b4 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -476,16 +476,12 @@ impl DebugAdapterClient { self.request::(ContinueArguments { thread_id, - single_thread: if supports_single_thread_execution_requests { - Some(true) - } else { - None - }, + single_thread: supports_single_thread_execution_requests.then(|| true), }) .await } - pub async fn step_over(&self, thread_id: u64) -> Result<()> { + pub async fn step_over(&self, thread_id: u64, granularity: SteppingGranularity) -> Result<()> { let capabilities = self.capabilities(); let supports_single_thread_execution_requests = capabilities @@ -497,21 +493,13 @@ impl DebugAdapterClient { self.request::(NextArguments { thread_id, - granularity: if supports_stepping_granularity { - Some(SteppingGranularity::Statement) - } else { - None - }, - single_thread: if supports_single_thread_execution_requests { - Some(true) - } else { - None - }, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), }) .await } - pub async fn step_in(&self, thread_id: u64) -> Result<()> { + pub async fn step_in(&self, thread_id: u64, granularity: SteppingGranularity) -> Result<()> { let capabilities = self.capabilities(); let supports_single_thread_execution_requests = capabilities @@ -524,21 +512,13 @@ impl DebugAdapterClient { self.request::(StepInArguments { thread_id, target_id: None, - granularity: if supports_stepping_granularity { - Some(SteppingGranularity::Statement) - } else { - None - }, - single_thread: if supports_single_thread_execution_requests { - Some(true) - } else { - None - }, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), }) .await } - pub async fn step_out(&self, thread_id: u64) -> Result<()> { + pub async fn step_out(&self, thread_id: u64, granularity: SteppingGranularity) -> Result<()> { let capabilities = self.capabilities(); let supports_single_thread_execution_requests = capabilities @@ -550,21 +530,13 @@ impl DebugAdapterClient { self.request::(StepOutArguments { thread_id, - granularity: if supports_stepping_granularity { - Some(SteppingGranularity::Statement) - } else { - None - }, - single_thread: if supports_single_thread_execution_requests { - Some(true) - } else { - None - }, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), }) .await } - pub async fn step_back(&self, thread_id: u64) -> Result<()> { + pub async fn step_back(&self, thread_id: u64, granularity: SteppingGranularity) -> Result<()> { let capabilities = self.capabilities(); let supports_single_thread_execution_requests = capabilities @@ -576,16 +548,8 @@ impl DebugAdapterClient { self.request::(StepBackArguments { thread_id, - granularity: if supports_stepping_granularity { - Some(SteppingGranularity::Statement) - } else { - None - }, - single_thread: if supports_single_thread_execution_requests { - Some(true) - } else { - None - }, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), }) .await } diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index b75c9dbc084698..d8ac68a3b1ca7d 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -6,6 +6,10 @@ use settings::{Settings, SettingsSources}; #[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)] #[serde(default)] pub struct DebuggerSettings { + /// Determines the stepping granularity. + /// + /// Default: line + pub stepping_granularity: SteppingGranularity, /// Whether the breakpoints should be reused across Zed sessions. /// /// Default: true @@ -16,11 +20,35 @@ pub struct DebuggerSettings { pub button: bool, } +#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum SteppingGranularity { + /// The step should allow the program to run until the current statement has finished executing. + /// The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. + /// For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. + Statement, + /// The step should allow the program to run until the current source line has executed. + Line, + /// The step should allow one instruction to execute (e.g. one x86 instruction). + Instruction, +} + impl Default for DebuggerSettings { fn default() -> Self { Self { button: true, save_breakpoints: true, + stepping_granularity: SteppingGranularity::Line, + } + } +} + +impl DebuggerSettings { + pub fn stepping_granularity(&self) -> dap_types::SteppingGranularity { + match &self.stepping_granularity { + SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement, + SteppingGranularity::Line => dap_types::SteppingGranularity::Line, + SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction, } } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index f26a45fa70b8ac..3f0bd73b373327 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -4,6 +4,7 @@ use crate::variable_list::VariableList; use anyhow::Result; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::debugger_settings::DebuggerSettings; use dap::{ OutputEvent, OutputEventCategory, Scope, StackFrame, StoppedEvent, ThreadEvent, Variable, }; @@ -13,6 +14,7 @@ use gpui::{ FocusableView, ListState, Subscription, View, WeakView, }; use serde::Deserialize; +use settings::Settings; use std::sync::Arc; use ui::WindowContext; use ui::{prelude::*, Tooltip}; @@ -441,9 +443,10 @@ impl DebugPanelItem { let client = self.client.clone(); let thread_id = self.thread_id; let previous_status = self.current_thread_state().status; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); cx.spawn(|this, cx| async move { - client.step_over(thread_id).await?; + client.step_over(thread_id, granularity).await?; Self::update_thread_state(this, previous_status, None, cx) }) @@ -454,9 +457,10 @@ impl DebugPanelItem { let client = self.client.clone(); let thread_id = self.thread_id; let previous_status = self.current_thread_state().status; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); cx.spawn(|this, cx| async move { - client.step_in(thread_id).await?; + client.step_in(thread_id, granularity).await?; Self::update_thread_state(this, previous_status, None, cx) }) @@ -467,9 +471,10 @@ impl DebugPanelItem { let client = self.client.clone(); let thread_id = self.thread_id; let previous_status = self.current_thread_state().status; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); cx.spawn(|this, cx| async move { - client.step_out(thread_id).await?; + client.step_out(thread_id, granularity).await?; Self::update_thread_state(this, previous_status, None, cx) }) From 7b7a4757cde273025316ca1c817abb52d794a7be Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 3 Sep 2024 08:19:11 +0200 Subject: [PATCH 215/650] Show warning when session exited without hitting any breakpoint (#31) * Show warning when session did not stop but exited * Fix code was not formatted any more * Fix clippy --- crates/dap/src/client.rs | 3 ++ crates/debugger_ui/src/debugger_panel.rs | 60 +++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 3b7864979b64b4..30d32b55499f55 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -60,6 +60,9 @@ pub struct ThreadState { // HashMap>> pub variables: HashMap>>, pub current_stack_frame_id: u64, + // we update this value only once we stopped, + // we will use this to indicated if we should show a warning when debugger thread was exited + pub stopped: bool, } pub struct DebugAdapterClient { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index e015ae7c4cf433..6167ba10ab78d2 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -14,7 +14,7 @@ use editor::Editor; use futures::future::try_join_all; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, - Subscription, Task, View, ViewContext, WeakView, + FontWeight, Subscription, Task, View, ViewContext, WeakView, }; use serde_json::json; use settings::Settings; @@ -47,6 +47,7 @@ pub struct DebugPanel { focus_handle: FocusHandle, workspace: WeakView, _subscriptions: Vec, + show_did_not_stop_warning: bool, } impl DebugPanel { @@ -131,6 +132,7 @@ impl DebugPanel { size: px(300.), _subscriptions, focus_handle: cx.focus_handle(), + show_did_not_stop_warning: false, workspace: workspace.weak_handle(), } }) @@ -245,7 +247,7 @@ impl DebugPanel { Events::Continued(event) => Self::handle_continued_event(client, event, cx), Events::Exited(event) => Self::handle_exited_event(client, event, cx), Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx), - Events::Thread(event) => Self::handle_thread_event(client, event, cx), + Events::Thread(event) => Self::handle_thread_event(this, client, event, cx), Events::Output(event) => Self::handle_output_event(client, event, cx), Events::Breakpoint(_) => {} Events::Module(_) => {} @@ -508,6 +510,7 @@ impl DebugPanel { thread_state.current_stack_frame_id = current_stack_frame.clone().id; thread_state.stack_frames = stack_trace_response.stack_frames; thread_state.status = ThreadStatus::Stopped; + thread_state.stopped = true; client.thread_states().insert(thread_id, thread_state); @@ -570,12 +573,20 @@ impl DebugPanel { } fn handle_thread_event( + this: &mut Self, client: Arc, event: &ThreadEvent, cx: &mut ViewContext, ) { let thread_id = event.thread_id; + if let Some(thread_state) = client.thread_states().get(&thread_id) { + if !thread_state.stopped && event.reason == ThreadEventReason::Exited { + this.show_did_not_stop_warning = true; + cx.notify(); + }; + } + if event.reason == ThreadEventReason::Started { client .thread_states() @@ -656,6 +667,48 @@ impl DebugPanel { ) { cx.emit(DebugPanelEvent::Output((client.id(), event.clone()))); } + + fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement { + const TITLE: &str = "Debug session exited without hitting any breakpoints"; + const DESCRIPTION: &str = + "Try adding a breakpoint, or define the correct path mapping for your debugger."; + + div() + .absolute() + .right_3() + .bottom_12() + .max_w_96() + .py_2() + .px_3() + .elevation_2(cx) + .occlude() + .child( + v_flex() + .gap_0p5() + .child( + h_flex() + .gap_1p5() + .items_center() + .child(Icon::new(IconName::ExclamationTriangle).color(Color::Conflict)) + .child(Label::new(TITLE).weight(FontWeight::MEDIUM)), + ) + .child( + Label::new(DESCRIPTION) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + h_flex().justify_end().mt_1().child( + Button::new("dismiss", "Dismiss") + .color(Color::Muted) + .on_click(cx.listener(|this, _, cx| { + this.show_did_not_stop_warning = false; + cx.notify(); + })), + ), + ), + ) + } } impl EventEmitter for DebugPanel {} @@ -718,6 +771,9 @@ impl Render for DebugPanel { .key_context("DebugPanel") .track_focus(&self.focus_handle) .size_full() + .when(self.show_did_not_stop_warning, |this| { + this.child(self.render_did_not_stop_warning(cx)) + }) .map(|this| { if self.pane.read(cx).items_len() == 0 { this.child( From b009832229fc07233183367dc3bf343974d06beb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 7 Sep 2024 14:00:00 +0200 Subject: [PATCH 216/650] Allow setting variable value (#34) * Wip render set variable value editor * Remove unused subscriptions * Set current variable value & select all when setting value * Send set variable request * Rename tread entry to VariableListEntry * WIP allow setting variables for nested structures * Refactor & rename vars on thread state to be only the ids * Fix we did not correct notify the right context when updating variable list * Clean open entries when debugger stops * Use SetExpression if adapter supports it when setting value * Refetch scope variables after setting value This commit also reworks how we store scopes & variables * Remove debug code * Make Clippy happy * Rename variable * Change order for variable id * Allow cancelling set value using escape key --- Cargo.lock | 1 + crates/dap/src/client.rs | 21 +- crates/debugger_ui/Cargo.toml | 3 +- crates/debugger_ui/src/debugger_panel.rs | 29 +- crates/debugger_ui/src/debugger_panel_item.rs | 22 +- crates/debugger_ui/src/variable_list.rs | 530 ++++++++++++++---- 6 files changed, 449 insertions(+), 157 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3d04a89ffdb06..d42f10f6a5986d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3347,6 +3347,7 @@ dependencies = [ "futures 0.3.30", "gpui", "log", + "menu", "project", "serde", "serde_json", diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 30d32b55499f55..7f674bfbe147b1 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -25,7 +25,7 @@ use smol::{ process::{self, Child}, }; use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, net::{Ipv4Addr, SocketAddrV4}, path::{Path, PathBuf}, process::Stdio, @@ -51,14 +51,22 @@ pub enum ThreadStatus { #[repr(transparent)] pub struct DebugAdapterClientId(pub usize); +#[derive(Debug, Clone)] +pub struct VariableContainer { + pub container_reference: u64, + pub variable: Variable, + pub depth: usize, +} + #[derive(Debug, Default, Clone)] pub struct ThreadState { pub status: ThreadStatus, pub stack_frames: Vec, - // HashMap> - pub vars: HashMap>, - // HashMap>> - pub variables: HashMap>>, + /// HashMap> + pub scopes: HashMap>, + /// BTreeMap> + pub variables: BTreeMap>, + pub fetched_variable_ids: HashSet, pub current_stack_frame_id: u64, // we update this value only once we stopped, // we will use this to indicated if we should show a warning when debugger thread was exited @@ -75,7 +83,8 @@ pub struct DebugAdapterClient { server_tx: Sender, sequence_count: AtomicU64, config: DebugAdapterConfig, - thread_states: Arc>>, // thread_id -> thread_state + /// thread_id -> thread_state + thread_states: Arc>>, capabilities: Arc>>, } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 17206635beb576..3b188f79ab934f 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -14,6 +14,8 @@ dap.workspace = true editor.workspace = true futures.workspace = true gpui.workspace = true +log.workspace = true +menu.workspace = true project.workspace = true serde.workspace = true serde_json.workspace = true @@ -21,7 +23,6 @@ settings.workspace = true task.workspace = true tasks_ui.workspace = true theme.workspace = true -log.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 6167ba10ab78d2..43c2b3bb74d5bb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,6 +1,6 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; -use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus, VariableContainer}; use dap::debugger_settings::DebuggerSettings; use dap::requests::{Request, Scopes, StackTrace, StartDebugging}; use dap::transport::Payload; @@ -8,7 +8,7 @@ use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, ScopesArguments, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, TerminatedEvent, - ThreadEvent, ThreadEventReason, Variable, + ThreadEvent, ThreadEventReason, }; use editor::Editor; use futures::future::try_join_all; @@ -18,7 +18,7 @@ use gpui::{ }; use serde_json::json; use settings::Settings; -use std::collections::{BTreeMap, HashSet}; +use std::collections::HashSet; use std::path::Path; use std::sync::Arc; use task::DebugRequestType; @@ -486,22 +486,25 @@ impl DebugPanel { } for (stack_frame_id, scopes) in try_join_all(stack_frame_tasks).await? { - let stack_frame_state = thread_state - .variables - .entry(stack_frame_id) - .or_insert_with(BTreeMap::default); + thread_state + .scopes + .insert(stack_frame_id, scopes.iter().map(|s| s.0.clone()).collect()); for (scope, variables) in scopes { thread_state - .vars - .insert(scope.variables_reference, variables.clone()); + .fetched_variable_ids + .insert(scope.variables_reference); - stack_frame_state.insert( - scope, + thread_state.variables.insert( + scope.variables_reference, variables .into_iter() - .map(|v| (1, v)) - .collect::>(), + .map(|v| VariableContainer { + container_reference: scope.variables_reference, + variable: v, + depth: 1, + }) + .collect::>(), ); } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 3f0bd73b373327..19e531afe80c7a 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -5,9 +5,7 @@ use crate::variable_list::VariableList; use anyhow::Result; use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; -use dap::{ - OutputEvent, OutputEventCategory, Scope, StackFrame, StoppedEvent, ThreadEvent, Variable, -}; +use dap::{OutputEvent, OutputEventCategory, StackFrame, StoppedEvent, ThreadEvent}; use editor::Editor; use gpui::{ impl_actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, @@ -33,17 +31,6 @@ enum ThreadItem { Output, } -#[derive(Debug, Clone)] -pub enum ThreadEntry { - Scope(Scope), - Variable { - depth: usize, - scope: Scope, - variable: Arc, - has_children: bool, - }, -} - pub struct DebugPanelItem { thread_id: u64, variable_list: View, @@ -57,8 +44,6 @@ pub struct DebugPanelItem { workspace: WeakView, } -pub enum DebugPanelItemEvent {} - impl_actions!(debug_panel_item, [DebugItemAction]); /// This struct is for actions that should be triggered even when @@ -291,8 +276,8 @@ impl DebugPanelItem { let thread_state = self.current_thread_state(); - self.variable_list.update(cx, |variable_list, cx| { - variable_list.build_entries(thread_state, true, cx) + self.variable_list.update(cx, |variable_list, _| { + variable_list.build_entries(thread_state, true, false); }); } @@ -515,7 +500,6 @@ impl DebugPanelItem { } impl EventEmitter for DebugPanelItem {} -impl EventEmitter for DebugPanelItem {} impl FocusableView for DebugPanelItem { fn focus_handle(&self, _: &AppContext) -> FocusHandle { diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index c104d87341f139..a06ffe1513aafe 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,21 +1,57 @@ -use crate::debugger_panel_item::{DebugPanelItem, DebugPanelItemEvent, ThreadEntry}; -use dap::{client::ThreadState, Scope, Variable}; +use crate::debugger_panel_item::DebugPanelItem; +use dap::{ + client::{ThreadState, VariableContainer}, + requests::{SetExpression, SetVariable, Variables}, + Scope, SetExpressionArguments, SetVariableArguments, Variable, VariablesArguments, +}; +use editor::{ + actions::{self, SelectAll}, + Editor, EditorEvent, +}; +use futures::future::try_join_all; use gpui::{ anchored, deferred, list, AnyElement, ClipboardItem, DismissEvent, FocusHandle, FocusableView, ListState, Model, MouseDownEvent, Point, Subscription, View, }; +use menu::Confirm; +use std::{collections::HashMap, sync::Arc}; use ui::{prelude::*, ContextMenu, ListItem}; -use std::{collections::HashMap, sync::Arc}; +#[derive(Debug, Clone)] +pub struct SetVariableState { + name: String, + scope: Scope, + value: String, + stack_frame_id: u64, + evaluate_name: Option, + parent_variables_reference: u64, +} + +#[derive(Debug, Clone)] +pub enum VariableListEntry { + Scope(Scope), + SetVariableEditor { + depth: usize, + state: SetVariableState, + }, + Variable { + depth: usize, + scope: Scope, + variable: Arc, + has_children: bool, + container_reference: u64, + }, +} pub struct VariableList { - pub list: ListState, + list: ListState, focus_handle: FocusHandle, open_entries: Vec, - _subscriptions: Vec, + set_variable_editor: View, debug_panel_item: Model, - stack_frame_entries: HashMap>, + set_variable_state: Option, + stack_frame_entries: HashMap>, open_context_menu: Option<(View, Point, Subscription)>, } @@ -31,14 +67,25 @@ impl VariableList { .unwrap_or(div().into_any()) }); - let _subscriptions = vec![cx.subscribe(&debug_panel_item, Self::handle_events)]; + let set_variable_editor = cx.new_view(|cx| Editor::single_line(cx)); + + cx.subscribe( + &set_variable_editor, + |this: &mut Self, _, event: &EditorEvent, cx| { + if *event == EditorEvent::Blurred { + this.cancel_set_variable_value(cx); + } + }, + ) + .detach(); Self { list, focus_handle, - _subscriptions, debug_panel_item, + set_variable_editor, open_context_menu: None, + set_variable_state: None, open_entries: Default::default(), stack_frame_entries: Default::default(), } @@ -54,26 +101,29 @@ impl VariableList { }; match &entries[ix] { - ThreadEntry::Scope(scope) => self.render_scope(scope, cx), - ThreadEntry::Variable { + VariableListEntry::Scope(scope) => self.render_scope(scope, cx), + VariableListEntry::SetVariableEditor { depth, state } => { + self.render_set_variable_editor(*depth, state, cx) + } + VariableListEntry::Variable { depth, scope, variable, has_children, - .. - } => self.render_variable(ix, variable, scope, *depth, *has_children, cx), + container_reference: parent_variables_reference, + } => self.render_variable( + ix, + *parent_variables_reference, + variable, + scope, + *depth, + *has_children, + cx, + ), } } - fn handle_events( - &mut self, - _debug_panel_item: Model, - _event: &DebugPanelItemEvent, - _cx: &mut ViewContext, - ) { - } - - pub fn toggle_entry_collapsed(&mut self, entry_id: &SharedString, cx: &mut ViewContext) { + fn toggle_entry_collapsed(&mut self, entry_id: &SharedString, cx: &mut ViewContext) { match self.open_entries.binary_search(&entry_id) { Ok(ix) => { self.open_entries.remove(ix); @@ -85,9 +135,9 @@ impl VariableList { let thread_state = self .debug_panel_item - .read_with(cx, |panel, _cx| panel.current_thread_state()); + .read_with(cx, |panel, _| panel.current_thread_state()); - self.build_entries(thread_state, false, cx); + self.build_entries(thread_state, false, true); cx.notify(); } @@ -95,15 +145,23 @@ impl VariableList { &mut self, thread_state: ThreadState, open_first_scope: bool, - _cx: &mut ViewContext, + keep_open_entries: bool, ) { let stack_frame_id = thread_state.current_stack_frame_id; - let Some(scopes_and_vars) = thread_state.variables.get(&stack_frame_id) else { + let Some(scopes) = thread_state.scopes.get(&stack_frame_id) else { return; }; - let mut entries: Vec = Vec::default(); - for (scope, variables) in scopes_and_vars { + if !keep_open_entries { + self.open_entries.clear(); + } + + let mut entries: Vec = Vec::default(); + for scope in scopes { + let Some(variables) = thread_state.variables.get(&scope.variables_reference) else { + continue; + }; + if variables.is_empty() { continue; } @@ -111,8 +169,7 @@ impl VariableList { if open_first_scope && entries.is_empty() { self.open_entries.push(scope_entry_id(scope)); } - - entries.push(ThreadEntry::Scope(scope.clone())); + entries.push(VariableListEntry::Scope(scope.clone())); if self .open_entries @@ -124,32 +181,46 @@ impl VariableList { let mut depth_check: Option = None; - for (depth, variable) in variables { - if depth_check.is_some_and(|d| *depth > d) { + for variable_container in variables { + let depth = variable_container.depth; + let variable = &variable_container.variable; + let container_reference = variable_container.container_reference; + + if depth_check.is_some_and(|d| depth > d) { continue; } - if depth_check.is_some_and(|d| d >= *depth) { + if depth_check.is_some_and(|d| d >= depth) { depth_check = None; } - let has_children = variable.variables_reference > 0; - if self .open_entries - .binary_search(&variable_entry_id(&variable, &scope, *depth)) + .binary_search(&variable_entry_id(scope, variable, depth)) .is_err() { - if depth_check.is_none() || depth_check.is_some_and(|d| d > *depth) { - depth_check = Some(*depth); + if depth_check.is_none() || depth_check.is_some_and(|d| d > depth) { + depth_check = Some(depth); + } + } + + if let Some(state) = self.set_variable_state.as_ref() { + if state.parent_variables_reference == container_reference + && state.name == variable.name + { + entries.push(VariableListEntry::SetVariableEditor { + depth, + state: state.clone(), + }); } } - entries.push(ThreadEntry::Variable { - has_children, - depth: *depth, + entries.push(VariableListEntry::Variable { + depth, scope: scope.clone(), variable: Arc::new(variable.clone()), + has_children: variable.variables_reference > 0, + container_reference, }); } } @@ -161,27 +232,75 @@ impl VariableList { fn deploy_variable_context_menu( &mut self, - variable: Variable, + parent_variables_reference: u64, + scope: &Scope, + variable: &Variable, position: Point, cx: &mut ViewContext, ) { let this = cx.view().clone(); + let (stack_frame_id, client) = self.debug_panel_item.read_with(cx, |p, _| { + (p.current_thread_state().current_stack_frame_id, p.client()) + }); + let support_set_variable = client + .capabilities() + .supports_set_variable + .unwrap_or_default(); + let context_menu = ContextMenu::build(cx, |menu, cx| { menu.entry( "Copy name", None, - cx.handler_for(&this, move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(variable.name.clone())) + cx.handler_for(&this, { + let variable = variable.clone(); + move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(variable.name.clone())) + } }), ) .entry( "Copy value", None, - cx.handler_for(&this, move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(variable.value.clone())) + cx.handler_for(&this, { + let variable = variable.clone(); + move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(variable.value.clone())) + } }), ) + .when(support_set_variable, move |menu| { + let variable = variable.clone(); + let scope = scope.clone(); + + menu.entry( + "Set value", + None, + cx.handler_for(&this, move |this, cx| { + this.set_variable_state = Some(SetVariableState { + parent_variables_reference, + name: variable.name.clone(), + scope: scope.clone(), + evaluate_name: variable.evaluate_name.clone(), + value: variable.value.clone(), + stack_frame_id, + }); + + this.set_variable_editor.update(cx, |editor, cx| { + editor.set_text(variable.value.clone(), cx); + editor.select_all(&SelectAll, cx); + editor.focus(cx); + }); + + let thread_state = this + .debug_panel_item + .read_with(cx, |panel, _| panel.current_thread_state()); + this.build_entries(thread_state, false, true); + + cx.notify(); + }), + ) + }) }); cx.focus_view(&context_menu); @@ -199,9 +318,158 @@ impl VariableList { self.open_context_menu = Some((context_menu, position, subscription)); } - pub fn render_variable( + fn cancel_set_variable_value(&mut self, cx: &mut ViewContext) { + if self.set_variable_state.take().is_none() { + return; + }; + + let thread_state = self + .debug_panel_item + .read_with(cx, |panel, _| panel.current_thread_state()); + + self.build_entries(thread_state, false, true); + cx.notify(); + } + + fn set_variable_value(&mut self, _: &Confirm, cx: &mut ViewContext) { + let new_variable_value = self.set_variable_editor.update(cx, |editor, cx| { + let new_variable_value = editor.text(cx); + + editor.clear(cx); + + new_variable_value + }); + + let Some(state) = self.set_variable_state.take() else { + cx.notify(); + return; + }; + + if new_variable_value == state.value { + cx.notify(); + return; + } + + let (mut thread_state, client) = self + .debug_panel_item + .read_with(cx, |p, _| (p.current_thread_state(), p.client())); + let variables_reference = state.parent_variables_reference; + let scope = state.scope; + let name = state.name; + let evaluate_name = state.evaluate_name; + let stack_frame_id = state.stack_frame_id; + let supports_set_expression = client + .capabilities() + .supports_set_expression + .unwrap_or_default(); + + cx.spawn(|this, mut cx| async move { + if let Some(evaluate_name) = supports_set_expression.then(|| evaluate_name).flatten() { + client + .request::(SetExpressionArguments { + expression: evaluate_name, + value: new_variable_value, + frame_id: Some(stack_frame_id), + format: None, + }) + .await?; + } else { + client + .request::(SetVariableArguments { + variables_reference, + name, + value: new_variable_value, + format: None, + }) + .await?; + } + + let Some(scope_variables) = thread_state.variables.remove(&scope.variables_reference) + else { + return anyhow::Ok(()); + }; + + let mut tasks = Vec::new(); + + for variable_container in scope_variables { + let client = client.clone(); + tasks.push(async move { + let variables = client + .request::(VariablesArguments { + variables_reference: variable_container.container_reference, + filter: None, + start: None, + count: None, + format: None, + }) + .await? + .variables; + + let depth = variable_container.depth; + let container_reference = variable_container.container_reference; + + anyhow::Ok( + variables + .into_iter() + .map(move |variable| VariableContainer { + container_reference, + variable, + depth, + }), + ) + }); + } + + let updated_variables = try_join_all(tasks).await?; + + this.update(&mut cx, |this, cx| { + let (thread_id, client) = this + .debug_panel_item + .read_with(cx, |panel, _| (panel.thread_id(), panel.client())); + + let mut thread_states = client.thread_states(); + + let Some(thread_state) = thread_states.get_mut(&thread_id) else { + return; + }; + + for variables in updated_variables { + thread_state + .variables + .insert(scope.variables_reference, variables.collect::<_>()); + } + + this.build_entries(thread_state.clone(), false, true); + cx.notify(); + }) + }) + .detach_and_log_err(cx); + } + + fn render_set_variable_editor( + &self, + depth: usize, + state: &SetVariableState, + cx: &mut ViewContext, + ) -> AnyElement { + div() + .h_4() + .size_full() + .on_action(cx.listener(Self::set_variable_value)) + .child( + ListItem::new(SharedString::from(state.name.clone())) + .indent_level(depth + 1) + .indent_step_size(px(20.)) + .child(self.set_variable_editor.clone()), + ) + .into_any_element() + } + + #[allow(clippy::too_many_arguments)] + fn render_variable( &self, ix: usize, + parent_variables_reference: u64, variable: &Variable, scope: &Scope, depth: usize, @@ -209,9 +477,13 @@ impl VariableList { cx: &mut ViewContext, ) -> AnyElement { let variable_reference = variable.variables_reference; - let variable_id = variable_entry_id(variable, scope, depth); + let variable_id = variable_entry_id(scope, variable, depth); - let disclosed = has_children.then(|| self.open_entries.binary_search(&variable_id).is_ok()); + let disclosed = has_children.then(|| { + self.open_entries + .binary_search(&variable_entry_id(scope, variable, depth)) + .is_ok() + }); div() .id(variable_id.clone()) @@ -224,86 +496,105 @@ impl VariableList { .indent_step_size(px(20.)) .always_show_disclosure_icon(true) .toggle(disclosed) - .on_toggle(cx.listener(move |this, _, cx| { - if !has_children { - return; - } - - let debug_item = this.debug_panel_item.read(cx); - - // if we already opened the variable/we already fetched it - // we can just toggle it because we already have the nested variable - if disclosed.unwrap_or(true) - || debug_item - .current_thread_state() - .vars - .contains_key(&variable_reference) - { - return this.toggle_entry_collapsed(&variable_id, cx); - } - - let Some(entries) = this - .stack_frame_entries - .get(&debug_item.current_thread_state().current_stack_frame_id) - else { - return; - }; - - let Some(entry) = entries.get(ix) else { - return; - }; - - if let ThreadEntry::Variable { scope, depth, .. } = entry { - let variable_id = variable_id.clone(); - let client = debug_item.client(); - let scope = scope.clone(); - let depth = *depth; - - cx.spawn(|this, mut cx| async move { - let variables = client.variables(variable_reference).await?; - - this.update(&mut cx, |this, cx| { - let client = client.clone(); - let mut thread_states = client.thread_states(); - let Some(thread_state) = thread_states - .get_mut(&this.debug_panel_item.read(cx).thread_id()) - else { - return; - }; - - if let Some(state) = thread_state - .variables - .get_mut(&thread_state.current_stack_frame_id) - .and_then(|s| s.get_mut(&scope)) - { - let position = state.iter().position(|(d, v)| { - variable_entry_id(v, &scope, *d) == variable_id + .on_toggle(cx.listener({ + let variable_id = variable_id.clone(); + move |this, _, cx| { + if !has_children { + return; + } + + let debug_item = this.debug_panel_item.read(cx); + + // if we already opened the variable/we already fetched it + // we can just toggle it because we already have the nested variable + if disclosed.unwrap_or(true) + || debug_item + .current_thread_state() + .fetched_variable_ids + .contains(&variable_reference) + { + return this.toggle_entry_collapsed(&variable_id, cx); + } + + let Some(entries) = this + .stack_frame_entries + .get(&debug_item.current_thread_state().current_stack_frame_id) + else { + return; + }; + + let Some(entry) = entries.get(ix) else { + return; + }; + + if let VariableListEntry::Variable { scope, depth, .. } = entry { + let variable_id = variable_id.clone(); + let client = debug_item.client(); + let scope = scope.clone(); + let depth = *depth; + + cx.spawn(|this, mut cx| async move { + let new_variables = + client.variables(variable_reference).await?; + + this.update(&mut cx, |this, cx| { + let client = client.clone(); + let mut thread_states = client.thread_states(); + let Some(thread_state) = thread_states + .get_mut(&this.debug_panel_item.read(cx).thread_id()) + else { + return; + }; + + let Some(variables) = thread_state + .variables + .get_mut(&scope.variables_reference) + else { + return; + }; + + let position = variables.iter().position(|v| { + variable_entry_id(&scope, &v.variable, v.depth) + == variable_id }); if let Some(position) = position { - state.splice( + variables.splice( position + 1..position + 1, - variables - .clone() - .into_iter() - .map(|v| (depth + 1, v)), + new_variables.clone().into_iter().map(|variable| { + VariableContainer { + container_reference: variable_reference, + variable, + depth: depth + 1, + } + }), ); - } - thread_state.vars.insert(variable_reference, variables); - } + thread_state + .fetched_variable_ids + .insert(variable_reference); + } - drop(thread_states); - this.toggle_entry_collapsed(&variable_id, cx); + drop(thread_states); + this.toggle_entry_collapsed(&variable_id, cx); + cx.notify(); + }) }) - }) - .detach_and_log_err(cx); + .detach_and_log_err(cx); + } } })) .on_secondary_mouse_down(cx.listener({ + let scope = scope.clone(); let variable = variable.clone(); move |this, event: &MouseDownEvent, cx| { - this.deploy_variable_context_menu(variable.clone(), event.position, cx) + this.deploy_variable_context_menu( + parent_variables_reference, + &scope, + &variable, + event.position, + cx, + ) } })) .child( @@ -356,9 +647,12 @@ impl FocusableView for VariableList { } impl Render for VariableList { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() .size_full() + .on_action( + cx.listener(|this, _: &actions::Cancel, cx| this.cancel_set_variable_value(cx)), + ) .child(list(self.list.clone()).gap_1_5().size_full()) .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { deferred( @@ -372,10 +666,10 @@ impl Render for VariableList { } } -pub fn variable_entry_id(variable: &Variable, scope: &Scope, depth: usize) -> SharedString { +pub fn variable_entry_id(scope: &Scope, variable: &Variable, depth: usize) -> SharedString { SharedString::from(format!( "variable-{}-{}-{}", - depth, scope.variables_reference, variable.name + scope.variables_reference, variable.name, depth )) } From edf4e53571882c2bf807dd3327b5d0459b3b67c7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 7 Sep 2024 19:17:23 +0200 Subject: [PATCH 217/650] Fix Clippy errors --- crates/debugger_ui/src/console.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/variable_list.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index eea3321684fc2b..b26a2491783f83 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -20,7 +20,7 @@ impl Console { editor }); - let query_bar = cx.new_view(|cx| Editor::single_line(cx)); + let query_bar = cx.new_view(Editor::single_line); Self { console, query_bar } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 19e531afe80c7a..16a3fe83334de5 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -81,7 +81,7 @@ impl DebugPanelItem { let model = cx.model().clone(); let variable_list = cx.new_view(|cx| VariableList::new(model, cx)); - let console = cx.new_view(|cx| Console::new(cx)); + let console = cx.new_view(Console::new); let weakview = cx.view().downgrade(); let stack_frame_list = diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index a06ffe1513aafe..8181386520431f 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -67,7 +67,7 @@ impl VariableList { .unwrap_or(div().into_any()) }); - let set_variable_editor = cx.new_view(|cx| Editor::single_line(cx)); + let set_variable_editor = cx.new_view(Editor::single_line); cx.subscribe( &set_variable_editor, From b00d63b6c4a2039c06614504b051726c8af53e25 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 8 Sep 2024 11:06:42 +0200 Subject: [PATCH 218/650] Move breakpoint & debug client code to dap_store (#36) * Move breakpoint and client code to dap store This also changes how we sync breakpoints. Now we just update the dap store model instead of having a RWlock. The goal is to prepare for making inlay hints for debugging work. * Remove debug code * Remove unused method * Fix don't grow the amount tasks for sending all the breakpoints * Partially implement terminate clients when app quits * Sync open breakpoints to closed breakpoints when buffer is closed * Call terminate request also for not already started clients * Fix missing import * Remove not needed read_with call --- crates/dap/src/client.rs | 10 + crates/debugger_ui/src/debugger_panel.rs | 10 +- crates/editor/src/editor.rs | 63 ++-- crates/editor/src/element.rs | 4 +- crates/project/src/dap_store.rs | 287 +++++++++++++++++++ crates/project/src/lsp_store.rs | 7 + crates/project/src/project.rs | 287 ++++++------------- crates/remote_server/src/headless_project.rs | 7 +- crates/workspace/src/workspace.rs | 35 +-- 9 files changed, 444 insertions(+), 266 deletions(-) create mode 100644 crates/project/src/dap_store.rs diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 7f674bfbe147b1..8f88e152099cac 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -653,6 +653,16 @@ impl DebugAdapterClient { } } + pub async fn shutdown(&self, should_terminate: bool) -> Result<()> { + if should_terminate { + let _ = self.terminate().await; + } + + // TODO debugger: close channels & kill process + + anyhow::Ok(()) + } + pub async fn terminate(&self) -> Result<()> { let support_terminate_request = self .capabilities() diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 43c2b3bb74d5bb..51ea8ca9e9710e 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -154,7 +154,11 @@ impl DebugPanel { ) -> Option> { self.workspace .update(cx, |this, cx| { - this.project().read(cx).debug_adapter_by_id(client_id) + this.project() + .read(cx) + .dap_store() + .read(cx) + .client_by_id(client_id) }) .ok() .flatten() @@ -654,7 +658,9 @@ impl DebugPanel { cx.update(|cx| { workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - project.stop_debug_adapter_client(client.id(), false, cx) + project.dap_store().update(cx, |store, cx| { + store.shutdown_client(client.id(), false, cx) + }) }) }) })? diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d89b4673c2efb8..c4b2a3256ae15d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -96,6 +96,7 @@ use language::{ }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; +use project::dap_store::DapStore; use task::{ResolvedTask, TaskTemplate, TaskVariables}; use dap::client::Breakpoint; @@ -128,6 +129,7 @@ use serde::{Deserialize, Serialize}; use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore}; use smallvec::SmallVec; use snippet::Snippet; +use std::sync::Arc; use std::{ any::TypeId, borrow::Cow, @@ -138,7 +140,6 @@ use std::{ ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, - sync::Arc, time::{Duration, Instant}, }; pub use sum_tree::Bias; @@ -592,8 +593,7 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - /// All the breakpoints that are contained within open buffers in the editor - breakpoints: Option>>>>, + dap_store: Option>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown @@ -1837,11 +1837,7 @@ impl Editor { None }; - let opened_breakpoints = if let Some(project) = project.as_ref() { - project.read_with(cx, |project, _cx| Some(project.open_breakpoints.clone())) - } else { - None - }; + let dap_store = project.as_ref().map(|project| project.read(cx).dap_store()); let mut this = Self { focus_handle, @@ -1944,7 +1940,7 @@ impl Editor { blame_subscription: None, file_header_size, tasks: Default::default(), - breakpoints: opened_breakpoints, + dap_store, gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -1995,8 +1991,14 @@ impl Editor { Some((buffer.project_path(cx)?, buffer.remote_id(), snapshot)) }) { if let Some(project) = this.project.as_ref() { - project.update(cx, |project, _cx| { - project.convert_to_open_breakpoints(&project_path, buffer_id, snapshot) + project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, _| { + store.sync_closed_breakpoint_to_open_breakpoint( + &buffer_id, + &project_path, + snapshot, + ); + }); }); } } @@ -5288,17 +5290,17 @@ impl Editor { /// /// This function is used to handle overlaps between breakpoints and Code action/runner symbol. /// It's also used to set the color of line numbers with breakpoints to the breakpoint color. - /// TODO Debugger: Use this function to color toggle symbols that house nested breakpoints + /// TODO debugger: Use this function to color toggle symbols that house nested breakpoints fn active_breakpoint_points(&mut self, cx: &mut ViewContext) -> HashSet { let mut breakpoint_display_points = HashSet::default(); - let Some(opened_breakpoints) = self.breakpoints.clone() else { + let Some(dap_store) = self.dap_store.clone() else { return breakpoint_display_points; }; let snapshot = self.snapshot(cx); - let opened_breakpoints = opened_breakpoints.read(); + let opened_breakpoints = dap_store.read(cx).open_breakpoints(); if let Some(buffer) = self.buffer.read(cx).as_singleton() { let buffer = buffer.read(cx); @@ -5307,7 +5309,7 @@ impl Editor { for breakpoint in breakpoints { breakpoint_display_points .insert(breakpoint.position.to_display_point(&snapshot)); - // Breakpoints TODO: Multibuffer bp toggle failing here + // Breakpoints TODO debugger: Multibuffer bp toggle failing here // dued to invalid excerpt id. Multibuffer excerpt id isn't the same as a singular buffer id } }; @@ -6255,35 +6257,22 @@ impl Editor { return; }; - let Some(breakpoints) = &self.breakpoints else { + if self.dap_store.is_none() { return; - }; + } let Some(buffer_id) = breakpoint_position.buffer_id else { return; }; - let breakpoint = Breakpoint { - position: breakpoint_position, - }; - - // Putting the write guard within it's own scope so it's dropped - // before project updates it's breakpoints. This is done to prevent - // a data race condition where project waits to get a read lock - { - let mut write_guard = breakpoints.write(); - - let breakpoint_set = write_guard.entry(buffer_id).or_default(); - - if !breakpoint_set.remove(&breakpoint) { - breakpoint_set.insert(breakpoint); - } - } - project.update(cx, |project, cx| { - if project.has_active_debugger() { - project.update_file_breakpoints(buffer_id, cx); - } + project.toggle_breakpoint( + buffer_id, + Breakpoint { + position: breakpoint_position, + }, + cx, + ); }); cx.notify(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e58659211e6d0e..47da4fac9241dc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1592,8 +1592,8 @@ impl EditorElement { cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { - if editor.breakpoints.is_none() { - return vec![]; + if editor.dap_store.is_none() { + return Vec::new(); }; breakpoints diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs new file mode 100644 index 00000000000000..2edf3304729b2b --- /dev/null +++ b/crates/project/src/dap_store.rs @@ -0,0 +1,287 @@ +use anyhow::Context as _; +use collections::{HashMap, HashSet}; +use dap::{ + client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId, SerializedBreakpoint}, + transport::Payload, +}; +use gpui::{EventEmitter, ModelContext, Subscription, Task}; +use language::{Buffer, BufferSnapshot}; +use multi_buffer::MultiBufferSnapshot; +use std::{ + collections::BTreeMap, + future::Future, + path::PathBuf, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, +}; +use task::DebugAdapterConfig; +use text::{Bias, BufferId, Point}; +use util::ResultExt as _; + +use crate::{Item, ProjectPath}; + +pub enum DapStoreEvent { + DebugClientStarted(DebugAdapterClientId), + DebugClientStopped(DebugAdapterClientId), + DebugClientEvent { + client_id: DebugAdapterClientId, + payload: Payload, + }, +} + +pub enum DebugAdapterClientState { + Starting(Task>>), + Running(Arc), +} + +pub struct DapStore { + next_client_id: AtomicUsize, + clients: HashMap, + open_breakpoints: BTreeMap>, + /// All breakpoints that belong to this project but are in closed files + pub closed_breakpoints: BTreeMap>, + _subscription: Vec, +} + +impl EventEmitter for DapStore {} + +impl DapStore { + pub fn new(cx: &mut ModelContext) -> Self { + Self { + next_client_id: Default::default(), + clients: Default::default(), + open_breakpoints: Default::default(), + closed_breakpoints: Default::default(), + _subscription: vec![cx.on_app_quit(Self::shutdown_clients)], + } + } + + pub fn next_client_id(&self) -> DebugAdapterClientId { + DebugAdapterClientId(self.next_client_id.fetch_add(1, SeqCst)) + } + + pub fn running_clients(&self) -> impl Iterator> + '_ { + self.clients.values().filter_map(|state| match state { + DebugAdapterClientState::Starting(_) => None, + DebugAdapterClientState::Running(client) => Some(client.clone()), + }) + } + + pub fn client_by_id(&self, id: DebugAdapterClientId) -> Option> { + self.clients.get(&id).and_then(|state| match state { + DebugAdapterClientState::Starting(_) => None, + DebugAdapterClientState::Running(client) => Some(client.clone()), + }) + } + + pub fn open_breakpoints(&self) -> &BTreeMap> { + &self.open_breakpoints + } + + pub fn closed_breakpoints(&self) -> &BTreeMap> { + &self.closed_breakpoints + } + + pub fn sync_open_breakpoints_to_closed_breakpoints( + &mut self, + buffer_id: &BufferId, + buffer: &mut Buffer, + cx: &mut ModelContext, + ) { + let Some(breakpoints) = self.open_breakpoints.remove(&buffer_id) else { + return; + }; + + if let Some(project_path) = buffer.project_path(cx) { + self.closed_breakpoints + .entry(project_path.clone()) + .or_default() + .extend( + breakpoints + .into_iter() + .map(|bp| bp.to_serialized(buffer, project_path.path.clone())), + ); + } + } + + pub fn sync_closed_breakpoint_to_open_breakpoint( + &mut self, + buffer_id: &BufferId, + project_path: &ProjectPath, + snapshot: MultiBufferSnapshot, + ) { + let Some(closed_breakpoints) = self.closed_breakpoints.remove(project_path) else { + return; + }; + + let open_breakpoints = self.open_breakpoints.entry(*buffer_id).or_default(); + + for closed_breakpoint in closed_breakpoints { + // serialized breakpoints start at index one and need to converted + // to index zero in order to display/work properly with open breakpoints + let position = snapshot.anchor_at( + Point::new(closed_breakpoint.position.saturating_sub(1), 0), + Bias::Left, + ); + + open_breakpoints.insert(Breakpoint { position }); + } + } + + pub fn start_client( + &mut self, + config: DebugAdapterConfig, + command: String, + args: Vec, + cwd: PathBuf, + request_args: Option, + cx: &mut ModelContext, + ) { + let client_id = self.next_client_id(); + + let start_client_task = cx.spawn(|this, mut cx| async move { + let dap_store = this.clone(); + let client = DebugAdapterClient::new( + client_id, + config, + &command, + &args, + &cwd, + request_args, + move |payload, cx| { + dap_store + .update(cx, |_, cx| { + cx.emit(DapStoreEvent::DebugClientEvent { client_id, payload }) + }) + .log_err(); + }, + &mut cx, + ) + .await + .log_err()?; + + this.update(&mut cx, |store, cx| { + let handle = store + .clients + .get_mut(&client_id) + .with_context(|| "Failed to find starting debug client")?; + + *handle = DebugAdapterClientState::Running(client.clone()); + + cx.emit(DapStoreEvent::DebugClientStarted(client_id)); + + anyhow::Ok(()) + }) + .log_err(); + + Some(client) + }); + + self.clients.insert( + client_id, + DebugAdapterClientState::Starting(start_client_task), + ); + } + + fn shutdown_clients(&mut self, _: &mut ModelContext) -> impl Future { + let shutdown_futures = self + .clients + .drain() + .map(|(_, client_state)| async { + match client_state { + DebugAdapterClientState::Starting(task) => { + task.await?.shutdown(true).await.ok() + } + DebugAdapterClientState::Running(client) => client.shutdown(true).await.ok(), + } + }) + .collect::>(); + + async move { + futures::future::join_all(shutdown_futures).await; + } + } + + pub fn shutdown_client( + &mut self, + client_id: DebugAdapterClientId, + should_terminate: bool, + cx: &mut ModelContext, + ) { + let Some(debug_client) = self.clients.remove(&client_id) else { + return; + }; + + cx.emit(DapStoreEvent::DebugClientStopped(client_id)); + + cx.background_executor() + .spawn(async move { + match debug_client { + DebugAdapterClientState::Starting(task) => { + task.await?.shutdown(should_terminate).await.ok() + } + DebugAdapterClientState::Running(client) => { + client.shutdown(should_terminate).await.ok() + } + } + }) + .detach(); + } + + pub fn toggle_breakpoint_for_buffer( + &mut self, + buffer_id: &BufferId, + breakpoint: Breakpoint, + buffer_path: PathBuf, + buffer_snapshot: BufferSnapshot, + cx: &mut ModelContext, + ) { + let breakpoint_set = self.open_breakpoints.entry(*buffer_id).or_default(); + + if !breakpoint_set.remove(&breakpoint) { + breakpoint_set.insert(breakpoint); + } + + self.send_changed_breakpoints(buffer_id, buffer_path, buffer_snapshot, cx); + } + + pub fn send_changed_breakpoints( + &self, + buffer_id: &BufferId, + buffer_path: PathBuf, + buffer_snapshot: BufferSnapshot, + cx: &mut ModelContext, + ) { + let clients = self.running_clients().collect::>(); + + if clients.is_empty() { + return; + } + + let Some(breakpoints) = self.open_breakpoints.get(buffer_id) else { + return; + }; + + let source_breakpoints = breakpoints + .iter() + .map(|bp| bp.source_for_snapshot(&buffer_snapshot)) + .collect::>(); + + let mut tasks = Vec::new(); + for client in clients { + let buffer_path = buffer_path.clone(); + let source_breakpoints = source_breakpoints.clone(); + tasks.push(async move { + client + .set_breakpoints(Arc::from(buffer_path), source_breakpoints) + .await + }); + } + + cx.background_executor() + .spawn(async move { futures::future::join_all(tasks).await }) + .detach() + } +} diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 67b0128575f350..2301a6fd62d3c3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1,5 +1,6 @@ use crate::{ buffer_store::{BufferStore, BufferStoreEvent}, + dap_store::DapStore, environment::ProjectEnvironment, lsp_command::{self, *}, lsp_ext_command, @@ -92,6 +93,7 @@ pub struct LspStore { http_client: Option>, fs: Arc, nonce: u128, + dap_store: Model, buffer_store: Model, worktree_store: Model, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots @@ -213,6 +215,7 @@ impl LspStore { pub fn new( buffer_store: Model, worktree_store: Model, + dap_store: Model, environment: Option>, languages: Arc, http_client: Option>, @@ -236,6 +239,7 @@ impl LspStore { project_id: remote_id.unwrap_or(0), buffer_store, worktree_store, + dap_store, languages: languages.clone(), environment, nonce: StdRng::from_entropy().gen(), @@ -341,6 +345,9 @@ impl LspStore { self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_servers(buffer, cx); cx.observe_release(buffer, |this, buffer, cx| { + this.dap_store.update(cx, |store, cx| { + store.sync_open_breakpoints_to_closed_breakpoints(&buffer.remote_id(), buffer, cx); + }); // this.breakpoint_store.sync(); // Serialize the breakpoints of this buffer and set them // as unopened breakpoints to maintain correct state. diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fc5b89a4081259..c38c3114ff92dc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,5 +1,6 @@ pub mod buffer_store; pub mod connection_manager; +pub mod dap_store; pub mod debounced_delay; pub mod lsp_command; pub mod lsp_ext_command; @@ -32,7 +33,8 @@ use dap::{ transport::Payload, }; -use collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use collections::{BTreeSet, HashMap, HashSet}; +use dap_store::{DapStore, DapStoreEvent}; use debounced_delay::DebouncedDelay; pub use environment::ProjectEnvironment; use futures::{ @@ -63,7 +65,7 @@ use language::{ }; use lsp::{CompletionContext, DocumentHighlightKind, LanguageServer, LanguageServerId}; use lsp_command::*; -use multi_buffer::{MultiBuffer, MultiBufferSnapshot}; +use multi_buffer::MultiBuffer; use node_runtime::NodeRuntime; use parking_lot::{Mutex, RwLock}; use paths::{ @@ -88,10 +90,7 @@ use std::{ ops::Range, path::{Component, Path, PathBuf}, str, - sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, - Arc, - }, + sync::Arc, time::Duration, }; use task::{ @@ -100,7 +99,7 @@ use task::{ TaskVariables, VariableName, }; use terminals::Terminals; -use text::{Anchor, Bias, BufferId, Point}; +use text::{Anchor, BufferId}; use util::{defer, maybe, paths::compare_paths, ResultExt}; use worktree::{CreatedEntry, Snapshot, Traversal}; use worktree_store::{WorktreeStore, WorktreeStoreEvent}; @@ -157,11 +156,7 @@ pub struct Project { active_entry: Option, buffer_ordered_messages_tx: mpsc::UnboundedSender, languages: Arc, - debug_adapters: HashMap, - pub open_breakpoints: Arc>>>, - /// All breakpoints that belong to this project but are in closed files - pub closed_breakpoints: Arc>>>, - next_debugger_id: AtomicUsize, + dap_store: Model, client: Arc, current_lsp_settings: HashMap, LspSettings>, join_project_response_message_id: u32, @@ -672,11 +667,15 @@ impl Project { SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx) }); + let dap_store = cx.new_model(DapStore::new); + cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); + let environment = ProjectEnvironment::new(&worktree_store, env, cx); let lsp_store = cx.new_model(|cx| { LspStore::new( buffer_store.clone(), worktree_store.clone(), + dap_store.clone(), Some(environment.clone()), languages.clone(), Some(client.http_client()), @@ -711,10 +710,7 @@ impl Project { settings_observer, fs, ssh_session: None, - next_debugger_id: Default::default(), - debug_adapters: Default::default(), - open_breakpoints: Default::default(), - closed_breakpoints: Default::default(), + dap_store, buffers_needing_diff: Default::default(), git_diff_debouncer: DebouncedDelay::new(), terminals: Terminals { @@ -854,10 +850,13 @@ impl Project { let buffer_store = cx.new_model(|cx| BufferStore::new(worktree_store.clone(), Some(remote_id), cx))?; + let dap_store = cx.new_model(DapStore::new)?; + let lsp_store = cx.new_model(|cx| { let mut lsp_store = LspStore::new( buffer_store.clone(), worktree_store.clone(), + dap_store.clone(), None, languages.clone(), Some(client.http_client()), @@ -896,6 +895,8 @@ impl Project { .detach(); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); + cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); + let mut this = Self { buffer_ordered_messages_tx: tx, buffer_store: buffer_store.clone(), @@ -910,7 +911,6 @@ impl Project { snippets, fs, ssh_session: None, - next_debugger_id: Default::default(), settings_observer: settings_observer.clone(), client_subscriptions: Default::default(), _subscriptions: vec![cx.on_release(Self::release)], @@ -922,9 +922,7 @@ impl Project { replica_id, in_room: response.payload.dev_server_project_id.is_none(), }, - debug_adapters: Default::default(), - open_breakpoints: Default::default(), - closed_breakpoints: Default::default(), + dap_store, buffers_needing_diff: Default::default(), git_diff_debouncer: DebouncedDelay::new(), terminals: Terminals { @@ -1055,22 +1053,6 @@ impl Project { } } - pub fn running_debug_adapters(&self) -> impl Iterator> + '_ { - self.debug_adapters - .values() - .filter_map(|state| match state { - DebugAdapterClientState::Starting(_) => None, - DebugAdapterClientState::Running(client) => Some(client.clone()), - }) - } - - pub fn debug_adapter_by_id(&self, id: DebugAdapterClientId) -> Option> { - self.debug_adapters.get(&id).and_then(|state| match state { - DebugAdapterClientState::Starting(_) => None, - DebugAdapterClientState::Running(client) => Some(client.clone()), - }) - } - pub fn all_breakpoints( &self, as_abs_path: bool, @@ -1078,7 +1060,8 @@ impl Project { ) -> HashMap, Vec> { let mut all_breakpoints: HashMap, Vec> = Default::default(); - for (buffer_id, breakpoints) in self.open_breakpoints.read().iter() { + let open_breakpoints = self.dap_store.read(cx).open_breakpoints(); + for (buffer_id, breakpoints) in open_breakpoints.iter() { let Some(buffer) = maybe!({ let buffer = self.buffer_for_id(*buffer_id, cx)?; Some(buffer.read(cx)) @@ -1116,7 +1099,8 @@ impl Project { ); } - for (project_path, serialized_breakpoints) in self.closed_breakpoints.read().iter() { + let closed_breakpoints = self.dap_store.read(cx).closed_breakpoints(); + for (project_path, serialized_breakpoints) in closed_breakpoints.iter() { let file_path = maybe!({ if as_abs_path { Some(Arc::from(self.absolute_path(project_path, cx)?)) @@ -1164,12 +1148,6 @@ impl Project { }) } - pub fn has_active_debugger(&self) -> bool { - self.debug_adapters - .values() - .any(|c| matches!(c, DebugAdapterClientState::Running(_))) - } - pub fn start_debug_adapter_client_from_task( &mut self, debug_task: task::ResolvedTask, @@ -1208,49 +1186,9 @@ impl Project { request_args: Option, cx: &mut ModelContext, ) { - let id = DebugAdapterClientId(self.next_debugger_id()); - let task = cx.spawn(|this, mut cx| async move { - let project = this.clone(); - let client = DebugAdapterClient::new( - id, - config, - &command, - &args, - &cwd, - request_args, - move |event, cx| { - project - .update(cx, |_, cx| { - cx.emit(Event::DebugClientEvent { - client_id: id, - payload: event, - }) - }) - .log_err(); - }, - &mut cx, - ) - .await - .log_err()?; - - this.update(&mut cx, |this, cx| { - let handle = this - .debug_adapters - .get_mut(&id) - .with_context(|| "Failed to find debug adapter with given id")?; - *handle = DebugAdapterClientState::Running(client.clone()); - - cx.emit(Event::DebugClientStarted(id)); - - anyhow::Ok(()) - }) - .log_err(); - - Some(client) + self.dap_store.update(cx, |store, cx| { + store.start_client(config, command, args, cwd, request_args, cx); }); - - self.debug_adapters - .insert(id, DebugAdapterClientState::Starting(task)); } /// Get all serialized breakpoints that belong to a buffer @@ -1276,11 +1214,11 @@ impl Project { .worktree_for_id(project_path.worktree_id, cx)? .read(cx) .abs_path(); - let bp_read_guard = self.open_breakpoints.read(); + let open_breakpoints = self.dap_store.read(cx).open_breakpoints(); Some(( worktree_path, - bp_read_guard + open_breakpoints .get(buffer_id)? .iter() .map(|bp| bp.to_serialized(buffer, project_path.path.clone())) @@ -1288,36 +1226,6 @@ impl Project { )) } - // Convert serialize breakpoints to active buffer breakpoints - // - // When a new buffer is opened, project converts any serialize - // breakpoints to active breakpoints that the buffer is aware - // of. - pub fn convert_to_open_breakpoints( - &mut self, - project_path: &ProjectPath, - buffer_id: BufferId, - snapshot: MultiBufferSnapshot, - ) { - if let Some(serialized_breakpoints) = - { self.closed_breakpoints.write().remove(project_path) } - { - let mut write_guard = self.open_breakpoints.write(); - - for serialized_bp in serialized_breakpoints { - write_guard - .entry(buffer_id) - .or_default() - .insert(Breakpoint { - position: snapshot.anchor_at( - Point::new(serialized_bp.position.saturating_sub(1), 0), - Bias::Left, - ), - }); // Serialized breakpoints start at index one so we shift when converting to open breakpoints - } - } - } - /// Serialize all breakpoints to save within workspace's database /// /// # Return @@ -1334,9 +1242,8 @@ impl Project { return result; } - let breakpoint_read_guard = self.open_breakpoints.read(); - - for buffer_id in breakpoint_read_guard.keys() { + let open_breakpoints = self.dap_store.read(cx).open_breakpoints(); + for buffer_id in open_breakpoints.keys() { if let Some((worktree_path, mut serialized_breakpoint)) = self.serialize_breakpoints_for_buffer_id(&buffer_id, cx) { @@ -1347,7 +1254,8 @@ impl Project { } } - for (project_path, serialized_bp) in self.closed_breakpoints.read().iter() { + let closed_breakpoints = self.dap_store.read(cx).closed_breakpoints(); + for (project_path, serialized_bp) in closed_breakpoints.iter() { let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else { continue; }; @@ -1368,79 +1276,35 @@ impl Project { /// This function is called whenever a breakpoint is toggled, and it doesn't need /// to send breakpoints from closed files because those breakpoints can't change /// without opening a buffer. - pub fn update_file_breakpoints(&self, buffer_id: BufferId, cx: &ModelContext) { - let clients = self.running_debug_adapters().collect::>(); - - if clients.is_empty() { - return; - } - + pub fn toggle_breakpoint( + &self, + buffer_id: BufferId, + breakpoint: Breakpoint, + cx: &mut ModelContext, + ) { let Some(buffer) = self.buffer_for_id(buffer_id, cx) else { return; }; - let buffer = buffer.read(cx); - let abs_file_path = maybe!({ - let project_path = buffer.project_path(cx)?; + let project_path = buffer.read(cx).project_path(cx)?; let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; - let path = worktree.read(cx).absolutize(&project_path.path).ok()?; - - Some(path) + worktree.read(cx).absolutize(&project_path.path).ok() }); - let Some(file_path) = abs_file_path else { + let Some(buffer_path) = abs_file_path else { return; }; - let read_guard = self.open_breakpoints.read(); - - let breakpoints = read_guard.get(&buffer_id); - let snapshot = buffer.snapshot(); - - if let Some(breakpoints) = breakpoints { - // TODO debugger: Send correct value for sourceModified - - for client in clients { - let file_path = file_path.clone(); - let source_breakpoints = breakpoints - .iter() - .map(|bp| bp.source_for_snapshot(&snapshot)) - .collect::>(); - - cx.background_executor() - .spawn(async move { - client - .set_breakpoints(Arc::from(file_path), source_breakpoints) - .await?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - } - } - - pub fn stop_debug_adapter_client( - &mut self, - client_id: DebugAdapterClientId, - should_terminate: bool, - cx: &mut ModelContext, - ) { - let Some(debug_client) = self.debug_adapters.remove(&client_id) else { - return; - }; - - cx.emit(Event::DebugClientStopped(client_id)); - - if !should_terminate { - return; - } - - if let DebugAdapterClientState::Running(client) = debug_client { - cx.spawn(|_, _| async move { client.terminate().await }) - .detach_and_log_err(cx) - } + self.dap_store.update(cx, |store, cx| { + store.toggle_breakpoint_for_buffer( + &buffer_id, + breakpoint, + buffer_path, + buffer.read(cx).snapshot(), + cx, + ); + }); } #[cfg(any(test, feature = "test-support"))] @@ -1533,6 +1397,10 @@ impl Project { project } + pub fn dap_store(&self) -> Model { + self.dap_store.clone() + } + pub fn lsp_store(&self) -> Model { self.lsp_store.clone() } @@ -2235,24 +2103,13 @@ impl Project { let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx))?; project.update(&mut cx, |project, cx| { - let serialized_breakpoints = { project.closed_breakpoints.write().remove(&path) }; - let snapshot = multi_buffer.read(cx).snapshot(cx); - - if let Some(serialized_breakpoints) = serialized_breakpoints { - let mut write_guard = project.open_breakpoints.write(); - let buffer_breakpoints = write_guard.entry(buffer_id).or_default(); - - for serialized_bp in serialized_breakpoints { - // serialized breakpoints start at index one and need to converted - // to index zero in order to display/work properly with open breakpoints - let position = snapshot.anchor_at( - Point::new(serialized_bp.position.saturating_sub(1), 0), - Bias::Left, - ); - - buffer_breakpoints.insert(Breakpoint { position }); - } - } + project.dap_store.update(cx, |store, cx| { + store.sync_closed_breakpoint_to_open_breakpoint( + &buffer_id, + &path, + multi_buffer.read(cx).snapshot(cx), + ); + }); })?; let buffer: &AnyModel = &buffer; @@ -2509,6 +2366,28 @@ impl Project { } } + fn on_dap_store_event( + &mut self, + _: Model, + event: &DapStoreEvent, + cx: &mut ModelContext, + ) { + match event { + DapStoreEvent::DebugClientStarted(client_id) => { + cx.emit(Event::DebugClientStarted(*client_id)); + } + DapStoreEvent::DebugClientStopped(client_id) => { + cx.emit(Event::DebugClientStopped(*client_id)); + } + DapStoreEvent::DebugClientEvent { client_id, payload } => { + cx.emit(Event::DebugClientEvent { + client_id: *client_id, + payload: payload.clone(), + }); + } + } + } + fn on_lsp_store_event( &mut self, _: Model, @@ -5319,10 +5198,6 @@ impl Project { .language_server_for_buffer(buffer, server_id, cx) } - fn next_debugger_id(&mut self) -> usize { - self.next_debugger_id.fetch_add(1, SeqCst) - } - pub fn task_context_for_location( &self, captured_variables: TaskVariables, diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 60f29bb573fbf8..032a27121a011c 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -3,8 +3,9 @@ use fs::Fs; use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task}; use language::LanguageRegistry; use project::{ - buffer_store::BufferStore, project_settings::SettingsObserver, search::SearchQuery, - worktree_store::WorktreeStore, LspStore, ProjectPath, WorktreeId, WorktreeSettings, + buffer_store::BufferStore, dap_store::DapStore, project_settings::SettingsObserver, + search::SearchQuery, worktree_store::WorktreeStore, LspStore, ProjectPath, WorktreeId, + WorktreeSettings, }; use remote::SshSession; use rpc::{ @@ -55,11 +56,13 @@ impl HeadlessProject { observer.shared(SSH_PROJECT_ID, session.clone().into(), cx); observer }); + let dap_store = cx.new_model(DapStore::new); let environment = project::ProjectEnvironment::new(&worktree_store, None, cx); let lsp_store = cx.new_model(|cx| { LspStore::new( buffer_store.clone(), worktree_store.clone(), + dap_store.clone(), Some(environment), languages, None, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9a3866dccad66f..ac065598aa9540 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4233,26 +4233,27 @@ impl Workspace { // Add unopened breakpoints to project before opening any items workspace.update(&mut cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - let mut write_guard = project.closed_breakpoints.write(); + project.dap_store().update(cx, |store, cx| { + for worktree in project.worktrees(cx) { + let (worktree_id, worktree_path) = + worktree.read_with(cx, |tree, _cx| (tree.id(), tree.abs_path())); - for worktree in project.worktrees(cx) { - let (worktree_id, worktree_path) = - worktree.read_with(cx, |tree, _cx| (tree.id(), tree.abs_path())); - - if let Some(serialized_breakpoints) = - serialized_workspace.breakpoints.remove(&worktree_path) - { - for serialized_bp in serialized_breakpoints { - write_guard - .entry(ProjectPath { - worktree_id, - path: serialized_bp.path.clone(), - }) - .or_default() - .push(serialized_bp); + if let Some(serialized_breakpoints) = + serialized_workspace.breakpoints.remove(&worktree_path) + { + for serialized_bp in serialized_breakpoints { + store + .closed_breakpoints + .entry(ProjectPath { + worktree_id, + path: serialized_bp.path.clone(), + }) + .or_default() + .push(serialized_bp); + } } } - } + }); }) })?; From b2927a07e4c408d6062aa0a74cebf97e487a8757 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 8 Sep 2024 11:08:25 +0200 Subject: [PATCH 219/650] Remove commented code --- crates/project/src/lsp_store.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 2301a6fd62d3c3..2ea19be93eb3b9 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -348,25 +348,6 @@ impl LspStore { this.dap_store.update(cx, |store, cx| { store.sync_open_breakpoints_to_closed_breakpoints(&buffer.remote_id(), buffer, cx); }); - // this.breakpoint_store.sync(); - // Serialize the breakpoints of this buffer and set them - // as unopened breakpoints to maintain correct state. - // Otherwise, project wouldn't allow breakpoints within - // closed files. - // TODO: debugger Fix this - // if let Some(breakpoints) = this.open_breakpoints.write().remove(&buffer.remote_id()) { - // if let Some(project_path) = buffer.project_path(cx) { - // this.closed_breakpoints - // .write() - // .entry(project_path.clone()) - // .or_default() - // .extend( - // breakpoints - // .into_iter() - // .map(|bp| bp.to_serialized(buffer, project_path.path.clone())), - // ); - // } - // } if let Some(file) = File::from_dyn(buffer.file()) { if file.is_local() { From dc5d0f41483aecb6abcdf5ca5f9e23b1dbf93e1a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 8 Sep 2024 12:37:16 +0200 Subject: [PATCH 220/650] Don't pre-paint breakpoints that are outside the viewport --- crates/editor/src/element.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 47da4fac9241dc..ccb469da5dc76a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1583,6 +1583,7 @@ impl EditorElement { fn layout_breakpoints( &self, line_height: Pixels, + range: Range, scroll_pixel_position: gpui::Point, gutter_dimensions: &GutterDimensions, gutter_hitbox: &Hitbox, @@ -1601,6 +1602,10 @@ impl EditorElement { .filter_map(|point| { let row = MultiBufferRow { 0: point.row().0 }; + if range.start > point.row() || range.end < point.row() { + return None; + } + if snapshot.is_line_folded(row) { return None; } @@ -5592,6 +5597,7 @@ impl Element for EditorElement { let breakpoints = self.layout_breakpoints( line_height, + start_row..end_row, scroll_pixel_position, &gutter_dimensions, &gutter_hitbox, From 1e99694f29a824c427e16f77197351b1a8ae1174 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 8 Sep 2024 10:12:29 -0400 Subject: [PATCH 221/650] Make Debug tasks easier for a user to config * Start setting up new debug task format * Set up blueprint for converting from debug task to regular task * Create debug adapter trait * Get debug.json schema to json lsp to show users hints * Start debugger task refactor to enable easier debugger setup * Get python adapter to work within task.json co-authored-by: Piotr * Start work on getting Php Debug adapter working * Make debug adapter trait work with async functions Fix CICD spell check & clippy warnings Co-authored-by: Remco Smits --------- Co-authored-by: Piotr Co-authored-by: Remco Smits --- Cargo.lock | 1 + crates/dap/Cargo.toml | 1 + crates/dap/src/adapters.rs | 270 ++++++++++++++++++ crates/dap/src/client.rs | 185 ++---------- crates/dap/src/lib.rs | 1 + crates/dap/src/transport.rs | 4 +- crates/debugger_ui/src/debugger_panel.rs | 36 +-- crates/debugger_ui/src/debugger_panel_item.rs | 6 +- crates/debugger_ui/src/lib.rs | 2 +- crates/languages/src/json.rs | 9 +- crates/paths/src/paths.rs | 6 + crates/project/src/dap_store.rs | 14 +- crates/project/src/project.rs | 28 +- crates/task/src/debug_format.rs | 156 +++++++++- crates/task/src/lib.rs | 13 +- crates/task/src/static_source.rs | 1 + crates/task/src/task_template.rs | 113 ++++---- crates/tasks_ui/src/lib.rs | 8 +- crates/tasks_ui/src/modal.rs | 25 +- crates/workspace/src/persistence.rs | 3 +- 20 files changed, 556 insertions(+), 326 deletions(-) create mode 100644 crates/dap/src/adapters.rs diff --git a/Cargo.lock b/Cargo.lock index 2db46cb5f26270..ce3f03acc8c15a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3329,6 +3329,7 @@ name = "dap" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "dap-types", "futures 0.3.30", "gpui", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index c8b688acfd73b9..989e0937cb8081 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] anyhow.workspace = true +async-trait.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types" } futures.workspace = true gpui.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs new file mode 100644 index 00000000000000..ed58d7c712dd5a --- /dev/null +++ b/crates/dap/src/adapters.rs @@ -0,0 +1,270 @@ +use crate::client::TransportParams; +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use futures::AsyncReadExt; +use gpui::AsyncAppContext; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use smol::{ + self, + io::BufReader, + net::{TcpListener, TcpStream}, + process, +}; +use std::{ + fmt::Debug, + net::{Ipv4Addr, SocketAddrV4}, + path::PathBuf, + process::Stdio, + sync::Arc, + time::Duration, +}; +use task::{DebugAdapterConfig, DebugAdapterKind, TCPHost}; + +pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { + match adapter_config.kind { + DebugAdapterKind::Custom => Err(anyhow!("Custom is not implemented")), + DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new(adapter_config))), + DebugAdapterKind::Php => Ok(Box::new(PhpDebugAdapter::new(adapter_config))), + } +} + +/// Get an open port to use with the tcp client when not supplied by debug config +async fn get_port(host: Ipv4Addr) -> Option { + Some( + TcpListener::bind(SocketAddrV4::new(host, 0)) + .await + .ok()? + .local_addr() + .ok()? + .port(), + ) +} + +/// Creates a debug client that connects to an adapter through tcp +/// +/// TCP clients don't have an error communication stream with an adapter +/// +/// # Parameters +/// - `command`: The command that starts the debugger +/// - `args`: Arguments of the command that starts the debugger +/// - `cwd`: The absolute path of the project that is being debugged +/// - `cx`: The context that the new client belongs too +async fn create_tcp_client( + host: TCPHost, + command: &String, + args: &Vec, + cx: &mut AsyncAppContext, +) -> Result { + let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); + + let mut port = host.port; + if port.is_none() { + port = get_port(host_address).await; + } + + let mut command = process::Command::new(command); + command + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true); + + let process = command + .spawn() + .with_context(|| "failed to start debug adapter.")?; + + if let Some(delay) = host.delay { + // some debug adapters need some time to start the TCP server + // so we have to wait few milliseconds before we can connect to it + cx.background_executor() + .timer(Duration::from_millis(delay)) + .await; + } + + let address = SocketAddrV4::new( + host_address, + port.ok_or(anyhow!("Port is required to connect to TCP server"))?, + ); + + let (rx, tx) = TcpStream::connect(address).await?.split(); + + Ok(TransportParams::new( + Box::new(BufReader::new(rx)), + Box::new(tx), + None, + Some(process), + )) +} + +/// Creates a debug client that connects to an adapter through std input/output +/// +/// # Parameters +/// - `command`: The command that starts the debugger +/// - `args`: Arguments of the command that starts the debugger +fn create_stdio_client(command: &String, args: &Vec) -> Result { + let mut command = process::Command::new(command); + command + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true); + + let mut process = command + .spawn() + .with_context(|| "failed to spawn command.")?; + + let stdin = process + .stdin + .take() + .ok_or_else(|| anyhow!("Failed to open stdin"))?; + let stdout = process + .stdout + .take() + .ok_or_else(|| anyhow!("Failed to open stdout"))?; + let stderr = process + .stderr + .take() + .ok_or_else(|| anyhow!("Failed to open stderr"))?; + + Ok(TransportParams::new( + Box::new(BufReader::new(stdout)), + Box::new(stdin), + Some(Box::new(BufReader::new(stderr))), + Some(process), + )) +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] +pub struct DebugAdapterName(pub Arc); + +pub struct DebugAdapterBinary { + pub path: PathBuf, +} + +#[async_trait(?Send)] +pub trait DebugAdapter: Debug + Send + Sync + 'static { + fn id(&self) -> String { + "".to_string() + } + + fn name(&self) -> DebugAdapterName; + + async fn connect(&self, cx: &mut AsyncAppContext) -> anyhow::Result; + + fn get_debug_adapter_start_command(&self) -> String; + + fn is_installed(&self) -> Option; + + fn download_adapter(&self) -> anyhow::Result; + + fn request_args(&self) -> Value; +} + +#[derive(Debug, Eq, PartialEq, Clone)] +struct PythonDebugAdapter { + program: String, + adapter_path: Option, +} + +impl PythonDebugAdapter { + const _ADAPTER_NAME: &'static str = "debugpy"; + + fn new(adapter_config: &DebugAdapterConfig) -> Self { + PythonDebugAdapter { + program: adapter_config.program.clone(), + adapter_path: adapter_config.adapter_path.clone(), + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for PythonDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::_ADAPTER_NAME.into()) + } + + async fn connect(&self, _cx: &mut AsyncAppContext) -> Result { + let command = "python3".to_string(); + let args = vec![self + .adapter_path + .clone() + .unwrap_or("/Users/eid/Developer/zed_debugger/".to_string())]; + + create_stdio_client(&command, &args) + } + + fn get_debug_adapter_start_command(&self) -> String { + "fail".to_string() + } + + fn is_installed(&self) -> Option { + None + } + + fn download_adapter(&self) -> anyhow::Result { + Err(anyhow::format_err!("Not implemented")) + } + + fn request_args(&self) -> Value { + json!({"program": format!("{}", &self.program)}) + } +} + +#[derive(Debug, Eq, PartialEq, Clone)] +struct PhpDebugAdapter { + program: String, + adapter_path: Option, +} + +impl PhpDebugAdapter { + const _ADAPTER_NAME: &'static str = "vscode-php-debug"; + + fn new(adapter_config: &DebugAdapterConfig) -> Self { + PhpDebugAdapter { + program: adapter_config.program.clone(), + adapter_path: adapter_config.adapter_path.clone(), + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for PhpDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::_ADAPTER_NAME.into()) + } + + async fn connect(&self, cx: &mut AsyncAppContext) -> Result { + let command = "bun".to_string(); + let args = vec![self + .adapter_path + .clone() + .unwrap_or("/Users/eid/Developer/zed_debugger/".to_string())]; + + let host = TCPHost { + port: None, + host: None, + delay: None, + }; + + create_tcp_client(host, &command, &args, cx).await + } + + fn get_debug_adapter_start_command(&self) -> String { + "fail".to_string() + } + + fn is_installed(&self) -> Option { + None + } + + fn download_adapter(&self) -> anyhow::Result { + Err(anyhow::format_err!("Not implemented")) + } + + fn request_args(&self) -> Value { + json!({"program": format!("{}", &self.program)}) + } +} diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 8f88e152099cac..e8cf98ade05970 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,6 +1,7 @@ use crate::transport::{Payload, Response, Transport}; use anyhow::{anyhow, Context, Result}; +use crate::adapters::{build_adapter, DebugAdapter}; use dap_types::{ requests::{ Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, @@ -13,29 +14,24 @@ use dap_types::{ StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, }; -use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite}; +use futures::{AsyncBufRead, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; use language::{Buffer, BufferSnapshot}; use parking_lot::{Mutex, MutexGuard}; use serde_json::Value; use smol::{ channel::{bounded, unbounded, Receiver, Sender}, - io::BufReader, - net::{TcpListener, TcpStream}, - process::{self, Child}, + process::Child, }; use std::{ collections::{BTreeMap, HashMap, HashSet}, - net::{Ipv4Addr, SocketAddrV4}, - path::{Path, PathBuf}, - process::Stdio, + path::Path, sync::{ atomic::{AtomicU64, Ordering}, Arc, }, - time::Duration, }; -use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType, TCPHost}; +use task::{DebugAdapterConfig, DebugRequestType}; use text::Point; #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -75,10 +71,7 @@ pub struct ThreadState { pub struct DebugAdapterClient { id: DebugAdapterClientId, - pub args: Vec, - pub command: String, - pub cwd: PathBuf, - pub request_args: Option, + adapter: Arc>, _process: Option, server_tx: Sender, sequence_count: AtomicU64, @@ -95,6 +88,22 @@ pub struct TransportParams { process: Option, } +impl TransportParams { + pub fn new( + rx: Box, + tx: Box, + err: Option>, + process: Option, + ) -> Self { + TransportParams { + rx, + tx, + err, + process, + } + } +} + impl DebugAdapterClient { /// Creates & returns a new debug adapter client /// @@ -105,26 +114,17 @@ impl DebugAdapterClient { /// - `args`: Arguments of the command that starts the debugger /// - `cwd`: The absolute path of the project that is being debugged /// - `cx`: The context that the new client belongs too - #[allow(clippy::too_many_arguments)] pub async fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, - command: &String, - args: &Vec, - cwd: &PathBuf, - request_args: Option, event_handler: F, cx: &mut AsyncAppContext, ) -> Result> where F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, { - let transport_params = match config.connection.clone() { - DebugConnectionType::TCP(host) => { - Self::create_tcp_client(host, command, args, cwd, cx).await? - } - DebugConnectionType::STDIO => Self::create_stdio_client(command, args, cwd).await?, - }; + let adapter = Arc::new(build_adapter(&config).context("Creating debug adapter")?); + let transport_params = adapter.connect(cx).await?; let server_tx = Self::handle_transport( transport_params.rx, @@ -138,10 +138,7 @@ impl DebugAdapterClient { id, config, server_tx, - request_args, - cwd: cwd.clone(), - args: args.clone(), - command: command.clone(), + adapter, capabilities: Default::default(), thread_states: Default::default(), sequence_count: AtomicU64::new(1), @@ -149,122 +146,6 @@ impl DebugAdapterClient { })) } - /// Creates a debug client that connects to an adapter through tcp - /// - /// TCP clients don't have an error communication stream with an adapter - /// - /// # Parameters - /// - `command`: The command that starts the debugger - /// - `args`: Arguments of the command that starts the debugger - /// - `cwd`: The absolute path of the project that is being debugged - /// - `cx`: The context that the new client belongs too - async fn create_tcp_client( - host: TCPHost, - command: &String, - args: &Vec, - cwd: &PathBuf, - cx: &mut AsyncAppContext, - ) -> Result { - let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); - - let mut port = host.port; - if port.is_none() { - port = Self::get_port(host_address).await; - } - - let mut command = process::Command::new(command); - command - .current_dir(cwd) - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .kill_on_drop(true); - - let process = command - .spawn() - .with_context(|| "failed to start debug adapter.")?; - - if let Some(delay) = host.delay { - // some debug adapters need some time to start the TCP server - // so we have to wait few milliseconds before we can connect to it - cx.background_executor() - .timer(Duration::from_millis(delay)) - .await; - } - - let address = SocketAddrV4::new( - host_address, - port.ok_or(anyhow!("Port is required to connect to TCP server"))?, - ); - - let (rx, tx) = TcpStream::connect(address).await?.split(); - - Ok(TransportParams { - rx: Box::new(BufReader::new(rx)), - tx: Box::new(tx), - err: None, - process: Some(process), - }) - } - - /// Get an open port to use with the tcp client when not supplied by debug config - async fn get_port(host: Ipv4Addr) -> Option { - Some( - TcpListener::bind(SocketAddrV4::new(host, 0)) - .await - .ok()? - .local_addr() - .ok()? - .port(), - ) - } - - /// Creates a debug client that connects to an adapter through std input/output - /// - /// # Parameters - /// - `command`: The command that starts the debugger - /// - `args`: Arguments of the command that starts the debugger - /// - `cwd`: The absolute path of the project that is being debugged - async fn create_stdio_client( - command: &String, - args: &Vec, - cwd: &PathBuf, - ) -> Result { - let mut command = process::Command::new(command); - command - .current_dir(cwd) - .args(args) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .kill_on_drop(true); - - let mut process = command - .spawn() - .with_context(|| "failed to spawn command.")?; - - let stdin = process - .stdin - .take() - .ok_or_else(|| anyhow!("Failed to open stdin"))?; - let stdout = process - .stdout - .take() - .ok_or_else(|| anyhow!("Failed to open stdout"))?; - let stderr = process - .stderr - .take() - .ok_or_else(|| anyhow!("Failed to open stderr"))?; - - Ok(TransportParams { - rx: Box::new(BufReader::new(stdout)), - tx: Box::new(stdin), - err: Some(Box::new(BufReader::new(stderr))), - process: Some(process), - }) - } - pub fn handle_transport( rx: Box, tx: Box, @@ -403,7 +284,8 @@ impl DebugAdapterClient { } pub fn request_args(&self) -> Option { - self.request_args.clone() + // TODO Debugger: Get request args from adapter + Some(self.adapter.request_args()) } pub fn request_type(&self) -> DebugRequestType { @@ -443,7 +325,7 @@ impl DebugAdapterClient { let args = dap_types::InitializeRequestArguments { client_id: Some("zed".to_owned()), client_name: Some("Zed".to_owned()), - adapter_id: self.config.id.clone(), + adapter_id: self.adapter.id(), locale: Some("en-us".to_owned()), path_format: Some(InitializeRequestArgumentsPathFormat::Path), supports_variable_type: Some(true), @@ -568,12 +450,7 @@ impl DebugAdapterClient { pub async fn restart(&self) -> Result<()> { self.request::(RestartArguments { - raw: self - .config - .request_args - .as_ref() - .map(|v| v.args.clone()) - .unwrap_or(Value::Null), + raw: self.adapter.request_args(), }) .await } @@ -619,8 +496,6 @@ impl DebugAdapterClient { absolute_file_path: Arc, breakpoints: Vec, ) -> Result { - let adapter_data = self.request_args.clone(); - self.request::(SetBreakpointsArguments { source: Source { path: Some(String::from(absolute_file_path.to_string_lossy())), @@ -629,7 +504,7 @@ impl DebugAdapterClient { presentation_hint: None, origin: None, sources: None, - adapter_data, + adapter_data: None, checksums: None, }, breakpoints: Some(breakpoints), diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index da1f4793e7aad1..df62861da7bf1b 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,3 +1,4 @@ +pub mod adapters; pub mod client; pub mod transport; pub use dap_types::*; diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 0144c6f094520f..54c1f849bb95aa 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -140,7 +140,7 @@ impl Transport { .with_context(|| "reading a message from server")? == 0 { - return Err(anyhow!("reader stream closed")); + return Err(anyhow!("debugger reader stream closed")); }; if buffer == "\r\n" { @@ -175,7 +175,7 @@ impl Transport { ) -> Result<()> { buffer.truncate(0); if err.read_line(buffer).await? == 0 { - return Err(anyhow!("error stream closed")); + return Err(anyhow!("debugger error stream closed")); }; Ok(()) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 51ea8ca9e9710e..216694669dc154 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -7,8 +7,7 @@ use dap::transport::Payload; use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, ScopesArguments, StackFrame, - StackTraceArguments, StartDebuggingRequestArguments, StoppedEvent, TerminatedEvent, - ThreadEvent, ThreadEventReason, + StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, }; use editor::Editor; use futures::future::try_join_all; @@ -16,14 +15,13 @@ use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, FontWeight, Subscription, Task, View, ViewContext, WeakView, }; -use serde_json::json; use settings::Settings; use std::collections::HashSet; use std::path::Path; use std::sync::Arc; use task::DebugRequestType; use ui::prelude::*; -use util::{merge_json_value_into, ResultExt}; +use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, @@ -89,10 +87,8 @@ impl DebugPanel { } Payload::Request(request) => { if StartDebugging::COMMAND == request.command { - Self::handle_start_debugging_request( - this, client, request, cx, - ) - .log_err(); + Self::handle_start_debugging_request(this, client, cx) + .log_err(); } } _ => unreachable!(), @@ -208,33 +204,11 @@ impl DebugPanel { fn handle_start_debugging_request( this: &mut Self, client: Arc, - request: &dap::transport::Request, cx: &mut ViewContext, ) -> Result<()> { - let arguments: StartDebuggingRequestArguments = - serde_json::from_value(request.arguments.clone().unwrap_or_default())?; - - let mut json = json!({}); - if let Some(args) = client - .config() - .request_args - .as_ref() - .map(|a| a.args.clone()) - { - merge_json_value_into(args, &mut json); - } - merge_json_value_into(arguments.configuration, &mut json); - this.workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client( - client.config(), - client.command.clone(), - client.args.clone(), - client.cwd.clone(), - Some(json), - cx, - ); + project.start_debug_adapter_client(client.config(), cx); }) }) } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 16a3fe83334de5..29ffd131e677fb 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -517,7 +517,7 @@ impl Item for DebugPanelItem { ) -> AnyElement { Label::new(format!( "{} - Thread {}", - self.client.config().id, + self.client.id().0, self.thread_id )) .color(if params.selected { @@ -530,8 +530,8 @@ impl Item for DebugPanelItem { fn tab_tooltip_text(&self, _: &AppContext) -> Option { Some(SharedString::from(format!( - "{} Thread {} - {:?}", - self.client.config().id, + "{:?} Thread {} - {:?}", + self.client.config().kind, self.thread_id, self.current_thread_state().status ))) diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index aacf47f8510e4d..b35bdf0d640057 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -21,7 +21,7 @@ pub fn init(cx: &mut AppContext) { workspace.toggle_panel_focus::(cx); }) .register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| { - tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach(); + tasks_ui::toggle_modal(workspace, cx, task::TaskModal::DebugModal).detach(); }) .register_action(DebugPanelItem::workspace_action_handler); }, diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 6b5f74c2634b45..225bce527251e1 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -87,6 +87,7 @@ impl JsonLspAdapter { cx, ); let tasks_schema = task::TaskTemplates::generate_json_schema(); + let debug_schema = task::DebugTaskFile::generate_json_schema(); let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap(); let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap(); @@ -125,8 +126,14 @@ impl JsonLspAdapter { paths::local_tasks_file_relative_path() ], "schema": tasks_schema, + }, + { + "fileMatch": [ + schema_file_match(paths::debug_tasks_file()), + paths::local_debug_file_relative_path() + ], + "schema": debug_schema, } - ] } }) diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index f9f377bbe60bdb..cbaf424e5cfe03 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -140,6 +140,12 @@ pub fn tasks_file() -> &'static PathBuf { TASKS_FILE.get_or_init(|| config_dir().join("tasks.json")) } +/// Returns the path to the `debug.json` file. +pub fn debug_tasks_file() -> &'static PathBuf { + static DEBUG_TASKS_FILE: OnceLock = OnceLock::new(); + DEBUG_TASKS_FILE.get_or_init(|| config_dir().join("debug.json")) +} + /// Returns the path to the extensions directory. /// /// This is where installed extensions are stored. diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 2edf3304729b2b..20d3444c30f3b0 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -130,15 +130,7 @@ impl DapStore { } } - pub fn start_client( - &mut self, - config: DebugAdapterConfig, - command: String, - args: Vec, - cwd: PathBuf, - request_args: Option, - cx: &mut ModelContext, - ) { + pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { let client_id = self.next_client_id(); let start_client_task = cx.spawn(|this, mut cx| async move { @@ -146,10 +138,6 @@ impl DapStore { let client = DebugAdapterClient::new( client_id, config, - &command, - &args, - &cwd, - request_args, move |payload, cx| { dap_store .update(cx, |_, cx| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c38c3114ff92dc..1f0b3dcd2bdc74 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1153,42 +1153,20 @@ impl Project { debug_task: task::ResolvedTask, cx: &mut ModelContext, ) { - let debug_template = debug_task.original_task(); - let cwd = debug_template - .cwd - .clone() - .expect("Debug tasks need to know what directory to open"); let adapter_config = debug_task .debug_adapter_config() .expect("Debug tasks need to specify adapter configuration"); - let debug_template = debug_template.clone(); - let command = debug_template.command.clone(); - let args = debug_template.args.clone(); - let request_args = adapter_config.request_args.as_ref().map(|a| a.args.clone()); - - self.start_debug_adapter_client( - adapter_config, - command, - args, - cwd.into(), - request_args, - cx, - ); + self.start_debug_adapter_client(adapter_config, cx); } pub fn start_debug_adapter_client( &mut self, config: DebugAdapterConfig, - command: String, - args: Vec, - cwd: PathBuf, - request_args: Option, cx: &mut ModelContext, ) { - self.dap_store.update(cx, |store, cx| { - store.start_client(config, command, args, cwd, request_args, cx); - }); + self.dap_store + .update(cx, |store, cx| store.start_client(config, cx)); } /// Get all serialized breakpoints that belong to a buffer diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 7f3a49ac51a409..f352eeb1482ecd 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -1,24 +1,158 @@ -use anyhow::bail; -use collections::HashMap; -use serde::Deserialize; +use schemars::{gen::SchemaSettings, JsonSchema}; +use serde::{Deserialize, Serialize}; +use std::net::Ipv4Addr; use util::ResultExt; -use crate::{TaskTemplate, TaskTemplates, VariableName}; +use crate::{TaskTemplate, TaskTemplates, TaskType}; -struct ZedDebugTaskFile {} +impl Default for DebugConnectionType { + fn default() -> Self { + DebugConnectionType::TCP(TCPHost::default()) + } +} + +/// Represents the host information of the debug adapter +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +pub struct TCPHost { + /// The port that the debug adapter is listening on + pub port: Option, + /// The host that the debug adapter is listening too + pub host: Option, + /// The delay in ms between starting and connecting to the debug adapter + pub delay: Option, +} + +/// Represents the type that will determine which request to call on the debug adapter +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum DebugRequestType { + /// Call the `launch` request on the debug adapter + #[default] + Launch, + /// Call the `attach` request on the debug adapter + Attach, +} + +/// The Debug adapter to use +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum DebugAdapterKind { + /// Manually setup starting a debug adapter + #[default] + Custom, + /// Use debugpy + Python, + /// Use vscode-php-debug + Php, +} + +/// Represents the configuration for the debug adapter +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub struct DebugAdapterConfig { + /// Unique id of for the debug adapter, + /// that will be send with the `initialize` request + pub kind: DebugAdapterKind, + /// The type of connection the adapter should use + /// The type of request that should be called on the debug adapter + #[serde(default)] + pub request: DebugRequestType, + /// The configuration options that are send with the `launch` or `attach` request + /// to the debug adapter + // pub request_args: Option, + pub program: String, + /// The path to the adapter + pub adapter_path: Option, +} -impl ZedDebugTaskFile { - fn to_zed_format(self) -> anyhow::Result {} +/// Represents the configuration for the debug adapter that is send with the launch request +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(transparent)] +pub struct DebugRequestArgs { + pub args: serde_json::Value, } -impl TryFrom for TaskTemplates { + +/// Represents the type of the debugger adapter connection +#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "lowercase", tag = "connection")] +pub enum DebugConnectionType { + /// Connect to the debug adapter via TCP + TCP(TCPHost), + /// Connect to the debug adapter via STDIO + STDIO, +} + +// "label" : "Name of debug task", +// "command": "Null", +// "task_type": "debug", +// "debug_adapter or adapter or debugger": "name of adapter or custom", +// "adapter_path": "Abs path to adapter (we would eventually remove this)", +// "session_type": "launch|attach", +// "program": "Program to debug (main.out)" +// + +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub struct DebugTaskDefinition { + /// Name of the debug tasks + label: String, + /// Program to run the debugger on + program: String, + /// Launch | Request depending on the session the adapter should be ran as + #[serde(default)] + session_type: DebugRequestType, + /// The adapter to run + adapter: DebugAdapterKind, +} + +impl DebugTaskDefinition { + fn to_zed_format(self) -> anyhow::Result { + let command = "".to_string(); + let task_type = TaskType::Debug(DebugAdapterConfig { + kind: self.adapter, + request: self.session_type, + program: self.program, + adapter_path: None, + }); + + let args: Vec = Vec::new(); + + Ok(TaskTemplate { + label: self.label, + command, + args, + task_type, + ..Default::default() + }) + } +} + +/// A group of Debug Tasks defined in a JSON file. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct DebugTaskFile(pub Vec); + +impl DebugTaskFile { + /// Generates JSON schema of Tasks JSON template format. + pub fn generate_json_schema() -> serde_json_lenient::Value { + let schema = SchemaSettings::draft07() + .with(|settings| settings.option_add_null_type = false) + .into_generator() + .into_root_schema_for::(); + + serde_json_lenient::to_value(schema).unwrap() + } +} + +impl TryFrom for TaskTemplates { type Error = anyhow::Error; - fn try_from(value: ZedDebugTaskFile) -> Result { + fn try_from(value: DebugTaskFile) -> Result { let templates = value - .tasks + .0 .into_iter() - .filter_map(|debug_task_file| debug_task_file.to_zed_format(&replacer).log_err()) + .filter_map(|debug_definition| debug_definition.to_zed_format().log_err()) .collect(); + Ok(Self(templates)) } } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 1f25df402b5d50..eae6f26acb6dab 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -1,6 +1,7 @@ //! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic. #![deny(missing_docs)] +mod debug_format; pub mod static_source; mod task_template; mod vscode_format; @@ -13,9 +14,12 @@ use std::borrow::Cow; use std::path::PathBuf; use std::str::FromStr; +pub use debug_format::{ + DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, DebugRequestType, DebugTaskFile, + TCPHost, +}; pub use task_template::{ - DebugAdapterConfig, DebugConnectionType, DebugRequestType, HideStrategy, RevealStrategy, - TCPHost, TaskTemplate, TaskTemplates, TaskType, + HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, TaskType, }; pub use vscode_format::VsCodeTaskFile; @@ -90,7 +94,10 @@ impl ResolvedTask { /// Get the configuration for the debug adapter that should be used for this task. pub fn debug_adapter_config(&self) -> Option { - self.original_task.debug_adapter.clone() + match self.original_task.task_type.clone() { + TaskType::Script => None, + TaskType::Debug(adapter_config) => Some(adapter_config), + } } /// Variables that were substituted during the task template resolution. diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 3ae9ee0b7036a7..650479967c7500 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -94,6 +94,7 @@ impl TrackedFile { let Some(new_contents) = new_contents.try_into().log_err() else { continue; }; + let mut contents = parsed_contents.write(); if *contents != new_contents { *contents = new_contents; diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 6a5ae1c7394f39..fcb8969ab8297c 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,4 +1,4 @@ -use std::{net::Ipv4Addr, path::PathBuf}; +use std::path::PathBuf; use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; @@ -8,8 +8,8 @@ use sha2::{Digest, Sha256}; use util::{truncate_and_remove_front, ResultExt}; use crate::{ - ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName, - ZED_VARIABLE_NAME_PREFIX, + debug_format::DebugAdapterConfig, ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, + VariableName, ZED_VARIABLE_NAME_PREFIX, }; /// A template definition of a Zed task to run. @@ -54,11 +54,6 @@ pub struct TaskTemplate { /// If this task should start a debugger or not #[serde(default)] pub task_type: TaskType, - /// Specific configuration for the debug adapter - /// This is only used if `task_type` is `Debug` - #[serde(default)] - pub debug_adapter: Option, - /// Represents the tags which this template attaches to. Adding this removes this task from other UI. #[serde(default)] pub tags: Vec, @@ -68,77 +63,65 @@ pub struct TaskTemplate { } /// Represents the type of task that is being ran -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "snake_case")] +#[derive(Default, Deserialize, Serialize, Eq, PartialEq, JsonSchema, Clone, Debug)] +#[serde(rename_all = "snake_case", tag = "type")] pub enum TaskType { /// Act like a typically task that runs commands #[default] Script, /// This task starts the debugger for a language - Debug, + Debug(DebugAdapterConfig), } -/// Represents the type of the debugger adapter connection -#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase", tag = "connection")] -pub enum DebugConnectionType { - /// Connect to the debug adapter via TCP - TCP(TCPHost), - /// Connect to the debug adapter via STDIO - STDIO, -} +#[cfg(test)] +mod deserialization_tests { + use crate::DebugAdapterKind; -impl Default for DebugConnectionType { - fn default() -> Self { - DebugConnectionType::TCP(TCPHost::default()) + use super::*; + use serde_json::json; + + #[test] + fn deserialize_task_type_script() { + let json = json!({"type": "script"}); + + let task_type: TaskType = + serde_json::from_value(json).expect("Failed to deserialize TaskType::Script"); + assert_eq!(task_type, TaskType::Script); } -} -/// Represents the host information of the debug adapter -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct TCPHost { - /// The port that the debug adapter is listening on - pub port: Option, - /// The host that the debug adapter is listening too - pub host: Option, - /// The delay in ms between starting and connecting to the debug adapter - pub delay: Option, -} + #[test] + fn deserialize_task_type_debug() { + let adapter_config = DebugAdapterConfig { + kind: DebugAdapterKind::Python, + request: crate::DebugRequestType::Launch, + program: "main".to_string(), + adapter_path: None, + }; + let json = json!({ + "type": "debug", + "kind": "python", + "request": "launch", + "program": "main" -/// Represents the type that will determine which request to call on the debug adapter -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "snake_case")] -pub enum DebugRequestType { - /// Call the `launch` request on the debug adapter - #[default] - Launch, - /// Call the `attach` request on the debug adapter - Attach, -} + }); -/// Represents the configuration for the debug adapter -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "snake_case")] -pub struct DebugAdapterConfig { - /// Unique id of for the debug adapter, - /// that will be send with the `initialize` request - pub id: String, - /// The type of connection the adapter should use - #[serde(default, flatten)] - pub connection: DebugConnectionType, - /// The type of request that should be called on the debug adapter - #[serde(default)] - pub request: DebugRequestType, - /// The configuration options that are send with the `launch` or `attach` request - /// to the debug adapter - pub request_args: Option, + let task_type: TaskType = + serde_json::from_value(json).expect("Failed to deserialize TaskType::Debug"); + if let TaskType::Debug(config) = task_type { + assert_eq!(config, adapter_config); + } else { + panic!("Expected TaskType::Debug"); + } + } } -/// Represents the configuration for the debug adapter that is send with the launch request -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(transparent)] -pub struct DebugRequestArgs { - pub args: serde_json::Value, +#[derive(Clone, Debug, PartialEq, Eq)] +/// The type of task modal to spawn +pub enum TaskModal { + /// Show regular tasks + ScriptModal, + /// Show debug tasks + DebugModal, } /// What to do with the terminal pane and tab, after the command was started. diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 9b3a2a01c696d9..d56aee6799cbdb 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -3,7 +3,7 @@ use editor::{tasks::task_context, Editor}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; use modal::TasksModal; use project::{Location, WorktreeId}; -use task::TaskType; +use task::TaskModal; use workspace::tasks::schedule_task; use workspace::{tasks::schedule_resolved_task, Workspace}; @@ -71,7 +71,7 @@ pub fn init(cx: &mut AppContext) { ); } } else { - toggle_modal(workspace, cx, TaskType::Script).detach(); + toggle_modal(workspace, cx, TaskModal::ScriptModal).detach(); }; }); }, @@ -82,14 +82,14 @@ pub fn init(cx: &mut AppContext) { fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext) { match &action.task_name { Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx), - None => toggle_modal(workspace, cx, task::TaskType::Script).detach(), + None => toggle_modal(workspace, cx, TaskModal::ScriptModal).detach(), } } pub fn toggle_modal( workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>, - task_type: TaskType, + task_type: TaskModal, ) -> AsyncTask<()> { let project = workspace.project().clone(); let workspace_handle = workspace.weak_handle(); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 2c5eb5051b5496..b159ce410175e2 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -9,7 +9,7 @@ use gpui::{ }; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use project::{Project, TaskSourceKind}; -use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskType}; +use task::{ResolvedTask, TaskContext, TaskId, TaskModal, TaskTemplate, TaskType}; use ui::{ div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, @@ -74,7 +74,7 @@ pub(crate) struct TasksModalDelegate { task_context: TaskContext, placeholder_text: Arc, /// If this delegate is responsible for running a scripting task or a debugger - task_type: TaskType, + task_modal_type: TaskModal, } impl TasksModalDelegate { @@ -82,7 +82,7 @@ impl TasksModalDelegate { project: Model, task_context: TaskContext, workspace: WeakView, - task_type: TaskType, + task_modal_type: TaskModal, ) -> Self { Self { project, @@ -95,7 +95,7 @@ impl TasksModalDelegate { prompt: String::default(), task_context, placeholder_text: Arc::from("Find a task, or run a command"), - task_type, + task_modal_type, } } @@ -147,11 +147,11 @@ impl TasksModal { task_context: TaskContext, workspace: WeakView, cx: &mut ViewContext, - task_type: TaskType, + task_modal_type: TaskModal, ) -> Self { let picker = cx.new_view(|cx| { Picker::uniform_list( - TasksModalDelegate::new(project, task_context, workspace, task_type), + TasksModalDelegate::new(project, task_context, workspace, task_modal_type), cx, ) }); @@ -208,7 +208,7 @@ impl PickerDelegate for TasksModalDelegate { query: String, cx: &mut ViewContext>, ) -> Task<()> { - let task_type = self.task_type.clone(); + let task_type = self.task_modal_type.clone(); cx.spawn(move |picker, mut cx| async move { let Some(candidates_task) = picker .update(&mut cx, |picker, cx| { @@ -351,7 +351,7 @@ impl PickerDelegate for TasksModalDelegate { ), // TODO: Should create a schedule_resolved_debug_task function // This would allow users to access to debug history and other issues - TaskType::Debug => workspace.project().update(cx, |project, cx| { + TaskType::Debug(_) => workspace.project().update(cx, |project, cx| { project.start_debug_adapter_client_from_task(task, cx) }), }; @@ -506,7 +506,7 @@ impl PickerDelegate for TasksModalDelegate { ), // TODO: Should create a schedule_resolved_debug_task function // This would allow users to access to debug history and other issues - TaskType::Debug => workspace.project().update(cx, |project, cx| { + TaskType::Debug(_) => workspace.project().update(cx, |project, cx| { project.start_debug_adapter_client_from_task(task, cx) }), }; @@ -616,11 +616,14 @@ impl PickerDelegate for TasksModalDelegate { fn string_match_candidates<'a>( candidates: impl Iterator + 'a, - task_type: TaskType, + task_modal_type: TaskModal, ) -> Vec { candidates .enumerate() - .filter(|(_, (_, candidate))| candidate.task_type() == task_type) + .filter(|(_, (_, candidate))| match candidate.task_type() { + TaskType::Script => task_modal_type == TaskModal::ScriptModal, + TaskType::Debug(_) => task_modal_type == TaskModal::DebugModal, + }) .map(|(index, (_, candidate))| StringMatchCandidate { id: index, char_bag: candidate.resolved_label.chars().collect(), diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 7c668ec5a21b12..5932d8cbefcb27 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -494,7 +494,8 @@ impl WorkspaceDb { .warn_on_err() .flatten()?; - // dbg! + // dbg! Remove this comment if i don't figure this out by the end of the month + // TODO Debugger: // Figure out why the below query didn't work // let breakpoints: Result> = self // .select_bound(sql! { From 0e6042e12f490acbed36c85a85db42ffcf590623 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 8 Sep 2024 16:26:47 +0200 Subject: [PATCH 222/650] Remove unused dep --- Cargo.lock | 1 - crates/debugger_ui/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce3f03acc8c15a..8a2b675e7f2600 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3441,7 +3441,6 @@ dependencies = [ "menu", "project", "serde", - "serde_json", "settings", "task", "tasks_ui", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 3b188f79ab934f..606afd370d908d 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -18,7 +18,6 @@ log.workspace = true menu.workspace = true project.workspace = true serde.workspace = true -serde_json.workspace = true settings.workspace = true task.workspace = true tasks_ui.workspace = true From 165e058dce0ecb060a5922c46c50c2c7bb3d4889 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 8 Sep 2024 16:58:13 +0200 Subject: [PATCH 223/650] Make php adapter work again with hardcoded values --- crates/dap/src/adapters.rs | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index ed58d7c712dd5a..c67c3091e84a49 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -154,8 +154,6 @@ pub trait DebugAdapter: Debug + Send + Sync + 'static { async fn connect(&self, cx: &mut AsyncAppContext) -> anyhow::Result; - fn get_debug_adapter_start_command(&self) -> String; - fn is_installed(&self) -> Option; fn download_adapter(&self) -> anyhow::Result; @@ -188,16 +186,14 @@ impl DebugAdapter for PythonDebugAdapter { async fn connect(&self, _cx: &mut AsyncAppContext) -> Result { let command = "python3".to_string(); - let args = vec![self - .adapter_path - .clone() - .unwrap_or("/Users/eid/Developer/zed_debugger/".to_string())]; - create_stdio_client(&command, &args) - } + let args = if let Some(path) = self.adapter_path.clone() { + vec![path] + } else { + Vec::new() + }; - fn get_debug_adapter_start_command(&self) -> String { - "fail".to_string() + create_stdio_client(&command, &args) } fn is_installed(&self) -> Option { @@ -238,24 +234,22 @@ impl DebugAdapter for PhpDebugAdapter { async fn connect(&self, cx: &mut AsyncAppContext) -> Result { let command = "bun".to_string(); - let args = vec![self - .adapter_path - .clone() - .unwrap_or("/Users/eid/Developer/zed_debugger/".to_string())]; + + let args = if let Some(path) = self.adapter_path.clone() { + vec![path, "--server=8132".into()] + } else { + Vec::new() + }; let host = TCPHost { - port: None, + port: Some(8132), host: None, - delay: None, + delay: Some(1000), }; create_tcp_client(host, &command, &args, cx).await } - fn get_debug_adapter_start_command(&self) -> String { - "fail".to_string() - } - fn is_installed(&self) -> Option { None } From 09c195e78e38226004abef08f7d8deba8f227784 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 8 Sep 2024 17:04:16 +0200 Subject: [PATCH 224/650] Remove debug code --- crates/task/src/debug_format.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index f352eeb1482ecd..570fb766933042 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -82,15 +82,6 @@ pub enum DebugConnectionType { STDIO, } -// "label" : "Name of debug task", -// "command": "Null", -// "task_type": "debug", -// "debug_adapter or adapter or debugger": "name of adapter or custom", -// "adapter_path": "Abs path to adapter (we would eventually remove this)", -// "session_type": "launch|attach", -// "program": "Program to debug (main.out)" -// - #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugTaskDefinition { From 5d07ab0d039462db359904536464a90ac08721a5 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 10 Sep 2024 22:35:17 +0200 Subject: [PATCH 225/650] Show correctly adapter name in thread tab --- crates/dap/src/adapters.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 31 +++++++++++-------- crates/debugger_ui/src/debugger_panel_item.rs | 4 +-- crates/task/src/debug_format.rs | 2 +- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index c67c3091e84a49..ab795f62b383c2 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -25,7 +25,7 @@ pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result Err(anyhow!("Custom is not implemented")), DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new(adapter_config))), - DebugAdapterKind::Php => Ok(Box::new(PhpDebugAdapter::new(adapter_config))), + DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new(adapter_config))), } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 216694669dc154..bdfc4effa0279e 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -83,7 +83,7 @@ impl DebugPanel { match payload { Payload::Event(event) => { - Self::handle_debug_client_events(this, client, event, cx); + this.handle_debug_client_events(client, event, cx); } Payload::Request(request) => { if StartDebugging::COMMAND == request.command { @@ -214,19 +214,19 @@ impl DebugPanel { } fn handle_debug_client_events( - this: &mut Self, + &mut self, client: Arc, event: &Events, cx: &mut ViewContext, ) { match event { - Events::Initialized(event) => Self::handle_initialized_event(client, event, cx), - Events::Stopped(event) => Self::handle_stopped_event(client, event, cx), - Events::Continued(event) => Self::handle_continued_event(client, event, cx), - Events::Exited(event) => Self::handle_exited_event(client, event, cx), - Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx), - Events::Thread(event) => Self::handle_thread_event(this, client, event, cx), - Events::Output(event) => Self::handle_output_event(client, event, cx), + Events::Initialized(event) => self.handle_initialized_event(client, event, cx), + Events::Stopped(event) => self.handle_stopped_event(client, event, cx), + Events::Continued(event) => self.handle_continued_event(client, event, cx), + Events::Exited(event) => self.handle_exited_event(client, event, cx), + Events::Terminated(event) => self.handle_terminated_event(client, event, cx), + Events::Thread(event) => self.handle_thread_event(client, event, cx), + Events::Output(event) => self.handle_output_event(client, event, cx), Events::Breakpoint(_) => {} Events::Module(_) => {} Events::LoadedSource(_) => {} @@ -365,6 +365,7 @@ impl DebugPanel { } fn handle_initialized_event( + &mut self, client: Arc, _: &Option, cx: &mut ViewContext, @@ -386,6 +387,7 @@ impl DebugPanel { } fn handle_continued_event( + &mut self, client: Arc, event: &ContinuedEvent, cx: &mut ViewContext, @@ -404,6 +406,7 @@ impl DebugPanel { } fn handle_stopped_event( + &mut self, client: Arc, event: &StoppedEvent, cx: &mut ViewContext, @@ -554,7 +557,7 @@ impl DebugPanel { } fn handle_thread_event( - this: &mut Self, + &mut self, client: Arc, event: &ThreadEvent, cx: &mut ViewContext, @@ -563,7 +566,7 @@ impl DebugPanel { if let Some(thread_state) = client.thread_states().get(&thread_id) { if !thread_state.stopped && event.reason == ThreadEventReason::Exited { - this.show_did_not_stop_warning = true; + self.show_did_not_stop_warning = true; cx.notify(); }; } @@ -595,6 +598,7 @@ impl DebugPanel { } fn handle_exited_event( + &mut self, client: Arc, _: &ExitedEvent, cx: &mut ViewContext, @@ -610,13 +614,13 @@ impl DebugPanel { } fn handle_terminated_event( - this: &mut Self, + &mut self, client: Arc, event: &Option, cx: &mut ViewContext, ) { let restart_args = event.clone().and_then(|e| e.restart); - let workspace = this.workspace.clone(); + let workspace = self.workspace.clone(); cx.spawn(|_, mut cx| async move { Self::remove_highlights(workspace.clone(), cx.clone())?; @@ -644,6 +648,7 @@ impl DebugPanel { } fn handle_output_event( + &mut self, client: Arc, event: &OutputEvent, cx: &mut ViewContext, diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 29ffd131e677fb..596bf5bd56391f 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -516,8 +516,8 @@ impl Item for DebugPanelItem { _: &WindowContext, ) -> AnyElement { Label::new(format!( - "{} - Thread {}", - self.client.id().0, + "{:?} - Thread {}", + self.client.config().kind, self.thread_id )) .color(if params.selected { diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 570fb766933042..e2d04cb5b61ac8 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -43,7 +43,7 @@ pub enum DebugAdapterKind { /// Use debugpy Python, /// Use vscode-php-debug - Php, + PHP, } /// Represents the configuration for the debug adapter From 3683920dab5731f19b3bdc662b8c261ff35293d3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:45:22 -0400 Subject: [PATCH 226/650] Add support for LLDB --- crates/dap/src/adapters.rs | 43 +++++++++++++++++++++++++++++++++ crates/task/src/debug_format.rs | 2 ++ 2 files changed, 45 insertions(+) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index ab795f62b383c2..45fddf3b3fb163 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -26,6 +26,7 @@ pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result Err(anyhow!("Custom is not implemented")), DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new(adapter_config))), DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new(adapter_config))), + DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new(adapter_config))), } } @@ -262,3 +263,45 @@ impl DebugAdapter for PhpDebugAdapter { json!({"program": format!("{}", &self.program)}) } } + +#[derive(Debug, Eq, PartialEq, Clone)] +struct LldbDebugAdapter { + program: String, + adapter_path: Option, +} + +impl LldbDebugAdapter { + const _ADAPTER_NAME: &'static str = "lldb"; + + fn new(adapter_config: &DebugAdapterConfig) -> Self { + LldbDebugAdapter { + program: adapter_config.program.clone(), + adapter_path: adapter_config.adapter_path.clone(), + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for LldbDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::_ADAPTER_NAME.into()) + } + + async fn connect(&self, _: &mut AsyncAppContext) -> Result { + let command = "/opt/homebrew/opt/llvm/bin/lldb-dap".to_string(); + + create_stdio_client(&command, &vec![]) + } + + fn is_installed(&self) -> Option { + None + } + + fn download_adapter(&self) -> anyhow::Result { + Err(anyhow::format_err!("Not implemented")) + } + + fn request_args(&self) -> Value { + json!({"program": format!("{}", &self.program)}) + } +} diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index e2d04cb5b61ac8..1d1038d79b2d71 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -44,6 +44,8 @@ pub enum DebugAdapterKind { Python, /// Use vscode-php-debug PHP, + /// Use lldb + Lldb, } /// Represents the configuration for the debug adapter From 499024297d3b2449831311c42b1247bea8c2ffc7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:57:44 -0400 Subject: [PATCH 227/650] Touch up UI with borders and whatnot --- crates/debugger_ui/src/debugger_panel_item.rs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 596bf5bd56391f..1a7073e67608dd 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -304,6 +304,7 @@ impl DebugPanelItem { v_flex() .rounded_md() + .w_full() .group("") .id(("stack-frame", stack_frame.id)) .tooltip({ @@ -552,7 +553,6 @@ impl Render for DebugPanelItem { h_flex() .key_context("DebugPanelItem") .track_focus(&self.focus_handle) - .p_2() .size_full() .items_start() .child( @@ -561,12 +561,16 @@ impl Render for DebugPanelItem { .items_start() .child( h_flex() - .py_1() + .p_1() + .border_b_1() + .w_full() + .border_color(cx.theme().colors().border_variant) .gap_2() .map(|this| { if thread_status == ThreadStatus::Running { this.child( IconButton::new("debug-pause", IconName::DebugPause) + .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, cx| { cx.dispatch_action(Box::new(DebugItemAction { kind: DebugPanelItemActionKind::Pause, @@ -577,6 +581,7 @@ impl Render for DebugPanelItem { } else { this.child( IconButton::new("debug-continue", IconName::DebugContinue) + .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, cx| { cx.dispatch_action(Box::new(DebugItemAction { kind: DebugPanelItemActionKind::Continue, @@ -591,6 +596,7 @@ impl Render for DebugPanelItem { }) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) + .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, cx| { cx.dispatch_action(Box::new(DebugItemAction { kind: DebugPanelItemActionKind::StepOver, @@ -601,6 +607,7 @@ impl Render for DebugPanelItem { ) .child( IconButton::new("debug-step-in", IconName::DebugStepInto) + .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, cx| { cx.dispatch_action(Box::new(DebugItemAction { kind: DebugPanelItemActionKind::StepIn, @@ -611,6 +618,7 @@ impl Render for DebugPanelItem { ) .child( IconButton::new("debug-step-out", IconName::DebugStepOut) + .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, cx| { cx.dispatch_action(Box::new(DebugItemAction { kind: DebugPanelItemActionKind::StepOut, @@ -621,6 +629,7 @@ impl Render for DebugPanelItem { ) .child( IconButton::new("debug-restart", IconName::DebugRestart) + .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, cx| { cx.dispatch_action(Box::new(DebugItemAction { kind: DebugPanelItemActionKind::Restart, @@ -639,6 +648,7 @@ impl Render for DebugPanelItem { ) .child( IconButton::new("debug-stop", IconName::DebugStop) + .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, cx| { cx.dispatch_action(Box::new(DebugItemAction { kind: DebugPanelItemActionKind::Stop, @@ -652,6 +662,7 @@ impl Render for DebugPanelItem { ) .child( IconButton::new("debug-disconnect", IconName::DebugDisconnect) + .icon_size(IconSize::Small) .on_click(cx.listener(|_, _, cx| { cx.dispatch_action(Box::new(DebugItemAction { kind: DebugPanelItemActionKind::Disconnect, @@ -675,10 +686,15 @@ impl Render for DebugPanelItem { ) .child( v_flex() + .border_l_1() + .border_color(cx.theme().colors().border_variant) .size_full() .items_start() .child( h_flex() + .border_b_1() + .w_full() + .border_color(cx.theme().colors().border_variant) .child( div() .id("variables") @@ -689,7 +705,7 @@ impl Render for DebugPanelItem { .when(*active_thread_item == ThreadItem::Variables, |this| { this.border_color(cx.theme().colors().border) }) - .child(Label::new("Variables")) + .child(Button::new("variables-button", "Variables")) .on_click(cx.listener(|this, _, _| { this.active_thread_item = ThreadItem::Variables; })), @@ -704,7 +720,7 @@ impl Render for DebugPanelItem { .when(*active_thread_item == ThreadItem::Console, |this| { this.border_color(cx.theme().colors().border) }) - .child(Label::new("Console")) + .child(Button::new("console-button", "Console")) .on_click(cx.listener(|this, _, _| { this.active_thread_item = ThreadItem::Console; })), @@ -719,7 +735,7 @@ impl Render for DebugPanelItem { .when(*active_thread_item == ThreadItem::Output, |this| { this.border_color(cx.theme().colors().border) }) - .child(Label::new("Output")) + .child(Button::new("output", "Output")) .on_click(cx.listener(|this, _, _| { this.active_thread_item = ThreadItem::Output; })), From 5a0c7d2885b5122c078c7009a887cdae82553243 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 11 Sep 2024 09:26:11 -0400 Subject: [PATCH 228/650] Change breakpoint position from multi_buffer::Anchor -> text::Anchor This fixes a crash that happened when placing a breakpoint within a multi buffer where it's excerpt_id != 1. Co-authored-by: Remco Smits --- crates/dap/src/client.rs | 23 +++++++++-------------- crates/editor/src/editor.rs | 21 ++++++++++----------- crates/editor/src/element.rs | 3 ++- crates/project/src/dap_store.rs | 10 ++++++---- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index e8cf98ade05970..9d070b8cccad80 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -585,16 +585,13 @@ impl DebugAdapterClient { #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Breakpoint { - pub position: multi_buffer::Anchor, + pub position: text::Anchor, } impl Breakpoint { pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { SourceBreakpoint { - line: (buffer - .summary_for_anchor::(&self.position.text_anchor) - .row - + 1) as u64, + line: (buffer.summary_for_anchor::(&self.position).row + 1) as u64, condition: None, hit_condition: None, log_message: None, @@ -604,15 +601,16 @@ impl Breakpoint { } pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { - buffer.summary_for_anchor::(&self.position.text_anchor) + buffer.summary_for_anchor::(&self.position) + } + + pub fn point_for_buffer_snapshot(&self, buffer_snapshot: &BufferSnapshot) -> Point { + buffer_snapshot.summary_for_anchor::(&self.position) } pub fn source_for_snapshot(&self, snapshot: &BufferSnapshot) -> SourceBreakpoint { SourceBreakpoint { - line: (snapshot - .summary_for_anchor::(&self.position.text_anchor) - .row - + 1) as u64, + line: (snapshot.summary_for_anchor::(&self.position).row + 1) as u64, condition: None, hit_condition: None, log_message: None, @@ -623,10 +621,7 @@ impl Breakpoint { pub fn to_serialized(&self, buffer: &Buffer, path: Arc) -> SerializedBreakpoint { SerializedBreakpoint { - position: buffer - .summary_for_anchor::(&self.position.text_anchor) - .row - + 1, + position: buffer.summary_for_anchor::(&self.position).row + 1, path, } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c4b2a3256ae15d..8c26b50a31664d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5307,10 +5307,9 @@ impl Editor { if let Some(breakpoints) = opened_breakpoints.get(&buffer.remote_id()) { for breakpoint in breakpoints { - breakpoint_display_points - .insert(breakpoint.position.to_display_point(&snapshot)); - // Breakpoints TODO debugger: Multibuffer bp toggle failing here - // dued to invalid excerpt id. Multibuffer excerpt id isn't the same as a singular buffer id + let point = breakpoint.point_for_buffer(&buffer); + + breakpoint_display_points.insert(point.to_display_point(&snapshot)); } }; @@ -5346,9 +5345,8 @@ impl Editor { if let Some(breakpoints) = opened_breakpoints.get(&info.buffer_id) { for breakpoint in breakpoints { - let breakpoint_position = info // Breakpoint's position within the singular buffer - .buffer - .summary_for_anchor::(&breakpoint.position.text_anchor); + let breakpoint_position = + breakpoint.point_for_buffer_snapshot(&info.buffer); if buffer_range.contains(&breakpoint_position) { // Translated breakpoint position from singular buffer to multi buffer @@ -5368,7 +5366,7 @@ impl Editor { fn render_breakpoint( &self, - position: Anchor, + anchor: text::Anchor, row: DisplayRow, cx: &mut ViewContext, ) -> IconButton { @@ -5387,7 +5385,7 @@ impl Editor { .icon_color(color) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); - editor.toggle_breakpoint_at_anchor(position, cx) + editor.toggle_breakpoint_at_anchor(anchor, cx); })) } @@ -6243,14 +6241,15 @@ impl Editor { .snapshot(cx) .display_snapshot .buffer_snapshot - .anchor_before(Point::new(cursor_position.row, 0)); + .anchor_before(Point::new(cursor_position.row, 0)) + .text_anchor; self.toggle_breakpoint_at_anchor(breakpoint_position, cx); } pub fn toggle_breakpoint_at_anchor( &mut self, - breakpoint_position: Anchor, + breakpoint_position: text::Anchor, cx: &mut ViewContext, ) { let Some(project) = &self.project else { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ccb469da5dc76a..2bc5980311c38c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1612,7 +1612,8 @@ impl EditorElement { let position = snapshot .display_snapshot - .display_point_to_anchor(*point, Bias::Left); + .display_point_to_anchor(*point, Bias::Left) + .text_anchor; let button = editor.render_breakpoint(position, point.row(), cx); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 20d3444c30f3b0..922af050f5ac6e 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -121,10 +121,12 @@ impl DapStore { for closed_breakpoint in closed_breakpoints { // serialized breakpoints start at index one and need to converted // to index zero in order to display/work properly with open breakpoints - let position = snapshot.anchor_at( - Point::new(closed_breakpoint.position.saturating_sub(1), 0), - Bias::Left, - ); + let position = snapshot + .anchor_at( + Point::new(closed_breakpoint.position.saturating_sub(1), 0), + Bias::Left, + ) + .text_anchor; open_breakpoints.insert(Breakpoint { position }); } From 0f5e5ea3b429d7731d5b726a28c61cca7ed2f755 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 11 Sep 2024 12:03:45 -0400 Subject: [PATCH 229/650] Change open_breakpoint's BTree to use project_path as it's key This is the first step of merging open/close breakpoints into one data structure. Co-authored-by: Remco Smits --- crates/editor/src/editor.rs | 53 ++++++++++++++++++++------------- crates/project/src/dap_store.rs | 52 ++++++++++++++++---------------- crates/project/src/lsp_store.rs | 8 +++-- crates/project/src/project.rs | 49 +++++++++++++++--------------- 4 files changed, 89 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8c26b50a31664d..ea721f2ec428f0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1984,22 +1984,21 @@ impl Editor { this.start_git_blame_inline(false, cx); } - // Check if this buffer should have breakpoints added too it - if let Some((project_path, buffer_id, snapshot)) = buffer.read_with(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - let buffer = buffer.as_singleton()?.read(cx); - Some((buffer.project_path(cx)?, buffer.remote_id(), snapshot)) - }) { - if let Some(project) = this.project.as_ref() { + if let Some(project) = this.project.as_ref() { + let snapshot = buffer.read(cx).snapshot(cx); + let project_paths: Vec<_> = buffer + .read(cx) + .all_buffers() + .iter() + .filter_map(|buffer| buffer.read(cx).project_path(cx)) + .collect(); + + for ref project_path in project_paths { project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, _| { - store.sync_closed_breakpoint_to_open_breakpoint( - &buffer_id, - &project_path, - snapshot, - ); - }); - }); + project.dap_store().update(cx, |store, _cx| { + store.sync_closed_breakpoint_to_open_breakpoint(project_path, &snapshot) + }) + }) } } } @@ -5305,18 +5304,23 @@ impl Editor { if let Some(buffer) = self.buffer.read(cx).as_singleton() { let buffer = buffer.read(cx); - if let Some(breakpoints) = opened_breakpoints.get(&buffer.remote_id()) { - for breakpoint in breakpoints { - let point = breakpoint.point_for_buffer(&buffer); + if let Some(project_path) = buffer.project_path(cx) { + if let Some(breakpoints) = opened_breakpoints.get(&project_path) { + for breakpoint in breakpoints { + let point = breakpoint.point_for_buffer(&buffer); - breakpoint_display_points.insert(point.to_display_point(&snapshot)); - } + breakpoint_display_points.insert(point.to_display_point(&snapshot)); + } + }; }; return breakpoint_display_points; } let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; + let Some(project) = self.project.as_ref() else { + return breakpoint_display_points; + }; for excerpt_boundary in multi_buffer_snapshot.excerpt_boundaries_in_range(Point::new(0, 0)..) @@ -5343,7 +5347,14 @@ impl Editor { .buffer .summary_for_anchor::(&info.range.context.end); - if let Some(breakpoints) = opened_breakpoints.get(&info.buffer_id) { + let Some(project_path) = project.read_with(cx, |this, cx| { + this.buffer_for_id(info.buffer_id, cx) + .and_then(|buffer| buffer.read_with(cx, |b, cx| b.project_path(cx))) + }) else { + continue; + }; + + if let Some(breakpoints) = opened_breakpoints.get(&project_path) { for breakpoint in breakpoints { let breakpoint_position = breakpoint.point_for_buffer_snapshot(&info.buffer); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 922af050f5ac6e..ec3d93fc1eaf84 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -17,10 +17,10 @@ use std::{ }, }; use task::DebugAdapterConfig; -use text::{Bias, BufferId, Point}; +use text::{Bias, Point}; use util::ResultExt as _; -use crate::{Item, ProjectPath}; +use crate::ProjectPath; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), @@ -39,7 +39,7 @@ pub enum DebugAdapterClientState { pub struct DapStore { next_client_id: AtomicUsize, clients: HashMap, - open_breakpoints: BTreeMap>, + open_breakpoints: BTreeMap>, /// All breakpoints that belong to this project but are in closed files pub closed_breakpoints: BTreeMap>, _subscription: Vec, @@ -76,7 +76,7 @@ impl DapStore { }) } - pub fn open_breakpoints(&self) -> &BTreeMap> { + pub fn open_breakpoints(&self) -> &BTreeMap> { &self.open_breakpoints } @@ -86,37 +86,36 @@ impl DapStore { pub fn sync_open_breakpoints_to_closed_breakpoints( &mut self, - buffer_id: &BufferId, + project_path: &ProjectPath, buffer: &mut Buffer, - cx: &mut ModelContext, ) { - let Some(breakpoints) = self.open_breakpoints.remove(&buffer_id) else { + let Some(breakpoints) = self.open_breakpoints.remove(project_path) else { return; }; - if let Some(project_path) = buffer.project_path(cx) { - self.closed_breakpoints - .entry(project_path.clone()) - .or_default() - .extend( - breakpoints - .into_iter() - .map(|bp| bp.to_serialized(buffer, project_path.path.clone())), - ); - } + self.closed_breakpoints + .entry(project_path.clone()) + .or_default() + .extend( + breakpoints + .into_iter() + .map(|bp| bp.to_serialized(buffer, project_path.path.clone())), + ); } pub fn sync_closed_breakpoint_to_open_breakpoint( &mut self, - buffer_id: &BufferId, project_path: &ProjectPath, - snapshot: MultiBufferSnapshot, + snapshot: &MultiBufferSnapshot, ) { let Some(closed_breakpoints) = self.closed_breakpoints.remove(project_path) else { return; }; - let open_breakpoints = self.open_breakpoints.entry(*buffer_id).or_default(); + let open_breakpoints = self + .open_breakpoints + .entry(project_path.clone()) + .or_default(); for closed_breakpoint in closed_breakpoints { // serialized breakpoints start at index one and need to converted @@ -222,24 +221,27 @@ impl DapStore { pub fn toggle_breakpoint_for_buffer( &mut self, - buffer_id: &BufferId, + project_path: &ProjectPath, breakpoint: Breakpoint, buffer_path: PathBuf, buffer_snapshot: BufferSnapshot, cx: &mut ModelContext, ) { - let breakpoint_set = self.open_breakpoints.entry(*buffer_id).or_default(); + let breakpoint_set = self + .open_breakpoints + .entry(project_path.clone()) + .or_default(); if !breakpoint_set.remove(&breakpoint) { breakpoint_set.insert(breakpoint); } - self.send_changed_breakpoints(buffer_id, buffer_path, buffer_snapshot, cx); + self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx); } pub fn send_changed_breakpoints( &self, - buffer_id: &BufferId, + project_path: &ProjectPath, buffer_path: PathBuf, buffer_snapshot: BufferSnapshot, cx: &mut ModelContext, @@ -250,7 +252,7 @@ impl DapStore { return; } - let Some(breakpoints) = self.open_breakpoints.get(buffer_id) else { + let Some(breakpoints) = self.open_breakpoints.get(project_path) else { return; }; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 2ea19be93eb3b9..3f8ac4edbc8724 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -345,9 +345,11 @@ impl LspStore { self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_servers(buffer, cx); cx.observe_release(buffer, |this, buffer, cx| { - this.dap_store.update(cx, |store, cx| { - store.sync_open_breakpoints_to_closed_breakpoints(&buffer.remote_id(), buffer, cx); - }); + if let Some(project_path) = buffer.project_path(cx) { + this.dap_store.update(cx, |store, _cx| { + store.sync_open_breakpoints_to_closed_breakpoints(&project_path, buffer); + }); + }; if let Some(file) = File::from_dyn(buffer.file()) { if file.is_local() { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1f0b3dcd2bdc74..e0210396ce3a3b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1061,8 +1061,10 @@ impl Project { let mut all_breakpoints: HashMap, Vec> = Default::default(); let open_breakpoints = self.dap_store.read(cx).open_breakpoints(); - for (buffer_id, breakpoints) in open_breakpoints.iter() { + for (project_path, breakpoints) in open_breakpoints.iter() { let Some(buffer) = maybe!({ + let buffer_store = self.buffer_store.read(cx); + let buffer_id = buffer_store.buffer_id_for_project_path(project_path)?; let buffer = self.buffer_for_id(*buffer_id, cx)?; Some(buffer.read(cx)) }) else { @@ -1070,8 +1072,6 @@ impl Project { }; let Some(path) = maybe!({ - let project_path = buffer.project_path(cx)?; - if as_abs_path { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; Some(Arc::from( @@ -1082,7 +1082,7 @@ impl Project { .as_path(), )) } else { - Some(project_path.path) + Some(project_path.clone().path) } }) else { continue; @@ -1181,13 +1181,17 @@ impl Project { /// /// `(Path, Vec, ) -> Option<(Arc, Vec)> { + let buffer_id = self + .buffer_store + .read(cx) + .buffer_id_for_project_path(project_path)?; let buffer = self.buffer_for_id(*buffer_id, cx)?.read(cx); - let project_path = buffer.project_path(cx)?; + let worktree_path = self .worktree_for_id(project_path.worktree_id, cx)? .read(cx) @@ -1197,7 +1201,7 @@ impl Project { Some(( worktree_path, open_breakpoints - .get(buffer_id)? + .get(&project_path)? .iter() .map(|bp| bp.to_serialized(buffer, project_path.path.clone())) .collect(), @@ -1221,9 +1225,9 @@ impl Project { } let open_breakpoints = self.dap_store.read(cx).open_breakpoints(); - for buffer_id in open_breakpoints.keys() { + for project_path in open_breakpoints.keys() { if let Some((worktree_path, mut serialized_breakpoint)) = - self.serialize_breakpoints_for_buffer_id(&buffer_id, cx) + self.serialize_breakpoints_for_project_path(&project_path, cx) { result .entry(worktree_path.clone()) @@ -1264,19 +1268,20 @@ impl Project { return; }; - let abs_file_path = maybe!({ + let Some((project_path, buffer_path)) = maybe!({ let project_path = buffer.read(cx).project_path(cx)?; - let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; - worktree.read(cx).absolutize(&project_path.path).ok() - }); - - let Some(buffer_path) = abs_file_path else { + let worktree = self.worktree_for_id(project_path.clone().worktree_id, cx)?; + Some(( + project_path.clone(), + worktree.read(cx).absolutize(&project_path.path).ok()?, + )) + }) else { return; }; self.dap_store.update(cx, |store, cx| { store.toggle_breakpoint_for_buffer( - &buffer_id, + &project_path, breakpoint, buffer_path, buffer.read(cx).snapshot(), @@ -2072,20 +2077,16 @@ impl Project { let task = self.open_buffer(path.clone(), cx); cx.spawn(move |project, mut cx| async move { let buffer = task.await?; - let (project_entry_id, buffer_id) = buffer.read_with(&cx, |buffer, cx| { - ( - File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)), - buffer.remote_id(), - ) + let project_entry_id = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) })?; let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx))?; project.update(&mut cx, |project, cx| { project.dap_store.update(cx, |store, cx| { store.sync_closed_breakpoint_to_open_breakpoint( - &buffer_id, &path, - multi_buffer.read(cx).snapshot(cx), + &multi_buffer.read(cx).snapshot(cx), ); }); })?; From e6049f98305401231ee651465c03cf3f16546b34 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 11 Sep 2024 15:21:31 -0400 Subject: [PATCH 230/650] Merge open/close breakpoints into one data structure This is done by making breakpoint.position field optional and adding a cached_position field too. I also added dap_store to buffer_store and got buffer_store to initialize breakpoints when a new buffer is opened. Fixing a bug where some breakpoints wouldn't appear within multi buffers --- crates/dap/src/client.rs | 54 +++++++++-- crates/editor/src/editor.rs | 34 +++---- crates/project/src/buffer_store.rs | 18 +++- crates/project/src/dap_store.rs | 98 +++++++++---------- crates/project/src/project.rs | 99 ++++++-------------- crates/remote_server/src/headless_project.rs | 10 +- crates/text/src/anchor.rs | 8 ++ crates/workspace/src/persistence.rs | 8 +- crates/workspace/src/workspace.rs | 11 +-- 9 files changed, 171 insertions(+), 169 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 9d070b8cccad80..fc6cf1e464f2d0 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -25,6 +25,7 @@ use smol::{ }; use std::{ collections::{BTreeMap, HashMap, HashSet}, + hash::Hash, path::Path, sync::{ atomic::{AtomicU64, Ordering}, @@ -583,15 +584,22 @@ impl DebugAdapterClient { } } -#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct Breakpoint { - pub position: text::Anchor, + pub active_position: Option, + pub cache_position: u32, } impl Breakpoint { pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { + let line = self + .active_position + .map(|position| buffer.summary_for_anchor::(&position).row) + .unwrap_or(self.cache_position) as u64 + + 1u64; + SourceBreakpoint { - line: (buffer.summary_for_anchor::(&self.position).row + 1) as u64, + line, condition: None, hit_condition: None, log_message: None, @@ -600,17 +608,34 @@ impl Breakpoint { } } + pub fn set_active_position(&mut self, buffer: &Buffer) { + if self.active_position.is_none() { + self.active_position = + Some(buffer.anchor_at(Point::new(self.cache_position, 0), text::Bias::Left)); + } + } + pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { - buffer.summary_for_anchor::(&self.position) + self.active_position + .map(|position| buffer.summary_for_anchor::(&position)) + .unwrap_or(Point::new(self.cache_position, 0)) } pub fn point_for_buffer_snapshot(&self, buffer_snapshot: &BufferSnapshot) -> Point { - buffer_snapshot.summary_for_anchor::(&self.position) + self.active_position + .map(|position| buffer_snapshot.summary_for_anchor::(&position)) + .unwrap_or(Point::new(self.cache_position, 0)) } pub fn source_for_snapshot(&self, snapshot: &BufferSnapshot) -> SourceBreakpoint { + let line = self + .active_position + .map(|position| snapshot.summary_for_anchor::(&position).row) + .unwrap_or(self.cache_position) as u64 + + 1u64; + SourceBreakpoint { - line: (snapshot.summary_for_anchor::(&self.position).row + 1) as u64, + line, condition: None, hit_condition: None, log_message: None, @@ -619,10 +644,19 @@ impl Breakpoint { } } - pub fn to_serialized(&self, buffer: &Buffer, path: Arc) -> SerializedBreakpoint { - SerializedBreakpoint { - position: buffer.summary_for_anchor::(&self.position).row + 1, - path, + pub fn to_serialized(&self, buffer: Option<&Buffer>, path: Arc) -> SerializedBreakpoint { + match buffer { + Some(buffer) => SerializedBreakpoint { + position: self + .active_position + .map(|position| buffer.summary_for_anchor::(&position).row + 1u32) + .unwrap_or(self.cache_position + 1u32), + path, + }, + None => SerializedBreakpoint { + position: self.cache_position + 1u32, + path, + }, } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ea721f2ec428f0..57e7d8b25f15ce 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1983,24 +1983,6 @@ impl Editor { this.git_blame_inline_enabled = true; this.start_git_blame_inline(false, cx); } - - if let Some(project) = this.project.as_ref() { - let snapshot = buffer.read(cx).snapshot(cx); - let project_paths: Vec<_> = buffer - .read(cx) - .all_buffers() - .iter() - .filter_map(|buffer| buffer.read(cx).project_path(cx)) - .collect(); - - for ref project_path in project_paths { - project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, _cx| { - store.sync_closed_breakpoint_to_open_breakpoint(project_path, &snapshot) - }) - }) - } - } } this.report_editor_event("open", None, cx); @@ -5299,7 +5281,7 @@ impl Editor { let snapshot = self.snapshot(cx); - let opened_breakpoints = dap_store.read(cx).open_breakpoints(); + let opened_breakpoints = dap_store.read(cx).breakpoints(); if let Some(buffer) = self.buffer.read(cx).as_singleton() { let buffer = buffer.read(cx); @@ -6275,11 +6257,23 @@ impl Editor { return; }; + let Some(cache_position) = self.buffer.read_with(cx, |buffer, cx| { + buffer.buffer(buffer_id).map(|buffer| { + buffer + .read(cx) + .summary_for_anchor::(&breakpoint_position) + .row + }) + }) else { + return; + }; + project.update(cx, |project, cx| { project.toggle_breakpoint( buffer_id, Breakpoint { - position: breakpoint_position, + cache_position, + active_position: Some(breakpoint_position), }, cx, ); diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 43db4f9f21901e..365b5791b27f95 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1,4 +1,5 @@ use crate::{ + dap_store::DapStore, search::SearchQuery, worktree_store::{WorktreeStore, WorktreeStoreEvent}, Item, NoRepositoryError, ProjectPath, @@ -36,6 +37,7 @@ pub struct BufferStore { remote_id: Option, #[allow(unused)] worktree_store: Model, + dap_store: Model, opened_buffers: HashMap, local_buffer_ids_by_path: HashMap, local_buffer_ids_by_entry_id: HashMap, @@ -89,6 +91,7 @@ impl BufferStore { pub fn new( worktree_store: Model, remote_id: Option, + dap_store: Model, cx: &mut ModelContext, ) -> Self { cx.subscribe(&worktree_store, |this, _, event, cx| { @@ -102,6 +105,7 @@ impl BufferStore { remote_id, downstream_client: None, worktree_store, + dap_store, opened_buffers: Default::default(), remote_buffer_listeners: Default::default(), loading_remote_buffers_by_id: Default::default(), @@ -117,8 +121,11 @@ impl BufferStore { project_path: ProjectPath, cx: &mut ModelContext, ) -> Task>> { - let existing_buffer = self.get_by_path(&project_path, cx); - if let Some(existing_buffer) = existing_buffer { + let buffer = self.get_by_path(&project_path, cx); + if let Some(existing_buffer) = buffer { + self.dap_store.update(cx, |store, cx| { + store.set_active_breakpoints(&project_path, &existing_buffer.read(cx)); + }); return Task::ready(Ok(existing_buffer)); } @@ -152,10 +159,15 @@ impl BufferStore { cx.spawn(move |this, mut cx| async move { let load_result = load_buffer.await; - *tx.borrow_mut() = Some(this.update(&mut cx, |this, _| { + *tx.borrow_mut() = Some(this.update(&mut cx, |this, cx| { // Record the fact that the buffer is no longer loading. this.loading_buffers_by_path.remove(&project_path); let buffer = load_result.map_err(Arc::new)?; + + this.dap_store.update(cx, |store, cx| { + store.set_active_breakpoints(&project_path, buffer.read(cx)); + }); + Ok(buffer) })?); anyhow::Ok(()) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index ec3d93fc1eaf84..df5bfe969ca212 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -6,7 +6,7 @@ use dap::{ }; use gpui::{EventEmitter, ModelContext, Subscription, Task}; use language::{Buffer, BufferSnapshot}; -use multi_buffer::MultiBufferSnapshot; +use settings::WorktreeId; use std::{ collections::BTreeMap, future::Future, @@ -17,7 +17,6 @@ use std::{ }, }; use task::DebugAdapterConfig; -use text::{Bias, Point}; use util::ResultExt as _; use crate::ProjectPath; @@ -39,9 +38,7 @@ pub enum DebugAdapterClientState { pub struct DapStore { next_client_id: AtomicUsize, clients: HashMap, - open_breakpoints: BTreeMap>, - /// All breakpoints that belong to this project but are in closed files - pub closed_breakpoints: BTreeMap>, + breakpoints: BTreeMap>, _subscription: Vec, } @@ -52,8 +49,7 @@ impl DapStore { Self { next_client_id: Default::default(), clients: Default::default(), - open_breakpoints: Default::default(), - closed_breakpoints: Default::default(), + breakpoints: Default::default(), _subscription: vec![cx.on_app_quit(Self::shutdown_clients)], } } @@ -76,58 +72,59 @@ impl DapStore { }) } - pub fn open_breakpoints(&self) -> &BTreeMap> { - &self.open_breakpoints + pub fn breakpoints(&self) -> &BTreeMap> { + &self.breakpoints } - pub fn closed_breakpoints(&self) -> &BTreeMap> { - &self.closed_breakpoints + pub fn set_active_breakpoints(&mut self, project_path: &ProjectPath, buffer: &Buffer) { + let entry = self.breakpoints.remove(project_path).unwrap_or_default(); + let mut set_bp: HashSet = HashSet::default(); + + for mut bp in entry.into_iter() { + bp.set_active_position(&buffer); + set_bp.insert(bp); + } + + self.breakpoints.insert(project_path.clone(), set_bp); } - pub fn sync_open_breakpoints_to_closed_breakpoints( + pub fn deserialize_breakpoints( &mut self, - project_path: &ProjectPath, - buffer: &mut Buffer, + worktree_id: WorktreeId, + serialize_breakpoints: Vec, ) { - let Some(breakpoints) = self.open_breakpoints.remove(project_path) else { - return; - }; - - self.closed_breakpoints - .entry(project_path.clone()) - .or_default() - .extend( - breakpoints - .into_iter() - .map(|bp| bp.to_serialized(buffer, project_path.path.clone())), - ); + for serialize_breakpoint in serialize_breakpoints { + self.breakpoints + .entry(ProjectPath { + worktree_id, + path: serialize_breakpoint.path.clone(), + }) + .or_default() + .insert(Breakpoint { + active_position: None, + cache_position: serialize_breakpoint.position.saturating_sub(1u32), + }); + } } - pub fn sync_closed_breakpoint_to_open_breakpoint( + pub fn sync_open_breakpoints_to_closed_breakpoints( &mut self, project_path: &ProjectPath, - snapshot: &MultiBufferSnapshot, + buffer: &mut Buffer, ) { - let Some(closed_breakpoints) = self.closed_breakpoints.remove(project_path) else { - return; - }; + if let Some(breakpoint_set) = self.breakpoints.remove(project_path) { + let breakpoint_iter = breakpoint_set.into_iter().map(|mut bp| { + bp.cache_position = bp.point_for_buffer(&buffer).row; + bp.active_position = None; + bp + }); + + let mut hash_set = HashSet::default(); + for bp in breakpoint_iter { + hash_set.insert(bp); + } - let open_breakpoints = self - .open_breakpoints - .entry(project_path.clone()) - .or_default(); - - for closed_breakpoint in closed_breakpoints { - // serialized breakpoints start at index one and need to converted - // to index zero in order to display/work properly with open breakpoints - let position = snapshot - .anchor_at( - Point::new(closed_breakpoint.position.saturating_sub(1), 0), - Bias::Left, - ) - .text_anchor; - - open_breakpoints.insert(Breakpoint { position }); + self.breakpoints.insert(project_path.clone(), hash_set); } } @@ -227,10 +224,7 @@ impl DapStore { buffer_snapshot: BufferSnapshot, cx: &mut ModelContext, ) { - let breakpoint_set = self - .open_breakpoints - .entry(project_path.clone()) - .or_default(); + let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); if !breakpoint_set.remove(&breakpoint) { breakpoint_set.insert(breakpoint); @@ -252,7 +246,7 @@ impl DapStore { return; } - let Some(breakpoints) = self.open_breakpoints.get(project_path) else { + let Some(breakpoints) = self.breakpoints.get(project_path) else { return; }; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e0210396ce3a3b..14e971f3129caa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -65,7 +65,6 @@ use language::{ }; use lsp::{CompletionContext, DocumentHighlightKind, LanguageServer, LanguageServerId}; use lsp_command::*; -use multi_buffer::MultiBuffer; use node_runtime::NodeRuntime; use parking_lot::{Mutex, RwLock}; use paths::{ @@ -658,8 +657,11 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); - let buffer_store = - cx.new_model(|cx| BufferStore::new(worktree_store.clone(), None, cx)); + let dap_store = cx.new_model(DapStore::new); + + let buffer_store = cx.new_model(|cx| { + BufferStore::new(worktree_store.clone(), None, dap_store.clone(), cx) + }); cx.subscribe(&buffer_store, Self::on_buffer_store_event) .detach(); @@ -667,7 +669,6 @@ impl Project { SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx) }); - let dap_store = cx.new_model(DapStore::new); cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); let environment = ProjectEnvironment::new(&worktree_store, env, cx); @@ -847,11 +848,18 @@ impl Project { } store })?; - let buffer_store = - cx.new_model(|cx| BufferStore::new(worktree_store.clone(), Some(remote_id), cx))?; let dap_store = cx.new_model(DapStore::new)?; + let buffer_store = cx.new_model(|cx| { + BufferStore::new( + worktree_store.clone(), + Some(remote_id), + dap_store.clone(), + cx, + ) + })?; + let lsp_store = cx.new_model(|cx| { let mut lsp_store = LspStore::new( buffer_store.clone(), @@ -1060,16 +1068,14 @@ impl Project { ) -> HashMap, Vec> { let mut all_breakpoints: HashMap, Vec> = Default::default(); - let open_breakpoints = self.dap_store.read(cx).open_breakpoints(); + let open_breakpoints = self.dap_store.read(cx).breakpoints(); for (project_path, breakpoints) in open_breakpoints.iter() { - let Some(buffer) = maybe!({ + let buffer = maybe!({ let buffer_store = self.buffer_store.read(cx); let buffer_id = buffer_store.buffer_id_for_project_path(project_path)?; let buffer = self.buffer_for_id(*buffer_id, cx)?; Some(buffer.read(cx)) - }) else { - continue; - }; + }); let Some(path) = maybe!({ if as_abs_path { @@ -1088,35 +1094,13 @@ impl Project { continue; }; - let Some(relative_path) = maybe!({ Some(buffer.project_path(cx)?.path) }) else { - continue; - }; - all_breakpoints.entry(path).or_default().extend( breakpoints .into_iter() - .map(|bp| bp.to_serialized(buffer, relative_path.clone())), + .map(|bp| bp.to_serialized(buffer, project_path.clone().path)), ); } - let closed_breakpoints = self.dap_store.read(cx).closed_breakpoints(); - for (project_path, serialized_breakpoints) in closed_breakpoints.iter() { - let file_path = maybe!({ - if as_abs_path { - Some(Arc::from(self.absolute_path(project_path, cx)?)) - } else { - Some(project_path.path.clone()) - } - }); - - if let Some(file_path) = file_path { - all_breakpoints - .entry(file_path) - .or_default() - .extend(serialized_breakpoints.iter().map(|bp| bp.clone())); - } - } - all_breakpoints } @@ -1186,21 +1170,24 @@ impl Project { project_path: &ProjectPath, cx: &ModelContext, ) -> Option<(Arc, Vec)> { - let buffer_id = self - .buffer_store - .read(cx) - .buffer_id_for_project_path(project_path)?; - let buffer = self.buffer_for_id(*buffer_id, cx)?.read(cx); + let buffer = maybe!({ + let buffer_id = self + .buffer_store + .read(cx) + .buffer_id_for_project_path(project_path)?; + Some(self.buffer_for_id(*buffer_id, cx)?.read(cx)) + }); let worktree_path = self .worktree_for_id(project_path.worktree_id, cx)? .read(cx) .abs_path(); - let open_breakpoints = self.dap_store.read(cx).open_breakpoints(); + + let breakpoints = self.dap_store.read(cx).breakpoints(); Some(( worktree_path, - open_breakpoints + breakpoints .get(&project_path)? .iter() .map(|bp| bp.to_serialized(buffer, project_path.path.clone())) @@ -1224,8 +1211,8 @@ impl Project { return result; } - let open_breakpoints = self.dap_store.read(cx).open_breakpoints(); - for project_path in open_breakpoints.keys() { + let breakpoints = self.dap_store.read(cx).breakpoints(); + for project_path in breakpoints.keys() { if let Some((worktree_path, mut serialized_breakpoint)) = self.serialize_breakpoints_for_project_path(&project_path, cx) { @@ -1236,20 +1223,6 @@ impl Project { } } - let closed_breakpoints = self.dap_store.read(cx).closed_breakpoints(); - for (project_path, serialized_bp) in closed_breakpoints.iter() { - let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else { - continue; - }; - - let worktree_path = worktree.read(cx).abs_path(); - - result - .entry(worktree_path) - .or_default() - .extend(serialized_bp.iter().map(|bp| bp.clone())); - } - result } @@ -2075,22 +2048,12 @@ impl Project { cx: &mut ModelContext, ) -> Task, AnyModel)>> { let task = self.open_buffer(path.clone(), cx); - cx.spawn(move |project, mut cx| async move { + cx.spawn(move |_project, cx| async move { let buffer = task.await?; let project_entry_id = buffer.read_with(&cx, |buffer, cx| { File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) })?; - let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx))?; - project.update(&mut cx, |project, cx| { - project.dap_store.update(cx, |store, cx| { - store.sync_closed_breakpoint_to_open_breakpoint( - &path, - &multi_buffer.read(cx).snapshot(cx), - ); - }); - })?; - let buffer: &AnyModel = &buffer; Ok((project_entry_id, buffer.clone())) }) diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 032a27121a011c..3b2631ca8aa988 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -43,11 +43,16 @@ impl HeadlessProject { Task::ready(()), cx.background_executor().clone(), )); + let dap_store = cx.new_model(DapStore::new); let worktree_store = cx.new_model(|_| WorktreeStore::new(true, fs.clone())); let buffer_store = cx.new_model(|cx| { - let mut buffer_store = - BufferStore::new(worktree_store.clone(), Some(SSH_PROJECT_ID), cx); + let mut buffer_store = BufferStore::new( + worktree_store.clone(), + Some(SSH_PROJECT_ID), + dap_store.clone(), + cx, + ); buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); buffer_store }); @@ -56,7 +61,6 @@ impl HeadlessProject { observer.shared(SSH_PROJECT_ID, session.clone().into(), cx); observer }); - let dap_store = cx.new_model(DapStore::new); let environment = project::ProjectEnvironment::new(&worktree_store, None, cx); let lsp_store = cx.new_model(|cx| { LspStore::new( diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index ee833326f58099..e7ca3b26c3f241 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -17,6 +17,14 @@ pub struct Anchor { } impl Anchor { + pub fn offset_for_breakpoint_anchor(offset: usize) -> Self { + Anchor { + timestamp: clock::Lamport::new(0), + offset, + bias: Bias::Left, + buffer_id: None, + } + } pub const MIN: Self = Self { timestamp: clock::Lamport::MIN, offset: usize::MIN, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 5932d8cbefcb27..2c6caa7bad1f76 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -167,6 +167,7 @@ impl Column for Breakpoint { .column_int(start_index) .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? as u32; + Ok((Breakpoint { position }, start_index + 1)) } } @@ -184,6 +185,7 @@ impl Column for Breakpoints { .column_int(index) .with_context(|| format!("Failed to read BreakPoint at index {index}"))? as u32; + breakpoints.push(Breakpoint { position }); index += 1; } @@ -494,7 +496,7 @@ impl WorkspaceDb { .warn_on_err() .flatten()?; - // dbg! Remove this comment if i don't figure this out by the end of the month + // dbg! Remove this comment if i don't figure this out by the end of the month 9/24 // TODO Debugger: // Figure out why the below query didn't work // let breakpoints: Result> = self @@ -517,7 +519,7 @@ impl WorkspaceDb { match breakpoints { Ok(bp) => { if bp.is_empty() { - log::error!("Breakpoints are empty"); + log::error!("Breakpoints are empty after querying database for them"); } let mut map: HashMap, Vec> = Default::default(); @@ -711,7 +713,7 @@ impl WorkspaceDb { workspace.id, relative_path, worktree_path.clone(), - Breakpoint { position: serialized_breakpoint.position }, + Breakpoint { position: serialized_breakpoint.position}, )) { Err(err) => { log::error!("{err}"); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ac065598aa9540..087dc80874d773 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4241,16 +4241,7 @@ impl Workspace { if let Some(serialized_breakpoints) = serialized_workspace.breakpoints.remove(&worktree_path) { - for serialized_bp in serialized_breakpoints { - store - .closed_breakpoints - .entry(ProjectPath { - worktree_id, - path: serialized_bp.path.clone(), - }) - .or_default() - .push(serialized_bp); - } + store.deserialize_breakpoints(worktree_id, serialized_breakpoints); } } }); From 3184ba1df00b9c9d994bf7a2218ab36e131e4a69 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 11 Sep 2024 18:20:19 -0400 Subject: [PATCH 231/650] Add a debug breakpoint button from lucide --- assets/icons/debug-breakpoint.svg | 1 + crates/editor/src/editor.rs | 2 +- crates/ui/src/components/icon.rs | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 assets/icons/debug-breakpoint.svg diff --git a/assets/icons/debug-breakpoint.svg b/assets/icons/debug-breakpoint.svg new file mode 100644 index 00000000000000..f6a7b35658eeff --- /dev/null +++ b/assets/icons/debug-breakpoint.svg @@ -0,0 +1 @@ + diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 57e7d8b25f15ce..d2e60a3b804312 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5372,7 +5372,7 @@ impl Editor { Color::Debugger }; - IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play) + IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::DebugBreakpoint) .icon_size(IconSize::XSmall) .size(ui::ButtonSize::None) .icon_color(color) diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 4a09d73d11d50b..8eb94172814e0e 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -154,6 +154,7 @@ pub enum IconName { Copy, CountdownTimer, Dash, + DebugBreakpoint, DebugPause, DebugContinue, DebugStepOver, @@ -336,6 +337,7 @@ impl IconName { IconName::CountdownTimer => "icons/countdown_timer.svg", IconName::Dash => "icons/dash.svg", IconName::Debug => "icons/debug.svg", + IconName::DebugBreakpoint => "icons/debug-breakpoint.svg", IconName::DebugPause => "icons/debug-pause.svg", IconName::DebugContinue => "icons/debug-continue.svg", IconName::DebugStepOver => "icons/debug-step-over.svg", From 18fb45f52634ab4cfb7f7f301a27b36f4d0af3cf Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 11 Sep 2024 18:41:04 -0400 Subject: [PATCH 232/650] Move debugger breakpoint code from dap::client to project::dap_store --- Cargo.lock | 1 + crates/dap/src/client.rs | 98 --------------------- crates/editor/src/editor.rs | 25 +++--- crates/project/Cargo.toml | 1 + crates/project/src/dap_store.rs | 102 +++++++++++++++++++++- crates/project/src/project.rs | 4 +- crates/workspace/src/persistence.rs | 2 +- crates/workspace/src/persistence/model.rs | 2 +- 8 files changed, 119 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a2b675e7f2600..d50d2544bc56f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8410,6 +8410,7 @@ dependencies = [ "clock", "collections", "dap", + "dap-types", "dev_server_projects", "env_logger", "fs", diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index fc6cf1e464f2d0..9104de0f39a630 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -16,7 +16,6 @@ use dap_types::{ }; use futures::{AsyncBufRead, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; -use language::{Buffer, BufferSnapshot}; use parking_lot::{Mutex, MutexGuard}; use serde_json::Value; use smol::{ @@ -33,7 +32,6 @@ use std::{ }, }; use task::{DebugAdapterConfig, DebugRequestType}; -use text::Point; #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ThreadStatus { @@ -583,99 +581,3 @@ impl DebugAdapterClient { ) } } - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct Breakpoint { - pub active_position: Option, - pub cache_position: u32, -} - -impl Breakpoint { - pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { - let line = self - .active_position - .map(|position| buffer.summary_for_anchor::(&position).row) - .unwrap_or(self.cache_position) as u64 - + 1u64; - - SourceBreakpoint { - line, - condition: None, - hit_condition: None, - log_message: None, - column: None, - mode: None, - } - } - - pub fn set_active_position(&mut self, buffer: &Buffer) { - if self.active_position.is_none() { - self.active_position = - Some(buffer.anchor_at(Point::new(self.cache_position, 0), text::Bias::Left)); - } - } - - pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { - self.active_position - .map(|position| buffer.summary_for_anchor::(&position)) - .unwrap_or(Point::new(self.cache_position, 0)) - } - - pub fn point_for_buffer_snapshot(&self, buffer_snapshot: &BufferSnapshot) -> Point { - self.active_position - .map(|position| buffer_snapshot.summary_for_anchor::(&position)) - .unwrap_or(Point::new(self.cache_position, 0)) - } - - pub fn source_for_snapshot(&self, snapshot: &BufferSnapshot) -> SourceBreakpoint { - let line = self - .active_position - .map(|position| snapshot.summary_for_anchor::(&position).row) - .unwrap_or(self.cache_position) as u64 - + 1u64; - - SourceBreakpoint { - line, - condition: None, - hit_condition: None, - log_message: None, - column: None, - mode: None, - } - } - - pub fn to_serialized(&self, buffer: Option<&Buffer>, path: Arc) -> SerializedBreakpoint { - match buffer { - Some(buffer) => SerializedBreakpoint { - position: self - .active_position - .map(|position| buffer.summary_for_anchor::(&position).row + 1u32) - .unwrap_or(self.cache_position + 1u32), - path, - }, - None => SerializedBreakpoint { - position: self.cache_position + 1u32, - path, - }, - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct SerializedBreakpoint { - pub position: u32, - pub path: Arc, -} - -impl SerializedBreakpoint { - pub fn to_source_breakpoint(&self) -> SourceBreakpoint { - SourceBreakpoint { - line: self.position as u64, - condition: None, - hit_condition: None, - log_message: None, - column: None, - mode: None, - } - } -} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d2e60a3b804312..a822a0abcf1628 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -80,6 +80,7 @@ use gpui::{ VisualContext, WeakFocusHandle, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; +use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; use hover_popover::{hide_hover, HoverState}; use hunk_diff::ExpandedHunks; pub(crate) use hunk_diff::HoveredHunk; @@ -96,12 +97,8 @@ use language::{ }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; -use project::dap_store::DapStore; use task::{ResolvedTask, TaskTemplate, TaskVariables}; -use dap::client::Breakpoint; -use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; - pub use lsp::CompletionContext; use lsp::{ CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat, @@ -118,6 +115,7 @@ use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::project_settings::{GitGutterSetting, ProjectSettings}; use project::{ + dap_store::{Breakpoint, DapStore}, CodeAction, Completion, CompletionIntent, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction, TaskSourceKind, }; @@ -5372,14 +5370,17 @@ impl Editor { Color::Debugger }; - IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::DebugBreakpoint) - .icon_size(IconSize::XSmall) - .size(ui::ButtonSize::None) - .icon_color(color) - .on_click(cx.listener(move |editor, _e, cx| { - editor.focus(cx); - editor.toggle_breakpoint_at_anchor(anchor, cx); - })) + IconButton::new( + ("breakpoint_indicator", row.0 as usize), + ui::IconName::DebugBreakpoint, + ) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) + .icon_color(color) + .on_click(cx.listener(move |editor, _e, cx| { + editor.focus(cx); + editor.toggle_breakpoint_at_anchor(anchor, cx); + })) } fn render_run_indicator( diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index ad67d483d57537..80ce0ccd670656 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -31,6 +31,7 @@ client.workspace = true clock.workspace = true collections.workspace = true dap.workspace = true +dap-types = { git = "https://github.com/zed-industries/dap-types" } dev_server_projects.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index df5bfe969ca212..3bd861cdae26c9 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,22 +1,24 @@ use anyhow::Context as _; use collections::{HashMap, HashSet}; use dap::{ - client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId, SerializedBreakpoint}, + client::{DebugAdapterClient, DebugAdapterClientId}, transport::Payload, }; +use dap_types::SourceBreakpoint; use gpui::{EventEmitter, ModelContext, Subscription, Task}; use language::{Buffer, BufferSnapshot}; use settings::WorktreeId; use std::{ collections::BTreeMap, future::Future, - path::PathBuf, + path::{Path, PathBuf}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, }; use task::DebugAdapterConfig; +use text::Point; use util::ResultExt as _; use crate::ProjectPath; @@ -271,3 +273,99 @@ impl DapStore { .detach() } } + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct Breakpoint { + pub active_position: Option, + pub cache_position: u32, +} + +impl Breakpoint { + pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { + let line = self + .active_position + .map(|position| buffer.summary_for_anchor::(&position).row) + .unwrap_or(self.cache_position) as u64 + + 1u64; + + SourceBreakpoint { + line, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + } + } + + pub fn set_active_position(&mut self, buffer: &Buffer) { + if self.active_position.is_none() { + self.active_position = + Some(buffer.anchor_at(Point::new(self.cache_position, 0), text::Bias::Left)); + } + } + + pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { + self.active_position + .map(|position| buffer.summary_for_anchor::(&position)) + .unwrap_or(Point::new(self.cache_position, 0)) + } + + pub fn point_for_buffer_snapshot(&self, buffer_snapshot: &BufferSnapshot) -> Point { + self.active_position + .map(|position| buffer_snapshot.summary_for_anchor::(&position)) + .unwrap_or(Point::new(self.cache_position, 0)) + } + + pub fn source_for_snapshot(&self, snapshot: &BufferSnapshot) -> SourceBreakpoint { + let line = self + .active_position + .map(|position| snapshot.summary_for_anchor::(&position).row) + .unwrap_or(self.cache_position) as u64 + + 1u64; + + SourceBreakpoint { + line, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + } + } + + pub fn to_serialized(&self, buffer: Option<&Buffer>, path: Arc) -> SerializedBreakpoint { + match buffer { + Some(buffer) => SerializedBreakpoint { + position: self + .active_position + .map(|position| buffer.summary_for_anchor::(&position).row + 1u32) + .unwrap_or(self.cache_position + 1u32), + path, + }, + None => SerializedBreakpoint { + position: self.cache_position + 1u32, + path, + }, + } + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct SerializedBreakpoint { + pub position: u32, + pub path: Arc, +} + +impl SerializedBreakpoint { + pub fn to_source_breakpoint(&self) -> SourceBreakpoint { + SourceBreakpoint { + line: self.position as u64, + condition: None, + hit_condition: None, + log_message: None, + column: None, + mode: None, + } + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 14e971f3129caa..e3e7504a54ad7a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,13 +28,13 @@ use client::{ use clock::ReplicaId; use dap::{ - client::{Breakpoint, DebugAdapterClient, DebugAdapterClientId, SerializedBreakpoint}, + client::{DebugAdapterClient, DebugAdapterClientId}, debugger_settings::DebuggerSettings, transport::Payload, }; use collections::{BTreeSet, HashMap, HashSet}; -use dap_store::{DapStore, DapStoreEvent}; +use dap_store::{Breakpoint, DapStore, DapStoreEvent, SerializedBreakpoint}; use debounced_delay::DebouncedDelay; pub use environment::ProjectEnvironment; use futures::{ diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 2c6caa7bad1f76..c95157eaaba8f1 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -8,9 +8,9 @@ use std::{ use anyhow::{anyhow, bail, Context, Result}; use client::DevServerProjectId; use collections::HashMap; -use dap::client::SerializedBreakpoint; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId}; +use project::dap_store::SerializedBreakpoint; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 52e3f4e0278cd2..cad882f4c05b3b 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -6,12 +6,12 @@ use anyhow::{Context, Result}; use async_recursion::async_recursion; use client::DevServerProjectId; use collections::HashMap; -use dap::client::SerializedBreakpoint; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Model, View, WeakView}; +use project::dap_store::SerializedBreakpoint; use project::Project; use serde::{Deserialize, Serialize}; use std::{ From a4ce44629c6a190e9c9c492498fa99131dd0aca5 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 12 Sep 2024 10:51:42 +0200 Subject: [PATCH 233/650] Remove unused deps --- Cargo.lock | 4 ---- crates/dap/Cargo.toml | 3 --- crates/project/Cargo.toml | 1 - crates/project/src/dap_store.rs | 2 +- 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d50d2544bc56f7..95f16136aba8d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3333,9 +3333,7 @@ dependencies = [ "dap-types", "futures 0.3.30", "gpui", - "language", "log", - "multi_buffer", "parking_lot", "schemars", "serde", @@ -3343,7 +3341,6 @@ dependencies = [ "settings", "smol", "task", - "text", ] [[package]] @@ -8410,7 +8407,6 @@ dependencies = [ "clock", "collections", "dap", - "dap-types", "dev_server_projects", "env_logger", "fs", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 989e0937cb8081..63c65ce4a30c9f 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -14,9 +14,7 @@ async-trait.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types" } futures.workspace = true gpui.workspace = true -language.workspace = true log.workspace = true -multi_buffer.workspace = true parking_lot.workspace = true schemars.workspace = true serde.workspace = true @@ -24,4 +22,3 @@ serde_json.workspace = true settings.workspace = true smol.workspace = true task.workspace = true -text.workspace = true diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 80ce0ccd670656..ad67d483d57537 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -31,7 +31,6 @@ client.workspace = true clock.workspace = true collections.workspace = true dap.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types" } dev_server_projects.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 3bd861cdae26c9..7ee61cdcf04e7e 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,10 +1,10 @@ use anyhow::Context as _; use collections::{HashMap, HashSet}; +use dap::SourceBreakpoint; use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, transport::Payload, }; -use dap_types::SourceBreakpoint; use gpui::{EventEmitter, ModelContext, Subscription, Task}; use language::{Buffer, BufferSnapshot}; use settings::WorktreeId; From ab6f3341c33ad036e14e5468db3831f82d87efc8 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 12 Sep 2024 10:56:24 +0200 Subject: [PATCH 234/650] Remove background for breakpoint icon --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a822a0abcf1628..46d7d86dda66d0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5377,6 +5377,7 @@ impl Editor { .icon_size(IconSize::XSmall) .size(ui::ButtonSize::None) .icon_color(color) + .style(ButtonStyle::Transparent) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); editor.toggle_breakpoint_at_anchor(anchor, cx); From 7a6f9d95595772e8246cf3297e274804fbd4c089 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 12 Sep 2024 11:06:33 +0200 Subject: [PATCH 235/650] Remove default value for DebugItemAction struct --- crates/debugger_ui/src/debugger_panel_item.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 1a7073e67608dd..5c28bc8d42badb 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -49,16 +49,15 @@ impl_actions!(debug_panel_item, [DebugItemAction]); /// This struct is for actions that should be triggered even when /// the debug pane is not in focus. This is done by setting workspace /// as the action listener then having workspace call `handle_workspace_action` -#[derive(Clone, Deserialize, PartialEq, Default)] +#[derive(Clone, Deserialize, PartialEq)] pub struct DebugItemAction { kind: DebugPanelItemActionKind, } /// Actions that can be sent to workspace /// currently all of these are button toggles -#[derive(Deserialize, PartialEq, Default, Clone, Debug)] +#[derive(Deserialize, PartialEq, Clone, Debug)] enum DebugPanelItemActionKind { - #[default] Continue, StepOver, StepIn, From 3fac36bd7799822d12096138b5446258a1b4fb43 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 8 Sep 2024 15:16:06 -0400 Subject: [PATCH 236/650] Add custom debug config option to speed up DAP development --- crates/dap/src/adapters.rs | 72 ++++++++++++++++++++++++++++++-- crates/task/src/debug_format.rs | 36 +++++++++++----- crates/task/src/lib.rs | 4 +- crates/task/src/task_template.rs | 1 + 4 files changed, 98 insertions(+), 15 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 45fddf3b3fb163..763b1ffffbce89 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -19,11 +19,14 @@ use std::{ sync::Arc, time::Duration, }; -use task::{DebugAdapterConfig, DebugAdapterKind, TCPHost}; +use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { - match adapter_config.kind { - DebugAdapterKind::Custom => Err(anyhow!("Custom is not implemented")), + match &adapter_config.kind { + DebugAdapterKind::Custom(start_args) => Ok(Box::new(CustomDebugAdapter::new( + adapter_config, + start_args.clone(), + ))), DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new(adapter_config))), DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new(adapter_config))), DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new(adapter_config))), @@ -162,6 +165,69 @@ pub trait DebugAdapter: Debug + Send + Sync + 'static { fn request_args(&self) -> Value; } +#[derive(Debug, Eq, PartialEq, Clone)] +struct CustomDebugAdapter { + start_command: String, + initialize_args: Option>, + program: String, + connection: DebugConnectionType, +} + +impl CustomDebugAdapter { + const _ADAPTER_NAME: &'static str = "custom_dap"; + + fn new(adapter_config: &DebugAdapterConfig, custom_args: CustomArgs) -> Self { + CustomDebugAdapter { + start_command: custom_args.start_command, + program: adapter_config.program.clone(), + connection: custom_args.connection, + initialize_args: adapter_config.initialize_args.clone(), + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for CustomDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::_ADAPTER_NAME.into()) + } + + async fn connect(&self, cx: &mut AsyncAppContext) -> Result { + match &self.connection { + DebugConnectionType::STDIO => create_stdio_client(&self.start_command, &vec![].into()), + DebugConnectionType::TCP(tcp_host) => { + create_tcp_client(tcp_host.clone(), &self.start_command, &vec![].into(), cx).await + } + } + } + + fn get_debug_adapter_start_command(&self) -> String { + "fail".to_string() + } + + fn is_installed(&self) -> Option { + None + } + + fn download_adapter(&self) -> anyhow::Result { + Err(anyhow::format_err!("Not implemented")) + } + + fn request_args(&self) -> Value { + let base_args = json!({ + "program": format!("{}", &self.program) + }); + + // TODO Debugger: Figure out a way to combine this with base args + // if let Some(args) = &self.initialize_args { + // let args = json!(args.clone()).as_object().into_iter(); + // base_args.as_object_mut().unwrap().extend(args); + // } + + base_args + } +} + #[derive(Debug, Eq, PartialEq, Clone)] struct PythonDebugAdapter { program: String, diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 1d1038d79b2d71..f83b0c3bb13f4c 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -34,12 +34,12 @@ pub enum DebugRequestType { } /// The Debug adapter to use -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "lowercase")] pub enum DebugAdapterKind { /// Manually setup starting a debug adapter - #[default] - Custom, + /// The argument within is used to start the DAP + Custom(CustomArgs), /// Use debugpy Python, /// Use vscode-php-debug @@ -48,6 +48,24 @@ pub enum DebugAdapterKind { Lldb, } +/// Custom arguments used to setup a custom debugger +#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +pub struct CustomArgs { + /// The connection that a custom debugger should use + pub connection: DebugConnectionType, + /// The cli command used to start the debug adapter + pub start_command: String, +} + +impl Default for DebugAdapterKind { + fn default() -> Self { + DebugAdapterKind::Custom(CustomArgs { + connection: DebugConnectionType::STDIO, + start_command: "".into(), + }) + } +} + /// Represents the configuration for the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] @@ -65,13 +83,8 @@ pub struct DebugAdapterConfig { pub program: String, /// The path to the adapter pub adapter_path: Option, -} - -/// Represents the configuration for the debug adapter that is send with the launch request -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(transparent)] -pub struct DebugRequestArgs { - pub args: serde_json::Value, + /// Additional initialization arguments to be sent on DAP initialization + pub initialize_args: Option>, } /// Represents the type of the debugger adapter connection @@ -96,6 +109,8 @@ pub struct DebugTaskDefinition { session_type: DebugRequestType, /// The adapter to run adapter: DebugAdapterKind, + /// Additional initialization arguments to be sent on DAP initialization + initialize_args: Option>, } impl DebugTaskDefinition { @@ -106,6 +121,7 @@ impl DebugTaskDefinition { request: self.session_type, program: self.program, adapter_path: None, + initialize_args: self.initialize_args, }); let args: Vec = Vec::new(); diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index eae6f26acb6dab..21f67b1586a165 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -15,8 +15,8 @@ use std::path::PathBuf; use std::str::FromStr; pub use debug_format::{ - DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, DebugRequestType, DebugTaskFile, - TCPHost, + CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, DebugRequestType, + DebugTaskFile, TCPHost, }; pub use task_template::{ HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, TaskType, diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index fcb8969ab8297c..de589b27e2496e 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -96,6 +96,7 @@ mod deserialization_tests { request: crate::DebugRequestType::Launch, program: "main".to_string(), adapter_path: None, + initialize_args: None, }; let json = json!({ "type": "debug", From 8a835bcf888cc41d9cc5a4ae613ce940bc0b0106 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 12 Sep 2024 08:58:02 -0400 Subject: [PATCH 237/650] Get .zed/debug.json to resolve debug tasks properly Co-authored-by: Piotr --- crates/dap/src/adapters.rs | 4 ---- crates/project/src/project.rs | 18 ++++++++++++++---- crates/task/src/debug_format.rs | 1 + crates/task/src/static_source.rs | 5 ++--- crates/task/src/task_template.rs | 4 +++- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 763b1ffffbce89..f82b0a56e4f0ee 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -201,10 +201,6 @@ impl DebugAdapter for CustomDebugAdapter { } } - fn get_debug_adapter_start_command(&self) -> String { - "fail".to_string() - } - fn is_installed(&self) -> Option { None } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e3e7504a54ad7a..1426713d024237 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -94,8 +94,8 @@ use std::{ }; use task::{ static_source::{StaticSource, TrackedFile}, - DebugAdapterConfig, HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, - TaskVariables, VariableName, + DebugAdapterConfig, DebugTaskFile, HideStrategy, RevealStrategy, Shell, TaskContext, + TaskTemplate, TaskVariables, VariableName, VsCodeTaskFile, }; use terminals::Terminals; use text::{Anchor, BufferId}; @@ -4170,7 +4170,13 @@ impl Project { abs_path, id_base: "local_vscode_tasks_for_worktree".into(), }, - |tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)), + |tx, cx| { + StaticSource::new(TrackedFile::new_convertible::( + tasks_file_rx, + tx, + cx, + )) + }, cx, ); } @@ -4191,7 +4197,11 @@ impl Project { id_base: "local_debug_file_for_worktree".into(), }, |tx, cx| { - StaticSource::new(TrackedFile::new(debug_task_file_rx, tx, cx)) + StaticSource::new(TrackedFile::new_convertible::( + debug_task_file_rx, + tx, + cx, + )) }, cx, ); diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index f83b0c3bb13f4c..d19bd70445b66e 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -138,6 +138,7 @@ impl DebugTaskDefinition { /// A group of Debug Tasks defined in a JSON file. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(transparent)] pub struct DebugTaskFile(pub Vec); impl DebugTaskFile { diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 650479967c7500..b78a020990d139 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -78,6 +78,7 @@ impl TrackedFile { cx.background_executor() .spawn({ let parsed_contents = parsed_contents.clone(); + async move { while let Some(new_contents) = tracker.next().await { if Arc::strong_count(&parsed_contents) == 1 { @@ -109,9 +110,7 @@ impl TrackedFile { } }) .detach_and_log_err(cx); - Self { - parsed_contents: Default::default(), - } + Self { parsed_contents } } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index de589b27e2496e..2cd7f999080bce 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -173,7 +173,9 @@ impl TaskTemplate { /// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources), /// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details. pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option { - if self.label.trim().is_empty() || self.command.trim().is_empty() { + if self.label.trim().is_empty() + || (self.command.trim().is_empty() && matches!(self.task_type, TaskType::Script)) + { return None; } From ed6da4a7f67cac0bc527f939d312a5eb02cdd183 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 12 Sep 2024 10:29:31 -0400 Subject: [PATCH 238/650] Get zed variables to work in debug tasks program --- crates/recent_projects/src/dev_servers.rs | 1 + crates/task/src/debug_format.rs | 4 +++- crates/task/src/lib.rs | 13 ++++++++++++- crates/task/src/task_template.rs | 11 +++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index d8b10f31f9f55e..f0f71f66263f63 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -1662,6 +1662,7 @@ pub async fn spawn_ssh_task( hide: HideStrategy::Never, env: Default::default(), shell: Default::default(), + program: None, }, cx, ) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index d19bd70445b66e..411e6b42cc0925 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -111,6 +111,8 @@ pub struct DebugTaskDefinition { adapter: DebugAdapterKind, /// Additional initialization arguments to be sent on DAP initialization initialize_args: Option>, + /// The path of the debug adapter to use + adapter_path: Option, } impl DebugTaskDefinition { @@ -120,7 +122,7 @@ impl DebugTaskDefinition { kind: self.adapter, request: self.session_type, program: self.program, - adapter_path: None, + adapter_path: self.adapter_path, initialize_args: self.initialize_args, }); diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 21f67b1586a165..fdfa42e86c3058 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -58,6 +58,8 @@ pub struct SpawnInTerminal { pub hide: HideStrategy, /// Which shell to use when spawning the task. pub shell: Shell, + /// Tells debug tasks which program to debug + pub program: Option, } /// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task. @@ -96,7 +98,16 @@ impl ResolvedTask { pub fn debug_adapter_config(&self) -> Option { match self.original_task.task_type.clone() { TaskType::Script => None, - TaskType::Debug(adapter_config) => Some(adapter_config), + TaskType::Debug(mut adapter_config) => { + adapter_config.program = match &self.resolved { + None => adapter_config.program, + Some(spawn_in_terminal) => spawn_in_terminal + .program + .clone() + .unwrap_or(adapter_config.program), + }; + Some(adapter_config) + } } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 2cd7f999080bce..b11219d2dc703e 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -242,6 +242,16 @@ impl TaskTemplate { &mut substituted_variables, )?; + let program = match &self.task_type { + TaskType::Script => None, + TaskType::Debug(adapter_config) => Some(substitute_all_template_variables_in_str( + &adapter_config.program, + &task_variables, + &variable_names, + &mut substituted_variables, + )?), + }; + let task_hash = to_hex_hash(self) .context("hashing task template") .log_err()?; @@ -296,6 +306,7 @@ impl TaskTemplate { reveal: self.reveal, hide: self.hide, shell: self.shell.clone(), + program, }), }) } From 571d99cecf506dff2f06809aa285318352bb65ab Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 12 Sep 2024 11:04:27 -0400 Subject: [PATCH 239/650] Fix bug where breakpoints were unable to be toggled on the first line --- crates/editor/src/editor.rs | 11 ++++++++++- crates/editor/src/element.rs | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 46d7d86dda66d0..5722b8233fa768 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6228,6 +6228,15 @@ impl Editor { pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { let cursor_position: Point = self.selections.newest(cx).head(); + // Bias is set to right when placing a breakpoint on the first row + // to avoid having the breakpoint's anchor be anchor::MIN & having + // it's buffer_id be None + let bias = if cursor_position.row == 0 { + Bias::Right + } else { + Bias::Left + }; + // We Set the column position to zero so this function interacts correctly // between calls by clicking on the gutter & using an action to toggle a // breakpoint. Otherwise, toggling a breakpoint through an action wouldn't @@ -6236,7 +6245,7 @@ impl Editor { .snapshot(cx) .display_snapshot .buffer_snapshot - .anchor_before(Point::new(cursor_position.row, 0)) + .anchor_at(Point::new(cursor_position.row, 0), bias) .text_anchor; self.toggle_breakpoint_at_anchor(breakpoint_position, cx); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2bc5980311c38c..93c3cba2c214ff 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1610,9 +1610,15 @@ impl EditorElement { return None; } + let bias = if point.is_zero() { + Bias::Right + } else { + Bias::Left + }; + let position = snapshot .display_snapshot - .display_point_to_anchor(*point, Bias::Left) + .display_point_to_anchor(*point, bias) .text_anchor; let button = editor.render_breakpoint(position, point.row(), cx); From 4405ae2d19d3ce49c8c3328ffa68e5d2601abde1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 13 Sep 2024 14:04:20 +0200 Subject: [PATCH 240/650] Correctly shutdown adapter (#37) * Kill adapter and close channels * Always try to terminate the debug adapter * Remove TODO * Always allow allow the user to restart if capability allows it Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> * Drop tasks Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> * WIP fix hang * Remove debug code * clean up --------- Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/dap/src/client.rs | 150 ++++++------------ crates/dap/src/transport.rs | 91 +++++------ crates/debugger_ui/src/debugger_panel.rs | 6 +- crates/debugger_ui/src/debugger_panel_item.rs | 4 +- crates/project/src/dap_store.rs | 31 ++-- 5 files changed, 107 insertions(+), 175 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 9104de0f39a630..26b75b490a6ffa 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -19,7 +19,7 @@ use gpui::{AppContext, AsyncAppContext}; use parking_lot::{Mutex, MutexGuard}; use serde_json::Value; use smol::{ - channel::{bounded, unbounded, Receiver, Sender}, + channel::{bounded, Receiver, Sender}, process::Child, }; use std::{ @@ -71,8 +71,8 @@ pub struct ThreadState { pub struct DebugAdapterClient { id: DebugAdapterClientId, adapter: Arc>, - _process: Option, - server_tx: Sender, + transport: Arc, + _process: Arc>>, sequence_count: AtomicU64, config: DebugAdapterConfig, /// thread_id -> thread_state @@ -104,15 +104,6 @@ impl TransportParams { } impl DebugAdapterClient { - /// Creates & returns a new debug adapter client - /// - /// # Parameters - /// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients - /// - `config`: The adapter specific configurations from debugger task that is starting - /// - `command`: The command that starts the debugger - /// - `args`: Arguments of the command that starts the debugger - /// - `cwd`: The absolute path of the project that is being debugged - /// - `cx`: The context that the new client belongs too pub async fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, @@ -125,23 +116,23 @@ impl DebugAdapterClient { let adapter = Arc::new(build_adapter(&config).context("Creating debug adapter")?); let transport_params = adapter.connect(cx).await?; - let server_tx = Self::handle_transport( + let transport = Self::handle_transport( transport_params.rx, transport_params.tx, transport_params.err, event_handler, cx, - )?; + ); Ok(Arc::new(Self { id, config, - server_tx, adapter, + transport, capabilities: Default::default(), thread_states: Default::default(), sequence_count: AtomicU64::new(1), - _process: transport_params.process, + _process: Arc::new(Mutex::new(transport_params.process)), })) } @@ -151,102 +142,43 @@ impl DebugAdapterClient { err: Option>, event_handler: F, cx: &mut AsyncAppContext, - ) -> Result> + ) -> Arc where F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, { - let (server_rx, server_tx) = Transport::start(rx, tx, err, cx); - let (client_tx, client_rx) = unbounded::(); + let transport = Transport::start(rx, tx, err, cx); - cx.update(|cx| { - cx.background_executor() - .spawn(Self::handle_recv(server_rx, client_tx.clone())) - .detach_and_log_err(cx); - - cx.spawn({ - |mut cx| async move { Self::handle_events(client_rx, event_handler, &mut cx).await } - }) - .detach_and_log_err(cx); - - server_tx + let server_rx = transport.server_rx.clone(); + let server_tr = transport.server_tx.clone(); + cx.spawn(|mut cx| async move { + Self::handle_recv(server_rx, server_tr, event_handler, &mut cx).await }) + .detach(); + + transport } - /// Set's up a client's event handler. - /// - /// This function should only be called once or else errors will arise - /// # Parameters - /// `client`: A pointer to the client to pass the event handler too - /// `event_handler`: The function that is called to handle events - /// should be DebugPanel::handle_debug_client_events - /// `cx`: The context that this task will run in - pub async fn handle_events( - client_rx: Receiver, + async fn handle_recv( + server_rx: Receiver, + client_tx: Sender, mut event_handler: F, cx: &mut AsyncAppContext, ) -> Result<()> where F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, { - while let Ok(payload) = client_rx.recv().await { - cx.update(|cx| event_handler(payload, cx))?; - } - - anyhow::Ok(()) - } - - // async fn handle_run_in_terminal_request( - // this: &Arc, - // request: crate::transport::Request, - // cx: &mut AsyncAppContext, - // ) -> Result<()> { - // let arguments: RunInTerminalRequestArguments = - // serde_json::from_value(request.arguments.unwrap_or_default())?; - - // let mut args = arguments.args.clone(); - // let mut command = process::Command::new(args.remove(0)); - - // let envs = arguments.env.as_ref().and_then(|e| e.as_object()).map(|e| { - // e.iter() - // .map(|(key, value)| ((key.clone(), value.clone().to_string()))) - // .collect::>() - // }); - - // if let Some(envs) = envs { - // command.envs(envs); - // } - - // let process = command - // .current_dir(arguments.cwd) - // .args(args) - // .spawn() - // .with_context(|| "failed to spawn run in terminal command.")?; - - // this.server_tx - // .send(Payload::Response(Response { - // request_seq: request.seq, - // success: true, - // command: RunInTerminal::COMMAND.into(), - // message: None, - // body: Some(serde_json::to_value(RunInTerminalResponse { - // process_id: Some(process.id() as u64), - // shell_process_id: None, - // })?), - // })) - // .await?; - - // anyhow::Ok(()) - // } - - async fn handle_recv(server_rx: Receiver, client_tx: Sender) -> Result<()> { while let Ok(payload) = server_rx.recv().await { match payload { - Payload::Event(ev) => client_tx.send(Payload::Event(ev)).await?, + Payload::Event(ev) => cx.update(|cx| event_handler(Payload::Event(ev), cx))?, Payload::Response(_) => unreachable!(), - Payload::Request(req) => client_tx.send(Payload::Request(req)).await?, + Payload::Request(req) => { + cx.update(|cx| event_handler(Payload::Request(req), cx))? + } }; } + drop(client_tx); + anyhow::Ok(()) } @@ -264,7 +196,10 @@ impl DebugAdapterClient { arguments: Some(serialized_arguments), }; - self.server_tx.send(Payload::Request(request)).await?; + self.transport + .server_tx + .send(Payload::Request(request)) + .await?; let response = callback_rx.recv().await??; @@ -527,14 +462,29 @@ impl DebugAdapterClient { } } - pub async fn shutdown(&self, should_terminate: bool) -> Result<()> { - if should_terminate { - let _ = self.terminate().await; - } + pub async fn shutdown(&self) -> Result<()> { + let _ = self.terminate().await; - // TODO debugger: close channels & kill process + self.transport.server_tx.close(); + self.transport.server_rx.close(); - anyhow::Ok(()) + let mut adapter = self._process.lock().take(); + + async move { + let mut pending_requests = self.transport.pending_requests.lock().await; + + pending_requests.clear(); + + if let Some(mut adapter) = adapter.take() { + adapter.kill()?; + } + + drop(pending_requests); + drop(adapter); + + anyhow::Ok(()) + } + .await } pub async fn terminate(&self) -> Result<()> { diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 54c1f849bb95aa..6edade0cb692af 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -88,7 +88,9 @@ where #[derive(Debug)] pub struct Transport { - pending_requests: Mutex>>>, + pub server_tx: Sender, + pub server_rx: Receiver, + pub pending_requests: Arc>>>>, } impl Transport { @@ -97,33 +99,37 @@ impl Transport { server_stdin: Box, server_stderr: Option>, cx: &mut AsyncAppContext, - ) -> (Receiver, Sender) { + ) -> Arc { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); - let transport = Arc::new(Self { - pending_requests: Mutex::new(HashMap::default()), - }); + let pending_requests = Arc::new(Mutex::new(HashMap::default())); - let _ = cx.update(|cx| { - let transport = transport.clone(); + cx.background_executor() + .spawn(Self::receive( + pending_requests.clone(), + server_stdout, + client_tx, + )) + .detach(); - cx.background_executor() - .spawn(Self::receive(transport.clone(), server_stdout, client_tx)) - .detach_and_log_err(cx); - - cx.background_executor() - .spawn(Self::send(transport.clone(), server_stdin, client_rx)) - .detach_and_log_err(cx); - - if let Some(stderr) = server_stderr { - cx.background_executor() - .spawn(Self::err(stderr)) - .detach_and_log_err(cx); - } - }); + if let Some(stderr) = server_stderr { + cx.background_executor().spawn(Self::err(stderr)).detach(); + } - (server_rx, server_tx) + cx.background_executor() + .spawn(Self::send( + pending_requests.clone(), + server_stdin, + client_rx, + )) + .detach(); + + Arc::new(Self { + server_rx, + server_tx, + pending_requests, + }) } async fn recv_server_message( @@ -182,21 +188,19 @@ impl Transport { } async fn send_payload_to_server( - &self, + pending_requests: &Mutex>>>, server_stdin: &mut Box, mut payload: Payload, ) -> Result<()> { if let Payload::Request(request) = &mut payload { if let Some(back) = request.back_ch.take() { - self.pending_requests.lock().await.insert(request.seq, back); + pending_requests.lock().await.insert(request.seq, back); } } - self.send_string_to_server(server_stdin, serde_json::to_string(&payload)?) - .await + Self::send_string_to_server(server_stdin, serde_json::to_string(&payload)?).await } async fn send_string_to_server( - &self, server_stdin: &mut Box, request: String, ) -> Result<()> { @@ -217,26 +221,18 @@ impl Transport { } async fn process_server_message( - &self, + pending_requests: &Arc>>>>, client_tx: &Sender, payload: Payload, ) -> Result<()> { match payload { Payload::Response(res) => { - if let Some(tx) = self.pending_requests.lock().await.remove(&res.request_seq) { - if !tx.is_closed() { - tx.send(Self::process_response(res)).await?; - } else { - log::warn!( - "Response stream associated with request seq: {} is closed", - &res.request_seq - ); // TODO: Fix this case so it never happens - } + if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) { + tx.send(Self::process_response(res)).await?; } else { client_tx.send(Payload::Response(res)).await?; }; } - Payload::Request(_) => { client_tx.send(payload).await?; } @@ -248,31 +244,28 @@ impl Transport { } async fn receive( - transport: Arc, + pending_requests: Arc>>>>, mut server_stdout: Box, client_tx: Sender, ) -> Result<()> { let mut recv_buffer = String::new(); - loop { - transport - .process_server_message( - &client_tx, - Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?, - ) + + while let Ok(msg) = Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await { + Self::process_server_message(&pending_requests, &client_tx, msg) .await .context("Process server message failed in transport::receive")?; } + + anyhow::Ok(()) } async fn send( - transport: Arc, + pending_requests: Arc>>>>, mut server_stdin: Box, client_rx: Receiver, ) -> Result<()> { while let Ok(payload) = client_rx.recv().await { - transport - .send_payload_to_server(&mut server_stdin, payload) - .await?; + Self::send_payload_to_server(&pending_requests, &mut server_stdin, payload).await?; } Ok(()) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index bdfc4effa0279e..d1b3d8f3c1117d 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -636,9 +636,9 @@ impl DebugPanel { cx.update(|cx| { workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.shutdown_client(client.id(), false, cx) - }) + project + .dap_store() + .update(cx, |store, cx| store.shutdown_client(client.id(), cx)) }) }) })? diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 5c28bc8d42badb..356046834353d0 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -639,9 +639,7 @@ impl Render for DebugPanelItem { .client .capabilities() .supports_restart_request - .unwrap_or_default() - || thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, + .unwrap_or_default(), ) .tooltip(move |cx| Tooltip::text("Restart", cx)), ) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 7ee61cdcf04e7e..9a669f5207dd49 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -5,7 +5,7 @@ use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, transport::Payload, }; -use gpui::{EventEmitter, ModelContext, Subscription, Task}; +use gpui::{EventEmitter, ModelContext, Task}; use language::{Buffer, BufferSnapshot}; use settings::WorktreeId; use std::{ @@ -41,18 +41,18 @@ pub struct DapStore { next_client_id: AtomicUsize, clients: HashMap, breakpoints: BTreeMap>, - _subscription: Vec, } impl EventEmitter for DapStore {} impl DapStore { pub fn new(cx: &mut ModelContext) -> Self { + cx.on_app_quit(Self::shutdown_clients).detach(); + Self { next_client_id: Default::default(), clients: Default::default(), breakpoints: Default::default(), - _subscription: vec![cx.on_app_quit(Self::shutdown_clients)], } } @@ -121,12 +121,10 @@ impl DapStore { bp }); - let mut hash_set = HashSet::default(); - for bp in breakpoint_iter { - hash_set.insert(bp); - } - - self.breakpoints.insert(project_path.clone(), hash_set); + self.breakpoints.insert( + project_path.clone(), + breakpoint_iter.collect::>(), + ); } } @@ -179,10 +177,8 @@ impl DapStore { .drain() .map(|(_, client_state)| async { match client_state { - DebugAdapterClientState::Starting(task) => { - task.await?.shutdown(true).await.ok() - } - DebugAdapterClientState::Running(client) => client.shutdown(true).await.ok(), + DebugAdapterClientState::Starting(task) => task.await?.shutdown().await.ok(), + DebugAdapterClientState::Running(client) => client.shutdown().await.ok(), } }) .collect::>(); @@ -195,7 +191,6 @@ impl DapStore { pub fn shutdown_client( &mut self, client_id: DebugAdapterClientId, - should_terminate: bool, cx: &mut ModelContext, ) { let Some(debug_client) = self.clients.remove(&client_id) else { @@ -207,12 +202,8 @@ impl DapStore { cx.background_executor() .spawn(async move { match debug_client { - DebugAdapterClientState::Starting(task) => { - task.await?.shutdown(should_terminate).await.ok() - } - DebugAdapterClientState::Running(client) => { - client.shutdown(should_terminate).await.ok() - } + DebugAdapterClientState::Starting(task) => task.await?.shutdown().await.ok(), + DebugAdapterClientState::Running(client) => client.shutdown().await.ok(), } }) .detach(); From 9b82278bb19f6711cc0050d2995556a66137901b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 13 Sep 2024 14:48:01 +0200 Subject: [PATCH 241/650] Move current stack frame id to debug panel item --- crates/dap/src/client.rs | 7 --- crates/debugger_ui/src/debugger_panel.rs | 28 ++++++------ crates/debugger_ui/src/debugger_panel_item.rs | 19 +++++--- crates/debugger_ui/src/variable_list.rs | 43 +++++++++++-------- crates/project/src/project.rs | 8 ++-- 5 files changed, 52 insertions(+), 53 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 26b75b490a6ffa..14ad2420b84dd9 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -62,7 +62,6 @@ pub struct ThreadState { /// BTreeMap> pub variables: BTreeMap>, pub fetched_variable_ids: HashSet, - pub current_stack_frame_id: u64, // we update this value only once we stopped, // we will use this to indicated if we should show a warning when debugger thread was exited pub stopped: bool, @@ -241,12 +240,6 @@ impl DebugAdapterClient { }; } - pub fn update_current_stack_frame(&self, thread_id: u64, stack_frame_id: u64) { - if let Some(thread_state) = self.thread_states().get_mut(&thread_id) { - thread_state.current_stack_frame_id = stack_frame_id; - }; - } - pub fn thread_states(&self) -> MutexGuard> { self.thread_states.lock() } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index d1b3d8f3c1117d..ce3d26e5878a1c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -87,8 +87,7 @@ impl DebugPanel { } Payload::Request(request) => { if StartDebugging::COMMAND == request.command { - Self::handle_start_debugging_request(this, client, cx) - .log_err(); + Self::handle_start_debugging_request(this, client, cx); } } _ => unreachable!(), @@ -205,12 +204,14 @@ impl DebugPanel { this: &mut Self, client: Arc, cx: &mut ViewContext, - ) -> Result<()> { - this.workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client(client.config(), cx); + ) { + this.workspace + .update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.start_debug_adapter_client(client.config(), cx); + }) }) - }) + .log_err(); } fn handle_debug_client_events( @@ -491,7 +492,6 @@ impl DebugPanel { } this.update(&mut cx, |this, cx| { - thread_state.current_stack_frame_id = current_stack_frame.clone().id; thread_state.stack_frames = stack_trace_response.stack_frames; thread_state.status = ThreadStatus::Stopped; thread_state.stopped = true; @@ -518,6 +518,7 @@ impl DebugPanel { this.workspace.clone(), client.clone(), thread_id, + current_stack_frame.clone().id, cx, ) }); @@ -603,14 +604,11 @@ impl DebugPanel { _: &ExitedEvent, cx: &mut ViewContext, ) { - cx.spawn(|this, mut cx| async move { - for thread_state in client.thread_states().values_mut() { - thread_state.status = ThreadStatus::Exited; - } + for thread_state in client.thread_states().values_mut() { + thread_state.status = ThreadStatus::Exited; + } - this.update(&mut cx, |_, cx| cx.notify()) - }) - .detach_and_log_err(cx); + cx.notify(); } fn handle_terminated_event( diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 356046834353d0..3c033fc7b1558e 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -33,15 +33,16 @@ enum ThreadItem { pub struct DebugPanelItem { thread_id: u64, - variable_list: View, console: View, focus_handle: FocusHandle, stack_frame_list: ListState, output_editor: View, + current_stack_frame_id: u64, active_thread_item: ThreadItem, + workspace: WeakView, client: Arc, + variable_list: View, _subscriptions: Vec, - workspace: WeakView, } impl_actions!(debug_panel_item, [DebugItemAction]); @@ -74,6 +75,7 @@ impl DebugPanelItem { workspace: WeakView, client: Arc, thread_id: u64, + current_stack_frame_id: u64, cx: &mut ViewContext, ) -> Self { let focus_handle = cx.focus_handle(); @@ -137,6 +139,7 @@ impl DebugPanelItem { output_editor, _subscriptions, stack_frame_list, + current_stack_frame_id, active_thread_item: ThreadItem::Variables, } } @@ -252,6 +255,10 @@ impl DebugPanelItem { self.thread_id } + pub fn curren_stack_frame_id(&self) -> u64 { + self.current_stack_frame_id + } + fn stack_frame_for_index(&self, ix: usize) -> StackFrame { self.client .thread_state_by_id(self.thread_id) @@ -270,13 +277,12 @@ impl DebugPanelItem { } fn update_stack_frame_id(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { - self.client - .update_current_stack_frame(self.thread_id, stack_frame_id); + self.current_stack_frame_id = stack_frame_id; let thread_state = self.current_thread_state(); self.variable_list.update(cx, |variable_list, _| { - variable_list.build_entries(thread_state, true, false); + variable_list.build_entries(thread_state, stack_frame_id, true, false); }); } @@ -292,8 +298,7 @@ impl DebugPanelItem { let stack_frame = self.stack_frame_for_index(ix); let source = stack_frame.source.clone(); - let is_selected_frame = - stack_frame.id == self.current_thread_state().current_stack_frame_id; + let is_selected_frame = stack_frame.id == self.current_stack_frame_id; let formatted_path = format!( "{}:{}", diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 8181386520431f..781888f2e6c892 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -95,7 +95,7 @@ impl VariableList { let debug_item = self.debug_panel_item.read(cx); let Some(entries) = self .stack_frame_entries - .get(&debug_item.current_thread_state().current_stack_frame_id) + .get(&debug_item.curren_stack_frame_id()) else { return div().into_any_element(); }; @@ -133,21 +133,21 @@ impl VariableList { } }; - let thread_state = self - .debug_panel_item - .read_with(cx, |panel, _| panel.current_thread_state()); + let (stack_frame_id, thread_state) = self.debug_panel_item.read_with(cx, |panel, _| { + (panel.curren_stack_frame_id(), panel.current_thread_state()) + }); - self.build_entries(thread_state, false, true); + self.build_entries(thread_state, stack_frame_id, false, true); cx.notify(); } pub fn build_entries( &mut self, thread_state: ThreadState, + stack_frame_id: u64, open_first_scope: bool, keep_open_entries: bool, ) { - let stack_frame_id = thread_state.current_stack_frame_id; let Some(scopes) = thread_state.scopes.get(&stack_frame_id) else { return; }; @@ -240,9 +240,9 @@ impl VariableList { ) { let this = cx.view().clone(); - let (stack_frame_id, client) = self.debug_panel_item.read_with(cx, |p, _| { - (p.current_thread_state().current_stack_frame_id, p.client()) - }); + let (stack_frame_id, client) = self + .debug_panel_item + .read_with(cx, |p, _| (p.curren_stack_frame_id(), p.client())); let support_set_variable = client .capabilities() .supports_set_variable @@ -295,7 +295,7 @@ impl VariableList { let thread_state = this .debug_panel_item .read_with(cx, |panel, _| panel.current_thread_state()); - this.build_entries(thread_state, false, true); + this.build_entries(thread_state, stack_frame_id, false, true); cx.notify(); }), @@ -323,11 +323,11 @@ impl VariableList { return; }; - let thread_state = self - .debug_panel_item - .read_with(cx, |panel, _| panel.current_thread_state()); + let (stack_frame_id, thread_state) = self.debug_panel_item.read_with(cx, |panel, _| { + (panel.curren_stack_frame_id(), panel.current_thread_state()) + }); - self.build_entries(thread_state, false, true); + self.build_entries(thread_state, stack_frame_id, false, true); cx.notify(); } @@ -423,9 +423,14 @@ impl VariableList { let updated_variables = try_join_all(tasks).await?; this.update(&mut cx, |this, cx| { - let (thread_id, client) = this - .debug_panel_item - .read_with(cx, |panel, _| (panel.thread_id(), panel.client())); + let (thread_id, stack_frame_id, client) = + this.debug_panel_item.read_with(cx, |panel, _| { + ( + panel.thread_id(), + panel.curren_stack_frame_id(), + panel.client(), + ) + }); let mut thread_states = client.thread_states(); @@ -439,7 +444,7 @@ impl VariableList { .insert(scope.variables_reference, variables.collect::<_>()); } - this.build_entries(thread_state.clone(), false, true); + this.build_entries(thread_state.clone(), stack_frame_id, false, true); cx.notify(); }) }) @@ -518,7 +523,7 @@ impl VariableList { let Some(entries) = this .stack_frame_entries - .get(&debug_item.current_thread_state().current_stack_frame_id) + .get(&debug_item.curren_stack_frame_id()) else { return; }; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1426713d024237..a1f8008cd09b0d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1137,11 +1137,9 @@ impl Project { debug_task: task::ResolvedTask, cx: &mut ModelContext, ) { - let adapter_config = debug_task - .debug_adapter_config() - .expect("Debug tasks need to specify adapter configuration"); - - self.start_debug_adapter_client(adapter_config, cx); + if let Some(adapter_config) = debug_task.debug_adapter_config() { + self.start_debug_adapter_client(adapter_config, cx); + } } pub fn start_debug_adapter_client( From 559173e5505e692e2aaa1e8e762d51a3e13f59bc Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 13 Sep 2024 15:05:16 +0200 Subject: [PATCH 242/650] Fix typo --- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/variable_list.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 3c033fc7b1558e..0acb8ec295bea5 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -255,7 +255,7 @@ impl DebugPanelItem { self.thread_id } - pub fn curren_stack_frame_id(&self) -> u64 { + pub fn current_stack_frame_id(&self) -> u64 { self.current_stack_frame_id } diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 781888f2e6c892..5bc4601b95fe9e 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -95,7 +95,7 @@ impl VariableList { let debug_item = self.debug_panel_item.read(cx); let Some(entries) = self .stack_frame_entries - .get(&debug_item.curren_stack_frame_id()) + .get(&debug_item.current_stack_frame_id()) else { return div().into_any_element(); }; @@ -134,7 +134,7 @@ impl VariableList { }; let (stack_frame_id, thread_state) = self.debug_panel_item.read_with(cx, |panel, _| { - (panel.curren_stack_frame_id(), panel.current_thread_state()) + (panel.current_stack_frame_id(), panel.current_thread_state()) }); self.build_entries(thread_state, stack_frame_id, false, true); @@ -242,7 +242,7 @@ impl VariableList { let (stack_frame_id, client) = self .debug_panel_item - .read_with(cx, |p, _| (p.curren_stack_frame_id(), p.client())); + .read_with(cx, |p, _| (p.current_stack_frame_id(), p.client())); let support_set_variable = client .capabilities() .supports_set_variable @@ -324,7 +324,7 @@ impl VariableList { }; let (stack_frame_id, thread_state) = self.debug_panel_item.read_with(cx, |panel, _| { - (panel.curren_stack_frame_id(), panel.current_thread_state()) + (panel.current_stack_frame_id(), panel.current_thread_state()) }); self.build_entries(thread_state, stack_frame_id, false, true); @@ -427,7 +427,7 @@ impl VariableList { this.debug_panel_item.read_with(cx, |panel, _| { ( panel.thread_id(), - panel.curren_stack_frame_id(), + panel.current_stack_frame_id(), panel.client(), ) }); @@ -523,7 +523,7 @@ impl VariableList { let Some(entries) = this .stack_frame_entries - .get(&debug_item.curren_stack_frame_id()) + .get(&debug_item.current_stack_frame_id()) else { return; }; From 3985963259f777554c100fd3beeb5708169b7dc2 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 13 Sep 2024 16:06:00 +0200 Subject: [PATCH 243/650] Move Request, Response, Event types to dap-types repo This also changes the concept of passing the receiver inside the request to make sure we handle all the requests in the right order. Now we have another peace of state that keeps track of the current requests, and when request comes in we move the receiver to the pending requests state and handle the request as we did before. --- Cargo.lock | 2 +- crates/dap/src/client.rs | 41 ++++--- crates/dap/src/transport.rs | 138 +++++++---------------- crates/debugger_ui/src/debugger_panel.rs | 12 +- crates/project/src/dap_store.rs | 12 +- crates/project/src/project.rs | 8 +- 6 files changed, 81 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95f16136aba8d7..9d0cc6fcd1d25c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types#04f7b33db5f825cd874b5be73919609f4c5d8169" +source = "git+https://github.com/zed-industries/dap-types#d4e23edcf7c8ded91a3bdfd32a216bcab68b710c" dependencies = [ "serde", "serde_json", diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 14ad2420b84dd9..067c24a0df62ef 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,8 +1,9 @@ -use crate::transport::{Payload, Response, Transport}; +use crate::transport::Transport; use anyhow::{anyhow, Context, Result}; use crate::adapters::{build_adapter, DebugAdapter}; use dap_types::{ + messages::{Message, Response}, requests::{ Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, Restart, SetBreakpoints, StepBack, StepIn, StepOut, Terminate, TerminateThreads, Variables, @@ -110,7 +111,7 @@ impl DebugAdapterClient { cx: &mut AsyncAppContext, ) -> Result> where - F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { let adapter = Arc::new(build_adapter(&config).context("Creating debug adapter")?); let transport_params = adapter.connect(cx).await?; @@ -143,7 +144,7 @@ impl DebugAdapterClient { cx: &mut AsyncAppContext, ) -> Arc where - F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { let transport = Transport::start(rx, tx, err, cx); @@ -158,20 +159,20 @@ impl DebugAdapterClient { } async fn handle_recv( - server_rx: Receiver, - client_tx: Sender, + server_rx: Receiver, + client_tx: Sender, mut event_handler: F, cx: &mut AsyncAppContext, ) -> Result<()> where - F: FnMut(Payload, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { while let Ok(payload) = server_rx.recv().await { match payload { - Payload::Event(ev) => cx.update(|cx| event_handler(Payload::Event(ev), cx))?, - Payload::Response(_) => unreachable!(), - Payload::Request(req) => { - cx.update(|cx| event_handler(Payload::Request(req), cx))? + Message::Event(ev) => cx.update(|cx| event_handler(Message::Event(ev), cx))?, + Message::Response(_) => unreachable!(), + Message::Request(req) => { + cx.update(|cx| event_handler(Message::Request(req), cx))? } }; } @@ -188,16 +189,25 @@ impl DebugAdapterClient { let (callback_tx, callback_rx) = bounded::>(1); - let request = crate::transport::Request { - back_ch: Some(callback_tx), - seq: self.next_sequence_id(), + let sequence_id = self.next_sequence_id(); + + let request = crate::messages::Request { + seq: sequence_id, command: R::COMMAND.to_string(), arguments: Some(serialized_arguments), }; + { + self.transport + .current_requests + .lock() + .await + .insert(sequence_id, callback_tx); + } + self.transport .server_tx - .send(Payload::Request(request)) + .send(Message::Request(request)) .await?; let response = callback_rx.recv().await??; @@ -464,14 +474,17 @@ impl DebugAdapterClient { let mut adapter = self._process.lock().take(); async move { + let mut current_requests = self.transport.current_requests.lock().await; let mut pending_requests = self.transport.pending_requests.lock().await; + current_requests.clear(); pending_requests.clear(); if let Some(mut adapter) = adapter.take() { adapter.kill()?; } + drop(current_requests); drop(pending_requests); drop(adapter); diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 6edade0cb692af..28e788a5ea0216 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -1,14 +1,7 @@ use anyhow::{anyhow, Context, Result}; -use dap_types::{ - BreakpointEvent, Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, - InvalidatedEvent, LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent, - ProgressEndEvent, ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent, - ThreadEvent, -}; +use dap_types::messages::{Message, Response}; use futures::{AsyncBufRead, AsyncWrite}; use gpui::AsyncAppContext; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; use smol::{ channel::{unbounded, Receiver, Sender}, io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt}, @@ -16,80 +9,11 @@ use smol::{ }; use std::{collections::HashMap, sync::Arc}; -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum Payload { - Event(Box), - Response(Response), - Request(Request), -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] -#[serde(tag = "event", content = "body")] -#[serde(rename_all = "camelCase")] -pub enum Events { - Initialized(Option), - Stopped(StoppedEvent), - Continued(ContinuedEvent), - Exited(ExitedEvent), - Terminated(Option), - Thread(ThreadEvent), - Output(OutputEvent), - Breakpoint(BreakpointEvent), - Module(ModuleEvent), - LoadedSource(LoadedSourceEvent), - Process(ProcessEvent), - Capabilities(CapabilitiesEvent), - ProgressStart(ProgressStartEvent), - ProgressUpdate(ProgressUpdateEvent), - ProgressEnd(ProgressEndEvent), - Invalidated(InvalidatedEvent), - Memory(MemoryEvent), - #[serde(untagged)] - Other(HashMap), -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Request { - #[serde(skip)] - pub back_ch: Option>>, - pub seq: u64, - pub command: String, - pub arguments: Option, -} - -impl PartialEq for Request { - fn eq(&self, other: &Self) -> bool { - self.seq == other.seq && self.command == other.command && self.arguments == other.arguments - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -pub struct Response { - pub request_seq: u64, - pub success: bool, - pub command: String, - pub message: Option, - #[serde(default, deserialize_with = "deserialize_empty_object")] - pub body: Option, -} - -fn deserialize_empty_object<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value = Value::deserialize(deserializer)?; - if value == Value::Object(serde_json::Map::new()) { - Ok(None) - } else { - Ok(Some(value)) - } -} - #[derive(Debug)] pub struct Transport { - pub server_tx: Sender, - pub server_rx: Receiver, + pub server_tx: Sender, + pub server_rx: Receiver, + pub current_requests: Arc>>>>, pub pending_requests: Arc>>>>, } @@ -100,9 +24,10 @@ impl Transport { server_stderr: Option>, cx: &mut AsyncAppContext, ) -> Arc { - let (client_tx, server_rx) = unbounded::(); - let (server_tx, client_rx) = unbounded::(); + let (client_tx, server_rx) = unbounded::(); + let (server_tx, client_rx) = unbounded::(); + let current_requests = Arc::new(Mutex::new(HashMap::default())); let pending_requests = Arc::new(Mutex::new(HashMap::default())); cx.background_executor() @@ -119,6 +44,7 @@ impl Transport { cx.background_executor() .spawn(Self::send( + current_requests.clone(), pending_requests.clone(), server_stdin, client_rx, @@ -128,6 +54,7 @@ impl Transport { Arc::new(Self { server_rx, server_tx, + current_requests, pending_requests, }) } @@ -135,7 +62,7 @@ impl Transport { async fn recv_server_message( reader: &mut Box, buffer: &mut String, - ) -> Result { + ) -> Result { let mut content_length = None; loop { buffer.truncate(0); @@ -172,7 +99,7 @@ impl Transport { .with_context(|| "reading after a loop")?; let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; - Ok(serde_json::from_str::(msg)?) + Ok(serde_json::from_str::(msg)?) } async fn recv_server_error( @@ -188,13 +115,16 @@ impl Transport { } async fn send_payload_to_server( + current_requests: &Mutex>>>, pending_requests: &Mutex>>>, server_stdin: &mut Box, - mut payload: Payload, + mut payload: Message, ) -> Result<()> { - if let Payload::Request(request) = &mut payload { - if let Some(back) = request.back_ch.take() { - pending_requests.lock().await.insert(request.seq, back); + if let Message::Request(request) = &mut payload { + { + if let Some(sender) = current_requests.lock().await.remove(&request.seq) { + pending_requests.lock().await.insert(request.seq, sender); + } } } Self::send_string_to_server(server_stdin, serde_json::to_string(&payload)?).await @@ -216,28 +146,29 @@ impl Transport { if response.success { Ok(response) } else { + dbg!(response); Err(anyhow!("Received failed response")) } } async fn process_server_message( pending_requests: &Arc>>>>, - client_tx: &Sender, - payload: Payload, + client_tx: &Sender, + message: Message, ) -> Result<()> { - match payload { - Payload::Response(res) => { + match message { + Message::Response(res) => { if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) { tx.send(Self::process_response(res)).await?; } else { - client_tx.send(Payload::Response(res)).await?; + client_tx.send(Message::Response(res)).await?; }; } - Payload::Request(_) => { - client_tx.send(payload).await?; + Message::Request(_) => { + client_tx.send(message).await?; } - Payload::Event(_) => { - client_tx.send(payload).await?; + Message::Event(_) => { + client_tx.send(message).await?; } } Ok(()) @@ -246,7 +177,7 @@ impl Transport { async fn receive( pending_requests: Arc>>>>, mut server_stdout: Box, - client_tx: Sender, + client_tx: Sender, ) -> Result<()> { let mut recv_buffer = String::new(); @@ -260,12 +191,19 @@ impl Transport { } async fn send( + current_requests: Arc>>>>, pending_requests: Arc>>>>, mut server_stdin: Box, - client_rx: Receiver, + client_rx: Receiver, ) -> Result<()> { while let Ok(payload) = client_rx.recv().await { - Self::send_payload_to_server(&pending_requests, &mut server_stdin, payload).await?; + Self::send_payload_to_server( + ¤t_requests, + &pending_requests, + &mut server_stdin, + payload, + ) + .await?; } Ok(()) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index ce3d26e5878a1c..64dc2a48ddb32a 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,10 +1,10 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; +use dap::client::DebugAdapterClient; use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus, VariableContainer}; use dap::debugger_settings::DebuggerSettings; +use dap::messages::{Events, Message}; use dap::requests::{Request, Scopes, StackTrace, StartDebugging}; -use dap::transport::Payload; -use dap::{client::DebugAdapterClient, transport::Events}; use dap::{ Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, ScopesArguments, StackFrame, StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, @@ -76,16 +76,16 @@ impl DebugPanel { cx.subscribe(&pane, Self::handle_pane_event), cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { - project::Event::DebugClientEvent { payload, client_id } => { + project::Event::DebugClientEvent { message, client_id } => { let Some(client) = this.debug_client_by_id(*client_id, cx) else { return cx.emit(DebugPanelEvent::ClientStopped(*client_id)); }; - match payload { - Payload::Event(event) => { + match message { + Message::Event(event) => { this.handle_debug_client_events(client, event, cx); } - Payload::Request(request) => { + Message::Request(request) => { if StartDebugging::COMMAND == request.command { Self::handle_start_debugging_request(this, client, cx); } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 9a669f5207dd49..0ff71d36c8706d 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,10 +1,8 @@ use anyhow::Context as _; use collections::{HashMap, HashSet}; +use dap::client::{DebugAdapterClient, DebugAdapterClientId}; +use dap::messages::Message; use dap::SourceBreakpoint; -use dap::{ - client::{DebugAdapterClient, DebugAdapterClientId}, - transport::Payload, -}; use gpui::{EventEmitter, ModelContext, Task}; use language::{Buffer, BufferSnapshot}; use settings::WorktreeId; @@ -28,7 +26,7 @@ pub enum DapStoreEvent { DebugClientStopped(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, - payload: Payload, + message: Message, }, } @@ -136,10 +134,10 @@ impl DapStore { let client = DebugAdapterClient::new( client_id, config, - move |payload, cx| { + move |message, cx| { dap_store .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, payload }) + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) }) .log_err(); }, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a1f8008cd09b0d..af01bc77658540 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -30,7 +30,7 @@ use clock::ReplicaId; use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, debugger_settings::DebuggerSettings, - transport::Payload, + messages::Message, }; use collections::{BTreeSet, HashMap, HashSet}; @@ -256,7 +256,7 @@ pub enum Event { DebugClientStopped(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, - payload: Payload, + message: Message, }, ActiveEntryChanged(Option), ActivateProjectPanel, @@ -2319,10 +2319,10 @@ impl Project { DapStoreEvent::DebugClientStopped(client_id) => { cx.emit(Event::DebugClientStopped(*client_id)); } - DapStoreEvent::DebugClientEvent { client_id, payload } => { + DapStoreEvent::DebugClientEvent { client_id, message } => { cx.emit(Event::DebugClientEvent { client_id: *client_id, - payload: payload.clone(), + message: message.clone(), }); } } From 4694de8e8a8f7f14424d720f4740869bd34d6f29 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 14 Sep 2024 20:28:26 +0200 Subject: [PATCH 244/650] Return adapter error message when error response comes back --- crates/dap/src/transport.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 28e788a5ea0216..5dbde29321ee7e 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -1,5 +1,8 @@ use anyhow::{anyhow, Context, Result}; -use dap_types::messages::{Message, Response}; +use dap_types::{ + messages::{Message, Response}, + ErrorResponse, +}; use futures::{AsyncBufRead, AsyncWrite}; use gpui::AsyncAppContext; use smol::{ @@ -146,8 +149,15 @@ impl Transport { if response.success { Ok(response) } else { - dbg!(response); - Err(anyhow!("Received failed response")) + if let Some(body) = response.body { + if let Ok(error) = serde_json::from_value::(body) { + if let Some(message) = error.error { + return Err(anyhow!(message.format)); + }; + }; + } + + Err(anyhow!("Received error response from adapter")) } } @@ -187,7 +197,7 @@ impl Transport { .context("Process server message failed in transport::receive")?; } - anyhow::Ok(()) + Ok(()) } async fn send( From 56943e2c78ef0be008ad834b73a874cdcc2bf9b1 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 14 Sep 2024 14:53:59 -0400 Subject: [PATCH 245/650] Fix bug where breakpoints from past sessions on line 1 were unable to be toggle --- crates/project/src/dap_store.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 0ff71d36c8706d..89a43b08e756e4 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -289,8 +289,13 @@ impl Breakpoint { pub fn set_active_position(&mut self, buffer: &Buffer) { if self.active_position.is_none() { - self.active_position = - Some(buffer.anchor_at(Point::new(self.cache_position, 0), text::Bias::Left)); + let bias = if self.cache_position == 0 { + text::Bias::Right + } else { + text::Bias::Left + }; + + self.active_position = Some(buffer.anchor_at(Point::new(self.cache_position, 0), bias)); } } From 716a81756f1eb6669f2ba050b50ac469e9a24b8a Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 14 Sep 2024 03:24:17 -0400 Subject: [PATCH 246/650] Add on_right_click function to IconButton struct --- crates/editor/src/editor.rs | 1 + .../ui/src/components/button/button_like.rs | 40 ++++++++++++++++++- .../ui/src/components/button/icon_button.rs | 8 ++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5722b8233fa768..8a22a72362eaeb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5382,6 +5382,7 @@ impl Editor { editor.focus(cx); editor.toggle_breakpoint_at_anchor(anchor, cx); })) + .on_right_click(cx.listener(move |_editor, _e, _cx| {})) } fn render_run_indicator( diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 625875e4c9eb92..829fa2fe313904 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -1,4 +1,4 @@ -use gpui::{relative, CursorStyle, DefiniteLength, MouseButton}; +use gpui::{relative, CursorStyle, DefiniteLength, MouseButton, MouseDownEvent, MouseUpEvent}; use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems}; use smallvec::SmallVec; @@ -354,6 +354,7 @@ pub struct ButtonLike { tooltip: Option AnyView>>, cursor_style: CursorStyle, on_click: Option>, + on_right_click: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -374,6 +375,7 @@ impl ButtonLike { children: SmallVec::new(), cursor_style: CursorStyle::PointingHand, on_click: None, + on_right_click: None, layer: None, } } @@ -395,6 +397,14 @@ impl ButtonLike { self.rounding = rounding.into(); self } + + pub fn on_right_click( + mut self, + handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + ) -> Self { + self.on_right_click = Some(Box::new(handler)); + self + } } impl Disableable for ButtonLike { @@ -513,6 +523,34 @@ impl RenderOnce for ButtonLike { .hover(|hover| hover.bg(style.hovered(self.layer, cx).background)) .active(|active| active.bg(style.active(cx).background)) }) + .when_some( + self.on_right_click.filter(|_| !self.disabled), + |this, on_right_click| { + this.on_mouse_down(MouseButton::Right, |_event, cx| { + cx.prevent_default(); + cx.stop_propagation(); + }) + .on_mouse_up(MouseButton::Right, move |event, cx| { + cx.stop_propagation(); + let click_event = ClickEvent { + down: MouseDownEvent { + button: MouseButton::Right, + position: event.position, + modifiers: event.modifiers, + click_count: 1, + first_mouse: false, + }, + up: MouseUpEvent { + button: MouseButton::Right, + position: event.position, + modifiers: event.modifiers, + click_count: 1, + }, + }; + (on_right_click)(&click_event, cx) + }) + }, + ) .when_some( self.on_click.filter(|_| !self.disabled), |this, on_click| { diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index cf564efabb3817..5380254821db76 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -56,6 +56,14 @@ impl IconButton { self.selected_icon = icon.into(); self } + + pub fn on_right_click( + mut self, + handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static, + ) -> Self { + self.base = self.base.on_right_click(handler); + self + } } impl Disableable for IconButton { From 8cdb1fb55a06110cc5c1a55ae64b6f95259df85a Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 14 Sep 2024 14:32:25 -0400 Subject: [PATCH 247/650] Get toggle breakpoint working from breakpoint context menu --- crates/editor/src/editor.rs | 42 ++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8a22a72362eaeb..a111a8a7c86f07 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -71,13 +71,13 @@ use git::blame::GitBlame; use git::diff_hunk_to_display; use gpui::{ div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement, - AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry, - ClipboardItem, Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle, - FocusOutEvent, FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, - KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, - SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, - UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, - VisualContext, WeakFocusHandle, WeakView, WindowContext, + AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClickEvent, + ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, EntityId, EventEmitter, + FocusHandle, FocusOutEvent, FocusableView, FontId, FontWeight, HighlightStyle, Hsla, + InteractiveText, KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, + Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, + TextStyle, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, + ViewInputHandler, VisualContext, WeakFocusHandle, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; @@ -5382,7 +5382,33 @@ impl Editor { editor.focus(cx); editor.toggle_breakpoint_at_anchor(anchor, cx); })) - .on_right_click(cx.listener(move |_editor, _e, _cx| {})) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { + let source = editor + .buffer + .read(cx) + .snapshot(cx) + .anchor_at(Point::new(row.0, 0u32), Bias::Left); + + let clicked_point = event.down.position; + let focus_handle = editor.focus_handle.clone(); + let editor_weak = cx.view().downgrade(); + + let context_menu = ui::ContextMenu::build(cx, move |menu, _cx| { + let anchor = anchor.clone(); + menu.on_blur_subscription(Subscription::new(|| {})) + .context(focus_handle) + .entry("Toggle Breakpoint", None, move |cx| { + if let Some(editor) = editor_weak.upgrade() { + editor.update(cx, |this, cx| { + this.toggle_breakpoint_at_anchor(anchor, cx); + }) + } + }) + }); + + editor.mouse_context_menu = + MouseContextMenu::pinned_to_editor(editor, source, clicked_point, context_menu, cx) + })) } fn render_run_indicator( From 621d1812d1315cfe405d38f5f736e3fbf8b2b19f Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 16 Sep 2024 01:24:31 -0400 Subject: [PATCH 248/650] Set up basic infrastructure for log breakpoints --- assets/icons/debug-log-breakpoint.svg | 1 + crates/editor/src/editor.rs | 120 ++++++++++++++++---------- crates/editor/src/element.rs | 22 +++-- crates/project/src/dap_store.rs | 16 +++- crates/ui/src/components/icon.rs | 2 + 5 files changed, 106 insertions(+), 55 deletions(-) create mode 100644 assets/icons/debug-log-breakpoint.svg diff --git a/assets/icons/debug-log-breakpoint.svg b/assets/icons/debug-log-breakpoint.svg new file mode 100644 index 00000000000000..a878ce3e04189d --- /dev/null +++ b/assets/icons/debug-log-breakpoint.svg @@ -0,0 +1 @@ + diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a111a8a7c86f07..28e23d5e217d8c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -115,7 +115,7 @@ use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::project_settings::{GitGutterSetting, ProjectSettings}; use project::{ - dap_store::{Breakpoint, DapStore}, + dap_store::{Breakpoint, BreakpointKind, DapStore}, CodeAction, Completion, CompletionIntent, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction, TaskSourceKind, }; @@ -5270,8 +5270,11 @@ impl Editor { /// This function is used to handle overlaps between breakpoints and Code action/runner symbol. /// It's also used to set the color of line numbers with breakpoints to the breakpoint color. /// TODO debugger: Use this function to color toggle symbols that house nested breakpoints - fn active_breakpoint_points(&mut self, cx: &mut ViewContext) -> HashSet { - let mut breakpoint_display_points = HashSet::default(); + fn active_breakpoint_points( + &mut self, + cx: &mut ViewContext, + ) -> HashMap { + let mut breakpoint_display_points = HashMap::default(); let Some(dap_store) = self.dap_store.clone() else { return breakpoint_display_points; @@ -5289,7 +5292,8 @@ impl Editor { for breakpoint in breakpoints { let point = breakpoint.point_for_buffer(&buffer); - breakpoint_display_points.insert(point.to_display_point(&snapshot)); + breakpoint_display_points + .insert(point.to_display_point(&snapshot), breakpoint.kind.clone()); } }; }; @@ -5345,7 +5349,7 @@ impl Editor { let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0); - breakpoint_display_points.insert(position); + breakpoint_display_points.insert(position, breakpoint.kind.clone()); } } }; @@ -5357,8 +5361,9 @@ impl Editor { fn render_breakpoint( &self, - anchor: text::Anchor, + position: text::Anchor, row: DisplayRow, + kind: &BreakpointKind, cx: &mut ViewContext, ) -> IconButton { let color = if self @@ -5370,45 +5375,68 @@ impl Editor { Color::Debugger }; - IconButton::new( - ("breakpoint_indicator", row.0 as usize), - ui::IconName::DebugBreakpoint, - ) - .icon_size(IconSize::XSmall) - .size(ui::ButtonSize::None) - .icon_color(color) - .style(ButtonStyle::Transparent) - .on_click(cx.listener(move |editor, _e, cx| { - editor.focus(cx); - editor.toggle_breakpoint_at_anchor(anchor, cx); - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { - let source = editor - .buffer - .read(cx) - .snapshot(cx) - .anchor_at(Point::new(row.0, 0u32), Bias::Left); - - let clicked_point = event.down.position; - let focus_handle = editor.focus_handle.clone(); - let editor_weak = cx.view().downgrade(); - - let context_menu = ui::ContextMenu::build(cx, move |menu, _cx| { - let anchor = anchor.clone(); - menu.on_blur_subscription(Subscription::new(|| {})) - .context(focus_handle) - .entry("Toggle Breakpoint", None, move |cx| { - if let Some(editor) = editor_weak.upgrade() { - editor.update(cx, |this, cx| { - this.toggle_breakpoint_at_anchor(anchor, cx); - }) - } - }) - }); + let icon = match kind { + BreakpointKind::Standard => ui::IconName::DebugBreakpoint, + BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint, + }; - editor.mouse_context_menu = - MouseContextMenu::pinned_to_editor(editor, source, clicked_point, context_menu, cx) - })) + IconButton::new(("breakpoint_indicator", row.0 as usize), icon) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) + .icon_color(color) + .style(ButtonStyle::Transparent) + .on_click(cx.listener(move |editor, _e, cx| { + editor.focus(cx); + editor.toggle_breakpoint_at_anchor(position, BreakpointKind::Standard, cx); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { + let source = editor + .buffer + .read(cx) + .snapshot(cx) + .anchor_at(Point::new(row.0, 0u32), Bias::Left); + + let clicked_point = event.down.position; + let focus_handle = editor.focus_handle.clone(); + let editor_weak = cx.view().downgrade(); + let second_weak = editor_weak.clone(); + + let context_menu = ui::ContextMenu::build(cx, move |menu, _cx| { + let anchor = position.clone(); + menu.on_blur_subscription(Subscription::new(|| {})) + .context(focus_handle) + .entry("Toggle Breakpoint", None, move |cx| { + if let Some(editor) = editor_weak.upgrade() { + editor.update(cx, |this, cx| { + this.toggle_breakpoint_at_anchor( + anchor, + BreakpointKind::Standard, + cx, + ); + }) + } + }) + .entry("Toggle Log Breakpoint", None, move |cx| { + if let Some(editor) = second_weak.clone().upgrade() { + editor.update(cx, |this, cx| { + this.toggle_breakpoint_at_anchor( + anchor, + BreakpointKind::Log("Log breakpoint".to_string()), + cx, + ); + }) + } + }) + }); + + editor.mouse_context_menu = MouseContextMenu::pinned_to_editor( + editor, + source, + clicked_point, + context_menu, + cx, + ) + })) } fn render_run_indicator( @@ -6275,12 +6303,13 @@ impl Editor { .anchor_at(Point::new(cursor_position.row, 0), bias) .text_anchor; - self.toggle_breakpoint_at_anchor(breakpoint_position, cx); + self.toggle_breakpoint_at_anchor(breakpoint_position, BreakpointKind::Standard, cx); } pub fn toggle_breakpoint_at_anchor( &mut self, breakpoint_position: text::Anchor, + kind: BreakpointKind, cx: &mut ViewContext, ) { let Some(project) = &self.project else { @@ -6312,6 +6341,7 @@ impl Editor { Breakpoint { cache_position, active_position: Some(breakpoint_position), + kind, }, cx, ); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 93c3cba2c214ff..cdc87f7a7d9889 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -51,6 +51,7 @@ use language::{ use lsp::DiagnosticSeverity; use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow}; use project::{ + dap_store::BreakpointKind, project_settings::{GitGutterSetting, ProjectSettings}, ProjectPath, }; @@ -1589,7 +1590,7 @@ impl EditorElement { gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, snapshot: &EditorSnapshot, - breakpoints: HashSet, + breakpoints: HashMap, cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { @@ -1599,7 +1600,7 @@ impl EditorElement { breakpoints .iter() - .filter_map(|point| { + .filter_map(|(point, kind)| { let row = MultiBufferRow { 0: point.row().0 }; if range.start > point.row() || range.end < point.row() { @@ -1621,7 +1622,7 @@ impl EditorElement { .display_point_to_anchor(*point, bias) .text_anchor; - let button = editor.render_breakpoint(position, point.row(), cx); + let button = editor.render_breakpoint(position, point.row(), &kind, cx); let button = prepaint_gutter_button( button, @@ -1649,7 +1650,7 @@ impl EditorElement { gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, snapshot: &EditorSnapshot, - breakpoints: &mut HashSet, + breakpoints: &mut HashMap, cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { @@ -1689,7 +1690,7 @@ impl EditorElement { &self.style, Some(display_row) == active_task_indicator_row, display_row, - breakpoints.remove(&display_point), + breakpoints.remove(&display_point).is_some(), cx, ); @@ -1718,7 +1719,7 @@ impl EditorElement { gutter_dimensions: &GutterDimensions, gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, - breakpoint_points: &mut HashSet, + breakpoint_points: &mut HashMap, cx: &mut WindowContext, ) -> Option { let mut active = false; @@ -1736,7 +1737,10 @@ impl EditorElement { }); let button = button?; - let button = if breakpoint_points.remove(&DisplayPoint::new(row, 0)) { + let button = if breakpoint_points + .remove(&DisplayPoint::new(row, 0)) + .is_some() + { button.icon_color(Color::Debugger) } else { button @@ -5230,7 +5234,7 @@ impl Element for EditorElement { self.editor.read(cx).gutter_breakpoint_indicator; let breakpoint_rows = breakpoint_lines - .iter() + .keys() .map(|display_point| display_point.row()) .collect(); @@ -5240,7 +5244,7 @@ impl Element for EditorElement { // Otherwise, when a cursor is on a line number it will always be white even // if that line has a breakpoint if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator { - breakpoint_lines.insert(gutter_breakpoint_point); + breakpoint_lines.insert(gutter_breakpoint_point, BreakpointKind::Standard); } let line_numbers = self.layout_line_numbers( diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 89a43b08e756e4..b020cb8887e1e8 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -103,6 +103,7 @@ impl DapStore { .insert(Breakpoint { active_position: None, cache_position: serialize_breakpoint.position.saturating_sub(1u32), + kind: BreakpointKind::Standard, }); } } @@ -262,11 +263,19 @@ impl DapStore { .detach() } } +type LogMessage = String; + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum BreakpointKind { + Standard, + Log(LogMessage), +} #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct Breakpoint { pub active_position: Option, pub cache_position: u32, + pub kind: BreakpointKind, } impl Breakpoint { @@ -318,11 +327,16 @@ impl Breakpoint { .unwrap_or(self.cache_position) as u64 + 1u64; + let log_message = match &self.kind { + BreakpointKind::Standard => None, + BreakpointKind::Log(log_message) => Some(log_message.clone()), + }; + SourceBreakpoint { line, condition: None, hit_condition: None, - log_message: None, + log_message, column: None, mode: None, } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 8eb94172814e0e..412dfb940e1dbd 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -164,6 +164,7 @@ pub enum IconName { Debug, DebugStop, DebugDisconnect, + DebugLogBreakpoint, DatabaseZap, Delete, Disconnected, @@ -346,6 +347,7 @@ impl IconName { IconName::DebugRestart => "icons/debug-restart.svg", IconName::DebugStop => "icons/debug-stop.svg", IconName::DebugDisconnect => "icons/debug-disconnect.svg", + IconName::DebugLogBreakpoint => "icons/debug-log-breakpoint.svg", IconName::DatabaseZap => "icons/database_zap.svg", IconName::Delete => "icons/delete.svg", IconName::Disconnected => "icons/disconnected.svg", From e7f7fb759d6947a2f6cf2599d6842947d21ae832 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 16 Sep 2024 01:51:15 -0400 Subject: [PATCH 249/650] Send log breakpoint's to debug adapter's when able --- crates/project/src/dap_store.rs | 10 +++++++++- crates/workspace/src/persistence.rs | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index b020cb8887e1e8..a3a72f61768344 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -350,10 +350,12 @@ impl Breakpoint { .map(|position| buffer.summary_for_anchor::(&position).row + 1u32) .unwrap_or(self.cache_position + 1u32), path, + kind: self.kind.clone(), }, None => SerializedBreakpoint { position: self.cache_position + 1u32, path, + kind: self.kind.clone(), }, } } @@ -363,15 +365,21 @@ impl Breakpoint { pub struct SerializedBreakpoint { pub position: u32, pub path: Arc, + pub kind: BreakpointKind, } impl SerializedBreakpoint { pub fn to_source_breakpoint(&self) -> SourceBreakpoint { + let log_message = match &self.kind { + BreakpointKind::Standard => None, + BreakpointKind::Log(message) => Some(message.clone()), + }; + SourceBreakpoint { line: self.position as u64, condition: None, hit_condition: None, - log_message: None, + log_message, column: None, mode: None, } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c95157eaaba8f1..ad3c9021471a77 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -530,6 +530,7 @@ impl WorkspaceDb { .push(SerializedBreakpoint { position: breakpoint.position, path: Arc::from(file_path.as_path()), + kind: project::dap_store::BreakpointKind::Standard, }); } From 142a6dea173cb6b1754229acb72a718aa9defec8 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 16 Sep 2024 02:14:16 -0400 Subject: [PATCH 250/650] Get log breakpoint indicator to change into hint color on hover --- crates/editor/src/element.rs | 6 ++++-- crates/project/src/dap_store.rs | 22 +++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index cdc87f7a7d9889..98f85ef85901de 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -5239,12 +5239,14 @@ impl Element for EditorElement { .collect(); // We want all lines with breakpoint's to have their number's painted - // red & we still want to render a grey breakpoint for the gutter + // debugg accent color & we still want to render a grey breakpoint for the gutter // indicator so we add that in after creating breakpoint_rows for layout line nums // Otherwise, when a cursor is on a line number it will always be white even // if that line has a breakpoint if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator { - breakpoint_lines.insert(gutter_breakpoint_point, BreakpointKind::Standard); + breakpoint_lines + .entry(gutter_breakpoint_point) + .or_insert(BreakpointKind::Standard); } let line_numbers = self.layout_line_numbers( diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index a3a72f61768344..e29abb5fcff459 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -9,6 +9,7 @@ use settings::WorktreeId; use std::{ collections::BTreeMap, future::Future, + hash::{Hash, Hasher}, path::{Path, PathBuf}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -271,13 +272,32 @@ pub enum BreakpointKind { Log(LogMessage), } -#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Debug)] pub struct Breakpoint { pub active_position: Option, pub cache_position: u32, pub kind: BreakpointKind, } +// Custom implementation for PartialEq, Eq, and Hash is done +// to get toggle breakpoint to solely be based on a breakpoint's +// location. Otherwise, a user can get in situation's where there's +// overlapping breakpoint's with them being aware. +impl PartialEq for Breakpoint { + fn eq(&self, other: &Self) -> bool { + self.active_position == other.active_position && self.cache_position == other.cache_position + } +} + +impl Eq for Breakpoint {} + +impl Hash for Breakpoint { + fn hash(&self, state: &mut H) { + self.active_position.hash(state); + self.cache_position.hash(state); + } +} + impl Breakpoint { pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { let line = self From c1ab059d54f7cd6e77f173e11fef032c2cce2c6b Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 16 Sep 2024 21:37:59 -0400 Subject: [PATCH 251/650] Get log breakpoint's to serialize/deserialize correctly in the database --- Cargo.lock | 1 + crates/dap/src/adapters.rs | 4 +-- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 2 +- crates/project/Cargo.toml | 1 + crates/project/src/dap_store.rs | 52 +++++++++++++++++++++++++++-- crates/workspace/src/persistence.rs | 37 ++++++++++++++------ 7 files changed, 81 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d0cc6fcd1d25c..8ebc864a26a1ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8445,6 +8445,7 @@ dependencies = [ "smol", "snippet", "snippet_provider", + "sqlez", "task", "tempfile", "terminal", diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index f82b0a56e4f0ee..cfb4731916d1cd 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -194,9 +194,9 @@ impl DebugAdapter for CustomDebugAdapter { async fn connect(&self, cx: &mut AsyncAppContext) -> Result { match &self.connection { - DebugConnectionType::STDIO => create_stdio_client(&self.start_command, &vec![].into()), + DebugConnectionType::STDIO => create_stdio_client(&self.start_command, &vec![]), DebugConnectionType::TCP(tcp_host) => { - create_tcp_client(tcp_host.clone(), &self.start_command, &vec![].into(), cx).await + create_tcp_client(tcp_host.clone(), &self.start_command, &vec![], cx).await } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 28e23d5e217d8c..886561d7cce74e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5402,7 +5402,7 @@ impl Editor { let second_weak = editor_weak.clone(); let context_menu = ui::ContextMenu::build(cx, move |menu, _cx| { - let anchor = position.clone(); + let anchor = position; menu.on_blur_subscription(Subscription::new(|| {})) .context(focus_handle) .entry("Toggle Breakpoint", None, move |cx| { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 98f85ef85901de..e7448906824712 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -5239,7 +5239,7 @@ impl Element for EditorElement { .collect(); // We want all lines with breakpoint's to have their number's painted - // debugg accent color & we still want to render a grey breakpoint for the gutter + // debug accent color & we still want to render a grey breakpoint for the gutter // indicator so we add that in after creating breakpoint_rows for layout line nums // Otherwise, when a cursor is on a line number it will always be white even // if that line has a breakpoint diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index ad67d483d57537..6e2da2b7112f97 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -55,6 +55,7 @@ regex.workspace = true remote.workspace = true rpc.workspace = true schemars.workspace = true +sqlez.workspace = true task.workspace = true tempfile.workspace = true serde.workspace = true diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e29abb5fcff459..99c2b94b08a284 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,3 +1,4 @@ +use crate::ProjectPath; use anyhow::Context as _; use collections::{HashMap, HashSet}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; @@ -6,6 +7,10 @@ use dap::SourceBreakpoint; use gpui::{EventEmitter, ModelContext, Task}; use language::{Buffer, BufferSnapshot}; use settings::WorktreeId; +use sqlez::{ + bindable::{Bind, Column, StaticColumnCount}, + statement::Statement, +}; use std::{ collections::BTreeMap, future::Future, @@ -20,8 +25,6 @@ use task::DebugAdapterConfig; use text::Point; use util::ResultExt as _; -use crate::ProjectPath; - pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), DebugClientStopped(DebugAdapterClientId), @@ -104,7 +107,7 @@ impl DapStore { .insert(Breakpoint { active_position: None, cache_position: serialize_breakpoint.position.saturating_sub(1u32), - kind: BreakpointKind::Standard, + kind: serialize_breakpoint.kind, }); } } @@ -272,6 +275,49 @@ pub enum BreakpointKind { Log(LogMessage), } +impl BreakpointKind { + fn to_int(&self) -> i32 { + match self { + BreakpointKind::Standard => 0, + BreakpointKind::Log(_) => 1, + } + } +} + +impl StaticColumnCount for BreakpointKind { + fn column_count() -> usize { + 2 + } +} + +impl Bind for BreakpointKind { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + let next_index = statement.bind(&self.to_int(), start_index)?; + + match self { + BreakpointKind::Standard => { + statement.bind_null(next_index)?; + Ok(next_index + 1) + } + BreakpointKind::Log(message) => statement.bind(message, next_index), + } + } +} + +impl Column for BreakpointKind { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + let kind = statement.column_int(start_index)?; + match kind { + 0 => Ok((BreakpointKind::Standard, start_index + 2)), + 1 => { + let message = statement.column_text(start_index + 1)?.to_string(); + Ok((BreakpointKind::Log(message), start_index + 2)) + } + _ => Err(anyhow::anyhow!("Invalid BreakpointKind discriminant")), + } + } +} + #[derive(Clone, Debug)] pub struct Breakpoint { pub active_position: Option, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index ad3c9021471a77..4759d73531e220 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -10,7 +10,7 @@ use client::DevServerProjectId; use collections::HashMap; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId}; -use project::dap_store::SerializedBreakpoint; +use project::dap_store::{BreakpointKind, SerializedBreakpoint}; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -143,6 +143,7 @@ impl Column for SerializedWindowBounds { #[derive(Debug)] pub struct Breakpoint { pub position: u32, + pub kind: BreakpointKind, } /// This struct is used to implement traits on Vec @@ -150,14 +151,20 @@ pub struct Breakpoint { #[allow(dead_code)] struct Breakpoints(Vec); -impl sqlez::bindable::StaticColumnCount for Breakpoint {} +impl sqlez::bindable::StaticColumnCount for Breakpoint { + fn column_count() -> usize { + 1 + BreakpointKind::column_count() + } +} + impl sqlez::bindable::Bind for Breakpoint { fn bind( &self, statement: &sqlez::statement::Statement, start_index: i32, ) -> anyhow::Result { - statement.bind(&self.position, start_index) + let next_index = statement.bind(&self.position, start_index)?; + statement.bind(&self.kind, next_index) } } @@ -168,7 +175,9 @@ impl Column for Breakpoint { .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? as u32; - Ok((Breakpoint { position }, start_index + 1)) + let (kind, next_index) = BreakpointKind::column(statement, start_index + 1)?; + + Ok((Breakpoint { position, kind }, next_index)) } } @@ -186,8 +195,10 @@ impl Column for Breakpoints { .with_context(|| format!("Failed to read BreakPoint at index {index}"))? as u32; - breakpoints.push(Breakpoint { position }); - index += 1; + let (kind, next_index) = BreakpointKind::column(statement, index + 1)?; + + breakpoints.push(Breakpoint { position, kind }); + index = next_index; } } } @@ -270,6 +281,8 @@ define_connection! { // worktree_path: PathBuf, // Path of worktree that this breakpoint belong's too. Used to determine the absolute path of a breakpoint // relative_path: PathBuf, // References the file that the breakpoints belong too // breakpoint_location: Vec, // A list of the locations of breakpoints + // kind: int, // The kind of breakpoint (standard, log) + // log_message: String, // log message for log breakpoints, otherwise it's Null // ) pub static ref DB: WorkspaceDb<()> = &[sql!( @@ -424,6 +437,8 @@ define_connection! { worktree_path BLOB NOT NULL, relative_path BLOB NOT NULL, breakpoint_location INTEGER NOT NULL, + kind INTEGER NOT NULL, + log_message TEXT, FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ON UPDATE CASCADE @@ -509,7 +524,7 @@ impl WorkspaceDb { let breakpoints: Result> = self .select_bound(sql! { - SELECT worktree_path, relative_path, breakpoint_location + SELECT worktree_path, relative_path, breakpoint_location, kind, log_message FROM breakpoints WHERE workspace_id = ? }) @@ -530,7 +545,7 @@ impl WorkspaceDb { .push(SerializedBreakpoint { position: breakpoint.position, path: Arc::from(file_path.as_path()), - kind: project::dap_store::BreakpointKind::Standard, + kind: breakpoint.kind, }); } @@ -708,13 +723,13 @@ impl WorkspaceDb { let relative_path = serialized_breakpoint.path; match conn.exec_bound(sql!( - INSERT INTO breakpoints (workspace_id, relative_path, worktree_path, breakpoint_location) - VALUES (?1, ?2, ?3, ?4);))? + INSERT INTO breakpoints (workspace_id, relative_path, worktree_path, breakpoint_location, kind, log_message) + VALUES (?1, ?2, ?3, ?4, ?5, ?6);))? (( workspace.id, relative_path, worktree_path.clone(), - Breakpoint { position: serialized_breakpoint.position}, + Breakpoint { position: serialized_breakpoint.position, kind: serialized_breakpoint.kind}, )) { Err(err) => { log::error!("{err}"); From c3a778724f6b759baa0145fff77e509d2ee217eb Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 17 Sep 2024 02:01:46 -0400 Subject: [PATCH 252/650] Handle breakpoint toggle with different kinds & on_click for log breakpoints --- crates/editor/src/editor.rs | 7 ++++--- crates/project/src/dap_store.rs | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 886561d7cce74e..6dbd7f23eb3d34 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5375,10 +5375,11 @@ impl Editor { Color::Debugger }; - let icon = match kind { + let icon = match &kind { BreakpointKind::Standard => ui::IconName::DebugBreakpoint, BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint, }; + let arc_kind = Arc::new(kind.clone()); IconButton::new(("breakpoint_indicator", row.0 as usize), icon) .icon_size(IconSize::XSmall) @@ -5387,7 +5388,7 @@ impl Editor { .style(ButtonStyle::Transparent) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); - editor.toggle_breakpoint_at_anchor(position, BreakpointKind::Standard, cx); + editor.toggle_breakpoint_at_anchor(position.clone(), (*arc_kind).clone(), cx); })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { let source = editor @@ -5421,7 +5422,7 @@ impl Editor { editor.update(cx, |this, cx| { this.toggle_breakpoint_at_anchor( anchor, - BreakpointKind::Log("Log breakpoint".to_string()), + BreakpointKind::Log("Log breakpoint".into()), cx, ); }) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 99c2b94b08a284..7115bf1dad4316 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -222,7 +222,11 @@ impl DapStore { ) { let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); - if !breakpoint_set.remove(&breakpoint) { + if let Some(gotten_breakpoint) = breakpoint_set.take(&breakpoint) { + if gotten_breakpoint.kind != breakpoint.kind { + breakpoint_set.insert(breakpoint); + } + } else { breakpoint_set.insert(breakpoint); } @@ -267,7 +271,7 @@ impl DapStore { .detach() } } -type LogMessage = String; +type LogMessage = Arc; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum BreakpointKind { @@ -311,7 +315,7 @@ impl Column for BreakpointKind { 0 => Ok((BreakpointKind::Standard, start_index + 2)), 1 => { let message = statement.column_text(start_index + 1)?.to_string(); - Ok((BreakpointKind::Log(message), start_index + 2)) + Ok((BreakpointKind::Log(message.into()), start_index + 2)) } _ => Err(anyhow::anyhow!("Invalid BreakpointKind discriminant")), } @@ -395,7 +399,7 @@ impl Breakpoint { let log_message = match &self.kind { BreakpointKind::Standard => None, - BreakpointKind::Log(log_message) => Some(log_message.clone()), + BreakpointKind::Log(log_message) => Some(log_message.clone().to_string()), }; SourceBreakpoint { @@ -438,7 +442,7 @@ impl SerializedBreakpoint { pub fn to_source_breakpoint(&self) -> SourceBreakpoint { let log_message = match &self.kind { BreakpointKind::Standard => None, - BreakpointKind::Log(message) => Some(message.clone()), + BreakpointKind::Log(message) => Some(message.clone().to_string()), }; SourceBreakpoint { From c88316655c16498aad7045c37b3f31e6b8f6a0ef Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 17 Sep 2024 02:10:10 -0400 Subject: [PATCH 253/650] Fix typos & clippy warnings --- crates/collab/src/db/ids.rs | 2 +- crates/editor/src/editor.rs | 2 +- crates/editor/src/inlay_hint_cache.rs | 2 +- crates/language/src/buffer.rs | 2 +- crates/language/src/buffer_tests.rs | 2 +- crates/picker/src/picker.rs | 2 +- crates/terminal/src/terminal.rs | 2 +- crates/ui/src/components/icon.rs | 2 +- crates/zed/src/reliability.rs | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 82ff8a56e5dc2d..1434bc07cf6c37 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -104,7 +104,7 @@ pub enum ChannelRole { /// Admin can read/write and change permissions. #[sea_orm(string_value = "admin")] Admin, - /// Member can read/write, but not change pemissions. + /// Member can read/write, but not change permissions. #[sea_orm(string_value = "member")] #[default] Member, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6dbd7f23eb3d34..ec7ac30516a4d3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5388,7 +5388,7 @@ impl Editor { .style(ButtonStyle::Transparent) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); - editor.toggle_breakpoint_at_anchor(position.clone(), (*arc_kind).clone(), cx); + editor.toggle_breakpoint_at_anchor(position, (*arc_kind).clone(), cx); })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { let source = editor diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 01b681dd450fd1..28117d457e79c9 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -337,7 +337,7 @@ impl InlayHintCache { /// If needed, queries LSP for new inlay hints, using the invalidation strategy given. /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first, /// followed by the delayed queries of the same range above and below the visible one. - /// This way, concequent refresh invocations are less likely to trigger LSP queries for the invisible ranges. + /// This way, consequent refresh invocations are less likely to trigger LSP queries for the invisible ranges. pub(super) fn spawn_hint_refresh( &mut self, reason_description: &'static str, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4e8af1ea8cb8de..7da55b068cb704 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3022,7 +3022,7 @@ impl BufferSnapshot { let mut start = text.len(); let end = start + buffer_range.len(); - // When multiple names are captured, then the matcheable text + // When multiple names are captured, then the matchable text // includes the whitespace in between the names. if !name_ranges.is_empty() { start -= 1; diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 8584eee4c7d493..91cd57ce91f3c3 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -392,7 +392,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { let buffer = cx.new_model(|cx| Buffer::local(text, cx)); // Spawn a task to format the buffer's whitespace. - // Pause so that the foratting task starts running. + // Pause so that the formatting task starts running. let format = buffer.update(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx)); smol::future::yield_now().await; diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index dc661d91fbabab..8350be2b20e07d 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -509,7 +509,7 @@ impl Picker { .on_mouse_up( MouseButton::Right, cx.listener(move |this, event: &MouseUpEvent, cx| { - // We specficially want to use the platform key here, as + // We specifically want to use the platform key here, as // ctrl will already be held down for the tab switcher. this.handle_click(ix, event.modifiers.platform, cx) }), diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 224e660f5f2c59..ce9e411f5c04c9 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1619,7 +1619,7 @@ fn task_summary(task: &TaskState, error_code: Option) -> (bool, String, Str /// the cursor's `point` is not updated to the new line and column values /// /// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic. -/// Still, concequent `append_text_to_term` invocations are possible and display the contents correctly. +/// Still, consequent `append_text_to_term` invocations are possible and display the contents correctly. /// /// Despite the quirks, this is the simplest approach to appending text to the terminal: its alternative, `grid_mut` manipulations, /// do not properly set the scrolling state and display odd text after appending; also those manipulations are more tedious and error-prone. diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 412dfb940e1dbd..3815d1fd7a9271 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -45,7 +45,7 @@ impl RenderOnce for AnyIcon { /// The decoration for an icon. /// /// For example, this can show an indicator, an "x", -/// or a diagonal strkethrough to indicate something is disabled. +/// or a diagonal strikethrough to indicate something is disabled. #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum IconDecoration { Strikethrough, diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 9731401b3024e0..188cf417f7c38b 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -176,7 +176,7 @@ pub fn monitor_main_thread_hangs( let background_executor = cx.background_executor(); let telemetry_settings = *client::TelemetrySettings::get_global(cx); - // Initialize SIGUSR2 handler to send a backrace to a channel. + // Initialize SIGUSR2 handler to send a backtrace to a channel. let (backtrace_tx, backtrace_rx) = mpsc::channel(); static BACKTRACE: Mutex> = Mutex::new(Vec::new()); static BACKTRACE_SENDER: OnceLock> = OnceLock::new(); From 9612b60ccb5c271bbe641d1acdfc5d721c0bf7ad Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 21 Sep 2024 17:31:50 +0200 Subject: [PATCH 254/650] Refactor: Move types to the correct place and move specific request code to the dapstore (#39) * Make dap store global * Partially move initialize & capability code to dap store * Reuse shutdown for shutdown clients * Rename merge_capabilities * Correctly fallback to current thread id for checking to skip event * Move mthod * Move terminate threads to dap store * Move disconnect and restart to dap store * Update dap-types to the correct version This includes the capabilities::merge method * Change dap store to WeakModel in debug panels * Make clippy happy * Move pause thread to dap store * WIP refactor out thread state and capbilities * Move update thread status to debug panel method * Remove double notify * Move continue thread to dap store * Change WeakModel dapStore to Model dapStore * Move step over to dap store * Move step in to dap store * Move step out to dap store * Remove step back we don't support this yet * Move threadState type to debugPanel * Change to background task * Fix panic when debugSession stopped * Remove capabilities when debug client stops * Add missing notify * Remove drain that causes panic * Remove Arc from debug_panel_item instead use the id * Reset stack_frame_list to prevent crash * Refactor ThreadState to model to prevent recursion dependency in variable_list * WIP * WIP move set_variable_value and get_variables to dap store * Remove unused method * Fix correctly insert updated variables Before this changes you would see the variables * scopes. Because it did not insert the the variables per scope. * Correctly update current stack frame on variable list * Only allow building variable list entries for current stack frame * Make toggle variables & scopes work again * Fix clippy * Pass around id instead of entire client * Move set breakpoints to dap store * Show thread status again in tooltip text * Move stack frames and scope requests to dap store * Move terminate request to dap store * Remove gap that is not doing anything * Add retain back to remove also threads that belong to the client * Add debug kind back to tab content --- Cargo.lock | 2 +- crates/dap/src/adapters.rs | 4 +- crates/dap/src/client.rs | 321 +--------- crates/debugger_ui/src/debugger_panel.rs | 561 +++++++++-------- crates/debugger_ui/src/debugger_panel_item.rs | 276 ++++---- crates/debugger_ui/src/lib.rs | 2 + crates/debugger_ui/src/variable_list.rs | 400 ++++++------ crates/project/src/dap_store.rs | 596 ++++++++++++++++-- crates/project/src/project.rs | 46 +- crates/remote_server/src/headless_project.rs | 2 +- 10 files changed, 1228 insertions(+), 982 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d0cc6fcd1d25c..b4c694974ba3c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types#d4e23edcf7c8ded91a3bdfd32a216bcab68b710c" +source = "git+https://github.com/zed-industries/dap-types#b7404edcd158d7d3ed8a7e81cf6cb3145ff3eb19" dependencies = [ "serde", "serde_json", diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index f82b0a56e4f0ee..cfb4731916d1cd 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -194,9 +194,9 @@ impl DebugAdapter for CustomDebugAdapter { async fn connect(&self, cx: &mut AsyncAppContext) -> Result { match &self.connection { - DebugConnectionType::STDIO => create_stdio_client(&self.start_command, &vec![].into()), + DebugConnectionType::STDIO => create_stdio_client(&self.start_command, &vec![]), DebugConnectionType::TCP(tcp_host) => { - create_tcp_client(tcp_host.clone(), &self.start_command, &vec![].into(), cx).await + create_tcp_client(tcp_host.clone(), &self.start_command, &vec![], cx).await } } } diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 067c24a0df62ef..3f2875a6c94917 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -4,29 +4,18 @@ use anyhow::{anyhow, Context, Result}; use crate::adapters::{build_adapter, DebugAdapter}; use dap_types::{ messages::{Message, Response}, - requests::{ - Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request, - Restart, SetBreakpoints, StepBack, StepIn, StepOut, Terminate, TerminateThreads, Variables, - }, - AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse, - DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, - NextArguments, PauseArguments, RestartArguments, Scope, SetBreakpointsArguments, - SetBreakpointsResponse, Source, SourceBreakpoint, StackFrame, StepBackArguments, - StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, - TerminateThreadsArguments, Variable, VariablesArguments, + requests::Request, }; use futures::{AsyncBufRead, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; -use parking_lot::{Mutex, MutexGuard}; +use parking_lot::Mutex; use serde_json::Value; use smol::{ channel::{bounded, Receiver, Sender}, process::Child, }; use std::{ - collections::{BTreeMap, HashMap, HashSet}, hash::Hash, - path::Path, sync::{ atomic::{AtomicU64, Ordering}, Arc, @@ -47,27 +36,6 @@ pub enum ThreadStatus { #[repr(transparent)] pub struct DebugAdapterClientId(pub usize); -#[derive(Debug, Clone)] -pub struct VariableContainer { - pub container_reference: u64, - pub variable: Variable, - pub depth: usize, -} - -#[derive(Debug, Default, Clone)] -pub struct ThreadState { - pub status: ThreadStatus, - pub stack_frames: Vec, - /// HashMap> - pub scopes: HashMap>, - /// BTreeMap> - pub variables: BTreeMap>, - pub fetched_variable_ids: HashSet, - // we update this value only once we stopped, - // we will use this to indicated if we should show a warning when debugger thread was exited - pub stopped: bool, -} - pub struct DebugAdapterClient { id: DebugAdapterClientId, adapter: Arc>, @@ -75,9 +43,6 @@ pub struct DebugAdapterClient { _process: Arc>>, sequence_count: AtomicU64, config: DebugAdapterConfig, - /// thread_id -> thread_state - thread_states: Arc>>, - capabilities: Arc>>, } pub struct TransportParams { @@ -129,8 +94,6 @@ impl DebugAdapterClient { config, adapter, transport, - capabilities: Default::default(), - thread_states: Default::default(), sequence_count: AtomicU64::new(1), _process: Arc::new(Mutex::new(transport_params.process)), })) @@ -226,17 +189,16 @@ impl DebugAdapterClient { self.config.clone() } - pub fn request_args(&self) -> Option { - // TODO Debugger: Get request args from adapter - Some(self.adapter.request_args()) + pub fn adapter(&self) -> Arc> { + self.adapter.clone() } - pub fn request_type(&self) -> DebugRequestType { - self.config.request.clone() + pub fn request_args(&self) -> Value { + self.adapter.request_args() } - pub fn capabilities(&self) -> dap_types::Capabilities { - self.capabilities.lock().clone().unwrap_or_default() + pub fn request_type(&self) -> DebugRequestType { + self.config.request.clone() } /// Get the next sequence id to be used in a request @@ -244,230 +206,7 @@ impl DebugAdapterClient { self.sequence_count.fetch_add(1, Ordering::Relaxed) } - pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) { - if let Some(thread_state) = self.thread_states().get_mut(&thread_id) { - thread_state.status = status; - }; - } - - pub fn thread_states(&self) -> MutexGuard> { - self.thread_states.lock() - } - - pub fn thread_state_by_id(&self, thread_id: u64) -> ThreadState { - self.thread_states.lock().get(&thread_id).cloned().unwrap() - } - - pub async fn initialize(&self) -> Result { - let args = dap_types::InitializeRequestArguments { - client_id: Some("zed".to_owned()), - client_name: Some("Zed".to_owned()), - adapter_id: self.adapter.id(), - locale: Some("en-us".to_owned()), - path_format: Some(InitializeRequestArgumentsPathFormat::Path), - supports_variable_type: Some(true), - supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(true), - supports_memory_references: Some(true), - supports_progress_reporting: Some(true), - supports_invalidated_event: Some(true), - lines_start_at1: Some(true), - columns_start_at1: Some(true), - supports_memory_event: Some(true), - supports_args_can_be_interpreted_by_shell: Some(true), - supports_start_debugging_request: Some(true), - }; - - let capabilities = self.request::(args).await?; - - *self.capabilities.lock() = Some(capabilities.clone()); - - Ok(capabilities) - } - - pub async fn launch(&self, args: Option) -> Result<()> { - self.request::(LaunchRequestArguments { - raw: args.unwrap_or(Value::Null), - }) - .await - } - - pub async fn attach(&self, args: Option) -> Result<()> { - self.request::(AttachRequestArguments { - raw: args.unwrap_or(Value::Null), - }) - .await - } - - pub async fn resume(&self, thread_id: u64) -> Result { - let supports_single_thread_execution_requests = self - .capabilities() - .supports_single_thread_execution_requests - .unwrap_or_default(); - - self.request::(ContinueArguments { - thread_id, - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - } - - pub async fn step_over(&self, thread_id: u64, granularity: SteppingGranularity) -> Result<()> { - let capabilities = self.capabilities(); - - let supports_single_thread_execution_requests = capabilities - .supports_single_thread_execution_requests - .unwrap_or_default(); - let supports_stepping_granularity = capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - self.request::(NextArguments { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - } - - pub async fn step_in(&self, thread_id: u64, granularity: SteppingGranularity) -> Result<()> { - let capabilities = self.capabilities(); - - let supports_single_thread_execution_requests = capabilities - .supports_single_thread_execution_requests - .unwrap_or_default(); - let supports_stepping_granularity = capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - self.request::(StepInArguments { - thread_id, - target_id: None, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - } - - pub async fn step_out(&self, thread_id: u64, granularity: SteppingGranularity) -> Result<()> { - let capabilities = self.capabilities(); - - let supports_single_thread_execution_requests = capabilities - .supports_single_thread_execution_requests - .unwrap_or_default(); - let supports_stepping_granularity = capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - self.request::(StepOutArguments { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - } - - pub async fn step_back(&self, thread_id: u64, granularity: SteppingGranularity) -> Result<()> { - let capabilities = self.capabilities(); - - let supports_single_thread_execution_requests = capabilities - .supports_single_thread_execution_requests - .unwrap_or_default(); - let supports_stepping_granularity = capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - self.request::(StepBackArguments { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - } - - pub async fn restart(&self) -> Result<()> { - self.request::(RestartArguments { - raw: self.adapter.request_args(), - }) - .await - } - - pub async fn pause(&self, thread_id: u64) -> Result<()> { - self.request::(PauseArguments { thread_id }).await - } - - pub async fn disconnect( - &self, - restart: Option, - terminate: Option, - suspend: Option, - ) -> Result<()> { - let supports_terminate_debuggee = self - .capabilities() - .support_terminate_debuggee - .unwrap_or_default(); - - let supports_suspend_debuggee = self - .capabilities() - .support_terminate_debuggee - .unwrap_or_default(); - - self.request::(DisconnectArguments { - restart, - terminate_debuggee: if supports_terminate_debuggee { - terminate - } else { - None - }, - suspend_debuggee: if supports_suspend_debuggee { - suspend - } else { - None - }, - }) - .await - } - - pub async fn set_breakpoints( - &self, - absolute_file_path: Arc, - breakpoints: Vec, - ) -> Result { - self.request::(SetBreakpointsArguments { - source: Source { - path: Some(String::from(absolute_file_path.to_string_lossy())), - name: None, - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - }, - breakpoints: Some(breakpoints), - source_modified: None, - lines: None, - }) - .await - } - - pub async fn configuration_done(&self) -> Result<()> { - let support_configuration_done_request = self - .capabilities() - .supports_configuration_done_request - .unwrap_or_default(); - - if support_configuration_done_request { - self.request::(ConfigurationDoneArguments) - .await - } else { - Ok(()) - } - } - pub async fn shutdown(&self) -> Result<()> { - let _ = self.terminate().await; - self.transport.server_tx.close(); self.transport.server_rx.close(); @@ -492,48 +231,4 @@ impl DebugAdapterClient { } .await } - - pub async fn terminate(&self) -> Result<()> { - let support_terminate_request = self - .capabilities() - .supports_terminate_request - .unwrap_or_default(); - - if support_terminate_request { - self.request::(TerminateArguments { - restart: Some(false), - }) - .await - } else { - self.disconnect(None, Some(true), None).await - } - } - - pub async fn terminate_threads(&self, thread_ids: Option>) -> Result<()> { - let support_terminate_threads = self - .capabilities() - .supports_terminate_threads_request - .unwrap_or_default(); - - if support_terminate_threads { - self.request::(TerminateThreadsArguments { thread_ids }) - .await - } else { - self.terminate().await - } - } - - pub async fn variables(&self, variables_reference: u64) -> Result> { - anyhow::Ok( - self.request::(VariablesArguments { - variables_reference, - filter: None, - start: None, - count: None, - format: None, - }) - .await? - .variables, - ) - } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 64dc2a48ddb32a..62b254825ee56c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,25 +1,26 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use dap::client::DebugAdapterClient; -use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus, VariableContainer}; +use dap::client::{DebugAdapterClientId, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; use dap::messages::{Events, Message}; -use dap::requests::{Request, Scopes, StackTrace, StartDebugging}; +use dap::requests::{Request, StartDebugging}; use dap::{ - Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, ScopesArguments, StackFrame, - StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, + Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, Scope, StackFrame, StoppedEvent, + TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, }; use editor::Editor; use futures::future::try_join_all; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, - FontWeight, Subscription, Task, View, ViewContext, WeakView, + FontWeight, Model, Subscription, Task, View, ViewContext, WeakView, }; +use project::dap_store::DapStore; use settings::Settings; -use std::collections::HashSet; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::Path; use std::sync::Arc; -use task::DebugRequestType; +use std::u64; use ui::prelude::*; use util::ResultExt; use workspace::{ @@ -39,13 +40,36 @@ pub enum DebugPanelEvent { actions!(debug_panel, [ToggleFocus]); +#[derive(Debug, Clone)] +pub struct VariableContainer { + pub container_reference: u64, + pub variable: Variable, + pub depth: usize, +} + +#[derive(Debug, Default, Clone)] +pub struct ThreadState { + pub status: ThreadStatus, + pub stack_frames: Vec, + /// HashMap> + pub scopes: HashMap>, + /// BTreeMap> + pub variables: BTreeMap>, + pub fetched_variable_ids: HashSet, + // we update this value only once we stopped, + // we will use this to indicated if we should show a warning when debugger thread was exited + pub stopped: bool, +} + pub struct DebugPanel { size: Pixels, pane: View, focus_handle: FocusHandle, + dap_store: Model, workspace: WeakView, - _subscriptions: Vec, show_did_not_stop_warning: bool, + _subscriptions: Vec, + thread_states: BTreeMap<(DebugAdapterClientId, u64), Model>, } impl DebugPanel { @@ -77,13 +101,13 @@ impl DebugPanel { cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { project::Event::DebugClientEvent { message, client_id } => { - let Some(client) = this.debug_client_by_id(*client_id, cx) else { + let Some(client) = this.debug_client_by_id(client_id, cx) else { return cx.emit(DebugPanelEvent::ClientStopped(*client_id)); }; match message { Message::Event(event) => { - this.handle_debug_client_events(client, event, cx); + this.handle_debug_client_events(client_id, event, cx); } Message::Request(request) => { if StartDebugging::COMMAND == request.command { @@ -93,29 +117,13 @@ impl DebugPanel { _ => unreachable!(), } } - project::Event::DebugClientStarted(client_id) => { - let Some(client) = this.debug_client_by_id(*client_id, cx) else { - return cx.emit(DebugPanelEvent::ClientStopped(*client_id)); - }; - - cx.background_executor() - .spawn(async move { - client.initialize().await?; - - // send correct request based on adapter config - match client.config().request { - DebugRequestType::Launch => { - client.launch(client.request_args()).await - } - DebugRequestType::Attach => { - client.attach(client.request_args()).await - } - } - }) - .detach_and_log_err(cx); - } project::Event::DebugClientStopped(client_id) => { cx.emit(DebugPanelEvent::ClientStopped(*client_id)); + + this.thread_states + .retain(|&(client_id_, _), _| client_id_ != *client_id); + + cx.notify(); } _ => {} } @@ -126,8 +134,10 @@ impl DebugPanel { pane, size: px(300.), _subscriptions, + dap_store: DapStore::global(cx), focus_handle: cx.focus_handle(), show_did_not_stop_warning: false, + thread_states: Default::default(), workspace: workspace.weak_handle(), } }) @@ -142,9 +152,41 @@ impl DebugPanel { }) } + pub fn update_thread_state_status( + &mut self, + client_id: &DebugAdapterClientId, + thread_id: Option, + status: ThreadStatus, + all_threads_continued: Option, + cx: &mut ViewContext, + ) { + if all_threads_continued.unwrap_or(false) { + for (_, thread_state) in self + .thread_states + .range_mut((*client_id, u64::MIN)..(*client_id, u64::MAX)) + { + thread_state.update(cx, |thread_state, cx| { + thread_state.status = status; + + cx.notify(); + }); + } + } else if let Some(thread_state) = + thread_id.and_then(|thread_id| self.thread_states.get_mut(&(*client_id, thread_id))) + { + thread_state.update(cx, |thread_state, cx| { + thread_state.status = ThreadStatus::Running; + + cx.notify(); + }); + } + + cx.notify(); + } + fn debug_client_by_id( &self, - client_id: DebugAdapterClientId, + client_id: &DebugAdapterClientId, cx: &mut ViewContext, ) -> Option> { self.workspace @@ -169,21 +211,17 @@ impl DebugPanel { pane::Event::RemovedItem { item } => { let thread_panel = item.downcast::().unwrap(); - thread_panel.update(cx, |pane, cx| { - let thread_id = pane.thread_id(); - let client = pane.client(); - let thread_status = client.thread_state_by_id(thread_id).status; - - // only terminate thread if the thread has not yet ended - if thread_status != ThreadStatus::Ended && thread_status != ThreadStatus::Exited - { - let client = client.clone(); - cx.background_executor() - .spawn(async move { - client.terminate_threads(Some(vec![thread_id; 1])).await - }) - .detach_and_log_err(cx); - } + let thread_id = thread_panel.read(cx).thread_id(); + let client_id = thread_panel.read(cx).client_id(); + + self.thread_states.remove(&(client_id, thread_id)); + + cx.notify(); + + self.dap_store.update(cx, |store, cx| { + store + .terminate_threads(&client_id, Some(vec![thread_id; 1]), cx) + .detach() }); } pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), @@ -216,18 +254,18 @@ impl DebugPanel { fn handle_debug_client_events( &mut self, - client: Arc, + client_id: &DebugAdapterClientId, event: &Events, cx: &mut ViewContext, ) { match event { - Events::Initialized(event) => self.handle_initialized_event(client, event, cx), - Events::Stopped(event) => self.handle_stopped_event(client, event, cx), - Events::Continued(event) => self.handle_continued_event(client, event, cx), - Events::Exited(event) => self.handle_exited_event(client, event, cx), - Events::Terminated(event) => self.handle_terminated_event(client, event, cx), - Events::Thread(event) => self.handle_thread_event(client, event, cx), - Events::Output(event) => self.handle_output_event(client, event, cx), + Events::Initialized(event) => self.handle_initialized_event(&client_id, event, cx), + Events::Stopped(event) => self.handle_stopped_event(&client_id, event, cx), + Events::Continued(event) => self.handle_continued_event(&client_id, event, cx), + Events::Exited(event) => self.handle_exited_event(&client_id, event, cx), + Events::Terminated(event) => self.handle_terminated_event(&client_id, event, cx), + Events::Thread(event) => self.handle_thread_event(&client_id, event, cx), + Events::Output(event) => self.handle_output_event(&client_id, event, cx), Events::Breakpoint(_) => {} Events::Module(_) => {} Events::LoadedSource(_) => {} @@ -304,111 +342,116 @@ impl DebugPanel { }) } - async fn remove_highlights_for_thread( - workspace: WeakView, - client: Arc, - thread_id: u64, - cx: AsyncWindowContext, - ) -> Result<()> { - let mut tasks = Vec::new(); - let mut paths: HashSet = HashSet::new(); - let thread_state = client.thread_state_by_id(thread_id); - - for stack_frame in thread_state.stack_frames.into_iter() { - let Some(path) = stack_frame.source.clone().and_then(|s| s.path.clone()) else { - continue; - }; - - if paths.contains(&path) { - continue; - } - - paths.insert(path.clone()); - tasks.push(Self::remove_editor_highlight( - workspace.clone(), - path, - cx.clone(), - )); - } - - if !tasks.is_empty() { - try_join_all(tasks).await?; - } - - anyhow::Ok(()) - } - - async fn remove_editor_highlight( - workspace: WeakView, - path: String, - mut cx: AsyncWindowContext, - ) -> Result<()> { - let task = workspace.update(&mut cx, |workspace, cx| { - let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_absolute_path(&Path::new(&path), cx) - }); - - if let Some(project_path) = project_path { - workspace.open_path(project_path, None, false, cx) - } else { - Task::ready(Err(anyhow::anyhow!( - "No project path found for path: {}", - path - ))) - } - })?; - - let editor = task.await?.downcast::().unwrap(); - - editor.update(&mut cx, |editor, _| { - editor.clear_row_highlights::(); - }) - } + // async fn remove_highlights_for_thread( + // workspace: WeakView, + // client: Arc, + // thread_id: u64, + // cx: AsyncWindowContext, + // ) -> Result<()> { + // let mut tasks = Vec::new(); + // let mut paths: HashSet = HashSet::new(); + // let thread_state = client.thread_state_by_id(thread_id); + + // for stack_frame in thread_state.stack_frames.into_iter() { + // let Some(path) = stack_frame.source.clone().and_then(|s| s.path.clone()) else { + // continue; + // }; + + // if paths.contains(&path) { + // continue; + // } + + // paths.insert(path.clone()); + // tasks.push(Self::remove_editor_highlight( + // workspace.clone(), + // path, + // cx.clone(), + // )); + // } + + // if !tasks.is_empty() { + // try_join_all(tasks).await?; + // } + + // anyhow::Ok(()) + // } + + // async fn remove_editor_highlight( + // workspace: WeakView, + // path: String, + // mut cx: AsyncWindowContext, + // ) -> Result<()> { + // let task = workspace.update(&mut cx, |workspace, cx| { + // let project_path = workspace.project().read_with(cx, |project, cx| { + // project.project_path_for_absolute_path(&Path::new(&path), cx) + // }); + + // if let Some(project_path) = project_path { + // workspace.open_path(project_path, None, false, cx) + // } else { + // Task::ready(Err(anyhow::anyhow!( + // "No project path found for path: {}", + // path + // ))) + // } + // })?; + + // let editor = task.await?.downcast::().unwrap(); + + // editor.update(&mut cx, |editor, _| { + // editor.clear_row_highlights::(); + // }) + // } fn handle_initialized_event( &mut self, - client: Arc, - _: &Option, + client_id: &DebugAdapterClientId, + capabilities: &Option, cx: &mut ViewContext, ) { - cx.spawn(|this, mut cx| async move { - let task = this.update(&mut cx, |this, cx| { - this.workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.send_breakpoints(client.clone(), cx) - }) - }) - })??; + if let Some(capabilities) = capabilities { + self.dap_store.update(cx, |store, cx| { + store.merge_capabilities_for_client(&client_id, capabilities, cx); + }); + } - task.await?; + let send_breakpoints_task = self.workspace.update(cx, |workspace, cx| { + workspace + .project() + .update(cx, |project, cx| project.send_breakpoints(&client_id, cx)) + }); - client.configuration_done().await - }) - .detach_and_log_err(cx); + let configuration_done_task = self.dap_store.update(cx, |store, cx| { + store.send_configuration_done(&client_id, cx) + }); + + cx.background_executor() + .spawn(async move { + send_breakpoints_task?.await; + + configuration_done_task.await + }) + .detach_and_log_err(cx); } fn handle_continued_event( &mut self, - client: Arc, + client_id: &DebugAdapterClientId, event: &ContinuedEvent, cx: &mut ViewContext, ) { - let all_threads = event.all_threads_continued.unwrap_or(false); - - if all_threads { - for thread in client.thread_states().values_mut() { - thread.status = ThreadStatus::Running; - } - } else { - client.update_thread_state_status(event.thread_id, ThreadStatus::Running); - } - - cx.notify(); + self.update_thread_state_status( + client_id, + Some(event.thread_id), + ThreadStatus::Running, + event.all_threads_continued, + cx, + ); } fn handle_stopped_event( &mut self, - client: Arc, + client_id: &DebugAdapterClientId, event: &StoppedEvent, cx: &mut ViewContext, ) { @@ -416,87 +459,106 @@ impl DebugPanel { return; }; - let client_id = client.id(); + let Some(client_kind) = self + .dap_store + .read(cx) + .client_by_id(client_id) + .map(|c| c.config().kind) + else { + return; // this can never happen + }; + + let client_id = *client_id; + cx.spawn({ let event = event.clone(); |this, mut cx| async move { - let stack_trace_response = client - .request::(StackTraceArguments { - thread_id, - start_frame: None, - levels: None, - format: None, + let stack_frames_task = this.update(&mut cx, |this, cx| { + this.dap_store.update(cx, |store, cx| { + store.stack_frames(&client_id, thread_id, cx) }) - .await?; + })?; + + let stack_frames = stack_frames_task.await?; - let mut thread_state = ThreadState::default(); + let current_stack_frame = stack_frames.first().unwrap().clone(); - let current_stack_frame = - stack_trace_response.stack_frames.first().unwrap().clone(); let mut scope_tasks = Vec::new(); - for stack_frame in stack_trace_response.stack_frames.clone().into_iter() { - let client = client.clone(); + for stack_frame in stack_frames.clone().into_iter() { + let stack_frame_scopes_task = this.update(&mut cx, |this, cx| { + this.dap_store + .update(cx, |store, cx| store.scopes(&client_id, stack_frame.id, cx)) + }); + scope_tasks.push(async move { - anyhow::Ok(( - stack_frame.id, - client - .request::(ScopesArguments { - frame_id: stack_frame.id, - }) - .await?, - )) + anyhow::Ok((stack_frame.id, stack_frame_scopes_task?.await?)) }); } let mut stack_frame_tasks = Vec::new(); - for (stack_frame_id, response) in try_join_all(scope_tasks).await? { - let client = client.clone(); - stack_frame_tasks.push(async move { - let mut variable_tasks = Vec::new(); - - for scope in response.scopes { - let scope_reference = scope.variables_reference; + for (stack_frame_id, scopes) in try_join_all(scope_tasks).await? { + let variable_tasks = this.update(&mut cx, |this, cx| { + this.dap_store.update(cx, |store, cx| { + let mut tasks = Vec::new(); + + for scope in scopes { + let variables_task = + store.variables(&client_id, scope.variables_reference, cx); + tasks.push( + async move { anyhow::Ok((scope, variables_task.await?)) }, + ); + } - let client = client.clone(); - variable_tasks.push(async move { - anyhow::Ok((scope, client.variables(scope_reference).await?)) - }); - } + tasks + }) + })?; + stack_frame_tasks.push(async move { anyhow::Ok((stack_frame_id, try_join_all(variable_tasks).await?)) }); } - for (stack_frame_id, scopes) in try_join_all(stack_frame_tasks).await? { - thread_state - .scopes - .insert(stack_frame_id, scopes.iter().map(|s| s.0.clone()).collect()); + let thread_state = this.update(&mut cx, |this, cx| { + this.thread_states + .entry((client_id, thread_id)) + .or_insert(cx.new_model(|_| ThreadState::default())) + .clone() + })?; - for (scope, variables) in scopes { + for (stack_frame_id, scopes) in try_join_all(stack_frame_tasks).await? { + thread_state.update(&mut cx, |thread_state, _| { thread_state - .fetched_variable_ids - .insert(scope.variables_reference); - - thread_state.variables.insert( - scope.variables_reference, - variables - .into_iter() - .map(|v| VariableContainer { - container_reference: scope.variables_reference, - variable: v, - depth: 1, - }) - .collect::>(), - ); - } + .scopes + .insert(stack_frame_id, scopes.iter().map(|s| s.0.clone()).collect()); + + for (scope, variables) in scopes { + thread_state + .fetched_variable_ids + .insert(scope.variables_reference); + + thread_state.variables.insert( + scope.variables_reference, + variables + .into_iter() + .map(|v| VariableContainer { + container_reference: scope.variables_reference, + variable: v, + depth: 1, + }) + .collect::>(), + ); + } + })?; } this.update(&mut cx, |this, cx| { - thread_state.stack_frames = stack_trace_response.stack_frames; - thread_state.status = ThreadStatus::Stopped; - thread_state.stopped = true; + thread_state.update(cx, |thread_state, cx| { + thread_state.stack_frames = stack_frames; + thread_state.status = ThreadStatus::Stopped; + thread_state.stopped = true; - client.thread_states().insert(thread_id, thread_state); + cx.notify(); + }); let existing_item = this .pane @@ -506,7 +568,7 @@ impl DebugPanel { .any(|item| { let item = item.read(cx); - item.client().id() == client_id && item.thread_id() == thread_id + item.client_id() == client_id && item.thread_id() == thread_id }); if !existing_item { @@ -516,7 +578,10 @@ impl DebugPanel { DebugPanelItem::new( debug_panel, this.workspace.clone(), - client.clone(), + this.dap_store.clone(), + thread_state.clone(), + &client_id, + &client_kind, thread_id, current_stack_frame.clone().id, cx, @@ -534,7 +599,7 @@ impl DebugPanel { if let Some(item) = this.pane.read(cx).active_item() { if let Some(pane) = item.downcast::() { let pane = pane.read(cx); - if pane.thread_id() == thread_id && pane.client().id() == client_id { + if pane.thread_id() == thread_id && pane.client_id() == client_id { let workspace = this.workspace.clone(); return cx.spawn(|_, cx| async move { Self::go_to_stack_frame( @@ -559,99 +624,87 @@ impl DebugPanel { fn handle_thread_event( &mut self, - client: Arc, + client_id: &DebugAdapterClientId, event: &ThreadEvent, cx: &mut ViewContext, ) { let thread_id = event.thread_id; - if let Some(thread_state) = client.thread_states().get(&thread_id) { - if !thread_state.stopped && event.reason == ThreadEventReason::Exited { + if let Some(thread_state) = self.thread_states.get(&(*client_id, thread_id)) { + if !thread_state.read(cx).stopped && event.reason == ThreadEventReason::Exited { self.show_did_not_stop_warning = true; cx.notify(); }; } if event.reason == ThreadEventReason::Started { - client - .thread_states() - .insert(thread_id, ThreadState::default()); + self.thread_states.insert( + (*client_id, thread_id), + cx.new_model(|_| ThreadState::default()), + ); } else { - client.update_thread_state_status(thread_id, ThreadStatus::Ended); - - cx.notify(); - - // TODO: we want to figure out for witch clients/threads we should remove the highlights - cx.spawn({ - let client = client.clone(); - |this, mut cx| async move { - let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?; - - Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?; - - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); + self.update_thread_state_status( + client_id, + Some(thread_id), + ThreadStatus::Ended, + None, + cx, + ); + + // TODO debugger: we want to figure out for witch clients/threads we should remove the highlights + // cx.spawn({ + // let client = client.clone(); + // |this, mut cx| async move { + // let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?; + + // Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?; + + // anyhow::Ok(()) + // } + // }) + // .detach_and_log_err(cx); } - cx.emit(DebugPanelEvent::Thread((client.id(), event.clone()))); + cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); } fn handle_exited_event( &mut self, - client: Arc, + client_id: &DebugAdapterClientId, _: &ExitedEvent, cx: &mut ViewContext, ) { - for thread_state in client.thread_states().values_mut() { - thread_state.status = ThreadStatus::Exited; - } - - cx.notify(); + self.update_thread_state_status(client_id, None, ThreadStatus::Exited, Some(true), cx); } fn handle_terminated_event( &mut self, - client: Arc, + client_id: &DebugAdapterClientId, event: &Option, cx: &mut ViewContext, ) { let restart_args = event.clone().and_then(|e| e.restart); - let workspace = self.workspace.clone(); - cx.spawn(|_, mut cx| async move { - Self::remove_highlights(workspace.clone(), cx.clone())?; + // TODO debugger: remove current hightlights + self.dap_store.update(cx, |store, cx| { if restart_args.is_some() { - client.disconnect(Some(true), None, None).await?; - - match client.request_type() { - DebugRequestType::Launch => client.launch(restart_args).await, - DebugRequestType::Attach => client.attach(restart_args).await, - } + store + .restart(&client_id, restart_args, cx) + .detach_and_log_err(cx); } else { - cx.update(|cx| { - workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project - .dap_store() - .update(cx, |store, cx| store.shutdown_client(client.id(), cx)) - }) - }) - })? + store.shutdown_client(&client_id, cx).detach_and_log_err(cx); } - }) - .detach_and_log_err(cx); + }); } fn handle_output_event( &mut self, - client: Arc, + client_id: &DebugAdapterClientId, event: &OutputEvent, cx: &mut ViewContext, ) { - cx.emit(DebugPanelEvent::Output((client.id(), event.clone()))); + cx.emit(DebugPanelEvent::Output((*client_id, event.clone()))); } fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement { diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 0acb8ec295bea5..e53983ffdbab5c 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,19 +1,19 @@ use crate::console::Console; -use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; +use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState}; use crate::variable_list::VariableList; -use anyhow::Result; -use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus}; +use dap::client::{DebugAdapterClientId, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; -use dap::{OutputEvent, OutputEventCategory, StackFrame, StoppedEvent, ThreadEvent}; +use dap::{Capabilities, OutputEvent, OutputEventCategory, StackFrame, StoppedEvent, ThreadEvent}; use editor::Editor; use gpui::{ - impl_actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, ListState, Subscription, View, WeakView, + impl_actions, list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, + ListState, Model, Subscription, View, WeakView, }; +use project::dap_store::DapStore; use serde::Deserialize; use settings::Settings; -use std::sync::Arc; +use task::DebugAdapterKind; use ui::WindowContext; use ui::{prelude::*, Tooltip}; use workspace::dock::Panel; @@ -35,12 +35,16 @@ pub struct DebugPanelItem { thread_id: u64, console: View, focus_handle: FocusHandle, + dap_store: Model, stack_frame_list: ListState, output_editor: View, current_stack_frame_id: u64, + client_kind: DebugAdapterKind, + debug_panel: View, active_thread_item: ThreadItem, workspace: WeakView, - client: Arc, + client_id: DebugAdapterClientId, + thread_state: Model, variable_list: View, _subscriptions: Vec, } @@ -70,18 +74,32 @@ enum DebugPanelItemActionKind { } impl DebugPanelItem { + #[allow(clippy::too_many_arguments)] pub fn new( debug_panel: View, workspace: WeakView, - client: Arc, + dap_store: Model, + thread_state: Model, + client_id: &DebugAdapterClientId, + client_kind: &DebugAdapterKind, thread_id: u64, current_stack_frame_id: u64, cx: &mut ViewContext, ) -> Self { let focus_handle = cx.focus_handle(); - let model = cx.model().clone(); - let variable_list = cx.new_view(|cx| VariableList::new(model, cx)); + let capabilities = dap_store.read(cx).capabilities_by_id(&client_id); + + let variable_list = cx.new_view(|cx| { + VariableList::new( + dap_store.clone(), + &client_id, + &thread_state, + &capabilities, + current_stack_frame_id, + cx, + ) + }); let console = cx.new_view(Console::new); let weakview = cx.view().downgrade(); @@ -130,16 +148,20 @@ impl DebugPanelItem { }); Self { - client, + console, thread_id, + dap_store, workspace, + debug_panel, + thread_state, focus_handle, - variable_list, - console, output_editor, + variable_list, _subscriptions, stack_frame_list, + client_id: *client_id, current_stack_frame_id, + client_kind: client_kind.clone(), active_thread_item: ThreadItem::Variables, } } @@ -149,7 +171,7 @@ impl DebugPanelItem { client_id: &DebugAdapterClientId, thread_id: u64, ) -> bool { - thread_id != this.thread_id || *client_id != this.client.id() + thread_id != this.thread_id || *client_id != this.client_id } fn handle_stopped_event( @@ -158,11 +180,11 @@ impl DebugPanelItem { event: &StoppedEvent, cx: &mut ViewContext, ) { - if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or_default()) { + if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or(this.thread_id)) { return; } - let thread_state = this.current_thread_state(); + let thread_state = this.thread_state.read(cx); this.stack_frame_list.reset(thread_state.stack_frames.len()); if let Some(stack_frame) = thread_state.stack_frames.first() { @@ -182,7 +204,7 @@ impl DebugPanelItem { return; } - // TODO: handle thread event + // TODO debugger: handle thread event } fn handle_output_event( @@ -244,58 +266,55 @@ impl DebugPanelItem { return; } + this.stack_frame_list.reset(0); + + cx.notify(); + cx.emit(Event::Close); } - pub fn client(&self) -> Arc { - self.client.clone() + pub fn client_id(&self) -> DebugAdapterClientId { + self.client_id } pub fn thread_id(&self) -> u64 { self.thread_id } - pub fn current_stack_frame_id(&self) -> u64 { - self.current_stack_frame_id + pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { + self.dap_store + .read_with(cx, |store, _| store.capabilities_by_id(&self.client_id)) } - fn stack_frame_for_index(&self, ix: usize) -> StackFrame { - self.client - .thread_state_by_id(self.thread_id) + fn stack_frame_for_index(&self, ix: usize, cx: &mut ViewContext) -> StackFrame { + self.thread_state + .read(cx) .stack_frames .get(ix) .cloned() .unwrap() } - pub fn current_thread_state(&self) -> ThreadState { - self.client - .thread_states() - .get(&self.thread_id) - .cloned() - .unwrap() - } - fn update_stack_frame_id(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { self.current_stack_frame_id = stack_frame_id; - let thread_state = self.current_thread_state(); - - self.variable_list.update(cx, |variable_list, _| { - variable_list.build_entries(thread_state, stack_frame_id, true, false); + self.variable_list.update(cx, |variable_list, cx| { + variable_list.update_stack_frame_id(stack_frame_id, cx); + variable_list.build_entries(true, false, cx); }); + + cx.notify(); } fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { v_flex() - .gap_3() .size_full() .child(list(self.stack_frame_list.clone()).size_full()) .into_any() } fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { - let stack_frame = self.stack_frame_for_index(ix); + let stack_frame = self.stack_frame_for_index(ix, cx); let source = stack_frame.source.clone(); let is_selected_frame = stack_frame.id == self.current_stack_frame_id; @@ -352,31 +371,6 @@ impl DebugPanelItem { .into_any() } - // if the debug adapter does not send the continued event, - // and the status of the thread did not change we have to assume the thread is running - // so we have to update the thread state status to running - fn update_thread_state( - this: WeakView, - previous_status: ThreadStatus, - all_threads_continued: Option, - mut cx: AsyncWindowContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - if previous_status == this.current_thread_state().status { - if all_threads_continued.unwrap_or(false) { - for thread in this.client.thread_states().values_mut() { - thread.status = ThreadStatus::Running; - } - } else { - this.client - .update_thread_state_status(this.thread_id, ThreadStatus::Running); - } - - cx.notify(); - } - }) - } - /// Actions that should be handled even when Debug Panel is not in focus pub fn workspace_action_handler( workspace: &mut Workspace, @@ -417,90 +411,113 @@ impl DebugPanelItem { } fn handle_continue_action(&mut self, cx: &mut ViewContext) { - let client = self.client.clone(); - let thread_id = self.thread_id; - let previous_status = self.current_thread_state().status; - - cx.spawn(|this, cx| async move { - let response = client.resume(thread_id).await?; + self.debug_panel.update(cx, |panel, cx| { + panel.update_thread_state_status( + &self.client_id, + Some(self.thread_id), + ThreadStatus::Running, + None, + cx, + ); + }); - Self::update_thread_state(this, previous_status, response.all_threads_continued, cx) - }) - .detach_and_log_err(cx); + self.dap_store.update(cx, |store, cx| { + store + .continue_thread(&self.client_id, self.thread_id, cx) + .detach_and_log_err(cx); + }); } fn handle_step_over_action(&mut self, cx: &mut ViewContext) { - let client = self.client.clone(); - let thread_id = self.thread_id; - let previous_status = self.current_thread_state().status; - let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); + self.debug_panel.update(cx, |panel, cx| { + panel.update_thread_state_status( + &self.client_id, + Some(self.thread_id), + ThreadStatus::Running, + None, + cx, + ); + }); - cx.spawn(|this, cx| async move { - client.step_over(thread_id, granularity).await?; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); - Self::update_thread_state(this, previous_status, None, cx) - }) - .detach_and_log_err(cx); + self.dap_store.update(cx, |store, cx| { + store + .step_over(&self.client_id, self.thread_id, granularity, cx) + .detach_and_log_err(cx); + }); } fn handle_step_in_action(&mut self, cx: &mut ViewContext) { - let client = self.client.clone(); - let thread_id = self.thread_id; - let previous_status = self.current_thread_state().status; - let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); + self.debug_panel.update(cx, |panel, cx| { + panel.update_thread_state_status( + &self.client_id, + Some(self.thread_id), + ThreadStatus::Running, + None, + cx, + ); + }); - cx.spawn(|this, cx| async move { - client.step_in(thread_id, granularity).await?; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); - Self::update_thread_state(this, previous_status, None, cx) - }) - .detach_and_log_err(cx); + self.dap_store.update(cx, |store, cx| { + store + .step_in(&self.client_id, self.thread_id, granularity, cx) + .detach_and_log_err(cx); + }); } fn handle_step_out_action(&mut self, cx: &mut ViewContext) { - let client = self.client.clone(); - let thread_id = self.thread_id; - let previous_status = self.current_thread_state().status; - let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); + self.debug_panel.update(cx, |panel, cx| { + panel.update_thread_state_status( + &self.client_id, + Some(self.thread_id), + ThreadStatus::Running, + None, + cx, + ); + }); - cx.spawn(|this, cx| async move { - client.step_out(thread_id, granularity).await?; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); - Self::update_thread_state(this, previous_status, None, cx) - }) - .detach_and_log_err(cx); + self.dap_store.update(cx, |store, cx| { + store + .step_out(&self.client_id, self.thread_id, granularity, cx) + .detach_and_log_err(cx); + }); } fn handle_restart_action(&mut self, cx: &mut ViewContext) { - let client = self.client.clone(); - - cx.background_executor() - .spawn(async move { client.restart().await }) - .detach_and_log_err(cx); + self.dap_store.update(cx, |store, cx| { + store + .restart(&self.client_id, None, cx) + .detach_and_log_err(cx); + }); } fn handle_pause_action(&mut self, cx: &mut ViewContext) { - let client = self.client.clone(); - let thread_id = self.thread_id; - cx.background_executor() - .spawn(async move { client.pause(thread_id).await }) - .detach_and_log_err(cx); + self.dap_store.update(cx, |store, cx| { + store + .pause_thread(&self.client_id, self.thread_id, cx) + .detach_and_log_err(cx) + }); } fn handle_stop_action(&mut self, cx: &mut ViewContext) { - let client = self.client.clone(); - let thread_ids = vec![self.thread_id; 1]; - - cx.background_executor() - .spawn(async move { client.terminate_threads(Some(thread_ids)).await }) - .detach_and_log_err(cx); + self.dap_store.update(cx, |store, cx| { + store + .terminate_threads(&self.client_id, Some(vec![self.thread_id; 1]), cx) + .detach_and_log_err(cx) + }); } fn handle_disconnect_action(&mut self, cx: &mut ViewContext) { - let client = self.client.clone(); - cx.background_executor() - .spawn(async move { client.disconnect(None, Some(true), None).await }) - .detach_and_log_err(cx); + self.dap_store.update(cx, |store, cx| { + store + .disconnect_client(&self.client_id, cx) + .detach_and_log_err(cx); + }); } } @@ -522,8 +539,7 @@ impl Item for DebugPanelItem { ) -> AnyElement { Label::new(format!( "{:?} - Thread {}", - self.client.config().kind, - self.thread_id + self.client_kind, self.thread_id )) .color(if params.selected { Color::Default @@ -533,12 +549,12 @@ impl Item for DebugPanelItem { .into_any_element() } - fn tab_tooltip_text(&self, _: &AppContext) -> Option { + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(SharedString::from(format!( "{:?} Thread {} - {:?}", - self.client.config().kind, + self.client_kind, self.thread_id, - self.current_thread_state().status + self.thread_state.read(cx).status, ))) } @@ -551,9 +567,11 @@ impl Item for DebugPanelItem { impl Render for DebugPanelItem { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let thread_status = self.current_thread_state().status; + let thread_status = self.thread_state.read(cx).status; let active_thread_item = &self.active_thread_item; + let capabilities = self.capabilities(cx); + h_flex() .key_context("DebugPanelItem") .track_focus(&self.focus_handle) @@ -640,11 +658,7 @@ impl Render for DebugPanelItem { })) })) .disabled( - !self - .client - .capabilities() - .supports_restart_request - .unwrap_or_default(), + !capabilities.supports_restart_request.unwrap_or_default(), ) .tooltip(move |cx| Tooltip::text("Restart", cx)), ) diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index b35bdf0d640057..6aecbc72d93c6d 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -2,6 +2,7 @@ use dap::debugger_settings::DebuggerSettings; use debugger_panel::{DebugPanel, ToggleFocus}; use debugger_panel_item::DebugPanelItem; use gpui::AppContext; +use project::dap_store::{self}; use settings::Settings; use ui::ViewContext; use workspace::{StartDebugger, Workspace}; @@ -13,6 +14,7 @@ mod variable_list; pub fn init(cx: &mut AppContext) { DebuggerSettings::register(cx); + dap_store::init(cx); cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 5bc4601b95fe9e..2ef9220dcfa9c5 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,10 +1,5 @@ -use crate::debugger_panel_item::DebugPanelItem; -use dap::{ - client::{ThreadState, VariableContainer}, - requests::{SetExpression, SetVariable, Variables}, - Scope, SetExpressionArguments, SetVariableArguments, Variable, VariablesArguments, -}; - +use crate::debugger_panel::{ThreadState, VariableContainer}; +use dap::{client::DebugAdapterClientId, Capabilities, Scope, Variable}; use editor::{ actions::{self, SelectAll}, Editor, EditorEvent, @@ -15,6 +10,7 @@ use gpui::{ ListState, Model, MouseDownEvent, Point, Subscription, View, }; use menu::Confirm; +use project::dap_store::DapStore; use std::{collections::HashMap, sync::Arc}; use ui::{prelude::*, ContextMenu, ListItem}; @@ -46,17 +42,28 @@ pub enum VariableListEntry { pub struct VariableList { list: ListState, + stack_frame_id: u64, + dap_store: Model, focus_handle: FocusHandle, + capabilities: Capabilities, + client_id: DebugAdapterClientId, open_entries: Vec, + thread_state: Model, set_variable_editor: View, - debug_panel_item: Model, set_variable_state: Option, - stack_frame_entries: HashMap>, + entries: HashMap>, open_context_menu: Option<(View, Point, Subscription)>, } impl VariableList { - pub fn new(debug_panel_item: Model, cx: &mut ViewContext) -> Self { + pub fn new( + dap_store: Model, + client_id: &DebugAdapterClientId, + thread_state: &Model, + capabilities: &Capabilities, + stack_frame_id: u64, + cx: &mut ViewContext, + ) -> Self { let weakview = cx.view().downgrade(); let focus_handle = cx.focus_handle(); @@ -81,22 +88,22 @@ impl VariableList { Self { list, + dap_store, focus_handle, - debug_panel_item, + stack_frame_id, set_variable_editor, + client_id: *client_id, open_context_menu: None, set_variable_state: None, + entries: Default::default(), open_entries: Default::default(), - stack_frame_entries: Default::default(), + thread_state: thread_state.clone(), + capabilities: capabilities.clone(), } } fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let debug_item = self.debug_panel_item.read(cx); - let Some(entries) = self - .stack_frame_entries - .get(&debug_item.current_stack_frame_id()) - else { + let Some(entries) = self.entries.get(&self.stack_frame_id) else { return div().into_any_element(); }; @@ -133,22 +140,24 @@ impl VariableList { } }; - let (stack_frame_id, thread_state) = self.debug_panel_item.read_with(cx, |panel, _| { - (panel.current_stack_frame_id(), panel.current_thread_state()) - }); + self.build_entries(false, true, cx); + } + + pub fn update_stack_frame_id(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { + self.stack_frame_id = stack_frame_id; - self.build_entries(thread_state, stack_frame_id, false, true); cx.notify(); } pub fn build_entries( &mut self, - thread_state: ThreadState, - stack_frame_id: u64, open_first_scope: bool, keep_open_entries: bool, + cx: &mut ViewContext, ) { - let Some(scopes) = thread_state.scopes.get(&stack_frame_id) else { + let thread_state = self.thread_state.read(cx); + + let Some(scopes) = thread_state.scopes.get(&self.stack_frame_id) else { return; }; @@ -226,8 +235,10 @@ impl VariableList { } let len = entries.len(); - self.stack_frame_entries.insert(stack_frame_id, entries); + self.entries.insert(self.stack_frame_id, entries); self.list.reset(len); + + cx.notify(); } fn deploy_variable_context_menu( @@ -240,13 +251,8 @@ impl VariableList { ) { let this = cx.view().clone(); - let (stack_frame_id, client) = self - .debug_panel_item - .read_with(cx, |p, _| (p.current_stack_frame_id(), p.client())); - let support_set_variable = client - .capabilities() - .supports_set_variable - .unwrap_or_default(); + let stack_frame_id = self.stack_frame_id; + let support_set_variable = self.capabilities.supports_set_variable.unwrap_or_default(); let context_menu = ContextMenu::build(cx, |menu, cx| { menu.entry( @@ -292,12 +298,7 @@ impl VariableList { editor.focus(cx); }); - let thread_state = this - .debug_panel_item - .read_with(cx, |panel, _| panel.current_thread_state()); - this.build_entries(thread_state, stack_frame_id, false, true); - - cx.notify(); + this.build_entries(false, true, cx); }), ) }) @@ -323,12 +324,7 @@ impl VariableList { return; }; - let (stack_frame_id, thread_state) = self.debug_panel_item.read_with(cx, |panel, _| { - (panel.current_stack_frame_id(), panel.current_thread_state()) - }); - - self.build_entries(thread_state, stack_frame_id, false, true); - cx.notify(); + self.build_entries(false, true, cx); } fn set_variable_value(&mut self, _: &Confirm, cx: &mut ViewContext) { @@ -341,111 +337,89 @@ impl VariableList { }); let Some(state) = self.set_variable_state.take() else { - cx.notify(); - return; + return cx.notify(); }; - if new_variable_value == state.value { - cx.notify(); - return; + if new_variable_value == state.value || state.stack_frame_id != self.stack_frame_id { + return cx.notify(); } - let (mut thread_state, client) = self - .debug_panel_item - .read_with(cx, |p, _| (p.current_thread_state(), p.client())); + let client_id = self.client_id; let variables_reference = state.parent_variables_reference; let scope = state.scope; let name = state.name; let evaluate_name = state.evaluate_name; let stack_frame_id = state.stack_frame_id; - let supports_set_expression = client - .capabilities() - .supports_set_expression - .unwrap_or_default(); cx.spawn(|this, mut cx| async move { - if let Some(evaluate_name) = supports_set_expression.then(|| evaluate_name).flatten() { - client - .request::(SetExpressionArguments { - expression: evaluate_name, - value: new_variable_value, - frame_id: Some(stack_frame_id), - format: None, - }) - .await?; - } else { - client - .request::(SetVariableArguments { + let set_value_task = this.update(&mut cx, |this, cx| { + this.dap_store.update(cx, |store, cx| { + store.set_variable_value( + &client_id, + stack_frame_id, variables_reference, name, - value: new_variable_value, - format: None, - }) - .await?; - } + new_variable_value, + evaluate_name, + cx, + ) + }) + }); + + set_value_task?.await?; - let Some(scope_variables) = thread_state.variables.remove(&scope.variables_reference) + let Some(scope_variables) = this.update(&mut cx, |this, cx| { + this.thread_state.update(cx, |thread_state, _| { + thread_state.variables.remove(&scope.variables_reference) + }) + })? else { - return anyhow::Ok(()); + return Ok(()); }; - let mut tasks = Vec::new(); - - for variable_container in scope_variables { - let client = client.clone(); - tasks.push(async move { - let variables = client - .request::(VariablesArguments { - variables_reference: variable_container.container_reference, - filter: None, - start: None, - count: None, - format: None, - }) - .await? - .variables; - - let depth = variable_container.depth; - let container_reference = variable_container.container_reference; - - anyhow::Ok( - variables - .into_iter() - .map(move |variable| VariableContainer { - container_reference, - variable, - depth, - }), - ) - }); - } + let tasks = this.update(&mut cx, |this, cx| { + let mut tasks = Vec::new(); - let updated_variables = try_join_all(tasks).await?; + for variable_container in scope_variables { + let fetch_variables_task = this.dap_store.update(cx, |store, cx| { + store.variables(&client_id, variable_container.container_reference, cx) + }); - this.update(&mut cx, |this, cx| { - let (thread_id, stack_frame_id, client) = - this.debug_panel_item.read_with(cx, |panel, _| { - ( - panel.thread_id(), - panel.current_stack_frame_id(), - panel.client(), + tasks.push(async move { + let depth = variable_container.depth; + let container_reference = variable_container.container_reference; + + anyhow::Ok( + fetch_variables_task + .await? + .into_iter() + .map(move |variable| VariableContainer { + container_reference, + variable, + depth, + }) + .collect::>(), ) }); + } - let mut thread_states = client.thread_states(); + tasks + })?; - let Some(thread_state) = thread_states.get_mut(&thread_id) else { - return; - }; + let updated_variables = try_join_all(tasks).await?; - for variables in updated_variables { - thread_state - .variables - .insert(scope.variables_reference, variables.collect::<_>()); - } + this.update(&mut cx, |this, cx| { + this.thread_state.update(cx, |thread_state, cx| { + for variables in updated_variables { + thread_state + .variables + .insert(scope.variables_reference, variables); + } - this.build_entries(thread_state.clone(), stack_frame_id, false, true); - cx.notify(); + cx.notify(); + }); + + this.build_entries(false, true, cx); }) }) .detach_and_log_err(cx); @@ -470,6 +444,88 @@ impl VariableList { .into_any_element() } + fn on_toggle_variable( + &mut self, + ix: usize, + variable_id: &SharedString, + variable_reference: u64, + has_children: bool, + disclosed: Option, + cx: &mut ViewContext, + ) { + if !has_children { + return; + } + + // if we already opened the variable/we already fetched it + // we can just toggle it because we already have the nested variable + if disclosed.unwrap_or(true) + || self + .thread_state + .read(cx) + .fetched_variable_ids + .contains(&variable_reference) + { + return self.toggle_entry_collapsed(&variable_id, cx); + } + + let Some(entries) = self.entries.get(&self.stack_frame_id) else { + return; + }; + + let Some(entry) = entries.get(ix) else { + return; + }; + + if let VariableListEntry::Variable { scope, depth, .. } = entry { + let variable_id = variable_id.clone(); + let scope = scope.clone(); + let depth = *depth; + + let fetch_variables_task = self.dap_store.update(cx, |store, cx| { + store.variables(&self.client_id, variable_reference, cx) + }); + + cx.spawn(|this, mut cx| async move { + let new_variables = fetch_variables_task.await?; + + this.update(&mut cx, |this, cx| { + this.thread_state.update(cx, |thread_state, cx| { + let Some(variables) = + thread_state.variables.get_mut(&scope.variables_reference) + else { + return; + }; + + let position = variables.iter().position(|v| { + variable_entry_id(&scope, &v.variable, v.depth) == variable_id + }); + + if let Some(position) = position { + variables.splice( + position + 1..position + 1, + new_variables.clone().into_iter().map(|variable| { + VariableContainer { + container_reference: variable_reference, + variable, + depth: depth + 1, + } + }), + ); + + thread_state.fetched_variable_ids.insert(variable_reference); + } + + cx.notify(); + }); + + this.toggle_entry_collapsed(&variable_id, cx); + }) + }) + .detach_and_log_err(cx); + } + } + #[allow(clippy::too_many_arguments)] fn render_variable( &self, @@ -501,93 +557,15 @@ impl VariableList { .indent_step_size(px(20.)) .always_show_disclosure_icon(true) .toggle(disclosed) - .on_toggle(cx.listener({ - let variable_id = variable_id.clone(); - move |this, _, cx| { - if !has_children { - return; - } - - let debug_item = this.debug_panel_item.read(cx); - - // if we already opened the variable/we already fetched it - // we can just toggle it because we already have the nested variable - if disclosed.unwrap_or(true) - || debug_item - .current_thread_state() - .fetched_variable_ids - .contains(&variable_reference) - { - return this.toggle_entry_collapsed(&variable_id, cx); - } - - let Some(entries) = this - .stack_frame_entries - .get(&debug_item.current_stack_frame_id()) - else { - return; - }; - - let Some(entry) = entries.get(ix) else { - return; - }; - - if let VariableListEntry::Variable { scope, depth, .. } = entry { - let variable_id = variable_id.clone(); - let client = debug_item.client(); - let scope = scope.clone(); - let depth = *depth; - - cx.spawn(|this, mut cx| async move { - let new_variables = - client.variables(variable_reference).await?; - - this.update(&mut cx, |this, cx| { - let client = client.clone(); - let mut thread_states = client.thread_states(); - let Some(thread_state) = thread_states - .get_mut(&this.debug_panel_item.read(cx).thread_id()) - else { - return; - }; - - let Some(variables) = thread_state - .variables - .get_mut(&scope.variables_reference) - else { - return; - }; - - let position = variables.iter().position(|v| { - variable_entry_id(&scope, &v.variable, v.depth) - == variable_id - }); - - if let Some(position) = position { - variables.splice( - position + 1..position + 1, - new_variables.clone().into_iter().map(|variable| { - VariableContainer { - container_reference: variable_reference, - variable, - depth: depth + 1, - } - }), - ); - - thread_state - .fetched_variable_ids - .insert(variable_reference); - } - - drop(thread_states); - this.toggle_entry_collapsed(&variable_id, cx); - cx.notify(); - }) - }) - .detach_and_log_err(cx); - } - } + .on_toggle(cx.listener(move |this, _, cx| { + this.on_toggle_variable( + ix, + &variable_id, + variable_reference, + has_children, + disclosed, + cx, + ) })) .on_secondary_mouse_down(cx.listener({ let scope = scope.clone(); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 89a43b08e756e4..29246486712ff7 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,10 +1,24 @@ -use anyhow::Context as _; +use anyhow::{anyhow, Context as _, Result}; use collections::{HashMap, HashSet}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::Message; -use dap::SourceBreakpoint; -use gpui::{EventEmitter, ModelContext, Task}; +use dap::requests::{ + Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Scopes, + SetBreakpoints, SetExpression, SetVariable, StackTrace, StepIn, StepOut, Terminate, + TerminateThreads, Variables, +}; +use dap::{ + AttachRequestArguments, Capabilities, ConfigurationDoneArguments, ContinueArguments, + DisconnectArguments, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, + LaunchRequestArguments, NextArguments, PauseArguments, Scope, ScopesArguments, + SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, + SourceBreakpoint, StackFrame, StackTraceArguments, StepInArguments, StepOutArguments, + SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, + VariablesArguments, +}; +use gpui::{AppContext, Context, EventEmitter, Global, Model, ModelContext, Task}; use language::{Buffer, BufferSnapshot}; +use serde_json::Value; use settings::WorktreeId; use std::{ collections::BTreeMap, @@ -15,7 +29,7 @@ use std::{ Arc, }, }; -use task::DebugAdapterConfig; +use task::{DebugAdapterConfig, DebugRequestType}; use text::Point; use util::ResultExt as _; @@ -39,18 +53,33 @@ pub struct DapStore { next_client_id: AtomicUsize, clients: HashMap, breakpoints: BTreeMap>, + capabilities: HashMap, } impl EventEmitter for DapStore {} +struct GlobalDapStore(Model); + +impl Global for GlobalDapStore {} + +pub fn init(cx: &mut AppContext) { + let store = GlobalDapStore(cx.new_model(DapStore::new)); + cx.set_global(store); +} + impl DapStore { + pub fn global(cx: &AppContext) -> Model { + cx.global::().0.clone() + } + pub fn new(cx: &mut ModelContext) -> Self { cx.on_app_quit(Self::shutdown_clients).detach(); Self { - next_client_id: Default::default(), clients: Default::default(), + capabilities: HashMap::default(), breakpoints: Default::default(), + next_client_id: Default::default(), } } @@ -65,13 +94,33 @@ impl DapStore { }) } - pub fn client_by_id(&self, id: DebugAdapterClientId) -> Option> { - self.clients.get(&id).and_then(|state| match state { + pub fn client_by_id(&self, id: &DebugAdapterClientId) -> Option> { + self.clients.get(id).and_then(|state| match state { DebugAdapterClientState::Starting(_) => None, DebugAdapterClientState::Running(client) => Some(client.clone()), }) } + pub fn capabilities_by_id(&self, client_id: &DebugAdapterClientId) -> Capabilities { + self.capabilities + .get(client_id) + .cloned() + .unwrap_or_default() + } + + pub fn merge_capabilities_for_client( + &mut self, + client_id: &DebugAdapterClientId, + other: &Capabilities, + cx: &mut ModelContext, + ) { + if let Some(capabilities) = self.capabilities.get_mut(client_id) { + *capabilities = capabilities.merge(other.clone()); + + cx.notify(); + } + } + pub fn breakpoints(&self) -> &BTreeMap> { &self.breakpoints } @@ -169,42 +218,467 @@ impl DapStore { ); } - fn shutdown_clients(&mut self, _: &mut ModelContext) -> impl Future { - let shutdown_futures = self - .clients - .drain() - .map(|(_, client_state)| async { - match client_state { - DebugAdapterClientState::Starting(task) => task.await?.shutdown().await.ok(), - DebugAdapterClientState::Running(client) => client.shutdown().await.ok(), + pub fn initialize( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|this, mut cx| async move { + let capabilities = client + .request::(InitializeRequestArguments { + client_id: Some("zed".to_owned()), + client_name: Some("Zed".to_owned()), + adapter_id: client.adapter().id(), + locale: Some("en-US".to_owned()), + path_format: Some(InitializeRequestArgumentsPathFormat::Path), + supports_variable_type: Some(true), + supports_variable_paging: Some(false), + supports_run_in_terminal_request: Some(false), + supports_memory_references: Some(true), + supports_progress_reporting: Some(false), + supports_invalidated_event: Some(false), + lines_start_at1: Some(true), + columns_start_at1: Some(true), + supports_memory_event: Some(false), + supports_args_can_be_interpreted_by_shell: Some(true), + supports_start_debugging_request: Some(true), + }) + .await?; + + this.update(&mut cx, |store, cx| { + store.capabilities.insert(client.id(), capabilities); + + cx.notify(); + })?; + + // send correct request based on adapter config + match client.config().request { + DebugRequestType::Launch => { + client + .request::(LaunchRequestArguments { + raw: client.request_args(), + }) + .await? } + DebugRequestType::Attach => { + client + .request::(AttachRequestArguments { + raw: client.request_args(), + }) + .await? + } + } + + Ok(()) + }) + } + + pub fn stack_frames( + &mut self, + client_id: &DebugAdapterClientId, + thread_id: u64, + cx: &mut ModelContext, + ) -> Task>> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Client was not found"))); + }; + + cx.spawn(|_, _| async move { + Ok(client + .request::(StackTraceArguments { + thread_id, + start_frame: None, + levels: None, + format: None, + }) + .await? + .stack_frames) + }) + } + + pub fn scopes( + &mut self, + client_id: &DebugAdapterClientId, + stack_frame_id: u64, + cx: &mut ModelContext, + ) -> Task>> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Client was not found"))); + }; + + cx.spawn(|_, _| async move { + Ok(client + .request::(ScopesArguments { + frame_id: stack_frame_id, + }) + .await? + .scopes) + }) + } + + pub fn send_configuration_done( + &self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + let capabilities = self.capabilities_by_id(client_id); + + cx.spawn(|_, _| async move { + let support_configuration_done_request = capabilities + .supports_configuration_done_request + .unwrap_or_default(); + + if support_configuration_done_request { + client + .request::(ConfigurationDoneArguments) + .await + } else { + Ok(()) + } + }) + } + + pub fn continue_thread( + &self, + client_id: &DebugAdapterClientId, + thread_id: u64, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|_, _| async move { + client + .request::(ContinueArguments { + thread_id, + single_thread: Some(true), + }) + .await?; + + Ok(()) + }) + } + + pub fn step_over( + &self, + client_id: &DebugAdapterClientId, + thread_id: u64, + granularity: SteppingGranularity, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + let capabilities = self.capabilities_by_id(client_id); + + let supports_single_thread_execution_requests = capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(); + let supports_stepping_granularity = capabilities + .supports_stepping_granularity + .unwrap_or_default(); + + cx.spawn(|_, _| async move { + client + .request::(NextArguments { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + }) + .await + }) + } + + pub fn step_in( + &self, + client_id: &DebugAdapterClientId, + thread_id: u64, + granularity: SteppingGranularity, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + let capabilities = self.capabilities_by_id(client_id); + + let supports_single_thread_execution_requests = capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(); + let supports_stepping_granularity = capabilities + .supports_stepping_granularity + .unwrap_or_default(); + + cx.spawn(|_, _| async move { + client + .request::(StepInArguments { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + target_id: None, + }) + .await + }) + } + + pub fn step_out( + &self, + client_id: &DebugAdapterClientId, + thread_id: u64, + granularity: SteppingGranularity, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + let capabilities = self.capabilities_by_id(client_id); + + let supports_single_thread_execution_requests = capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(); + let supports_stepping_granularity = capabilities + .supports_stepping_granularity + .unwrap_or_default(); + + cx.spawn(|_, _| async move { + client + .request::(StepOutArguments { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + }) + .await + }) + } + + pub fn variables( + &self, + client_id: &DebugAdapterClientId, + variables_reference: u64, + cx: &mut ModelContext, + ) -> Task>> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|_, _| async move { + Ok(client + .request::(VariablesArguments { + variables_reference, + filter: None, + start: None, + count: None, + format: None, + }) + .await? + .variables) + }) + } + + #[allow(clippy::too_many_arguments)] + pub fn set_variable_value( + &self, + client_id: &DebugAdapterClientId, + stack_frame_id: u64, + variables_reference: u64, + name: String, + value: String, + evaluate_name: Option, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + let supports_set_expression = self + .capabilities_by_id(client_id) + .supports_set_expression + .unwrap_or_default(); + + cx.spawn(|_, _| async move { + if let Some(evaluate_name) = supports_set_expression.then(|| evaluate_name).flatten() { + client + .request::(SetExpressionArguments { + expression: evaluate_name, + value, + frame_id: Some(stack_frame_id), + format: None, + }) + .await?; + } else { + client + .request::(SetVariableArguments { + variables_reference, + name, + value, + format: None, + }) + .await?; + } + + Ok(()) + }) + } + + pub fn pause_thread( + &mut self, + client_id: &DebugAdapterClientId, + thread_id: u64, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|_, _| async move { client.request::(PauseArguments { thread_id }).await }) + } + + pub fn terminate_threads( + &mut self, + client_id: &DebugAdapterClientId, + thread_ids: Option>, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + let capabilities = self.capabilities_by_id(client_id); + + if capabilities + .supports_terminate_threads_request + .unwrap_or_default() + { + cx.spawn(|_, _| async move { + client + .request::(TerminateThreadsArguments { thread_ids }) + .await }) - .collect::>(); + } else { + self.shutdown_client(client_id, cx) + } + } + + pub fn disconnect_client( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|_, _| async move { + client + .request::(DisconnectArguments { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }) + .await + }) + } + + pub fn restart( + &mut self, + client_id: &DebugAdapterClientId, + args: Option, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + let restart_args = args.unwrap_or(Value::Null); + + cx.spawn(|_, _| async move { + client + .request::(DisconnectArguments { + restart: Some(true), + terminate_debuggee: Some(false), + suspend_debuggee: Some(false), + }) + .await?; + + match client.request_type() { + DebugRequestType::Launch => { + client + .request::(LaunchRequestArguments { raw: restart_args }) + .await? + } + DebugRequestType::Attach => { + client + .request::(AttachRequestArguments { raw: restart_args }) + .await? + } + } + + Ok(()) + }) + } + + fn shutdown_clients(&mut self, cx: &mut ModelContext) -> impl Future { + let mut tasks = Vec::new(); + + let client_ids = self.clients.keys().cloned().collect::>(); + for client_id in client_ids { + tasks.push(self.shutdown_client(&client_id, cx)); + } async move { - futures::future::join_all(shutdown_futures).await; + futures::future::join_all(tasks).await; } } pub fn shutdown_client( &mut self, - client_id: DebugAdapterClientId, + client_id: &DebugAdapterClientId, cx: &mut ModelContext, - ) { - let Some(debug_client) = self.clients.remove(&client_id) else { - return; + ) -> Task> { + let Some(client) = self.clients.remove(&client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.emit(DapStoreEvent::DebugClientStopped(client_id)); + cx.emit(DapStoreEvent::DebugClientStopped(*client_id)); - cx.background_executor() - .spawn(async move { - match debug_client { - DebugAdapterClientState::Starting(task) => task.await?.shutdown().await.ok(), - DebugAdapterClientState::Running(client) => client.shutdown().await.ok(), - } - }) - .detach(); + let capabilities = self.capabilities.remove(client_id); + + cx.notify(); + + cx.spawn(|_, _| async move { + let client = match client { + DebugAdapterClientState::Starting(task) => task.await, + DebugAdapterClientState::Running(client) => Some(client), + }; + + let Some(client) = client else { + return Ok(()); + }; + + if capabilities + .and_then(|c| c.supports_terminate_request) + .unwrap_or_default() + { + let _ = client + .request::(TerminateArguments { + restart: Some(false), + }) + .await; + } + + client.shutdown().await + }) } pub fn toggle_breakpoint_for_buffer( @@ -221,7 +695,42 @@ impl DapStore { breakpoint_set.insert(breakpoint); } - self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx); + self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) + .detach(); + } + + pub fn send_breakpoints( + &self, + client_id: &DebugAdapterClientId, + absolute_file_path: Arc, + breakpoints: Vec, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|_, _| async move { + client + .request::(SetBreakpointsArguments { + source: Source { + path: Some(String::from(absolute_file_path.to_string_lossy())), + name: None, + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }, + breakpoints: Some(breakpoints), + source_modified: None, + lines: None, + }) + .await?; + + Ok(()) + }) } pub fn send_changed_breakpoints( @@ -230,15 +739,15 @@ impl DapStore { buffer_path: PathBuf, buffer_snapshot: BufferSnapshot, cx: &mut ModelContext, - ) { + ) -> Task<()> { let clients = self.running_clients().collect::>(); if clients.is_empty() { - return; + return Task::ready(()); } let Some(breakpoints) = self.breakpoints.get(project_path) else { - return; + return Task::ready(()); }; let source_breakpoints = breakpoints @@ -248,18 +757,17 @@ impl DapStore { let mut tasks = Vec::new(); for client in clients { - let buffer_path = buffer_path.clone(); - let source_breakpoints = source_breakpoints.clone(); - tasks.push(async move { - client - .set_breakpoints(Arc::from(buffer_path), source_breakpoints) - .await - }); + tasks.push(self.send_breakpoints( + &client.id(), + Arc::from(buffer_path.clone()), + source_breakpoints.clone(), + cx, + )) } - cx.background_executor() - .spawn(async move { futures::future::join_all(tasks).await }) - .detach() + cx.background_executor().spawn(async move { + futures::future::join_all(tasks).await; + }) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index af01bc77658540..b99978089914c4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -39,7 +39,7 @@ use debounced_delay::DebouncedDelay; pub use environment::ProjectEnvironment; use futures::{ channel::mpsc::{self, UnboundedReceiver}, - future::try_join_all, + future::{join_all, try_join_all}, stream::FuturesUnordered, AsyncWriteExt, FutureExt, StreamExt, }; @@ -252,7 +252,6 @@ pub enum Event { Notification(String), LanguageServerPrompt(LanguageServerPromptRequest), LanguageNotFound(Model), - DebugClientStarted(DebugAdapterClientId), DebugClientStopped(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, @@ -644,6 +643,8 @@ impl Project { env: Option>, cx: &mut AppContext, ) -> Model { + let dap_store = DapStore::global(cx); + cx.new_model(|cx: &mut ModelContext| { let (tx, rx) = mpsc::unbounded(); cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) @@ -657,8 +658,6 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); - let dap_store = cx.new_model(DapStore::new); - let buffer_store = cx.new_model(|cx| { BufferStore::new(worktree_store.clone(), None, dap_store.clone(), cx) }); @@ -849,7 +848,7 @@ impl Project { store })?; - let dap_store = cx.new_model(DapStore::new)?; + let dap_store = cx.update(|cx| DapStore::global(cx))?; let buffer_store = cx.new_model(|cx| { BufferStore::new( @@ -1106,29 +1105,24 @@ impl Project { pub fn send_breakpoints( &self, - client: Arc, + client_id: &DebugAdapterClientId, cx: &mut ModelContext, - ) -> Task> { - cx.spawn(|project, mut cx| async move { - let task = project.update(&mut cx, |project, cx| { - let mut tasks = Vec::new(); - - for (abs_path, serialized_breakpoints) in project.all_breakpoints(true, cx) { - let source_breakpoints = serialized_breakpoints - .iter() - .map(|bp| bp.to_source_breakpoint()) - .collect::>(); - - tasks - .push(client.set_breakpoints(abs_path.clone(), source_breakpoints.clone())); - } + ) -> Task<()> { + let mut tasks = Vec::new(); - try_join_all(tasks) - })?; + for (abs_path, serialized_breakpoints) in self.all_breakpoints(true, cx) { + let source_breakpoints = serialized_breakpoints + .iter() + .map(|bp| bp.to_source_breakpoint()) + .collect::>(); - task.await?; + tasks.push(self.dap_store.update(cx, |store, cx| { + store.send_breakpoints(client_id, abs_path, source_breakpoints, cx) + })); + } - Ok(()) + cx.background_executor().spawn(async move { + join_all(tasks).await; }) } @@ -2314,7 +2308,9 @@ impl Project { ) { match event { DapStoreEvent::DebugClientStarted(client_id) => { - cx.emit(Event::DebugClientStarted(*client_id)); + self.dap_store.update(cx, |store, cx| { + store.initialize(client_id, cx).detach_and_log_err(cx) + }); } DapStoreEvent::DebugClientStopped(client_id) => { cx.emit(Event::DebugClientStopped(*client_id)); diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 3b2631ca8aa988..a65b4b849375eb 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -43,7 +43,7 @@ impl HeadlessProject { Task::ready(()), cx.background_executor().clone(), )); - let dap_store = cx.new_model(DapStore::new); + let dap_store = DapStore::global(cx); let worktree_store = cx.new_model(|_| WorktreeStore::new(true, fs.clone())); let buffer_store = cx.new_model(|cx| { From f1f14266356f9ebbc0c4e24656a352d86751b56e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 21 Sep 2024 18:38:40 +0200 Subject: [PATCH 255/650] Make CI pass --- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/remote_server/src/headless_project.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 2e070395733443..37bcdd8c84872f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -685,7 +685,7 @@ impl DebugPanel { ) { let restart_args = event.clone().and_then(|e| e.restart); - // TODO debugger: remove current hightlights + // TODO debugger: remove current highlights self.dap_store.update(cx, |store, cx| { if restart_args.is_some() { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 973ceaba10ccb6..fd63dc261498ee 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -4,9 +4,12 @@ use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext}; use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry}; use node_runtime::DummyNodeRuntime; use project::{ - buffer_store::{BufferStore, BufferStoreEvent}, dap_store::DapStore, project_settings::SettingsObserver, - search::SearchQuery, worktree_store::WorktreeStore, LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId, - WorktreeSettings, + buffer_store::{BufferStore, BufferStoreEvent}, + dap_store::DapStore, + project_settings::SettingsObserver, + search::SearchQuery, + worktree_store::WorktreeStore, + LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId, }; use remote::SshSession; use rpc::{ From c26a8f1537835114b030b27501f143362bc588ee Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 21 Sep 2024 18:42:34 +0200 Subject: [PATCH 256/650] Remove unused dep --- Cargo.lock | 4 ---- crates/dap/Cargo.toml | 1 - crates/editor/Cargo.toml | 1 - crates/project/Cargo.toml | 1 - crates/workspace/Cargo.toml | 1 - 5 files changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 570fa63a220d79..cce0f6eaf49b8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3388,7 +3388,6 @@ dependencies = [ "dap-types", "futures 0.3.30", "gpui", - "log", "parking_lot", "schemars", "serde", @@ -3762,7 +3761,6 @@ dependencies = [ "collections", "convert_case 0.6.0", "ctor", - "dap", "db", "emojis", "env_logger", @@ -8510,7 +8508,6 @@ dependencies = [ "language", "log", "lsp", - "multi_buffer", "node_runtime", "parking_lot", "pathdiff", @@ -14144,7 +14141,6 @@ dependencies = [ "client", "clock", "collections", - "dap", "db", "derive_more", "dev_server_projects", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 63c65ce4a30c9f..3e73958f8069ad 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -14,7 +14,6 @@ async-trait.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types" } futures.workspace = true gpui.workspace = true -log.workspace = true parking_lot.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index d797924419284a..b6b22ef64d33f6 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -37,7 +37,6 @@ clock.workspace = true collections.workspace = true convert_case.workspace = true db.workspace = true -dap.workspace = true emojis.workspace = true file_icons.workspace = true futures.workspace = true diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index ad67d483d57537..84a221f617e37c 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -71,7 +71,6 @@ terminal.workspace = true text.workspace = true util.workspace = true which.workspace = true -multi_buffer.workspace = true [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 988ddcedaa3589..1b998eeabe5373 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -35,7 +35,6 @@ call.workspace = true client.workspace = true clock.workspace = true collections.workspace = true -dap.workspace = true db.workspace = true derive_more.workspace = true fs.workspace = true From 4ddb65bdaa74f3e94459bb86dc4eed3e5db3d2eb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 21 Sep 2024 19:25:11 +0200 Subject: [PATCH 257/650] Make test pass again --- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/lib.rs | 2 -- crates/project/src/dap_store.rs | 15 +-------------- crates/project/src/project.rs | 10 +++++----- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 37bcdd8c84872f..4eecfcd840a0c7 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -134,7 +134,7 @@ impl DebugPanel { pane, size: px(300.), _subscriptions, - dap_store: DapStore::global(cx), + dap_store: project.read(cx).dap_store(), focus_handle: cx.focus_handle(), show_did_not_stop_warning: false, thread_states: Default::default(), diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 6aecbc72d93c6d..b35bdf0d640057 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -2,7 +2,6 @@ use dap::debugger_settings::DebuggerSettings; use debugger_panel::{DebugPanel, ToggleFocus}; use debugger_panel_item::DebugPanelItem; use gpui::AppContext; -use project::dap_store::{self}; use settings::Settings; use ui::ViewContext; use workspace::{StartDebugger, Workspace}; @@ -14,7 +13,6 @@ mod variable_list; pub fn init(cx: &mut AppContext) { DebuggerSettings::register(cx); - dap_store::init(cx); cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 29246486712ff7..52cac5a8910032 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -16,7 +16,7 @@ use dap::{ SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, }; -use gpui::{AppContext, Context, EventEmitter, Global, Model, ModelContext, Task}; +use gpui::{EventEmitter, ModelContext, Task}; use language::{Buffer, BufferSnapshot}; use serde_json::Value; use settings::WorktreeId; @@ -58,20 +58,7 @@ pub struct DapStore { impl EventEmitter for DapStore {} -struct GlobalDapStore(Model); - -impl Global for GlobalDapStore {} - -pub fn init(cx: &mut AppContext) { - let store = GlobalDapStore(cx.new_model(DapStore::new)); - cx.set_global(store); -} - impl DapStore { - pub fn global(cx: &AppContext) -> Model { - cx.global::().0.clone() - } - pub fn new(cx: &mut ModelContext) -> Self { cx.on_app_quit(Self::shutdown_clients).detach(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e147b12b29f129..419fdbdb2cc635 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -640,8 +640,6 @@ impl Project { env: Option>, cx: &mut AppContext, ) -> Model { - let dap_store = DapStore::global(cx); - cx.new_model(|cx: &mut ModelContext| { let (tx, rx) = mpsc::unbounded(); cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) @@ -655,6 +653,8 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); + let dap_store = cx.new_model(|cx| DapStore::new(cx)); + let buffer_store = cx.new_model(|cx| { BufferStore::new(worktree_store.clone(), None, dap_store.clone(), cx) }); @@ -743,8 +743,6 @@ impl Project { fs: Arc, cx: &mut AppContext, ) -> Model { - let dap_store = DapStore::global(cx); - cx.new_model(|cx: &mut ModelContext| { let (tx, rx) = mpsc::unbounded(); cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) @@ -759,6 +757,8 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); + let dap_store = cx.new_model(|cx| DapStore::new(cx)); + let buffer_store = cx.new_model(|cx| { BufferStore::new(worktree_store.clone(), None, dap_store.clone(), cx) }); @@ -916,7 +916,7 @@ impl Project { store })?; - let dap_store = cx.update(|cx| DapStore::global(cx))?; + let dap_store = cx.new_model(|cx| DapStore::new(cx))?; let buffer_store = cx.new_model(|cx| { BufferStore::new( From 8b96ac81380cebf431b27614319f53bce9c8358c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 21 Sep 2024 19:39:55 +0200 Subject: [PATCH 258/650] Make clippy pass --- crates/project/src/project.rs | 6 +++--- crates/remote_server/src/headless_project.rs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 419fdbdb2cc635..851922e5eee90e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -653,7 +653,7 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); - let dap_store = cx.new_model(|cx| DapStore::new(cx)); + let dap_store = cx.new_model(DapStore::new); let buffer_store = cx.new_model(|cx| { BufferStore::new(worktree_store.clone(), None, dap_store.clone(), cx) @@ -757,7 +757,7 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); - let dap_store = cx.new_model(|cx| DapStore::new(cx)); + let dap_store = cx.new_model(DapStore::new); let buffer_store = cx.new_model(|cx| { BufferStore::new(worktree_store.clone(), None, dap_store.clone(), cx) @@ -916,7 +916,7 @@ impl Project { store })?; - let dap_store = cx.new_model(|cx| DapStore::new(cx))?; + let dap_store = cx.new_model(DapStore::new)?; let buffer_store = cx.new_model(|cx| { BufferStore::new( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index fd63dc261498ee..f47dbd9f67c67b 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -43,7 +43,6 @@ impl HeadlessProject { } pub fn new(session: Arc, fs: Arc, cx: &mut ModelContext) -> Self { - let dap_store = DapStore::global(cx); let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); let worktree_store = cx.new_model(|cx| { @@ -51,6 +50,8 @@ impl HeadlessProject { store.shared(SSH_PROJECT_ID, session.clone().into(), cx); store }); + + let dap_store = cx.new_model(DapStore::new); let buffer_store = cx.new_model(|cx| { let mut buffer_store = BufferStore::new( worktree_store.clone(), From a3dff431c54656dfdc967797ea4c9079706d8d31 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 22 Sep 2024 17:02:33 +0200 Subject: [PATCH 259/650] Move all the debug panel actions to the workspace (#41) This PR moves out all the actions from the **debug_panel_item** to the **workspace** which allows people to use these actions inside their key binds. I also had to remove the debug_panel dependency inside the debug_panel_item, because we hit a `"cannot update debug_panel while it is already being updated"` panic. So instead of updating the thread status inside the **debug_panel** we now do this inside the **debug_panel_item** to prevent this panic. I also move the actions to its own debugger namespace, so it's more clear what the actions are for. The new actions can now also be used for key binds: ``` debugger: start debugger: continue debugger: step into debugger: step over debugger: step out debugger: restart debugger: stop debugger: pause ``` /cc @Anthony-Eid We now can have key binds for debugger actions. --- crates/debugger_ui/src/debugger_panel.rs | 60 +--- crates/debugger_ui/src/debugger_panel_item.rs | 273 +++++++----------- crates/debugger_ui/src/lib.rs | 83 +++++- crates/workspace/src/workspace.rs | 6 +- 4 files changed, 196 insertions(+), 226 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 4eecfcd840a0c7..5de32ff4682cb8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -27,13 +27,15 @@ use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; -use workspace::{pane, Pane, StartDebugger}; +use workspace::{pane, Pane, Start}; enum DebugCurrentRowHighlight {} pub enum DebugPanelEvent { + Exited(DebugAdapterClientId), Stopped((DebugAdapterClientId, StoppedEvent)), Thread((DebugAdapterClientId, ThreadEvent)), + Continued((DebugAdapterClientId, ContinuedEvent)), Output((DebugAdapterClientId, OutputEvent)), ClientStopped(DebugAdapterClientId), } @@ -152,36 +154,14 @@ impl DebugPanel { }) } - pub fn update_thread_state_status( - &mut self, - client_id: &DebugAdapterClientId, - thread_id: Option, - status: ThreadStatus, - all_threads_continued: Option, + pub fn active_debug_panel_item( + &self, cx: &mut ViewContext, - ) { - if all_threads_continued.unwrap_or(false) { - for (_, thread_state) in self - .thread_states - .range_mut((*client_id, u64::MIN)..(*client_id, u64::MAX)) - { - thread_state.update(cx, |thread_state, cx| { - thread_state.status = status; - - cx.notify(); - }); - } - } else if let Some(thread_state) = - thread_id.and_then(|thread_id| self.thread_states.get_mut(&(*client_id, thread_id))) - { - thread_state.update(cx, |thread_state, cx| { - thread_state.status = ThreadStatus::Running; - - cx.notify(); - }); - } - - cx.notify(); + ) -> Option> { + self.pane + .read(cx) + .active_item() + .and_then(|panel| panel.downcast::()) } fn debug_client_by_id( @@ -440,13 +420,7 @@ impl DebugPanel { event: &ContinuedEvent, cx: &mut ViewContext, ) { - self.update_thread_state_status( - client_id, - Some(event.thread_id), - ThreadStatus::Running, - event.all_threads_continued, - cx, - ); + cx.emit(DebugPanelEvent::Continued((*client_id, event.clone()))); } fn handle_stopped_event( @@ -643,14 +617,6 @@ impl DebugPanel { cx.new_model(|_| ThreadState::default()), ); } else { - self.update_thread_state_status( - client_id, - Some(thread_id), - ThreadStatus::Ended, - None, - cx, - ); - // TODO debugger: we want to figure out for witch clients/threads we should remove the highlights // cx.spawn({ // let client = client.clone(); @@ -674,7 +640,7 @@ impl DebugPanel { _: &ExitedEvent, cx: &mut ViewContext, ) { - self.update_thread_state_status(client_id, None, ThreadStatus::Exited, Some(true), cx); + cx.emit(DebugPanelEvent::Exited(*client_id)); } fn handle_terminated_event( @@ -835,7 +801,7 @@ impl Render for DebugPanel { ) .label_size(LabelSize::Small) .on_click(move |_, cx| { - cx.dispatch_action(StartDebugger.boxed_clone()); + cx.dispatch_action(Start.boxed_clone()); }) ), ), diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index e53983ffdbab5c..12b8c8f13846c0 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -4,19 +4,20 @@ use crate::variable_list::VariableList; use dap::client::{DebugAdapterClientId, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; -use dap::{Capabilities, OutputEvent, OutputEventCategory, StackFrame, StoppedEvent, ThreadEvent}; +use dap::{ + Capabilities, ContinuedEvent, OutputEvent, OutputEventCategory, StackFrame, StoppedEvent, + ThreadEvent, +}; use editor::Editor; use gpui::{ - impl_actions, list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, - ListState, Model, Subscription, View, WeakView, + list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListState, Model, + Subscription, View, WeakView, }; use project::dap_store::DapStore; -use serde::Deserialize; use settings::Settings; use task::DebugAdapterKind; use ui::WindowContext; use ui::{prelude::*, Tooltip}; -use workspace::dock::Panel; use workspace::item::{Item, ItemEvent}; use workspace::Workspace; @@ -40,7 +41,6 @@ pub struct DebugPanelItem { output_editor: View, current_stack_frame_id: u64, client_kind: DebugAdapterKind, - debug_panel: View, active_thread_item: ThreadItem, workspace: WeakView, client_id: DebugAdapterClientId, @@ -49,30 +49,6 @@ pub struct DebugPanelItem { _subscriptions: Vec, } -impl_actions!(debug_panel_item, [DebugItemAction]); - -/// This struct is for actions that should be triggered even when -/// the debug pane is not in focus. This is done by setting workspace -/// as the action listener then having workspace call `handle_workspace_action` -#[derive(Clone, Deserialize, PartialEq)] -pub struct DebugItemAction { - kind: DebugPanelItemActionKind, -} - -/// Actions that can be sent to workspace -/// currently all of these are button toggles -#[derive(Deserialize, PartialEq, Clone, Debug)] -enum DebugPanelItemActionKind { - Continue, - StepOver, - StepIn, - StepOut, - Restart, - Pause, - Stop, - Disconnect, -} - impl DebugPanelItem { #[allow(clippy::too_many_arguments)] pub fn new( @@ -118,16 +94,22 @@ impl DebugPanelItem { move |this: &mut Self, _, event: &DebugPanelEvent, cx| { match event { DebugPanelEvent::Stopped((client_id, event)) => { - Self::handle_stopped_event(this, client_id, event, cx) + this.handle_stopped_event(client_id, event, cx) } DebugPanelEvent::Thread((client_id, event)) => { - Self::handle_thread_event(this, client_id, event, cx) + this.handle_thread_event(client_id, event, cx) } DebugPanelEvent::Output((client_id, event)) => { - Self::handle_output_event(this, client_id, event, cx) + this.handle_output_event(client_id, event, cx) } DebugPanelEvent::ClientStopped(client_id) => { - Self::handle_client_stopped_event(this, client_id, cx) + this.handle_client_stopped_event(client_id, cx) + } + DebugPanelEvent::Continued((client_id, event)) => { + this.handle_thread_continued_event(client_id, event, cx); + } + DebugPanelEvent::Exited(client_id) => { + this.handle_client_exited_event(client_id, cx); } }; } @@ -152,7 +134,6 @@ impl DebugPanelItem { thread_id, dap_store, workspace, - debug_panel, thread_state, focus_handle, output_editor, @@ -166,54 +147,73 @@ impl DebugPanelItem { } } - fn should_skip_event( - this: &mut Self, + pub fn update_thread_state_status(&mut self, status: ThreadStatus, cx: &mut ViewContext) { + self.thread_state.update(cx, |thread_state, cx| { + thread_state.status = status; + + cx.notify(); + }); + + cx.notify(); + } + + fn should_skip_event(&self, client_id: &DebugAdapterClientId, thread_id: u64) -> bool { + thread_id != self.thread_id || *client_id != self.client_id + } + + fn handle_thread_continued_event( + &mut self, client_id: &DebugAdapterClientId, - thread_id: u64, - ) -> bool { - thread_id != this.thread_id || *client_id != this.client_id + event: &ContinuedEvent, + cx: &mut ViewContext, + ) { + if self.should_skip_event(client_id, event.thread_id) { + return; + } + + self.update_thread_state_status(ThreadStatus::Running, cx); } fn handle_stopped_event( - this: &mut Self, + &mut self, client_id: &DebugAdapterClientId, event: &StoppedEvent, cx: &mut ViewContext, ) { - if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or(this.thread_id)) { + if self.should_skip_event(client_id, event.thread_id.unwrap_or(self.thread_id)) { return; } - let thread_state = this.thread_state.read(cx); + let thread_state = self.thread_state.read(cx); - this.stack_frame_list.reset(thread_state.stack_frames.len()); + self.stack_frame_list.reset(thread_state.stack_frames.len()); if let Some(stack_frame) = thread_state.stack_frames.first() { - this.update_stack_frame_id(stack_frame.id, cx); + self.update_stack_frame_id(stack_frame.id, cx); }; cx.notify(); } fn handle_thread_event( - this: &mut Self, + &mut self, client_id: &DebugAdapterClientId, event: &ThreadEvent, - _: &mut ViewContext, + cx: &mut ViewContext, ) { - if Self::should_skip_event(this, client_id, event.thread_id) { + if self.should_skip_event(client_id, event.thread_id) { return; } - // TODO debugger: handle thread event + self.update_thread_state_status(ThreadStatus::Ended, cx); } fn handle_output_event( - this: &mut Self, + &mut self, client_id: &DebugAdapterClientId, event: &OutputEvent, cx: &mut ViewContext, ) { - if Self::should_skip_event(this, client_id, this.thread_id) { + if self.should_skip_event(client_id, self.thread_id) { return; } @@ -226,13 +226,13 @@ impl DebugPanelItem { match output_category { OutputEventCategory::Console => { - this.console.update(cx, |console, cx| { + self.console.update(cx, |console, cx| { console.add_message(&event.output, cx); }); } // OutputEventCategory::Stderr => {} OutputEventCategory::Stdout => { - this.output_editor.update(cx, |editor, cx| { + self.output_editor.update(cx, |editor, cx| { editor.set_read_only(false); editor.move_to_end(&editor::actions::MoveToEnd, cx); editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); @@ -245,7 +245,7 @@ impl DebugPanelItem { // OutputEventCategory::Important => {} OutputEventCategory::Telemetry => {} _ => { - this.output_editor.update(cx, |editor, cx| { + self.output_editor.update(cx, |editor, cx| { editor.set_read_only(false); editor.move_to_end(&editor::actions::MoveToEnd, cx); editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); @@ -258,17 +258,29 @@ impl DebugPanelItem { } fn handle_client_stopped_event( - this: &mut Self, + &mut self, client_id: &DebugAdapterClientId, cx: &mut ViewContext, ) { - if Self::should_skip_event(this, client_id, this.thread_id) { + if self.should_skip_event(client_id, self.thread_id) { return; } - this.stack_frame_list.reset(0); + self.update_thread_state_status(ThreadStatus::Stopped, cx); - cx.notify(); + cx.emit(Event::Close); + } + + fn handle_client_exited_event( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ViewContext, + ) { + if Self::should_skip_event(self, client_id, self.thread_id) { + return; + } + + self.update_thread_state_status(ThreadStatus::Exited, cx); cx.emit(Event::Close); } @@ -371,55 +383,8 @@ impl DebugPanelItem { .into_any() } - /// Actions that should be handled even when Debug Panel is not in focus - pub fn workspace_action_handler( - workspace: &mut Workspace, - action: &DebugItemAction, - cx: &mut ViewContext, - ) { - let Some(pane) = workspace - .panel::(cx) - .and_then(|panel| panel.read(cx).pane()) - else { - log::error!( - "Can't get Debug panel to handle Debug action: {:?} - This shouldn't happen because there has to be an Debug panel to click a button and trigger this action", - action.kind - ); - return; - }; - - pane.update(cx, |this, cx| { - let Some(active_item) = this - .active_item() - .and_then(|item| item.downcast::()) - else { - return; - }; - - active_item.update(cx, |item, cx| match action.kind { - DebugPanelItemActionKind::Stop => item.handle_stop_action(cx), - DebugPanelItemActionKind::Continue => item.handle_continue_action(cx), - DebugPanelItemActionKind::StepIn => item.handle_step_in_action(cx), - DebugPanelItemActionKind::StepOut => item.handle_step_out_action(cx), - DebugPanelItemActionKind::StepOver => item.handle_step_over_action(cx), - DebugPanelItemActionKind::Pause => item.handle_pause_action(cx), - DebugPanelItemActionKind::Disconnect => item.handle_disconnect_action(cx), - DebugPanelItemActionKind::Restart => item.handle_restart_action(cx), - }); - }); - } - - fn handle_continue_action(&mut self, cx: &mut ViewContext) { - self.debug_panel.update(cx, |panel, cx| { - panel.update_thread_state_status( - &self.client_id, - Some(self.thread_id), - ThreadStatus::Running, - None, - cx, - ); - }); + pub fn continue_thread(&mut self, cx: &mut ViewContext) { + self.update_thread_state_status(ThreadStatus::Running, cx); self.dap_store.update(cx, |store, cx| { store @@ -428,16 +393,8 @@ impl DebugPanelItem { }); } - fn handle_step_over_action(&mut self, cx: &mut ViewContext) { - self.debug_panel.update(cx, |panel, cx| { - panel.update_thread_state_status( - &self.client_id, - Some(self.thread_id), - ThreadStatus::Running, - None, - cx, - ); - }); + pub fn step_over(&mut self, cx: &mut ViewContext) { + self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); @@ -448,16 +405,8 @@ impl DebugPanelItem { }); } - fn handle_step_in_action(&mut self, cx: &mut ViewContext) { - self.debug_panel.update(cx, |panel, cx| { - panel.update_thread_state_status( - &self.client_id, - Some(self.thread_id), - ThreadStatus::Running, - None, - cx, - ); - }); + pub fn step_in(&mut self, cx: &mut ViewContext) { + self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); @@ -468,16 +417,8 @@ impl DebugPanelItem { }); } - fn handle_step_out_action(&mut self, cx: &mut ViewContext) { - self.debug_panel.update(cx, |panel, cx| { - panel.update_thread_state_status( - &self.client_id, - Some(self.thread_id), - ThreadStatus::Running, - None, - cx, - ); - }); + pub fn step_out(&mut self, cx: &mut ViewContext) { + self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); @@ -488,7 +429,7 @@ impl DebugPanelItem { }); } - fn handle_restart_action(&mut self, cx: &mut ViewContext) { + pub fn restart_client(&self, cx: &mut ViewContext) { self.dap_store.update(cx, |store, cx| { store .restart(&self.client_id, None, cx) @@ -496,7 +437,7 @@ impl DebugPanelItem { }); } - fn handle_pause_action(&mut self, cx: &mut ViewContext) { + pub fn pause_thread(&self, cx: &mut ViewContext) { self.dap_store.update(cx, |store, cx| { store .pause_thread(&self.client_id, self.thread_id, cx) @@ -504,7 +445,7 @@ impl DebugPanelItem { }); } - fn handle_stop_action(&mut self, cx: &mut ViewContext) { + pub fn stop_thread(&self, cx: &mut ViewContext) { self.dap_store.update(cx, |store, cx| { store .terminate_threads(&self.client_id, Some(vec![self.thread_id; 1]), cx) @@ -512,7 +453,7 @@ impl DebugPanelItem { }); } - fn handle_disconnect_action(&mut self, cx: &mut ViewContext) { + pub fn disconnect_client(&self, cx: &mut ViewContext) { self.dap_store.update(cx, |store, cx| { store .disconnect_client(&self.client_id, cx) @@ -593,10 +534,8 @@ impl Render for DebugPanelItem { this.child( IconButton::new("debug-pause", IconName::DebugPause) .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(DebugItemAction { - kind: DebugPanelItemActionKind::Pause, - })) + .on_click(cx.listener(|this, _, cx| { + this.pause_thread(cx); })) .tooltip(move |cx| Tooltip::text("Pause program", cx)), ) @@ -604,11 +543,9 @@ impl Render for DebugPanelItem { this.child( IconButton::new("debug-continue", IconName::DebugContinue) .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(DebugItemAction { - kind: DebugPanelItemActionKind::Continue, - })) - })) + .on_click( + cx.listener(|this, _, cx| this.continue_thread(cx)), + ) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| { Tooltip::text("Continue program", cx) @@ -619,10 +556,8 @@ impl Render for DebugPanelItem { .child( IconButton::new("debug-step-over", IconName::DebugStepOver) .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(DebugItemAction { - kind: DebugPanelItemActionKind::StepOver, - })) + .on_click(cx.listener(|this, _, cx| { + this.step_over(cx); })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Step over", cx)), @@ -630,10 +565,8 @@ impl Render for DebugPanelItem { .child( IconButton::new("debug-step-in", IconName::DebugStepInto) .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(DebugItemAction { - kind: DebugPanelItemActionKind::StepIn, - })) + .on_click(cx.listener(|this, _, cx| { + this.step_over(cx); })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Step in", cx)), @@ -641,10 +574,8 @@ impl Render for DebugPanelItem { .child( IconButton::new("debug-step-out", IconName::DebugStepOut) .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(DebugItemAction { - kind: DebugPanelItemActionKind::StepOut, - })) + .on_click(cx.listener(|this, _, cx| { + this.step_out(cx); })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Step out", cx)), @@ -652,10 +583,8 @@ impl Render for DebugPanelItem { .child( IconButton::new("debug-restart", IconName::DebugRestart) .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(DebugItemAction { - kind: DebugPanelItemActionKind::Restart, - })) + .on_click(cx.listener(|this, _, cx| { + this.restart_client(cx); })) .disabled( !capabilities.supports_restart_request.unwrap_or_default(), @@ -665,10 +594,8 @@ impl Render for DebugPanelItem { .child( IconButton::new("debug-stop", IconName::DebugStop) .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(DebugItemAction { - kind: DebugPanelItemActionKind::Stop, - })) + .on_click(cx.listener(|this, _, cx| { + this.stop_thread(cx); })) .disabled( thread_status != ThreadStatus::Stopped @@ -679,10 +606,8 @@ impl Render for DebugPanelItem { .child( IconButton::new("debug-disconnect", IconName::DebugDisconnect) .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, cx| { - cx.dispatch_action(Box::new(DebugItemAction { - kind: DebugPanelItemActionKind::Disconnect, - })) + .on_click(cx.listener(|this, _, cx| { + this.disconnect_client(cx); })) .disabled( thread_status == ThreadStatus::Exited diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index b35bdf0d640057..28048d0eb3b5fb 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,10 +1,9 @@ use dap::debugger_settings::DebuggerSettings; use debugger_panel::{DebugPanel, ToggleFocus}; -use debugger_panel_item::DebugPanelItem; use gpui::AppContext; use settings::Settings; use ui::ViewContext; -use workspace::{StartDebugger, Workspace}; +use workspace::{Continue, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, Workspace}; mod console; pub mod debugger_panel; @@ -20,10 +19,86 @@ pub fn init(cx: &mut AppContext) { .register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::(cx); }) - .register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| { + .register_action(|workspace: &mut Workspace, _: &Start, cx| { tasks_ui::toggle_modal(workspace, cx, task::TaskModal::DebugModal).detach(); }) - .register_action(DebugPanelItem::workspace_action_handler); + .register_action(|workspace: &mut Workspace, _: &Stop, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.stop_thread(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &Continue, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.continue_thread(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &StepInto, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.step_in(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &StepOut, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.step_out(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &StepOver, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.step_over(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &Restart, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.restart_client(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &Pause, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.pause_thread(cx)) + }); + }); }, ) .detach(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8273444b618d02..f1aba57c3ca7c5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -125,6 +125,11 @@ pub struct RemoveWorktreeFromProject(pub WorktreeId); actions!(assistant, [ShowConfiguration]); +actions!( + debugger, + [Start, Continue, Disconnect, Pause, Restart, StepInto, StepOver, StepOut, Stop] +); + actions!( workspace, [ @@ -150,7 +155,6 @@ actions!( ReloadActiveItem, SaveAs, SaveWithoutFormat, - StartDebugger, ToggleBottomDock, ToggleCenteredLayout, ToggleLeftDock, From 9016a03e90e763c703ff08e7eab8a917d3694a0f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 22 Sep 2024 17:05:20 +0200 Subject: [PATCH 260/650] Update the correct status for continued event --- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 12b8c8f13846c0..bf7b1f54833379 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -204,7 +204,7 @@ impl DebugPanelItem { return; } - self.update_thread_state_status(ThreadStatus::Ended, cx); + self.update_thread_state_status(ThreadStatus::Running, cx); } fn handle_output_event( From ce777737961460556ee903e8104ce6ff5a5ce195 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 23 Sep 2024 19:45:12 +0200 Subject: [PATCH 261/650] Remove useless method --- crates/debugger_ui/src/debugger_panel.rs | 10 +++------- crates/project/src/project.rs | 16 ++++------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 5de32ff4682cb8..86a002e01d2760 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -223,13 +223,9 @@ impl DebugPanel { client: Arc, cx: &mut ViewContext, ) { - this.workspace - .update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client(client.config(), cx); - }) - }) - .log_err(); + this.dap_store.update(cx, |store, cx| { + store.start_client(client.config(), cx); + }); } fn handle_debug_client_events( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 851922e5eee90e..3ca4e4c083b891 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -92,8 +92,8 @@ use std::{ }; use task::{ static_source::{StaticSource, TrackedFile}, - DebugAdapterConfig, DebugTaskFile, HideStrategy, RevealStrategy, Shell, TaskContext, - TaskTemplate, TaskVariables, VariableName, VsCodeTaskFile, + DebugTaskFile, HideStrategy, RevealStrategy, Shell, TaskContext, TaskTemplate, TaskVariables, + VariableName, VsCodeTaskFile, }; use terminals::Terminals; use text::{Anchor, BufferId}; @@ -1193,19 +1193,11 @@ impl Project { cx: &mut ModelContext, ) { if let Some(adapter_config) = debug_task.debug_adapter_config() { - self.start_debug_adapter_client(adapter_config, cx); + self.dap_store + .update(cx, |store, cx| store.start_client(adapter_config, cx)); } } - pub fn start_debug_adapter_client( - &mut self, - config: DebugAdapterConfig, - cx: &mut ModelContext, - ) { - self.dap_store - .update(cx, |store, cx| store.start_client(config, cx)); - } - /// Get all serialized breakpoints that belong to a buffer /// /// # Parameters From 9bc08f9f84ab4a23f2c499f82938de9a4b34859f Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 24 Sep 2024 10:54:45 -0400 Subject: [PATCH 262/650] Move breakpoint sql binding to sqlez crate --- Cargo.lock | 2 +- crates/debugger_ui/src/debugger_panel.rs | 1 - crates/project/Cargo.toml | 1 - crates/project/src/dap_store.rs | 40 +----------------------- crates/sqlez/Cargo.toml | 1 + crates/sqlez/src/bindable.rs | 35 +++++++++++++++++++++ 6 files changed, 38 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e890f1c617225..a8f71f10ed2d8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8531,7 +8531,6 @@ dependencies = [ "smol", "snippet", "snippet_provider", - "sqlez", "task", "tempfile", "terminal", @@ -10643,6 +10642,7 @@ dependencies = [ "indoc", "libsqlite3-sys", "parking_lot", + "project", "smol", "thread_local", "util", diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 86a002e01d2760..d9e9da2aaf4f2f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -22,7 +22,6 @@ use std::path::Path; use std::sync::Arc; use std::u64; use ui::prelude::*; -use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 85217d96f00ada..84a221f617e37c 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -55,7 +55,6 @@ regex.workspace = true remote.workspace = true rpc.workspace = true schemars.workspace = true -sqlez.workspace = true task.workspace = true tempfile.workspace = true serde.workspace = true diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 6826b429753c33..4848f3b98e2e81 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -21,10 +21,6 @@ use gpui::{EventEmitter, ModelContext, Task}; use language::{Buffer, BufferSnapshot}; use serde_json::Value; use settings::WorktreeId; -use sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, -}; use std::{ collections::BTreeMap, future::Future, @@ -775,7 +771,7 @@ pub enum BreakpointKind { } impl BreakpointKind { - fn to_int(&self) -> i32 { + pub fn to_int(&self) -> i32 { match self { BreakpointKind::Standard => 0, BreakpointKind::Log(_) => 1, @@ -783,40 +779,6 @@ impl BreakpointKind { } } -impl StaticColumnCount for BreakpointKind { - fn column_count() -> usize { - 2 - } -} - -impl Bind for BreakpointKind { - fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { - let next_index = statement.bind(&self.to_int(), start_index)?; - - match self { - BreakpointKind::Standard => { - statement.bind_null(next_index)?; - Ok(next_index + 1) - } - BreakpointKind::Log(message) => statement.bind(message, next_index), - } - } -} - -impl Column for BreakpointKind { - fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - let kind = statement.column_int(start_index)?; - match kind { - 0 => Ok((BreakpointKind::Standard, start_index + 2)), - 1 => { - let message = statement.column_text(start_index + 1)?.to_string(); - Ok((BreakpointKind::Log(message.into()), start_index + 2)) - } - _ => Err(anyhow::anyhow!("Invalid BreakpointKind discriminant")), - } - } -} - #[derive(Clone, Debug)] pub struct Breakpoint { pub active_position: Option, diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 461017dd8d86b1..8cca6e298e0d27 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -15,6 +15,7 @@ futures.workspace = true indoc.workspace = true libsqlite3-sys = { version = "0.28", features = ["bundled"] } parking_lot.workspace = true +project.workspace = true smol.workspace = true thread_local = "1.1.4" util.workspace = true diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index f888a1a7d2f4c7..05d4429f909e23 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -1,3 +1,4 @@ +use project::dap_store::BreakpointKind; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -381,6 +382,40 @@ impl Column for () { } } +impl StaticColumnCount for BreakpointKind { + fn column_count() -> usize { + 1 + } +} + +impl Bind for BreakpointKind { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + let next_index = statement.bind(&self.to_int(), start_index)?; + + match self { + BreakpointKind::Standard => { + statement.bind_null(next_index)?; + Ok(next_index + 1) + } + BreakpointKind::Log(message) => statement.bind(message, next_index), + } + } +} + +impl Column for BreakpointKind { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + let kind = statement.column_int(start_index)?; + match kind { + -1 => Ok((BreakpointKind::Standard, start_index + 2)), + 0 => { + let message = statement.column_text(start_index + 1)?.to_string(); + Ok((BreakpointKind::Log(message.into()), start_index + 1)) + } + _ => Err(anyhow::anyhow!("Invalid BreakpointKind discriminant")), + } + } +} + macro_rules! impl_tuple_row_traits { ( $($local:ident: $type:ident),+ ) => { impl<$($type: StaticColumnCount),+> StaticColumnCount for ($($type,)+) { From 3b3ac851994c9f4af5eefac8f592c7dab3522d9d Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 24 Sep 2024 11:20:33 -0400 Subject: [PATCH 263/650] Change source/serialize breakpoints to be indexed zero --- crates/project/src/dap_store.rs | 14 ++++++-------- crates/sqlez/src/bindable.rs | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 4848f3b98e2e81..9f5a70acf0634d 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -229,8 +229,8 @@ impl DapStore { supports_memory_references: Some(true), supports_progress_reporting: Some(false), supports_invalidated_event: Some(false), - lines_start_at1: Some(true), - columns_start_at1: Some(true), + lines_start_at1: Some(false), + columns_start_at1: Some(false), supports_memory_event: Some(false), supports_args_can_be_interpreted_by_shell: Some(true), supports_start_debugging_request: Some(true), @@ -810,8 +810,7 @@ impl Breakpoint { let line = self .active_position .map(|position| buffer.summary_for_anchor::(&position).row) - .unwrap_or(self.cache_position) as u64 - + 1u64; + .unwrap_or(self.cache_position) as u64; SourceBreakpoint { line, @@ -851,8 +850,7 @@ impl Breakpoint { let line = self .active_position .map(|position| snapshot.summary_for_anchor::(&position).row) - .unwrap_or(self.cache_position) as u64 - + 1u64; + .unwrap_or(self.cache_position) as u64; let log_message = match &self.kind { BreakpointKind::Standard => None, @@ -875,12 +873,12 @@ impl Breakpoint { position: self .active_position .map(|position| buffer.summary_for_anchor::(&position).row + 1u32) - .unwrap_or(self.cache_position + 1u32), + .unwrap_or(self.cache_position), path, kind: self.kind.clone(), }, None => SerializedBreakpoint { - position: self.cache_position + 1u32, + position: self.cache_position, path, kind: self.kind.clone(), }, diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index 05d4429f909e23..3bb657ffbdc528 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -406,8 +406,8 @@ impl Column for BreakpointKind { fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { let kind = statement.column_int(start_index)?; match kind { - -1 => Ok((BreakpointKind::Standard, start_index + 2)), - 0 => { + 0 => Ok((BreakpointKind::Standard, start_index + 2)), + 1 => { let message = statement.column_text(start_index + 1)?.to_string(); Ok((BreakpointKind::Log(message.into()), start_index + 1)) } From 29918d96a21beea94bca4d73a94f6bafe89530f9 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 24 Sep 2024 20:00:38 +0200 Subject: [PATCH 264/650] Fix wrong function call for step in button --- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index bf7b1f54833379..68a104c42a3d31 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -566,7 +566,7 @@ impl Render for DebugPanelItem { IconButton::new("debug-step-in", IconName::DebugStepInto) .icon_size(IconSize::Small) .on_click(cx.listener(|this, _, cx| { - this.step_over(cx); + this.step_in(cx); })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |cx| Tooltip::text("Step in", cx)), From bfddc634be5635dc6ebb3a000af2a27bdd0ad655 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 25 Sep 2024 17:53:40 +0200 Subject: [PATCH 265/650] Show current debug line when you reopen a buffer (#42) * Only include dap store if editor.mode is FULL * Move go to stack frame to debug_panel_item * Notify dap_store when updating active breakpoints location * Fix clippyyyy * Show active debug line when you reopen a buffer * Remove uncommented code This is not needed anymore, we already clear the highlights when thread exited * Make clippy happy * Fix todo for removing highlights on exited event --- crates/debugger_ui/src/debugger_panel.rs | 203 ++++-------------- crates/debugger_ui/src/debugger_panel_item.rs | 132 +++++++++--- crates/editor/src/editor.rs | 34 ++- crates/project/src/buffer_store.rs | 4 +- crates/project/src/dap_store.rs | 43 +++- 5 files changed, 219 insertions(+), 197 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index d9e9da2aaf4f2f..63950610863c88 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -9,7 +9,6 @@ use dap::{ Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, Scope, StackFrame, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, }; -use editor::Editor; use futures::future::try_join_all; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, @@ -18,7 +17,6 @@ use gpui::{ use project::dap_store::DapStore; use settings::Settings; use std::collections::{BTreeMap, HashMap, HashSet}; -use std::path::Path; use std::sync::Arc; use std::u64; use ui::prelude::*; @@ -28,11 +26,14 @@ use workspace::{ }; use workspace::{pane, Pane, Start}; -enum DebugCurrentRowHighlight {} - pub enum DebugPanelEvent { Exited(DebugAdapterClientId), - Stopped((DebugAdapterClientId, StoppedEvent)), + Terminated(DebugAdapterClientId), + Stopped { + client_id: DebugAdapterClientId, + event: StoppedEvent, + go_to_stack_frame: bool, + }, Thread((DebugAdapterClientId, ThreadEvent)), Continued((DebugAdapterClientId, ContinuedEvent)), Output((DebugAdapterClientId, OutputEvent)), @@ -213,6 +214,19 @@ impl DebugPanel { }) .ok(); } + pane::Event::ActivateItem { local } => { + if !local { + return; + } + + if let Some(active_item) = self.pane.read(cx).active_item() { + if let Some(debug_item) = active_item.downcast::() { + debug_item.update(cx, |panel, cx| { + panel.go_to_stack_frame(cx); + }); + } + } + } _ => {} } } @@ -255,129 +269,6 @@ impl DebugPanel { } } - pub async fn go_to_stack_frame( - workspace: WeakView, - stack_frame: StackFrame, - clear_highlights: bool, - mut cx: AsyncWindowContext, - ) -> Result<()> { - let Some(path) = &stack_frame.source.and_then(|s| s.path) else { - return Err(anyhow::anyhow!( - "Cannot go to stack frame, path doesn't exist" - )); - }; - - let row = (stack_frame.line.saturating_sub(1)) as u32; - let column = (stack_frame.column.saturating_sub(1)) as u32; - - if clear_highlights { - Self::remove_highlights(workspace.clone(), cx.clone())?; - } - - let task = workspace.update(&mut cx, |workspace, cx| { - let project_path = workspace.project().read_with(cx, |project, cx| { - project.project_path_for_absolute_path(&Path::new(&path), cx) - }); - - if let Some(project_path) = project_path { - workspace.open_path_preview(project_path, None, false, true, cx) - } else { - Task::ready(Err(anyhow::anyhow!( - "No project path found for path: {}", - path - ))) - } - })?; - - let editor = task.await?.downcast::().unwrap(); - - workspace.update(&mut cx, |_, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_line::( - row, - column, - Some(cx.theme().colors().editor_debugger_active_line_background), - cx, - ); - }) - }) - } - - fn remove_highlights(workspace: WeakView, mut cx: AsyncWindowContext) -> Result<()> { - workspace.update(&mut cx, |workspace, cx| { - let editor_views = workspace - .items_of_type::(cx) - .collect::>>(); - - for editor_view in editor_views { - editor_view.update(cx, |editor, _| { - editor.clear_row_highlights::(); - }); - } - }) - } - - // async fn remove_highlights_for_thread( - // workspace: WeakView, - // client: Arc, - // thread_id: u64, - // cx: AsyncWindowContext, - // ) -> Result<()> { - // let mut tasks = Vec::new(); - // let mut paths: HashSet = HashSet::new(); - // let thread_state = client.thread_state_by_id(thread_id); - - // for stack_frame in thread_state.stack_frames.into_iter() { - // let Some(path) = stack_frame.source.clone().and_then(|s| s.path.clone()) else { - // continue; - // }; - - // if paths.contains(&path) { - // continue; - // } - - // paths.insert(path.clone()); - // tasks.push(Self::remove_editor_highlight( - // workspace.clone(), - // path, - // cx.clone(), - // )); - // } - - // if !tasks.is_empty() { - // try_join_all(tasks).await?; - // } - - // anyhow::Ok(()) - // } - - // async fn remove_editor_highlight( - // workspace: WeakView, - // path: String, - // mut cx: AsyncWindowContext, - // ) -> Result<()> { - // let task = workspace.update(&mut cx, |workspace, cx| { - // let project_path = workspace.project().read_with(cx, |project, cx| { - // project.project_path_for_absolute_path(&Path::new(&path), cx) - // }); - - // if let Some(project_path) = project_path { - // workspace.open_path(project_path, None, false, cx) - // } else { - // Task::ready(Err(anyhow::anyhow!( - // "No project path found for path: {}", - // path - // ))) - // } - // })?; - - // let editor = task.await?.downcast::().unwrap(); - - // editor.update(&mut cx, |editor, _| { - // editor.clear_row_highlights::(); - // }) - // } - fn handle_initialized_event( &mut self, client_id: &DebugAdapterClientId, @@ -561,31 +452,24 @@ impl DebugPanel { }); } - cx.emit(DebugPanelEvent::Stopped((client_id, event))); - - cx.notify(); - - if let Some(item) = this.pane.read(cx).active_item() { - if let Some(pane) = item.downcast::() { + let go_to_stack_frame = if let Some(item) = this.pane.read(cx).active_item() { + item.downcast::().map_or(false, |pane| { let pane = pane.read(cx); - if pane.thread_id() == thread_id && pane.client_id() == client_id { - let workspace = this.workspace.clone(); - return cx.spawn(|_, cx| async move { - Self::go_to_stack_frame( - workspace, - current_stack_frame.clone(), - true, - cx, - ) - .await - }); - } - } - } - Task::ready(anyhow::Ok(())) - })? - .await + pane.thread_id() == thread_id && pane.client_id() == client_id + }) + } else { + true + }; + + cx.emit(DebugPanelEvent::Stopped { + client_id, + event, + go_to_stack_frame, + }); + + cx.notify(); + }) } }) .detach_and_log_err(cx); @@ -611,19 +495,6 @@ impl DebugPanel { (*client_id, thread_id), cx.new_model(|_| ThreadState::default()), ); - } else { - // TODO debugger: we want to figure out for witch clients/threads we should remove the highlights - // cx.spawn({ - // let client = client.clone(); - // |this, mut cx| async move { - // let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?; - - // Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?; - - // anyhow::Ok(()) - // } - // }) - // .detach_and_log_err(cx); } cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); @@ -646,8 +517,6 @@ impl DebugPanel { ) { let restart_args = event.clone().and_then(|e| e.restart); - // TODO debugger: remove current highlights - self.dap_store.update(cx, |store, cx| { if restart_args.is_some() { store @@ -657,6 +526,8 @@ impl DebugPanel { store.shutdown_client(&client_id, cx).detach_and_log_err(cx); } }); + + cx.emit(DebugPanelEvent::Terminated(*client_id)); } fn handle_output_event( diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 68a104c42a3d31..8337511f070528 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use crate::console::Console; use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState}; use crate::variable_list::VariableList; @@ -14,6 +16,7 @@ use gpui::{ Subscription, View, WeakView, }; use project::dap_store::DapStore; +use project::ProjectPath; use settings::Settings; use task::DebugAdapterKind; use ui::WindowContext; @@ -93,9 +96,11 @@ impl DebugPanelItem { let _subscriptions = vec![cx.subscribe(&debug_panel, { move |this: &mut Self, _, event: &DebugPanelEvent, cx| { match event { - DebugPanelEvent::Stopped((client_id, event)) => { - this.handle_stopped_event(client_id, event, cx) - } + DebugPanelEvent::Stopped { + client_id, + event, + go_to_stack_frame, + } => this.handle_stopped_event(client_id, event, *go_to_stack_frame, cx), DebugPanelEvent::Thread((client_id, event)) => { this.handle_thread_event(client_id, event, cx) } @@ -108,8 +113,8 @@ impl DebugPanelItem { DebugPanelEvent::Continued((client_id, event)) => { this.handle_thread_continued_event(client_id, event, cx); } - DebugPanelEvent::Exited(client_id) => { - this.handle_client_exited_event(client_id, cx); + DebugPanelEvent::Exited(client_id) | DebugPanelEvent::Terminated(client_id) => { + this.handle_client_exited_and_terminated_event(client_id, cx); } }; } @@ -154,6 +159,13 @@ impl DebugPanelItem { cx.notify(); }); + if status == ThreadStatus::Exited + || status == ThreadStatus::Ended + || status == ThreadStatus::Stopped + { + self.clear_highlights(cx); + } + cx.notify(); } @@ -178,6 +190,7 @@ impl DebugPanelItem { &mut self, client_id: &DebugAdapterClientId, event: &StoppedEvent, + go_to_stack_frame: bool, cx: &mut ViewContext, ) { if self.should_skip_event(client_id, event.thread_id.unwrap_or(self.thread_id)) { @@ -188,7 +201,7 @@ impl DebugPanelItem { self.stack_frame_list.reset(thread_state.stack_frames.len()); if let Some(stack_frame) = thread_state.stack_frames.first() { - self.update_stack_frame_id(stack_frame.id, cx); + self.update_stack_frame_id(stack_frame.id, go_to_stack_frame, cx); }; cx.notify(); @@ -271,7 +284,7 @@ impl DebugPanelItem { cx.emit(Event::Close); } - fn handle_client_exited_event( + fn handle_client_exited_and_terminated_event( &mut self, client_id: &DebugAdapterClientId, cx: &mut ViewContext, @@ -299,15 +312,15 @@ impl DebugPanelItem { } fn stack_frame_for_index(&self, ix: usize, cx: &mut ViewContext) -> StackFrame { - self.thread_state - .read(cx) - .stack_frames - .get(ix) - .cloned() - .unwrap() + self.thread_state.read(cx).stack_frames[ix].clone() } - fn update_stack_frame_id(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { + fn update_stack_frame_id( + &mut self, + stack_frame_id: u64, + go_to_stack_frame: bool, + cx: &mut ViewContext, + ) { self.current_stack_frame_id = stack_frame_id; self.variable_list.update(cx, |variable_list, cx| { @@ -315,9 +328,88 @@ impl DebugPanelItem { variable_list.build_entries(true, false, cx); }); + if go_to_stack_frame { + self.go_to_stack_frame(cx); + } + cx.notify(); } + fn clear_highlights(&self, cx: &mut ViewContext) { + self.workspace + .update(cx, |workspace, cx| { + let editor_views = workspace + .items_of_type::(cx) + .collect::>>(); + + for editor_view in editor_views { + editor_view.update(cx, |editor, _| { + editor.clear_row_highlights::(); + }); + } + }) + .ok(); + } + + pub fn project_path_from_stack_frame( + &self, + stack_frame: &StackFrame, + cx: &mut ViewContext, + ) -> Option { + let path = stack_frame.source.as_ref().and_then(|s| s.path.as_ref())?; + + self.workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.project_path_for_absolute_path(&Path::new(path), cx) + }) + }) + .ok()? + } + + pub fn go_to_stack_frame(&mut self, cx: &mut ViewContext) { + self.clear_highlights(cx); + + let stack_frame = self + .thread_state + .read(cx) + .stack_frames + .iter() + .find(|s| s.id == self.current_stack_frame_id) + .cloned(); + + let Some(stack_frame) = stack_frame else { + return; // this could never happen + }; + + let row = (stack_frame.line.saturating_sub(1)) as u32; + let column = (stack_frame.column.saturating_sub(1)) as u32; + + let Some(project_path) = self.project_path_from_stack_frame(&stack_frame, cx) else { + return; + }; + + self.dap_store.update(cx, |store, cx| { + store.set_active_debug_line(&project_path, row, column, cx); + }); + + cx.spawn({ + let workspace = self.workspace.clone(); + move |_, mut cx| async move { + let task = workspace.update(&mut cx, |workspace, cx| { + workspace.open_path_preview(project_path, None, false, true, cx) + })?; + + let editor = task.await?.downcast::().unwrap(); + + workspace.update(&mut cx, |_, cx| { + editor.update(cx, |editor, cx| editor.go_to_active_debug_line(cx)) + }) + } + }) + .detach_and_log_err(cx); + } + fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { v_flex() .size_full() @@ -352,18 +444,8 @@ impl DebugPanelItem { }) .on_click(cx.listener({ let stack_frame_id = stack_frame.id; - let stack_frame = stack_frame.clone(); move |this, _, cx| { - this.update_stack_frame_id(stack_frame_id, cx); - - let workspace = this.workspace.clone(); - let stack_frame = stack_frame.clone(); - cx.spawn(|_, cx| async move { - DebugPanel::go_to_stack_frame(workspace, stack_frame, true, cx).await - }) - .detach_and_log_err(cx); - - cx.notify(); + this.update_stack_frame_id(stack_frame_id, true, cx); } })) .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bf31bf08985e21..029c9196ceac7d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -264,6 +264,7 @@ impl InlayId { } } +pub enum DebugCurrentRowHighlight {} enum DiffRowHighlight {} enum DocumentHighlightRead {} enum DocumentHighlightWrite {} @@ -1871,7 +1872,11 @@ impl Editor { None }; - let dap_store = project.as_ref().map(|project| project.read(cx).dap_store()); + let dap_store = if mode == EditorMode::Full { + project.as_ref().map(|project| project.read(cx).dap_store()) + } else { + None + }; let mut this = Self { focus_handle, @@ -2019,6 +2024,8 @@ impl Editor { this.git_blame_inline_enabled = true; this.start_git_blame_inline(false, cx); } + + this.go_to_active_debug_line(cx); } this.report_editor_event("open", None, cx); @@ -11305,6 +11312,31 @@ impl Editor { } } + pub fn go_to_active_debug_line(&mut self, cx: &mut ViewContext) { + let Some(dap_store) = self.dap_store.as_ref() else { + return; + }; + + let Some(buffer) = self.buffer.read(cx).as_singleton() else { + return; + }; + + let Some(project_path) = buffer.read_with(cx, |buffer, cx| buffer.project_path(cx)) else { + return; + }; + + if let Some((path, position)) = dap_store.read(cx).active_debug_line() { + if path == project_path { + self.go_to_line::( + position.row, + position.column, + Some(cx.theme().colors().editor_debugger_active_line_background), + cx, + ); + } + } + } + pub fn toggle_git_blame(&mut self, _: &ToggleGitBlame, cx: &mut ViewContext) { self.show_git_blame_gutter = !self.show_git_blame_gutter; diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 9bf5db93efae87..5fe503051beaa8 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -121,7 +121,7 @@ impl BufferStore { let buffer = self.get_by_path(&project_path, cx); if let Some(existing_buffer) = buffer { self.dap_store.update(cx, |store, cx| { - store.set_active_breakpoints(&project_path, &existing_buffer.read(cx)); + store.on_open_buffer(&project_path, &existing_buffer, cx); }); return Task::ready(Ok(existing_buffer)); } @@ -162,7 +162,7 @@ impl BufferStore { let buffer = load_result.map_err(Arc::new)?; this.dap_store.update(cx, |store, cx| { - store.set_active_breakpoints(&project_path, buffer.read(cx)); + store.on_open_buffer(&project_path, &buffer, cx); }); Ok(buffer) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 9f5a70acf0634d..6efad123312416 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -17,7 +17,7 @@ use dap::{ SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, }; -use gpui::{EventEmitter, ModelContext, Task}; +use gpui::{EventEmitter, Model, ModelContext, Task}; use language::{Buffer, BufferSnapshot}; use serde_json::Value; use settings::WorktreeId; @@ -49,11 +49,18 @@ pub enum DebugAdapterClientState { Running(Arc), } +#[derive(Clone, Debug)] +pub struct DebugPosition { + pub row: u32, + pub column: u32, +} + pub struct DapStore { next_client_id: AtomicUsize, clients: HashMap, breakpoints: BTreeMap>, capabilities: HashMap, + active_debug_line: Option<(ProjectPath, DebugPosition)>, } impl EventEmitter for DapStore {} @@ -63,9 +70,10 @@ impl DapStore { cx.on_app_quit(Self::shutdown_clients).detach(); Self { + active_debug_line: None, clients: Default::default(), - capabilities: HashMap::default(), breakpoints: Default::default(), + capabilities: HashMap::default(), next_client_id: Default::default(), } } @@ -108,20 +116,49 @@ impl DapStore { } } + pub fn active_debug_line(&self) -> Option<(ProjectPath, DebugPosition)> { + self.active_debug_line.clone() + } + + pub fn set_active_debug_line( + &mut self, + project_path: &ProjectPath, + row: u32, + column: u32, + cx: &mut ModelContext, + ) { + self.active_debug_line = Some((project_path.clone(), DebugPosition { row, column })); + + cx.notify(); + } + + pub fn remove_active_debug_line(&mut self) { + self.active_debug_line.take(); + } + pub fn breakpoints(&self) -> &BTreeMap> { &self.breakpoints } - pub fn set_active_breakpoints(&mut self, project_path: &ProjectPath, buffer: &Buffer) { + pub fn on_open_buffer( + &mut self, + project_path: &ProjectPath, + buffer: &Model, + cx: &mut ModelContext, + ) { let entry = self.breakpoints.remove(project_path).unwrap_or_default(); let mut set_bp: HashSet = HashSet::default(); + let buffer = buffer.read(cx); + for mut bp in entry.into_iter() { bp.set_active_position(&buffer); set_bp.insert(bp); } self.breakpoints.insert(project_path.clone(), set_bp); + + cx.notify(); } pub fn deserialize_breakpoints( From 171ddfb554285e6ea327514e1646bafead070b25 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 25 Sep 2024 19:15:04 +0200 Subject: [PATCH 266/650] Add stop debug adapter command --- crates/debugger_ui/src/lib.rs | 12 +++++++++++- crates/project/src/dap_store.rs | 10 ++++------ crates/workspace/src/workspace.rs | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 28048d0eb3b5fb..79ad1d36928719 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -3,7 +3,10 @@ use debugger_panel::{DebugPanel, ToggleFocus}; use gpui::AppContext; use settings::Settings; use ui::ViewContext; -use workspace::{Continue, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, Workspace}; +use workspace::{ + Continue, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, StopDebugAdapters, + Workspace, +}; mod console; pub mod debugger_panel; @@ -22,6 +25,13 @@ pub fn init(cx: &mut AppContext) { .register_action(|workspace: &mut Workspace, _: &Start, cx| { tasks_ui::toggle_modal(workspace, cx, task::TaskModal::DebugModal).detach(); }) + .register_action(|workspace: &mut Workspace, _: &StopDebugAdapters, cx| { + workspace.project().update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.shutdown_clients(cx).detach(); + }) + }) + }) .register_action(|workspace: &mut Workspace, _: &Stop, cx| { let debug_panel = workspace.panel::(cx).unwrap(); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 6efad123312416..f187fb94ca043e 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -23,7 +23,6 @@ use serde_json::Value; use settings::WorktreeId; use std::{ collections::BTreeMap, - future::Future, hash::{Hash, Hasher}, path::{Path, PathBuf}, sync::{ @@ -653,17 +652,16 @@ impl DapStore { }) } - fn shutdown_clients(&mut self, cx: &mut ModelContext) -> impl Future { + pub fn shutdown_clients(&mut self, cx: &mut ModelContext) -> Task<()> { let mut tasks = Vec::new(); - let client_ids = self.clients.keys().cloned().collect::>(); - for client_id in client_ids { + for client_id in self.clients.keys().cloned().collect::>() { tasks.push(self.shutdown_client(&client_id, cx)); } - async move { + cx.background_executor().spawn(async move { futures::future::join_all(tasks).await; - } + }) } pub fn shutdown_client( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f1aba57c3ca7c5..b07c8b43df2025 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -155,6 +155,7 @@ actions!( ReloadActiveItem, SaveAs, SaveWithoutFormat, + StopDebugAdapters, ToggleBottomDock, ToggleCenteredLayout, ToggleLeftDock, From 1914cef0aa9621e2ea1122ff2e1db70ac5b93b48 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 27 Sep 2024 11:11:53 -0400 Subject: [PATCH 267/650] Improve contrast for breakpoint & debug active line colors --- crates/theme/src/default_colors.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 6901423ca3ad3a..515f85b3ee9086 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -48,7 +48,7 @@ impl ThemeColors { icon_disabled: neutral().light().step_9(), icon_placeholder: neutral().light().step_10(), icon_accent: blue().light().step_11(), - debugger_accent: red().dark().step_3(), + debugger_accent: red().light().step_10(), status_bar_background: neutral().light().step_2(), title_bar_background: neutral().light().step_2(), title_bar_inactive_background: neutral().light().step_3(), @@ -72,7 +72,7 @@ impl ThemeColors { editor_subheader_background: neutral().light().step_2(), editor_active_line_background: neutral().light_alpha().step_3(), editor_highlighted_line_background: neutral().light_alpha().step_3(), - editor_debugger_active_line_background: neutral().light().step_8(), + editor_debugger_active_line_background: yellow().dark_alpha().step_3(), editor_line_number: neutral().light().step_10(), editor_active_line_number: neutral().light().step_11(), editor_invisible: neutral().light().step_10(), @@ -149,7 +149,7 @@ impl ThemeColors { icon_disabled: neutral().dark().step_9(), icon_placeholder: neutral().dark().step_10(), icon_accent: blue().dark().step_11(), - debugger_accent: red().dark().step_3(), + debugger_accent: red().light().step_10(), status_bar_background: neutral().dark().step_2(), title_bar_background: neutral().dark().step_2(), title_bar_inactive_background: neutral().dark().step_3(), @@ -172,8 +172,8 @@ impl ThemeColors { editor_gutter_background: neutral().dark().step_1(), editor_subheader_background: neutral().dark().step_3(), editor_active_line_background: neutral().dark_alpha().step_3(), - editor_highlighted_line_background: neutral().dark_alpha().step_4(), - editor_debugger_active_line_background: neutral().light_alpha().step_5(), + editor_highlighted_line_background: yellow().dark_alpha().step_4(), + editor_debugger_active_line_background: yellow().dark_alpha().step_3(), editor_line_number: neutral().dark_alpha().step_10(), editor_active_line_number: neutral().dark_alpha().step_12(), editor_invisible: neutral().dark_alpha().step_4(), From 842bf0287d46530b90cf85c6c75ea0ededf4830e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 4 Oct 2024 09:00:45 +0200 Subject: [PATCH 268/650] Debug console (#43) * Use buffer settings for font, size etc. * Trim end of message * By default send the output values to the output editor * WIP send evaluate request * Rename variable * Add result to console from evaluate response * Remove not needed arc * Remove store capabilities on variable_list * Specify the capacity for the task vec * Add placeholder * WIP add completion provider for existing variables * Add value to auto completion label * Make todo for debugger * Specify the capacity of the vec's * Make clippy happy * Remove not needed notifies and add missing one * WIP move scopes and variables to variable_list * Rename configuration done method * Add support for adapter completions and manual variable completions * Move type to variabel_list * Change update to read * Show debug panel when debug session stops Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Co-Authored-By: Mikayla Maki * Also use scope reference to determine to which where the set value editor should display * Refetch existing variables after * Rebuild entries after refetching the variables --------- Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Co-authored-by: Mikayla Maki --- Cargo.lock | 3 + crates/debugger_ui/Cargo.toml | 3 + crates/debugger_ui/src/console.rs | 291 ++++++++++++++++- crates/debugger_ui/src/debugger_panel.rs | 97 +----- crates/debugger_ui/src/debugger_panel_item.rs | 35 +- crates/debugger_ui/src/variable_list.rs | 302 ++++++++++++------ crates/project/src/dap_store.rs | 74 ++++- 7 files changed, 585 insertions(+), 220 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8f71f10ed2d8a..2ce61133fd295d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3487,9 +3487,12 @@ dependencies = [ "dap", "editor", "futures 0.3.30", + "fuzzy", "gpui", + "language", "log", "menu", + "parking_lot", "project", "serde", "settings", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 606afd370d908d..1e77ec8af4ec97 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -13,9 +13,12 @@ anyhow.workspace = true dap.workspace = true editor.workspace = true futures.workspace = true +fuzzy.workspace = true gpui.workspace = true +language.workspace = true log.workspace = true menu.workspace = true +parking_lot.workspace = true project.workspace = true serde.workspace = true settings.workspace = true diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index b26a2491783f83..c4d8dab4f272b6 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -1,39 +1,121 @@ -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{Render, TextStyle, View, ViewContext}; +use crate::variable_list::VariableList; +use dap::client::DebugAdapterClientId; +use editor::{CompletionProvider, Editor, EditorElement, EditorStyle}; +use fuzzy::StringMatchCandidate; +use gpui::{Model, Render, Task, TextStyle, View, ViewContext, WeakView}; +use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16}; +use menu::Confirm; +use parking_lot::RwLock; +use project::{dap_store::DapStore, Completion}; use settings::Settings; +use std::{collections::HashMap, sync::Arc}; use theme::ThemeSettings; use ui::prelude::*; pub struct Console { console: View, query_bar: View, + dap_store: Model, + current_stack_frame_id: u64, + client_id: DebugAdapterClientId, + variable_list: View, } impl Console { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new( + client_id: &DebugAdapterClientId, + current_stack_frame_id: u64, + variable_list: View, + dap_store: Model, + cx: &mut ViewContext, + ) -> Self { let console = cx.new_view(|cx| { let mut editor = Editor::multi_line(cx); editor.move_to_end(&editor::actions::MoveToEnd, cx); editor.set_read_only(true); editor.set_show_gutter(false, cx); + editor.set_use_autoclose(false); + editor.set_show_wrap_guides(false, cx); + editor.set_show_indent_guides(false, cx); editor.set_show_inline_completions(Some(false), cx); editor }); - let query_bar = cx.new_view(Editor::single_line); + let this = cx.view().downgrade(); + let query_bar = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("Evaluate an expression", cx); + editor.set_use_autoclose(false); + editor.set_show_gutter(false, cx); + editor.set_show_wrap_guides(false, cx); + editor.set_show_indent_guides(false, cx); + editor.set_completion_provider(Box::new(ConsoleQueryBarCompletionProvider(this))); + + editor + }); + + Self { + console, + dap_store, + query_bar, + variable_list, + client_id: *client_id, + current_stack_frame_id, + } + } + + pub fn update_current_stack_frame_id( + &mut self, + stack_frame_id: u64, + cx: &mut ViewContext, + ) { + self.current_stack_frame_id = stack_frame_id; - Self { console, query_bar } + cx.notify(); } pub fn add_message(&mut self, message: &str, cx: &mut ViewContext) { self.console.update(cx, |console, cx| { console.set_read_only(false); console.move_to_end(&editor::actions::MoveToEnd, cx); - console.insert(format!("{}\n", message).as_str(), cx); + console.insert(format!("{}\n", message.trim_end()).as_str(), cx); console.set_read_only(true); }); + } - cx.notify(); + fn evaluate(&mut self, _: &Confirm, cx: &mut ViewContext) { + let expession = self.query_bar.update(cx, |editor, cx| { + let expession = editor.text(cx); + + editor.clear(cx); + + expession + }); + + let evaluate_task = self.dap_store.update(cx, |store, cx| { + store.evaluate( + &self.client_id, + self.current_stack_frame_id, + expession, + dap::EvaluateArgumentsContext::Variables, + cx, + ) + }); + + cx.spawn(|this, mut cx| async move { + let response = evaluate_task.await?; + + this.update(&mut cx, |console, cx| { + console.add_message(&response.result, cx); + + console.variable_list.update(cx, |variable_list, cx| { + variable_list + .refetch_existing_variables(cx) + .detach_and_log_err(cx); + }) + }) + }) + .detach_and_log_err(cx); } fn render_console(&self, cx: &ViewContext) -> impl IntoElement { @@ -46,9 +128,9 @@ impl Console { }, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), - font_size: rems(0.875).into(), + font_size: settings.buffer_font_size.into(), font_weight: settings.buffer_font.weight, - line_height: relative(1.3), + line_height: relative(settings.buffer_line_height.value()), ..Default::default() }; @@ -71,10 +153,11 @@ impl Console { } else { cx.theme().colors().text }, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), + font_size: TextSize::Editor.rems(cx).into(), + font_weight: settings.ui_font.weight, line_height: relative(1.3), ..Default::default() }; @@ -94,6 +177,8 @@ impl Console { impl Render for Console { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() + .key_context("DebugConsole") + .on_action(cx.listener(Self::evaluate)) .size_full() .child(self.render_console(cx)) .child( @@ -104,3 +189,183 @@ impl Render for Console { .border_2() } } + +struct ConsoleQueryBarCompletionProvider(WeakView); + +impl CompletionProvider for ConsoleQueryBarCompletionProvider { + fn completions( + &self, + buffer: &Model, + buffer_position: language::Anchor, + _trigger: editor::CompletionContext, + cx: &mut ViewContext, + ) -> gpui::Task>> { + let Some(console) = self.0.upgrade() else { + return Task::ready(Ok(Vec::new())); + }; + + let support_completions = console.update(cx, |this, cx| { + this.dap_store + .read(cx) + .capabilities_by_id(&this.client_id) + .supports_completions_request + .unwrap_or_default() + }); + + if support_completions { + self.client_completions(&console, buffer, buffer_position, cx) + } else { + self.variable_list_completions(&console, buffer, buffer_position, cx) + } + } + + fn resolve_completions( + &self, + _buffer: Model, + _completion_indices: Vec, + _completions: Arc>>, + _cx: &mut ViewContext, + ) -> gpui::Task> { + Task::ready(Ok(false)) + } + + fn apply_additional_edits_for_completion( + &self, + _buffer: Model, + _completion: project::Completion, + _push_to_history: bool, + _cx: &mut ViewContext, + ) -> gpui::Task>> { + Task::ready(Ok(None)) + } + + fn is_completion_trigger( + &self, + _buffer: &Model, + _position: language::Anchor, + _text: &str, + _trigger_in_words: bool, + _cx: &mut ViewContext, + ) -> bool { + true + } +} + +impl ConsoleQueryBarCompletionProvider { + fn variable_list_completions( + &self, + console: &View, + buffer: &Model, + buffer_position: language::Anchor, + cx: &mut ViewContext, + ) -> gpui::Task>> { + let (variables, string_matches) = console.update(cx, |console, cx| { + let mut variables = HashMap::new(); + let mut string_matches = Vec::new(); + + for variable in console.variable_list.read(cx).variables() { + if let Some(evaluate_name) = &variable.variable.evaluate_name { + variables.insert(evaluate_name.clone(), variable.variable.value.clone()); + string_matches.push(StringMatchCandidate { + id: 0, + string: evaluate_name.clone(), + char_bag: evaluate_name.chars().collect(), + }); + } + + variables.insert( + variable.variable.name.clone(), + variable.variable.value.clone(), + ); + + string_matches.push(StringMatchCandidate { + id: 0, + string: variable.variable.name.clone(), + char_bag: variable.variable.name.chars().collect(), + }); + } + + (variables, string_matches) + }); + + let query = buffer.read(cx).text(); + let start_position = buffer.read(cx).anchor_before(0); + + cx.spawn(|_, cx| async move { + let matches = fuzzy::match_strings( + &string_matches, + &query, + true, + 10, + &Default::default(), + cx.background_executor().clone(), + ) + .await; + + Ok(matches + .iter() + .filter_map(|string_match| { + let variable_value = variables.get(&string_match.string)?; + + Some(project::Completion { + old_range: start_position..buffer_position, + new_text: string_match.string.clone(), + label: CodeLabel { + filter_range: 0..string_match.string.len(), + text: format!("{} {}", string_match.string.clone(), variable_value), + runs: Vec::new(), + }, + server_id: LanguageServerId(0), // TODO debugger: read from client + documentation: None, + lsp_completion: Default::default(), + confirm: None, + }) + }) + .collect()) + }) + } + + fn client_completions( + &self, + console: &View, + buffer: &Model, + buffer_position: language::Anchor, + cx: &mut ViewContext, + ) -> gpui::Task>> { + let text = buffer.read(cx).text(); + let start_position = buffer.read(cx).anchor_before(0); + let snapshot = buffer.read(cx).snapshot(); + + let completion_task = console.update(cx, |console, cx| { + console.dap_store.update(cx, |store, cx| { + store.completions( + &console.client_id, + console.current_stack_frame_id, + text, + buffer_position.to_offset_utf16(&snapshot).0 as u64, + cx, + ) + }) + }); + + cx.background_executor().spawn(async move { + Ok(completion_task + .await? + .iter() + .map(|completion| project::Completion { + old_range: start_position..buffer_position, + new_text: completion.text.clone().unwrap_or(completion.label.clone()), + label: CodeLabel { + filter_range: 0..completion.label.len(), + text: completion.label.clone(), + runs: Vec::new(), + }, + server_id: LanguageServerId(0), // TODO debugger: read from client + documentation: None, + lsp_completion: Default::default(), + confirm: None, + }) + .collect()) + }) + } +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 63950610863c88..a1d1ed9aab4d0f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -6,17 +6,16 @@ use dap::debugger_settings::DebuggerSettings; use dap::messages::{Events, Message}; use dap::requests::{Request, StartDebugging}; use dap::{ - Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, Scope, StackFrame, StoppedEvent, - TerminatedEvent, ThreadEvent, ThreadEventReason, Variable, + Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, StackFrame, StoppedEvent, + TerminatedEvent, ThreadEvent, ThreadEventReason, }; -use futures::future::try_join_all; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, FontWeight, Model, Subscription, Task, View, ViewContext, WeakView, }; use project::dap_store::DapStore; use settings::Settings; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::BTreeMap; use std::sync::Arc; use std::u64; use ui::prelude::*; @@ -42,22 +41,10 @@ pub enum DebugPanelEvent { actions!(debug_panel, [ToggleFocus]); -#[derive(Debug, Clone)] -pub struct VariableContainer { - pub container_reference: u64, - pub variable: Variable, - pub depth: usize, -} - #[derive(Debug, Default, Clone)] pub struct ThreadState { pub status: ThreadStatus, pub stack_frames: Vec, - /// HashMap> - pub scopes: HashMap>, - /// BTreeMap> - pub variables: BTreeMap>, - pub fetched_variable_ids: HashSet, // we update this value only once we stopped, // we will use this to indicated if we should show a warning when debugger thread was exited pub stopped: bool, @@ -287,9 +274,9 @@ impl DebugPanel { .update(cx, |project, cx| project.send_breakpoints(&client_id, cx)) }); - let configuration_done_task = self.dap_store.update(cx, |store, cx| { - store.send_configuration_done(&client_id, cx) - }); + let configuration_done_task = self + .dap_store + .update(cx, |store, cx| store.configuration_done(&client_id, cx)); cx.background_executor() .spawn(async move { @@ -343,41 +330,6 @@ impl DebugPanel { let current_stack_frame = stack_frames.first().unwrap().clone(); - let mut scope_tasks = Vec::new(); - for stack_frame in stack_frames.clone().into_iter() { - let stack_frame_scopes_task = this.update(&mut cx, |this, cx| { - this.dap_store - .update(cx, |store, cx| store.scopes(&client_id, stack_frame.id, cx)) - }); - - scope_tasks.push(async move { - anyhow::Ok((stack_frame.id, stack_frame_scopes_task?.await?)) - }); - } - - let mut stack_frame_tasks = Vec::new(); - for (stack_frame_id, scopes) in try_join_all(scope_tasks).await? { - let variable_tasks = this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - let mut tasks = Vec::new(); - - for scope in scopes { - let variables_task = - store.variables(&client_id, scope.variables_reference, cx); - tasks.push( - async move { anyhow::Ok((scope, variables_task.await?)) }, - ); - } - - tasks - }) - })?; - - stack_frame_tasks.push(async move { - anyhow::Ok((stack_frame_id, try_join_all(variable_tasks).await?)) - }); - } - let thread_state = this.update(&mut cx, |this, cx| { this.thread_states .entry((client_id, thread_id)) @@ -385,33 +337,7 @@ impl DebugPanel { .clone() })?; - for (stack_frame_id, scopes) in try_join_all(stack_frame_tasks).await? { - thread_state.update(&mut cx, |thread_state, _| { - thread_state - .scopes - .insert(stack_frame_id, scopes.iter().map(|s| s.0.clone()).collect()); - - for (scope, variables) in scopes { - thread_state - .fetched_variable_ids - .insert(scope.variables_reference); - - thread_state.variables.insert( - scope.variables_reference, - variables - .into_iter() - .map(|v| VariableContainer { - container_reference: scope.variables_reference, - variable: v, - depth: 1, - }) - .collect::>(), - ); - } - })?; - } - - this.update(&mut cx, |this, cx| { + let workspace = this.update(&mut cx, |this, cx| { thread_state.update(cx, |thread_state, cx| { thread_state.stack_frames = stack_frames; thread_state.status = ThreadStatus::Stopped; @@ -455,7 +381,6 @@ impl DebugPanel { let go_to_stack_frame = if let Some(item) = this.pane.read(cx).active_item() { item.downcast::().map_or(false, |pane| { let pane = pane.read(cx); - pane.thread_id() == thread_id && pane.client_id() == client_id }) } else { @@ -469,6 +394,14 @@ impl DebugPanel { }); cx.notify(); + + this.workspace.clone() + })?; + + cx.update(|cx| { + workspace.update(cx, |workspace, cx| { + workspace.focus_panel::(cx); + }) }) } }) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 8337511f070528..2ccf3ffd0c997b 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -67,19 +67,24 @@ impl DebugPanelItem { ) -> Self { let focus_handle = cx.focus_handle(); - let capabilities = dap_store.read(cx).capabilities_by_id(&client_id); - let variable_list = cx.new_view(|cx| { VariableList::new( dap_store.clone(), &client_id, &thread_state, - &capabilities, current_stack_frame_id, cx, ) }); - let console = cx.new_view(Console::new); + let console = cx.new_view(|cx| { + Console::new( + client_id, + current_stack_frame_id, + variable_list.clone(), + dap_store.clone(), + cx, + ) + }); let weakview = cx.view().downgrade(); let stack_frame_list = @@ -204,6 +209,10 @@ impl DebugPanelItem { self.update_stack_frame_id(stack_frame.id, go_to_stack_frame, cx); }; + self.variable_list.update(cx, |variable_list, cx| { + variable_list.on_stopped_event(cx); + }); + cx.notify(); } @@ -243,20 +252,6 @@ impl DebugPanelItem { console.add_message(&event.output, cx); }); } - // OutputEventCategory::Stderr => {} - OutputEventCategory::Stdout => { - self.output_editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.move_to_end(&editor::actions::MoveToEnd, cx); - editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); - editor.set_read_only(true); - - cx.notify(); - }); - } - // OutputEventCategory::Unknown => {} - // OutputEventCategory::Important => {} - OutputEventCategory::Telemetry => {} _ => { self.output_editor.update(cx, |editor, cx| { editor.set_read_only(false); @@ -328,6 +323,10 @@ impl DebugPanelItem { variable_list.build_entries(true, false, cx); }); + self.console.update(cx, |console, cx| { + console.update_current_stack_frame_id(stack_frame_id, cx); + }); + if go_to_stack_frame { self.go_to_stack_frame(cx); } diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 2ef9220dcfa9c5..80c4857e24be1f 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,5 +1,6 @@ -use crate::debugger_panel::{ThreadState, VariableContainer}; -use dap::{client::DebugAdapterClientId, Capabilities, Scope, Variable}; +use crate::debugger_panel::ThreadState; +use anyhow::Result; +use dap::{client::DebugAdapterClientId, Scope, Variable}; use editor::{ actions::{self, SelectAll}, Editor, EditorEvent, @@ -7,13 +8,23 @@ use editor::{ use futures::future::try_join_all; use gpui::{ anchored, deferred, list, AnyElement, ClipboardItem, DismissEvent, FocusHandle, FocusableView, - ListState, Model, MouseDownEvent, Point, Subscription, View, + ListState, Model, MouseDownEvent, Point, Subscription, Task, View, }; use menu::Confirm; use project::dap_store::DapStore; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::Arc, +}; use ui::{prelude::*, ContextMenu, ListItem}; +#[derive(Debug, Clone)] +pub struct VariableContainer { + pub container_reference: u64, + pub variable: Variable, + pub depth: usize, +} + #[derive(Debug, Clone)] pub struct SetVariableState { name: String, @@ -33,7 +44,7 @@ pub enum VariableListEntry { }, Variable { depth: usize, - scope: Scope, + scope: Arc, variable: Arc, has_children: bool, container_reference: u64, @@ -42,16 +53,20 @@ pub enum VariableListEntry { pub struct VariableList { list: ListState, - stack_frame_id: u64, dap_store: Model, focus_handle: FocusHandle, - capabilities: Capabilities, + current_stack_frame_id: u64, client_id: DebugAdapterClientId, open_entries: Vec, + scopes: HashMap>, thread_state: Model, set_variable_editor: View, + fetched_variable_ids: HashSet, set_variable_state: Option, entries: HashMap>, + fetch_variables_task: Option>>, + // (stack_frame_id, scope.variables_reference) -> variables + variables: BTreeMap<(u64, u64), Vec>, open_context_menu: Option<(View, Point, Subscription)>, } @@ -60,8 +75,7 @@ impl VariableList { dap_store: Model, client_id: &DebugAdapterClientId, thread_state: &Model, - capabilities: &Capabilities, - stack_frame_id: u64, + current_stack_frame_id: u64, cx: &mut ViewContext, ) -> Self { let weakview = cx.view().downgrade(); @@ -90,20 +104,30 @@ impl VariableList { list, dap_store, focus_handle, - stack_frame_id, set_variable_editor, client_id: *client_id, + current_stack_frame_id, open_context_menu: None, set_variable_state: None, + fetch_variables_task: None, + scopes: Default::default(), entries: Default::default(), + variables: Default::default(), open_entries: Default::default(), thread_state: thread_state.clone(), - capabilities: capabilities.clone(), + fetched_variable_ids: Default::default(), } } + pub fn variables(&self) -> Vec { + self.variables + .range((self.current_stack_frame_id, u64::MIN)..(self.current_stack_frame_id, u64::MAX)) + .flat_map(|(_, containers)| containers.iter().cloned()) + .collect() + } + fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let Some(entries) = self.entries.get(&self.stack_frame_id) else { + let Some(entries) = self.entries.get(&self.current_stack_frame_id) else { return div().into_any_element(); }; @@ -144,7 +168,7 @@ impl VariableList { } pub fn update_stack_frame_id(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { - self.stack_frame_id = stack_frame_id; + self.current_stack_frame_id = stack_frame_id; cx.notify(); } @@ -155,9 +179,7 @@ impl VariableList { keep_open_entries: bool, cx: &mut ViewContext, ) { - let thread_state = self.thread_state.read(cx); - - let Some(scopes) = thread_state.scopes.get(&self.stack_frame_id) else { + let Some(scopes) = self.scopes.get(&self.current_stack_frame_id) else { return; }; @@ -167,7 +189,10 @@ impl VariableList { let mut entries: Vec = Vec::default(); for scope in scopes { - let Some(variables) = thread_state.variables.get(&scope.variables_reference) else { + let Some(variables) = self + .variables + .get(&(self.current_stack_frame_id, scope.variables_reference)) + else { continue; }; @@ -215,6 +240,7 @@ impl VariableList { if let Some(state) = self.set_variable_state.as_ref() { if state.parent_variables_reference == container_reference + && state.scope.variables_reference == scope.variables_reference && state.name == variable.name { entries.push(VariableListEntry::SetVariableEditor { @@ -226,7 +252,7 @@ impl VariableList { entries.push(VariableListEntry::Variable { depth, - scope: scope.clone(), + scope: Arc::new(scope.clone()), variable: Arc::new(variable.clone()), has_children: variable.variables_reference > 0, container_reference, @@ -235,12 +261,88 @@ impl VariableList { } let len = entries.len(); - self.entries.insert(self.stack_frame_id, entries); + self.entries.insert(self.current_stack_frame_id, entries); self.list.reset(len); cx.notify(); } + pub fn on_stopped_event(&mut self, cx: &mut ViewContext) { + let stack_frames = self.thread_state.read(cx).stack_frames.clone(); + + self.fetch_variables_task.take(); + self.variables.clear(); + self.scopes.clear(); + self.fetched_variable_ids.clear(); + + self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { + let mut scope_tasks = Vec::with_capacity(stack_frames.len()); + for stack_frame in stack_frames.clone().into_iter() { + let stack_frame_scopes_task = this.update(&mut cx, |this, cx| { + this.dap_store.update(cx, |store, cx| { + store.scopes(&this.client_id, stack_frame.id, cx) + }) + }); + + scope_tasks.push(async move { + anyhow::Ok((stack_frame.id, stack_frame_scopes_task?.await?)) + }); + } + + let mut stack_frame_tasks = Vec::with_capacity(scope_tasks.len()); + for (stack_frame_id, scopes) in try_join_all(scope_tasks).await? { + let variable_tasks = this.update(&mut cx, |this, cx| { + this.dap_store.update(cx, |store, cx| { + let mut tasks = Vec::with_capacity(scopes.len()); + + for scope in scopes { + let variables_task = + store.variables(&this.client_id, scope.variables_reference, cx); + tasks.push(async move { anyhow::Ok((scope, variables_task.await?)) }); + } + + tasks + }) + })?; + + stack_frame_tasks.push(async move { + anyhow::Ok((stack_frame_id, try_join_all(variable_tasks).await?)) + }); + } + + for (stack_frame_id, scopes) in try_join_all(stack_frame_tasks).await? { + this.update(&mut cx, |this, _| { + for (scope, variables) in scopes { + this.scopes + .entry(stack_frame_id) + .or_default() + .push(scope.clone()); + + this.fetched_variable_ids.insert(scope.variables_reference); + + this.variables.insert( + (stack_frame_id, scope.variables_reference), + variables + .into_iter() + .map(|v| VariableContainer { + container_reference: scope.variables_reference, + variable: v, + depth: 1, + }) + .collect::>(), + ); + } + })?; + } + + this.update(&mut cx, |this, cx| { + this.build_entries(true, false, cx); + + this.fetch_variables_task.take(); + }) + })); + } + fn deploy_variable_context_menu( &mut self, parent_variables_reference: u64, @@ -251,8 +353,13 @@ impl VariableList { ) { let this = cx.view().clone(); - let stack_frame_id = self.stack_frame_id; - let support_set_variable = self.capabilities.supports_set_variable.unwrap_or_default(); + let stack_frame_id = self.current_stack_frame_id; + let support_set_variable = self.dap_store.read_with(cx, |store, _| { + store + .capabilities_by_id(&self.client_id) + .supports_set_variable + .unwrap_or_default() + }); let context_menu = ContextMenu::build(cx, |menu, cx| { menu.entry( @@ -340,13 +447,13 @@ impl VariableList { return cx.notify(); }; - if new_variable_value == state.value || state.stack_frame_id != self.stack_frame_id { + if new_variable_value == state.value || state.stack_frame_id != self.current_stack_frame_id + { return cx.notify(); } let client_id = self.client_id; let variables_reference = state.parent_variables_reference; - let scope = state.scope; let name = state.name; let evaluate_name = state.evaluate_name; let stack_frame_id = state.stack_frame_id; @@ -368,61 +475,67 @@ impl VariableList { set_value_task?.await?; - let Some(scope_variables) = this.update(&mut cx, |this, cx| { - this.thread_state.update(cx, |thread_state, _| { - thread_state.variables.remove(&scope.variables_reference) - }) - })? - else { - return Ok(()); - }; + this.update(&mut cx, |this, cx| this.refetch_existing_variables(cx))? + .await?; - let tasks = this.update(&mut cx, |this, cx| { - let mut tasks = Vec::new(); + this.update(&mut cx, |this, cx| { + this.build_entries(false, true, cx); + }) + }) + .detach_and_log_err(cx); + } - for variable_container in scope_variables { - let fetch_variables_task = this.dap_store.update(cx, |store, cx| { - store.variables(&client_id, variable_container.container_reference, cx) - }); + pub fn refetch_existing_variables(&mut self, cx: &mut ViewContext) -> Task> { + let mut scope_tasks = Vec::with_capacity(self.variables.len()); + + for ((stack_frame_id, scope_id), variable_containers) in self.variables.clone().into_iter() + { + let mut variable_tasks = Vec::with_capacity(variable_containers.len()); - tasks.push(async move { - let depth = variable_container.depth; - let container_reference = variable_container.container_reference; + for variable_container in variable_containers { + let fetch_variables_task = self.dap_store.update(cx, |store, cx| { + store.variables(&self.client_id, variable_container.container_reference, cx) + }); - anyhow::Ok( - fetch_variables_task - .await? - .into_iter() - .map(move |variable| VariableContainer { - container_reference, - variable, - depth, - }) - .collect::>(), - ) - }); - } + variable_tasks.push(async move { + let depth = variable_container.depth; + let container_reference = variable_container.container_reference; + + anyhow::Ok( + fetch_variables_task + .await? + .into_iter() + .map(move |variable| VariableContainer { + container_reference, + variable, + depth, + }) + .collect::>(), + ) + }); + } - tasks - })?; + scope_tasks.push(async move { + anyhow::Ok(( + (stack_frame_id, scope_id), + try_join_all(variable_tasks).await?, + )) + }); + } - let updated_variables = try_join_all(tasks).await?; + cx.spawn(|this, mut cx| async move { + let updated_variables = try_join_all(scope_tasks).await?; this.update(&mut cx, |this, cx| { - this.thread_state.update(cx, |thread_state, cx| { - for variables in updated_variables { - thread_state - .variables - .insert(scope.variables_reference, variables); + for (entry_id, variable_containers) in updated_variables { + for variables in variable_containers { + this.variables.insert(entry_id, variables); } - - cx.notify(); - }); + } this.build_entries(false, true, cx); }) }) - .detach_and_log_err(cx); } fn render_set_variable_editor( @@ -459,17 +572,11 @@ impl VariableList { // if we already opened the variable/we already fetched it // we can just toggle it because we already have the nested variable - if disclosed.unwrap_or(true) - || self - .thread_state - .read(cx) - .fetched_variable_ids - .contains(&variable_reference) - { + if disclosed.unwrap_or(true) || self.fetched_variable_ids.contains(&variable_reference) { return self.toggle_entry_collapsed(&variable_id, cx); } - let Some(entries) = self.entries.get(&self.stack_frame_id) else { + let Some(entries) = self.entries.get(&self.current_stack_frame_id) else { return; }; @@ -481,6 +588,7 @@ impl VariableList { let variable_id = variable_id.clone(); let scope = scope.clone(); let depth = *depth; + let stack_frame_id = self.current_stack_frame_id; let fetch_variables_task = self.dap_store.update(cx, |store, cx| { store.variables(&self.client_id, variable_reference, cx) @@ -490,34 +598,32 @@ impl VariableList { let new_variables = fetch_variables_task.await?; this.update(&mut cx, |this, cx| { - this.thread_state.update(cx, |thread_state, cx| { - let Some(variables) = - thread_state.variables.get_mut(&scope.variables_reference) - else { - return; - }; - - let position = variables.iter().position(|v| { - variable_entry_id(&scope, &v.variable, v.depth) == variable_id - }); + let Some(variables) = this + .variables + .get_mut(&(stack_frame_id, scope.variables_reference)) + else { + return; + }; + + let position = variables.iter().position(|v| { + variable_entry_id(&scope, &v.variable, v.depth) == variable_id + }); - if let Some(position) = position { - variables.splice( - position + 1..position + 1, - new_variables.clone().into_iter().map(|variable| { - VariableContainer { - container_reference: variable_reference, - variable, - depth: depth + 1, - } + if let Some(position) = position { + variables.splice( + position + 1..position + 1, + new_variables + .clone() + .into_iter() + .map(|variable| VariableContainer { + container_reference: variable_reference, + variable, + depth: depth + 1, }), - ); + ); - thread_state.fetched_variable_ids.insert(variable_reference); - } - - cx.notify(); - }); + this.fetched_variable_ids.insert(variable_reference); + } this.toggle_entry_collapsed(&variable_id, cx); }) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index f187fb94ca043e..3a07bd267e300f 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -4,16 +4,17 @@ use collections::{HashMap, HashSet}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::Message; use dap::requests::{ - Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Scopes, - SetBreakpoints, SetExpression, SetVariable, StackTrace, StepIn, StepOut, Terminate, - TerminateThreads, Variables, + Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, + Next, Pause, Scopes, SetBreakpoints, SetExpression, SetVariable, StackTrace, StepIn, StepOut, + Terminate, TerminateThreads, Variables, }; use dap::{ - AttachRequestArguments, Capabilities, ConfigurationDoneArguments, ContinueArguments, - DisconnectArguments, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, - LaunchRequestArguments, NextArguments, PauseArguments, Scope, ScopesArguments, - SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, - SourceBreakpoint, StackFrame, StackTraceArguments, StepInArguments, StepOutArguments, + AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, + ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, EvaluateArguments, + EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, + InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, + Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, + Source, SourceBreakpoint, StackFrame, StackTraceArguments, StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, }; @@ -344,7 +345,7 @@ impl DapStore { }) } - pub fn send_configuration_done( + pub fn configuration_done( &self, client_id: &DebugAdapterClientId, cx: &mut ModelContext, @@ -510,6 +511,58 @@ impl DapStore { }) } + pub fn evaluate( + &self, + client_id: &DebugAdapterClientId, + stack_frame_id: u64, + expression: String, + context: EvaluateArgumentsContext, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|_, _| async move { + client + .request::(EvaluateArguments { + expression: expression.clone(), + frame_id: Some(stack_frame_id), + context: Some(context), + format: None, + line: None, + column: None, + source: None, + }) + .await + }) + } + + pub fn completions( + &self, + client_id: &DebugAdapterClientId, + stack_frame_id: u64, + text: String, + completion_column: u64, + cx: &mut ModelContext, + ) -> Task>> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|_, _| async move { + Ok(client + .request::(CompletionsArguments { + frame_id: Some(stack_frame_id), + line: None, + text, + column: completion_column, + }) + .await? + .targets) + }) + } + #[allow(clippy::too_many_arguments)] pub fn set_variable_value( &self, @@ -722,6 +775,8 @@ impl DapStore { breakpoint_set.insert(breakpoint); } + cx.notify(); + self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) .detach(); } @@ -797,6 +852,7 @@ impl DapStore { }) } } + type LogMessage = Arc; #[derive(Clone, Debug, Eq, PartialEq, Hash)] From 6b4ebac82236be60608804a5e44d579d4521a2df Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 4 Oct 2024 20:10:49 +0200 Subject: [PATCH 269/650] Fix current debug line highlight did not work Co-authored-by: Anthony Eid --- crates/editor/src/editor.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c358d6a7d8b3bb..84ae287e122ccc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9768,13 +9768,16 @@ impl Editor { cx: &mut ViewContext, ) { let snapshot = self.snapshot(cx).display_snapshot; - let point = snapshot + let start = snapshot .buffer_snapshot .clip_point(Point::new(row, column), Bias::Left); - let anchor = snapshot.buffer_snapshot.anchor_before(point); + let end = start + Point::new(1, 0); + let start = snapshot.buffer_snapshot.anchor_before(start); + let end = snapshot.buffer_snapshot.anchor_before(end); + self.clear_row_highlights::(); self.highlight_rows::( - anchor..anchor, + start..end, highlight_color .unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background), true, From 93af1bfae7007a8a636c26b89c7fd03903ec7a04 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:16:04 -0400 Subject: [PATCH 270/650] Breakpoint prompt editor (#44) * Create basic breakpoint prompt editor structure * Get breakpoint prompt to properly render * Fix bug where toggle breakpoint failed to work This bug occurs when a breakpoint anchor position is moved from the begining of a line. This causes dap_store.breakpoint hashmap to fail to properly get the correct element, thus toggling the wrong breakpoint. The fix to this bug is passing a breakpoint anchor to an editor's display map and to the render breakpoint function. Instead of creating a new anchor when clicking on a breakpoint icon, zed will use the breakpoint anchor passed to the display map. In the case of using toggle breakpoint action, zed will iterate through all breakpoints in that buffer to check if any are on the cursor's line number, then use anchor if found. Otherwise, zed creates a new anchor. * Fix bug where breakpoint icon overlaps with other icons This bug happened when an icon {code action | code runner} was rendered on the same line of a breakpoint where that breakpoint's anchor was not at the start of the line * Get breakpoint prompt to add log breakpoint's correctly * Clean up breakpoint prompt UI & allow editting of log messages --------- Co-authored-by: Remco Smits --- Cargo.lock | 67 +++--- crates/debugger_ui/src/console.rs | 8 +- crates/editor/Cargo.toml | 1 + crates/editor/src/display_map.rs | 11 + crates/editor/src/editor.rs | 272 ++++++++++++++++++++++-- crates/editor/src/element.rs | 100 ++++----- crates/multi_buffer/src/multi_buffer.rs | 10 + crates/project/src/dap_store.rs | 79 +++++-- crates/project/src/project.rs | 4 +- crates/text/src/anchor.rs | 8 - crates/text/src/text.rs | 12 ++ 11 files changed, 435 insertions(+), 137 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac5056a01bd983..2ad8d7bc552934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,9 +893,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", @@ -2085,9 +2085,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.20.5" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88da5a13c620b4ca0078845707ea9c3faf11edbc3ffd8497d11d686211cd1ac0" +checksum = "ad639525b1c67b6a298f378417b060fbc04618bea559482a8484381cce27d965" dependencies = [ "serde", "toml 0.8.19", @@ -2275,9 +2275,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -2285,9 +2285,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -2307,9 +2307,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3740,6 +3740,7 @@ dependencies = [ "log", "lsp", "markdown", + "menu", "multi_buffer", "ordered-float 2.10.1", "parking_lot", @@ -6499,9 +6500,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libdbus-sys" @@ -7911,9 +7912,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -8421,9 +8422,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.4.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", "yansi", @@ -10238,9 +10239,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "indexmap 2.4.0", "itoa", @@ -10781,9 +10782,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.6" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ "nom", "unicode_categories", @@ -11618,12 +11619,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix 0.38.35", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -11748,18 +11749,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -12074,9 +12075,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes 1.7.1", "futures-core", @@ -13218,9 +13219,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" dependencies = [ "futures-util", "js-sys", @@ -14573,9 +14574,9 @@ dependencies = [ [[package]] name = "yansi" -version = "1.0.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "yazi" diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index c4d8dab4f272b6..6b87205da65dd6 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -84,19 +84,19 @@ impl Console { } fn evaluate(&mut self, _: &Confirm, cx: &mut ViewContext) { - let expession = self.query_bar.update(cx, |editor, cx| { - let expession = editor.text(cx); + let expression = self.query_bar.update(cx, |editor, cx| { + let expression = editor.text(cx); editor.clear(cx); - expession + expression }); let evaluate_task = self.dap_store.update(cx, |store, cx| { store.evaluate( &self.client_id, self.current_stack_frame_id, - expession, + expression, dap::EvaluateArgumentsContext::Variables, cx, ) diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index cfd9284f807650..ca9f264789b8e7 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -52,6 +52,7 @@ linkify.workspace = true log.workspace = true lsp.workspace = true markdown.workspace = true +menu.workspace = true multi_buffer.workspace = true ordered-float.workspace = true parking_lot.workspace = true diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 69cacad403f95f..f983dac0c2edbc 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -592,6 +592,17 @@ impl DisplaySnapshot { .anchor_at(point.to_offset(self, bias), bias) } + pub fn display_point_to_breakpoint_anchor(&self, point: DisplayPoint) -> Anchor { + let bias = if point.is_zero() { + Bias::Right + } else { + Bias::Left + }; + + self.buffer_snapshot + .anchor_at(point.to_offset(self, bias), bias) + } + fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 84ae287e122ccc..3276db49b65f06 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -99,6 +99,7 @@ use language::{ }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; +use project::dap_store::BreakpointEditAction; pub use proposed_changes_editor::{ ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar, }; @@ -5438,7 +5439,7 @@ impl Editor { fn active_breakpoint_points( &mut self, cx: &mut ViewContext, - ) -> HashMap { + ) -> HashMap { let mut breakpoint_display_points = HashMap::default(); let Some(dap_store) = self.dap_store.clone() else { @@ -5458,7 +5459,7 @@ impl Editor { let point = breakpoint.point_for_buffer(&buffer); breakpoint_display_points - .insert(point.to_display_point(&snapshot), breakpoint.kind.clone()); + .insert(point.to_display_point(&snapshot).row(), breakpoint.clone()); } }; }; @@ -5514,7 +5515,7 @@ impl Editor { let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0); - breakpoint_display_points.insert(position, breakpoint.kind.clone()); + breakpoint_display_points.insert(position.row(), breakpoint.clone()); } } }; @@ -5545,6 +5546,7 @@ impl Editor { BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint, }; let arc_kind = Arc::new(kind.clone()); + let arc_kind2 = arc_kind.clone(); IconButton::new(("breakpoint_indicator", row.0 as usize), icon) .icon_size(IconSize::XSmall) @@ -5553,7 +5555,12 @@ impl Editor { .style(ButtonStyle::Transparent) .on_click(cx.listener(move |editor, _e, cx| { editor.focus(cx); - editor.toggle_breakpoint_at_anchor(position, (*arc_kind).clone(), cx); + editor.edit_breakpoint_at_anchor( + position, + (*arc_kind).clone(), + BreakpointEditAction::Toggle, + cx, + ); })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { let source = editor @@ -5567,6 +5574,14 @@ impl Editor { let editor_weak = cx.view().downgrade(); let second_weak = editor_weak.clone(); + let log_message = arc_kind2.log_message(); + + let second_entry_msg = if log_message.is_some() { + "Edit Log Breakpoint" + } else { + "Toggle Log Breakpoint" + }; + let context_menu = ui::ContextMenu::build(cx, move |menu, _cx| { let anchor = position; menu.on_blur_subscription(Subscription::new(|| {})) @@ -5574,23 +5589,61 @@ impl Editor { .entry("Toggle Breakpoint", None, move |cx| { if let Some(editor) = editor_weak.upgrade() { editor.update(cx, |this, cx| { - this.toggle_breakpoint_at_anchor( + this.edit_breakpoint_at_anchor( anchor, BreakpointKind::Standard, + BreakpointEditAction::Toggle, cx, ); }) } }) - .entry("Toggle Log Breakpoint", None, move |cx| { + .entry(second_entry_msg, None, move |cx| { if let Some(editor) = second_weak.clone().upgrade() { + let log_message = log_message.clone(); editor.update(cx, |this, cx| { - this.toggle_breakpoint_at_anchor( - anchor, - BreakpointKind::Log("Log breakpoint".into()), - cx, + let position = this.snapshot(cx).display_point_to_anchor( + DisplayPoint::new(row, 0), + Bias::Right, ); - }) + + let weak_editor = cx.view().downgrade(); + let bp_prompt = cx.new_view(|cx| { + BreakpointPromptEditor::new( + weak_editor, + anchor, + log_message, + cx, + ) + }); + + let height = bp_prompt.update(cx, |this, cx| { + this.prompt.update(cx, |prompt, cx| { + prompt.max_point(cx).row().0 + 1 + 2 + }) + }); + let cloned_prompt = bp_prompt.clone(); + let blocks = vec![BlockProperties { + style: BlockStyle::Sticky, + position, + height, + render: Box::new(move |cx| { + *cloned_prompt.read(cx).gutter_dimensions.lock() = + *cx.gutter_dimensions; + cloned_prompt.clone().into_any_element() + }), + disposition: BlockDisposition::Above, + priority: 0, + }]; + + let focus_handle = bp_prompt.focus_handle(cx); + cx.focus(&focus_handle); + + let block_ids = this.insert_blocks(blocks, None, cx); + bp_prompt.update(cx, |prompt, _| { + prompt.add_block_ids(block_ids); + }); + }); } }) }); @@ -6446,15 +6499,6 @@ impl Editor { pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { let cursor_position: Point = self.selections.newest(cx).head(); - // Bias is set to right when placing a breakpoint on the first row - // to avoid having the breakpoint's anchor be anchor::MIN & having - // it's buffer_id be None - let bias = if cursor_position.row == 0 { - Bias::Right - } else { - Bias::Left - }; - // We Set the column position to zero so this function interacts correctly // between calls by clicking on the gutter & using an action to toggle a // breakpoint. Otherwise, toggling a breakpoint through an action wouldn't @@ -6463,16 +6507,50 @@ impl Editor { .snapshot(cx) .display_snapshot .buffer_snapshot - .anchor_at(Point::new(cursor_position.row, 0), bias) + .breakpoint_anchor(Point::new(cursor_position.row, 0)) .text_anchor; - self.toggle_breakpoint_at_anchor(breakpoint_position, BreakpointKind::Standard, cx); + let project = self.project.clone(); + + let found_bp = maybe!({ + let buffer_id = breakpoint_position.buffer_id?; + let buffer = + project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?; + let (buffer_snapshot, project_path) = ( + buffer.read(cx).snapshot(), + buffer.read(cx).project_path(cx)?, + ); + + let row = buffer_snapshot + .summary_for_anchor::(&breakpoint_position) + .row; + + let bp = self.dap_store.clone()?.read_with(cx, |store, _cx| { + store.breakpoint_at_row(row, &project_path, buffer_snapshot) + })?; + + Some((bp.active_position?, bp.kind)) + }); + + let edit_action = BreakpointEditAction::Toggle; + + if let Some((anchor, kind)) = found_bp { + self.edit_breakpoint_at_anchor(anchor, kind, edit_action, cx); + } else { + self.edit_breakpoint_at_anchor( + breakpoint_position, + BreakpointKind::Standard, + edit_action, + cx, + ); + } } - pub fn toggle_breakpoint_at_anchor( + pub fn edit_breakpoint_at_anchor( &mut self, breakpoint_position: text::Anchor, kind: BreakpointKind, + edit_action: BreakpointEditAction, cx: &mut ViewContext, ) { let Some(project) = &self.project else { @@ -6506,6 +6584,7 @@ impl Editor { active_position: Some(breakpoint_position), kind, }, + edit_action, cx, ); }); @@ -14534,3 +14613,150 @@ fn check_multiline_range(buffer: &Buffer, range: Range) -> Range { range.start..range.start } } + +struct BreakpointPromptEditor { + pub(crate) prompt: View, + editor: WeakView, + breakpoint_anchor: text::Anchor, + block_ids: HashSet, + gutter_dimensions: Arc>, + _subscriptions: Vec, +} + +impl BreakpointPromptEditor { + const MAX_LINES: u8 = 4; + + fn new( + editor: WeakView, + breakpoint_anchor: text::Anchor, + log_message: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.new_model(|cx| { + Buffer::local( + log_message.map(|msg| msg.to_string()).unwrap_or_default(), + cx, + ) + }); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let prompt = cx.new_view(|cx| { + let mut prompt = Editor::new( + EditorMode::AutoHeight { + max_lines: Self::MAX_LINES as usize, + }, + buffer, + None, + false, + cx, + ); + prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + // Since the prompt editors for all inline assistants are linked, + // always show the cursor (even when it isn't focused) because + // typing in one will make what you typed appear in all of them. + prompt.set_show_cursor_when_unfocused(true, cx); + prompt.set_placeholder_text( + "Message to log when breakpoint is hit. Expressions within {} are interpolated.", + cx, + ); + + prompt + }); + + Self { + prompt, + editor, + breakpoint_anchor, + gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), + block_ids: Default::default(), + _subscriptions: vec![], + } + } + + pub(crate) fn add_block_ids(&mut self, block_ids: Vec) { + self.block_ids.extend(block_ids) + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if let Some(editor) = self.editor.upgrade() { + let log_message = self + .prompt + .read(cx) + .buffer + .read(cx) + .as_singleton() + .expect("A multi buffer in breakpoint prompt isn't possible") + .read(cx) + .as_rope() + .to_string(); + + editor.update(cx, |editor, cx| { + editor.edit_breakpoint_at_anchor( + self.breakpoint_anchor, + BreakpointKind::Log(log_message.into()), + BreakpointEditAction::EditLogMessage, + cx, + ); + + editor.remove_blocks(self.block_ids.clone(), None, cx); + }); + } + } + + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + if let Some(editor) = self.editor.upgrade() { + editor.update(cx, |editor, cx| { + editor.remove_blocks(self.block_ids.clone(), None, cx); + }); + } + } + + fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.prompt.read(cx).read_only(cx) { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.buffer_font.family.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: settings.buffer_font_size.into(), + font_weight: settings.buffer_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }; + EditorElement::new( + &self.prompt, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } +} + +impl Render for BreakpointPromptEditor { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let gutter_dimensions = *self.gutter_dimensions.lock(); + h_flex() + .key_context("Editor") + .bg(cx.theme().colors().editor_background) + .border_y_1() + .border_color(cx.theme().status().info_border) + .size_full() + .py(cx.line_height() / 2.5) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))) + .child(div().flex_1().child(self.render_prompt_editor(cx))) + } +} + +impl FocusableView for BreakpointPromptEditor { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.prompt.focus_handle(cx) + } +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 815d87ea6d9b57..7b52aeb4ec93c8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -24,7 +24,7 @@ use crate::{ CURSORS_VISIBLE_FOR, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, }; use client::ParticipantIndex; -use collections::{BTreeMap, HashMap, HashSet}; +use collections::{BTreeMap, HashMap}; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; use gpui::Subscription; use gpui::{ @@ -48,7 +48,7 @@ use language::{ use lsp::DiagnosticSeverity; use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow}; use project::{ - dap_store::BreakpointKind, + dap_store::{Breakpoint, BreakpointKind}, project_settings::{GitGutterSetting, ProjectSettings}, ProjectPath, }; @@ -1649,7 +1649,7 @@ impl EditorElement { gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, snapshot: &EditorSnapshot, - breakpoints: HashMap, + breakpoints: HashMap, cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { @@ -1659,10 +1659,10 @@ impl EditorElement { breakpoints .iter() - .filter_map(|(point, kind)| { - let row = MultiBufferRow { 0: point.row().0 }; + .filter_map(|(point, bp)| { + let row = MultiBufferRow { 0: point.0 }; - if range.start > point.row() || range.end < point.row() { + if range.start > *point || range.end < *point { return None; } @@ -1670,22 +1670,20 @@ impl EditorElement { return None; } - let bias = if point.is_zero() { - Bias::Right - } else { - Bias::Left - }; - - let position = snapshot - .display_snapshot - .display_point_to_anchor(*point, bias) + let backup_position = snapshot + .display_point_to_breakpoint_anchor(DisplayPoint::new(*point, 0)) .text_anchor; - let button = editor.render_breakpoint(position, point.row(), &kind, cx); + let button = editor.render_breakpoint( + bp.active_position.unwrap_or(backup_position), + *point, + &bp.kind, + cx, + ); let button = prepaint_gutter_button( button, - point.row(), + *point, line_height, gutter_dimensions, scroll_pixel_position, @@ -1709,7 +1707,7 @@ impl EditorElement { gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, snapshot: &EditorSnapshot, - breakpoints: &mut HashMap, + breakpoints: &mut HashMap, cx: &mut WindowContext, ) -> Vec { self.editor.update(cx, |editor, cx| { @@ -1753,12 +1751,11 @@ impl EditorElement { } let display_row = multibuffer_point.to_display_point(snapshot).row(); - let display_point = DisplayPoint::new(display_row, 0); let button = editor.render_run_indicator( &self.style, Some(display_row) == active_task_indicator_row, display_row, - breakpoints.remove(&display_point).is_some(), + breakpoints.remove(&display_row).is_some(), cx, ); @@ -1787,7 +1784,7 @@ impl EditorElement { gutter_dimensions: &GutterDimensions, gutter_hitbox: &Hitbox, rows_with_hunk_bounds: &HashMap>, - breakpoint_points: &mut HashMap, + breakpoint_points: &mut HashMap, cx: &mut WindowContext, ) -> Option { let mut active = false; @@ -1805,10 +1802,7 @@ impl EditorElement { }); let button = button?; - let button = if breakpoint_points - .remove(&DisplayPoint::new(row, 0)) - .is_some() - { + let button = if breakpoint_points.remove(&row).is_some() { button.icon_color(Color::Debugger) } else { button @@ -1897,7 +1891,7 @@ impl EditorElement { active_rows: &BTreeMap, newest_selection_head: Option, snapshot: &EditorSnapshot, - breakpoint_rows: HashSet, + breakpoint_rows: &HashMap, cx: &mut WindowContext, ) -> Vec> { let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| { @@ -1937,7 +1931,7 @@ impl EditorElement { .map(|(ix, multibuffer_row)| { let multibuffer_row = multibuffer_row?; let display_row = DisplayRow(rows.start.0 + ix as u32); - let color = if breakpoint_rows.contains(&display_row) { + let color = if breakpoint_rows.contains_key(&display_row) { cx.theme().colors().debugger_accent } else if active_rows.contains_key(&display_row) { cx.theme().colors().editor_active_line_number @@ -5075,7 +5069,7 @@ impl Element for EditorElement { cx.set_view_id(self.editor.entity_id()); cx.set_focus_handle(&focus_handle); - let mut breakpoint_lines = self + let mut breakpoint_rows = self .editor .update(cx, |editor, cx| editor.active_breakpoint_points(cx)); @@ -5259,35 +5253,35 @@ impl Element for EditorElement { cx, ); - let gutter_breakpoint_indicator = - self.editor.read(cx).gutter_breakpoint_indicator; - - let breakpoint_rows = breakpoint_lines - .keys() - .map(|display_point| display_point.row()) - .collect(); - - // We want all lines with breakpoint's to have their number's painted - // debug accent color & we still want to render a grey breakpoint for the gutter - // indicator so we add that in after creating breakpoint_rows for layout line nums - // Otherwise, when a cursor is on a line number it will always be white even - // if that line has a breakpoint - if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator { - breakpoint_lines - .entry(gutter_breakpoint_point) - .or_insert(BreakpointKind::Standard); - } - let line_numbers = self.layout_line_numbers( start_row..end_row, buffer_rows.iter().copied(), &active_rows, newest_selection_head, &snapshot, - breakpoint_rows, + &breakpoint_rows, cx, ); + // We add the gutter breakpoint indicator to breakpoint_rows after painting + // line numbers so we don't paint a line number debug accent color if a user + // has their mouse over that line when a breakpoint isn't there + let gutter_breakpoint_indicator = + self.editor.read(cx).gutter_breakpoint_indicator; + if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator { + breakpoint_rows + .entry(gutter_breakpoint_point.row()) + .or_insert(Breakpoint { + active_position: Some( + snapshot + .display_point_to_breakpoint_anchor(gutter_breakpoint_point) + .text_anchor, + ), + cache_position: 0, + kind: BreakpointKind::Standard, + }); + } + let mut gutter_fold_toggles = cx.with_element_namespace("gutter_fold_toggles", |cx| { self.layout_gutter_fold_toggles( @@ -5612,7 +5606,7 @@ impl Element for EditorElement { &gutter_dimensions, &gutter_hitbox, &rows_with_hunk_bounds, - &mut breakpoint_lines, + &mut breakpoint_rows, cx, ); } @@ -5631,7 +5625,7 @@ impl Element for EditorElement { &gutter_hitbox, &rows_with_hunk_bounds, &snapshot, - &mut breakpoint_lines, + &mut breakpoint_rows, cx, ) } else { @@ -5646,7 +5640,7 @@ impl Element for EditorElement { &gutter_hitbox, &rows_with_hunk_bounds, &snapshot, - breakpoint_lines, + breakpoint_rows, cx, ); @@ -6513,7 +6507,7 @@ mod tests { let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); let element = EditorElement::new(&editor, style); let snapshot = window.update(cx, |editor, cx| editor.snapshot(cx)).unwrap(); - let breakpoint_rows = HashSet::default(); + let breakpoint_rows = HashMap::default(); let layouts = cx .update_window(*window, |_, cx| { @@ -6523,7 +6517,7 @@ mod tests { &Default::default(), Some(DisplayPoint::new(DisplayRow(0), 0)), &snapshot, - breakpoint_rows, + &breakpoint_rows, cx, ) }) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 7aa733ba8fa373..f4fb53e221a5d1 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -3166,6 +3166,16 @@ impl MultiBufferSnapshot { self.anchor_at(position, Bias::Right) } + pub fn breakpoint_anchor(&self, position: T) -> Anchor { + let bias = if position.to_offset(self) == 0usize { + Bias::Right + } else { + Bias::Left + }; + + self.anchor_at(position, bias) + } + pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { let offset = position.to_offset(self); if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 3a07bd267e300f..7e506ae5470812 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -140,6 +140,20 @@ impl DapStore { &self.breakpoints } + pub fn breakpoint_at_row( + &self, + row: u32, + project_path: &ProjectPath, + buffer_snapshot: BufferSnapshot, + ) -> Option { + let breakpoint_set = self.breakpoints.get(project_path)?; + + breakpoint_set + .iter() + .find(|bp| bp.point_for_buffer_snapshot(&buffer_snapshot).row == row) + .cloned() + } + pub fn on_open_buffer( &mut self, project_path: &ProjectPath, @@ -763,16 +777,21 @@ impl DapStore { breakpoint: Breakpoint, buffer_path: PathBuf, buffer_snapshot: BufferSnapshot, + edit_action: BreakpointEditAction, cx: &mut ModelContext, ) { let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); - if let Some(gotten_breakpoint) = breakpoint_set.take(&breakpoint) { - if gotten_breakpoint.kind != breakpoint.kind { + match edit_action { + BreakpointEditAction::Toggle => { + if !breakpoint_set.remove(&breakpoint) { + breakpoint_set.insert(breakpoint); + } + } + BreakpointEditAction::EditLogMessage => { + breakpoint_set.remove(&breakpoint); breakpoint_set.insert(breakpoint); } - } else { - breakpoint_set.insert(breakpoint); } cx.notify(); @@ -855,7 +874,13 @@ impl DapStore { type LogMessage = Arc; -#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Debug)] +pub enum BreakpointEditAction { + Toggle, + EditLogMessage, +} + +#[derive(Clone, Debug)] pub enum BreakpointKind { Standard, Log(LogMessage), @@ -868,6 +893,27 @@ impl BreakpointKind { BreakpointKind::Log(_) => 1, } } + + pub fn log_message(&self) -> Option { + match self { + BreakpointKind::Standard => None, + BreakpointKind::Log(message) => Some(message.clone()), + } + } +} + +impl PartialEq for BreakpointKind { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } +} + +impl Eq for BreakpointKind {} + +impl Hash for BreakpointKind { + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + } } #[derive(Clone, Debug)] @@ -883,7 +929,12 @@ pub struct Breakpoint { // overlapping breakpoint's with them being aware. impl PartialEq for Breakpoint { fn eq(&self, other: &Self) -> bool { - self.active_position == other.active_position && self.cache_position == other.cache_position + match (&self.active_position, &other.active_position) { + (None, None) => self.cache_position == other.cache_position, + (None, Some(_)) => false, + (Some(_), None) => false, + (Some(self_position), Some(other_position)) => self_position == other_position, + } } } @@ -891,8 +942,11 @@ impl Eq for Breakpoint {} impl Hash for Breakpoint { fn hash(&self, state: &mut H) { - self.active_position.hash(state); - self.cache_position.hash(state); + if self.active_position.is_some() { + self.active_position.hash(state); + } else { + self.cache_position.hash(state); + } } } @@ -915,13 +969,8 @@ impl Breakpoint { pub fn set_active_position(&mut self, buffer: &Buffer) { if self.active_position.is_none() { - let bias = if self.cache_position == 0 { - text::Bias::Right - } else { - text::Bias::Left - }; - - self.active_position = Some(buffer.anchor_at(Point::new(self.cache_position, 0), bias)); + self.active_position = + Some(buffer.breakpoint_anchor(Point::new(self.cache_position, 0))); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9637f9023fe522..67ea6f2663acda 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -35,7 +35,7 @@ use dap::{ }; use collections::{BTreeSet, HashMap, HashSet}; -use dap_store::{Breakpoint, DapStore, DapStoreEvent, SerializedBreakpoint}; +use dap_store::{Breakpoint, BreakpointEditAction, DapStore, DapStoreEvent, SerializedBreakpoint}; use debounced_delay::DebouncedDelay; pub use environment::ProjectEnvironment; use futures::{ @@ -1244,6 +1244,7 @@ impl Project { &self, buffer_id: BufferId, breakpoint: Breakpoint, + edit_action: BreakpointEditAction, cx: &mut ModelContext, ) { let Some(buffer) = self.buffer_for_id(buffer_id, cx) else { @@ -1267,6 +1268,7 @@ impl Project { breakpoint, buffer_path, buffer.read(cx).snapshot(), + edit_action, cx, ); }); diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index a0865f1cdf6fbd..3bc5889caeb700 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -17,14 +17,6 @@ pub struct Anchor { } impl Anchor { - pub fn offset_for_breakpoint_anchor(offset: usize) -> Self { - Anchor { - timestamp: clock::Lamport::new(0), - offset, - bias: Bias::Left, - buffer_id: None, - } - } pub const MIN: Self = Self { timestamp: clock::Lamport::MIN, offset: usize::MIN, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index a5b65e6f4bcf1b..efd27bdf82721d 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2270,6 +2270,18 @@ impl BufferSnapshot { self.anchor_at_offset(position.to_offset(self), bias) } + pub fn breakpoint_anchor(&self, position: T) -> Anchor { + let offset = position.to_offset(self); + + let bias = if offset == 0usize { + Bias::Right + } else { + Bias::Left + }; + + self.anchor_at_offset(offset, bias) + } + fn anchor_at_offset(&self, offset: usize, bias: Bias) -> Anchor { if bias == Bias::Left && offset == 0 { Anchor::MIN From 3c301c3ea433d3a64880a0404cdd7ea45431d02b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 4 Oct 2024 20:21:07 +0200 Subject: [PATCH 271/650] Format file --- crates/remote_server/src/headless_project.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index d9f97c31f0a637..e010a115ad2fe7 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -57,7 +57,8 @@ impl HeadlessProject { let dap_store = cx.new_model(DapStore::new); let buffer_store = cx.new_model(|cx| { - let mut buffer_store = BufferStore::local(worktree_store.clone(), dap_store.clone(), cx); + let mut buffer_store = + BufferStore::local(worktree_store.clone(), dap_store.clone(), cx); buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); buffer_store }); From f9b045bd23fb612cba1785c90c33214f737fcb75 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 4 Oct 2024 20:23:20 +0200 Subject: [PATCH 272/650] Remove unused crates --- Cargo.lock | 2 -- crates/debugger_ui/Cargo.toml | 2 -- 2 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ad8d7bc552934..efd91a1b67e938 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3450,7 +3450,6 @@ dependencies = [ "fuzzy", "gpui", "language", - "log", "menu", "parking_lot", "project", @@ -3460,7 +3459,6 @@ dependencies = [ "tasks_ui", "theme", "ui", - "util", "workspace", ] diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 1e77ec8af4ec97..0d04a8fbb7d36f 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -16,7 +16,6 @@ futures.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true -log.workspace = true menu.workspace = true parking_lot.workspace = true project.workspace = true @@ -26,7 +25,6 @@ task.workspace = true tasks_ui.workspace = true theme.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true [dev-dependencies] From c2ed56af0b182268703f79fd0dd5fde73febf0f3 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 5 Oct 2024 13:51:24 +0200 Subject: [PATCH 273/650] Add copy memory reference menu item to variable list --- crates/debugger_ui/src/variable_list.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 80c4857e24be1f..c9cbf8e056afb0 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -366,9 +366,9 @@ impl VariableList { "Copy name", None, cx.handler_for(&this, { - let variable = variable.clone(); + let variable_name = variable.name.clone(); move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(variable.name.clone())) + cx.write_to_clipboard(ClipboardItem::new_string(variable_name.clone())) } }), ) @@ -376,12 +376,26 @@ impl VariableList { "Copy value", None, cx.handler_for(&this, { - let variable = variable.clone(); + let variable_value = variable.value.clone(); move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(variable.value.clone())) + cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone())) } }), ) + .when_some( + variable.memory_reference.clone(), + |menu, memory_reference| { + menu.entry( + "Copy memory reference", + None, + cx.handler_for(&this, move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + memory_reference.clone(), + )) + }), + ) + }, + ) .when(support_set_variable, move |menu| { let variable = variable.clone(); let scope = scope.clone(); From 8c0a7b102429d93f93e502c15a3f8caff0647a1a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 6 Oct 2024 01:43:00 +0200 Subject: [PATCH 274/650] Module list (#46) --- crates/debugger_ui/src/debugger_panel.rs | 14 +- crates/debugger_ui/src/debugger_panel_item.rs | 124 +++++++++++------- crates/debugger_ui/src/lib.rs | 1 + crates/debugger_ui/src/module_list.rs | 111 ++++++++++++++++ crates/project/src/dap_store.rs | 40 +++++- 5 files changed, 234 insertions(+), 56 deletions(-) create mode 100644 crates/debugger_ui/src/module_list.rs diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a1d1ed9aab4d0f..867cfedf8a65fb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -6,7 +6,7 @@ use dap::debugger_settings::DebuggerSettings; use dap::messages::{Events, Message}; use dap::requests::{Request, StartDebugging}; use dap::{ - Capabilities, ContinuedEvent, ExitedEvent, OutputEvent, StackFrame, StoppedEvent, + Capabilities, ContinuedEvent, ExitedEvent, ModuleEvent, OutputEvent, StackFrame, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, }; use gpui::{ @@ -36,6 +36,7 @@ pub enum DebugPanelEvent { Thread((DebugAdapterClientId, ThreadEvent)), Continued((DebugAdapterClientId, ContinuedEvent)), Output((DebugAdapterClientId, OutputEvent)), + Module((DebugAdapterClientId, ModuleEvent)), ClientStopped(DebugAdapterClientId), } @@ -243,7 +244,7 @@ impl DebugPanel { Events::Thread(event) => self.handle_thread_event(&client_id, event, cx), Events::Output(event) => self.handle_output_event(&client_id, event, cx), Events::Breakpoint(_) => {} - Events::Module(_) => {} + Events::Module(event) => self.handle_module_event(&client_id, event, cx), Events::LoadedSource(_) => {} Events::Capabilities(_) => {} Events::Memory(_) => {} @@ -472,6 +473,15 @@ impl DebugPanel { cx.emit(DebugPanelEvent::Output((*client_id, event.clone()))); } + fn handle_module_event( + &mut self, + client_id: &DebugAdapterClientId, + event: &ModuleEvent, + cx: &mut ViewContext, + ) { + cx.emit(DebugPanelEvent::Module((*client_id, event.clone()))); + } + fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement { const TITLE: &str = "Debug session exited without hitting any breakpoints"; const DESCRIPTION: &str = diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 2ccf3ffd0c997b..872e5fb90ba1c6 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -2,13 +2,14 @@ use std::path::Path; use crate::console::Console; use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState}; +use crate::module_list::ModuleList; use crate::variable_list::VariableList; use dap::client::{DebugAdapterClientId, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; use dap::{ - Capabilities, ContinuedEvent, OutputEvent, OutputEventCategory, StackFrame, StoppedEvent, - ThreadEvent, + Capabilities, ContinuedEvent, ModuleEvent, OutputEvent, OutputEventCategory, StackFrame, + StoppedEvent, ThreadEvent, }; use editor::Editor; use gpui::{ @@ -28,9 +29,10 @@ pub enum Event { Close, } -#[derive(PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] enum ThreadItem { Variables, + Modules, Console, Output, } @@ -43,6 +45,7 @@ pub struct DebugPanelItem { stack_frame_list: ListState, output_editor: View, current_stack_frame_id: u64, + module_list: View, client_kind: DebugAdapterKind, active_thread_item: ThreadItem, workspace: WeakView, @@ -76,6 +79,9 @@ impl DebugPanelItem { cx, ) }); + + let module_list = cx.new_view(|cx| ModuleList::new(dap_store.clone(), &client_id, cx)); + let console = cx.new_view(|cx| { Console::new( client_id, @@ -112,6 +118,9 @@ impl DebugPanelItem { DebugPanelEvent::Output((client_id, event)) => { this.handle_output_event(client_id, event, cx) } + DebugPanelEvent::Module((client_id, event)) => { + this.handle_module_event(client_id, event, cx) + } DebugPanelEvent::ClientStopped(client_id) => { this.handle_client_stopped_event(client_id, cx) } @@ -144,6 +153,7 @@ impl DebugPanelItem { thread_id, dap_store, workspace, + module_list, thread_state, focus_handle, output_editor, @@ -265,6 +275,21 @@ impl DebugPanelItem { } } + fn handle_module_event( + &mut self, + client_id: &DebugAdapterClientId, + event: &ModuleEvent, + cx: &mut ViewContext, + ) { + if self.should_skip_event(client_id, self.thread_id) { + return; + } + + self.module_list.update(cx, |variable_list, cx| { + variable_list.on_module_event(event, cx); + }); + } + fn handle_client_stopped_event( &mut self, client_id: &DebugAdapterClientId, @@ -464,6 +489,28 @@ impl DebugPanelItem { .into_any() } + fn render_entry_button( + &self, + label: &SharedString, + thread_item: ThreadItem, + cx: &mut ViewContext, + ) -> AnyElement { + div() + .id(label.clone()) + .px_2() + .py_1() + .cursor_pointer() + .border_b_2() + .when(self.active_thread_item == thread_item, |this| { + this.border_color(cx.theme().colors().border) + }) + .child(Button::new(label.clone(), label.clone())) + .on_click(cx.listener(move |this, _, _| { + this.active_thread_item = thread_item.clone(); + })) + .into_any_element() + } + pub fn continue_thread(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); @@ -717,55 +764,38 @@ impl Render for DebugPanelItem { .border_b_1() .w_full() .border_color(cx.theme().colors().border_variant) - .child( - div() - .id("variables") - .px_2() - .py_1() - .cursor_pointer() - .border_b_2() - .when(*active_thread_item == ThreadItem::Variables, |this| { - this.border_color(cx.theme().colors().border) - }) - .child(Button::new("variables-button", "Variables")) - .on_click(cx.listener(|this, _, _| { - this.active_thread_item = ThreadItem::Variables; - })), - ) - .child( - div() - .id("console") - .px_2() - .py_1() - .cursor_pointer() - .border_b_2() - .when(*active_thread_item == ThreadItem::Console, |this| { - this.border_color(cx.theme().colors().border) - }) - .child(Button::new("console-button", "Console")) - .on_click(cx.listener(|this, _, _| { - this.active_thread_item = ThreadItem::Console; - })), + .child(self.render_entry_button( + &SharedString::from("Variables"), + ThreadItem::Variables, + cx, + )) + .when( + capabilities.supports_modules_request.unwrap_or_default(), + |this| { + this.child(self.render_entry_button( + &SharedString::from("Modules"), + ThreadItem::Modules, + cx, + )) + }, ) - .child( - div() - .id("output") - .px_2() - .py_1() - .cursor_pointer() - .border_b_2() - .when(*active_thread_item == ThreadItem::Output, |this| { - this.border_color(cx.theme().colors().border) - }) - .child(Button::new("output", "Output")) - .on_click(cx.listener(|this, _, _| { - this.active_thread_item = ThreadItem::Output; - })), - ), + .child(self.render_entry_button( + &SharedString::from("Console"), + ThreadItem::Console, + cx, + )) + .child(self.render_entry_button( + &SharedString::from("Output"), + ThreadItem::Output, + cx, + )), ) .when(*active_thread_item == ThreadItem::Variables, |this| { this.size_full().child(self.variable_list.clone()) }) + .when(*active_thread_item == ThreadItem::Modules, |this| { + this.size_full().child(self.module_list.clone()) + }) .when(*active_thread_item == ThreadItem::Output, |this| { this.child(self.output_editor.clone()) }) diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 79ad1d36928719..f69d521c7a7c90 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -11,6 +11,7 @@ use workspace::{ mod console; pub mod debugger_panel; mod debugger_panel_item; +mod module_list; mod variable_list; pub fn init(cx: &mut AppContext) { diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs new file mode 100644 index 00000000000000..1a87913f34c94a --- /dev/null +++ b/crates/debugger_ui/src/module_list.rs @@ -0,0 +1,111 @@ +use anyhow::Result; +use dap::{client::DebugAdapterClientId, Module, ModuleEvent}; +use gpui::{list, AnyElement, FocusHandle, FocusableView, ListState, Model, Task}; +use project::dap_store::DapStore; +use ui::prelude::*; + +pub struct ModuleList { + list: ListState, + modules: Vec, + focus_handle: FocusHandle, + dap_store: Model, + client_id: DebugAdapterClientId, +} + +impl ModuleList { + pub fn new( + dap_store: Model, + client_id: &DebugAdapterClientId, + cx: &mut ViewContext, + ) -> Self { + let weakview = cx.view().downgrade(); + let focus_handle = cx.focus_handle(); + + let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { + weakview + .upgrade() + .map(|view| view.update(cx, |this, cx| this.render_entry(ix, cx))) + .unwrap_or(div().into_any()) + }); + + let this = Self { + list, + dap_store, + focus_handle, + client_id: *client_id, + modules: Vec::default(), + }; + + this.fetch_modules(cx).detach_and_log_err(cx); + + this + } + + pub fn on_module_event(&mut self, event: &ModuleEvent, cx: &mut ViewContext) { + match event.reason { + dap::ModuleEventReason::New => self.modules.push(event.module.clone()), + dap::ModuleEventReason::Changed => { + if let Some(module) = self.modules.iter_mut().find(|m| m.id == event.module.id) { + *module = event.module.clone(); + } + } + dap::ModuleEventReason::Removed => self.modules.retain(|m| m.id != event.module.id), + } + + self.list.reset(self.modules.len()); + cx.notify(); + } + + fn fetch_modules(&self, cx: &mut ViewContext) -> Task> { + let task = self + .dap_store + .update(cx, |store, cx| store.modules(&self.client_id, cx)); + + cx.spawn(|this, mut cx| async move { + let mut modules = task.await?; + + this.update(&mut cx, |this, cx| { + std::mem::swap(&mut this.modules, &mut modules); + this.list.reset(this.modules.len()); + + cx.notify(); + }) + }) + } + + fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let module = &self.modules[ix]; + + v_flex() + .rounded_md() + .w_full() + .group("") + .p_1() + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone())) + .child( + h_flex() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .when_some(module.path.clone(), |this, path| { + this.child(div().child(path)) + }), + ) + .into_any() + } +} + +impl FocusableView for ModuleList { + fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for ModuleList { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + div() + .size_full() + .p_1() + .child(list(self.list.clone()).size_full()) + } +} diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 7e506ae5470812..0e7ed0655fafcf 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -5,18 +5,18 @@ use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::Message; use dap::requests::{ Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, - Next, Pause, Scopes, SetBreakpoints, SetExpression, SetVariable, StackTrace, StepIn, StepOut, - Terminate, TerminateThreads, Variables, + Modules, Next, Pause, Scopes, SetBreakpoints, SetExpression, SetVariable, StackTrace, StepIn, + StepOut, Terminate, TerminateThreads, Variables, }; use dap::{ AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, - InitializeRequestArgumentsPathFormat, LaunchRequestArguments, NextArguments, PauseArguments, - Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, - Source, SourceBreakpoint, StackFrame, StackTraceArguments, StepInArguments, StepOutArguments, - SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, - VariablesArguments, + InitializeRequestArgumentsPathFormat, LaunchRequestArguments, Module, ModulesArguments, + NextArguments, PauseArguments, Scope, ScopesArguments, SetBreakpointsArguments, + SetExpressionArguments, SetVariableArguments, Source, SourceBreakpoint, StackFrame, + StackTraceArguments, StepInArguments, StepOutArguments, SteppingGranularity, + TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, }; use gpui::{EventEmitter, Model, ModelContext, Task}; use language::{Buffer, BufferSnapshot}; @@ -316,6 +316,32 @@ impl DapStore { }) } + pub fn modules( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Task>> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Client was not found"))); + }; + + let capabilities = self.capabilities_by_id(client_id); + + if !capabilities.supports_modules_request.unwrap_or_default() { + return Task::ready(Ok(Vec::default())); + } + + cx.spawn(|_, _| async move { + Ok(client + .request::(ModulesArguments { + start_module: None, + module_count: None, + }) + .await? + .modules) + }) + } + pub fn stack_frames( &mut self, client_id: &DebugAdapterClientId, From f76b7c9337e648aa47255bfeb59e7873a8aad0e1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 6 Oct 2024 13:55:20 +0200 Subject: [PATCH 275/650] Implement handle Capabilities event --- crates/debugger_ui/src/debugger_panel.rs | 24 ++++++++++++++++--- crates/debugger_ui/src/debugger_panel_item.rs | 15 ++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 867cfedf8a65fb..a0fdd43bdc0ee8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -6,8 +6,8 @@ use dap::debugger_settings::DebuggerSettings; use dap::messages::{Events, Message}; use dap::requests::{Request, StartDebugging}; use dap::{ - Capabilities, ContinuedEvent, ExitedEvent, ModuleEvent, OutputEvent, StackFrame, StoppedEvent, - TerminatedEvent, ThreadEvent, ThreadEventReason, + Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, ModuleEvent, OutputEvent, + StackFrame, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, }; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, @@ -38,6 +38,7 @@ pub enum DebugPanelEvent { Output((DebugAdapterClientId, OutputEvent)), Module((DebugAdapterClientId, ModuleEvent)), ClientStopped(DebugAdapterClientId), + CapabilitiesChanged(DebugAdapterClientId), } actions!(debug_panel, [ToggleFocus]); @@ -246,7 +247,9 @@ impl DebugPanel { Events::Breakpoint(_) => {} Events::Module(event) => self.handle_module_event(&client_id, event, cx), Events::LoadedSource(_) => {} - Events::Capabilities(_) => {} + Events::Capabilities(event) => { + self.handle_capabilities_changed_event(client_id, event, cx); + } Events::Memory(_) => {} Events::Process(_) => {} Events::ProgressEnd(_) => {} @@ -267,6 +270,8 @@ impl DebugPanel { self.dap_store.update(cx, |store, cx| { store.merge_capabilities_for_client(&client_id, capabilities, cx); }); + + cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id)); } let send_breakpoints_task = self.workspace.update(cx, |workspace, cx| { @@ -482,6 +487,19 @@ impl DebugPanel { cx.emit(DebugPanelEvent::Module((*client_id, event.clone()))); } + fn handle_capabilities_changed_event( + &mut self, + client_id: &DebugAdapterClientId, + event: &CapabilitiesEvent, + cx: &mut ViewContext, + ) { + self.dap_store.update(cx, |store, cx| { + store.merge_capabilities_for_client(client_id, &event.capabilities, cx); + }); + + cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id)); + } + fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement { const TITLE: &str = "Debug session exited without hitting any breakpoints"; const DESCRIPTION: &str = diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 872e5fb90ba1c6..e8f51355cebae7 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -130,6 +130,9 @@ impl DebugPanelItem { DebugPanelEvent::Exited(client_id) | DebugPanelEvent::Terminated(client_id) => { this.handle_client_exited_and_terminated_event(client_id, cx); } + DebugPanelEvent::CapabilitiesChanged(client_id) => { + this.handle_capabilities_changed_event(client_id, cx); + } }; } })]; @@ -318,6 +321,18 @@ impl DebugPanelItem { cx.emit(Event::Close); } + fn handle_capabilities_changed_event( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ViewContext, + ) { + if Self::should_skip_event(self, client_id, self.thread_id) { + return; + } + + cx.notify(); + } + pub fn client_id(&self) -> DebugAdapterClientId { self.client_id } From 3b545a79ba60ee7f018683ccf86aefba558fc5bd Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 7 Oct 2024 19:39:46 +0200 Subject: [PATCH 276/650] Move stack frame list to its own view (#47) * WIP Move stack frame list to own module * Add missing notify * Remove some more duplicated state for the current stack frame * Clear highlights when stack frame has changed * Fix go to stack frame again --- crates/debugger_ui/src/console.rs | 38 ++- crates/debugger_ui/src/debugger_panel.rs | 28 +- crates/debugger_ui/src/debugger_panel_item.rs | 295 +++++------------- crates/debugger_ui/src/lib.rs | 1 + crates/debugger_ui/src/stack_frame_list.rs | 260 +++++++++++++++ crates/debugger_ui/src/variable_list.rs | 73 +++-- 6 files changed, 413 insertions(+), 282 deletions(-) create mode 100644 crates/debugger_ui/src/stack_frame_list.rs diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 6b87205da65dd6..71ec97597be865 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -1,8 +1,11 @@ -use crate::variable_list::VariableList; +use crate::{ + stack_frame_list::{StackFrameList, StackFrameListEvent}, + variable_list::VariableList, +}; use dap::client::DebugAdapterClientId; use editor::{CompletionProvider, Editor, EditorElement, EditorStyle}; use fuzzy::StringMatchCandidate; -use gpui::{Model, Render, Task, TextStyle, View, ViewContext, WeakView}; +use gpui::{Model, Render, Subscription, Task, TextStyle, View, ViewContext, WeakView}; use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16}; use menu::Confirm; use parking_lot::RwLock; @@ -16,15 +19,16 @@ pub struct Console { console: View, query_bar: View, dap_store: Model, - current_stack_frame_id: u64, client_id: DebugAdapterClientId, + _subscriptions: Vec, variable_list: View, + stack_frame_list: View, } impl Console { pub fn new( + stack_frame_list: &View, client_id: &DebugAdapterClientId, - current_stack_frame_id: u64, variable_list: View, dap_store: Model, cx: &mut ViewContext, @@ -54,24 +58,32 @@ impl Console { editor }); + let _subscriptions = + vec![cx.subscribe(stack_frame_list, Self::handle_stack_frame_list_events)]; + Self { console, dap_store, query_bar, variable_list, + _subscriptions, client_id: *client_id, - current_stack_frame_id, + stack_frame_list: stack_frame_list.clone(), } } - pub fn update_current_stack_frame_id( + fn handle_stack_frame_list_events( &mut self, - stack_frame_id: u64, + _: View, + event: &StackFrameListEvent, cx: &mut ViewContext, ) { - self.current_stack_frame_id = stack_frame_id; - - cx.notify(); + match event { + StackFrameListEvent::ChangedStackFrame => cx.notify(), + StackFrameListEvent::StackFramesUpdated => { + // TODO debugger: check if we need to do something here + } + } } pub fn add_message(&mut self, message: &str, cx: &mut ViewContext) { @@ -95,7 +107,7 @@ impl Console { let evaluate_task = self.dap_store.update(cx, |store, cx| { store.evaluate( &self.client_id, - self.current_stack_frame_id, + self.stack_frame_list.read(cx).current_stack_frame_id(), expression, dap::EvaluateArgumentsContext::Variables, cx, @@ -263,7 +275,7 @@ impl ConsoleQueryBarCompletionProvider { let mut variables = HashMap::new(); let mut string_matches = Vec::new(); - for variable in console.variable_list.read(cx).variables() { + for variable in console.variable_list.update(cx, |v, cx| v.variables(cx)) { if let Some(evaluate_name) = &variable.variable.evaluate_name { variables.insert(evaluate_name.clone(), variable.variable.value.clone()); string_matches.push(StringMatchCandidate { @@ -340,7 +352,7 @@ impl ConsoleQueryBarCompletionProvider { console.dap_store.update(cx, |store, cx| { store.completions( &console.client_id, - console.current_stack_frame_id, + console.stack_frame_list.read(cx).current_stack_frame_id(), text, buffer_position.to_offset_utf16(&snapshot).0 as u64, cx, diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a0fdd43bdc0ee8..b7179b9e651109 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -7,7 +7,7 @@ use dap::messages::{Events, Message}; use dap::requests::{Request, StartDebugging}; use dap::{ Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, ModuleEvent, OutputEvent, - StackFrame, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, + StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, }; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, @@ -46,7 +46,6 @@ actions!(debug_panel, [ToggleFocus]); #[derive(Debug, Default, Clone)] pub struct ThreadState { pub status: ThreadStatus, - pub stack_frames: Vec, // we update this value only once we stopped, // we will use this to indicated if we should show a warning when debugger thread was exited pub stopped: bool, @@ -211,7 +210,7 @@ impl DebugPanel { if let Some(active_item) = self.pane.read(cx).active_item() { if let Some(debug_item) = active_item.downcast::() { debug_item.update(cx, |panel, cx| { - panel.go_to_stack_frame(cx); + panel.go_to_current_stack_frame(cx); }); } } @@ -326,28 +325,16 @@ impl DebugPanel { cx.spawn({ let event = event.clone(); |this, mut cx| async move { - let stack_frames_task = this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - store.stack_frames(&client_id, thread_id, cx) - }) - })?; - - let stack_frames = stack_frames_task.await?; - - let current_stack_frame = stack_frames.first().unwrap().clone(); - - let thread_state = this.update(&mut cx, |this, cx| { - this.thread_states + let workspace = this.update(&mut cx, |this, cx| { + let thread_state = this + .thread_states .entry((client_id, thread_id)) .or_insert(cx.new_model(|_| ThreadState::default())) - .clone() - })?; + .clone(); - let workspace = this.update(&mut cx, |this, cx| { thread_state.update(cx, |thread_state, cx| { - thread_state.stack_frames = stack_frames; - thread_state.status = ThreadStatus::Stopped; thread_state.stopped = true; + thread_state.status = ThreadStatus::Stopped; cx.notify(); }); @@ -375,7 +362,6 @@ impl DebugPanel { &client_id, &client_kind, thread_id, - current_stack_frame.clone().id, cx, ) }); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index e8f51355cebae7..2b2a0e34295b12 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,23 +1,21 @@ -use std::path::Path; - use crate::console::Console; use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState}; use crate::module_list::ModuleList; +use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; use dap::client::{DebugAdapterClientId, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; use dap::{ - Capabilities, ContinuedEvent, ModuleEvent, OutputEvent, OutputEventCategory, StackFrame, - StoppedEvent, ThreadEvent, + Capabilities, ContinuedEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, + ThreadEvent, }; use editor::Editor; use gpui::{ - list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, ListState, Model, - Subscription, View, WeakView, + AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, View, + WeakView, }; use project::dap_store::DapStore; -use project::ProjectPath; use settings::Settings; use task::DebugAdapterKind; use ui::WindowContext; @@ -25,8 +23,10 @@ use ui::{prelude::*, Tooltip}; use workspace::item::{Item, ItemEvent}; use workspace::Workspace; -pub enum Event { +#[derive(Debug)] +pub enum DebugPanelItemEvent { Close, + Stopped { go_to_stack_frame: bool }, } #[derive(Clone, PartialEq, Eq)] @@ -42,9 +42,7 @@ pub struct DebugPanelItem { console: View, focus_handle: FocusHandle, dap_store: Model, - stack_frame_list: ListState, output_editor: View, - current_stack_frame_id: u64, module_list: View, client_kind: DebugAdapterKind, active_thread_item: ThreadItem, @@ -53,6 +51,7 @@ pub struct DebugPanelItem { thread_state: Model, variable_list: View, _subscriptions: Vec, + stack_frame_list: View, } impl DebugPanelItem { @@ -65,77 +64,72 @@ impl DebugPanelItem { client_id: &DebugAdapterClientId, client_kind: &DebugAdapterKind, thread_id: u64, - current_stack_frame_id: u64, cx: &mut ViewContext, ) -> Self { let focus_handle = cx.focus_handle(); - let variable_list = cx.new_view(|cx| { - VariableList::new( - dap_store.clone(), - &client_id, - &thread_state, - current_stack_frame_id, - cx, - ) + let this = cx.view().clone(); + let stack_frame_list = cx.new_view(|cx| { + StackFrameList::new(&workspace, &this, &dap_store, client_id, thread_id, cx) }); + let variable_list = cx + .new_view(|cx| VariableList::new(&stack_frame_list, dap_store.clone(), &client_id, cx)); + let module_list = cx.new_view(|cx| ModuleList::new(dap_store.clone(), &client_id, cx)); let console = cx.new_view(|cx| { Console::new( + &stack_frame_list, client_id, - current_stack_frame_id, variable_list.clone(), dap_store.clone(), cx, ) }); - let weakview = cx.view().downgrade(); - let stack_frame_list = - ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { - if let Some(view) = weakview.upgrade() { - view.update(cx, |view, cx| { - view.render_stack_frame(ix, cx).into_any_element() - }) - } else { - div().into_any() + let _subscriptions = vec![ + cx.subscribe(&debug_panel, { + move |this: &mut Self, _, event: &DebugPanelEvent, cx| { + match event { + DebugPanelEvent::Stopped { + client_id, + event, + go_to_stack_frame, + } => this.handle_stopped_event(client_id, event, *go_to_stack_frame, cx), + DebugPanelEvent::Thread((client_id, event)) => { + this.handle_thread_event(client_id, event, cx) + } + DebugPanelEvent::Output((client_id, event)) => { + this.handle_output_event(client_id, event, cx) + } + DebugPanelEvent::Module((client_id, event)) => { + this.handle_module_event(client_id, event, cx) + } + DebugPanelEvent::ClientStopped(client_id) => { + this.handle_client_stopped_event(client_id, cx) + } + DebugPanelEvent::Continued((client_id, event)) => { + this.handle_thread_continued_event(client_id, event, cx); + } + DebugPanelEvent::Exited(client_id) + | DebugPanelEvent::Terminated(client_id) => { + this.handle_client_exited_and_terminated_event(client_id, cx); + } + DebugPanelEvent::CapabilitiesChanged(client_id) => { + this.handle_capabilities_changed_event(client_id, cx); + } + }; } - }); - - let _subscriptions = vec![cx.subscribe(&debug_panel, { - move |this: &mut Self, _, event: &DebugPanelEvent, cx| { - match event { - DebugPanelEvent::Stopped { - client_id, - event, - go_to_stack_frame, - } => this.handle_stopped_event(client_id, event, *go_to_stack_frame, cx), - DebugPanelEvent::Thread((client_id, event)) => { - this.handle_thread_event(client_id, event, cx) - } - DebugPanelEvent::Output((client_id, event)) => { - this.handle_output_event(client_id, event, cx) - } - DebugPanelEvent::Module((client_id, event)) => { - this.handle_module_event(client_id, event, cx) - } - DebugPanelEvent::ClientStopped(client_id) => { - this.handle_client_stopped_event(client_id, cx) - } - DebugPanelEvent::Continued((client_id, event)) => { - this.handle_thread_continued_event(client_id, event, cx); - } - DebugPanelEvent::Exited(client_id) | DebugPanelEvent::Terminated(client_id) => { - this.handle_client_exited_and_terminated_event(client_id, cx); - } - DebugPanelEvent::CapabilitiesChanged(client_id) => { - this.handle_capabilities_changed_event(client_id, cx); - } - }; - } - })]; + }), + cx.subscribe( + &stack_frame_list, + move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { + StackFrameListEvent::ChangedStackFrame => this.clear_highlights(cx), + StackFrameListEvent::StackFramesUpdated => {} + }, + ), + ]; let output_editor = cx.new_view(|cx| { let mut editor = Editor::multi_line(cx); @@ -164,7 +158,6 @@ impl DebugPanelItem { _subscriptions, stack_frame_list, client_id: *client_id, - current_stack_frame_id, client_kind: client_kind.clone(), active_thread_item: ThreadItem::Variables, } @@ -215,18 +208,7 @@ impl DebugPanelItem { return; } - let thread_state = self.thread_state.read(cx); - - self.stack_frame_list.reset(thread_state.stack_frames.len()); - if let Some(stack_frame) = thread_state.stack_frames.first() { - self.update_stack_frame_id(stack_frame.id, go_to_stack_frame, cx); - }; - - self.variable_list.update(cx, |variable_list, cx| { - variable_list.on_stopped_event(cx); - }); - - cx.notify(); + cx.emit(DebugPanelItemEvent::Stopped { go_to_stack_frame }); } fn handle_thread_event( @@ -304,7 +286,7 @@ impl DebugPanelItem { self.update_thread_state_status(ThreadStatus::Stopped, cx); - cx.emit(Event::Close); + cx.emit(DebugPanelItemEvent::Close); } fn handle_client_exited_and_terminated_event( @@ -318,7 +300,7 @@ impl DebugPanelItem { self.update_thread_state_status(ThreadStatus::Exited, cx); - cx.emit(Event::Close); + cx.emit(DebugPanelItemEvent::Close); } fn handle_capabilities_changed_event( @@ -346,34 +328,6 @@ impl DebugPanelItem { .read_with(cx, |store, _| store.capabilities_by_id(&self.client_id)) } - fn stack_frame_for_index(&self, ix: usize, cx: &mut ViewContext) -> StackFrame { - self.thread_state.read(cx).stack_frames[ix].clone() - } - - fn update_stack_frame_id( - &mut self, - stack_frame_id: u64, - go_to_stack_frame: bool, - cx: &mut ViewContext, - ) { - self.current_stack_frame_id = stack_frame_id; - - self.variable_list.update(cx, |variable_list, cx| { - variable_list.update_stack_frame_id(stack_frame_id, cx); - variable_list.build_entries(true, false, cx); - }); - - self.console.update(cx, |console, cx| { - console.update_current_stack_frame_id(stack_frame_id, cx); - }); - - if go_to_stack_frame { - self.go_to_stack_frame(cx); - } - - cx.notify(); - } - fn clear_highlights(&self, cx: &mut ViewContext) { self.workspace .update(cx, |workspace, cx| { @@ -390,118 +344,12 @@ impl DebugPanelItem { .ok(); } - pub fn project_path_from_stack_frame( - &self, - stack_frame: &StackFrame, - cx: &mut ViewContext, - ) -> Option { - let path = stack_frame.source.as_ref().and_then(|s| s.path.as_ref())?; - - self.workspace - .update(cx, |workspace, cx| { - workspace.project().read_with(cx, |project, cx| { - project.project_path_for_absolute_path(&Path::new(path), cx) - }) - }) - .ok()? - } - - pub fn go_to_stack_frame(&mut self, cx: &mut ViewContext) { - self.clear_highlights(cx); - - let stack_frame = self - .thread_state - .read(cx) - .stack_frames - .iter() - .find(|s| s.id == self.current_stack_frame_id) - .cloned(); - - let Some(stack_frame) = stack_frame else { - return; // this could never happen - }; - - let row = (stack_frame.line.saturating_sub(1)) as u32; - let column = (stack_frame.column.saturating_sub(1)) as u32; - - let Some(project_path) = self.project_path_from_stack_frame(&stack_frame, cx) else { - return; - }; - - self.dap_store.update(cx, |store, cx| { - store.set_active_debug_line(&project_path, row, column, cx); + pub fn go_to_current_stack_frame(&self, cx: &mut ViewContext) { + self.stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list + .go_to_stack_frame(cx) + .detach_and_log_err(cx); }); - - cx.spawn({ - let workspace = self.workspace.clone(); - move |_, mut cx| async move { - let task = workspace.update(&mut cx, |workspace, cx| { - workspace.open_path_preview(project_path, None, false, true, cx) - })?; - - let editor = task.await?.downcast::().unwrap(); - - workspace.update(&mut cx, |_, cx| { - editor.update(cx, |editor, cx| editor.go_to_active_debug_line(cx)) - }) - } - }) - .detach_and_log_err(cx); - } - - fn render_stack_frames(&self, _cx: &mut ViewContext) -> impl IntoElement { - v_flex() - .size_full() - .child(list(self.stack_frame_list.clone()).size_full()) - .into_any() - } - - fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { - let stack_frame = self.stack_frame_for_index(ix, cx); - - let source = stack_frame.source.clone(); - let is_selected_frame = stack_frame.id == self.current_stack_frame_id; - - let formatted_path = format!( - "{}:{}", - source.clone().and_then(|s| s.name).unwrap_or_default(), - stack_frame.line, - ); - - v_flex() - .rounded_md() - .w_full() - .group("") - .id(("stack-frame", stack_frame.id)) - .tooltip({ - let formatted_path = formatted_path.clone(); - move |cx| Tooltip::text(formatted_path.clone(), cx) - }) - .p_1() - .when(is_selected_frame, |this| { - this.bg(cx.theme().colors().element_hover) - }) - .on_click(cx.listener({ - let stack_frame_id = stack_frame.id; - move |this, _, cx| { - this.update_stack_frame_id(stack_frame_id, true, cx); - } - })) - .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) - .child( - h_flex() - .gap_0p5() - .text_ui_sm(cx) - .child(stack_frame.name.clone()) - .child(formatted_path), - ) - .child( - h_flex() - .text_ui_xs(cx) - .text_color(cx.theme().colors().text_muted) - .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), - ) - .into_any() } fn render_entry_button( @@ -520,8 +368,10 @@ impl DebugPanelItem { this.border_color(cx.theme().colors().border) }) .child(Button::new(label.clone(), label.clone())) - .on_click(cx.listener(move |this, _, _| { + .on_click(cx.listener(move |this, _, cx| { this.active_thread_item = thread_item.clone(); + + cx.notify(); })) .into_any_element() } @@ -605,7 +455,7 @@ impl DebugPanelItem { } } -impl EventEmitter for DebugPanelItem {} +impl EventEmitter for DebugPanelItem {} impl FocusableView for DebugPanelItem { fn focus_handle(&self, _: &AppContext) -> FocusHandle { @@ -614,7 +464,7 @@ impl FocusableView for DebugPanelItem { } impl Item for DebugPanelItem { - type Event = Event; + type Event = DebugPanelItemEvent; fn tab_content( &self, @@ -644,7 +494,8 @@ impl Item for DebugPanelItem { fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { match event { - Event::Close => f(ItemEvent::CloseItem), + DebugPanelItemEvent::Close => f(ItemEvent::CloseItem), + DebugPanelItemEvent::Stopped { .. } => {} } } } @@ -765,7 +616,7 @@ impl Render for DebugPanelItem { .items_start() .p_1() .gap_4() - .child(self.render_stack_frames(cx)), + .child(self.stack_frame_list.clone()), ), ) .child( diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index f69d521c7a7c90..a45a6e5227f49f 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -12,6 +12,7 @@ mod console; pub mod debugger_panel; mod debugger_panel_item; mod module_list; +mod stack_frame_list; mod variable_list; pub fn init(cx: &mut AppContext) { diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs new file mode 100644 index 00000000000000..6d31097339582a --- /dev/null +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -0,0 +1,260 @@ +use std::path::Path; + +use anyhow::{anyhow, Result}; +use dap::client::DebugAdapterClientId; +use dap::StackFrame; +use editor::Editor; +use gpui::{ + list, AnyElement, EventEmitter, FocusHandle, ListState, Subscription, Task, View, WeakView, +}; +use gpui::{FocusableView, Model}; +use project::dap_store::DapStore; +use project::ProjectPath; +use ui::ViewContext; +use ui::{prelude::*, Tooltip}; +use workspace::Workspace; + +use crate::debugger_panel_item::DebugPanelItemEvent::Stopped; +use crate::debugger_panel_item::{self, DebugPanelItem}; + +#[derive(Debug)] +pub enum StackFrameListEvent { + ChangedStackFrame, + StackFramesUpdated, +} + +pub struct StackFrameList { + thread_id: u64, + list: ListState, + focus_handle: FocusHandle, + dap_store: Model, + current_stack_frame_id: u64, + stack_frames: Vec, + workspace: WeakView, + client_id: DebugAdapterClientId, + _subscriptions: Vec, +} + +impl StackFrameList { + pub fn new( + workspace: &WeakView, + debug_panel_item: &View, + dap_store: &Model, + client_id: &DebugAdapterClientId, + thread_id: u64, + cx: &mut ViewContext, + ) -> Self { + let weakview = cx.view().downgrade(); + let focus_handle = cx.focus_handle(); + + let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { + weakview + .upgrade() + .map(|view| view.update(cx, |this, cx| this.render_entry(ix, cx))) + .unwrap_or(div().into_any()) + }); + + let _subscriptions = + vec![cx.subscribe(debug_panel_item, Self::handle_debug_panel_item_event)]; + + Self { + list, + thread_id, + focus_handle, + _subscriptions, + client_id: *client_id, + workspace: workspace.clone(), + dap_store: dap_store.clone(), + stack_frames: Default::default(), + current_stack_frame_id: Default::default(), + } + } + + pub fn stack_frames(&self) -> &Vec { + &self.stack_frames + } + + pub fn current_stack_frame_id(&self) -> u64 { + self.current_stack_frame_id + } + + fn handle_debug_panel_item_event( + &mut self, + _: View, + event: &debugger_panel_item::DebugPanelItemEvent, + cx: &mut ViewContext, + ) { + match event { + Stopped { go_to_stack_frame } => { + self.fetch_stack_frames(*go_to_stack_frame, cx) + .detach_and_log_err(cx); + } + _ => {} + } + } + + fn fetch_stack_frames( + &self, + go_to_stack_frame: bool, + cx: &mut ViewContext, + ) -> Task> { + let task = self.dap_store.update(cx, |store, cx| { + store.stack_frames(&self.client_id, self.thread_id, cx) + }); + + cx.spawn(|this, mut cx| async move { + let mut stack_frames = task.await?; + + let task = this.update(&mut cx, |this, cx| { + std::mem::swap(&mut this.stack_frames, &mut stack_frames); + + if let Some(stack_frame) = this.stack_frames.first() { + this.current_stack_frame_id = stack_frame.id; + cx.emit(StackFrameListEvent::ChangedStackFrame); + } + + this.list.reset(this.stack_frames.len()); + cx.notify(); + + cx.emit(StackFrameListEvent::StackFramesUpdated); + + if go_to_stack_frame { + Some(this.go_to_stack_frame(cx)) + } else { + None + } + })?; + + if let Some(task) = task { + task.await?; + } + + Ok(()) + }) + } + + pub fn go_to_stack_frame(&mut self, cx: &mut ViewContext) -> Task> { + let stack_frame = self + .stack_frames + .iter() + .find(|s| s.id == self.current_stack_frame_id) + .cloned(); + + let Some(stack_frame) = stack_frame else { + return Task::ready(Ok(())); // this could never happen + }; + + let row = (stack_frame.line.saturating_sub(1)) as u32; + let column = (stack_frame.column.saturating_sub(1)) as u32; + + let Some(project_path) = self.project_path_from_stack_frame(&stack_frame, cx) else { + return Task::ready(Err(anyhow!("Project path not found"))); + }; + + self.dap_store.update(cx, |store, cx| { + store.set_active_debug_line(&project_path, row, column, cx); + }); + + cx.spawn({ + let workspace = self.workspace.clone(); + move |_, mut cx| async move { + let task = workspace.update(&mut cx, |workspace, cx| { + workspace.open_path_preview(project_path, None, false, true, cx) + })?; + + let editor = task.await?.downcast::().unwrap(); + + workspace.update(&mut cx, |_, cx| { + editor.update(cx, |editor, cx| editor.go_to_active_debug_line(cx)) + }) + } + }) + } + + pub fn project_path_from_stack_frame( + &self, + stack_frame: &StackFrame, + cx: &mut ViewContext, + ) -> Option { + let path = stack_frame.source.as_ref().and_then(|s| s.path.as_ref())?; + + self.workspace + .update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.project_path_for_absolute_path(&Path::new(path), cx) + }) + }) + .ok()? + } + + fn render_entry(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let stack_frame = &self.stack_frames[ix]; + + let source = stack_frame.source.clone(); + let is_selected_frame = stack_frame.id == self.current_stack_frame_id; + + let formatted_path = format!( + "{}:{}", + source.clone().and_then(|s| s.name).unwrap_or_default(), + stack_frame.line, + ); + + v_flex() + .rounded_md() + .w_full() + .group("") + .id(("stack-frame", stack_frame.id)) + .tooltip({ + let formatted_path = formatted_path.clone(); + move |cx| Tooltip::text(formatted_path.clone(), cx) + }) + .p_1() + .when(is_selected_frame, |this| { + this.bg(cx.theme().colors().element_hover) + }) + .on_click(cx.listener({ + let stack_frame_id = stack_frame.id; + move |this, _, cx| { + this.current_stack_frame_id = stack_frame_id; + + this.go_to_stack_frame(cx).detach_and_log_err(cx); + + cx.notify(); + + cx.emit(StackFrameListEvent::ChangedStackFrame); + } + })) + .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .child( + h_flex() + .gap_0p5() + .text_ui_sm(cx) + .child(stack_frame.name.clone()) + .child(formatted_path), + ) + .child( + h_flex() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), + ) + .into_any() + } +} + +impl Render for StackFrameList { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + div() + .size_full() + .p_1() + .child(list(self.list.clone()).size_full()) + } +} + +impl FocusableView for StackFrameList { + fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl EventEmitter for StackFrameList {} diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index c9cbf8e056afb0..f2d407e57887cc 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,4 +1,4 @@ -use crate::debugger_panel::ThreadState; +use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use anyhow::Result; use dap::{client::DebugAdapterClientId, Scope, Variable}; use editor::{ @@ -55,13 +55,13 @@ pub struct VariableList { list: ListState, dap_store: Model, focus_handle: FocusHandle, - current_stack_frame_id: u64, client_id: DebugAdapterClientId, open_entries: Vec, scopes: HashMap>, - thread_state: Model, set_variable_editor: View, + _subscriptions: Vec, fetched_variable_ids: HashSet, + stack_frame_list: View, set_variable_state: Option, entries: HashMap>, fetch_variables_task: Option>>, @@ -72,10 +72,9 @@ pub struct VariableList { impl VariableList { pub fn new( + stack_frame_list: &View, dap_store: Model, client_id: &DebugAdapterClientId, - thread_state: &Model, - current_stack_frame_id: u64, cx: &mut ViewContext, ) -> Self { let weakview = cx.view().downgrade(); @@ -100,13 +99,16 @@ impl VariableList { ) .detach(); + let _subscriptions = + vec![cx.subscribe(stack_frame_list, Self::handle_stack_frame_list_events)]; + Self { list, dap_store, focus_handle, + _subscriptions, set_variable_editor, client_id: *client_id, - current_stack_frame_id, open_context_menu: None, set_variable_state: None, fetch_variables_task: None, @@ -114,20 +116,40 @@ impl VariableList { entries: Default::default(), variables: Default::default(), open_entries: Default::default(), - thread_state: thread_state.clone(), fetched_variable_ids: Default::default(), + stack_frame_list: stack_frame_list.clone(), + } + } + + fn handle_stack_frame_list_events( + &mut self, + _: View, + event: &StackFrameListEvent, + cx: &mut ViewContext, + ) { + match event { + StackFrameListEvent::ChangedStackFrame => { + self.build_entries(true, false, cx); + } + StackFrameListEvent::StackFramesUpdated => { + self.fetch_variables(cx); + } } } - pub fn variables(&self) -> Vec { + pub fn variables(&self, cx: &mut ViewContext) -> Vec { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + self.variables - .range((self.current_stack_frame_id, u64::MIN)..(self.current_stack_frame_id, u64::MAX)) + .range((stack_frame_id, u64::MIN)..(stack_frame_id, u64::MAX)) .flat_map(|(_, containers)| containers.iter().cloned()) .collect() } fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let Some(entries) = self.entries.get(&self.current_stack_frame_id) else { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + + let Some(entries) = self.entries.get(&stack_frame_id) else { return div().into_any_element(); }; @@ -167,19 +189,15 @@ impl VariableList { self.build_entries(false, true, cx); } - pub fn update_stack_frame_id(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { - self.current_stack_frame_id = stack_frame_id; - - cx.notify(); - } - pub fn build_entries( &mut self, open_first_scope: bool, keep_open_entries: bool, cx: &mut ViewContext, ) { - let Some(scopes) = self.scopes.get(&self.current_stack_frame_id) else { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + + let Some(scopes) = self.scopes.get(&stack_frame_id) else { return; }; @@ -191,7 +209,7 @@ impl VariableList { for scope in scopes { let Some(variables) = self .variables - .get(&(self.current_stack_frame_id, scope.variables_reference)) + .get(&(stack_frame_id, scope.variables_reference)) else { continue; }; @@ -261,14 +279,14 @@ impl VariableList { } let len = entries.len(); - self.entries.insert(self.current_stack_frame_id, entries); + self.entries.insert(stack_frame_id, entries); self.list.reset(len); cx.notify(); } - pub fn on_stopped_event(&mut self, cx: &mut ViewContext) { - let stack_frames = self.thread_state.read(cx).stack_frames.clone(); + fn fetch_variables(&mut self, cx: &mut ViewContext) { + let stack_frames = self.stack_frame_list.read(cx).stack_frames().clone(); self.fetch_variables_task.take(); self.variables.clear(); @@ -339,6 +357,8 @@ impl VariableList { this.build_entries(true, false, cx); this.fetch_variables_task.take(); + + cx.notify(); }) })); } @@ -353,7 +373,6 @@ impl VariableList { ) { let this = cx.view().clone(); - let stack_frame_id = self.current_stack_frame_id; let support_set_variable = self.dap_store.read_with(cx, |store, _| { store .capabilities_by_id(&self.client_id) @@ -410,7 +429,7 @@ impl VariableList { scope: scope.clone(), evaluate_name: variable.evaluate_name.clone(), value: variable.value.clone(), - stack_frame_id, + stack_frame_id: this.stack_frame_list.read(cx).current_stack_frame_id(), }); this.set_variable_editor.update(cx, |editor, cx| { @@ -461,7 +480,8 @@ impl VariableList { return cx.notify(); }; - if new_variable_value == state.value || state.stack_frame_id != self.current_stack_frame_id + if new_variable_value == state.value + || state.stack_frame_id != self.stack_frame_list.read(cx).current_stack_frame_id() { return cx.notify(); } @@ -590,7 +610,9 @@ impl VariableList { return self.toggle_entry_collapsed(&variable_id, cx); } - let Some(entries) = self.entries.get(&self.current_stack_frame_id) else { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + + let Some(entries) = self.entries.get(&stack_frame_id) else { return; }; @@ -602,7 +624,6 @@ impl VariableList { let variable_id = variable_id.clone(); let scope = scope.clone(); let depth = *depth; - let stack_frame_id = self.current_stack_frame_id; let fetch_variables_task = self.dap_store.update(cx, |store, cx| { store.variables(&self.client_id, variable_reference, cx) From 984cb686a8cfa30152c42d3b911e8b654a7f5823 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 7 Oct 2024 20:02:55 +0200 Subject: [PATCH 277/650] Merge main --- Cargo.lock | 66 +++++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efd91a1b67e938..797685fbab40a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,9 +893,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -2085,9 +2085,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.20.4" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad639525b1c67b6a298f378417b060fbc04618bea559482a8484381cce27d965" +checksum = "88da5a13c620b4ca0078845707ea9c3faf11edbc3ffd8497d11d686211cd1ac0" dependencies = [ "serde", "toml 0.8.19", @@ -2275,9 +2275,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" dependencies = [ "clap_builder", "clap_derive", @@ -2285,9 +2285,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" dependencies = [ "anstream", "anstyle", @@ -2307,9 +2307,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -6498,9 +6498,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libdbus-sys" @@ -7910,9 +7910,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -8420,9 +8420,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", @@ -10237,9 +10237,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "indexmap 2.4.0", "itoa", @@ -10780,9 +10780,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ "nom", "unicode_categories", @@ -11617,12 +11617,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ "rustix 0.38.35", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -11747,18 +11747,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -12073,9 +12073,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes 1.7.1", "futures-core", @@ -13217,9 +13217,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -14572,9 +14572,9 @@ dependencies = [ [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yazi" From 187d9097368ac3de9564bb1b7a591ed6446237ea Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:33:03 -0400 Subject: [PATCH 278/650] DapAdapter Updates (#40) * Pass http client to dap store * Set up DapAdapterDelegate to use for DAP binary fetches * WIP to get debug adapters to use zed directory for installs * Get debugpy automatic download working * Change DapAdapter fetch_or_install to return a Result * Add node_runtime to dap delegate Co-authored-by: Remco Smits * Create dap_adapter crate & move language dap adapter code to that crate This is the first phase of dap::client refactor to organize debug adapters with zed lsp adapters. Eventually dap::client will have a TransportParams pass to it instead of an adapter, and adapters will handle custom debug events. Co-authored-by: Remco Smits * Move language specific dap adapter code to their own files * Move DebugAdapter member out of ClientDebugAdapter struct This change was done to make dap::client more in line with zed's lsp::client, it might be reverted depending on if this solution is cleaner or not. * Get php debug adapter to auto download when needed * Get adapter_path argument to work with auto download dap adapters --------- Co-authored-by: Remco Smits --- Cargo.lock | 90 ++++-- Cargo.toml | 2 + crates/dap/Cargo.toml | 5 + crates/dap/src/adapters.rs | 307 ++++--------------- crates/dap/src/client.rs | 23 +- crates/dap/src/lib.rs | 2 +- crates/dap_adapters/Cargo.toml | 28 ++ crates/dap_adapters/src/custom.rs | 63 ++++ crates/dap_adapters/src/dap_adapters.rs | 41 +++ crates/dap_adapters/src/javascript.rs | 0 crates/dap_adapters/src/lldb.rs | 44 +++ crates/dap_adapters/src/php.rs | 153 +++++++++ crates/dap_adapters/src/python.rs | 144 +++++++++ crates/paths/src/paths.rs | 8 + crates/project/Cargo.toml | 1 + crates/project/src/dap_store.rs | 83 ++++- crates/project/src/project.rs | 22 +- crates/remote_server/src/headless_project.rs | 2 +- 18 files changed, 723 insertions(+), 295 deletions(-) create mode 100644 crates/dap_adapters/Cargo.toml create mode 100644 crates/dap_adapters/src/custom.rs create mode 100644 crates/dap_adapters/src/dap_adapters.rs create mode 100644 crates/dap_adapters/src/javascript.rs create mode 100644 crates/dap_adapters/src/lldb.rs create mode 100644 crates/dap_adapters/src/php.rs create mode 100644 crates/dap_adapters/src/python.rs diff --git a/Cargo.lock b/Cargo.lock index eca04c7508ce35..7eecdda1e3308b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,9 +893,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", @@ -2085,9 +2085,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.20.5" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88da5a13c620b4ca0078845707ea9c3faf11edbc3ffd8497d11d686211cd1ac0" +checksum = "ad639525b1c67b6a298f378417b060fbc04618bea559482a8484381cce27d965" dependencies = [ "serde", "toml 0.8.19", @@ -2275,9 +2275,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -2285,9 +2285,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -2307,9 +2307,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3346,9 +3346,14 @@ dependencies = [ "anyhow", "async-trait", "dap-types", + "fs", "futures 0.3.30", "gpui", + "http_client", + "log", + "node_runtime", "parking_lot", + "paths", "schemars", "serde", "serde_json", @@ -3366,6 +3371,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "dap_adapters" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "dap", + "fs", + "futures 0.3.30", + "gpui", + "http_client", + "paths", + "serde", + "serde_json", + "smol", + "task", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -6498,9 +6521,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libdbus-sys" @@ -7910,9 +7933,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -8420,9 +8443,9 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "1.4.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", "yansi", @@ -8510,6 +8533,7 @@ dependencies = [ "clock", "collections", "dap", + "dap_adapters", "dev_server_projects", "env_logger", "fs", @@ -10237,9 +10261,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "indexmap 2.4.0", "itoa", @@ -10781,9 +10805,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.6" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ "nom", "unicode_categories", @@ -11620,12 +11644,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix 0.38.35", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -11750,18 +11774,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -12076,9 +12100,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes 1.7.1", "futures-core", @@ -13220,9 +13244,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" dependencies = [ "futures-util", "js-sys", @@ -14576,9 +14600,9 @@ dependencies = [ [[package]] name = "yansi" -version = "1.0.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "yazi" diff --git a/Cargo.toml b/Cargo.toml index 9c6b2ab5eaaecb..84fdc691ea321f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "crates/context_servers", "crates/copilot", "crates/dap", + "crates/dap_adapters", "crates/debugger_ui", "crates/db", "crates/dev_server_projects", @@ -207,6 +208,7 @@ command_palette_hooks = { path = "crates/command_palette_hooks" } context_servers = { path = "crates/context_servers" } copilot = { path = "crates/copilot" } dap = { path = "crates/dap" } +dap_adapters = { path = "crates/dap_adapters" } db = { path = "crates/db" } debugger_ui = { path = "crates/debugger_ui" } dev_server_projects = { path = "crates/dev_server_projects" } diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 3e73958f8069ad..3ed84886ac7040 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -12,9 +12,14 @@ workspace = true anyhow.workspace = true async-trait.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types" } +fs.workspace = true futures.workspace = true gpui.workspace = true +http_client.workspace = true +log.workspace = true +node_runtime.workspace = true parking_lot.workspace = true +paths.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index cfb4731916d1cd..c7aee65877ad96 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,10 +1,12 @@ use crate::client::TransportParams; +use ::fs::Fs; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::AsyncReadExt; use gpui::AsyncAppContext; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use http_client::HttpClient; +use node_runtime::NodeRuntime; +use serde_json::Value; use smol::{ self, io::BufReader, @@ -12,26 +14,17 @@ use smol::{ process, }; use std::{ + collections::HashMap, + ffi::OsString, fmt::Debug, net::{Ipv4Addr, SocketAddrV4}, - path::PathBuf, + path::{Path, PathBuf}, process::Stdio, sync::Arc, time::Duration, }; -use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; -pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { - match &adapter_config.kind { - DebugAdapterKind::Custom(start_args) => Ok(Box::new(CustomDebugAdapter::new( - adapter_config, - start_args.clone(), - ))), - DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new(adapter_config))), - DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new(adapter_config))), - DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new(adapter_config))), - } -} +use task::TCPHost; /// Get an open port to use with the tcp client when not supplied by debug config async fn get_port(host: Ipv4Addr) -> Option { @@ -45,19 +38,20 @@ async fn get_port(host: Ipv4Addr) -> Option { ) } -/// Creates a debug client that connects to an adapter through tcp -/// +pub trait DapDelegate { + fn http_client(&self) -> Option>; + fn node_runtime(&self) -> Option; + fn fs(&self) -> Arc; +} + /// TCP clients don't have an error communication stream with an adapter -/// /// # Parameters -/// - `command`: The command that starts the debugger -/// - `args`: Arguments of the command that starts the debugger -/// - `cwd`: The absolute path of the project that is being debugged +/// - `host`: The ip/port that that the client will connect too +/// - `adapter_binary`: The debug adapter binary to start /// - `cx`: The context that the new client belongs too -async fn create_tcp_client( +pub async fn create_tcp_client( host: TCPHost, - command: &String, - args: &Vec, + adapter_binary: DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result { let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); @@ -66,10 +60,17 @@ async fn create_tcp_client( if port.is_none() { port = get_port(host_address).await; } + let mut command = if let Some(start_command) = &adapter_binary.start_command { + let mut command = process::Command::new(start_command); + command.arg(adapter_binary.path); + command + } else { + process::Command::new(adapter_binary.path) + }; - let mut command = process::Command::new(command); command - .args(args) + .args(adapter_binary.arguments) + .envs(adapter_binary.env.clone().unwrap_or_default()) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) @@ -93,6 +94,7 @@ async fn create_tcp_client( ); let (rx, tx) = TcpStream::connect(address).await?.split(); + log::info!("Debug adapter has connected to tcp server"); Ok(TransportParams::new( Box::new(BufReader::new(rx)), @@ -105,12 +107,20 @@ async fn create_tcp_client( /// Creates a debug client that connects to an adapter through std input/output /// /// # Parameters -/// - `command`: The command that starts the debugger -/// - `args`: Arguments of the command that starts the debugger -fn create_stdio_client(command: &String, args: &Vec) -> Result { - let mut command = process::Command::new(command); +/// - `adapter_binary`: The debug adapter binary to start +pub fn create_stdio_client(adapter_binary: DebugAdapterBinary) -> Result { + let mut command = if let Some(start_command) = &adapter_binary.start_command { + let mut command = process::Command::new(start_command); + command.arg(adapter_binary.path); + command + } else { + let command = process::Command::new(adapter_binary.path); + command + }; + command - .args(args) + .args(adapter_binary.arguments) + .envs(adapter_binary.env.clone().unwrap_or_default()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -133,6 +143,8 @@ fn create_stdio_client(command: &String, args: &Vec) -> Result) -> Result); +impl AsRef for DebugAdapterName { + fn as_ref(&self) -> &Path { + Path::new(&*self.0) + } +} + +impl std::fmt::Display for DebugAdapterName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} + +#[derive(Debug, Clone)] pub struct DebugAdapterBinary { + pub start_command: Option, pub path: PathBuf, + pub arguments: Vec, + pub env: Option>, } #[async_trait(?Send)] @@ -156,214 +183,16 @@ pub trait DebugAdapter: Debug + Send + Sync + 'static { fn name(&self) -> DebugAdapterName; - async fn connect(&self, cx: &mut AsyncAppContext) -> anyhow::Result; - - fn is_installed(&self) -> Option; + async fn connect( + &self, + adapter_binary: DebugAdapterBinary, + cx: &mut AsyncAppContext, + ) -> anyhow::Result; - fn download_adapter(&self) -> anyhow::Result; + async fn install_or_fetch_binary( + &self, + delegate: Box, + ) -> Result; fn request_args(&self) -> Value; } - -#[derive(Debug, Eq, PartialEq, Clone)] -struct CustomDebugAdapter { - start_command: String, - initialize_args: Option>, - program: String, - connection: DebugConnectionType, -} - -impl CustomDebugAdapter { - const _ADAPTER_NAME: &'static str = "custom_dap"; - - fn new(adapter_config: &DebugAdapterConfig, custom_args: CustomArgs) -> Self { - CustomDebugAdapter { - start_command: custom_args.start_command, - program: adapter_config.program.clone(), - connection: custom_args.connection, - initialize_args: adapter_config.initialize_args.clone(), - } - } -} - -#[async_trait(?Send)] -impl DebugAdapter for CustomDebugAdapter { - fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::_ADAPTER_NAME.into()) - } - - async fn connect(&self, cx: &mut AsyncAppContext) -> Result { - match &self.connection { - DebugConnectionType::STDIO => create_stdio_client(&self.start_command, &vec![]), - DebugConnectionType::TCP(tcp_host) => { - create_tcp_client(tcp_host.clone(), &self.start_command, &vec![], cx).await - } - } - } - - fn is_installed(&self) -> Option { - None - } - - fn download_adapter(&self) -> anyhow::Result { - Err(anyhow::format_err!("Not implemented")) - } - - fn request_args(&self) -> Value { - let base_args = json!({ - "program": format!("{}", &self.program) - }); - - // TODO Debugger: Figure out a way to combine this with base args - // if let Some(args) = &self.initialize_args { - // let args = json!(args.clone()).as_object().into_iter(); - // base_args.as_object_mut().unwrap().extend(args); - // } - - base_args - } -} - -#[derive(Debug, Eq, PartialEq, Clone)] -struct PythonDebugAdapter { - program: String, - adapter_path: Option, -} - -impl PythonDebugAdapter { - const _ADAPTER_NAME: &'static str = "debugpy"; - - fn new(adapter_config: &DebugAdapterConfig) -> Self { - PythonDebugAdapter { - program: adapter_config.program.clone(), - adapter_path: adapter_config.adapter_path.clone(), - } - } -} - -#[async_trait(?Send)] -impl DebugAdapter for PythonDebugAdapter { - fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::_ADAPTER_NAME.into()) - } - - async fn connect(&self, _cx: &mut AsyncAppContext) -> Result { - let command = "python3".to_string(); - - let args = if let Some(path) = self.adapter_path.clone() { - vec![path] - } else { - Vec::new() - }; - - create_stdio_client(&command, &args) - } - - fn is_installed(&self) -> Option { - None - } - - fn download_adapter(&self) -> anyhow::Result { - Err(anyhow::format_err!("Not implemented")) - } - - fn request_args(&self) -> Value { - json!({"program": format!("{}", &self.program)}) - } -} - -#[derive(Debug, Eq, PartialEq, Clone)] -struct PhpDebugAdapter { - program: String, - adapter_path: Option, -} - -impl PhpDebugAdapter { - const _ADAPTER_NAME: &'static str = "vscode-php-debug"; - - fn new(adapter_config: &DebugAdapterConfig) -> Self { - PhpDebugAdapter { - program: adapter_config.program.clone(), - adapter_path: adapter_config.adapter_path.clone(), - } - } -} - -#[async_trait(?Send)] -impl DebugAdapter for PhpDebugAdapter { - fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::_ADAPTER_NAME.into()) - } - - async fn connect(&self, cx: &mut AsyncAppContext) -> Result { - let command = "bun".to_string(); - - let args = if let Some(path) = self.adapter_path.clone() { - vec![path, "--server=8132".into()] - } else { - Vec::new() - }; - - let host = TCPHost { - port: Some(8132), - host: None, - delay: Some(1000), - }; - - create_tcp_client(host, &command, &args, cx).await - } - - fn is_installed(&self) -> Option { - None - } - - fn download_adapter(&self) -> anyhow::Result { - Err(anyhow::format_err!("Not implemented")) - } - - fn request_args(&self) -> Value { - json!({"program": format!("{}", &self.program)}) - } -} - -#[derive(Debug, Eq, PartialEq, Clone)] -struct LldbDebugAdapter { - program: String, - adapter_path: Option, -} - -impl LldbDebugAdapter { - const _ADAPTER_NAME: &'static str = "lldb"; - - fn new(adapter_config: &DebugAdapterConfig) -> Self { - LldbDebugAdapter { - program: adapter_config.program.clone(), - adapter_path: adapter_config.adapter_path.clone(), - } - } -} - -#[async_trait(?Send)] -impl DebugAdapter for LldbDebugAdapter { - fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::_ADAPTER_NAME.into()) - } - - async fn connect(&self, _: &mut AsyncAppContext) -> Result { - let command = "/opt/homebrew/opt/llvm/bin/lldb-dap".to_string(); - - create_stdio_client(&command, &vec![]) - } - - fn is_installed(&self) -> Option { - None - } - - fn download_adapter(&self) -> anyhow::Result { - Err(anyhow::format_err!("Not implemented")) - } - - fn request_args(&self) -> Value { - json!({"program": format!("{}", &self.program)}) - } -} diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 3f2875a6c94917..ddf987c02dcfb3 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,7 +1,5 @@ use crate::transport::Transport; -use anyhow::{anyhow, Context, Result}; - -use crate::adapters::{build_adapter, DebugAdapter}; +use anyhow::{anyhow, Result}; use dap_types::{ messages::{Message, Response}, requests::Request, @@ -38,7 +36,8 @@ pub struct DebugAdapterClientId(pub usize); pub struct DebugAdapterClient { id: DebugAdapterClientId, - adapter: Arc>, + adapter_id: String, + request_args: Value, transport: Arc, _process: Arc>>, sequence_count: AtomicU64, @@ -71,16 +70,16 @@ impl TransportParams { impl DebugAdapterClient { pub async fn new( id: DebugAdapterClientId, + adapter_id: String, + request_args: Value, config: DebugAdapterConfig, + transport_params: TransportParams, event_handler: F, cx: &mut AsyncAppContext, ) -> Result> where F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { - let adapter = Arc::new(build_adapter(&config).context("Creating debug adapter")?); - let transport_params = adapter.connect(cx).await?; - let transport = Self::handle_transport( transport_params.rx, transport_params.tx, @@ -88,11 +87,11 @@ impl DebugAdapterClient { event_handler, cx, ); - Ok(Arc::new(Self { id, + adapter_id, + request_args, config, - adapter, transport, sequence_count: AtomicU64::new(1), _process: Arc::new(Mutex::new(transport_params.process)), @@ -189,12 +188,12 @@ impl DebugAdapterClient { self.config.clone() } - pub fn adapter(&self) -> Arc> { - self.adapter.clone() + pub fn adapter_id(&self) -> String { + self.adapter_id.clone() } pub fn request_args(&self) -> Value { - self.adapter.request_args() + self.request_args.clone() } pub fn request_type(&self) -> DebugRequestType { diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index df62861da7bf1b..c2726569c9c106 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,5 +1,5 @@ -pub mod adapters; pub mod client; pub mod transport; +pub mod adapters; pub use dap_types::*; pub mod debugger_settings; diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml new file mode 100644 index 00000000000000..d6515c69b8a41c --- /dev/null +++ b/crates/dap_adapters/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "dap_adapters" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + + +[lints] +workspace = true + +[lib] +path = "src/dap_adapters.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +dap.workspace = true +fs.workspace = true +futures.workspace = true +gpui.workspace = true +http_client.workspace = true +task.workspace = true +smol.workspace = true +serde.workspace = true +serde_json.workspace = true +paths.workspace = true diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs new file mode 100644 index 00000000000000..14ba91ef7a11fe --- /dev/null +++ b/crates/dap_adapters/src/custom.rs @@ -0,0 +1,63 @@ +use crate::*; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub(crate) struct CustomDebugAdapter { + start_command: String, + initialize_args: Option>, + program: String, + connection: DebugConnectionType, +} + +impl CustomDebugAdapter { + const _ADAPTER_NAME: &'static str = "custom_dap"; + + pub(crate) fn new(adapter_config: &DebugAdapterConfig, custom_args: CustomArgs) -> Self { + CustomDebugAdapter { + start_command: custom_args.start_command, + program: adapter_config.program.clone(), + connection: custom_args.connection, + initialize_args: adapter_config.initialize_args.clone(), + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for CustomDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::_ADAPTER_NAME.into()) + } + + async fn connect( + &self, + adapter_binary: DebugAdapterBinary, + cx: &mut AsyncAppContext, + ) -> Result { + match &self.connection { + DebugConnectionType::STDIO => create_stdio_client(adapter_binary), + DebugConnectionType::TCP(tcp_host) => { + create_tcp_client(tcp_host.clone(), adapter_binary, cx).await + } + } + } + + async fn install_or_fetch_binary( + &self, + _delegate: Box, + ) -> Result { + bail!("Install or fetch not implemented for custom debug adapter (yet)"); + } + + fn request_args(&self) -> Value { + let base_args = json!({ + "program": format!("{}", &self.program) + }); + + // TODO Debugger: Figure out a way to combine this with base args + // if let Some(args) = &self.initialize_args { + // let args = json!(args.clone()).as_object().into_iter(); + // base_args.as_object_mut().unwrap().extend(args); + // } + + base_args + } +} diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs new file mode 100644 index 00000000000000..7e1acbfdf6c643 --- /dev/null +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -0,0 +1,41 @@ +mod custom; +mod javascript; +mod lldb; +mod php; +mod python; + +use custom::CustomDebugAdapter; +use lldb::LldbDebugAdapter; +use php::PhpDebugAdapter; +use python::PythonDebugAdapter; + +use anyhow::{anyhow, bail, Context, Result}; +use async_trait::async_trait; +use dap::{ + adapters::{ + create_stdio_client, create_tcp_client, DapDelegate, DebugAdapter, DebugAdapterBinary, + DebugAdapterName, + }, + client::TransportParams, +}; +use gpui::AsyncAppContext; +use http_client::github::latest_github_release; +use serde_json::{json, Value}; +use smol::{ + fs::{self, File}, + process, +}; +use std::{fmt::Debug, process::Stdio}; +use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; + +pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { + match &adapter_config.kind { + DebugAdapterKind::Custom(start_args) => Ok(Box::new(CustomDebugAdapter::new( + adapter_config, + start_args.clone(), + ))), + DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new(adapter_config))), + DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new(adapter_config))), + DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new(adapter_config))), + } +} diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs new file mode 100644 index 00000000000000..f44011135389ae --- /dev/null +++ b/crates/dap_adapters/src/lldb.rs @@ -0,0 +1,44 @@ +use crate::*; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub(crate) struct LldbDebugAdapter { + program: String, + adapter_path: Option, +} + +impl LldbDebugAdapter { + const _ADAPTER_NAME: &'static str = "lldb"; + + pub(crate) fn new(adapter_config: &DebugAdapterConfig) -> Self { + LldbDebugAdapter { + program: adapter_config.program.clone(), + adapter_path: adapter_config.adapter_path.clone(), + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for LldbDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::_ADAPTER_NAME.into()) + } + + async fn connect( + &self, + adapter_binary: DebugAdapterBinary, + _: &mut AsyncAppContext, + ) -> Result { + create_stdio_client(adapter_binary) + } + + async fn install_or_fetch_binary( + &self, + _delegate: Box, + ) -> Result { + bail!("Install or fetch binary not implemented for lldb debug adapter (yet)"); + } + + fn request_args(&self) -> Value { + json!({"program": format!("{}", &self.program)}) + } +} diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs new file mode 100644 index 00000000000000..239ad0f0ecac9c --- /dev/null +++ b/crates/dap_adapters/src/php.rs @@ -0,0 +1,153 @@ +use std::str::FromStr; + +use crate::*; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub(crate) struct PhpDebugAdapter { + program: String, + adapter_path: Option, +} + +impl PhpDebugAdapter { + const ADAPTER_NAME: &'static str = "vscode-php-debug"; + + pub(crate) fn new(adapter_config: &DebugAdapterConfig) -> Self { + PhpDebugAdapter { + program: adapter_config.program.clone(), + adapter_path: adapter_config.adapter_path.clone(), + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for PhpDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::ADAPTER_NAME.into()) + } + + async fn connect( + &self, + adapter_binary: DebugAdapterBinary, + cx: &mut AsyncAppContext, + ) -> Result { + let host = TCPHost { + port: Some(8132), + host: None, + delay: Some(1000), + }; + + create_tcp_client(host, adapter_binary, cx).await + } + + async fn install_or_fetch_binary( + &self, + delegate: Box, + ) -> Result { + let adapter_path = paths::debug_adapters_dir().join(self.name()); + let fs = delegate.fs(); + + if let Some(adapter_path) = self.adapter_path.as_ref() { + return Ok(DebugAdapterBinary { + start_command: Some("bun".into()), + path: std::path::PathBuf::from_str(adapter_path)?, + arguments: vec!["--server=8132".into()], + env: None, + }); + } + + if fs.is_dir(adapter_path.as_path()).await { + return Ok(DebugAdapterBinary { + start_command: Some("bun".into()), + path: adapter_path.join("out/phpDebug.js"), + arguments: vec!["--server=8132".into()], + env: None, + }); + } else if let Some(http_client) = delegate.http_client() { + if !adapter_path.exists() { + fs.create_dir(&adapter_path.as_path()).await?; + } + + let release = + latest_github_release("xdebug/vscode-php-debug", false, false, http_client.clone()) + .await?; + + let asset_name = format!("{}-{}", self.name(), release.tag_name); + let zip_path = adapter_path.join(asset_name); + + if fs::metadata(&zip_path).await.is_err() { + let mut response = http_client + .get(&release.zipball_url, Default::default(), true) + .await + .context("Error downloading release")?; + + let mut file = File::create(&zip_path).await?; + futures::io::copy(response.body_mut(), &mut file).await?; + + let _unzip_status = process::Command::new("unzip") + .current_dir(&adapter_path) + .arg(&zip_path) + .output() + .await? + .status; + + let mut ls = process::Command::new("ls") + .current_dir(&adapter_path) + .stdout(Stdio::piped()) + .spawn()?; + + let std = ls + .stdout + .take() + .ok_or(anyhow!("Failed to list directories"))? + .into_stdio() + .await?; + + let file_name = String::from_utf8( + process::Command::new("grep") + .arg("xdebug-vscode-php-debug") + .stdin(std) + .output() + .await? + .stdout, + )?; + + let file_name = file_name.trim_end(); + + process::Command::new("sh") + .current_dir(&adapter_path) + .arg("-c") + .arg(format!("mv {file_name}/* .")) + .output() + .await?; + + process::Command::new("rm") + .current_dir(&adapter_path) + .arg("-rf") + .arg(file_name) + .arg(zip_path) + .output() + .await?; + + let _npm = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "run", &["build"]) + .await + .is_ok(); + + return Ok(DebugAdapterBinary { + start_command: Some("bun".into()), + path: adapter_path.join("out/phpDebug.js"), + arguments: vec!["--server=8132".into()], + env: None, + }); + } + } + + bail!("Install or fetch not implemented for Php debug adapter (yet)"); + } + + fn request_args(&self) -> Value { + json!({"program": format!("{}", &self.program)}) + } +} diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs new file mode 100644 index 00000000000000..518b67c551ff84 --- /dev/null +++ b/crates/dap_adapters/src/python.rs @@ -0,0 +1,144 @@ +use std::str::FromStr; + +use crate::*; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub(crate) struct PythonDebugAdapter { + program: String, + adapter_path: Option, +} + +impl PythonDebugAdapter { + const ADAPTER_NAME: &'static str = "debugpy"; + + pub(crate) fn new(adapter_config: &DebugAdapterConfig) -> Self { + PythonDebugAdapter { + program: adapter_config.program.clone(), + adapter_path: adapter_config.adapter_path.clone(), + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for PythonDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::ADAPTER_NAME.into()) + } + + async fn connect( + &self, + adapter_binary: DebugAdapterBinary, + _cx: &mut AsyncAppContext, + ) -> Result { + create_stdio_client(adapter_binary) + } + + async fn install_or_fetch_binary( + &self, + delegate: Box, + ) -> Result { + let adapter_path = paths::debug_adapters_dir().join("debugpy/src/debugpy/adapter"); + let fs = delegate.fs(); + + if let Some(adapter_path) = self.adapter_path.as_ref() { + return Ok(DebugAdapterBinary { + start_command: Some("python3".to_string()), + path: std::path::PathBuf::from_str(&adapter_path)?, + arguments: vec![], + env: None, + }); + } + + if fs.is_dir(adapter_path.as_path()).await { + return Ok(DebugAdapterBinary { + start_command: Some("python3".to_string()), + path: adapter_path, + arguments: vec![], + env: None, + }); + } else if let Some(http_client) = delegate.http_client() { + let debugpy_dir = paths::debug_adapters_dir().join("debugpy"); + + if !debugpy_dir.exists() { + fs.create_dir(&debugpy_dir.as_path()).await?; + } + + let release = + latest_github_release("microsoft/debugpy", false, false, http_client.clone()) + .await?; + let asset_name = format!("{}.zip", release.tag_name); + + let zip_path = debugpy_dir.join(asset_name); + + if fs::metadata(&zip_path).await.is_err() { + let mut response = http_client + .get(&release.zipball_url, Default::default(), true) + .await + .context("Error downloading release")?; + + let mut file = File::create(&zip_path).await?; + futures::io::copy(response.body_mut(), &mut file).await?; + + let _unzip_status = process::Command::new("unzip") + .current_dir(&debugpy_dir) + .arg(&zip_path) + .output() + .await? + .status; + + let mut ls = process::Command::new("ls") + .current_dir(&debugpy_dir) + .stdout(Stdio::piped()) + .spawn()?; + + let std = ls + .stdout + .take() + .ok_or(anyhow!("Failed to list directories"))? + .into_stdio() + .await?; + + let file_name = String::from_utf8( + process::Command::new("grep") + .arg("microsoft-debugpy") + .stdin(std) + .output() + .await? + .stdout, + )?; + + let file_name = file_name.trim_end(); + process::Command::new("sh") + .current_dir(&debugpy_dir) + .arg("-c") + .arg(format!("mv {file_name}/* .")) + .output() + .await?; + + process::Command::new("rm") + .current_dir(&debugpy_dir) + .arg("-rf") + .arg(file_name) + .arg(zip_path) + .output() + .await?; + + return Ok(DebugAdapterBinary { + start_command: Some("python3".to_string()), + path: adapter_path, + arguments: vec![], + env: None, + }); + } + return Err(anyhow!("Failed to download debugpy")); + } else { + return Err(anyhow!( + "Could not find debugpy in paths or connect to http" + )); + } + } + + fn request_args(&self) -> Value { + json!({"program": format!("{}", &self.program)}) + } +} diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index ce6c049af88b2a..80485f149e9b72 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -245,6 +245,14 @@ pub fn languages_dir() -> &'static PathBuf { LANGUAGES_DIR.get_or_init(|| support_dir().join("languages")) } +/// Returns the path to the debug adapters directory +/// +/// This is where debug adapters are downloaded to for DAPs that are built-in to Zed. +pub fn debug_adapters_dir() -> &'static PathBuf { + static DEBUG_ADAPTERS_DIR: OnceLock = OnceLock::new(); + DEBUG_ADAPTERS_DIR.get_or_init(|| support_dir().join("debug_adapters")) +} + /// Returns the path to the Copilot directory. pub fn copilot_dir() -> &'static PathBuf { static COPILOT_DIR: OnceLock = OnceLock::new(); diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 84a221f617e37c..2795ecf1218cdd 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -31,6 +31,7 @@ client.workspace = true clock.workspace = true collections.workspace = true dap.workspace = true +dap_adapters.workspace = true dev_server_projects.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 0e7ed0655fafcf..47debba3f7ceee 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -18,8 +18,12 @@ use dap::{ StackTraceArguments, StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, }; +use dap_adapters::build_adapter; +use fs::Fs; use gpui::{EventEmitter, Model, ModelContext, Task}; +use http_client::HttpClient; use language::{Buffer, BufferSnapshot}; +use node_runtime::NodeRuntime; use serde_json::Value; use settings::WorktreeId; use std::{ @@ -33,7 +37,7 @@ use std::{ }; use task::{DebugAdapterConfig, DebugRequestType}; use text::Point; -use util::ResultExt as _; +use util::ResultExt; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), @@ -61,12 +65,20 @@ pub struct DapStore { breakpoints: BTreeMap>, capabilities: HashMap, active_debug_line: Option<(ProjectPath, DebugPosition)>, + http_client: Option>, + node_runtime: Option, + fs: Arc, } impl EventEmitter for DapStore {} impl DapStore { - pub fn new(cx: &mut ModelContext) -> Self { + pub fn new( + http_client: Option>, + node_runtime: Option, + fs: Arc, + cx: &mut ModelContext, + ) -> Self { cx.on_app_quit(Self::shutdown_clients).detach(); Self { @@ -75,6 +87,9 @@ impl DapStore { breakpoints: Default::default(), capabilities: HashMap::default(), next_client_id: Default::default(), + http_client, + node_runtime, + fs, } } @@ -216,12 +231,33 @@ impl DapStore { pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { let client_id = self.next_client_id(); - + let adapter_delegate = Box::new(DapAdapterDelegate::new( + self.http_client.clone(), + self.node_runtime.clone(), + self.fs.clone(), + )); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); + let adapter = Arc::new( + build_adapter(&config) + .context("Creating debug adapter") + .log_err()?, + ); + let binary = adapter + .install_or_fetch_binary(adapter_delegate) + .await + .context("Failed to get debug adapter binary") + .log_err()?; + let transport_params = adapter.connect(binary, &mut cx).await.log_err()?; + let request_args = adapter.request_args(); + let adapter_id = adapter.id(); + let client = DebugAdapterClient::new( client_id, + adapter_id, + request_args, config, + transport_params, move |message, cx| { dap_store .update(cx, |_, cx| { @@ -271,7 +307,7 @@ impl DapStore { .request::(InitializeRequestArguments { client_id: Some("zed".to_owned()), client_name: Some("Zed".to_owned()), - adapter_id: client.adapter().id(), + adapter_id: client.adapter_id(), locale: Some("en-US".to_owned()), path_format: Some(InitializeRequestArgumentsPathFormat::Path), supports_variable_type: Some(true), @@ -402,9 +438,10 @@ impl DapStore { .unwrap_or_default(); if support_configuration_done_request { - client + let res = client .request::(ConfigurationDoneArguments) - .await + .await; + res } else { Ok(()) } @@ -1076,3 +1113,37 @@ impl SerializedBreakpoint { } } } + +pub struct DapAdapterDelegate { + fs: Arc, + http_client: Option>, + node_runtime: Option, +} + +impl DapAdapterDelegate { + pub fn new( + http_client: Option>, + node_runtime: Option, + fs: Arc, + ) -> Self { + Self { + fs, + http_client, + node_runtime, + } + } +} + +impl dap::adapters::DapDelegate for DapAdapterDelegate { + fn http_client(&self) -> Option> { + self.http_client.clone() + } + + fn node_runtime(&self) -> Option { + self.node_runtime.clone() + } + + fn fs(&self) -> Arc { + self.fs.clone() + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ede83df1fc9324..61b6e923d0fb86 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -618,7 +618,15 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); - let dap_store = cx.new_model(DapStore::new); + let dap_store = cx.new_model(|cx| { + DapStore::new( + Some(client.http_client()), + Some(node.clone()), + fs.clone(), + cx, + ) + }); + let buffer_store = cx .new_model(|cx| BufferStore::local(worktree_store.clone(), dap_store.clone(), cx)); cx.subscribe(&buffer_store, Self::on_buffer_store_event) @@ -750,7 +758,14 @@ impl Project { }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - let dap_store = cx.new_model(DapStore::new); + let dap_store = cx.new_model(|cx| { + DapStore::new( + Some(client.http_client()), + Some(node.clone()), + fs.clone(), + cx, + ) + }); cx.on_release(|this, cx| { if let Some(ssh_client) = this.ssh_client.as_ref() { ssh_client @@ -900,7 +915,8 @@ impl Project { BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx) })?; - let dap_store = cx.new_model(DapStore::new)?; + let dap_store = + cx.new_model(|cx| DapStore::new(Some(client.http_client()), None, fs.clone(), cx))?; let lsp_store = cx.new_model(|cx| { let mut lsp_store = LspStore::new_remote( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index f6ed06127520a3..47a8a73f4c5f0c 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -55,7 +55,7 @@ impl HeadlessProject { store }); - let dap_store = cx.new_model(DapStore::new); + let dap_store = cx.new_model(|cx| DapStore::new(None, None, fs.clone(), cx)); let buffer_store = cx.new_model(|cx| { let mut buffer_store = BufferStore::local(worktree_store.clone(), dap_store.clone(), cx); From 00b6fdc09818464cdc39520730105fa4417a3e52 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 8 Oct 2024 07:45:33 +0200 Subject: [PATCH 279/650] Make ci pass --- crates/dap/src/lib.rs | 2 +- crates/dap_adapters/src/javascript.rs | 1 + crates/project/src/dap_store.rs | 5 ++--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index c2726569c9c106..df62861da7bf1b 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,5 +1,5 @@ +pub mod adapters; pub mod client; pub mod transport; -pub mod adapters; pub use dap_types::*; pub mod debugger_settings; diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index e69de29bb2d1d6..8b137891791fe9 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -0,0 +1 @@ + diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 47debba3f7ceee..8b69945bb0e020 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -438,10 +438,9 @@ impl DapStore { .unwrap_or_default(); if support_configuration_done_request { - let res = client + client .request::(ConfigurationDoneArguments) - .await; - res + .await } else { Ok(()) } From eedd865ae8cea6497f793ebe41877bbcedc6fd8d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 8 Oct 2024 07:46:46 +0200 Subject: [PATCH 280/650] Remove unused dep --- Cargo.lock | 1 - crates/dap/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7eecdda1e3308b..f981873048078f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3353,7 +3353,6 @@ dependencies = [ "log", "node_runtime", "parking_lot", - "paths", "schemars", "serde", "serde_json", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 3ed84886ac7040..248a51fe0ea44c 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -19,7 +19,6 @@ http_client.workspace = true log.workspace = true node_runtime.workspace = true parking_lot.workspace = true -paths.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true From 08935b29efb0c7fff655b5b9b4c2d99877d1308c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 8 Oct 2024 23:14:28 +0200 Subject: [PATCH 281/650] Remove new lines from variable list value This fixes a display issue, for javascript expression values. --- crates/debugger_ui/src/variable_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index f2d407e57887cc..e07351e155dd31 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -730,7 +730,7 @@ impl VariableList { div() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .child(variable.value.clone()), + .child(variable.value.replace("\n", " ").clone()), ), ), ) From 91926cdcfe01397e1b7478b4cf50810a08c97c1e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 8 Oct 2024 23:25:33 +0200 Subject: [PATCH 282/650] Fix correctly install and build php adapter --- crates/dap_adapters/src/php.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 239ad0f0ecac9c..36f96fe3db0325 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -43,9 +43,6 @@ impl DebugAdapter for PhpDebugAdapter { &self, delegate: Box, ) -> Result { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let fs = delegate.fs(); - if let Some(adapter_path) = self.adapter_path.as_ref() { return Ok(DebugAdapterBinary { start_command: Some("bun".into()), @@ -55,6 +52,9 @@ impl DebugAdapter for PhpDebugAdapter { }); } + let adapter_path = paths::debug_adapters_dir().join(self.name()); + let fs = delegate.fs(); + if fs.is_dir(adapter_path.as_path()).await { return Ok(DebugAdapterBinary { start_command: Some("bun".into()), @@ -128,10 +128,17 @@ impl DebugAdapter for PhpDebugAdapter { .output() .await?; - let _npm = delegate + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "install", &[]) + .await + .is_ok(); + + let _ = delegate .node_runtime() .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["build"]) + .run_npm_subcommand(&adapter_path, "build", &[]) .await .is_ok(); From 5f1de1ab655738e9177930799da66573453bb542 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 8 Oct 2024 23:54:53 +0200 Subject: [PATCH 283/650] Add missing license --- crates/dap_adapters/LICENSE-GPL | 1 + 1 file changed, 1 insertion(+) create mode 120000 crates/dap_adapters/LICENSE-GPL diff --git a/crates/dap_adapters/LICENSE-GPL b/crates/dap_adapters/LICENSE-GPL new file mode 120000 index 00000000000000..4cea79fe04149a --- /dev/null +++ b/crates/dap_adapters/LICENSE-GPL @@ -0,0 +1 @@ +/Users/remcosmits/Documents/code/zed/LICENSE-GPL \ No newline at end of file From 171c74223c2b86826cc64f33aa347fafd22279d9 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 9 Oct 2024 11:30:59 +0200 Subject: [PATCH 284/650] Make start debugging request work again This also fixes an issue that you could not add your own initialize_args for non custom adapters --- Cargo.lock | 1 + crates/dap_adapters/src/custom.rs | 2 +- crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/debugger_panel.rs | 17 ++++++++++++-- crates/project/src/dap_store.rs | 29 +++++++++++++++++++----- crates/project/src/project.rs | 2 +- crates/task/src/debug_format.rs | 4 ++-- 7 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f981873048078f..81057b5a2589c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3476,6 +3476,7 @@ dependencies = [ "parking_lot", "project", "serde", + "serde_json", "settings", "task", "tasks_ui", diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 14ba91ef7a11fe..07427a397d950e 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -3,7 +3,7 @@ use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct CustomDebugAdapter { start_command: String, - initialize_args: Option>, + initialize_args: Option, program: String, connection: DebugConnectionType, } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 0d04a8fbb7d36f..1e720b53cda995 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -20,6 +20,7 @@ menu.workspace = true parking_lot.workspace = true project.workspace = true serde.workspace = true +serde_json.workspace = true settings.workspace = true task.workspace = true tasks_ui.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b7179b9e651109..71fe32f3aec8ee 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -14,6 +14,7 @@ use gpui::{ FontWeight, Model, Subscription, Task, View, ViewContext, WeakView, }; use project::dap_store::DapStore; +use serde_json::Value; use settings::Settings; use std::collections::BTreeMap; use std::sync::Arc; @@ -101,7 +102,12 @@ impl DebugPanel { } Message::Request(request) => { if StartDebugging::COMMAND == request.command { - Self::handle_start_debugging_request(this, client, cx); + Self::handle_start_debugging_request( + this, + client, + request.arguments.clone(), + cx, + ); } } _ => unreachable!(), @@ -222,10 +228,17 @@ impl DebugPanel { fn handle_start_debugging_request( this: &mut Self, client: Arc, + request_args: Option, cx: &mut ViewContext, ) { + let start_args = if let Some(args) = request_args { + serde_json::from_value(args.clone()).ok() + } else { + None + }; + this.dap_store.update(cx, |store, cx| { - store.start_client(client.config(), cx); + store.start_client(client.config(), start_args, cx); }); } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 8b69945bb0e020..20636e0780e832 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -15,8 +15,9 @@ use dap::{ InitializeRequestArgumentsPathFormat, LaunchRequestArguments, Module, ModulesArguments, NextArguments, PauseArguments, Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, SourceBreakpoint, StackFrame, - StackTraceArguments, StepInArguments, StepOutArguments, SteppingGranularity, - TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, + StackTraceArguments, StartDebuggingRequestArguments, StepInArguments, StepOutArguments, + SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, + VariablesArguments, }; use dap_adapters::build_adapter; use fs::Fs; @@ -24,7 +25,7 @@ use gpui::{EventEmitter, Model, ModelContext, Task}; use http_client::HttpClient; use language::{Buffer, BufferSnapshot}; use node_runtime::NodeRuntime; -use serde_json::Value; +use serde_json::{json, Value}; use settings::WorktreeId; use std::{ collections::BTreeMap, @@ -37,7 +38,7 @@ use std::{ }; use task::{DebugAdapterConfig, DebugRequestType}; use text::Point; -use util::ResultExt; +use util::{merge_json_value_into, ResultExt}; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), @@ -229,7 +230,12 @@ impl DapStore { } } - pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { + pub fn start_client( + &mut self, + config: DebugAdapterConfig, + args: Option, + cx: &mut ModelContext, + ) { let client_id = self.next_client_id(); let adapter_delegate = Box::new(DapAdapterDelegate::new( self.http_client.clone(), @@ -249,7 +255,18 @@ impl DapStore { .context("Failed to get debug adapter binary") .log_err()?; let transport_params = adapter.connect(binary, &mut cx).await.log_err()?; - let request_args = adapter.request_args(); + + let mut request_args = json!({}); + if let Some(config_args) = config.initialize_args.clone() { + merge_json_value_into(config_args, &mut request_args); + } + + merge_json_value_into(adapter.request_args(), &mut request_args); + + if let Some(args) = args { + merge_json_value_into(args.configuration.clone(), &mut request_args); + } + let adapter_id = adapter.id(); let client = DebugAdapterClient::new( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 61b6e923d0fb86..e40797cd0be450 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1185,7 +1185,7 @@ impl Project { ) { if let Some(adapter_config) = debug_task.debug_adapter_config() { self.dap_store - .update(cx, |store, cx| store.start_client(adapter_config, cx)); + .update(cx, |store, cx| store.start_client(adapter_config, None, cx)); } } diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 411e6b42cc0925..e10ba4f0cb2db8 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -84,7 +84,7 @@ pub struct DebugAdapterConfig { /// The path to the adapter pub adapter_path: Option, /// Additional initialization arguments to be sent on DAP initialization - pub initialize_args: Option>, + pub initialize_args: Option, } /// Represents the type of the debugger adapter connection @@ -110,7 +110,7 @@ pub struct DebugTaskDefinition { /// The adapter to run adapter: DebugAdapterKind, /// Additional initialization arguments to be sent on DAP initialization - initialize_args: Option>, + initialize_args: Option, /// The path of the debug adapter to use adapter_path: Option, } From a728f9e7511eb79e24ede8e84d1cdcc7e3b1a789 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 9 Oct 2024 11:39:02 +0200 Subject: [PATCH 285/650] Remove not needed clone --- crates/project/src/dap_store.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 20636e0780e832..7635fff817b9b2 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -264,14 +264,12 @@ impl DapStore { merge_json_value_into(adapter.request_args(), &mut request_args); if let Some(args) = args { - merge_json_value_into(args.configuration.clone(), &mut request_args); + merge_json_value_into(args.configuration, &mut request_args); } - let adapter_id = adapter.id(); - let client = DebugAdapterClient::new( client_id, - adapter_id, + adapter.id(), request_args, config, transport_params, From 7e2c1386fc85624d1b2e58ee8c11c6f568fa3886 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 9 Oct 2024 15:47:07 +0200 Subject: [PATCH 286/650] Cleanup adapters code and add javascript adapter (#48) * Clean up how adapters install and fetch their binary Before this was in one method, which was hard to read and doing to many things. * Add javascript debug adapter * Get node binary from node_runtime * Undo remove request args for lldb * Remove initialize value for now * Remove default values * Fix did not compile javascript * Fix php did not build --- crates/dap/src/adapters.rs | 39 ++--- crates/dap_adapters/src/custom.rs | 48 +++---- crates/dap_adapters/src/dap_adapters.rs | 15 +- crates/dap_adapters/src/javascript.rs | 180 ++++++++++++++++++++++++ crates/dap_adapters/src/lldb.rs | 33 +++-- crates/dap_adapters/src/php.rs | 77 ++++++---- crates/dap_adapters/src/python.rs | 70 +++++---- crates/project/src/dap_store.rs | 17 ++- crates/task/src/debug_format.rs | 2 + 9 files changed, 341 insertions(+), 140 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index c7aee65877ad96..95cb03c848e500 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -24,10 +24,10 @@ use std::{ time::Duration, }; -use task::TCPHost; +use task::{DebugAdapterConfig, TCPHost}; /// Get an open port to use with the tcp client when not supplied by debug config -async fn get_port(host: Ipv4Addr) -> Option { +async fn get_open_port(host: Ipv4Addr) -> Option { Some( TcpListener::bind(SocketAddrV4::new(host, 0)) .await @@ -51,25 +51,25 @@ pub trait DapDelegate { /// - `cx`: The context that the new client belongs too pub async fn create_tcp_client( host: TCPHost, - adapter_binary: DebugAdapterBinary, + adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result { let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); let mut port = host.port; if port.is_none() { - port = get_port(host_address).await; + port = get_open_port(host_address).await; } let mut command = if let Some(start_command) = &adapter_binary.start_command { let mut command = process::Command::new(start_command); - command.arg(adapter_binary.path); + command.arg(adapter_binary.path.clone()); command } else { - process::Command::new(adapter_binary.path) + process::Command::new(adapter_binary.path.clone()) }; command - .args(adapter_binary.arguments) + .args(adapter_binary.arguments.clone()) .envs(adapter_binary.env.clone().unwrap_or_default()) .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -108,18 +108,18 @@ pub async fn create_tcp_client( /// /// # Parameters /// - `adapter_binary`: The debug adapter binary to start -pub fn create_stdio_client(adapter_binary: DebugAdapterBinary) -> Result { +pub fn create_stdio_client(adapter_binary: &DebugAdapterBinary) -> Result { let mut command = if let Some(start_command) = &adapter_binary.start_command { let mut command = process::Command::new(start_command); - command.arg(adapter_binary.path); + command.arg(adapter_binary.path.clone()); command } else { - let command = process::Command::new(adapter_binary.path); + let command = process::Command::new(adapter_binary.path.clone()); command }; command - .args(adapter_binary.arguments) + .args(adapter_binary.arguments.clone()) .envs(adapter_binary.env.clone().unwrap_or_default()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -176,7 +176,7 @@ pub struct DebugAdapterBinary { } #[async_trait(?Send)] -pub trait DebugAdapter: Debug + Send + Sync + 'static { +pub trait DebugAdapter: 'static + Send + Sync { fn id(&self) -> String { "".to_string() } @@ -185,14 +185,21 @@ pub trait DebugAdapter: Debug + Send + Sync + 'static { async fn connect( &self, - adapter_binary: DebugAdapterBinary, + adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> anyhow::Result; - async fn install_or_fetch_binary( + /// Installs the binary for the debug adapter. + /// This method is called when the adapter binary is not found or needs to be updated. + /// It should download and install the necessary files for the debug adapter to function. + async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()>; + + async fn fetch_binary( &self, - delegate: Box, + delegate: &dyn DapDelegate, + config: &DebugAdapterConfig, ) -> Result; - fn request_args(&self) -> Value; + /// Should return base configuration to make the debug adapter work + fn request_args(&self, config: &DebugAdapterConfig) -> Value; } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 07427a397d950e..7f249767fcb9ad 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,23 +1,18 @@ +use serde_json::Value; +use task::DebugAdapterConfig; + use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct CustomDebugAdapter { - start_command: String, - initialize_args: Option, - program: String, - connection: DebugConnectionType, + custom_args: CustomArgs, } impl CustomDebugAdapter { const _ADAPTER_NAME: &'static str = "custom_dap"; - pub(crate) fn new(adapter_config: &DebugAdapterConfig, custom_args: CustomArgs) -> Self { - CustomDebugAdapter { - start_command: custom_args.start_command, - program: adapter_config.program.clone(), - connection: custom_args.connection, - initialize_args: adapter_config.initialize_args.clone(), - } + pub(crate) fn new(custom_args: CustomArgs) -> Self { + CustomDebugAdapter { custom_args } } } @@ -29,10 +24,10 @@ impl DebugAdapter for CustomDebugAdapter { async fn connect( &self, - adapter_binary: DebugAdapterBinary, + adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result { - match &self.connection { + match &self.custom_args.connection { DebugConnectionType::STDIO => create_stdio_client(adapter_binary), DebugConnectionType::TCP(tcp_host) => { create_tcp_client(tcp_host.clone(), adapter_binary, cx).await @@ -40,24 +35,19 @@ impl DebugAdapter for CustomDebugAdapter { } } - async fn install_or_fetch_binary( - &self, - _delegate: Box, - ) -> Result { - bail!("Install or fetch not implemented for custom debug adapter (yet)"); + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + json!({"program": config.program}) } - fn request_args(&self) -> Value { - let base_args = json!({ - "program": format!("{}", &self.program) - }); - - // TODO Debugger: Figure out a way to combine this with base args - // if let Some(args) = &self.initialize_args { - // let args = json!(args.clone()).as_object().into_iter(); - // base_args.as_object_mut().unwrap().extend(args); - // } + async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { + bail!("Install or fetch not implemented for custom debug adapter (yet)") + } - base_args + async fn fetch_binary( + &self, + _: &dyn DapDelegate, + _: &DebugAdapterConfig, + ) -> Result { + bail!("Install or fetch not implemented for custom debug adapter (yet)") } } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 7e1acbfdf6c643..fac4f3c77c7377 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -5,6 +5,7 @@ mod php; mod python; use custom::CustomDebugAdapter; +use javascript::JsDebugAdapter; use lldb::LldbDebugAdapter; use php::PhpDebugAdapter; use python::PythonDebugAdapter; @@ -30,12 +31,12 @@ use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { match &adapter_config.kind { - DebugAdapterKind::Custom(start_args) => Ok(Box::new(CustomDebugAdapter::new( - adapter_config, - start_args.clone(), - ))), - DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new(adapter_config))), - DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new(adapter_config))), - DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new(adapter_config))), + DebugAdapterKind::Custom(start_args) => { + Ok(Box::new(CustomDebugAdapter::new(start_args.clone()))) + } + DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new())), + DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new())), + DebugAdapterKind::Javascript => Ok(Box::new(JsDebugAdapter::new())), + DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new())), } } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 8b137891791fe9..5d24f5052ec506 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1 +1,181 @@ +use crate::*; +use std::str::FromStr; +#[derive(Debug, Eq, PartialEq, Clone)] +pub(crate) struct JsDebugAdapter {} + +impl JsDebugAdapter { + const ADAPTER_NAME: &'static str = "vscode-js-debug"; + const ADAPTER_PATH: &'static str = "src/dapDebugServer.js"; + + pub(crate) fn new() -> Self { + JsDebugAdapter {} + } +} + +#[async_trait(?Send)] +impl DebugAdapter for JsDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::ADAPTER_NAME.into()) + } + + async fn connect( + &self, + adapter_binary: &DebugAdapterBinary, + cx: &mut AsyncAppContext, + ) -> Result { + let host = TCPHost { + port: Some(8133), + host: None, + delay: Some(1000), + }; + + create_tcp_client(host, adapter_binary, cx).await + } + + async fn fetch_binary( + &self, + delegate: &dyn DapDelegate, + config: &DebugAdapterConfig, + ) -> Result { + let node_runtime = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))?; + + if let Some(adapter_path) = config.adapter_path.as_ref() { + return Ok(DebugAdapterBinary { + start_command: Some( + node_runtime + .binary_path() + .await? + .to_string_lossy() + .into_owned(), + ), + path: std::path::PathBuf::from_str(adapter_path)?, + arguments: vec!["8133".into()], + env: None, + }); + } + + let adapter_path = paths::debug_adapters_dir().join(self.name()); + + Ok(DebugAdapterBinary { + start_command: Some( + node_runtime + .binary_path() + .await? + .to_string_lossy() + .into_owned(), + ), + path: adapter_path.join(Self::ADAPTER_PATH), + arguments: vec!["8133".into()], + env: None, + }) + } + + async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { + let adapter_path = paths::debug_adapters_dir().join(self.name()); + let fs = delegate.fs(); + + if fs.is_dir(adapter_path.as_path()).await { + return Ok(()); + } + + if let Some(http_client) = delegate.http_client() { + if !adapter_path.exists() { + fs.create_dir(&adapter_path.as_path()).await?; + } + + let release = latest_github_release( + "microsoft/vscode-js-debug", + false, + false, + http_client.clone(), + ) + .await?; + + let asset_name = format!("{}-{}", self.name(), release.tag_name); + let zip_path = adapter_path.join(asset_name); + + if fs::metadata(&zip_path).await.is_err() { + let mut response = http_client + .get(&release.zipball_url, Default::default(), true) + .await + .context("Error downloading release")?; + + let mut file = File::create(&zip_path).await?; + futures::io::copy(response.body_mut(), &mut file).await?; + + let _unzip_status = process::Command::new("unzip") + .current_dir(&adapter_path) + .arg(&zip_path) + .output() + .await? + .status; + + let mut ls = process::Command::new("ls") + .current_dir(&adapter_path) + .stdout(Stdio::piped()) + .spawn()?; + + let std = ls + .stdout + .take() + .ok_or(anyhow!("Failed to list directories"))? + .into_stdio() + .await?; + + let file_name = String::from_utf8( + process::Command::new("grep") + .arg("microsoft-vscode-js-debug") + .stdin(std) + .output() + .await? + .stdout, + )?; + + let file_name = file_name.trim_end(); + + process::Command::new("sh") + .current_dir(&adapter_path) + .arg("-c") + .arg(format!("mv {file_name}/* .")) + .output() + .await?; + + process::Command::new("rm") + .current_dir(&adapter_path) + .arg("-rf") + .arg(file_name) + .arg(zip_path) + .output() + .await?; + + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "install", &[]) + .await + .ok(); + + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "run", &["compile"]) + .await + .ok(); + + return Ok(()); + } + } + + bail!("Install or fetch not implemented for Javascript debug adapter (yet)"); + } + + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + json!({ + "program": config.program, + "type": "pwa-node", + }) + } +} diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index f44011135389ae..19b0e77011983d 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -1,19 +1,17 @@ +use anyhow::Result; +use async_trait::async_trait; +use task::DebugAdapterConfig; + use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] -pub(crate) struct LldbDebugAdapter { - program: String, - adapter_path: Option, -} +pub(crate) struct LldbDebugAdapter {} impl LldbDebugAdapter { const _ADAPTER_NAME: &'static str = "lldb"; - pub(crate) fn new(adapter_config: &DebugAdapterConfig) -> Self { - LldbDebugAdapter { - program: adapter_config.program.clone(), - adapter_path: adapter_config.adapter_path.clone(), - } + pub(crate) fn new() -> Self { + LldbDebugAdapter {} } } @@ -25,20 +23,25 @@ impl DebugAdapter for LldbDebugAdapter { async fn connect( &self, - adapter_binary: DebugAdapterBinary, + adapter_binary: &DebugAdapterBinary, _: &mut AsyncAppContext, ) -> Result { create_stdio_client(adapter_binary) } - async fn install_or_fetch_binary( + async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { + bail!("Install or fetch not implemented for lldb debug adapter (yet)") + } + + async fn fetch_binary( &self, - _delegate: Box, + _: &dyn DapDelegate, + _: &DebugAdapterConfig, ) -> Result { - bail!("Install or fetch binary not implemented for lldb debug adapter (yet)"); + bail!("Install or fetch not implemented for lldb debug adapter (yet)") } - fn request_args(&self) -> Value { - json!({"program": format!("{}", &self.program)}) + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + json!({"program": config.program}) } } diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 36f96fe3db0325..7f3ed4629af226 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -3,19 +3,14 @@ use std::str::FromStr; use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] -pub(crate) struct PhpDebugAdapter { - program: String, - adapter_path: Option, -} +pub(crate) struct PhpDebugAdapter {} impl PhpDebugAdapter { const ADAPTER_NAME: &'static str = "vscode-php-debug"; + const ADAPTER_PATH: &'static str = "out/phpDebug.js"; - pub(crate) fn new(adapter_config: &DebugAdapterConfig) -> Self { - PhpDebugAdapter { - program: adapter_config.program.clone(), - adapter_path: adapter_config.adapter_path.clone(), - } + pub(crate) fn new() -> Self { + PhpDebugAdapter {} } } @@ -27,7 +22,7 @@ impl DebugAdapter for PhpDebugAdapter { async fn connect( &self, - adapter_binary: DebugAdapterBinary, + adapter_binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result { let host = TCPHost { @@ -39,30 +34,55 @@ impl DebugAdapter for PhpDebugAdapter { create_tcp_client(host, adapter_binary, cx).await } - async fn install_or_fetch_binary( + async fn fetch_binary( &self, - delegate: Box, + delegate: &dyn DapDelegate, + config: &DebugAdapterConfig, ) -> Result { - if let Some(adapter_path) = self.adapter_path.as_ref() { + let node_runtime = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))?; + + if let Some(adapter_path) = config.adapter_path.as_ref() { return Ok(DebugAdapterBinary { - start_command: Some("bun".into()), + start_command: Some( + node_runtime + .binary_path() + .await? + .to_string_lossy() + .into_owned(), + ), path: std::path::PathBuf::from_str(adapter_path)?, arguments: vec!["--server=8132".into()], env: None, }); } + let adapter_path = paths::debug_adapters_dir().join(self.name()); + + Ok(DebugAdapterBinary { + start_command: Some( + node_runtime + .binary_path() + .await? + .to_string_lossy() + .into_owned(), + ), + path: adapter_path.join(Self::ADAPTER_PATH), + arguments: vec!["--server=8132".into()], + env: None, + }) + } + + async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { let adapter_path = paths::debug_adapters_dir().join(self.name()); let fs = delegate.fs(); if fs.is_dir(adapter_path.as_path()).await { - return Ok(DebugAdapterBinary { - start_command: Some("bun".into()), - path: adapter_path.join("out/phpDebug.js"), - arguments: vec!["--server=8132".into()], - env: None, - }); - } else if let Some(http_client) = delegate.http_client() { + return Ok(()); + } + + if let Some(http_client) = delegate.http_client() { if !adapter_path.exists() { fs.create_dir(&adapter_path.as_path()).await?; } @@ -138,23 +158,18 @@ impl DebugAdapter for PhpDebugAdapter { let _ = delegate .node_runtime() .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "build", &[]) + .run_npm_subcommand(&adapter_path, "run", &["build"]) .await .is_ok(); - return Ok(DebugAdapterBinary { - start_command: Some("bun".into()), - path: adapter_path.join("out/phpDebug.js"), - arguments: vec!["--server=8132".into()], - env: None, - }); + return Ok(()); } } - bail!("Install or fetch not implemented for Php debug adapter (yet)"); + bail!("Install or fetch not implemented for PHP debug adapter (yet)"); } - fn request_args(&self) -> Value { - json!({"program": format!("{}", &self.program)}) + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + json!({"program": config.program}) } } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 518b67c551ff84..98b625b61b22db 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -3,19 +3,14 @@ use std::str::FromStr; use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] -pub(crate) struct PythonDebugAdapter { - program: String, - adapter_path: Option, -} +pub(crate) struct PythonDebugAdapter {} impl PythonDebugAdapter { const ADAPTER_NAME: &'static str = "debugpy"; + const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; - pub(crate) fn new(adapter_config: &DebugAdapterConfig) -> Self { - PythonDebugAdapter { - program: adapter_config.program.clone(), - adapter_path: adapter_config.adapter_path.clone(), - } + pub(crate) fn new() -> Self { + PythonDebugAdapter {} } } @@ -27,20 +22,18 @@ impl DebugAdapter for PythonDebugAdapter { async fn connect( &self, - adapter_binary: DebugAdapterBinary, - _cx: &mut AsyncAppContext, + adapter_binary: &DebugAdapterBinary, + _: &mut AsyncAppContext, ) -> Result { create_stdio_client(adapter_binary) } - async fn install_or_fetch_binary( + async fn fetch_binary( &self, - delegate: Box, + _: &dyn DapDelegate, + config: &DebugAdapterConfig, ) -> Result { - let adapter_path = paths::debug_adapters_dir().join("debugpy/src/debugpy/adapter"); - let fs = delegate.fs(); - - if let Some(adapter_path) = self.adapter_path.as_ref() { + if let Some(adapter_path) = config.adapter_path.as_ref() { return Ok(DebugAdapterBinary { start_command: Some("python3".to_string()), path: std::path::PathBuf::from_str(&adapter_path)?, @@ -49,14 +42,25 @@ impl DebugAdapter for PythonDebugAdapter { }); } + let adapter_path = paths::debug_adapters_dir().join(self.name()); + + Ok(DebugAdapterBinary { + start_command: Some("python3".to_string()), + path: adapter_path.join(Self::ADAPTER_PATH), + arguments: vec![], + env: None, + }) + } + + async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { + let adapter_path = paths::debug_adapters_dir().join(self.name()); + let fs = delegate.fs(); + if fs.is_dir(adapter_path.as_path()).await { - return Ok(DebugAdapterBinary { - start_command: Some("python3".to_string()), - path: adapter_path, - arguments: vec![], - env: None, - }); - } else if let Some(http_client) = delegate.http_client() { + return Ok(()); + } + + if let Some(http_client) = delegate.http_client() { let debugpy_dir = paths::debug_adapters_dir().join("debugpy"); if !debugpy_dir.exists() { @@ -123,22 +127,14 @@ impl DebugAdapter for PythonDebugAdapter { .output() .await?; - return Ok(DebugAdapterBinary { - start_command: Some("python3".to_string()), - path: adapter_path, - arguments: vec![], - env: None, - }); + return Ok(()); } - return Err(anyhow!("Failed to download debugpy")); - } else { - return Err(anyhow!( - "Could not find debugpy in paths or connect to http" - )); } + + bail!("Install or fetch not implemented for Python debug adapter (yet)"); } - fn request_args(&self) -> Value { - json!({"program": format!("{}", &self.program)}) + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + json!({"program": config.program}) } } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 7635fff817b9b2..d30ac85b744ac4 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -237,11 +237,11 @@ impl DapStore { cx: &mut ModelContext, ) { let client_id = self.next_client_id(); - let adapter_delegate = Box::new(DapAdapterDelegate::new( + let adapter_delegate = DapAdapterDelegate::new( self.http_client.clone(), self.node_runtime.clone(), self.fs.clone(), - )); + ); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); let adapter = Arc::new( @@ -249,24 +249,31 @@ impl DapStore { .context("Creating debug adapter") .log_err()?, ); + let _ = adapter + .install_binary(&adapter_delegate) + .await + .context("Failed to install debug adapter binary") + .log_err()?; + let binary = adapter - .install_or_fetch_binary(adapter_delegate) + .fetch_binary(&adapter_delegate, &config) .await .context("Failed to get debug adapter binary") .log_err()?; - let transport_params = adapter.connect(binary, &mut cx).await.log_err()?; let mut request_args = json!({}); if let Some(config_args) = config.initialize_args.clone() { merge_json_value_into(config_args, &mut request_args); } - merge_json_value_into(adapter.request_args(), &mut request_args); + merge_json_value_into(adapter.request_args(&config), &mut request_args); if let Some(args) = args { merge_json_value_into(args.configuration, &mut request_args); } + let transport_params = adapter.connect(&binary, &mut cx).await.log_err()?; + let client = DebugAdapterClient::new( client_id, adapter.id(), diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index e10ba4f0cb2db8..b1f3a0b1c1cca7 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -44,6 +44,8 @@ pub enum DebugAdapterKind { Python, /// Use vscode-php-debug PHP, + /// Use vscode-js-debug + Javascript, /// Use lldb Lldb, } From 554a402cec91cf2020b73010ac314e48569a6d20 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 10 Oct 2024 17:46:23 +0200 Subject: [PATCH 287/650] Fix wrong symlink for license --- crates/dap_adapters/LICENSE-GPL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dap_adapters/LICENSE-GPL b/crates/dap_adapters/LICENSE-GPL index 4cea79fe04149a..e0f9dbd5d63fef 120000 --- a/crates/dap_adapters/LICENSE-GPL +++ b/crates/dap_adapters/LICENSE-GPL @@ -1 +1 @@ -/Users/remcosmits/Documents/code/zed/LICENSE-GPL \ No newline at end of file +LICENSE-GPL \ No newline at end of file From 177ae28ab22601dc5f057668341c74f5271e7673 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 10 Oct 2024 21:07:17 +0200 Subject: [PATCH 288/650] Add support for custom adapters Also simplify the DebugAdapterBInary struct --- crates/dap/src/adapters.rs | 47 +++++++++++++-------------- crates/dap_adapters/src/custom.rs | 26 ++++++++++----- crates/dap_adapters/src/javascript.rs | 42 +++++++++--------------- crates/dap_adapters/src/lldb.rs | 4 +-- crates/dap_adapters/src/php.rs | 39 +++++++--------------- crates/dap_adapters/src/python.rs | 22 +++---------- crates/task/src/debug_format.rs | 34 +++++++------------ 7 files changed, 86 insertions(+), 128 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 95cb03c848e500..236297eba01510 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -18,7 +18,7 @@ use std::{ ffi::OsString, fmt::Debug, net::{Ipv4Addr, SocketAddrV4}, - path::{Path, PathBuf}, + path::Path, process::Stdio, sync::Arc, time::Duration, @@ -60,17 +60,18 @@ pub async fn create_tcp_client( if port.is_none() { port = get_open_port(host_address).await; } - let mut command = if let Some(start_command) = &adapter_binary.start_command { - let mut command = process::Command::new(start_command); - command.arg(adapter_binary.path.clone()); - command - } else { - process::Command::new(adapter_binary.path.clone()) - }; + + let mut command = process::Command::new(&adapter_binary.command); + + if let Some(args) = &adapter_binary.arguments { + command.args(args); + } + + if let Some(envs) = &adapter_binary.envs { + command.envs(envs); + } command - .args(adapter_binary.arguments.clone()) - .envs(adapter_binary.env.clone().unwrap_or_default()) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) @@ -109,18 +110,17 @@ pub async fn create_tcp_client( /// # Parameters /// - `adapter_binary`: The debug adapter binary to start pub fn create_stdio_client(adapter_binary: &DebugAdapterBinary) -> Result { - let mut command = if let Some(start_command) = &adapter_binary.start_command { - let mut command = process::Command::new(start_command); - command.arg(adapter_binary.path.clone()); - command - } else { - let command = process::Command::new(adapter_binary.path.clone()); - command - }; + let mut command = process::Command::new(&adapter_binary.command); + + if let Some(args) = &adapter_binary.arguments { + command.args(args); + } + + if let Some(envs) = &adapter_binary.envs { + command.envs(envs); + } command - .args(adapter_binary.arguments.clone()) - .envs(adapter_binary.env.clone().unwrap_or_default()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -169,10 +169,9 @@ impl std::fmt::Display for DebugAdapterName { #[derive(Debug, Clone)] pub struct DebugAdapterBinary { - pub start_command: Option, - pub path: PathBuf, - pub arguments: Vec, - pub env: Option>, + pub command: String, + pub arguments: Option>, + pub envs: Option>, } #[async_trait(?Send)] diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 7f249767fcb9ad..a33e1802839b5b 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,3 +1,5 @@ +use std::ffi::OsString; + use serde_json::Value; use task::DebugAdapterConfig; @@ -9,7 +11,7 @@ pub(crate) struct CustomDebugAdapter { } impl CustomDebugAdapter { - const _ADAPTER_NAME: &'static str = "custom_dap"; + const ADAPTER_NAME: &'static str = "custom_dap"; pub(crate) fn new(custom_args: CustomArgs) -> Self { CustomDebugAdapter { custom_args } @@ -19,7 +21,7 @@ impl CustomDebugAdapter { #[async_trait(?Send)] impl DebugAdapter for CustomDebugAdapter { fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::_ADAPTER_NAME.into()) + DebugAdapterName(Self::ADAPTER_NAME.into()) } async fn connect( @@ -35,12 +37,8 @@ impl DebugAdapter for CustomDebugAdapter { } } - fn request_args(&self, config: &DebugAdapterConfig) -> Value { - json!({"program": config.program}) - } - async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { - bail!("Install or fetch not implemented for custom debug adapter (yet)") + Ok(()) } async fn fetch_binary( @@ -48,6 +46,18 @@ impl DebugAdapter for CustomDebugAdapter { _: &dyn DapDelegate, _: &DebugAdapterConfig, ) -> Result { - bail!("Install or fetch not implemented for custom debug adapter (yet)") + Ok(DebugAdapterBinary { + command: self.custom_args.command.clone(), + arguments: self + .custom_args + .args + .clone() + .map(|args| args.iter().map(OsString::from).collect()), + envs: self.custom_args.envs.clone(), + }) + } + + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + json!({"program": config.program}) } } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 5d24f5052ec506..288c2c05080243 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,5 +1,4 @@ use crate::*; -use std::str::FromStr; #[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct JsDebugAdapter {} @@ -36,40 +35,25 @@ impl DebugAdapter for JsDebugAdapter { async fn fetch_binary( &self, delegate: &dyn DapDelegate, - config: &DebugAdapterConfig, + _: &DebugAdapterConfig, ) -> Result { let node_runtime = delegate .node_runtime() .ok_or(anyhow!("Couldn't get npm runtime"))?; - if let Some(adapter_path) = config.adapter_path.as_ref() { - return Ok(DebugAdapterBinary { - start_command: Some( - node_runtime - .binary_path() - .await? - .to_string_lossy() - .into_owned(), - ), - path: std::path::PathBuf::from_str(adapter_path)?, - arguments: vec!["8133".into()], - env: None, - }); - } - let adapter_path = paths::debug_adapters_dir().join(self.name()); Ok(DebugAdapterBinary { - start_command: Some( - node_runtime - .binary_path() - .await? - .to_string_lossy() - .into_owned(), - ), - path: adapter_path.join(Self::ADAPTER_PATH), - arguments: vec!["8133".into()], - env: None, + command: node_runtime + .binary_path() + .await? + .to_string_lossy() + .into_owned(), + arguments: Some(vec![ + adapter_path.join(Self::ADAPTER_PATH).into(), + "8133".into(), + ]), + envs: None, }) } @@ -176,6 +160,10 @@ impl DebugAdapter for JsDebugAdapter { json!({ "program": config.program, "type": "pwa-node", + "skipFiles": [ + "/**", + "**/node_modules/**" + ] }) } } diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 19b0e77011983d..56cda6750a2752 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -8,7 +8,7 @@ use crate::*; pub(crate) struct LldbDebugAdapter {} impl LldbDebugAdapter { - const _ADAPTER_NAME: &'static str = "lldb"; + const ADAPTER_NAME: &'static str = "lldb"; pub(crate) fn new() -> Self { LldbDebugAdapter {} @@ -18,7 +18,7 @@ impl LldbDebugAdapter { #[async_trait(?Send)] impl DebugAdapter for LldbDebugAdapter { fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::_ADAPTER_NAME.into()) + DebugAdapterName(Self::ADAPTER_NAME.into()) } async fn connect( diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 7f3ed4629af226..62ecb16506bb82 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] @@ -37,40 +35,25 @@ impl DebugAdapter for PhpDebugAdapter { async fn fetch_binary( &self, delegate: &dyn DapDelegate, - config: &DebugAdapterConfig, + _: &DebugAdapterConfig, ) -> Result { let node_runtime = delegate .node_runtime() .ok_or(anyhow!("Couldn't get npm runtime"))?; - if let Some(adapter_path) = config.adapter_path.as_ref() { - return Ok(DebugAdapterBinary { - start_command: Some( - node_runtime - .binary_path() - .await? - .to_string_lossy() - .into_owned(), - ), - path: std::path::PathBuf::from_str(adapter_path)?, - arguments: vec!["--server=8132".into()], - env: None, - }); - } - let adapter_path = paths::debug_adapters_dir().join(self.name()); Ok(DebugAdapterBinary { - start_command: Some( - node_runtime - .binary_path() - .await? - .to_string_lossy() - .into_owned(), - ), - path: adapter_path.join(Self::ADAPTER_PATH), - arguments: vec!["--server=8132".into()], - env: None, + command: node_runtime + .binary_path() + .await? + .to_string_lossy() + .into_owned(), + arguments: Some(vec![ + adapter_path.join(Self::ADAPTER_PATH).into(), + "--server=8132".into(), + ]), + envs: None, }) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 98b625b61b22db..12b16280f0de08 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] @@ -31,24 +29,14 @@ impl DebugAdapter for PythonDebugAdapter { async fn fetch_binary( &self, _: &dyn DapDelegate, - config: &DebugAdapterConfig, + _: &DebugAdapterConfig, ) -> Result { - if let Some(adapter_path) = config.adapter_path.as_ref() { - return Ok(DebugAdapterBinary { - start_command: Some("python3".to_string()), - path: std::path::PathBuf::from_str(&adapter_path)?, - arguments: vec![], - env: None, - }); - } - let adapter_path = paths::debug_adapters_dir().join(self.name()); Ok(DebugAdapterBinary { - start_command: Some("python3".to_string()), - path: adapter_path.join(Self::ADAPTER_PATH), - arguments: vec![], - env: None, + command: "python3".to_string(), + arguments: Some(vec![adapter_path.join(Self::ADAPTER_PATH).into()]), + envs: None, }) } @@ -135,6 +123,6 @@ impl DebugAdapter for PythonDebugAdapter { } fn request_args(&self, config: &DebugAdapterConfig) -> Value { - json!({"program": config.program}) + json!({"program": config.program, "subProcess": true}) } } diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index b1f3a0b1c1cca7..de0d18b4243e8c 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -1,5 +1,6 @@ use schemars::{gen::SchemaSettings, JsonSchema}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::net::Ipv4Addr; use util::ResultExt; @@ -35,7 +36,7 @@ pub enum DebugRequestType { /// The Debug adapter to use #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "lowercase", tag = "kind")] pub enum DebugAdapterKind { /// Manually setup starting a debug adapter /// The argument within is used to start the DAP @@ -55,36 +56,28 @@ pub enum DebugAdapterKind { pub struct CustomArgs { /// The connection that a custom debugger should use pub connection: DebugConnectionType, - /// The cli command used to start the debug adapter - pub start_command: String, -} - -impl Default for DebugAdapterKind { - fn default() -> Self { - DebugAdapterKind::Custom(CustomArgs { - connection: DebugConnectionType::STDIO, - start_command: "".into(), - }) - } + /// The cli command used to start the debug adapter e.g. `python3`, `node` or the adapter binary + pub command: String, + /// The cli arguments used to start the debug adapter + pub args: Option>, + /// The cli envs used to start the debug adapter + pub envs: Option>, } /// Represents the configuration for the debug adapter -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugAdapterConfig { /// Unique id of for the debug adapter, /// that will be send with the `initialize` request + #[serde(flatten)] pub kind: DebugAdapterKind, /// The type of connection the adapter should use /// The type of request that should be called on the debug adapter #[serde(default)] pub request: DebugRequestType, - /// The configuration options that are send with the `launch` or `attach` request - /// to the debug adapter - // pub request_args: Option, + /// The program that you trying to debug pub program: String, - /// The path to the adapter - pub adapter_path: Option, /// Additional initialization arguments to be sent on DAP initialization pub initialize_args: Option, } @@ -99,7 +92,7 @@ pub enum DebugConnectionType { STDIO, } -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugTaskDefinition { /// Name of the debug tasks @@ -113,8 +106,6 @@ pub struct DebugTaskDefinition { adapter: DebugAdapterKind, /// Additional initialization arguments to be sent on DAP initialization initialize_args: Option, - /// The path of the debug adapter to use - adapter_path: Option, } impl DebugTaskDefinition { @@ -124,7 +115,6 @@ impl DebugTaskDefinition { kind: self.adapter, request: self.session_type, program: self.program, - adapter_path: self.adapter_path, initialize_args: self.initialize_args, }); From b1d24a052438e29ede3e9cc39c8fc04e09773d1a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 11 Oct 2024 17:56:18 +0200 Subject: [PATCH 289/650] Flatten custom adapter connection config --- crates/task/src/debug_format.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index de0d18b4243e8c..dca06f15da99de 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -55,6 +55,7 @@ pub enum DebugAdapterKind { #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] pub struct CustomArgs { /// The connection that a custom debugger should use + #[serde(flatten)] pub connection: DebugConnectionType, /// The cli command used to start the debug adapter e.g. `python3`, `node` or the adapter binary pub command: String, From 5bb7f2408a8b64c6b0e9e16159b843651c6e1c1f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 11 Oct 2024 18:55:05 +0200 Subject: [PATCH 290/650] Update dap types to fix invalid value for PresentationHint enums --- Cargo.lock | 2 +- crates/dap/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b1320b9493fc4..aa5f5722082f73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3402,7 +3402,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types#b7404edcd158d7d3ed8a7e81cf6cb3145ff3eb19" +source = "git+https://github.com/zed-industries/dap-types?rev=ca46648b65c5b21c148da9939b41255824e4b7f8#ca46648b65c5b21c148da9939b41255824e4b7f8" dependencies = [ "serde", "serde_json", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 248a51fe0ea44c..aa91e4d4f52693 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -11,7 +11,7 @@ workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "ca46648b65c5b21c148da9939b41255824e4b7f8" } fs.workspace = true futures.workspace = true gpui.workspace = true From 222cd4ba43325db328975721ed3e8d822ea4144f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 13 Oct 2024 15:19:05 +0200 Subject: [PATCH 291/650] Fix missing thread_state status update when client was terminated Before this change the debug panel was still in the running state, which is/was wrong. So updating the status to Ended, will update the UI to an correct state. --- crates/debugger_ui/src/debugger_panel.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 71fe32f3aec8ee..7ec909eccb64a3 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -455,6 +455,17 @@ impl DebugPanel { ) { let restart_args = event.clone().and_then(|e| e.restart); + for (_, thread_state) in self + .thread_states + .range_mut(&(*client_id, u64::MIN)..&(*client_id, u64::MAX)) + { + thread_state.update(cx, |thread_state, cx| { + thread_state.status = ThreadStatus::Ended; + + cx.notify(); + }); + } + self.dap_store.update(cx, |store, cx| { if restart_args.is_some() { store From b46b8aa76a1e506d16255fc954be123a3f092d91 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 14 Oct 2024 15:38:26 +0200 Subject: [PATCH 292/650] Fix clippy --- crates/task/src/task_template.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index b11219d2dc703e..b599c68661ea6b 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -95,7 +95,6 @@ mod deserialization_tests { kind: DebugAdapterKind::Python, request: crate::DebugRequestType::Launch, program: "main".to_string(), - adapter_path: None, initialize_args: None, }; let json = json!({ From 5758f664bc05984e89e5444767db193960ac1c51 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 14 Oct 2024 16:08:32 +0200 Subject: [PATCH 293/650] Add loaded sources (#49) * Remove not needed notify * Add loaded sources list * Remove not needed double nested div.child() * Remove not needed block * Fix todo for updating loaded source --- crates/dap/src/transport.rs | 6 +- crates/debugger_ui/src/debugger_panel.rs | 20 ++- crates/debugger_ui/src/debugger_panel_item.rs | 53 +++++- crates/debugger_ui/src/lib.rs | 1 + crates/debugger_ui/src/loaded_source_list.rs | 155 ++++++++++++++++++ crates/debugger_ui/src/module_list.rs | 4 +- crates/project/src/dap_store.rs | 42 ++++- 7 files changed, 254 insertions(+), 27 deletions(-) create mode 100644 crates/debugger_ui/src/loaded_source_list.rs diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 5dbde29321ee7e..5a7dbbd4ff3135 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -124,10 +124,8 @@ impl Transport { mut payload: Message, ) -> Result<()> { if let Message::Request(request) = &mut payload { - { - if let Some(sender) = current_requests.lock().await.remove(&request.seq) { - pending_requests.lock().await.insert(request.seq, sender); - } + if let Some(sender) = current_requests.lock().await.remove(&request.seq) { + pending_requests.lock().await.insert(request.seq, sender); } } Self::send_string_to_server(server_stdin, serde_json::to_string(&payload)?).await diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 7ec909eccb64a3..aa70e43fe5c4ef 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -6,8 +6,8 @@ use dap::debugger_settings::DebuggerSettings; use dap::messages::{Events, Message}; use dap::requests::{Request, StartDebugging}; use dap::{ - Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, ModuleEvent, OutputEvent, - StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, + Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, LoadedSourceEvent, ModuleEvent, + OutputEvent, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, }; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, @@ -38,6 +38,7 @@ pub enum DebugPanelEvent { Continued((DebugAdapterClientId, ContinuedEvent)), Output((DebugAdapterClientId, OutputEvent)), Module((DebugAdapterClientId, ModuleEvent)), + LoadedSource((DebugAdapterClientId, LoadedSourceEvent)), ClientStopped(DebugAdapterClientId), CapabilitiesChanged(DebugAdapterClientId), } @@ -258,7 +259,7 @@ impl DebugPanel { Events::Output(event) => self.handle_output_event(&client_id, event, cx), Events::Breakpoint(_) => {} Events::Module(event) => self.handle_module_event(&client_id, event, cx), - Events::LoadedSource(_) => {} + Events::LoadedSource(event) => self.handle_loaded_source_event(&client_id, event, cx), Events::Capabilities(event) => { self.handle_capabilities_changed_event(client_id, event, cx); } @@ -497,6 +498,15 @@ impl DebugPanel { cx.emit(DebugPanelEvent::Module((*client_id, event.clone()))); } + fn handle_loaded_source_event( + &mut self, + client_id: &DebugAdapterClientId, + event: &LoadedSourceEvent, + cx: &mut ViewContext, + ) { + cx.emit(DebugPanelEvent::LoadedSource((*client_id, event.clone()))); + } + fn handle_capabilities_changed_event( &mut self, client_id: &DebugAdapterClientId, @@ -511,8 +521,8 @@ impl DebugPanel { } fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement { - const TITLE: &str = "Debug session exited without hitting any breakpoints"; - const DESCRIPTION: &str = + const TITLE: &'static str = "Debug session exited without hitting any breakpoints"; + const DESCRIPTION: &'static str = "Try adding a breakpoint, or define the correct path mapping for your debugger."; div() diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 2b2a0e34295b12..7007ba6cf7a588 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,5 +1,6 @@ use crate::console::Console; use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState}; +use crate::loaded_source_list::LoadedSourceList; use crate::module_list::ModuleList; use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; @@ -7,8 +8,8 @@ use crate::variable_list::VariableList; use dap::client::{DebugAdapterClientId, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; use dap::{ - Capabilities, ContinuedEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, - ThreadEvent, + Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, + StoppedEvent, ThreadEvent, }; use editor::Editor; use gpui::{ @@ -31,10 +32,11 @@ pub enum DebugPanelItemEvent { #[derive(Clone, PartialEq, Eq)] enum ThreadItem { - Variables, - Modules, Console, + LoadedSource, + Modules, Output, + Variables, } pub struct DebugPanelItem { @@ -52,6 +54,7 @@ pub struct DebugPanelItem { variable_list: View, _subscriptions: Vec, stack_frame_list: View, + loaded_source_list: View, } impl DebugPanelItem { @@ -78,6 +81,9 @@ impl DebugPanelItem { let module_list = cx.new_view(|cx| ModuleList::new(dap_store.clone(), &client_id, cx)); + let loaded_source_list = + cx.new_view(|cx| LoadedSourceList::new(&this, dap_store.clone(), &client_id, cx)); + let console = cx.new_view(|cx| { Console::new( &stack_frame_list, @@ -106,6 +112,9 @@ impl DebugPanelItem { DebugPanelEvent::Module((client_id, event)) => { this.handle_module_event(client_id, event, cx) } + DebugPanelEvent::LoadedSource((client_id, event)) => { + this.handle_loaded_source_event(client_id, event, cx) + } DebugPanelEvent::ClientStopped(client_id) => { this.handle_client_stopped_event(client_id, cx) } @@ -126,7 +135,7 @@ impl DebugPanelItem { &stack_frame_list, move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { StackFrameListEvent::ChangedStackFrame => this.clear_highlights(cx), - StackFrameListEvent::StackFramesUpdated => {} + _ => {} }, ), ]; @@ -157,6 +166,7 @@ impl DebugPanelItem { variable_list, _subscriptions, stack_frame_list, + loaded_source_list, client_id: *client_id, client_kind: client_kind.clone(), active_thread_item: ThreadItem::Variables, @@ -176,8 +186,6 @@ impl DebugPanelItem { { self.clear_highlights(cx); } - - cx.notify(); } fn should_skip_event(&self, client_id: &DebugAdapterClientId, thread_id: u64) -> bool { @@ -275,6 +283,22 @@ impl DebugPanelItem { }); } + fn handle_loaded_source_event( + &mut self, + client_id: &DebugAdapterClientId, + event: &LoadedSourceEvent, + cx: &mut ViewContext, + ) { + if self.should_skip_event(client_id, self.thread_id) { + return; + } + + self.loaded_source_list + .update(cx, |loaded_source_list, cx| { + loaded_source_list.on_loaded_source_event(event, cx); + }); + } + fn handle_client_stopped_event( &mut self, client_id: &DebugAdapterClientId, @@ -645,6 +669,18 @@ impl Render for DebugPanelItem { )) }, ) + .when( + capabilities + .supports_loaded_sources_request + .unwrap_or_default(), + |this| { + this.child(self.render_entry_button( + &SharedString::from("Loaded Sources"), + ThreadItem::LoadedSource, + cx, + )) + }, + ) .child(self.render_entry_button( &SharedString::from("Console"), ThreadItem::Console, @@ -662,6 +698,9 @@ impl Render for DebugPanelItem { .when(*active_thread_item == ThreadItem::Modules, |this| { this.size_full().child(self.module_list.clone()) }) + .when(*active_thread_item == ThreadItem::LoadedSource, |this| { + this.size_full().child(self.loaded_source_list.clone()) + }) .when(*active_thread_item == ThreadItem::Output, |this| { this.child(self.output_editor.clone()) }) diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index a45a6e5227f49f..a81b4de92b2589 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -11,6 +11,7 @@ use workspace::{ mod console; pub mod debugger_panel; mod debugger_panel_item; +mod loaded_source_list; mod module_list; mod stack_frame_list; mod variable_list; diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs new file mode 100644 index 00000000000000..761a33be05e5d4 --- /dev/null +++ b/crates/debugger_ui/src/loaded_source_list.rs @@ -0,0 +1,155 @@ +use anyhow::Result; +use dap::{client::DebugAdapterClientId, LoadedSourceEvent, Source}; +use gpui::{ + list, AnyElement, FocusHandle, FocusableView, ListState, Model, Subscription, Task, View, +}; +use project::dap_store::DapStore; +use ui::prelude::*; + +use crate::debugger_panel_item::{self, DebugPanelItem, DebugPanelItemEvent}; + +pub struct LoadedSourceList { + list: ListState, + sources: Vec, + focus_handle: FocusHandle, + dap_store: Model, + client_id: DebugAdapterClientId, + _subscriptions: Vec, +} + +impl LoadedSourceList { + pub fn new( + debug_panel_item: &View, + dap_store: Model, + client_id: &DebugAdapterClientId, + cx: &mut ViewContext, + ) -> Self { + let weakview = cx.view().downgrade(); + let focus_handle = cx.focus_handle(); + + let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { + weakview + .upgrade() + .map(|view| view.update(cx, |this, cx| this.render_entry(ix, cx))) + .unwrap_or(div().into_any()) + }); + + let _subscriptions = + vec![cx.subscribe(debug_panel_item, Self::handle_debug_panel_item_event)]; + + Self { + list, + dap_store, + focus_handle, + _subscriptions, + client_id: *client_id, + sources: Vec::default(), + } + } + + fn handle_debug_panel_item_event( + &mut self, + _: View, + event: &debugger_panel_item::DebugPanelItemEvent, + cx: &mut ViewContext, + ) { + match event { + DebugPanelItemEvent::Stopped { .. } => { + self.fetch_loaded_sources(cx).detach_and_log_err(cx); + } + _ => {} + } + } + + pub fn on_loaded_source_event( + &mut self, + event: &LoadedSourceEvent, + cx: &mut ViewContext, + ) { + match event.reason { + dap::LoadedSourceEventReason::New => self.sources.push(event.source.clone()), + dap::LoadedSourceEventReason::Changed => { + let updated_source = + if let Some(ref_id) = event.source.source_reference.filter(|&r| r != 0) { + self.sources + .iter_mut() + .find(|s| s.source_reference == Some(ref_id)) + } else if let Some(path) = &event.source.path { + self.sources + .iter_mut() + .find(|s| s.path.as_ref() == Some(path)) + } else { + self.sources + .iter_mut() + .find(|s| s.name == event.source.name) + }; + + if let Some(loaded_source) = updated_source { + *loaded_source = event.source.clone(); + } + } + dap::LoadedSourceEventReason::Removed => { + self.sources.retain(|source| *source != event.source) + } + } + + self.list.reset(self.sources.len()); + cx.notify(); + } + + fn fetch_loaded_sources(&self, cx: &mut ViewContext) -> Task> { + let task = self + .dap_store + .update(cx, |store, cx| store.loaded_sources(&self.client_id, cx)); + + cx.spawn(|this, mut cx| async move { + let mut sources = task.await?; + + this.update(&mut cx, |this, cx| { + std::mem::swap(&mut this.sources, &mut sources); + this.list.reset(this.sources.len()); + + cx.notify(); + }) + }) + } + + fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let source = &self.sources[ix]; + + v_flex() + .rounded_md() + .w_full() + .group("") + .p_1() + .hover(|s| s.bg(cx.theme().colors().element_hover)) + .child( + h_flex() + .gap_0p5() + .text_ui_sm(cx) + .when_some(source.name.clone(), |this, name| this.child(name)), + ) + .child( + h_flex() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .when_some(source.path.clone(), |this, path| this.child(path)), + ) + .into_any() + } +} + +impl FocusableView for LoadedSourceList { + fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for LoadedSourceList { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + div() + .size_full() + .p_1() + .child(list(self.list.clone()).size_full()) + } +} diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 1a87913f34c94a..5973cfd7682b53 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -87,9 +87,7 @@ impl ModuleList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(module.path.clone(), |this, path| { - this.child(div().child(path)) - }), + .when_some(module.path.clone(), |this, path| this.child(path)), ) .into_any() } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index d30ac85b744ac4..e401f54cae6616 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -5,19 +5,19 @@ use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::Message; use dap::requests::{ Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, - Modules, Next, Pause, Scopes, SetBreakpoints, SetExpression, SetVariable, StackTrace, StepIn, - StepOut, Terminate, TerminateThreads, Variables, + LoadedSources, Modules, Next, Pause, Scopes, SetBreakpoints, SetExpression, SetVariable, + StackTrace, StepIn, StepOut, Terminate, TerminateThreads, Variables, }; use dap::{ AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, - InitializeRequestArgumentsPathFormat, LaunchRequestArguments, Module, ModulesArguments, - NextArguments, PauseArguments, Scope, ScopesArguments, SetBreakpointsArguments, - SetExpressionArguments, SetVariableArguments, Source, SourceBreakpoint, StackFrame, - StackTraceArguments, StartDebuggingRequestArguments, StepInArguments, StepOutArguments, - SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, - VariablesArguments, + InitializeRequestArgumentsPathFormat, LaunchRequestArguments, LoadedSourcesArguments, Module, + ModulesArguments, NextArguments, PauseArguments, Scope, ScopesArguments, + SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, + SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, + StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, + TerminateThreadsArguments, Variable, VariablesArguments, }; use dap_adapters::build_adapter; use fs::Fs; @@ -400,6 +400,32 @@ impl DapStore { }) } + pub fn loaded_sources( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Task>> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Client was not found"))); + }; + + let capabilities = self.capabilities_by_id(client_id); + + if !capabilities + .supports_loaded_sources_request + .unwrap_or_default() + { + return Task::ready(Ok(Vec::default())); + } + + cx.spawn(|_, _| async move { + Ok(client + .request::(LoadedSourcesArguments {}) + .await? + .sources) + }) + } + pub fn stack_frames( &mut self, client_id: &DebugAdapterClientId, From c65ed1c738e177bae0b90dec62cd60795cb8c2f7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 14 Oct 2024 17:40:13 +0200 Subject: [PATCH 294/650] Remove type duplication for debug settings JSON schema --- Cargo.lock | 3 ++- crates/dap/Cargo.toml | 2 +- crates/dap/src/debugger_settings.rs | 24 +------------------ crates/debugger_ui/src/debugger_panel_item.rs | 6 ++--- 4 files changed, 7 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a9334b958b77f..4f2d0c5c9f2698 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3365,8 +3365,9 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=ca46648b65c5b21c148da9939b41255824e4b7f8#ca46648b65c5b21c148da9939b41255824e4b7f8" +source = "git+https://github.com/zed-industries/dap-types?rev=b95818130022bfc72bbcd639bdd0c0358c7549fc#b95818130022bfc72bbcd639bdd0c0358c7549fc" dependencies = [ + "schemars", "serde", "serde_json", ] diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index aa91e4d4f52693..33971d417bfd14 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -11,7 +11,7 @@ workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "ca46648b65c5b21c148da9939b41255824e4b7f8" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b95818130022bfc72bbcd639bdd0c0358c7549fc" } fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index d8ac68a3b1ca7d..0d5a744a849365 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -1,3 +1,4 @@ +use dap_types::SteppingGranularity; use gpui::{AppContext, Global}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -20,19 +21,6 @@ pub struct DebuggerSettings { pub button: bool, } -#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum SteppingGranularity { - /// The step should allow the program to run until the current statement has finished executing. - /// The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. - /// For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. - Statement, - /// The step should allow the program to run until the current source line has executed. - Line, - /// The step should allow one instruction to execute (e.g. one x86 instruction). - Instruction, -} - impl Default for DebuggerSettings { fn default() -> Self { Self { @@ -43,16 +31,6 @@ impl Default for DebuggerSettings { } } -impl DebuggerSettings { - pub fn stepping_granularity(&self) -> dap_types::SteppingGranularity { - match &self.stepping_granularity { - SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement, - SteppingGranularity::Line => dap_types::SteppingGranularity::Line, - SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction, - } - } -} - impl Settings for DebuggerSettings { const KEY: Option<&'static str> = Some("debugger"); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 7007ba6cf7a588..c203817fa2fadb 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -413,7 +413,7 @@ impl DebugPanelItem { pub fn step_over(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); - let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); + let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.dap_store.update(cx, |store, cx| { store @@ -425,7 +425,7 @@ impl DebugPanelItem { pub fn step_in(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); - let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); + let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.dap_store.update(cx, |store, cx| { store @@ -437,7 +437,7 @@ impl DebugPanelItem { pub fn step_out(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); - let granularity = DebuggerSettings::get_global(cx).stepping_granularity(); + let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.dap_store.update(cx, |store, cx| { store From 13afc3741f4e5ec19a10378230266afbbfabb650 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 14 Oct 2024 20:57:07 +0200 Subject: [PATCH 295/650] Fix flicker the variable list less when clicking fast step over control --- crates/debugger_ui/src/variable_list.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index e07351e155dd31..ab713f965fdd75 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -288,11 +288,6 @@ impl VariableList { fn fetch_variables(&mut self, cx: &mut ViewContext) { let stack_frames = self.stack_frame_list.read(cx).stack_frames().clone(); - self.fetch_variables_task.take(); - self.variables.clear(); - self.scopes.clear(); - self.fetched_variable_ids.clear(); - self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { let mut scope_tasks = Vec::with_capacity(stack_frames.len()); for stack_frame in stack_frames.clone().into_iter() { @@ -328,8 +323,14 @@ impl VariableList { }); } - for (stack_frame_id, scopes) in try_join_all(stack_frame_tasks).await? { - this.update(&mut cx, |this, _| { + let result = try_join_all(stack_frame_tasks).await?; + + this.update(&mut cx, |this, cx| { + this.variables.clear(); + this.scopes.clear(); + this.fetched_variable_ids.clear(); + + for (stack_frame_id, scopes) in result { for (scope, variables) in scopes { this.scopes .entry(stack_frame_id) @@ -350,10 +351,8 @@ impl VariableList { .collect::>(), ); } - })?; - } + } - this.update(&mut cx, |this, cx| { this.build_entries(true, false, cx); this.fetch_variables_task.take(); From 1c1e34b3d256c5a665d5b423b33a4ec4e01c6522 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 17 Oct 2024 13:46:11 +0200 Subject: [PATCH 296/650] Debug terminal (#50) * Make program optional in debug task format * Remove default config for skipFile JavaScript debugger * Add Debug case to TerminalKind * Don't allow serializing debug terminals * Add respond method so we can send response back for reverse requests * Implement run in terminal reverse request * Move client calls to dap store This commit also fixes an issue with not sending a response for the `StartDebugging` reverse request. * Make clippy happy --- Cargo.lock | 2 + crates/dap/src/client.rs | 25 +-- crates/dap_adapters/src/javascript.rs | 4 - crates/debugger_ui/Cargo.toml | 2 + crates/debugger_ui/src/debugger_panel.rs | 195 ++++++++++++++++----- crates/project/src/dap_store.rs | 85 ++++++++- crates/project/src/terminals.rs | 27 +++ crates/task/src/debug_format.rs | 4 +- crates/task/src/lib.rs | 9 +- crates/task/src/task_template.rs | 20 ++- crates/terminal/src/pty_info.rs | 4 + crates/terminal/src/terminal.rs | 7 + crates/terminal_view/src/terminal_panel.rs | 6 +- 13 files changed, 303 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f2d0c5c9f2698..c07d975fc11515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3468,6 +3468,7 @@ name = "debugger_ui" version = "0.1.0" dependencies = [ "anyhow", + "collections", "dap", "editor", "futures 0.3.30", @@ -3482,6 +3483,7 @@ dependencies = [ "settings", "task", "tasks_ui", + "terminal_view", "theme", "ui", "workspace", diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index ddf987c02dcfb3..e484d1516179fe 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -159,18 +159,13 @@ impl DebugAdapterClient { arguments: Some(serialized_arguments), }; - { - self.transport - .current_requests - .lock() - .await - .insert(sequence_id, callback_tx); - } - self.transport - .server_tx - .send(Message::Request(request)) - .await?; + .current_requests + .lock() + .await + .insert(sequence_id, callback_tx); + + self.respond(Message::Request(request)).await?; let response = callback_rx.recv().await??; @@ -180,6 +175,14 @@ impl DebugAdapterClient { } } + pub async fn respond(&self, message: Message) -> Result<()> { + self.transport + .server_tx + .send(message) + .await + .map_err(|e| anyhow::anyhow!("Failed to send response back: {}", e)) + } + pub fn id(&self) -> DebugAdapterClientId { self.id } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 288c2c05080243..185a785a6ff4cb 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -160,10 +160,6 @@ impl DebugAdapter for JsDebugAdapter { json!({ "program": config.program, "type": "pwa-node", - "skipFiles": [ - "/**", - "**/node_modules/**" - ] }) } } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 1e720b53cda995..ab52ad8eee06b8 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] anyhow.workspace = true +collections.workspace = true dap.workspace = true editor.workspace = true futures.workspace = true @@ -24,6 +25,7 @@ serde_json.workspace = true settings.workspace = true task.workspace = true tasks_ui.workspace = true +terminal_view.workspace = true theme.workspace = true ui.workspace = true workspace.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index aa70e43fe5c4ef..37563ad4911469 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,24 +1,26 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; -use dap::client::DebugAdapterClient; +use collections::{BTreeMap, HashMap}; use dap::client::{DebugAdapterClientId, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; use dap::messages::{Events, Message}; -use dap::requests::{Request, StartDebugging}; +use dap::requests::{Request, RunInTerminal, StartDebugging}; use dap::{ Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, LoadedSourceEvent, ModuleEvent, - OutputEvent, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, + OutputEvent, RunInTerminalRequestArguments, StoppedEvent, TerminatedEvent, ThreadEvent, + ThreadEventReason, }; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, FontWeight, Model, Subscription, Task, View, ViewContext, WeakView, }; use project::dap_store::DapStore; +use project::terminals::TerminalKind; use serde_json::Value; use settings::Settings; -use std::collections::BTreeMap; -use std::sync::Arc; +use std::path::PathBuf; use std::u64; +use terminal_view::terminal_panel::TerminalPanel; use ui::prelude::*; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -92,28 +94,29 @@ impl DebugPanel { cx.subscribe(&pane, Self::handle_pane_event), cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { - project::Event::DebugClientEvent { message, client_id } => { - let Some(client) = this.debug_client_by_id(client_id, cx) else { - return cx.emit(DebugPanelEvent::ClientStopped(*client_id)); - }; - - match message { - Message::Event(event) => { - this.handle_debug_client_events(client_id, event, cx); - } - Message::Request(request) => { - if StartDebugging::COMMAND == request.command { - Self::handle_start_debugging_request( - this, - client, - request.arguments.clone(), - cx, - ); - } + project::Event::DebugClientEvent { message, client_id } => match message { + Message::Event(event) => { + this.handle_debug_client_events(client_id, event, cx); + } + Message::Request(request) => { + if StartDebugging::COMMAND == request.command { + this.handle_start_debugging_request( + client_id, + request.seq, + request.arguments.clone(), + cx, + ); + } else if RunInTerminal::COMMAND == request.command { + this.handle_run_in_terminal_request( + client_id, + request.seq, + request.arguments.clone(), + cx, + ); } - _ => unreachable!(), } - } + _ => unreachable!(), + }, project::Event::DebugClientStopped(client_id) => { cx.emit(DebugPanelEvent::ClientStopped(*client_id)); @@ -131,11 +134,11 @@ impl DebugPanel { pane, size: px(300.), _subscriptions, - dap_store: project.read(cx).dap_store(), focus_handle: cx.focus_handle(), show_did_not_stop_warning: false, thread_states: Default::default(), workspace: workspace.weak_handle(), + dap_store: project.read(cx).dap_store(), } }) } @@ -159,23 +162,6 @@ impl DebugPanel { .and_then(|panel| panel.downcast::()) } - fn debug_client_by_id( - &self, - client_id: &DebugAdapterClientId, - cx: &mut ViewContext, - ) -> Option> { - self.workspace - .update(cx, |this, cx| { - this.project() - .read(cx) - .dap_store() - .read(cx) - .client_by_id(client_id) - }) - .ok() - .flatten() - } - fn handle_pane_event( &mut self, _: View, @@ -227,20 +213,133 @@ impl DebugPanel { } fn handle_start_debugging_request( - this: &mut Self, - client: Arc, + &mut self, + client_id: &DebugAdapterClientId, + seq: u64, request_args: Option, cx: &mut ViewContext, ) { - let start_args = if let Some(args) = request_args { + let args = if let Some(args) = request_args { serde_json::from_value(args.clone()).ok() } else { None }; - this.dap_store.update(cx, |store, cx| { - store.start_client(client.config(), start_args, cx); + self.dap_store.update(cx, |store, cx| { + store + .respond_to_start_debugging(client_id, seq, args, cx) + .detach_and_log_err(cx); + }); + } + + fn handle_run_in_terminal_request( + &mut self, + client_id: &DebugAdapterClientId, + seq: u64, + request_args: Option, + cx: &mut ViewContext, + ) { + let Some(request_args) = request_args else { + self.dap_store.update(cx, |store, cx| { + store + .respond_to_run_in_terminal(client_id, false, seq, None, cx) + .detach_and_log_err(cx); + }); + + return; + }; + + let request_args: RunInTerminalRequestArguments = + serde_json::from_value(request_args).unwrap(); + + let mut envs: HashMap = Default::default(); + + if let Some(Value::Object(env)) = request_args.env { + // Special handling for VSCODE_INSPECTOR_OPTIONS: + // The JavaScript debug adapter expects this value to be a valid JSON object. + // However, it's often passed as an escaped string, which the adapter can't parse. + // We need to unescape it and reformat it so the adapter can read it correctly. + for (key, value) in env { + let value_str = match (key.as_str(), value) { + ("VSCODE_INSPECTOR_OPTIONS", Value::String(value)) => { + serde_json::from_str::(&value[3..]) + .map(|json| format!(":::{}", json)) + .unwrap_or_else(|_| value) + } + (_, value) => value.to_string(), + }; + + envs.insert(key, value_str.trim_matches('"').to_string()); + } + } + + let terminal_task = self.workspace.update(cx, |workspace, cx| { + let terminal_panel = workspace.panel::(cx).unwrap(); + + terminal_panel.update(cx, |terminal_panel, cx| { + let mut args = request_args.args.clone(); + + // Handle special case for NodeJS debug adapter + // If only the Node binary path is provided, we set the command to None + // This prevents the NodeJS REPL from appearing, which is not the desired behavior + // The expected usage is for users to provide their own Node command, e.g., `node test.js` + // This allows the NodeJS debug client to attach correctly + let command = if args.len() > 1 { + Some(args.remove(0)) + } else { + None + }; + + let terminal_task = terminal_panel.add_terminal( + TerminalKind::Debug { + command, + args, + envs, + cwd: PathBuf::from(request_args.cwd), + }, + task::RevealStrategy::Always, + cx, + ); + + cx.spawn(|_, mut cx| async move { + let pid_task = async move { + let terminal = terminal_task.await?; + + terminal.read_with(&mut cx, |terminal, _| terminal.pty_info.pid()) + }; + + pid_task.await + }) + }) }); + + let client_id = *client_id; + cx.spawn(|this, mut cx| async move { + // Ensure a response is always sent, even in error cases, + // to maintain proper communication with the debug adapter + let (success, pid) = match terminal_task { + Ok(pid_task) => match pid_task.await { + Ok(pid) => (true, pid), + Err(_) => (false, None), + }, + Err(_) => (false, None), + }; + + let respond_task = this.update(&mut cx, |this, cx| { + this.dap_store.update(cx, |store, cx| { + store.respond_to_run_in_terminal( + &client_id, + success, + seq, + pid.map(|pid| pid.as_u32() as u64), + cx, + ) + }) + }); + + respond_task?.await + }) + .detach_and_log_err(cx); } fn handle_debug_client_events( diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e401f54cae6616..12715a96644cec 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -2,18 +2,19 @@ use crate::ProjectPath; use anyhow::{anyhow, Context as _, Result}; use collections::{HashMap, HashSet}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; -use dap::messages::Message; +use dap::messages::{Message, Response}; use dap::requests::{ Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, - LoadedSources, Modules, Next, Pause, Scopes, SetBreakpoints, SetExpression, SetVariable, - StackTrace, StepIn, StepOut, Terminate, TerminateThreads, Variables, + LoadedSources, Modules, Next, Pause, Request as _, RunInTerminal, Scopes, SetBreakpoints, + SetExpression, SetVariable, StackTrace, StartDebugging, StepIn, StepOut, Terminate, + TerminateThreads, Variables, }; use dap::{ AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, - ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, EvaluateArguments, - EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, + ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, + EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, LoadedSourcesArguments, Module, - ModulesArguments, NextArguments, PauseArguments, Scope, ScopesArguments, + ModulesArguments, NextArguments, PauseArguments, RunInTerminalResponse, Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, @@ -334,14 +335,14 @@ impl DapStore { path_format: Some(InitializeRequestArgumentsPathFormat::Path), supports_variable_type: Some(true), supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(false), + supports_run_in_terminal_request: Some(true), supports_memory_references: Some(true), supports_progress_reporting: Some(false), supports_invalidated_event: Some(false), lines_start_at1: Some(false), columns_start_at1: Some(false), supports_memory_event: Some(false), - supports_args_can_be_interpreted_by_shell: Some(true), + supports_args_can_be_interpreted_by_shell: Some(false), supports_start_debugging_request: Some(true), }) .await?; @@ -495,6 +496,74 @@ impl DapStore { }) } + pub fn respond_to_start_debugging( + &self, + client_id: &DebugAdapterClientId, + seq: u64, + args: Option, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|this, mut cx| async move { + client + .respond(Message::Response(Response { + seq, + request_seq: seq, + success: true, + command: StartDebugging::COMMAND.to_string(), + body: None, + })) + .await?; + + this.update(&mut cx, |store, cx| { + store.start_client(client.config(), args, cx); + }) + }) + } + + pub fn respond_to_run_in_terminal( + &self, + client_id: &DebugAdapterClientId, + success: bool, + seq: u64, + shell_pid: Option, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not found client"))); + }; + + cx.spawn(|_, _| async move { + if success { + client + .respond(Message::Response(Response { + seq, + request_seq: seq, + success: true, + command: RunInTerminal::COMMAND.to_string(), + body: Some(serde_json::to_value(RunInTerminalResponse { + process_id: Some(std::process::id() as u64), + shell_process_id: shell_pid, + })?), + })) + .await + } else { + client + .respond(Message::Response(Response { + seq, + request_seq: seq, + success: false, + command: RunInTerminal::COMMAND.to_string(), + body: Some(serde_json::to_value(ErrorResponse { error: None })?), + })) + .await + } + }) + } + pub fn continue_thread( &self, client_id: &DebugAdapterClientId, diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 11b3152f0cf660..dfcfaf56fcb198 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -32,6 +32,13 @@ pub enum TerminalKind { Shell(Option), /// Run a task. Task(SpawnInTerminal), + /// Run a debug terminal. + Debug { + command: Option, + args: Vec, + envs: HashMap, + cwd: PathBuf, + }, } /// SshCommand describes how to connect to a remote server @@ -100,6 +107,7 @@ impl Project { self.active_project_directory(cx) } } + TerminalKind::Debug { cwd, .. } => Some(cwd.clone()), }; let ssh_command = self.ssh_command(cx); @@ -136,6 +144,8 @@ impl Project { .and_then(|path| self.python_venv_directory(path, settings, cx)); let mut python_venv_activate_command = None; + let debug_terminal = matches!(kind, TerminalKind::Debug { .. }); + let (spawn_task, shell) = match kind { TerminalKind::Shell(_) => { if let Some(python_venv_directory) = python_venv_directory { @@ -162,6 +172,22 @@ impl Project { None => (None, settings.shell.clone()), } } + TerminalKind::Debug { + command, + args, + envs, + .. + } => { + env.extend(envs); + + let shell = if let Some(program) = command { + Shell::WithArguments { program, args } + } else { + settings.shell.clone() + }; + + (None, shell) + } TerminalKind::Task(spawn_task) => { let task_state = Some(TaskState { id: spawn_task.id, @@ -224,6 +250,7 @@ impl Project { settings.max_scroll_history_lines, window, completion_tx, + debug_terminal, cx, ) .map(|builder| { diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index dca06f15da99de..8aa08cc4470cb4 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -78,7 +78,7 @@ pub struct DebugAdapterConfig { #[serde(default)] pub request: DebugRequestType, /// The program that you trying to debug - pub program: String, + pub program: Option, /// Additional initialization arguments to be sent on DAP initialization pub initialize_args: Option, } @@ -99,7 +99,7 @@ pub struct DebugTaskDefinition { /// Name of the debug tasks label: String, /// Program to run the debugger on - program: String, + program: Option, /// Launch | Request depending on the session the adapter should be ran as #[serde(default)] session_type: DebugRequestType, diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index bf094775b7bb5b..f2f76fdfff2daa 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -99,13 +99,12 @@ impl ResolvedTask { match self.original_task.task_type.clone() { TaskType::Script => None, TaskType::Debug(mut adapter_config) => { - adapter_config.program = match &self.resolved { + let program = match &self.resolved { None => adapter_config.program, - Some(spawn_in_terminal) => spawn_in_terminal - .program - .clone() - .unwrap_or(adapter_config.program), + Some(spawn_in_terminal) => spawn_in_terminal.program.clone(), }; + + adapter_config.program = program; Some(adapter_config) } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index b599c68661ea6b..4b5d2e78073cd8 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -94,7 +94,7 @@ mod deserialization_tests { let adapter_config = DebugAdapterConfig { kind: DebugAdapterKind::Python, request: crate::DebugRequestType::Launch, - program: "main".to_string(), + program: Some("main".to_string()), initialize_args: None, }; let json = json!({ @@ -243,12 +243,18 @@ impl TaskTemplate { let program = match &self.task_type { TaskType::Script => None, - TaskType::Debug(adapter_config) => Some(substitute_all_template_variables_in_str( - &adapter_config.program, - &task_variables, - &variable_names, - &mut substituted_variables, - )?), + TaskType::Debug(adapter_config) => { + if let Some(program) = &adapter_config.program { + Some(substitute_all_template_variables_in_str( + program, + &task_variables, + &variable_names, + &mut substituted_variables, + )?) + } else { + None + } + } }; let task_hash = to_hex_hash(self) diff --git a/crates/terminal/src/pty_info.rs b/crates/terminal/src/pty_info.rs index 559d022fda8a0c..79b00c36137c50 100644 --- a/crates/terminal/src/pty_info.rs +++ b/crates/terminal/src/pty_info.rs @@ -142,4 +142,8 @@ impl PtyProcessInfo { } has_changed } + + pub fn pid(&self) -> Option { + self.pid_getter.pid() + } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d02da38d94ee6f..468f4f4248208a 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -330,6 +330,7 @@ impl TerminalBuilder { max_scroll_history_lines: Option, window: AnyWindowHandle, completion_tx: Sender<()>, + debug_terminal: bool, cx: &AppContext, ) -> Result { // If the parent environment doesn't have a locale set @@ -457,6 +458,7 @@ impl TerminalBuilder { url_regex: RegexSearch::new(URL_REGEX).unwrap(), word_regex: RegexSearch::new(WORD_REGEX).unwrap(), vi_mode_enabled: false, + debug_terminal, }; Ok(TerminalBuilder { @@ -613,6 +615,7 @@ pub struct Terminal { word_regex: RegexSearch, task: Option, vi_mode_enabled: bool, + debug_terminal: bool, } pub struct TaskState { @@ -1682,6 +1685,10 @@ impl Terminal { self.task.as_ref() } + pub fn debug_terminal(&self) -> bool { + self.debug_terminal + } + pub fn wait_for_completed_task(&self, cx: &AppContext) -> Task<()> { if let Some(task) = self.task() { if task.status == TaskStatus::Running { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index cfe5638af34cb6..b4e9d43b4fb461 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -564,7 +564,7 @@ impl TerminalPanel { }) } - fn add_terminal( + pub fn add_terminal( &mut self, kind: TerminalKind, reveal_strategy: RevealStrategy, @@ -620,7 +620,9 @@ impl TerminalPanel { .items() .filter_map(|item| { let terminal_view = item.act_as::(cx)?; - if terminal_view.read(cx).terminal().read(cx).task().is_some() { + let terminal = terminal_view.read(cx).terminal().read(cx); + + if terminal.task().is_some() || terminal.debug_terminal() { None } else { let id = item.item_id().as_u64(); From afe228fd77f520281acfeda2841f92ba1fa5dd79 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 17 Oct 2024 20:14:12 +0200 Subject: [PATCH 297/650] Fix debug kind in debug panel item tab content --- crates/task/src/debug_format.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 8aa08cc4470cb4..51e60d19755c19 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -51,6 +51,18 @@ pub enum DebugAdapterKind { Lldb, } +impl std::fmt::Display for DebugAdapterKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Custom(_) => "Custom", + Self::Python => "Python", + Self::PHP => "PHP", + Self::Javascript => "JavaScript", + Self::Lldb => "LLDB", + }) + } +} + /// Custom arguments used to setup a custom debugger #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] pub struct CustomArgs { From 2e028a70382fabb78be8f4a1f6a42e9dfad582d2 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 20 Oct 2024 04:10:16 -0400 Subject: [PATCH 298/650] Fix debug tasks loading from .zed/debug.json --- crates/project/src/project_settings.rs | 33 +++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 3ecf0e300878ca..8d740f677b5a2a 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -4,8 +4,8 @@ use fs::Fs; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext}; use language::LanguageServerName; use paths::{ - local_settings_file_relative_path, local_tasks_file_relative_path, - local_vscode_tasks_file_relative_path, + local_debug_file_relative_path, local_settings_file_relative_path, + local_tasks_file_relative_path, local_vscode_tasks_file_relative_path, }; use rpc::{proto, AnyProtoClient, TypedEnvelope}; use schemars::JsonSchema; @@ -19,7 +19,7 @@ use std::{ sync::Arc, time::Duration, }; -use task::{TaskTemplates, VsCodeTaskFile}; +use task::{DebugTaskFile, TaskTemplates, VsCodeTaskFile}; use util::ResultExt; use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId}; @@ -453,6 +453,18 @@ impl SettingsObserver { .unwrap(), ); (settings_dir, LocalSettingsKind::Tasks) + } else if path.ends_with(local_debug_file_relative_path()) { + let settings_dir = Arc::::from( + path.ancestors() + .nth( + local_debug_file_relative_path() + .components() + .count() + .saturating_sub(1), + ) + .unwrap(), + ); + (settings_dir, LocalSettingsKind::Tasks) } else { continue; }; @@ -493,6 +505,21 @@ impl SettingsObserver { "serializing Zed tasks into JSON, file {abs_path:?}" ) }) + } else if abs_path.ends_with(local_debug_file_relative_path()) { + let debug_tasks_content = parse_json_with_comments::(&content).with_context(|| { + format!("Parsing Zed debug tasks, file {abs_path:?}") + })?; + + let zed_debug_tasks = TaskTemplates::try_from(debug_tasks_content) + .with_context(|| { + format!("Converting zed debugger tasks into Zed tasks, file {abs_path:?}") + })?; + + serde_json::to_string(&zed_debug_tasks).with_context(|| { + format!( + "serializing Zed debug tasks into JSON, file {abs_path:?}" + ) + }) } else { Ok(content) } From 6735cfad677399cc2b40975b1d30420b0fcf926d Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 20 Oct 2024 06:16:07 -0400 Subject: [PATCH 299/650] Fix debug tasks and regular tasks overwritting each other --- crates/project/src/project_settings.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 8d740f677b5a2a..e8db15d5b6ea40 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -464,7 +464,12 @@ impl SettingsObserver { ) .unwrap(), ); - (settings_dir, LocalSettingsKind::Tasks) + ( + // Debug task file name has to be unique because it will overwrite tasks + // from .zed/tasks.json file if it is not (It was also being overwritten too) + Arc::from(settings_dir.join("debug").as_path()), + LocalSettingsKind::Tasks, + ) } else { continue; }; From fc78c40385d0715e6e58cb869c9734c99ba2d279 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 20 Oct 2024 19:29:34 +0200 Subject: [PATCH 300/650] Move how we connect to an debug adpater to the transport layer (#52) * Move how we connect to an debug adpater to the transport layer This PR cleans up how we connect to a debug adapter. Previously, this was done inside the debug adapter implementation. While reviewing the debugger RPC log view PR, I noticed that we could simplify the transport/client code, making it easier to attach handlers for logging RPC messages. * Remove not needed async block * Change hardcoded delay before connecting to tcp adapter to timeout approach Co-Authored-By: Anthony Eid --------- Co-authored-by: Anthony Eid --- crates/dap/src/adapters.rs | 156 +------- crates/dap/src/client.rs | 132 ++----- crates/dap/src/transport.rs | 478 +++++++++++++++++------- crates/dap_adapters/src/custom.rs | 13 +- crates/dap_adapters/src/dap_adapters.rs | 9 +- crates/dap_adapters/src/javascript.rs | 16 +- crates/dap_adapters/src/lldb.rs | 9 +- crates/dap_adapters/src/php.rs | 16 +- crates/dap_adapters/src/python.rs | 10 +- crates/project/src/dap_store.rs | 43 ++- crates/task/src/debug_format.rs | 4 +- 11 files changed, 430 insertions(+), 456 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 236297eba01510..ca4662bac61669 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,42 +1,13 @@ -use crate::client::TransportParams; +use crate::transport::Transport; use ::fs::Fs; -use anyhow::{anyhow, Context, Result}; +use anyhow::Result; use async_trait::async_trait; -use futures::AsyncReadExt; -use gpui::AsyncAppContext; use http_client::HttpClient; use node_runtime::NodeRuntime; use serde_json::Value; -use smol::{ - self, - io::BufReader, - net::{TcpListener, TcpStream}, - process, -}; -use std::{ - collections::HashMap, - ffi::OsString, - fmt::Debug, - net::{Ipv4Addr, SocketAddrV4}, - path::Path, - process::Stdio, - sync::Arc, - time::Duration, -}; +use std::{collections::HashMap, ffi::OsString, path::Path, sync::Arc}; -use task::{DebugAdapterConfig, TCPHost}; - -/// Get an open port to use with the tcp client when not supplied by debug config -async fn get_open_port(host: Ipv4Addr) -> Option { - Some( - TcpListener::bind(SocketAddrV4::new(host, 0)) - .await - .ok()? - .local_addr() - .ok()? - .port(), - ) -} +use task::DebugAdapterConfig; pub trait DapDelegate { fn http_client(&self) -> Option>; @@ -44,115 +15,6 @@ pub trait DapDelegate { fn fs(&self) -> Arc; } -/// TCP clients don't have an error communication stream with an adapter -/// # Parameters -/// - `host`: The ip/port that that the client will connect too -/// - `adapter_binary`: The debug adapter binary to start -/// - `cx`: The context that the new client belongs too -pub async fn create_tcp_client( - host: TCPHost, - adapter_binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, -) -> Result { - let host_address = host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); - - let mut port = host.port; - if port.is_none() { - port = get_open_port(host_address).await; - } - - let mut command = process::Command::new(&adapter_binary.command); - - if let Some(args) = &adapter_binary.arguments { - command.args(args); - } - - if let Some(envs) = &adapter_binary.envs { - command.envs(envs); - } - - command - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .kill_on_drop(true); - - let process = command - .spawn() - .with_context(|| "failed to start debug adapter.")?; - - if let Some(delay) = host.delay { - // some debug adapters need some time to start the TCP server - // so we have to wait few milliseconds before we can connect to it - cx.background_executor() - .timer(Duration::from_millis(delay)) - .await; - } - - let address = SocketAddrV4::new( - host_address, - port.ok_or(anyhow!("Port is required to connect to TCP server"))?, - ); - - let (rx, tx) = TcpStream::connect(address).await?.split(); - log::info!("Debug adapter has connected to tcp server"); - - Ok(TransportParams::new( - Box::new(BufReader::new(rx)), - Box::new(tx), - None, - Some(process), - )) -} - -/// Creates a debug client that connects to an adapter through std input/output -/// -/// # Parameters -/// - `adapter_binary`: The debug adapter binary to start -pub fn create_stdio_client(adapter_binary: &DebugAdapterBinary) -> Result { - let mut command = process::Command::new(&adapter_binary.command); - - if let Some(args) = &adapter_binary.arguments { - command.args(args); - } - - if let Some(envs) = &adapter_binary.envs { - command.envs(envs); - } - - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .kill_on_drop(true); - - let mut process = command - .spawn() - .with_context(|| "failed to spawn command.")?; - - let stdin = process - .stdin - .take() - .ok_or_else(|| anyhow!("Failed to open stdin"))?; - let stdout = process - .stdout - .take() - .ok_or_else(|| anyhow!("Failed to open stdout"))?; - let stderr = process - .stderr - .take() - .ok_or_else(|| anyhow!("Failed to open stderr"))?; - - log::info!("Debug adapter has connected to stdio adapter"); - - Ok(TransportParams::new( - Box::new(BufReader::new(stdout)), - Box::new(stdin), - Some(Box::new(BufReader::new(stderr))), - Some(process), - )) -} - pub struct DebugAdapterName(pub Arc); impl AsRef for DebugAdapterName { @@ -176,17 +38,9 @@ pub struct DebugAdapterBinary { #[async_trait(?Send)] pub trait DebugAdapter: 'static + Send + Sync { - fn id(&self) -> String { - "".to_string() - } - fn name(&self) -> DebugAdapterName; - async fn connect( - &self, - adapter_binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, - ) -> anyhow::Result; + fn transport(&self) -> Box; /// Installs the binary for the debug adapter. /// This method is called when the adapter binary is not found or needs to be updated. diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index e484d1516179fe..71343f1a44f7fb 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,17 +1,15 @@ -use crate::transport::Transport; +use crate::{ + adapters::{DebugAdapter, DebugAdapterBinary}, + transport::TransportDelegate, +}; use anyhow::{anyhow, Result}; use dap_types::{ messages::{Message, Response}, requests::Request, }; -use futures::{AsyncBufRead, AsyncWrite}; use gpui::{AppContext, AsyncAppContext}; -use parking_lot::Mutex; use serde_json::Value; -use smol::{ - channel::{bounded, Receiver, Sender}, - process::Child, -}; +use smol::channel::{bounded, Receiver, Sender}; use std::{ hash::Hash, sync::{ @@ -38,89 +36,49 @@ pub struct DebugAdapterClient { id: DebugAdapterClientId, adapter_id: String, request_args: Value, - transport: Arc, - _process: Arc>>, sequence_count: AtomicU64, config: DebugAdapterConfig, -} - -pub struct TransportParams { - rx: Box, - tx: Box, - err: Option>, - process: Option, -} - -impl TransportParams { - pub fn new( - rx: Box, - tx: Box, - err: Option>, - process: Option, - ) -> Self { - TransportParams { - rx, - tx, - err, - process, - } - } + transport_delegate: TransportDelegate, } impl DebugAdapterClient { - pub async fn new( + pub fn new( id: DebugAdapterClientId, - adapter_id: String, request_args: Value, config: DebugAdapterConfig, - transport_params: TransportParams, - event_handler: F, - cx: &mut AsyncAppContext, - ) -> Result> - where - F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, - { - let transport = Self::handle_transport( - transport_params.rx, - transport_params.tx, - transport_params.err, - event_handler, - cx, - ); - Ok(Arc::new(Self { + adapter: Arc>, + ) -> Self { + Self { id, - adapter_id, - request_args, config, - transport, + request_args, sequence_count: AtomicU64::new(1), - _process: Arc::new(Mutex::new(transport_params.process)), - })) + adapter_id: adapter.name().to_string(), + transport_delegate: TransportDelegate::new(adapter.transport()), + } } - pub fn handle_transport( - rx: Box, - tx: Box, - err: Option>, - event_handler: F, + pub async fn start( + &mut self, + binary: &DebugAdapterBinary, + message_handler: F, cx: &mut AsyncAppContext, - ) -> Arc + ) -> Result<()> where F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { - let transport = Transport::start(rx, tx, err, cx); + let (server_rx, server_tx) = self.transport_delegate.start(binary, cx).await?; - let server_rx = transport.server_rx.clone(); - let server_tr = transport.server_tx.clone(); + // start handling events/reverse requests cx.spawn(|mut cx| async move { - Self::handle_recv(server_rx, server_tr, event_handler, &mut cx).await + Self::handle_receive_messages(server_rx, server_tx, message_handler, &mut cx).await }) .detach(); - transport + Ok(()) } - async fn handle_recv( + async fn handle_receive_messages( server_rx: Receiver, client_tx: Sender, mut event_handler: F, @@ -159,13 +117,11 @@ impl DebugAdapterClient { arguments: Some(serialized_arguments), }; - self.transport - .current_requests - .lock() - .await - .insert(sequence_id, callback_tx); + self.transport_delegate + .add_pending_request(sequence_id, callback_tx) + .await; - self.respond(Message::Request(request)).await?; + self.send_message(Message::Request(request)).await?; let response = callback_rx.recv().await??; @@ -175,12 +131,8 @@ impl DebugAdapterClient { } } - pub async fn respond(&self, message: Message) -> Result<()> { - self.transport - .server_tx - .send(message) - .await - .map_err(|e| anyhow::anyhow!("Failed to send response back: {}", e)) + pub async fn send_message(&self, message: Message) -> Result<()> { + self.transport_delegate.send_message(message).await } pub fn id(&self) -> DebugAdapterClientId { @@ -209,28 +161,6 @@ impl DebugAdapterClient { } pub async fn shutdown(&self) -> Result<()> { - self.transport.server_tx.close(); - self.transport.server_rx.close(); - - let mut adapter = self._process.lock().take(); - - async move { - let mut current_requests = self.transport.current_requests.lock().await; - let mut pending_requests = self.transport.pending_requests.lock().await; - - current_requests.clear(); - pending_requests.clear(); - - if let Some(mut adapter) = adapter.take() { - adapter.kill()?; - } - - drop(current_requests); - drop(pending_requests); - drop(adapter); - - anyhow::Ok(()) - } - .await + self.transport_delegate.shutdown().await } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 5a7dbbd4ff3135..7849870471d53e 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -1,68 +1,215 @@ use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; use dap_types::{ messages::{Message, Response}, ErrorResponse, }; -use futures::{AsyncBufRead, AsyncWrite}; +use futures::{select, AsyncBufRead, AsyncReadExt as _, AsyncWrite, FutureExt as _}; use gpui::AsyncAppContext; use smol::{ channel::{unbounded, Receiver, Sender}, - io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt}, + io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, lock::Mutex, + net::{TcpListener, TcpStream}, + process::{self, Child}, }; -use std::{collections::HashMap, sync::Arc}; - -#[derive(Debug)] -pub struct Transport { - pub server_tx: Sender, - pub server_rx: Receiver, - pub current_requests: Arc>>>>, - pub pending_requests: Arc>>>>, +use std::{ + borrow::BorrowMut, + collections::HashMap, + net::{Ipv4Addr, SocketAddrV4}, + process::Stdio, + sync::Arc, + time::Duration, +}; +use task::TCPHost; + +use crate::adapters::DebugAdapterBinary; + +pub struct TransportParams { + input: Box, + output: Box, + error: Box, + process: Child, +} + +impl TransportParams { + pub fn new( + input: Box, + output: Box, + error: Box, + process: Child, + ) -> Self { + TransportParams { + input, + output, + error, + process, + } + } +} + +type Requests = Arc>>>>; + +pub(crate) struct TransportDelegate { + current_requests: Requests, + pending_requests: Requests, + transport: Box, + process: Arc>>, + server_tx: Option>, } -impl Transport { - pub fn start( - server_stdout: Box, - server_stdin: Box, - server_stderr: Option>, +impl TransportDelegate { + pub fn new(transport: Box) -> Self { + Self { + transport, + server_tx: None, + process: Default::default(), + current_requests: Default::default(), + pending_requests: Default::default(), + } + } + + pub(crate) async fn start( + &mut self, + binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Arc { + ) -> Result<(Receiver, Sender)> { + let params = self.transport.start(binary, cx).await?; + let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); - let current_requests = Arc::new(Mutex::new(HashMap::default())); - let pending_requests = Arc::new(Mutex::new(HashMap::default())); + self.process = Arc::new(Mutex::new(Some(params.process))); + self.server_tx = Some(server_tx.clone()); cx.background_executor() - .spawn(Self::receive( - pending_requests.clone(), - server_stdout, + .spawn(Self::handle_output( + params.output, client_tx, + self.pending_requests.clone(), )) .detach(); - if let Some(stderr) = server_stderr { - cx.background_executor().spawn(Self::err(stderr)).detach(); - } + cx.background_executor() + .spawn(Self::handle_error(params.error)) + .detach(); cx.background_executor() - .spawn(Self::send( - current_requests.clone(), - pending_requests.clone(), - server_stdin, + .spawn(Self::handle_input( + params.input, client_rx, + self.current_requests.clone(), + self.pending_requests.clone(), )) .detach(); - Arc::new(Self { - server_rx, - server_tx, - current_requests, - pending_requests, - }) + Ok((server_rx, server_tx)) + } + + pub(crate) async fn add_pending_request( + &self, + sequence_id: u64, + request: Sender>, + ) { + let mut pending_requests = self.pending_requests.lock().await; + pending_requests.insert(sequence_id, request); } - async fn recv_server_message( + pub(crate) async fn send_message(&self, message: Message) -> Result<()> { + if let Some(server_tx) = self.server_tx.as_ref() { + server_tx + .send(message) + .await + .map_err(|e| anyhow!("Failed to send response back: {}", e)) + } else { + Err(anyhow!("Server tx already dropped")) + } + } + + async fn handle_input( + mut server_stdin: Box, + client_rx: Receiver, + current_requests: Requests, + pending_requests: Requests, + ) -> Result<()> { + while let Ok(mut payload) = client_rx.recv().await { + if let Message::Request(request) = payload.borrow_mut() { + if let Some(sender) = current_requests.lock().await.remove(&request.seq) { + pending_requests.lock().await.insert(request.seq, sender); + } + } + + let message = serde_json::to_string(&payload)?; + + server_stdin + .write_all( + format!("Content-Length: {}\r\n\r\n{}", message.len(), message).as_bytes(), + ) + .await?; + + server_stdin.flush().await?; + } + + Ok(()) + } + + async fn handle_output( + mut server_stdout: Box, + client_tx: Sender, + pending_requests: Requests, + ) -> Result<()> { + let mut recv_buffer = String::new(); + + while let Ok(message) = + Self::receive_server_message(&mut server_stdout, &mut recv_buffer).await + { + match message { + Message::Response(res) => { + if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) { + tx.send(Self::process_response(res)).await?; + } else { + client_tx.send(Message::Response(res)).await?; + }; + } + Message::Request(_) => { + client_tx.send(message).await?; + } + Message::Event(_) => { + client_tx.send(message).await?; + } + } + } + + Ok(()) + } + + async fn handle_error(mut stderr: Box) -> Result<()> { + let mut buffer = String::new(); + loop { + buffer.truncate(0); + if stderr.read_line(&mut buffer).await? == 0 { + return Err(anyhow!("debugger error stream closed")); + } + } + } + + fn process_response(response: Response) -> Result { + if response.success { + Ok(response) + } else { + if let Some(body) = response.body { + if let Ok(error) = serde_json::from_value::(body) { + if let Some(message) = error.error { + return Err(anyhow!(message.format)); + }; + }; + } + + Err(anyhow!("Received error response from adapter")) + } + } + + async fn receive_server_message( reader: &mut Box, buffer: &mut String, ) -> Result { @@ -105,122 +252,191 @@ impl Transport { Ok(serde_json::from_str::(msg)?) } - async fn recv_server_error( - err: &mut (impl AsyncBufRead + Unpin + Send), - buffer: &mut String, - ) -> Result<()> { - buffer.truncate(0); - if err.read_line(buffer).await? == 0 { - return Err(anyhow!("debugger error stream closed")); - }; + pub async fn shutdown(&self) -> Result<()> { + if let Some(server_tx) = self.server_tx.as_ref() { + server_tx.close(); + } - Ok(()) - } + let mut adapter = self.process.lock().await.take(); + let mut current_requests = self.current_requests.lock().await; + let mut pending_requests = self.pending_requests.lock().await; - async fn send_payload_to_server( - current_requests: &Mutex>>>, - pending_requests: &Mutex>>>, - server_stdin: &mut Box, - mut payload: Message, - ) -> Result<()> { - if let Message::Request(request) = &mut payload { - if let Some(sender) = current_requests.lock().await.remove(&request.seq) { - pending_requests.lock().await.insert(request.seq, sender); - } + current_requests.clear(); + pending_requests.clear(); + + if let Some(mut adapter) = adapter.take() { + adapter.kill()?; } - Self::send_string_to_server(server_stdin, serde_json::to_string(&payload)?).await - } - async fn send_string_to_server( - server_stdin: &mut Box, - request: String, - ) -> Result<()> { - server_stdin - .write_all(format!("Content-Length: {}\r\n\r\n{}", request.len(), request).as_bytes()) - .await?; + drop(current_requests); + drop(pending_requests); + drop(adapter); - server_stdin.flush().await?; - Ok(()) + anyhow::Ok(()) } +} - fn process_response(response: Response) -> Result { - if response.success { - Ok(response) - } else { - if let Some(body) = response.body { - if let Ok(error) = serde_json::from_value::(body) { - if let Some(message) = error.error { - return Err(anyhow!(message.format)); - }; - }; - } +#[async_trait(?Send)] +pub trait Transport: 'static + Send + Sync { + async fn start( + &mut self, + binary: &DebugAdapterBinary, + cx: &mut AsyncAppContext, + ) -> Result; +} - Err(anyhow!("Received error response from adapter")) - } +pub struct TcpTransport { + config: TCPHost, +} + +impl TcpTransport { + pub fn new(config: TCPHost) -> Self { + Self { config } } - async fn process_server_message( - pending_requests: &Arc>>>>, - client_tx: &Sender, - message: Message, - ) -> Result<()> { - match message { - Message::Response(res) => { - if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) { - tx.send(Self::process_response(res)).await?; - } else { - client_tx.send(Message::Response(res)).await?; - }; - } - Message::Request(_) => { - client_tx.send(message).await?; - } - Message::Event(_) => { - client_tx.send(message).await?; - } - } - Ok(()) + /// Get an open port to use with the tcp client when not supplied by debug config + async fn get_open_port(host: Ipv4Addr) -> Option { + Some( + TcpListener::bind(SocketAddrV4::new(host, 0)) + .await + .ok()? + .local_addr() + .ok()? + .port(), + ) } +} - async fn receive( - pending_requests: Arc>>>>, - mut server_stdout: Box, - client_tx: Sender, - ) -> Result<()> { - let mut recv_buffer = String::new(); +#[async_trait(?Send)] +impl Transport for TcpTransport { + async fn start( + &mut self, + binary: &DebugAdapterBinary, + cx: &mut AsyncAppContext, + ) -> Result { + let host_address = self + .config + .host + .unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); - while let Ok(msg) = Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await { - Self::process_server_message(&pending_requests, &client_tx, msg) - .await - .context("Process server message failed in transport::receive")?; + let mut port = self.config.port; + if port.is_none() { + port = Self::get_open_port(host_address).await; } - Ok(()) - } + let mut command = process::Command::new(&binary.command); - async fn send( - current_requests: Arc>>>>, - pending_requests: Arc>>>>, - mut server_stdin: Box, - client_rx: Receiver, - ) -> Result<()> { - while let Ok(payload) = client_rx.recv().await { - Self::send_payload_to_server( - ¤t_requests, - &pending_requests, - &mut server_stdin, - payload, - ) - .await?; + if let Some(args) = &binary.arguments { + command.args(args); } - Ok(()) + if let Some(envs) = &binary.envs { + command.envs(envs); + } + + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .kill_on_drop(true); + + let mut process = command + .spawn() + .with_context(|| "failed to start debug adapter.")?; + + let stderr = process + .stderr + .take() + .ok_or_else(|| anyhow!("Failed to open stderr"))?; + + let address = SocketAddrV4::new( + host_address, + port.ok_or(anyhow!("Port is required to connect to TCP server"))?, + ); + + let timeout = self.config.timeout.unwrap_or(2000); + + let (rx, tx) = select! { + _ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => { + return Err(anyhow!("Connection to tcp DAP timeout")) + }, + result = cx.spawn(|cx| async move { + loop { + match TcpStream::connect(address).await { + Ok(stream) => return stream.split(), + Err(_) => { + cx.background_executor().timer(Duration::from_millis(100)).await; + } + } + } + }).fuse() => result + }; + log::info!("Debug adapter has connected to tcp server"); + + Ok(TransportParams::new( + Box::new(tx), + Box::new(BufReader::new(rx)), + Box::new(BufReader::new(stderr)), + process, + )) } +} - async fn err(mut server_stderr: Box) -> Result<()> { - let mut recv_buffer = String::new(); - loop { - Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await?; +pub struct StdioTransport {} + +impl StdioTransport { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait(?Send)] +impl Transport for StdioTransport { + async fn start( + &mut self, + binary: &DebugAdapterBinary, + _: &mut AsyncAppContext, + ) -> Result { + let mut command = process::Command::new(&binary.command); + + if let Some(args) = &binary.arguments { + command.args(args); } + + if let Some(envs) = &binary.envs { + command.envs(envs); + } + + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true); + + let mut process = command + .spawn() + .with_context(|| "failed to spawn command.")?; + + let stdin = process + .stdin + .take() + .ok_or_else(|| anyhow!("Failed to open stdin"))?; + let stdout = process + .stdout + .take() + .ok_or_else(|| anyhow!("Failed to open stdout"))?; + let stderr = process + .stderr + .take() + .ok_or_else(|| anyhow!("Failed to open stderr"))?; + + log::info!("Debug adapter has connected to stdio adapter"); + + Ok(TransportParams::new( + Box::new(stdin), + Box::new(BufReader::new(stdout)), + Box::new(BufReader::new(stderr)), + process, + )) } } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index a33e1802839b5b..2ad9cd97500743 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,5 +1,6 @@ use std::ffi::OsString; +use dap::transport::{StdioTransport, TcpTransport, Transport}; use serde_json::Value; use task::DebugAdapterConfig; @@ -24,16 +25,10 @@ impl DebugAdapter for CustomDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - async fn connect( - &self, - adapter_binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, - ) -> Result { + fn transport(&self) -> Box { match &self.custom_args.connection { - DebugConnectionType::STDIO => create_stdio_client(adapter_binary), - DebugConnectionType::TCP(tcp_host) => { - create_tcp_client(tcp_host.clone(), adapter_binary, cx).await - } + DebugConnectionType::STDIO => Box::new(StdioTransport::new()), + DebugConnectionType::TCP(tcp_host) => Box::new(TcpTransport::new(tcp_host.clone())), } } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index fac4f3c77c7377..1116cd1da89f69 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -12,14 +12,7 @@ use python::PythonDebugAdapter; use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; -use dap::{ - adapters::{ - create_stdio_client, create_tcp_client, DapDelegate, DebugAdapter, DebugAdapterBinary, - DebugAdapterName, - }, - client::TransportParams, -}; -use gpui::AsyncAppContext; +use dap::adapters::{DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName}; use http_client::github::latest_github_release; use serde_json::{json, Value}; use smol::{ diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 185a785a6ff4cb..4fafc9b85f7a5b 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,3 +1,5 @@ +use dap::transport::{TcpTransport, Transport}; + use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] @@ -18,18 +20,12 @@ impl DebugAdapter for JsDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - async fn connect( - &self, - adapter_binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, - ) -> Result { - let host = TCPHost { + fn transport(&self) -> Box { + Box::new(TcpTransport::new(TCPHost { port: Some(8133), host: None, - delay: Some(1000), - }; - - create_tcp_client(host, adapter_binary, cx).await + timeout: None, + })) } async fn fetch_binary( diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 56cda6750a2752..4d8e5e17c1e4b1 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -1,5 +1,6 @@ use anyhow::Result; use async_trait::async_trait; +use dap::transport::{StdioTransport, Transport}; use task::DebugAdapterConfig; use crate::*; @@ -21,12 +22,8 @@ impl DebugAdapter for LldbDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - async fn connect( - &self, - adapter_binary: &DebugAdapterBinary, - _: &mut AsyncAppContext, - ) -> Result { - create_stdio_client(adapter_binary) + fn transport(&self) -> Box { + Box::new(StdioTransport::new()) } async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 62ecb16506bb82..9e5c647eab20b3 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,3 +1,5 @@ +use dap::transport::{TcpTransport, Transport}; + use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] @@ -18,18 +20,12 @@ impl DebugAdapter for PhpDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - async fn connect( - &self, - adapter_binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, - ) -> Result { - let host = TCPHost { + fn transport(&self) -> Box { + Box::new(TcpTransport::new(TCPHost { port: Some(8132), host: None, - delay: Some(1000), - }; - - create_tcp_client(host, adapter_binary, cx).await + timeout: None, + })) } async fn fetch_binary( diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 12b16280f0de08..da96bb0dd0fa15 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,3 +1,5 @@ +use dap::transport::{StdioTransport, Transport}; + use crate::*; #[derive(Debug, Eq, PartialEq, Clone)] @@ -18,12 +20,8 @@ impl DebugAdapter for PythonDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - async fn connect( - &self, - adapter_binary: &DebugAdapterBinary, - _: &mut AsyncAppContext, - ) -> Result { - create_stdio_client(adapter_binary) + fn transport(&self) -> Box { + Box::new(StdioTransport::new()) } async fn fetch_binary( diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 12715a96644cec..40a242a83d4ec1 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -273,25 +273,24 @@ impl DapStore { merge_json_value_into(args.configuration, &mut request_args); } - let transport_params = adapter.connect(&binary, &mut cx).await.log_err()?; - - let client = DebugAdapterClient::new( - client_id, - adapter.id(), - request_args, - config, - transport_params, - move |message, cx| { - dap_store - .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) - }) - .log_err(); - }, - &mut cx, - ) - .await - .log_err()?; + let mut client = DebugAdapterClient::new(client_id, request_args, config, adapter); + + client + .start( + &binary, + move |message, cx| { + dap_store + .update(cx, |_, cx| { + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + }) + .log_err(); + }, + &mut cx, + ) + .await + .log_err()?; + + let client = Arc::new(client); this.update(&mut cx, |store, cx| { let handle = store @@ -509,7 +508,7 @@ impl DapStore { cx.spawn(|this, mut cx| async move { client - .respond(Message::Response(Response { + .send_message(Message::Response(Response { seq, request_seq: seq, success: true, @@ -539,7 +538,7 @@ impl DapStore { cx.spawn(|_, _| async move { if success { client - .respond(Message::Response(Response { + .send_message(Message::Response(Response { seq, request_seq: seq, success: true, @@ -552,7 +551,7 @@ impl DapStore { .await } else { client - .respond(Message::Response(Response { + .send_message(Message::Response(Response { seq, request_seq: seq, success: false, diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 51e60d19755c19..6aef1745c2e3de 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -19,8 +19,8 @@ pub struct TCPHost { pub port: Option, /// The host that the debug adapter is listening too pub host: Option, - /// The delay in ms between starting and connecting to the debug adapter - pub delay: Option, + /// The max amount of time to connect to a tcp DAP before returning an error + pub timeout: Option, } /// Represents the type that will determine which request to call on the debug adapter From 30a4c84a4844c2773ff7d3420e8aba9c60050d35 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 20 Oct 2024 21:23:16 +0200 Subject: [PATCH 301/650] Remove unused dependency & sort --- Cargo.lock | 1 - crates/dap_adapters/Cargo.toml | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cda382d803d10..9f89190e0ec7d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3381,7 +3381,6 @@ dependencies = [ "dap", "fs", "futures 0.3.30", - "gpui", "http_client", "paths", "serde", diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index d6515c69b8a41c..f97e0435dc7900 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -19,10 +19,9 @@ async-trait.workspace = true dap.workspace = true fs.workspace = true futures.workspace = true -gpui.workspace = true http_client.workspace = true -task.workspace = true -smol.workspace = true +paths.workspace = true serde.workspace = true serde_json.workspace = true -paths.workspace = true +smol.workspace = true +task.workspace = true From 43ea3b47a468ff0b39d9c7c1b4f28b128ffdb494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20Butkovi=C4=87?= Date: Mon, 21 Oct 2024 19:44:35 +0200 Subject: [PATCH 302/650] Implement logging for debug adapter clients (#45) * Implement RPC logging for debug adapter clients * Implement server logs for debugger servers * This cleans up the way we pass through the input and output readers for logging. So not each debug adapters have to map the AdapterLogIO fields. I also removed some specific when has logs from the client, because the client is not responsible for that. Removed an not needed/duplicated dependency Fix formatting & clippy This cleans up the way we pass through the input and output readers for logging. So not each debug adapters have to map the AdapterLogIO fields. I also removed some specific when has logs from the client, because the client is not responsible for that. Removed an not needed/duplicated dependency Fix formatting & clippy * Implement `has_adapter_logs` for each transport impl * Make adapter stdout logging work * Add conditional render for adapter log back * Oops forgot to pipe the output * Always enable rpc messages Previously, RPC messages were only stored when explicitly enabled, which occurred after the client was already running. This approach prevented debugging of requests sent during the initial connection period. By always enabling RPC messages, we ensure that all requests, including those during the connection phase, are captured and available for debugging purposes. This could help use debug when someone has troble getting a debug starting. This improvement could be particularly helpful in debugging scenarios where users encounter issues during the initial connection or startup phase of their debugging sessions. --------- Co-authored-by: Remco Smits Co-authored-by: Anthony Eid --- Cargo.lock | 17 + Cargo.toml | 2 + crates/dap/Cargo.toml | 2 + crates/dap/src/adapters.rs | 1 - crates/dap/src/client.rs | 14 +- crates/dap/src/transport.rs | 132 +++- crates/debugger_tools/Cargo.toml | 23 + crates/debugger_tools/src/dap_log.rs | 766 ++++++++++++++++++++ crates/debugger_tools/src/debugger_tools.rs | 8 + crates/project/src/project.rs | 18 + crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 2 + 13 files changed, 957 insertions(+), 30 deletions(-) create mode 100644 crates/debugger_tools/Cargo.toml create mode 100644 crates/debugger_tools/src/dap_log.rs create mode 100644 crates/debugger_tools/src/debugger_tools.rs diff --git a/Cargo.lock b/Cargo.lock index 9f89190e0ec7d3..e83e965252d2c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3354,10 +3354,12 @@ dependencies = [ "http_client", "log", "node_runtime", + "parking_lot", "schemars", "serde", "serde_json", "settings", + "smallvec", "smol", "task", ] @@ -3462,6 +3464,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "debugger_tools" +version = "0.1.0" +dependencies = [ + "anyhow", + "dap", + "editor", + "futures 0.3.30", + "gpui", + "project", + "serde_json", + "workspace", +] + [[package]] name = "debugger_ui" version = "0.1.0" @@ -14690,6 +14706,7 @@ dependencies = [ "command_palette_hooks", "copilot", "db", + "debugger_tools", "debugger_ui", "dev_server_projects", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index 505cba7c3611d8..a2ca527352df1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,6 +172,7 @@ members = [ # "tooling/xtask", + "crates/debugger_tools", ] default-members = ["crates/zed"] @@ -207,6 +208,7 @@ dap = { path = "crates/dap" } dap_adapters = { path = "crates/dap_adapters" } db = { path = "crates/db" } debugger_ui = { path = "crates/debugger_ui" } +debugger_tools = { path = "crates/debugger_tools" } dev_server_projects = { path = "crates/dev_server_projects" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index c3e6571c4d63a2..bf5c574dc66c3b 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -18,9 +18,11 @@ gpui.workspace = true http_client.workspace = true log.workspace = true node_runtime.workspace = true +parking_lot.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +smallvec.workspace = true smol.workspace = true task.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index ca4662bac61669..119dd113d781ff 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -6,7 +6,6 @@ use http_client::HttpClient; use node_runtime::NodeRuntime; use serde_json::Value; use std::{collections::HashMap, ffi::OsString, path::Path, sync::Arc}; - use task::DebugAdapterConfig; pub trait DapDelegate { diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 71343f1a44f7fb..672fa17c817c6c 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,8 +1,9 @@ use crate::{ adapters::{DebugAdapter, DebugAdapterBinary}, - transport::TransportDelegate, + transport::{IoKind, LogKind, TransportDelegate}, }; use anyhow::{anyhow, Result}; + use dap_types::{ messages::{Message, Response}, requests::Request, @@ -163,4 +164,15 @@ impl DebugAdapterClient { pub async fn shutdown(&self) -> Result<()> { self.transport_delegate.shutdown().await } + + pub fn has_adapter_logs(&self) -> bool { + self.transport_delegate.has_adapter_logs() + } + + pub fn add_log_handler(&self, f: F, kind: LogKind) + where + F: 'static + Send + FnMut(IoKind, &str), + { + self.transport_delegate.add_log_handler(f, kind); + } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 7849870471d53e..1e767e2882cdca 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -6,12 +6,13 @@ use dap_types::{ }; use futures::{select, AsyncBufRead, AsyncReadExt as _, AsyncWrite, FutureExt as _}; use gpui::AsyncAppContext; +use smallvec::SmallVec; use smol::{ channel::{unbounded, Receiver, Sender}, io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, lock::Mutex, net::{TcpListener, TcpStream}, - process::{self, Child}, + process::{self, Child, ChildStderr, ChildStdout}, }; use std::{ borrow::BorrowMut, @@ -25,10 +26,23 @@ use task::TCPHost; use crate::adapters::DebugAdapterBinary; +pub type IoHandler = Box; + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum LogKind { + Adapter, + Rpc, +} + +pub enum IoKind { + StdIn, + StdOut, + StdErr, +} + pub struct TransportParams { input: Box, output: Box, - error: Box, process: Child, } @@ -36,21 +50,21 @@ impl TransportParams { pub fn new( input: Box, output: Box, - error: Box, process: Child, ) -> Self { TransportParams { input, output, - error, process, } } } type Requests = Arc>>>>; +type LogHandlers = Arc>>; pub(crate) struct TransportDelegate { + log_handlers: LogHandlers, current_requests: Requests, pending_requests: Requests, transport: Box, @@ -64,6 +78,7 @@ impl TransportDelegate { transport, server_tx: None, process: Default::default(), + log_handlers: Default::default(), current_requests: Default::default(), pending_requests: Default::default(), } @@ -74,25 +89,31 @@ impl TransportDelegate { binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result<(Receiver, Sender)> { - let params = self.transport.start(binary, cx).await?; + let mut params = self.transport.start(binary, cx).await?; let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); - self.process = Arc::new(Mutex::new(Some(params.process))); - self.server_tx = Some(server_tx.clone()); + if let Some(stdout) = params.process.stdout.take() { + cx.background_executor() + .spawn(Self::handle_adapter_log(stdout, self.log_handlers.clone())) + .detach(); + } cx.background_executor() .spawn(Self::handle_output( params.output, client_tx, self.pending_requests.clone(), + self.log_handlers.clone(), )) .detach(); - cx.background_executor() - .spawn(Self::handle_error(params.error)) - .detach(); + if let Some(stderr) = params.process.stderr.take() { + cx.background_executor() + .spawn(Self::handle_error(stderr, self.log_handlers.clone())) + .detach(); + } cx.background_executor() .spawn(Self::handle_input( @@ -100,9 +121,13 @@ impl TransportDelegate { client_rx, self.current_requests.clone(), self.pending_requests.clone(), + self.log_handlers.clone(), )) .detach(); + self.process = Arc::new(Mutex::new(Some(params.process))); + self.server_tx = Some(server_tx.clone()); + Ok((server_rx, server_tx)) } @@ -126,11 +151,27 @@ impl TransportDelegate { } } + async fn handle_adapter_log(stdout: ChildStdout, log_handlers: LogHandlers) -> Result<()> { + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + while reader.read_line(&mut line).await? > 0 { + for (kind, handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Adapter) { + handler(IoKind::StdOut, line.as_str()); + } + } + line.truncate(0); + } + + Ok(()) + } + async fn handle_input( mut server_stdin: Box, client_rx: Receiver, current_requests: Requests, pending_requests: Requests, + log_handlers: LogHandlers, ) -> Result<()> { while let Ok(mut payload) = client_rx.recv().await { if let Message::Request(request) = payload.borrow_mut() { @@ -141,6 +182,12 @@ impl TransportDelegate { let message = serde_json::to_string(&payload)?; + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdIn, &message); + } + } + server_stdin .write_all( format!("Content-Length: {}\r\n\r\n{}", message.len(), message).as_bytes(), @@ -157,11 +204,12 @@ impl TransportDelegate { mut server_stdout: Box, client_tx: Sender, pending_requests: Requests, + log_handlers: LogHandlers, ) -> Result<()> { let mut recv_buffer = String::new(); while let Ok(message) = - Self::receive_server_message(&mut server_stdout, &mut recv_buffer).await + Self::receive_server_message(&mut server_stdout, &mut recv_buffer, &log_handlers).await { match message { Message::Response(res) => { @@ -183,13 +231,22 @@ impl TransportDelegate { Ok(()) } - async fn handle_error(mut stderr: Box) -> Result<()> { + async fn handle_error(stderr: ChildStderr, log_handlers: LogHandlers) -> Result<()> { let mut buffer = String::new(); + + let mut reader = BufReader::new(stderr); + loop { buffer.truncate(0); - if stderr.read_line(&mut buffer).await? == 0 { + if reader.read_line(&mut buffer).await? == 0 { return Err(anyhow!("debugger error stream closed")); } + + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdErr, buffer.as_str()); + } + } } } @@ -212,6 +269,7 @@ impl TransportDelegate { async fn receive_server_message( reader: &mut Box, buffer: &mut String, + log_handlers: &LogHandlers, ) -> Result { let mut content_length = None; loop { @@ -248,8 +306,15 @@ impl TransportDelegate { .await .with_context(|| "reading after a loop")?; - let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; - Ok(serde_json::from_str::(msg)?) + let message = std::str::from_utf8(&content).context("invalid utf8 from server")?; + + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdOut, &message); + } + } + + Ok(serde_json::from_str::(message)?) } pub async fn shutdown(&self) -> Result<()> { @@ -274,6 +339,18 @@ impl TransportDelegate { anyhow::Ok(()) } + + pub fn has_adapter_logs(&self) -> bool { + self.transport.has_adapter_logs() + } + + pub fn add_log_handler(&self, f: F, kind: LogKind) + where + F: 'static + Send + FnMut(IoKind, &str), + { + let mut log_handlers = self.log_handlers.lock(); + log_handlers.push((kind, Box::new(f))); + } } #[async_trait(?Send)] @@ -283,6 +360,8 @@ pub trait Transport: 'static + Send + Sync { binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result; + + fn has_adapter_logs(&self) -> bool; } pub struct TcpTransport { @@ -336,19 +415,14 @@ impl Transport for TcpTransport { command .stdin(Stdio::null()) - .stdout(Stdio::null()) + .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); - let mut process = command + let process = command .spawn() .with_context(|| "failed to start debug adapter.")?; - let stderr = process - .stderr - .take() - .ok_or_else(|| anyhow!("Failed to open stderr"))?; - let address = SocketAddrV4::new( host_address, port.ok_or(anyhow!("Port is required to connect to TCP server"))?, @@ -376,10 +450,13 @@ impl Transport for TcpTransport { Ok(TransportParams::new( Box::new(tx), Box::new(BufReader::new(rx)), - Box::new(BufReader::new(stderr)), process, )) } + + fn has_adapter_logs(&self) -> bool { + true + } } pub struct StdioTransport {} @@ -425,18 +502,17 @@ impl Transport for StdioTransport { .stdout .take() .ok_or_else(|| anyhow!("Failed to open stdout"))?; - let stderr = process - .stderr - .take() - .ok_or_else(|| anyhow!("Failed to open stderr"))?; log::info!("Debug adapter has connected to stdio adapter"); Ok(TransportParams::new( Box::new(stdin), Box::new(BufReader::new(stdout)), - Box::new(BufReader::new(stderr)), process, )) } + + fn has_adapter_logs(&self) -> bool { + false + } } diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml new file mode 100644 index 00000000000000..bdcfc6bc3d23e5 --- /dev/null +++ b/crates/debugger_tools/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "debugger_tools" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/debugger_tools.rs" +doctest = false + +[dependencies] +gpui.workspace = true +workspace.workspace = true +editor.workspace = true +project.workspace = true +dap.workspace = true +serde_json.workspace = true +futures.workspace = true +anyhow.workspace = true diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs new file mode 100644 index 00000000000000..6a61edf1ad2620 --- /dev/null +++ b/crates/debugger_tools/src/dap_log.rs @@ -0,0 +1,766 @@ +use dap::{ + client::{DebugAdapterClient, DebugAdapterClientId}, + transport::{IoKind, LogKind}, +}; +use editor::{Editor, EditorEvent}; +use futures::{ + channel::mpsc::{unbounded, UnboundedSender}, + StreamExt, +}; +use gpui::{ + actions, div, AnchorCorner, AppContext, Context, EventEmitter, FocusHandle, FocusableView, + IntoElement, Model, ModelContext, ParentElement, Render, SharedString, Styled, Subscription, + View, ViewContext, VisualContext, WeakModel, WindowContext, +}; +use project::{search::SearchQuery, Project}; +use std::{ + borrow::Cow, + collections::{HashMap, VecDeque}, + sync::Arc, +}; +use workspace::{ + item::Item, + searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, + ui::{h_flex, Button, Clickable, ContextMenu, Label, PopoverMenu}, + ToolbarItemEvent, ToolbarItemView, Workspace, +}; + +struct DapLogView { + editor: View, + focus_handle: FocusHandle, + log_store: Model, + editor_subscriptions: Vec, + current_view: Option<(DebugAdapterClientId, LogKind)>, + project: Model, + _subscriptions: Vec, +} + +struct LogStore { + projects: HashMap, ProjectState>, + debug_clients: HashMap, + rpc_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, + adapter_log_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, +} + +struct ProjectState { + _subscriptions: [gpui::Subscription; 2], +} + +struct DebugAdapterState { + log_messages: VecDeque, + rpc_messages: RpcMessages, +} + +struct RpcMessages { + messages: VecDeque, + last_message_kind: Option, +} + +impl RpcMessages { + const MESSAGE_QUEUE_LIMIT: usize = 255; + + fn new() -> Self { + Self { + last_message_kind: None, + messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT), + } + } +} + +const SEND: &str = "// Send"; +const RECEIVE: &str = "// Receive"; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum MessageKind { + Send, + Receive, +} + +impl MessageKind { + fn label(&self) -> &'static str { + match self { + Self::Send => SEND, + Self::Receive => RECEIVE, + } + } +} + +impl DebugAdapterState { + fn new() -> Self { + Self { + log_messages: VecDeque::new(), + rpc_messages: RpcMessages::new(), + } + } +} + +impl LogStore { + fn new(cx: &ModelContext) -> Self { + let (rpc_tx, mut rpc_rx) = unbounded::<(DebugAdapterClientId, IoKind, String)>(); + cx.spawn(|this, mut cx| async move { + while let Some((server_id, io_kind, message)) = rpc_rx.next().await { + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.on_rpc_log(server_id, io_kind, &message, cx); + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + let (adapter_log_tx, mut adapter_log_rx) = + unbounded::<(DebugAdapterClientId, IoKind, String)>(); + cx.spawn(|this, mut cx| async move { + while let Some((server_id, io_kind, message)) = adapter_log_rx.next().await { + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.on_adapter_log(server_id, io_kind, &message, cx); + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + Self { + rpc_tx, + adapter_log_tx, + projects: HashMap::new(), + debug_clients: HashMap::new(), + } + } + + fn on_rpc_log( + &mut self, + client_id: DebugAdapterClientId, + io_kind: IoKind, + message: &str, + cx: &mut ModelContext, + ) -> Option<()> { + self.add_debug_client_message(client_id, io_kind, message.to_string(), cx); + + Some(()) + } + + fn on_adapter_log( + &mut self, + client_id: DebugAdapterClientId, + io_kind: IoKind, + message: &str, + cx: &mut ModelContext, + ) -> Option<()> { + self.add_debug_client_log(client_id, io_kind, message.to_string(), cx); + + Some(()) + } + + pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { + let weak_project = project.downgrade(); + self.projects.insert( + project.downgrade(), + ProjectState { + _subscriptions: [ + cx.observe_release(project, move |this, _, _| { + this.projects.remove(&weak_project); + }), + cx.subscribe(project, |this, project, event, cx| match event { + project::Event::DebugClientStarted(client_id) => { + this.add_debug_client( + *client_id, + project.read(cx).debug_client_for_id(client_id, cx), + ); + } + project::Event::DebugClientStopped(id) => { + this.remove_debug_client(*id, cx); + } + + _ => {} + }), + ], + }, + ); + } + + fn get_debug_adapter_state( + &mut self, + id: DebugAdapterClientId, + ) -> Option<&mut DebugAdapterState> { + self.debug_clients.get_mut(&id) + } + + fn add_debug_client_message( + &mut self, + id: DebugAdapterClientId, + io_kind: IoKind, + message: String, + cx: &mut ModelContext, + ) { + let Some(debug_client_state) = self.get_debug_adapter_state(id) else { + return; + }; + + let kind = match io_kind { + IoKind::StdOut | IoKind::StdErr => MessageKind::Receive, + IoKind::StdIn => MessageKind::Send, + }; + + let rpc_messages = &mut debug_client_state.rpc_messages; + if rpc_messages.last_message_kind != Some(kind) { + Self::add_debug_client_entry( + &mut rpc_messages.messages, + id, + kind.label().to_string(), + LogKind::Rpc, + cx, + ); + rpc_messages.last_message_kind = Some(kind); + } + Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); + } + + fn add_debug_client_log( + &mut self, + id: DebugAdapterClientId, + io_kind: IoKind, + message: String, + cx: &mut ModelContext, + ) { + let Some(debug_client_state) = self.get_debug_adapter_state(id) else { + return; + }; + + let mut log_messages = &mut debug_client_state.log_messages; + + let message = match io_kind { + IoKind::StdErr => { + let mut message = message.clone(); + message.insert_str(0, "stderr: "); + message + } + _ => message, + }; + + Self::add_debug_client_entry(&mut log_messages, id, message, LogKind::Adapter, cx); + } + + fn add_debug_client_entry( + log_lines: &mut VecDeque, + id: DebugAdapterClientId, + message: String, + kind: LogKind, + cx: &mut ModelContext, + ) { + while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { + log_lines.pop_front(); + } + let entry: &str = message.as_ref(); + let entry = entry.to_string(); + log_lines.push_back(message); + + cx.emit(Event::NewLogEntry { id, entry, kind }); + cx.notify(); + } + + fn add_debug_client( + &mut self, + client_id: DebugAdapterClientId, + client: Option>, + ) -> Option<&mut DebugAdapterState> { + let client_state = self + .debug_clients + .entry(client_id) + .or_insert_with(DebugAdapterState::new); + + if let Some(client) = client { + let io_tx = self.rpc_tx.clone(); + + client.add_log_handler( + move |io_kind, message| { + io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }, + LogKind::Rpc, + ); + + let log_io_tx = self.adapter_log_tx.clone(); + client.add_log_handler( + move |io_kind, message| { + log_io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }, + LogKind::Adapter, + ); + } + + Some(client_state) + } + + fn remove_debug_client(&mut self, id: DebugAdapterClientId, cx: &mut ModelContext) { + self.debug_clients.remove(&id); + cx.notify(); + } + + fn log_messages_for_client( + &mut self, + client_id: DebugAdapterClientId, + ) -> Option<&mut VecDeque> { + Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) + } + + fn rpc_messages_for_client( + &mut self, + client_id: DebugAdapterClientId, + ) -> Option<&mut VecDeque> { + Some( + &mut self + .debug_clients + .get_mut(&client_id)? + .rpc_messages + .messages, + ) + } +} + +pub struct DapLogToolbarItemView { + log_view: Option>, +} + +impl DapLogToolbarItemView { + pub fn new() -> Self { + Self { log_view: None } + } +} + +impl Render for DapLogToolbarItemView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Some(log_view) = self.log_view.clone() else { + return div(); + }; + + let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| { + ( + log_view.menu_items(cx).unwrap_or_default(), + log_view.current_view.map(|(client_id, _)| client_id), + ) + }); + + let current_client = current_client_id.and_then(|current_client_id| { + if let Ok(ix) = menu_rows.binary_search_by_key(¤t_client_id, |e| e.client_id) { + Some(&menu_rows[ix]) + } else { + None + } + }); + + let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") + .anchor(AnchorCorner::TopLeft) + .trigger(Button::new( + "debug_server_menu_header", + current_client + .map(|row| { + Cow::Owned(format!( + "{} - {}", + row.client_name, + match row.selected_entry { + LogKind::Adapter => ADAPTER_LOGS, + LogKind::Rpc => RPC_MESSAGES, + } + )) + }) + .unwrap_or_else(|| "No server selected".into()), + )) + .menu(move |cx| { + let log_view = log_view.clone(); + let menu_rows = menu_rows.clone(); + ContextMenu::build(cx, move |mut menu, cx| { + for row in menu_rows.into_iter() { + menu = menu.header(row.client_name.to_string()); + + if row.has_adapter_logs { + menu = menu.entry( + ADAPTER_LOGS, + None, + cx.handler_for(&log_view, move |view, cx| { + view.show_log_messages_for_server(row.client_id, cx); + }), + ); + } + + menu = menu.custom_entry( + move |_| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .into_any_element() + }, + cx.handler_for(&log_view, move |view, cx| { + view.show_rpc_trace_for_server(row.client_id, cx); + }), + ); + } + menu + }) + .into() + }); + + h_flex().size_full().child(dap_menu).child( + div() + .child( + Button::new("clear_log_button", "Clear").on_click(cx.listener( + |this, _, cx| { + if let Some(log_view) = this.log_view.as_ref() { + log_view.update(cx, |log_view, cx| { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + }) + } + }, + )), + ) + .ml_2(), + ) + } +} + +impl EventEmitter for DapLogToolbarItemView {} + +impl ToolbarItemView for DapLogToolbarItemView { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn workspace::item::ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + if let Some(item) = active_pane_item { + if let Some(log_view) = item.downcast::() { + self.log_view = Some(log_view.clone()); + return workspace::ToolbarItemLocation::PrimaryLeft; + } + } + self.log_view = None; + + cx.notify(); + + workspace::ToolbarItemLocation::Hidden + } +} + +impl DapLogView { + pub fn new( + project: Model, + log_store: Model, + cx: &mut ViewContext, + ) -> Self { + let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), cx); + + let focus_handle = cx.focus_handle(); + + let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { + Event::NewLogEntry { id, entry, kind } => { + if log_view.current_view == Some((*id, *kind)) { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + let last_point = editor.buffer().read(cx).len(cx); + editor.edit( + vec![ + (last_point..last_point, entry.trim()), + (last_point..last_point, "\n"), + ], + cx, + ); + editor.set_read_only(true); + }); + } + } + }); + + Self { + editor, + focus_handle, + project, + log_store, + editor_subscriptions, + current_view: None, + _subscriptions: vec![events_subscriptions], + } + } + + fn editor_for_logs( + log_contents: String, + cx: &mut ViewContext, + ) -> (View, Vec) { + let editor = cx.new_view(|cx| { + let mut editor = Editor::multi_line(cx); + editor.set_text(log_contents, cx); + editor.move_to_end(&editor::actions::MoveToEnd, cx); + editor.set_read_only(true); + editor.set_show_inline_completions(Some(false), cx); + editor + }); + let editor_subscription = cx.subscribe( + &editor, + |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, DapLogView>| { + cx.emit(event.clone()) + }, + ); + let search_subscription = cx.subscribe( + &editor, + |_, _, event: &SearchEvent, cx: &mut ViewContext<'_, DapLogView>| { + cx.emit(event.clone()) + }, + ); + (editor, vec![editor_subscription, search_subscription]) + } + + fn menu_items(&self, cx: &AppContext) -> Option> { + let mut rows = self + .project + .read(cx) + .debug_clients(cx) + .map(|client| DapMenuItem { + client_id: client.id(), + client_name: client.config().kind.to_string(), + selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), + has_adapter_logs: client.has_adapter_logs(), + }) + .collect::>(); + rows.sort_by_key(|row| row.client_id); + rows.dedup_by_key(|row| row.client_id); + Some(rows) + } + + fn show_rpc_trace_for_server( + &mut self, + client_id: DebugAdapterClientId, + cx: &mut ViewContext, + ) { + let rpc_log = self.log_store.update(cx, |log_store, _| { + log_store + .rpc_messages_for_client(client_id) + .map(|state| log_contents(&state)) + }); + if let Some(rpc_log) = rpc_log { + self.current_view = Some((client_id, LogKind::Rpc)); + let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx); + let language = self.project.read(cx).languages().language_for_name("JSON"); + editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("log buffer should be a singleton") + .update(cx, |_, cx| { + cx.spawn({ + let buffer = cx.handle(); + |_, mut cx| async move { + let language = language.await.ok(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(language, cx); + }) + } + }) + .detach_and_log_err(cx); + }); + + self.editor = editor; + self.editor_subscriptions = editor_subscriptions; + cx.notify(); + } + + cx.focus(&self.focus_handle); + } + + fn show_log_messages_for_server( + &mut self, + client_id: DebugAdapterClientId, + cx: &mut ViewContext, + ) { + let message_log = self.log_store.update(cx, |log_store, _| { + log_store + .log_messages_for_client(client_id) + .map(|state| log_contents(&state)) + }); + if let Some(message_log) = message_log { + self.current_view = Some((client_id, LogKind::Adapter)); + let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, cx); + editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("log buffer should be a singleton"); + + self.editor = editor; + self.editor_subscriptions = editor_subscriptions; + cx.notify(); + } + + cx.focus(&self.focus_handle); + } +} + +fn log_contents(lines: &VecDeque) -> String { + let (a, b) = lines.as_slices(); + let a = a.iter().map(move |v| v.as_ref()); + let b = b.iter().map(move |v| v.as_ref()); + a.chain(b).fold(String::new(), |mut acc, el| { + acc.push_str(el); + acc.push('\n'); + acc + }) +} + +#[derive(Clone, PartialEq)] +pub(crate) struct DapMenuItem { + pub client_id: DebugAdapterClientId, + pub client_name: String, + pub has_adapter_logs: bool, + pub selected_entry: LogKind, +} + +const ADAPTER_LOGS: &str = "Adapter Logs"; +const RPC_MESSAGES: &str = "RPC Messages"; + +impl Render for DapLogView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + self.editor + .update(cx, |editor, cx| editor.render(cx).into_any_element()) + } +} + +actions!(debug, [OpenDebuggerServerLogs]); + +pub fn init(cx: &mut AppContext) { + let log_store = cx.new_model(|cx| LogStore::new(cx)); + + cx.observe_new_views(move |workspace: &mut Workspace, cx| { + let project = workspace.project(); + if project.read(cx).is_local() { + log_store.update(cx, |store, cx| { + store.add_project(project, cx); + }); + } + + let log_store = log_store.clone(); + workspace.register_action(move |workspace, _: &OpenDebuggerServerLogs, cx| { + let project = workspace.project().read(cx); + if project.is_local() { + workspace.add_item_to_active_pane( + Box::new(cx.new_view(|cx| { + DapLogView::new(workspace.project().clone(), log_store.clone(), cx) + })), + None, + true, + cx, + ); + } + }); + }) + .detach(); +} + +impl Item for DapLogView { + type Event = EditorEvent; + + fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) { + Editor::to_item_events(event, f) + } + + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some("DAP Logs".into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn as_searchable(&self, handle: &View) -> Option> { + Some(Box::new(handle.clone())) + } +} + +impl SearchableItem for DapLogView { + type Match = ::Match; + + fn clear_matches(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |e, cx| e.clear_matches(cx)) + } + + fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.update_matches(matches, cx)) + } + + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + self.editor.update(cx, |e, cx| e.query_suggestion(cx)) + } + + fn activate_match( + &mut self, + index: usize, + matches: &[Self::Match], + cx: &mut ViewContext, + ) { + self.editor + .update(cx, |e, cx| e.activate_match(index, matches, cx)) + } + + fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.select_matches(matches, cx)) + } + + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |e, cx| e.find_matches(query, cx)) + } + + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Since DAP Log is read-only, it doesn't make sense to support replace operation. + } + fn supported_options() -> workspace::searchable::SearchOptions { + workspace::searchable::SearchOptions { + case: true, + word: true, + regex: true, + // DAP log is read-only. + replacement: false, + selection: false, + } + } + fn active_match_index( + &mut self, + matches: &[Self::Match], + cx: &mut ViewContext, + ) -> Option { + self.editor + .update(cx, |e, cx| e.active_match_index(matches, cx)) + } +} + +impl FocusableView for DapLogView { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +pub enum Event { + NewLogEntry { + id: DebugAdapterClientId, + entry: String, + kind: LogKind, + }, +} + +impl EventEmitter for LogStore {} +impl EventEmitter for DapLogView {} +impl EventEmitter for DapLogView {} +impl EventEmitter for DapLogView {} diff --git a/crates/debugger_tools/src/debugger_tools.rs b/crates/debugger_tools/src/debugger_tools.rs new file mode 100644 index 00000000000000..744e90669bf402 --- /dev/null +++ b/crates/debugger_tools/src/debugger_tools.rs @@ -0,0 +1,8 @@ +mod dap_log; +pub use dap_log::*; + +use gpui::AppContext; + +pub fn init(cx: &mut AppContext) { + dap_log::init(cx); +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d69146fa90f92f..30e1d415139f29 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -241,11 +241,13 @@ pub enum Event { }, LanguageServerPrompt(LanguageServerPromptRequest), LanguageNotFound(Model), + DebugClientStarted(DebugAdapterClientId), DebugClientStopped(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, message: Message, }, + DebugClientLog(DebugAdapterClientId, String), ActiveEntryChanged(Option), ActivateProjectPanel, WorktreeAdded, @@ -2341,6 +2343,7 @@ impl Project { self.dap_store.update(cx, |store, cx| { store.initialize(client_id, cx).detach_and_log_err(cx) }); + cx.emit(Event::DebugClientStarted(*client_id)); } DapStoreEvent::DebugClientStopped(client_id) => { cx.emit(Event::DebugClientStopped(*client_id)); @@ -4292,6 +4295,21 @@ impl Project { .read(cx) .language_servers_for_buffer(buffer, cx) } + + pub fn debug_clients<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.dap_store.read(cx).running_clients() + } + + pub fn debug_client_for_id( + &self, + id: &DebugAdapterClientId, + cx: &AppContext, + ) -> Option> { + self.dap_store.read(cx).client_by_id(id) + } } fn deserialize_code_actions(code_actions: &HashMap) -> Vec { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2f3b79294d5479..7596d07c58d0bf 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -36,6 +36,7 @@ command_palette.workspace = true command_palette_hooks.workspace = true copilot.workspace = true debugger_ui.workspace = true +debugger_tools.workspace = true db.workspace = true dev_server_projects.workspace = true diagnostics.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ba445a5d37ade6..eea061f12f7fc0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -523,6 +523,7 @@ fn main() { zed::init(cx); project::Project::init(&client, cx); debugger_ui::init(cx); + debugger_tools::init(cx); client::init(&client, cx); language::init(cx); let telemetry = client.telemetry(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2303ec35832f38..778e7042f87cb0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -598,6 +598,8 @@ fn initialize_pane(workspace: &Workspace, pane: &View, cx: &mut ViewContex toolbar.add_item(project_search_bar, cx); let lsp_log_item = cx.new_view(|_| language_tools::LspLogToolbarItemView::new()); toolbar.add_item(lsp_log_item, cx); + let dap_log_item = cx.new_view(|_| debugger_tools::DapLogToolbarItemView::new()); + toolbar.add_item(dap_log_item, cx); let syntax_tree_item = cx.new_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); toolbar.add_item(syntax_tree_item, cx); From b426ff6074934ee975e29e2a17cc9d44959cb509 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 21 Oct 2024 20:00:30 +0200 Subject: [PATCH 303/650] Remove unused dep --- Cargo.lock | 1 - crates/debugger_tools/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5326c2f1651000..33f84e94dfa3b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3474,7 +3474,6 @@ dependencies = [ "futures 0.3.30", "gpui", "project", - "serde_json", "workspace", ] diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml index bdcfc6bc3d23e5..e139721a614f66 100644 --- a/crates/debugger_tools/Cargo.toml +++ b/crates/debugger_tools/Cargo.toml @@ -18,6 +18,5 @@ workspace.workspace = true editor.workspace = true project.workspace = true dap.workspace = true -serde_json.workspace = true futures.workspace = true anyhow.workspace = true From ffc058209d06336ad0bdec85581a14751f05ad84 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 21 Oct 2024 20:05:11 +0200 Subject: [PATCH 304/650] Add missing license --- crates/debugger_tools/LICENSE-GPL | 1 + 1 file changed, 1 insertion(+) create mode 120000 crates/debugger_tools/LICENSE-GPL diff --git a/crates/debugger_tools/LICENSE-GPL b/crates/debugger_tools/LICENSE-GPL new file mode 120000 index 00000000000000..e0f9dbd5d63fef --- /dev/null +++ b/crates/debugger_tools/LICENSE-GPL @@ -0,0 +1 @@ +LICENSE-GPL \ No newline at end of file From 8baa2805d0f7f79ac6f72c6befebdb5e96b40624 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 22 Oct 2024 08:13:22 +0200 Subject: [PATCH 305/650] Use the correct logkind for stderr --- crates/dap/src/transport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 1e767e2882cdca..377da21e9db645 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -243,7 +243,7 @@ impl TransportDelegate { } for (kind, log_handler) in log_handlers.lock().iter_mut() { - if matches!(kind, LogKind::Rpc) { + if matches!(kind, LogKind::Adapter) { log_handler(IoKind::StdErr, buffer.as_str()); } } From 61daad2377613dca6505cbdcd102397308ede01a Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 23 Oct 2024 04:45:02 -0400 Subject: [PATCH 306/650] Clean up DAP install code (#51) * Clean up dap adapter install_binary functions * Get lldb-dap working when on macos * Add release tag to name of downloaded DAPs * Check if adapter is already installed before installing one --- Cargo.lock | 3 + crates/dap/Cargo.toml | 2 + crates/dap/src/adapters.rs | 91 ++++++++++++++++++- crates/dap_adapters/Cargo.toml | 1 + crates/dap_adapters/src/dap_adapters.rs | 16 ++-- crates/dap_adapters/src/javascript.rs | 116 +++++------------------- crates/dap_adapters/src/lldb.rs | 24 ++++- crates/dap_adapters/src/php.rs | 112 +++++------------------ crates/dap_adapters/src/python.rs | 99 ++++---------------- crates/project/src/dap_store.rs | 27 +++--- crates/util/src/fs.rs | 23 ++++- 11 files changed, 223 insertions(+), 291 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33f84e94dfa3b5..ea1a65b7bef17f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3355,6 +3355,7 @@ dependencies = [ "log", "node_runtime", "parking_lot", + "paths", "schemars", "serde", "serde_json", @@ -3362,6 +3363,7 @@ dependencies = [ "smallvec", "smol", "task", + "util", ] [[package]] @@ -3389,6 +3391,7 @@ dependencies = [ "serde_json", "smol", "task", + "util", ] [[package]] diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index bf5c574dc66c3b..f3a955859869a8 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -19,6 +19,7 @@ http_client.workspace = true log.workspace = true node_runtime.workspace = true parking_lot.workspace = true +paths.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true @@ -26,3 +27,4 @@ settings.workspace = true smallvec.workspace = true smol.workspace = true task.workspace = true +util.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 119dd113d781ff..02e221088972fc 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,11 +1,18 @@ use crate::transport::Transport; use ::fs::Fs; -use anyhow::Result; +use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; -use http_client::HttpClient; +use http_client::{github::latest_github_release, HttpClient}; use node_runtime::NodeRuntime; use serde_json::Value; -use std::{collections::HashMap, ffi::OsString, path::Path, sync::Arc}; +use smol::{self, fs::File, process}; +use std::{ + collections::HashMap, + ffi::OsString, + fmt::Debug, + path::{Path, PathBuf}, + sync::Arc, +}; use task::DebugAdapterConfig; pub trait DapDelegate { @@ -35,8 +42,86 @@ pub struct DebugAdapterBinary { pub envs: Option>, } +pub async fn download_adapter_from_github( + adapter_name: DebugAdapterName, + github_repo: GithubRepo, + delegate: &dyn DapDelegate, +) -> Result { + let adapter_path = paths::debug_adapters_dir().join(&adapter_name); + let fs = delegate.fs(); + + if let Some(http_client) = delegate.http_client() { + if !adapter_path.exists() { + fs.create_dir(&adapter_path.as_path()).await?; + } + + let repo_name_with_owner = format!("{}/{}", github_repo.repo_owner, github_repo.repo_name); + let release = + latest_github_release(&repo_name_with_owner, false, false, http_client.clone()).await?; + + let asset_name = format!("{}_{}.zip", &adapter_name, release.tag_name); + let zip_path = adapter_path.join(&asset_name); + + if smol::fs::metadata(&zip_path).await.is_err() { + let mut response = http_client + .get(&release.zipball_url, Default::default(), true) + .await + .context("Error downloading release")?; + + let mut file = File::create(&zip_path).await?; + futures::io::copy(response.body_mut(), &mut file).await?; + + let _unzip_status = process::Command::new("unzip") + .current_dir(&adapter_path) + .arg(&zip_path) + .output() + .await? + .status; + + fs.remove_file(&zip_path.as_path(), Default::default()) + .await?; + + let file_name = util::fs::find_file_name_in_dir(&adapter_path.as_path(), |file_name| { + file_name.contains(&adapter_name.to_string()) + }) + .await + .ok_or_else(|| anyhow!("Unzipped directory not found")); + + let file_name = file_name?; + let downloaded_path = adapter_path + .join(format!("{}_{}", adapter_name, release.tag_name)) + .to_owned(); + + fs.rename( + file_name.as_path(), + downloaded_path.as_path(), + Default::default(), + ) + .await?; + + // if !unzip_status.success() { + // dbg!(unzip_status); + // Err(anyhow!("failed to unzip downloaded dap archive"))?; + // } + + return Ok(downloaded_path); + } + } + + bail!("Install failed to download & counldn't preinstalled dap") +} + +pub struct GithubRepo { + pub repo_name: String, + pub repo_owner: String, +} + #[async_trait(?Send)] pub trait DebugAdapter: 'static + Send + Sync { + fn id(&self) -> String { + "".to_string() + } + fn name(&self) -> DebugAdapterName; fn transport(&self) -> Box; diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index f97e0435dc7900..3e50c1eed82ff2 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -25,3 +25,4 @@ serde.workspace = true serde_json.workspace = true smol.workspace = true task.workspace = true +util.workspace = true diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 1116cd1da89f69..39edd6dfae3dd9 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -4,22 +4,18 @@ mod lldb; mod php; mod python; +use anyhow::{anyhow, bail, Result}; +use async_trait::async_trait; use custom::CustomDebugAdapter; +use dap::adapters::{ + self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, GithubRepo, +}; use javascript::JsDebugAdapter; use lldb::LldbDebugAdapter; use php::PhpDebugAdapter; use python::PythonDebugAdapter; - -use anyhow::{anyhow, bail, Context, Result}; -use async_trait::async_trait; -use dap::adapters::{DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName}; -use http_client::github::latest_github_release; use serde_json::{json, Value}; -use smol::{ - fs::{self, File}, - process, -}; -use std::{fmt::Debug, process::Stdio}; +use std::fmt::Debug; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 4fafc9b85f7a5b..dc4f4d02bd9f9f 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -38,6 +38,11 @@ impl DebugAdapter for JsDebugAdapter { .ok_or(anyhow!("Couldn't get npm runtime"))?; let adapter_path = paths::debug_adapters_dir().join(self.name()); + let adapter_path = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with("vscode-js-debug_") + }) + .await + .ok_or_else(|| anyhow!("Couldn't find javascript dap directory"))?; Ok(DebugAdapterBinary { command: node_runtime @@ -54,102 +59,29 @@ impl DebugAdapter for JsDebugAdapter { } async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let fs = delegate.fs(); - - if fs.is_dir(adapter_path.as_path()).await { - return Ok(()); - } - - if let Some(http_client) = delegate.http_client() { - if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()).await?; - } - - let release = latest_github_release( - "microsoft/vscode-js-debug", - false, - false, - http_client.clone(), - ) - .await?; - - let asset_name = format!("{}-{}", self.name(), release.tag_name); - let zip_path = adapter_path.join(asset_name); - - if fs::metadata(&zip_path).await.is_err() { - let mut response = http_client - .get(&release.zipball_url, Default::default(), true) - .await - .context("Error downloading release")?; - - let mut file = File::create(&zip_path).await?; - futures::io::copy(response.body_mut(), &mut file).await?; - - let _unzip_status = process::Command::new("unzip") - .current_dir(&adapter_path) - .arg(&zip_path) - .output() - .await? - .status; + let github_repo = GithubRepo { + repo_name: "vscode-js-debug".to_string(), + repo_owner: "microsoft".to_string(), + }; - let mut ls = process::Command::new("ls") - .current_dir(&adapter_path) - .stdout(Stdio::piped()) - .spawn()?; + let adapter_path = + adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; - let std = ls - .stdout - .take() - .ok_or(anyhow!("Failed to list directories"))? - .into_stdio() - .await?; - - let file_name = String::from_utf8( - process::Command::new("grep") - .arg("microsoft-vscode-js-debug") - .stdin(std) - .output() - .await? - .stdout, - )?; - - let file_name = file_name.trim_end(); - - process::Command::new("sh") - .current_dir(&adapter_path) - .arg("-c") - .arg(format!("mv {file_name}/* .")) - .output() - .await?; - - process::Command::new("rm") - .current_dir(&adapter_path) - .arg("-rf") - .arg(file_name) - .arg(zip_path) - .output() - .await?; - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "install", &[]) - .await - .ok(); - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["compile"]) - .await - .ok(); + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "install", &[]) + .await + .ok(); - return Ok(()); - } - } + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "run", &["compile"]) + .await + .ok(); - bail!("Install or fetch not implemented for Javascript debug adapter (yet)"); + return Ok(()); } fn request_args(&self, config: &DebugAdapterConfig) -> Value { diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 4d8e5e17c1e4b1..0911597e0490ce 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -26,8 +26,8 @@ impl DebugAdapter for LldbDebugAdapter { Box::new(StdioTransport::new()) } - async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { - bail!("Install or fetch not implemented for lldb debug adapter (yet)") + async fn install_binary(&self, _delegate: &dyn DapDelegate) -> Result<()> { + bail!("Install binary is not support for install_binary (yet)") } async fn fetch_binary( @@ -35,7 +35,25 @@ impl DebugAdapter for LldbDebugAdapter { _: &dyn DapDelegate, _: &DebugAdapterConfig, ) -> Result { - bail!("Install or fetch not implemented for lldb debug adapter (yet)") + #[cfg(target_os = "macos")] + { + let output = std::process::Command::new("xcrun") + .args(&["-f", "lldb-dap"]) + .output()?; + let lldb_dap_path = String::from_utf8(output.stdout)?.trim().to_string(); + + Ok(DebugAdapterBinary { + command: lldb_dap_path, + arguments: None, + envs: None, + }) + } + #[cfg(not(target_os = "macos"))] + { + Err(anyhow::anyhow!( + "LLDB-DAP is only supported on macOS (Right now)" + )) + } } fn request_args(&self, config: &DebugAdapterConfig) -> Value { diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 9e5c647eab20b3..f9cf3860018ca0 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -38,6 +38,11 @@ impl DebugAdapter for PhpDebugAdapter { .ok_or(anyhow!("Couldn't get npm runtime"))?; let adapter_path = paths::debug_adapters_dir().join(self.name()); + let adapter_path = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with("vscode-php-debug_") + }) + .await + .ok_or_else(|| anyhow!("Couldn't find javascript dap directory"))?; Ok(DebugAdapterBinary { command: node_runtime @@ -54,98 +59,29 @@ impl DebugAdapter for PhpDebugAdapter { } async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let fs = delegate.fs(); - - if fs.is_dir(adapter_path.as_path()).await { - return Ok(()); - } - - if let Some(http_client) = delegate.http_client() { - if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()).await?; - } - - let release = - latest_github_release("xdebug/vscode-php-debug", false, false, http_client.clone()) - .await?; - - let asset_name = format!("{}-{}", self.name(), release.tag_name); - let zip_path = adapter_path.join(asset_name); - - if fs::metadata(&zip_path).await.is_err() { - let mut response = http_client - .get(&release.zipball_url, Default::default(), true) - .await - .context("Error downloading release")?; - - let mut file = File::create(&zip_path).await?; - futures::io::copy(response.body_mut(), &mut file).await?; - - let _unzip_status = process::Command::new("unzip") - .current_dir(&adapter_path) - .arg(&zip_path) - .output() - .await? - .status; + let github_repo = GithubRepo { + repo_name: "vscode-php-debug".to_string(), + repo_owner: "xdebug".to_string(), + }; - let mut ls = process::Command::new("ls") - .current_dir(&adapter_path) - .stdout(Stdio::piped()) - .spawn()?; + let adapter_path = + adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; - let std = ls - .stdout - .take() - .ok_or(anyhow!("Failed to list directories"))? - .into_stdio() - .await?; - - let file_name = String::from_utf8( - process::Command::new("grep") - .arg("xdebug-vscode-php-debug") - .stdin(std) - .output() - .await? - .stdout, - )?; - - let file_name = file_name.trim_end(); - - process::Command::new("sh") - .current_dir(&adapter_path) - .arg("-c") - .arg(format!("mv {file_name}/* .")) - .output() - .await?; - - process::Command::new("rm") - .current_dir(&adapter_path) - .arg("-rf") - .arg(file_name) - .arg(zip_path) - .output() - .await?; - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "install", &[]) - .await - .is_ok(); - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["build"]) - .await - .is_ok(); + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "install", &[]) + .await + .is_ok(); - return Ok(()); - } - } + let _ = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))? + .run_npm_subcommand(&adapter_path, "run", &["build"]) + .await + .is_ok(); - bail!("Install or fetch not implemented for PHP debug adapter (yet)"); + Ok(()) } fn request_args(&self, config: &DebugAdapterConfig) -> Value { diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index da96bb0dd0fa15..e339289b8d0a59 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -24,6 +24,15 @@ impl DebugAdapter for PythonDebugAdapter { Box::new(StdioTransport::new()) } + async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { + let github_repo = GithubRepo { + repo_name: "debugpy".into(), + repo_owner: "microsoft".into(), + }; + + adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; + Ok(()) + } async fn fetch_binary( &self, _: &dyn DapDelegate, @@ -31,95 +40,19 @@ impl DebugAdapter for PythonDebugAdapter { ) -> Result { let adapter_path = paths::debug_adapters_dir().join(self.name()); + let debugpy_dir = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with("debugpy_") + }) + .await + .ok_or_else(|| anyhow!("Debugpy directory not found"))?; + Ok(DebugAdapterBinary { command: "python3".to_string(), - arguments: Some(vec![adapter_path.join(Self::ADAPTER_PATH).into()]), + arguments: Some(vec![debugpy_dir.join(Self::ADAPTER_PATH).into()]), envs: None, }) } - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let fs = delegate.fs(); - - if fs.is_dir(adapter_path.as_path()).await { - return Ok(()); - } - - if let Some(http_client) = delegate.http_client() { - let debugpy_dir = paths::debug_adapters_dir().join("debugpy"); - - if !debugpy_dir.exists() { - fs.create_dir(&debugpy_dir.as_path()).await?; - } - - let release = - latest_github_release("microsoft/debugpy", false, false, http_client.clone()) - .await?; - let asset_name = format!("{}.zip", release.tag_name); - - let zip_path = debugpy_dir.join(asset_name); - - if fs::metadata(&zip_path).await.is_err() { - let mut response = http_client - .get(&release.zipball_url, Default::default(), true) - .await - .context("Error downloading release")?; - - let mut file = File::create(&zip_path).await?; - futures::io::copy(response.body_mut(), &mut file).await?; - - let _unzip_status = process::Command::new("unzip") - .current_dir(&debugpy_dir) - .arg(&zip_path) - .output() - .await? - .status; - - let mut ls = process::Command::new("ls") - .current_dir(&debugpy_dir) - .stdout(Stdio::piped()) - .spawn()?; - - let std = ls - .stdout - .take() - .ok_or(anyhow!("Failed to list directories"))? - .into_stdio() - .await?; - - let file_name = String::from_utf8( - process::Command::new("grep") - .arg("microsoft-debugpy") - .stdin(std) - .output() - .await? - .stdout, - )?; - - let file_name = file_name.trim_end(); - process::Command::new("sh") - .current_dir(&debugpy_dir) - .arg("-c") - .arg(format!("mv {file_name}/* .")) - .output() - .await?; - - process::Command::new("rm") - .current_dir(&debugpy_dir) - .arg("-rf") - .arg(file_name) - .arg(zip_path) - .output() - .await?; - - return Ok(()); - } - } - - bail!("Install or fetch not implemented for Python debug adapter (yet)"); - } - fn request_args(&self, config: &DebugAdapterConfig) -> Value { json!({"program": config.program, "subProcess": true}) } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 40a242a83d4ec1..4966deaecc8a2a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -250,17 +250,22 @@ impl DapStore { .context("Creating debug adapter") .log_err()?, ); - let _ = adapter - .install_binary(&adapter_delegate) - .await - .context("Failed to install debug adapter binary") - .log_err()?; - let binary = adapter - .fetch_binary(&adapter_delegate, &config) - .await - .context("Failed to get debug adapter binary") - .log_err()?; + let mut binary = adapter.fetch_binary(&adapter_delegate, &config).await.ok(); + + if binary.is_none() { + let _ = adapter + .install_binary(&adapter_delegate) + .await + .context("Failed to install debug adapter binary") + .log_err()?; + + binary = adapter + .fetch_binary(&adapter_delegate, &config) + .await + .context("Failed to get debug adapter binary") + .log_err(); + } let mut request_args = json!({}); if let Some(config_args) = config.initialize_args.clone() { @@ -277,7 +282,7 @@ impl DapStore { client .start( - &binary, + &binary?, move |message, cx| { dap_store .update(cx, |_, cx| { diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index f235753e8b2af0..93340729df2b45 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::ResultExt; use async_fs as fs; @@ -26,3 +26,24 @@ where } } } + +pub async fn find_file_name_in_dir(dir: &Path, predicate: F) -> Option +where + F: Fn(&str) -> bool, +{ + if let Some(mut entries) = fs::read_dir(dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + + if let Some(file_name) = entry_path.file_name() { + if predicate(file_name.to_str().unwrap_or("")) { + return Some(entry_path); + } + } + } + } + } + + None +} From 932f4ed10a2dddff48d9aeb5db659088324d2ad0 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 23 Oct 2024 11:01:17 +0200 Subject: [PATCH 307/650] Remove not used method from merge --- crates/dap/src/adapters.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 02e221088972fc..1c099baae6affa 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -118,10 +118,6 @@ pub struct GithubRepo { #[async_trait(?Send)] pub trait DebugAdapter: 'static + Send + Sync { - fn id(&self) -> String { - "".to_string() - } - fn name(&self) -> DebugAdapterName; fn transport(&self) -> Box; From a95b16aa67a65966fea0d1625d23e2517aca81d0 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 23 Oct 2024 11:01:48 +0200 Subject: [PATCH 308/650] Remove borrow_mut because its not needed --- crates/dap/src/transport.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 377da21e9db645..b78e1e5acc66ae 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -15,7 +15,6 @@ use smol::{ process::{self, Child, ChildStderr, ChildStdout}, }; use std::{ - borrow::BorrowMut, collections::HashMap, net::{Ipv4Addr, SocketAddrV4}, process::Stdio, @@ -173,8 +172,8 @@ impl TransportDelegate { pending_requests: Requests, log_handlers: LogHandlers, ) -> Result<()> { - while let Ok(mut payload) = client_rx.recv().await { - if let Message::Request(request) = payload.borrow_mut() { + while let Ok(payload) = client_rx.recv().await { + if let Message::Request(request) = &payload { if let Some(sender) = current_requests.lock().await.remove(&request.seq) { pending_requests.lock().await.insert(request.seq, sender); } From e2d449a11f4692f26d681aaa87a4fbbc117b32fa Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 23 Oct 2024 11:44:24 +0200 Subject: [PATCH 309/650] Rename event case so its more clear --- crates/debugger_ui/src/console.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/stack_frame_list.rs | 6 +++--- crates/debugger_ui/src/variable_list.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 00b2c370156295..e4f1880fe5ac11 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -79,7 +79,7 @@ impl Console { cx: &mut ViewContext, ) { match event { - StackFrameListEvent::ChangedStackFrame => cx.notify(), + StackFrameListEvent::SelectedStackFrameChanged => cx.notify(), StackFrameListEvent::StackFramesUpdated => { // TODO debugger: check if we need to do something here } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index c203817fa2fadb..853255fb0bb3a7 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -134,7 +134,7 @@ impl DebugPanelItem { cx.subscribe( &stack_frame_list, move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { - StackFrameListEvent::ChangedStackFrame => this.clear_highlights(cx), + StackFrameListEvent::SelectedStackFrameChanged => this.clear_highlights(cx), _ => {} }, ), diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 6d31097339582a..907d9c05eeb317 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -19,7 +19,7 @@ use crate::debugger_panel_item::{self, DebugPanelItem}; #[derive(Debug)] pub enum StackFrameListEvent { - ChangedStackFrame, + SelectedStackFrameChanged, StackFramesUpdated, } @@ -110,7 +110,7 @@ impl StackFrameList { if let Some(stack_frame) = this.stack_frames.first() { this.current_stack_frame_id = stack_frame.id; - cx.emit(StackFrameListEvent::ChangedStackFrame); + cx.emit(StackFrameListEvent::SelectedStackFrameChanged); } this.list.reset(this.stack_frames.len()); @@ -221,7 +221,7 @@ impl StackFrameList { cx.notify(); - cx.emit(StackFrameListEvent::ChangedStackFrame); + cx.emit(StackFrameListEvent::SelectedStackFrameChanged); } })) .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index ab713f965fdd75..6bb05fbd11a927 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -128,7 +128,7 @@ impl VariableList { cx: &mut ViewContext, ) { match event { - StackFrameListEvent::ChangedStackFrame => { + StackFrameListEvent::SelectedStackFrameChanged => { self.build_entries(true, false, cx); } StackFrameListEvent::StackFramesUpdated => { From 290c76daefbaea45e346af029a891ba6d9130a2e Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:17:12 -0400 Subject: [PATCH 310/650] Enable Debug Adapter Updates (#53) * Get debug adapter to update when new versions are avaliable * Debug adapter download only happens if there's a new version avaliable * Use DebugAdapter.name() instead of string literals * Fix bug where some daps wouldn't update/install correctly * Clean up download adapter from github function * Add debug adapter caching * Add basic notification event to dap_store --- crates/dap/src/adapters.rs | 241 +++++++++++++++++------- crates/dap_adapters/src/custom.rs | 9 +- crates/dap_adapters/src/dap_adapters.rs | 3 +- crates/dap_adapters/src/javascript.rs | 41 +++- crates/dap_adapters/src/lldb.rs | 12 +- crates/dap_adapters/src/php.rs | 41 +++- crates/dap_adapters/src/python.rs | 31 ++- crates/project/src/dap_store.rs | 38 ++-- crates/project/src/project.rs | 7 +- crates/util/src/fs.rs | 29 ++- 10 files changed, 334 insertions(+), 118 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 1c099baae6affa..8b0d2154be95dc 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,15 +1,16 @@ use crate::transport::Transport; use ::fs::Fs; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use http_client::{github::latest_github_release, HttpClient}; use node_runtime::NodeRuntime; use serde_json::Value; -use smol::{self, fs::File, process}; +use smol::{self, fs::File, lock::Mutex, process}; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, ffi::OsString, fmt::Debug, + ops::Deref, path::{Path, PathBuf}, sync::Arc, }; @@ -19,10 +20,26 @@ pub trait DapDelegate { fn http_client(&self) -> Option>; fn node_runtime(&self) -> Option; fn fs(&self) -> Arc; + fn cached_binaries(&self) -> Arc>>; } +#[derive(PartialEq, Eq, Hash, Debug)] pub struct DebugAdapterName(pub Arc); +impl Deref for DebugAdapterName { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for DebugAdapterName { + fn as_ref(&self) -> &str { + &self.0 + } +} + impl AsRef for DebugAdapterName { fn as_ref(&self) -> &Path { Path::new(&*self.0) @@ -40,94 +57,190 @@ pub struct DebugAdapterBinary { pub command: String, pub arguments: Option>, pub envs: Option>, + pub version: String, +} + +pub struct AdapterVersion { + pub tag_name: String, + pub url: String, +} + +pub struct GithubRepo { + pub repo_name: String, + pub repo_owner: String, } pub async fn download_adapter_from_github( adapter_name: DebugAdapterName, - github_repo: GithubRepo, + github_version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(&adapter_name); + let version_dir = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name)); let fs = delegate.fs(); - if let Some(http_client) = delegate.http_client() { - if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()).await?; - } - - let repo_name_with_owner = format!("{}/{}", github_repo.repo_owner, github_repo.repo_name); - let release = - latest_github_release(&repo_name_with_owner, false, false, http_client.clone()).await?; - - let asset_name = format!("{}_{}.zip", &adapter_name, release.tag_name); - let zip_path = adapter_path.join(&asset_name); - - if smol::fs::metadata(&zip_path).await.is_err() { - let mut response = http_client - .get(&release.zipball_url, Default::default(), true) - .await - .context("Error downloading release")?; - - let mut file = File::create(&zip_path).await?; - futures::io::copy(response.body_mut(), &mut file).await?; + let http_client = delegate + .http_client() + .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; - let _unzip_status = process::Command::new("unzip") - .current_dir(&adapter_path) - .arg(&zip_path) - .output() - .await? - .status; - - fs.remove_file(&zip_path.as_path(), Default::default()) - .await?; + if !adapter_path.exists() { + fs.create_dir(&adapter_path.as_path()).await?; + } - let file_name = util::fs::find_file_name_in_dir(&adapter_path.as_path(), |file_name| { - file_name.contains(&adapter_name.to_string()) - }) - .await - .ok_or_else(|| anyhow!("Unzipped directory not found")); - - let file_name = file_name?; - let downloaded_path = adapter_path - .join(format!("{}_{}", adapter_name, release.tag_name)) - .to_owned(); - - fs.rename( - file_name.as_path(), - downloaded_path.as_path(), - Default::default(), - ) - .await?; - - // if !unzip_status.success() { - // dbg!(unzip_status); - // Err(anyhow!("failed to unzip downloaded dap archive"))?; - // } - - return Ok(downloaded_path); - } + if version_dir.exists() { + return Ok(version_dir); } - bail!("Install failed to download & counldn't preinstalled dap") + let asset_name = format!("{}_{}.zip", &adapter_name, github_version.tag_name); + let zip_path = adapter_path.join(&asset_name); + fs.remove_file( + zip_path.as_path(), + fs::RemoveOptions { + recursive: true, + ignore_if_not_exists: true, + }, + ) + .await?; + + let mut response = http_client + .get(&github_version.url, Default::default(), true) + .await + .context("Error downloading release")?; + + let mut file = File::create(&zip_path).await?; + futures::io::copy(response.body_mut(), &mut file).await?; + + let old_files: HashSet<_> = util::fs::collect_matching(&adapter_path.as_path(), |file_path| { + file_path != zip_path.as_path() + }) + .await + .into_iter() + .filter_map(|file_path| { + file_path + .file_name() + .and_then(|f| f.to_str()) + .map(|f| f.to_string()) + }) + .collect(); + + let _unzip_status = process::Command::new("unzip") + .current_dir(&adapter_path) + .arg(&zip_path) + .output() + .await? + .status; + + let file_name = util::fs::find_file_name_in_dir(&adapter_path.as_path(), |file_name| { + !file_name.ends_with(".zip") && !old_files.contains(file_name) + }) + .await + .ok_or_else(|| anyhow!("Unzipped directory not found")); + + let file_name = file_name?; + let downloaded_path = adapter_path + .join(format!("{}_{}", adapter_name, github_version.tag_name)) + .to_owned(); + + fs.rename( + file_name.as_path(), + downloaded_path.as_path(), + Default::default(), + ) + .await?; + + util::fs::remove_matching(&adapter_path, |entry| entry != version_dir).await; + + // if !unzip_status.success() { + // dbg!(unzip_status); + // Err(anyhow!("failed to unzip downloaded dap archive"))?; + // } + + Ok(downloaded_path) } -pub struct GithubRepo { - pub repo_name: String, - pub repo_owner: String, +pub async fn fetch_latest_adapter_version_from_github( + github_repo: GithubRepo, + delegate: &dyn DapDelegate, +) -> Result { + let http_client = delegate + .http_client() + .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; + let repo_name_with_owner = format!("{}/{}", github_repo.repo_owner, github_repo.repo_name); + let release = latest_github_release(&repo_name_with_owner, false, false, http_client).await?; + + Ok(AdapterVersion { + tag_name: release.tag_name, + url: release.zipball_url, + }) } #[async_trait(?Send)] pub trait DebugAdapter: 'static + Send + Sync { fn name(&self) -> DebugAdapterName; + async fn get_binary( + &self, + delegate: &dyn DapDelegate, + config: &DebugAdapterConfig, + ) -> Result { + if let Some(binary) = delegate.cached_binaries().lock().await.get(&self.name()) { + log::info!("Using cached debug adapter binary {}", self.name()); + return Ok(binary.clone()); + } + + log::info!("Getting latest version of debug adapter {}", self.name()); + let version = self.fetch_latest_adapter_version(delegate).await.ok(); + + let mut binary = self.get_installed_binary(delegate, config).await; + + if let Some(version) = version { + if binary + .as_ref() + .is_ok_and(|binary| binary.version == version.tag_name) + { + let binary = binary?; + + delegate + .cached_binaries() + .lock_arc() + .await + .insert(self.name(), binary.clone()); + + return Ok(binary); + } + + self.install_binary(version, delegate).await?; + binary = self.get_installed_binary(delegate, config).await; + } + + let binary = binary?; + + delegate + .cached_binaries() + .lock_arc() + .await + .insert(self.name(), binary.clone()); + + Ok(binary) + } + fn transport(&self) -> Box; + async fn fetch_latest_adapter_version( + &self, + delegate: &dyn DapDelegate, + ) -> Result; + /// Installs the binary for the debug adapter. /// This method is called when the adapter binary is not found or needs to be updated. /// It should download and install the necessary files for the debug adapter to function. - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()>; + async fn install_binary( + &self, + version: AdapterVersion, + delegate: &dyn DapDelegate, + ) -> Result<()>; - async fn fetch_binary( + async fn get_installed_binary( &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 2ad9cd97500743..6da45ea1351660 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -32,11 +32,15 @@ impl DebugAdapter for CustomDebugAdapter { } } - async fn install_binary(&self, _: &dyn DapDelegate) -> Result<()> { + async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { + bail!("Custom debug adapters don't have latest versions") + } + + async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> { Ok(()) } - async fn fetch_binary( + async fn get_installed_binary( &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, @@ -49,6 +53,7 @@ impl DebugAdapter for CustomDebugAdapter { .clone() .map(|args| args.iter().map(OsString::from).collect()), envs: self.custom_args.envs.clone(), + version: "Custom daps".to_string(), }) } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 39edd6dfae3dd9..e099f92430fec9 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -8,7 +8,8 @@ use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use custom::CustomDebugAdapter; use dap::adapters::{ - self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, GithubRepo, + self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, + GithubRepo, }; use javascript::JsDebugAdapter; use lldb::LldbDebugAdapter; diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index dc4f4d02bd9f9f..2c59a9ae2cbd59 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -28,7 +28,19 @@ impl DebugAdapter for JsDebugAdapter { })) } - async fn fetch_binary( + async fn fetch_latest_adapter_version( + &self, + delegate: &dyn DapDelegate, + ) -> Result { + let github_repo = GithubRepo { + repo_name: Self::ADAPTER_NAME.into(), + repo_owner: "microsoft".to_string(), + }; + + adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + } + + async fn get_installed_binary( &self, delegate: &dyn DapDelegate, _: &DebugAdapterConfig, @@ -38,11 +50,20 @@ impl DebugAdapter for JsDebugAdapter { .ok_or(anyhow!("Couldn't get npm runtime"))?; let adapter_path = paths::debug_adapters_dir().join(self.name()); + let file_name_prefix = format!("{}_", self.name()); + let adapter_path = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with("vscode-js-debug_") + file_name.starts_with(&file_name_prefix) }) .await - .ok_or_else(|| anyhow!("Couldn't find javascript dap directory"))?; + .ok_or_else(|| anyhow!("Couldn't find Javascript dap directory"))?; + + let version = adapter_path + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("Javascript debug adapter has invalid file name"))? + .to_string(); Ok(DebugAdapterBinary { command: node_runtime @@ -55,17 +76,17 @@ impl DebugAdapter for JsDebugAdapter { "8133".into(), ]), envs: None, + version, }) } - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let github_repo = GithubRepo { - repo_name: "vscode-js-debug".to_string(), - repo_owner: "microsoft".to_string(), - }; - + async fn install_binary( + &self, + version: AdapterVersion, + delegate: &dyn DapDelegate, + ) -> Result<()> { let adapter_path = - adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; + adapters::download_adapter_from_github(self.name(), version, delegate).await?; let _ = delegate .node_runtime() diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 0911597e0490ce..bccb68d6e0cbe9 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -26,11 +26,19 @@ impl DebugAdapter for LldbDebugAdapter { Box::new(StdioTransport::new()) } - async fn install_binary(&self, _delegate: &dyn DapDelegate) -> Result<()> { + async fn install_binary( + &self, + _version: AdapterVersion, + _delegate: &dyn DapDelegate, + ) -> Result<()> { bail!("Install binary is not support for install_binary (yet)") } - async fn fetch_binary( + async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { + bail!("Fetch latest adapter version not implemented for lldb (yet)") + } + + async fn get_installed_binary( &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index f9cf3860018ca0..a5aa647822a4d4 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -28,7 +28,19 @@ impl DebugAdapter for PhpDebugAdapter { })) } - async fn fetch_binary( + async fn fetch_latest_adapter_version( + &self, + delegate: &dyn DapDelegate, + ) -> Result { + let github_repo = GithubRepo { + repo_name: Self::ADAPTER_NAME.into(), + repo_owner: "xdebug".to_string(), + }; + + adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + } + + async fn get_installed_binary( &self, delegate: &dyn DapDelegate, _: &DebugAdapterConfig, @@ -38,11 +50,20 @@ impl DebugAdapter for PhpDebugAdapter { .ok_or(anyhow!("Couldn't get npm runtime"))?; let adapter_path = paths::debug_adapters_dir().join(self.name()); + let file_name_prefix = format!("{}_", self.name()); + let adapter_path = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with("vscode-php-debug_") + file_name.starts_with(&file_name_prefix) }) .await - .ok_or_else(|| anyhow!("Couldn't find javascript dap directory"))?; + .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; + + let version = adapter_path + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("Php debug adapter has invalid file name"))? + .to_string(); Ok(DebugAdapterBinary { command: node_runtime @@ -55,17 +76,17 @@ impl DebugAdapter for PhpDebugAdapter { "--server=8132".into(), ]), envs: None, + version, }) } - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { - let github_repo = GithubRepo { - repo_name: "vscode-php-debug".to_string(), - repo_owner: "xdebug".to_string(), - }; - + async fn install_binary( + &self, + version: AdapterVersion, + delegate: &dyn DapDelegate, + ) -> Result<()> { let adapter_path = - adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; + adapters::download_adapter_from_github(self.name(), version, delegate).await?; let _ = delegate .node_runtime() diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index e339289b8d0a59..136b95c0972776 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -24,32 +24,53 @@ impl DebugAdapter for PythonDebugAdapter { Box::new(StdioTransport::new()) } - async fn install_binary(&self, delegate: &dyn DapDelegate) -> Result<()> { + async fn fetch_latest_adapter_version( + &self, + delegate: &dyn DapDelegate, + ) -> Result { let github_repo = GithubRepo { - repo_name: "debugpy".into(), + repo_name: Self::ADAPTER_NAME.into(), repo_owner: "microsoft".into(), }; - adapters::download_adapter_from_github(self.name(), github_repo, delegate).await?; + adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + } + + async fn install_binary( + &self, + version: AdapterVersion, + delegate: &dyn DapDelegate, + ) -> Result<()> { + adapters::download_adapter_from_github(self.name(), version, delegate).await?; Ok(()) } - async fn fetch_binary( + + async fn get_installed_binary( &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(self.name()); + let file_name_prefix = format!("{}_", self.name()); let debugpy_dir = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with("debugpy_") + file_name.starts_with(&file_name_prefix) }) .await .ok_or_else(|| anyhow!("Debugpy directory not found"))?; + let version = debugpy_dir + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("Python debug adapter has invalid file name"))? + .to_string(); + Ok(DebugAdapterBinary { command: "python3".to_string(), arguments: Some(vec![debugpy_dir.join(Self::ADAPTER_PATH).into()]), envs: None, + version, }) } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 4966deaecc8a2a..159d69b7392146 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,6 +1,7 @@ use crate::ProjectPath; use anyhow::{anyhow, Context as _, Result}; -use collections::{HashMap, HashSet}; +use collections::HashSet; +use dap::adapters::{DebugAdapterBinary, DebugAdapterName}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::{Message, Response}; use dap::requests::{ @@ -28,8 +29,9 @@ use language::{Buffer, BufferSnapshot}; use node_runtime::NodeRuntime; use serde_json::{json, Value}; use settings::WorktreeId; +use smol::lock::Mutex; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, hash::{Hash, Hasher}, path::{Path, PathBuf}, sync::{ @@ -48,6 +50,7 @@ pub enum DapStoreEvent { client_id: DebugAdapterClientId, message: Message, }, + Notification(String), } pub enum DebugAdapterClientState { @@ -64,6 +67,7 @@ pub struct DebugPosition { pub struct DapStore { next_client_id: AtomicUsize, clients: HashMap, + cached_binaries: Arc>>, breakpoints: BTreeMap>, capabilities: HashMap, active_debug_line: Option<(ProjectPath, DebugPosition)>, @@ -86,6 +90,7 @@ impl DapStore { Self { active_debug_line: None, clients: Default::default(), + cached_binaries: Default::default(), breakpoints: Default::default(), capabilities: HashMap::default(), next_client_id: Default::default(), @@ -242,6 +247,7 @@ impl DapStore { self.http_client.clone(), self.node_runtime.clone(), self.fs.clone(), + self.cached_binaries.clone(), ); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); @@ -251,21 +257,10 @@ impl DapStore { .log_err()?, ); - let mut binary = adapter.fetch_binary(&adapter_delegate, &config).await.ok(); - - if binary.is_none() { - let _ = adapter - .install_binary(&adapter_delegate) - .await - .context("Failed to install debug adapter binary") - .log_err()?; - - binary = adapter - .fetch_binary(&adapter_delegate, &config) - .await - .context("Failed to get debug adapter binary") - .log_err(); - } + let binary = adapter + .get_binary(&adapter_delegate, &config) + .await + .log_err()?; let mut request_args = json!({}); if let Some(config_args) = config.initialize_args.clone() { @@ -282,7 +277,7 @@ impl DapStore { client .start( - &binary?, + &binary, move |message, cx| { dap_store .update(cx, |_, cx| { @@ -1238,6 +1233,7 @@ pub struct DapAdapterDelegate { fs: Arc, http_client: Option>, node_runtime: Option, + cached_binaries: Arc>>, } impl DapAdapterDelegate { @@ -1245,11 +1241,13 @@ impl DapAdapterDelegate { http_client: Option>, node_runtime: Option, fs: Arc, + cached_binaries: Arc>>, ) -> Self { Self { fs, http_client, node_runtime, + cached_binaries, } } } @@ -1266,4 +1264,8 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { fn fs(&self) -> Arc { self.fs.clone() } + + fn cached_binaries(&self) -> Arc>> { + self.cached_binaries.clone() + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 30e1d415139f29..f363e1d6dde6f8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -624,6 +624,7 @@ impl Project { cx, ) }); + cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); let buffer_store = cx .new_model(|cx| BufferStore::local(worktree_store.clone(), dap_store.clone(), cx)); @@ -663,8 +664,6 @@ impl Project { cx.subscribe(&settings_observer, Self::on_settings_observer_event) .detach(); - cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); - let lsp_store = cx.new_model(|cx| { LspStore::new_local( buffer_store.clone(), @@ -2354,6 +2353,10 @@ impl Project { message: message.clone(), }); } + DapStoreEvent::Notification(message) => cx.emit(Event::Toast { + notification_id: "dap".into(), + message: message.clone(), + }), } } diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index 93340729df2b45..5fe9b055ab3828 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -1,8 +1,7 @@ -use std::path::{Path, PathBuf}; - use crate::ResultExt; use async_fs as fs; use futures_lite::StreamExt; +use std::path::{Path, PathBuf}; /// Removes all files and directories matching the given predicate pub async fn remove_matching(dir: &Path, predicate: F) @@ -27,6 +26,25 @@ where } } +pub async fn collect_matching(dir: &Path, predicate: F) -> Vec +where + F: Fn(&Path) -> bool, +{ + let mut matching = vec![]; + + if let Some(mut entries) = fs::read_dir(dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + if predicate(entry.path().as_path()) { + matching.push(entry.path()); + } + } + } + } + + matching +} + pub async fn find_file_name_in_dir(dir: &Path, predicate: F) -> Option where F: Fn(&str) -> bool, @@ -36,8 +54,11 @@ where if let Some(entry) = entry.log_err() { let entry_path = entry.path(); - if let Some(file_name) = entry_path.file_name() { - if predicate(file_name.to_str().unwrap_or("")) { + if let Some(file_name) = entry_path + .file_name() + .and_then(|file_name| file_name.to_str()) + { + if predicate(file_name) { return Some(entry_path); } } From 57668dbaebb1e9d04077042b10429897d454a6a9 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 24 Oct 2024 20:50:40 +0200 Subject: [PATCH 311/650] Fix compile error for missing field --- crates/dap_adapters/src/lldb.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index bccb68d6e0cbe9..10ff48638cb2cf 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -54,6 +54,7 @@ impl DebugAdapter for LldbDebugAdapter { command: lldb_dap_path, arguments: None, envs: None, + version: "1".into(), }) } #[cfg(not(target_os = "macos"))] From a45aa3d014d1caaec0e30f82f0e2aa3d154175ca Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 26 Oct 2024 16:27:20 +0200 Subject: [PATCH 312/650] Correctly refetch/invalidate stackframes/scopes/variables (#55) * Correctly invalidate and refetch data * Fix show current stack frame after invalidating * Remove not used return value * Clean up * Don't send SelectedStackFrameChanged when id is still the same * Only update the active debug line if we got the editor --- crates/debugger_tools/src/dap_log.rs | 8 +- crates/debugger_ui/src/console.rs | 4 +- crates/debugger_ui/src/stack_frame_list.rs | 43 +- crates/debugger_ui/src/variable_list.rs | 723 ++++++++++++++------- 4 files changed, 504 insertions(+), 274 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 6a61edf1ad2620..74622d3d03e51c 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -136,10 +136,8 @@ impl LogStore { io_kind: IoKind, message: &str, cx: &mut ModelContext, - ) -> Option<()> { + ) { self.add_debug_client_message(client_id, io_kind, message.to_string(), cx); - - Some(()) } fn on_adapter_log( @@ -148,10 +146,8 @@ impl LogStore { io_kind: IoKind, message: &str, cx: &mut ModelContext, - ) -> Option<()> { + ) { self.add_debug_client_log(client_id, io_kind, message.to_string(), cx); - - Some(()) } pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index e4f1880fe5ac11..a2393a8dc6986e 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -121,9 +121,7 @@ impl Console { console.add_message(&response.result, cx); console.variable_list.update(cx, |variable_list, cx| { - variable_list - .refetch_existing_variables(cx) - .detach_and_log_err(cx); + variable_list.invalidate(cx); }) }) }) diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 907d9c05eeb317..7da06104e29145 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -33,6 +33,7 @@ pub struct StackFrameList { workspace: WeakView, client_id: DebugAdapterClientId, _subscriptions: Vec, + fetch_stack_frames_task: Option>>, } impl StackFrameList { @@ -65,6 +66,7 @@ impl StackFrameList { client_id: *client_id, workspace: workspace.clone(), dap_store: dap_store.clone(), + fetch_stack_frames_task: None, stack_frames: Default::default(), current_stack_frame_id: Default::default(), } @@ -86,31 +88,34 @@ impl StackFrameList { ) { match event { Stopped { go_to_stack_frame } => { - self.fetch_stack_frames(*go_to_stack_frame, cx) - .detach_and_log_err(cx); + self.fetch_stack_frames(*go_to_stack_frame, cx); } _ => {} } } - fn fetch_stack_frames( - &self, - go_to_stack_frame: bool, - cx: &mut ViewContext, - ) -> Task> { + pub fn invalidate(&mut self, cx: &mut ViewContext) { + self.fetch_stack_frames(true, cx); + } + + fn fetch_stack_frames(&mut self, go_to_stack_frame: bool, cx: &mut ViewContext) { let task = self.dap_store.update(cx, |store, cx| { store.stack_frames(&self.client_id, self.thread_id, cx) }); - cx.spawn(|this, mut cx| async move { + self.fetch_stack_frames_task = Some(cx.spawn(|this, mut cx| async move { let mut stack_frames = task.await?; let task = this.update(&mut cx, |this, cx| { std::mem::swap(&mut this.stack_frames, &mut stack_frames); + let previous_stack_frame_id = this.current_stack_frame_id; if let Some(stack_frame) = this.stack_frames.first() { this.current_stack_frame_id = stack_frame.id; - cx.emit(StackFrameListEvent::SelectedStackFrameChanged); + + if previous_stack_frame_id != this.current_stack_frame_id { + cx.emit(StackFrameListEvent::SelectedStackFrameChanged); + } } this.list.reset(this.stack_frames.len()); @@ -129,8 +134,10 @@ impl StackFrameList { task.await?; } - Ok(()) - }) + this.update(&mut cx, |this, _| { + this.fetch_stack_frames_task.take(); + }) + })); } pub fn go_to_stack_frame(&mut self, cx: &mut ViewContext) -> Task> { @@ -151,19 +158,21 @@ impl StackFrameList { return Task::ready(Err(anyhow!("Project path not found"))); }; - self.dap_store.update(cx, |store, cx| { - store.set_active_debug_line(&project_path, row, column, cx); - }); - cx.spawn({ let workspace = self.workspace.clone(); - move |_, mut cx| async move { + move |this, mut cx| async move { let task = workspace.update(&mut cx, |workspace, cx| { - workspace.open_path_preview(project_path, None, false, true, cx) + workspace.open_path_preview(project_path.clone(), None, false, true, cx) })?; let editor = task.await?.downcast::().unwrap(); + this.update(&mut cx, |this, cx| { + this.dap_store.update(cx, |store, cx| { + store.set_active_debug_line(&project_path, row, column, cx); + }) + })?; + workspace.update(&mut cx, |_, cx| { editor.update(cx, |editor, cx| editor.go_to_active_debug_line(cx)) }) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 6bb05fbd11a927..ae85b295265de8 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -18,7 +18,7 @@ use std::{ }; use ui::{prelude::*, ContextMenu, ListItem}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct VariableContainer { pub container_reference: u64, pub variable: Variable, @@ -35,6 +35,12 @@ pub struct SetVariableState { parent_variables_reference: u64, } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum OpenEntry { + Scope { name: String }, + Variable { name: String, depth: usize }, +} + #[derive(Debug, Clone)] pub enum VariableListEntry { Scope(Scope), @@ -51,22 +57,64 @@ pub enum VariableListEntry { }, } +#[derive(Debug)] +struct ScopeVariableIndex { + fetched_ids: HashSet, + variables: Vec, +} + +impl ScopeVariableIndex { + pub fn new() -> Self { + Self { + variables: Vec::new(), + fetched_ids: HashSet::default(), + } + } + + pub fn fetched(&self, container_reference: &u64) -> bool { + self.fetched_ids.contains(container_reference) + } + + /// All the variables should have the same depth and the same container reference + pub fn add_variables(&mut self, container_reference: u64, variables: Vec) { + let position = self + .variables + .iter() + .position(|v| v.variable.variables_reference == container_reference); + + self.fetched_ids.insert(container_reference); + + if let Some(position) = position { + self.variables.splice(position + 1..=position, variables); + } else { + self.variables.extend(variables); + } + } + + pub fn is_empty(&self) -> bool { + self.variables.is_empty() + } + + pub fn variables(&self) -> &[VariableContainer] { + &self.variables + } +} + pub struct VariableList { list: ListState, - dap_store: Model, focus_handle: FocusHandle, + dap_store: Model, + open_entries: Vec, client_id: DebugAdapterClientId, - open_entries: Vec, scopes: HashMap>, set_variable_editor: View, _subscriptions: Vec, - fetched_variable_ids: HashSet, stack_frame_list: View, set_variable_state: Option, entries: HashMap>, fetch_variables_task: Option>>, - // (stack_frame_id, scope.variables_reference) -> variables - variables: BTreeMap<(u64, u64), Vec>, + // (stack_frame_id, scope_id) -> VariableIndex + variables: BTreeMap<(u64, u64), ScopeVariableIndex>, open_context_menu: Option<(View, Point, Subscription)>, } @@ -116,7 +164,6 @@ impl VariableList { entries: Default::default(), variables: Default::default(), open_entries: Default::default(), - fetched_variable_ids: Default::default(), stack_frame_list: stack_frame_list.clone(), } } @@ -142,7 +189,7 @@ impl VariableList { self.variables .range((stack_frame_id, u64::MIN)..(stack_frame_id, u64::MAX)) - .flat_map(|(_, containers)| containers.iter().cloned()) + .flat_map(|(_, containers)| containers.variables.iter().cloned()) .collect() } @@ -163,10 +210,9 @@ impl VariableList { scope, variable, has_children, - container_reference: parent_variables_reference, + container_reference, } => self.render_variable( - ix, - *parent_variables_reference, + *container_reference, variable, scope, *depth, @@ -176,7 +222,7 @@ impl VariableList { } } - fn toggle_entry_collapsed(&mut self, entry_id: &SharedString, cx: &mut ViewContext) { + fn toggle_entry(&mut self, entry_id: &OpenEntry, cx: &mut ViewContext) { match self.open_entries.binary_search(&entry_id) { Ok(ix) => { self.open_entries.remove(ix); @@ -207,25 +253,35 @@ impl VariableList { let mut entries: Vec = Vec::default(); for scope in scopes { - let Some(variables) = self + let Some(index) = self .variables .get(&(stack_frame_id, scope.variables_reference)) else { continue; }; - if variables.is_empty() { + if index.is_empty() { continue; } - if open_first_scope && entries.is_empty() { - self.open_entries.push(scope_entry_id(scope)); + let scope_open_entry_id = OpenEntry::Scope { + name: scope.name.clone(), + }; + + if open_first_scope + && entries.is_empty() + && self + .open_entries + .binary_search(&scope_open_entry_id) + .is_err() + { + self.open_entries.push(scope_open_entry_id.clone()); } entries.push(VariableListEntry::Scope(scope.clone())); if self .open_entries - .binary_search(&scope_entry_id(scope)) + .binary_search(&scope_open_entry_id) .is_err() { continue; @@ -233,7 +289,7 @@ impl VariableList { let mut depth_check: Option = None; - for variable_container in variables { + for variable_container in index.variables().iter() { let depth = variable_container.depth; let variable = &variable_container.variable; let container_reference = variable_container.container_reference; @@ -248,7 +304,10 @@ impl VariableList { if self .open_entries - .binary_search(&variable_entry_id(scope, variable, depth)) + .binary_search(&OpenEntry::Variable { + name: variable.name.clone(), + depth, + }) .is_err() { if depth_check.is_none() || depth_check.is_some_and(|d| d > depth) { @@ -285,79 +344,134 @@ impl VariableList { cx.notify(); } - fn fetch_variables(&mut self, cx: &mut ViewContext) { - let stack_frames = self.stack_frame_list.read(cx).stack_frames().clone(); - - self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { - let mut scope_tasks = Vec::with_capacity(stack_frames.len()); - for stack_frame in stack_frames.clone().into_iter() { - let stack_frame_scopes_task = this.update(&mut cx, |this, cx| { + fn fetch_nested_variables( + &self, + variables_reference: u64, + depth: usize, + open_entries: &Vec, + cx: &mut ViewContext, + ) -> Task>> { + cx.spawn({ + let open_entries = open_entries.clone(); + |this, mut cx| async move { + let variables_task = this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { - store.scopes(&this.client_id, stack_frame.id, cx) + store.variables(&this.client_id, variables_reference, cx) }) - }); + })?; - scope_tasks.push(async move { - anyhow::Ok((stack_frame.id, stack_frame_scopes_task?.await?)) - }); + let mut variables = Vec::new(); + + for variable in variables_task.await? { + variables.push(VariableContainer { + variable: variable.clone(), + container_reference: variables_reference, + depth, + }); + + if open_entries + .binary_search(&&OpenEntry::Variable { + name: variable.name.clone(), + depth, + }) + .is_ok() + { + let task = this.update(&mut cx, |this, cx| { + this.fetch_nested_variables( + variable.variables_reference, + depth + 1, + &open_entries, + cx, + ) + })?; + + variables.extend(task.await?); + } + } + + anyhow::Ok(variables) } + }) + } - let mut stack_frame_tasks = Vec::with_capacity(scope_tasks.len()); - for (stack_frame_id, scopes) in try_join_all(scope_tasks).await? { - let variable_tasks = this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - let mut tasks = Vec::with_capacity(scopes.len()); + fn fetch_variables_for_stack_frame( + &self, + stack_frame_id: u64, + open_entries: &Vec, + cx: &mut ViewContext, + ) -> Task, HashMap>)>> { + let scopes_task = self.dap_store.update(cx, |store, cx| { + store.scopes(&self.client_id, stack_frame_id, cx) + }); - for scope in scopes { - let variables_task = - store.variables(&this.client_id, scope.variables_reference, cx); - tasks.push(async move { anyhow::Ok((scope, variables_task.await?)) }); - } + cx.spawn({ + let open_entries = open_entries.clone(); + |this, mut cx| async move { + let mut variables = HashMap::new(); - tasks - }) - })?; + let scopes = scopes_task.await?; + + for scope in scopes.iter() { + let variables_task = this.update(&mut cx, |this, cx| { + this.fetch_nested_variables(scope.variables_reference, 1, &open_entries, cx) + })?; - stack_frame_tasks.push(async move { - anyhow::Ok((stack_frame_id, try_join_all(variable_tasks).await?)) + variables.insert(scope.variables_reference, variables_task.await?); + } + + Ok((scopes, variables)) + } + }) + } + + fn fetch_variables(&mut self, cx: &mut ViewContext) { + let stack_frames = self.stack_frame_list.read(cx).stack_frames().clone(); + + self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { + let mut tasks = Vec::with_capacity(stack_frames.len()); + + let open_entries = this.update(&mut cx, |this, _| { + this.open_entries + .iter() + .filter(|e| matches!(e, OpenEntry::Variable { .. })) + .cloned() + .collect::>() + })?; + + for stack_frame in stack_frames.clone().into_iter() { + let task = this.update(&mut cx, |this, cx| { + this.fetch_variables_for_stack_frame(stack_frame.id, &open_entries, cx) }); + + tasks.push( + cx.background_executor() + .spawn(async move { anyhow::Ok((stack_frame.id, task?.await?)) }), + ); } - let result = try_join_all(stack_frame_tasks).await?; + let result = try_join_all(tasks).await?; this.update(&mut cx, |this, cx| { - this.variables.clear(); - this.scopes.clear(); - this.fetched_variable_ids.clear(); - - for (stack_frame_id, scopes) in result { - for (scope, variables) in scopes { - this.scopes - .entry(stack_frame_id) - .or_default() - .push(scope.clone()); - - this.fetched_variable_ids.insert(scope.variables_reference); - - this.variables.insert( - (stack_frame_id, scope.variables_reference), - variables - .into_iter() - .map(|v| VariableContainer { - container_reference: scope.variables_reference, - variable: v, - depth: 1, - }) - .collect::>(), - ); + let mut new_variables = BTreeMap::new(); + let mut new_scopes = HashMap::new(); + + for (stack_frame_id, (scopes, variables)) in result { + new_scopes.insert(stack_frame_id, scopes); + + for (scope_id, variables) in variables.into_iter() { + let mut variable_index = ScopeVariableIndex::new(); + variable_index.add_variables(scope_id, variables); + + new_variables.insert((stack_frame_id, scope_id), variable_index); } } - this.build_entries(true, false, cx); + std::mem::swap(&mut this.variables, &mut new_variables); + std::mem::swap(&mut this.scopes, &mut new_scopes); - this.fetch_variables_task.take(); + this.build_entries(true, true, cx); - cx.notify(); + this.fetch_variables_task.take(); }) })); } @@ -476,7 +590,7 @@ impl VariableList { }); let Some(state) = self.set_variable_state.take() else { - return cx.notify(); + return; }; if new_variable_value == state.value @@ -508,67 +622,18 @@ impl VariableList { set_value_task?.await?; - this.update(&mut cx, |this, cx| this.refetch_existing_variables(cx))? - .await?; - this.update(&mut cx, |this, cx| { this.build_entries(false, true, cx); + this.invalidate(cx); }) }) .detach_and_log_err(cx); } - pub fn refetch_existing_variables(&mut self, cx: &mut ViewContext) -> Task> { - let mut scope_tasks = Vec::with_capacity(self.variables.len()); - - for ((stack_frame_id, scope_id), variable_containers) in self.variables.clone().into_iter() - { - let mut variable_tasks = Vec::with_capacity(variable_containers.len()); - - for variable_container in variable_containers { - let fetch_variables_task = self.dap_store.update(cx, |store, cx| { - store.variables(&self.client_id, variable_container.container_reference, cx) - }); - - variable_tasks.push(async move { - let depth = variable_container.depth; - let container_reference = variable_container.container_reference; - - anyhow::Ok( - fetch_variables_task - .await? - .into_iter() - .map(move |variable| VariableContainer { - container_reference, - variable, - depth, - }) - .collect::>(), - ) - }); - } - - scope_tasks.push(async move { - anyhow::Ok(( - (stack_frame_id, scope_id), - try_join_all(variable_tasks).await?, - )) - }); - } - - cx.spawn(|this, mut cx| async move { - let updated_variables = try_join_all(scope_tasks).await?; - - this.update(&mut cx, |this, cx| { - for (entry_id, variable_containers) in updated_variables { - for variables in variable_containers { - this.variables.insert(entry_id, variables); - } - } - - this.build_entries(false, true, cx); - }) - }) + pub fn invalidate(&mut self, cx: &mut ViewContext) { + self.stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list.invalidate(cx); + }); } fn render_set_variable_editor( @@ -590,11 +655,13 @@ impl VariableList { .into_any_element() } + #[allow(clippy::too_many_arguments)] fn on_toggle_variable( &mut self, - ix: usize, - variable_id: &SharedString, + scope_id: u64, + entry_id: &OpenEntry, variable_reference: u64, + depth: usize, has_children: bool, disclosed: Option, cx: &mut ViewContext, @@ -603,73 +670,52 @@ impl VariableList { return; } - // if we already opened the variable/we already fetched it - // we can just toggle it because we already have the nested variable - if disclosed.unwrap_or(true) || self.fetched_variable_ids.contains(&variable_reference) { - return self.toggle_entry_collapsed(&variable_id, cx); - } - let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); - let Some(entries) = self.entries.get(&stack_frame_id) else { + let Some(index) = self.variables.get(&(stack_frame_id, scope_id)) else { return; }; - let Some(entry) = entries.get(ix) else { - return; - }; - - if let VariableListEntry::Variable { scope, depth, .. } = entry { - let variable_id = variable_id.clone(); - let scope = scope.clone(); - let depth = *depth; - - let fetch_variables_task = self.dap_store.update(cx, |store, cx| { - store.variables(&self.client_id, variable_reference, cx) - }); - - cx.spawn(|this, mut cx| async move { - let new_variables = fetch_variables_task.await?; - - this.update(&mut cx, |this, cx| { - let Some(variables) = this - .variables - .get_mut(&(stack_frame_id, scope.variables_reference)) - else { - return; - }; + // if we already opened the variable/we already fetched it + // we can just toggle it because we already have the nested variable + if disclosed.unwrap_or(true) || index.fetched(&variable_reference) { + return self.toggle_entry(&entry_id, cx); + } - let position = variables.iter().position(|v| { - variable_entry_id(&scope, &v.variable, v.depth) == variable_id - }); + let fetch_variables_task = self.dap_store.update(cx, |store, cx| { + store.variables(&self.client_id, variable_reference, cx) + }); - if let Some(position) = position { - variables.splice( - position + 1..position + 1, - new_variables - .clone() - .into_iter() - .map(|variable| VariableContainer { - container_reference: variable_reference, - variable, - depth: depth + 1, - }), - ); - - this.fetched_variable_ids.insert(variable_reference); - } + let entry_id = entry_id.clone(); + cx.spawn(|this, mut cx| async move { + let new_variables = fetch_variables_task.await?; - this.toggle_entry_collapsed(&variable_id, cx); - }) + this.update(&mut cx, |this, cx| { + let Some(index) = this.variables.get_mut(&(stack_frame_id, scope_id)) else { + return; + }; + + index.add_variables( + variable_reference, + new_variables + .into_iter() + .map(|variable| VariableContainer { + container_reference: variable_reference, + variable, + depth: depth + 1, + }) + .collect::>(), + ); + + this.toggle_entry(&entry_id, cx); }) - .detach_and_log_err(cx); - } + }) + .detach_and_log_err(cx); } #[allow(clippy::too_many_arguments)] fn render_variable( &self, - ix: usize, parent_variables_reference: u64, variable: &Variable, scope: &Scope, @@ -677,61 +723,68 @@ impl VariableList { has_children: bool, cx: &mut ViewContext, ) -> AnyElement { + let scope_id = scope.variables_reference; let variable_reference = variable.variables_reference; - let variable_id = variable_entry_id(scope, variable, depth); - let disclosed = has_children.then(|| { - self.open_entries - .binary_search(&variable_entry_id(scope, variable, depth)) - .is_ok() - }); + let entry_id = OpenEntry::Variable { + name: variable.name.clone(), + depth, + }; + let disclosed = has_children.then(|| self.open_entries.binary_search(&entry_id).is_ok()); div() - .id(variable_id.clone()) + .id(SharedString::from(format!( + "variable-{}-{}-{}", + scope.variables_reference, variable.name, depth + ))) .group("") .h_4() .size_full() .child( - ListItem::new(variable_id.clone()) - .indent_level(depth + 1) - .indent_step_size(px(20.)) - .always_show_disclosure_icon(true) - .toggle(disclosed) - .on_toggle(cx.listener(move |this, _, cx| { - this.on_toggle_variable( - ix, - &variable_id, - variable_reference, - has_children, - disclosed, + ListItem::new(SharedString::from(format!( + "variable-item-{}-{}-{}", + scope.variables_reference, variable.name, depth + ))) + .indent_level(depth + 1) + .indent_step_size(px(20.)) + .always_show_disclosure_icon(true) + .toggle(disclosed) + .on_toggle(cx.listener(move |this, _, cx| { + this.on_toggle_variable( + scope_id, + &entry_id, + variable_reference, + depth, + has_children, + disclosed, + cx, + ) + })) + .on_secondary_mouse_down(cx.listener({ + let scope = scope.clone(); + let variable = variable.clone(); + move |this, event: &MouseDownEvent, cx| { + this.deploy_variable_context_menu( + parent_variables_reference, + &scope, + &variable, + event.position, cx, ) - })) - .on_secondary_mouse_down(cx.listener({ - let scope = scope.clone(); - let variable = variable.clone(); - move |this, event: &MouseDownEvent, cx| { - this.deploy_variable_context_menu( - parent_variables_reference, - &scope, - &variable, - event.position, - cx, - ) - } - })) - .child( - h_flex() - .gap_1() - .text_ui_sm(cx) - .child(variable.name.clone()) - .child( - div() - .text_ui_xs(cx) - .text_color(cx.theme().colors().text_muted) - .child(variable.value.replace("\n", " ").clone()), - ), - ), + } + })) + .child( + h_flex() + .gap_1() + .text_ui_sm(cx) + .child(variable.name.clone()) + .child( + div() + .text_ui_xs(cx) + .text_color(cx.theme().colors().text_muted) + .child(variable.value.replace("\n", " ").clone()), + ), + ), ) .into_any() } @@ -739,8 +792,10 @@ impl VariableList { fn render_scope(&self, scope: &Scope, cx: &mut ViewContext) -> AnyElement { let element_id = scope.variables_reference; - let scope_id = scope_entry_id(scope); - let disclosed = self.open_entries.binary_search(&scope_id).is_ok(); + let entry_id = OpenEntry::Scope { + name: scope.name.clone(), + }; + let disclosed = self.open_entries.binary_search(&entry_id).is_ok(); div() .id(element_id as usize) @@ -749,15 +804,16 @@ impl VariableList { .w_full() .h_full() .child( - ListItem::new(scope_id.clone()) - .indent_level(1) - .indent_step_size(px(20.)) - .always_show_disclosure_icon(true) - .toggle(disclosed) - .on_toggle( - cx.listener(move |this, _, cx| this.toggle_entry_collapsed(&scope_id, cx)), - ) - .child(div().text_ui(cx).w_full().child(scope.name.clone())), + ListItem::new(SharedString::from(format!( + "scope-{}", + scope.variables_reference + ))) + .indent_level(1) + .indent_step_size(px(20.)) + .always_show_disclosure_icon(true) + .toggle(disclosed) + .on_toggle(cx.listener(move |this, _, cx| this.toggle_entry(&entry_id, cx))) + .child(div().text_ui(cx).w_full().child(scope.name.clone())), ) .into_any() } @@ -789,13 +845,184 @@ impl Render for VariableList { } } -pub fn variable_entry_id(scope: &Scope, variable: &Variable, depth: usize) -> SharedString { - SharedString::from(format!( - "variable-{}-{}-{}", - scope.variables_reference, variable.name, depth - )) -} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_initial_variables_to_index() { + let mut index = ScopeVariableIndex::new(); + + assert_eq!(index.variables(), &[]); + + let variable1 = VariableContainer { + variable: Variable { + name: "First variable".into(), + value: "First variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; -fn scope_entry_id(scope: &Scope) -> SharedString { - SharedString::from(format!("scope-{}", scope.variables_reference)) + let variable2 = VariableContainer { + variable: Variable { + name: "Second variable with child".into(), + value: "Second variable with child".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + let variable3 = VariableContainer { + variable: Variable { + name: "Third variable".into(), + value: "Third variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + index.add_variables( + 1, + vec![variable1.clone(), variable2.clone(), variable3.clone()], + ); + + assert_eq!( + index.variables(), + &[variable1.clone(), variable2.clone(), variable3.clone()] + ); + } + + /// This covers when you click on a variable that has a nested variable + /// We correctly insert the variables right after the variable you clicked on + #[test] + fn test_add_sub_variables_to_index() { + let mut index = ScopeVariableIndex::new(); + + assert_eq!(index.variables(), &[]); + + let variable1 = VariableContainer { + variable: Variable { + name: "First variable".into(), + value: "First variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + let variable2 = VariableContainer { + variable: Variable { + name: "Second variable with child".into(), + value: "Second variable with child".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + let variable3 = VariableContainer { + variable: Variable { + name: "Third variable".into(), + value: "Third variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + index.add_variables( + 1, + vec![variable1.clone(), variable2.clone(), variable3.clone()], + ); + + assert_eq!( + index.variables(), + &[variable1.clone(), variable2.clone(), variable3.clone()] + ); + + let variable4 = VariableContainer { + variable: Variable { + name: "Fourth variable".into(), + value: "Fourth variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + let variable5 = VariableContainer { + variable: Variable { + name: "Five variable".into(), + value: "Five variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + index.add_variables(2, vec![variable4.clone(), variable5.clone()]); + + assert_eq!( + index.variables(), + &[ + variable1.clone(), + variable2.clone(), + variable4.clone(), + variable5.clone(), + variable3.clone(), + ] + ); + } } From ddaf1508d3c9ffbeb8f73882abac9e4473c4b814 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 26 Oct 2024 17:00:13 +0200 Subject: [PATCH 313/650] Remove unused dependencies --- Cargo.lock | 4 ---- crates/dap_adapters/Cargo.toml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29842a905814a6..7bfff375b30591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3384,13 +3384,9 @@ dependencies = [ "anyhow", "async-trait", "dap", - "fs", - "futures 0.3.30", - "http_client", "paths", "serde", "serde_json", - "smol", "task", "util", ] diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 3e50c1eed82ff2..883e8cfe3f20bf 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -17,12 +17,8 @@ doctest = false anyhow.workspace = true async-trait.workspace = true dap.workspace = true -fs.workspace = true -futures.workspace = true -http_client.workspace = true paths.workspace = true serde.workspace = true serde_json.workspace = true -smol.workspace = true task.workspace = true util.workspace = true From d279afa41c61ba467cd64002837500c4dae7378e Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Sun, 27 Oct 2024 06:08:37 -0400 Subject: [PATCH 314/650] Show DAP status in activity bar (#54) * Begin integrating languages with DAP * Add dap status type to activity indicator Co-authored-by: Remco Smits * Show dap status to users * Change Status enum to use ServerStatus struct in activity indicator --------- Co-authored-by: Remco Smits --- .../src/activity_indicator.rs | 29 +++++++++++---- crates/dap/src/adapters.rs | 11 ++++++ crates/language/src/language_registry.rs | 36 +++++++++++++++++++ crates/project/src/dap_store.rs | 28 +++++++++++++-- crates/project/src/project.rs | 13 +++++-- crates/remote_server/src/headless_project.rs | 3 +- 6 files changed, 108 insertions(+), 12 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8020e0665a1af1..e2fb516f88f7b6 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -20,19 +20,19 @@ actions!(activity_indicator, [ShowErrorMessage]); pub enum Event { ShowError { - lsp_name: LanguageServerName, + server_name: LanguageServerName, error: String, }, } pub struct ActivityIndicator { - statuses: Vec, + statuses: Vec, project: Model, auto_updater: Option>, context_menu_handle: PopoverMenuHandle, } -struct LspStatus { +struct ServerStatus { name: LanguageServerName, status: LanguageServerBinaryStatus, } @@ -63,13 +63,27 @@ impl ActivityIndicator { while let Some((name, status)) = status_events.next().await { this.update(&mut cx, |this, cx| { this.statuses.retain(|s| s.name != name); - this.statuses.push(LspStatus { name, status }); + this.statuses.push(ServerStatus { name, status }); cx.notify(); })?; } anyhow::Ok(()) }) .detach(); + + let mut status_events = languages.dap_server_binary_statuses(); + cx.spawn(|this, mut cx| async move { + while let Some((name, status)) = status_events.next().await { + this.update(&mut cx, |this, cx| { + this.statuses.retain(|s| s.name != name); + this.statuses.push(ServerStatus { name, status }); + cx.notify(); + })?; + } + anyhow::Ok(()) + }) + .detach(); + cx.observe(&project, |_, _, cx| cx.notify()).detach(); if let Some(auto_updater) = auto_updater.as_ref() { @@ -85,7 +99,10 @@ impl ActivityIndicator { }); cx.subscribe(&this, move |_, _, event, cx| match event { - Event::ShowError { lsp_name, error } => { + Event::ShowError { + server_name: lsp_name, + error, + } => { let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx)); let project = project.clone(); let error = error.clone(); @@ -127,7 +144,7 @@ impl ActivityIndicator { self.statuses.retain(|status| { if let LanguageServerBinaryStatus::Failed { error } = &status.status { cx.emit(Event::ShowError { - lsp_name: status.name.clone(), + server_name: status.name.clone(), error: error.clone(), }); false diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 8b0d2154be95dc..691a20e7a52bb7 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -16,11 +16,20 @@ use std::{ }; use task::DebugAdapterConfig; +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DapStatus { + None, + CheckingForUpdate, + Downloading, + Failed { error: String }, +} + pub trait DapDelegate { fn http_client(&self) -> Option>; fn node_runtime(&self) -> Option; fn fs(&self) -> Arc; fn cached_binaries(&self) -> Arc>>; + fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus); } #[derive(PartialEq, Eq, Hash, Debug)] @@ -189,6 +198,7 @@ pub trait DebugAdapter: 'static + Send + Sync { } log::info!("Getting latest version of debug adapter {}", self.name()); + delegate.update_status(self.name(), DapStatus::CheckingForUpdate); let version = self.fetch_latest_adapter_version(delegate).await.ok(); let mut binary = self.get_installed_binary(delegate, config).await; @@ -209,6 +219,7 @@ pub trait DebugAdapter: 'static + Send + Sync { return Ok(binary); } + delegate.update_status(self.name(), DapStatus::Downloading); self.install_binary(version, delegate).await?; binary = self.get_installed_binary(delegate, config).await; } diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 880ae3b6115c37..c5c445ff4ed761 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -80,6 +80,7 @@ pub struct LanguageRegistry { language_server_download_dir: Option>, executor: BackgroundExecutor, lsp_binary_status_tx: LspBinaryStatusSender, + dap_binary_status_tx: DapBinaryStatusSender, } struct LanguageRegistryState { @@ -222,6 +223,7 @@ impl LanguageRegistry { }), language_server_download_dir: None, lsp_binary_status_tx: Default::default(), + dap_binary_status_tx: Default::default(), executor, }; this.add(PLAIN_TEXT.clone()); @@ -875,6 +877,14 @@ impl LanguageRegistry { self.lsp_binary_status_tx.send(server_name, status); } + pub fn update_dap_status( + &self, + server_name: LanguageServerName, + status: LanguageServerBinaryStatus, + ) { + self.dap_binary_status_tx.send(server_name, status); + } + pub fn next_language_server_id(&self) -> LanguageServerId { self.state.write().next_language_server_id() } @@ -930,6 +940,12 @@ impl LanguageRegistry { self.lsp_binary_status_tx.subscribe() } + pub fn dap_server_binary_statuses( + &self, + ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { + self.dap_binary_status_tx.subscribe() + } + pub async fn delete_server_container(&self, name: LanguageServerName) { log::info!("deleting server container"); let Some(dir) = self.language_server_download_dir(&name) else { @@ -1039,6 +1055,26 @@ impl LanguageRegistryState { } } +#[derive(Clone, Default)] +struct DapBinaryStatusSender { + txs: Arc>>>, +} + +impl DapBinaryStatusSender { + fn subscribe( + &self, + ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { + let (tx, rx) = mpsc::unbounded(); + self.txs.lock().push(tx); + rx + } + + fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) { + let mut txs = self.txs.lock(); + txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok()); + } +} + impl LspBinaryStatusSender { fn subscribe( &self, diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 159d69b7392146..9bca55c0075f2f 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,7 +1,7 @@ use crate::ProjectPath; use anyhow::{anyhow, Context as _, Result}; use collections::HashSet; -use dap::adapters::{DebugAdapterBinary, DebugAdapterName}; +use dap::adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::{Message, Response}; use dap::requests::{ @@ -23,9 +23,11 @@ use dap::{ }; use dap_adapters::build_adapter; use fs::Fs; -use gpui::{EventEmitter, Model, ModelContext, Task}; +use gpui::{EventEmitter, Model, ModelContext, SharedString, Task}; use http_client::HttpClient; -use language::{Buffer, BufferSnapshot}; +use language::{ + Buffer, BufferSnapshot, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName, +}; use node_runtime::NodeRuntime; use serde_json::{json, Value}; use settings::WorktreeId; @@ -73,6 +75,7 @@ pub struct DapStore { active_debug_line: Option<(ProjectPath, DebugPosition)>, http_client: Option>, node_runtime: Option, + languages: Arc, fs: Arc, } @@ -83,6 +86,7 @@ impl DapStore { http_client: Option>, node_runtime: Option, fs: Arc, + _languages: Arc, cx: &mut ModelContext, ) -> Self { cx.on_app_quit(Self::shutdown_clients).detach(); @@ -96,6 +100,7 @@ impl DapStore { next_client_id: Default::default(), http_client, node_runtime, + languages: _languages, fs, } } @@ -248,6 +253,7 @@ impl DapStore { self.node_runtime.clone(), self.fs.clone(), self.cached_binaries.clone(), + self.languages.clone(), ); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); @@ -1234,6 +1240,7 @@ pub struct DapAdapterDelegate { http_client: Option>, node_runtime: Option, cached_binaries: Arc>>, + languages: Arc, } impl DapAdapterDelegate { @@ -1242,12 +1249,14 @@ impl DapAdapterDelegate { node_runtime: Option, fs: Arc, cached_binaries: Arc>>, + languages: Arc, ) -> Self { Self { fs, http_client, node_runtime, cached_binaries, + languages, } } } @@ -1268,4 +1277,17 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { fn cached_binaries(&self) -> Arc>> { self.cached_binaries.clone() } + + fn update_status(&self, dap_name: DebugAdapterName, status: dap::adapters::DapStatus) { + let name = SharedString::from(dap_name.to_string()); + let status = match status { + DapStatus::None => LanguageServerBinaryStatus::None, + DapStatus::Downloading => LanguageServerBinaryStatus::Downloading, + DapStatus::Failed { error } => LanguageServerBinaryStatus::Failed { error }, + DapStatus::CheckingForUpdate => LanguageServerBinaryStatus::CheckingForUpdate, + }; + + self.languages + .update_dap_status(LanguageServerName(name), status); + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9f3411ceb338f5..ad0f9437c68c79 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -622,6 +622,7 @@ impl Project { Some(client.http_client()), Some(node.clone()), fs.clone(), + languages.clone(), cx, ) }); @@ -786,6 +787,7 @@ impl Project { Some(client.http_client()), Some(node.clone()), fs.clone(), + languages.clone(), cx, ) }); @@ -944,8 +946,15 @@ impl Project { BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx) })?; - let dap_store = - cx.new_model(|cx| DapStore::new(Some(client.http_client()), None, fs.clone(), cx))?; + let dap_store = cx.new_model(|cx| { + DapStore::new( + Some(client.http_client()), + None, + fs.clone(), + languages.clone(), + cx, + ) + })?; let lsp_store = cx.new_model(|cx| { let mut lsp_store = LspStore::new_remote( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index a523c8ca92cad1..c7a708b4e69777 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -73,7 +73,8 @@ impl HeadlessProject { store }); - let dap_store = cx.new_model(|cx| DapStore::new(None, None, fs.clone(), cx)); + let dap_store = + cx.new_model(|cx| DapStore::new(None, None, fs.clone(), languages.clone(), cx)); let buffer_store = cx.new_model(|cx| { let mut buffer_store = BufferStore::local(worktree_store.clone(), dap_store.clone(), cx); From f7eb5213a53774c615166313f147ca320df14700 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 27 Oct 2024 12:16:09 +0100 Subject: [PATCH 315/650] Allow users to configure `host`, `port` and `timeout` for build in TCP adapters (#56) * Remove space in cargo.toml * Add TCPHost for Javascript and PHP adapter * Allow falling back to first open port * Add some more docs to TCPHost * Fix cached binaries prevent from having multiple debug sessions for one adapters This was an issue, because we stored the port arg inside the DebugAdapterBinary which was cached so could not change anymore. Co-authored-by: Anthony Eid * Add default setting for tcp timeout Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --------- Co-authored-by: Anthony Eid Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/dap/src/adapters.rs | 37 +++++++--- crates/dap/src/debugger_settings.rs | 5 ++ crates/dap/src/transport.rs | 73 +++++++++++-------- crates/dap_adapters/Cargo.toml | 1 - crates/dap_adapters/src/custom.rs | 21 ++++-- crates/dap_adapters/src/dap_adapters.rs | 11 +-- crates/dap_adapters/src/javascript.rs | 25 ++++--- crates/dap_adapters/src/lldb.rs | 1 - crates/dap_adapters/src/php.rs | 28 ++++--- crates/dap_adapters/src/python.rs | 1 - crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 9 ++- crates/project/src/dap_store.rs | 22 +++--- crates/task/src/debug_format.rs | 32 +++++--- 14 files changed, 166 insertions(+), 102 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 691a20e7a52bb7..0ed5a7cd15bed5 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -28,7 +28,7 @@ pub trait DapDelegate { fn http_client(&self) -> Option>; fn node_runtime(&self) -> Option; fn fs(&self) -> Arc; - fn cached_binaries(&self) -> Arc>>; + fn updated_adapters(&self) -> Arc>>; fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus); } @@ -192,9 +192,15 @@ pub trait DebugAdapter: 'static + Send + Sync { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, ) -> Result { - if let Some(binary) = delegate.cached_binaries().lock().await.get(&self.name()) { + if delegate + .updated_adapters() + .lock() + .await + .contains(&self.name()) + { log::info!("Using cached debug adapter binary {}", self.name()); - return Ok(binary.clone()); + + return self.get_installed_binary(delegate, config).await; } log::info!("Getting latest version of debug adapter {}", self.name()); @@ -208,29 +214,40 @@ pub trait DebugAdapter: 'static + Send + Sync { .as_ref() .is_ok_and(|binary| binary.version == version.tag_name) { - let binary = binary?; - delegate - .cached_binaries() + .updated_adapters() .lock_arc() .await - .insert(self.name(), binary.clone()); + .insert(self.name()); - return Ok(binary); + return Ok(binary?); } delegate.update_status(self.name(), DapStatus::Downloading); self.install_binary(version, delegate).await?; binary = self.get_installed_binary(delegate, config).await; + } else { + log::error!( + "Failed getting latest version of debug adapter {}", + self.name() + ); } + if binary.is_err() { + delegate.update_status( + self.name(), + DapStatus::Failed { + error: format!("Failed to download {}", self.name()), + }, + ); + } let binary = binary?; delegate - .cached_binaries() + .updated_adapters() .lock_arc() .await - .insert(self.name(), binary.clone()); + .insert(self.name()); Ok(binary) } diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index 0d5a744a849365..4594a1785039e5 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -19,6 +19,10 @@ pub struct DebuggerSettings { /// /// Default: true pub button: bool, + /// Time in milliseconds until timeout error when connecting to a TCP debug adapter + /// + /// Default: 2000ms + pub timeout: u64, } impl Default for DebuggerSettings { @@ -27,6 +31,7 @@ impl Default for DebuggerSettings { button: true, save_breakpoints: true, stepping_granularity: SteppingGranularity::Line, + timeout: 2000, } } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index b78e1e5acc66ae..e68d1590c2a349 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -6,6 +6,7 @@ use dap_types::{ }; use futures::{select, AsyncBufRead, AsyncReadExt as _, AsyncWrite, FutureExt as _}; use gpui::AsyncAppContext; +use settings::Settings as _; use smallvec::SmallVec; use smol::{ channel::{unbounded, Receiver, Sender}, @@ -23,7 +24,7 @@ use std::{ }; use task::TCPHost; -use crate::adapters::DebugAdapterBinary; +use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; pub type IoHandler = Box; @@ -361,27 +362,36 @@ pub trait Transport: 'static + Send + Sync { ) -> Result; fn has_adapter_logs(&self) -> bool; + + fn clone_box(&self) -> Box; } +#[derive(Clone)] pub struct TcpTransport { - config: TCPHost, + port: u16, + host: Ipv4Addr, + timeout: Option, } impl TcpTransport { - pub fn new(config: TCPHost) -> Self { - Self { config } + pub fn new(host: Ipv4Addr, port: u16, timeout: Option) -> Self { + Self { + port, + host, + timeout, + } } /// Get an open port to use with the tcp client when not supplied by debug config - async fn get_open_port(host: Ipv4Addr) -> Option { - Some( - TcpListener::bind(SocketAddrV4::new(host, 0)) - .await - .ok()? - .local_addr() - .ok()? - .port(), - ) + pub async fn port(host: &TCPHost) -> Result { + if let Some(port) = host.port { + Ok(port) + } else { + Ok(TcpListener::bind(SocketAddrV4::new(host.host(), 0)) + .await? + .local_addr()? + .port()) + } } } @@ -392,16 +402,6 @@ impl Transport for TcpTransport { binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result { - let host_address = self - .config - .host - .unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); - - let mut port = self.config.port; - if port.is_none() { - port = Self::get_open_port(host_address).await; - } - let mut command = process::Command::new(&binary.command); if let Some(args) = &binary.arguments { @@ -422,16 +422,16 @@ impl Transport for TcpTransport { .spawn() .with_context(|| "failed to start debug adapter.")?; - let address = SocketAddrV4::new( - host_address, - port.ok_or(anyhow!("Port is required to connect to TCP server"))?, - ); + let address = SocketAddrV4::new(self.host, self.port); - let timeout = self.config.timeout.unwrap_or(2000); + let timeout = self.timeout.unwrap_or_else(|| { + cx.update(|cx| DebuggerSettings::get_global(cx).timeout) + .unwrap_or(2000u64) + }); let (rx, tx) = select! { _ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => { - return Err(anyhow!("Connection to tcp DAP timeout")) + return Err(anyhow!(format!("Connection to TCP DAP timeout {}:{}", self.host, self.port))) }, result = cx.spawn(|cx| async move { loop { @@ -444,7 +444,11 @@ impl Transport for TcpTransport { } }).fuse() => result }; - log::info!("Debug adapter has connected to tcp server"); + log::info!( + "Debug adapter has connected to TCP server {}:{}", + self.host, + self.port + ); Ok(TransportParams::new( Box::new(tx), @@ -456,8 +460,13 @@ impl Transport for TcpTransport { fn has_adapter_logs(&self) -> bool { true } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } +#[derive(Clone)] pub struct StdioTransport {} impl StdioTransport { @@ -514,4 +523,8 @@ impl Transport for StdioTransport { fn has_adapter_logs(&self) -> bool { false } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 883e8cfe3f20bf..73d5e8168f5a98 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" publish = false license = "GPL-3.0-or-later" - [lints] workspace = true diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 6da45ea1351660..8aa6a5612446cc 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -6,16 +6,26 @@ use task::DebugAdapterConfig; use crate::*; -#[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct CustomDebugAdapter { custom_args: CustomArgs, + transport: Box, } impl CustomDebugAdapter { const ADAPTER_NAME: &'static str = "custom_dap"; - pub(crate) fn new(custom_args: CustomArgs) -> Self { - CustomDebugAdapter { custom_args } + pub(crate) async fn new(custom_args: CustomArgs) -> Result { + Ok(CustomDebugAdapter { + transport: match &custom_args.connection { + DebugConnectionType::TCP(host) => Box::new(TcpTransport::new( + host.host(), + TcpTransport::port(&host).await?, + host.timeout, + )), + DebugConnectionType::STDIO => Box::new(StdioTransport::new()), + }, + custom_args, + }) } } @@ -26,10 +36,7 @@ impl DebugAdapter for CustomDebugAdapter { } fn transport(&self) -> Box { - match &self.custom_args.connection { - DebugConnectionType::STDIO => Box::new(StdioTransport::new()), - DebugConnectionType::TCP(tcp_host) => Box::new(TcpTransport::new(tcp_host.clone())), - } + self.transport.clone_box() } async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index e099f92430fec9..2b4caa771d0c1e 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -16,17 +16,18 @@ use lldb::LldbDebugAdapter; use php::PhpDebugAdapter; use python::PythonDebugAdapter; use serde_json::{json, Value}; -use std::fmt::Debug; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; -pub fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { +pub async fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { match &adapter_config.kind { DebugAdapterKind::Custom(start_args) => { - Ok(Box::new(CustomDebugAdapter::new(start_args.clone()))) + Ok(Box::new(CustomDebugAdapter::new(start_args.clone()).await?)) } DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new())), - DebugAdapterKind::PHP => Ok(Box::new(PhpDebugAdapter::new())), - DebugAdapterKind::Javascript => Ok(Box::new(JsDebugAdapter::new())), + DebugAdapterKind::PHP(host) => Ok(Box::new(PhpDebugAdapter::new(host.clone()).await?)), + DebugAdapterKind::Javascript(host) => { + Ok(Box::new(JsDebugAdapter::new(host.clone()).await?)) + } DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new())), } } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 2c59a9ae2cbd59..4b47243b43fa57 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,16 +1,25 @@ +use std::net::Ipv4Addr; + use dap::transport::{TcpTransport, Transport}; use crate::*; -#[derive(Debug, Eq, PartialEq, Clone)] -pub(crate) struct JsDebugAdapter {} +pub(crate) struct JsDebugAdapter { + port: u16, + host: Ipv4Addr, + timeout: Option, +} impl JsDebugAdapter { const ADAPTER_NAME: &'static str = "vscode-js-debug"; const ADAPTER_PATH: &'static str = "src/dapDebugServer.js"; - pub(crate) fn new() -> Self { - JsDebugAdapter {} + pub(crate) async fn new(host: TCPHost) -> Result { + Ok(JsDebugAdapter { + host: host.host(), + timeout: host.timeout, + port: TcpTransport::port(&host).await?, + }) } } @@ -21,11 +30,7 @@ impl DebugAdapter for JsDebugAdapter { } fn transport(&self) -> Box { - Box::new(TcpTransport::new(TCPHost { - port: Some(8133), - host: None, - timeout: None, - })) + Box::new(TcpTransport::new(self.host, self.port, self.timeout)) } async fn fetch_latest_adapter_version( @@ -73,7 +78,7 @@ impl DebugAdapter for JsDebugAdapter { .into_owned(), arguments: Some(vec![ adapter_path.join(Self::ADAPTER_PATH).into(), - "8133".into(), + self.port.to_string().into(), ]), envs: None, version, diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 10ff48638cb2cf..47dfd3a25f43d8 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -5,7 +5,6 @@ use task::DebugAdapterConfig; use crate::*; -#[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct LldbDebugAdapter {} impl LldbDebugAdapter { diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index a5aa647822a4d4..4e5ccb92d08c9f 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,16 +1,24 @@ use dap::transport::{TcpTransport, Transport}; +use std::net::Ipv4Addr; use crate::*; -#[derive(Debug, Eq, PartialEq, Clone)] -pub(crate) struct PhpDebugAdapter {} +pub(crate) struct PhpDebugAdapter { + port: u16, + host: Ipv4Addr, + timeout: Option, +} impl PhpDebugAdapter { const ADAPTER_NAME: &'static str = "vscode-php-debug"; const ADAPTER_PATH: &'static str = "out/phpDebug.js"; - pub(crate) fn new() -> Self { - PhpDebugAdapter {} + pub(crate) async fn new(host: TCPHost) -> Result { + Ok(PhpDebugAdapter { + port: TcpTransport::port(&host).await?, + host: host.host(), + timeout: host.timeout, + }) } } @@ -21,11 +29,7 @@ impl DebugAdapter for PhpDebugAdapter { } fn transport(&self) -> Box { - Box::new(TcpTransport::new(TCPHost { - port: Some(8132), - host: None, - timeout: None, - })) + Box::new(TcpTransport::new(self.host, self.port, self.timeout)) } async fn fetch_latest_adapter_version( @@ -34,7 +38,7 @@ impl DebugAdapter for PhpDebugAdapter { ) -> Result { let github_repo = GithubRepo { repo_name: Self::ADAPTER_NAME.into(), - repo_owner: "xdebug".to_string(), + repo_owner: "xdebug".into(), }; adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await @@ -62,7 +66,7 @@ impl DebugAdapter for PhpDebugAdapter { .file_name() .and_then(|file_name| file_name.to_str()) .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("Php debug adapter has invalid file name"))? + .ok_or_else(|| anyhow!("PHP debug adapter has invalid file name"))? .to_string(); Ok(DebugAdapterBinary { @@ -73,7 +77,7 @@ impl DebugAdapter for PhpDebugAdapter { .into_owned(), arguments: Some(vec![ adapter_path.join(Self::ADAPTER_PATH).into(), - "--server=8132".into(), + format!("--server={}", self.port).into(), ]), envs: None, version, diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 136b95c0972776..2a3a6cb2ded439 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -2,7 +2,6 @@ use dap::transport::{StdioTransport, Transport}; use crate::*; -#[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct PythonDebugAdapter {} impl PythonDebugAdapter { diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 74622d3d03e51c..5f907c3ec0dbd0 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -520,7 +520,7 @@ impl DapLogView { .debug_clients(cx) .map(|client| DapMenuItem { client_id: client.id(), - client_name: client.config().kind.to_string(), + client_name: client.config().kind.diplay_name().into(), selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), has_adapter_logs: client.has_adapter_logs(), }) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 853255fb0bb3a7..4e5eb1fefcf879 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -496,8 +496,9 @@ impl Item for DebugPanelItem { _: &WindowContext, ) -> AnyElement { Label::new(format!( - "{:?} - Thread {}", - self.client_kind, self.thread_id + "{} - Thread {}", + self.client_kind.diplay_name(), + self.thread_id )) .color(if params.selected { Color::Default @@ -509,8 +510,8 @@ impl Item for DebugPanelItem { fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(SharedString::from(format!( - "{:?} Thread {} - {:?}", - self.client_kind, + "{} Thread {} - {:?}", + self.client_kind.diplay_name(), self.thread_id, self.thread_state.read(cx).status, ))) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 9bca55c0075f2f..e80c50be6d8607 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,7 +1,6 @@ use crate::ProjectPath; use anyhow::{anyhow, Context as _, Result}; -use collections::HashSet; -use dap::adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName}; +use dap::adapters::{DapStatus, DebugAdapterName}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::{Message, Response}; use dap::requests::{ @@ -33,7 +32,7 @@ use serde_json::{json, Value}; use settings::WorktreeId; use smol::lock::Mutex; use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, hash::{Hash, Hasher}, path::{Path, PathBuf}, sync::{ @@ -69,7 +68,7 @@ pub struct DebugPosition { pub struct DapStore { next_client_id: AtomicUsize, clients: HashMap, - cached_binaries: Arc>>, + updated_adapters: Arc>>, breakpoints: BTreeMap>, capabilities: HashMap, active_debug_line: Option<(ProjectPath, DebugPosition)>, @@ -94,7 +93,7 @@ impl DapStore { Self { active_debug_line: None, clients: Default::default(), - cached_binaries: Default::default(), + updated_adapters: Default::default(), breakpoints: Default::default(), capabilities: HashMap::default(), next_client_id: Default::default(), @@ -252,13 +251,14 @@ impl DapStore { self.http_client.clone(), self.node_runtime.clone(), self.fs.clone(), - self.cached_binaries.clone(), + self.updated_adapters.clone(), self.languages.clone(), ); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); let adapter = Arc::new( build_adapter(&config) + .await .context("Creating debug adapter") .log_err()?, ); @@ -1239,7 +1239,7 @@ pub struct DapAdapterDelegate { fs: Arc, http_client: Option>, node_runtime: Option, - cached_binaries: Arc>>, + udpated_adapters: Arc>>, languages: Arc, } @@ -1248,14 +1248,14 @@ impl DapAdapterDelegate { http_client: Option>, node_runtime: Option, fs: Arc, - cached_binaries: Arc>>, + udpated_adapters: Arc>>, languages: Arc, ) -> Self { Self { fs, http_client, node_runtime, - cached_binaries, + udpated_adapters, languages, } } @@ -1274,8 +1274,8 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { self.fs.clone() } - fn cached_binaries(&self) -> Arc>> { - self.cached_binaries.clone() + fn updated_adapters(&self) -> Arc>> { + self.udpated_adapters.clone() } fn update_status(&self, dap_name: DebugAdapterName, status: dap::adapters::DapStatus) { diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 6aef1745c2e3de..62dafa2e358dcc 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -16,13 +16,26 @@ impl Default for DebugConnectionType { #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] pub struct TCPHost { /// The port that the debug adapter is listening on + /// + /// Default: We will try to find an open port pub port: Option, /// The host that the debug adapter is listening too + /// + /// Default: 127.0.0.1 pub host: Option, - /// The max amount of time to connect to a tcp DAP before returning an error + /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error + /// + /// Default: 2000ms pub timeout: Option, } +impl TCPHost { + /// Get the host or fallback to the default host + pub fn host(&self) -> Ipv4Addr { + self.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)) + } +} + /// Represents the type that will determine which request to call on the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "lowercase")] @@ -44,22 +57,23 @@ pub enum DebugAdapterKind { /// Use debugpy Python, /// Use vscode-php-debug - PHP, + PHP(TCPHost), /// Use vscode-js-debug - Javascript, + Javascript(TCPHost), /// Use lldb Lldb, } -impl std::fmt::Display for DebugAdapterKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { +impl DebugAdapterKind { + /// Returns the display name for the adapter kind + pub fn diplay_name(&self) -> &str { + match self { Self::Custom(_) => "Custom", Self::Python => "Python", - Self::PHP => "PHP", - Self::Javascript => "JavaScript", + Self::PHP(_) => "PHP", + Self::Javascript(_) => "JavaScript", Self::Lldb => "LLDB", - }) + } } } From f6556c5bb63536beebaa2aed47d9ac80135aa9a4 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 27 Oct 2024 07:29:21 -0400 Subject: [PATCH 316/650] Show users error notices when dap fails to start Co-authored-by: Remco Smits --- crates/dap/src/adapters.rs | 2 +- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 4 +- crates/project/src/dap_store.rs | 88 +++++++++++-------- crates/task/src/debug_format.rs | 2 +- 5 files changed, 54 insertions(+), 44 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 0ed5a7cd15bed5..e2fe415a8f3544 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -220,7 +220,7 @@ pub trait DebugAdapter: 'static + Send + Sync { .await .insert(self.name()); - return Ok(binary?); + return binary; } delegate.update_status(self.name(), DapStatus::Downloading); diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 5f907c3ec0dbd0..076e0dfc964468 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -520,7 +520,7 @@ impl DapLogView { .debug_clients(cx) .map(|client| DapMenuItem { client_id: client.id(), - client_name: client.config().kind.diplay_name().into(), + client_name: client.config().kind.display_name().into(), selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), has_adapter_logs: client.has_adapter_logs(), }) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 4e5eb1fefcf879..22aa393aa3780b 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -497,7 +497,7 @@ impl Item for DebugPanelItem { ) -> AnyElement { Label::new(format!( "{} - Thread {}", - self.client_kind.diplay_name(), + self.client_kind.display_name(), self.thread_id )) .color(if params.selected { @@ -511,7 +511,7 @@ impl Item for DebugPanelItem { fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(SharedString::from(format!( "{} Thread {} - {:?}", - self.client_kind.diplay_name(), + self.client_kind.display_name(), self.thread_id, self.thread_state.read(cx).status, ))) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e80c50be6d8607..992c6f125c48d7 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -42,7 +42,7 @@ use std::{ }; use task::{DebugAdapterConfig, DebugRequestType}; use text::Point; -use util::{merge_json_value_into, ResultExt}; +use util::{maybe, merge_json_value_into, ResultExt}; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), @@ -256,47 +256,57 @@ impl DapStore { ); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); - let adapter = Arc::new( - build_adapter(&config) - .await - .context("Creating debug adapter") - .log_err()?, - ); + let client = maybe!(async { + let adapter = Arc::new( + build_adapter(&config) + .await + .context("Creating debug adapter")?, + ); + + let binary = adapter.get_binary(&adapter_delegate, &config).await?; + + let mut request_args = json!({}); + if let Some(config_args) = config.initialize_args.clone() { + merge_json_value_into(config_args, &mut request_args); + } - let binary = adapter - .get_binary(&adapter_delegate, &config) - .await - .log_err()?; + merge_json_value_into(adapter.request_args(&config), &mut request_args); - let mut request_args = json!({}); - if let Some(config_args) = config.initialize_args.clone() { - merge_json_value_into(config_args, &mut request_args); - } + if let Some(args) = args { + merge_json_value_into(args.configuration, &mut request_args); + } - merge_json_value_into(adapter.request_args(&config), &mut request_args); + let mut client = DebugAdapterClient::new(client_id, request_args, config, adapter); - if let Some(args) = args { - merge_json_value_into(args.configuration, &mut request_args); - } + client + .start( + &binary, + move |message, cx| { + dap_store + .update(cx, |_, cx| { + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + }) + .log_err(); + }, + &mut cx, + ) + .await?; - let mut client = DebugAdapterClient::new(client_id, request_args, config, adapter); + anyhow::Ok(client) + }) + .await; - client - .start( - &binary, - move |message, cx| { - dap_store - .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) - }) - .log_err(); - }, - &mut cx, - ) - .await - .log_err()?; + let client = match client { + Err(error) => { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + .log_err()?; - let client = Arc::new(client); + return None; + } + Ok(client) => Arc::new(client), + }; this.update(&mut cx, |store, cx| { let handle = store @@ -1239,7 +1249,7 @@ pub struct DapAdapterDelegate { fs: Arc, http_client: Option>, node_runtime: Option, - udpated_adapters: Arc>>, + updated_adapters: Arc>>, languages: Arc, } @@ -1248,14 +1258,14 @@ impl DapAdapterDelegate { http_client: Option>, node_runtime: Option, fs: Arc, - udpated_adapters: Arc>>, + updated_adapters: Arc>>, languages: Arc, ) -> Self { Self { fs, http_client, node_runtime, - udpated_adapters, + updated_adapters, languages, } } @@ -1275,7 +1285,7 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { } fn updated_adapters(&self) -> Arc>> { - self.udpated_adapters.clone() + self.updated_adapters.clone() } fn update_status(&self, dap_name: DebugAdapterName, status: dap::adapters::DapStatus) { diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 62dafa2e358dcc..8b1865925b9ee5 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -66,7 +66,7 @@ pub enum DebugAdapterKind { impl DebugAdapterKind { /// Returns the display name for the adapter kind - pub fn diplay_name(&self) -> &str { + pub fn display_name(&self) -> &str { match self { Self::Custom(_) => "Custom", Self::Python => "Python", From 0c647ae07171959a4bcfad7dc2ee97d4c9757d80 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 27 Oct 2024 07:41:52 -0400 Subject: [PATCH 317/650] Switch debugpy to use TCP instead of STDIO Co-authored-by: Remco Smits --- crates/dap_adapters/src/dap_adapters.rs | 2 +- crates/dap_adapters/src/python.rs | 25 +++++++++++++++++++------ crates/task/src/debug_format.rs | 4 ++-- crates/task/src/task_template.rs | 13 ++++++------- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 2b4caa771d0c1e..fe18c83eb553e3 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -23,7 +23,7 @@ pub async fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result { Ok(Box::new(CustomDebugAdapter::new(start_args.clone()).await?)) } - DebugAdapterKind::Python => Ok(Box::new(PythonDebugAdapter::new())), + DebugAdapterKind::Python(host) => Ok(Box::new(PythonDebugAdapter::new(host).await?)), DebugAdapterKind::PHP(host) => Ok(Box::new(PhpDebugAdapter::new(host.clone()).await?)), DebugAdapterKind::Javascript(host) => { Ok(Box::new(JsDebugAdapter::new(host.clone()).await?)) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 2a3a6cb2ded439..f2f2c942e5edab 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,15 +1,24 @@ -use dap::transport::{StdioTransport, Transport}; +use dap::transport::{TcpTransport, Transport}; +use std::net::Ipv4Addr; use crate::*; -pub(crate) struct PythonDebugAdapter {} +pub(crate) struct PythonDebugAdapter { + port: u16, + host: Ipv4Addr, + timeout: Option, +} impl PythonDebugAdapter { const ADAPTER_NAME: &'static str = "debugpy"; const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; - pub(crate) fn new() -> Self { - PythonDebugAdapter {} + pub(crate) async fn new(host: &TCPHost) -> Result { + Ok(PythonDebugAdapter { + port: TcpTransport::port(host).await?, + host: host.host(), + timeout: host.timeout, + }) } } @@ -20,7 +29,7 @@ impl DebugAdapter for PythonDebugAdapter { } fn transport(&self) -> Box { - Box::new(StdioTransport::new()) + Box::new(TcpTransport::new(self.host, self.port, self.timeout)) } async fn fetch_latest_adapter_version( @@ -67,7 +76,11 @@ impl DebugAdapter for PythonDebugAdapter { Ok(DebugAdapterBinary { command: "python3".to_string(), - arguments: Some(vec![debugpy_dir.join(Self::ADAPTER_PATH).into()]), + arguments: Some(vec![ + debugpy_dir.join(Self::ADAPTER_PATH).into(), + format!("--port={}", self.port).into(), + format!("--host={}", self.host).into(), + ]), envs: None, version, }) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 8b1865925b9ee5..80823bce128a48 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -55,7 +55,7 @@ pub enum DebugAdapterKind { /// The argument within is used to start the DAP Custom(CustomArgs), /// Use debugpy - Python, + Python(TCPHost), /// Use vscode-php-debug PHP(TCPHost), /// Use vscode-js-debug @@ -69,7 +69,7 @@ impl DebugAdapterKind { pub fn display_name(&self) -> &str { match self { Self::Custom(_) => "Custom", - Self::Python => "Python", + Self::Python(_) => "Python", Self::PHP(_) => "PHP", Self::Javascript(_) => "JavaScript", Self::Lldb => "LLDB", diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 4b5d2e78073cd8..7879c105ff3965 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -75,7 +75,7 @@ pub enum TaskType { #[cfg(test)] mod deserialization_tests { - use crate::DebugAdapterKind; + use crate::{DebugAdapterKind, TCPHost}; use super::*; use serde_json::json; @@ -92,17 +92,16 @@ mod deserialization_tests { #[test] fn deserialize_task_type_debug() { let adapter_config = DebugAdapterConfig { - kind: DebugAdapterKind::Python, + kind: DebugAdapterKind::Python(TCPHost::default()), request: crate::DebugRequestType::Launch, program: Some("main".to_string()), initialize_args: None, }; let json = json!({ - "type": "debug", - "kind": "python", - "request": "launch", - "program": "main" - + "type": "debug", + "kind": "python", + "request": "launch", + "program": "main" }); let task_type: TaskType = From aa9875277a29b96733abcf08824e2aeddc1db229 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 27 Oct 2024 08:05:19 -0400 Subject: [PATCH 318/650] Fix dap update status not going away Co-authored-by: Remco Smits --- crates/dap/src/adapters.rs | 8 ----- crates/project/src/dap_store.rs | 57 ++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index e2fe415a8f3544..92884cbfed6408 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -233,14 +233,6 @@ pub trait DebugAdapter: 'static + Send + Sync { ); } - if binary.is_err() { - delegate.update_status( - self.name(), - DapStatus::Failed { - error: format!("Failed to download {}", self.name()), - }, - ); - } let binary = binary?; delegate diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 992c6f125c48d7..60549e53a74237 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,6 +1,6 @@ use crate::ProjectPath; use anyhow::{anyhow, Context as _, Result}; -use dap::adapters::{DapStatus, DebugAdapterName}; +use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::{Message, Response}; use dap::requests::{ @@ -67,15 +67,11 @@ pub struct DebugPosition { pub struct DapStore { next_client_id: AtomicUsize, - clients: HashMap, - updated_adapters: Arc>>, + delegate: Arc, breakpoints: BTreeMap>, - capabilities: HashMap, active_debug_line: Option<(ProjectPath, DebugPosition)>, - http_client: Option>, - node_runtime: Option, - languages: Arc, - fs: Arc, + capabilities: HashMap, + clients: HashMap, } impl EventEmitter for DapStore {} @@ -85,7 +81,7 @@ impl DapStore { http_client: Option>, node_runtime: Option, fs: Arc, - _languages: Arc, + languages: Arc, cx: &mut ModelContext, ) -> Self { cx.on_app_quit(Self::shutdown_clients).detach(); @@ -93,14 +89,15 @@ impl DapStore { Self { active_debug_line: None, clients: Default::default(), - updated_adapters: Default::default(), breakpoints: Default::default(), capabilities: HashMap::default(), next_client_id: Default::default(), - http_client, - node_runtime, - languages: _languages, - fs, + delegate: Arc::new(DapAdapterDelegate::new( + http_client.clone(), + node_runtime.clone(), + fs.clone(), + languages.clone(), + )), } } @@ -247,13 +244,7 @@ impl DapStore { cx: &mut ModelContext, ) { let client_id = self.next_client_id(); - let adapter_delegate = DapAdapterDelegate::new( - self.http_client.clone(), - self.node_runtime.clone(), - self.fs.clone(), - self.updated_adapters.clone(), - self.languages.clone(), - ); + let adapter_delegate = self.delegate.clone(); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); let client = maybe!(async { @@ -263,7 +254,23 @@ impl DapStore { .context("Creating debug adapter")?, ); - let binary = adapter.get_binary(&adapter_delegate, &config).await?; + let binary = match adapter.get_binary(adapter_delegate.as_ref(), &config).await { + Err(error) => { + adapter_delegate.update_status( + adapter.name(), + DapStatus::Failed { + error: error.to_string(), + }, + ); + + return Err(error); + } + Ok(binary) => { + adapter_delegate.update_status(adapter.name(), DapStatus::None); + + binary + } + }; let mut request_args = json!({}); if let Some(config_args) = config.initialize_args.clone() { @@ -1245,6 +1252,7 @@ impl SerializedBreakpoint { } } +#[derive(Clone)] pub struct DapAdapterDelegate { fs: Arc, http_client: Option>, @@ -1258,15 +1266,14 @@ impl DapAdapterDelegate { http_client: Option>, node_runtime: Option, fs: Arc, - updated_adapters: Arc>>, languages: Arc, ) -> Self { Self { fs, + languages, http_client, node_runtime, - updated_adapters, - languages, + updated_adapters: Default::default(), } } } From 747ef3e7ecfc070f82e115f081c6b5f2dd82b566 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 27 Oct 2024 08:35:51 -0400 Subject: [PATCH 319/650] Show debugger actions only during active sessions Co-authored-by: Remco Smits --- Cargo.lock | 1 + crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/debugger_panel.rs | 37 ++++++++++++++++++++++-- crates/project/src/project.rs | 5 ++-- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bfff375b30591..d5f0c7cad927a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3483,6 +3483,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "command_palette_hooks", "dap", "editor", "futures 0.3.30", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index ab52ad8eee06b8..370b1e1595c57d 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] anyhow.workspace = true +command_palette_hooks.workspace = true collections.workspace = true dap.workspace = true editor.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 37563ad4911469..8fe9ccb2d741e9 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,6 +1,7 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use collections::{BTreeMap, HashMap}; +use command_palette_hooks::CommandPaletteFilter; use dap::client::{DebugAdapterClientId, ThreadStatus}; use dap::debugger_settings::DebuggerSettings; use dap::messages::{Events, Message}; @@ -18,6 +19,7 @@ use project::dap_store::DapStore; use project::terminals::TerminalKind; use serde_json::Value; use settings::Settings; +use std::any::TypeId; use std::path::PathBuf; use std::u64; use terminal_view::terminal_panel::TerminalPanel; @@ -26,7 +28,9 @@ use workspace::{ dock::{DockPosition, Panel, PanelEvent}, Workspace, }; -use workspace::{pane, Pane, Start}; +use workspace::{ + pane, Continue, Disconnect, Pane, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, +}; pub enum DebugPanelEvent { Exited(DebugAdapterClientId), @@ -148,7 +152,36 @@ impl DebugPanel { cx: AsyncWindowContext, ) -> Task>> { cx.spawn(|mut cx| async move { - workspace.update(&mut cx, |workspace, cx| DebugPanel::new(workspace, cx)) + workspace.update(&mut cx, |workspace, cx| { + let debug_panel = DebugPanel::new(workspace, cx); + + cx.observe(&debug_panel, |_, debug_panel, cx| { + let has_active_session = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx).is_some()); + + let filter = CommandPaletteFilter::global_mut(cx); + let debugger_action_types = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + + if has_active_session { + filter.show_action_types(debugger_action_types.iter()); + } else { + // show only the `debug: start` + filter.hide_action_types(&debugger_action_types); + } + }) + .detach(); + + debug_panel + }) }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ad0f9437c68c79..282895a6066779 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2296,10 +2296,11 @@ impl Project { ) { match event { DapStoreEvent::DebugClientStarted(client_id) => { + cx.emit(Event::DebugClientStarted(*client_id)); + self.dap_store.update(cx, |store, cx| { - store.initialize(client_id, cx).detach_and_log_err(cx) + store.initialize(client_id, cx).detach_and_log_err(cx); }); - cx.emit(Event::DebugClientStarted(*client_id)); } DapStoreEvent::DebugClientStopped(client_id) => { cx.emit(Event::DebugClientStopped(*client_id)); From 15535d3cec123b64f40237e49b23c3639a6856f5 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 27 Oct 2024 17:21:45 +0100 Subject: [PATCH 320/650] Don't try to install a adapter that does not support it --- crates/dap_adapters/src/custom.rs | 28 ++++++++++++++++--------- crates/dap_adapters/src/lldb.rs | 34 +++++++++++++++++++------------ 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 8aa6a5612446cc..5336b3b394248e 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -39,15 +39,7 @@ impl DebugAdapter for CustomDebugAdapter { self.transport.clone_box() } - async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { - bail!("Custom debug adapters don't have latest versions") - } - - async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> { - Ok(()) - } - - async fn get_installed_binary( + async fn get_binary( &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, @@ -60,10 +52,26 @@ impl DebugAdapter for CustomDebugAdapter { .clone() .map(|args| args.iter().map(OsString::from).collect()), envs: self.custom_args.envs.clone(), - version: "Custom daps".to_string(), + version: "Custom Debug Adapter".to_string(), }) } + async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { + bail!("Custom debug adapters don't have latest versions") + } + + async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> { + bail!("Custom debug adapters cannot be installed") + } + + async fn get_installed_binary( + &self, + _: &dyn DapDelegate, + _: &DebugAdapterConfig, + ) -> Result { + bail!("Custom debug adapters cannot be installed") + } + fn request_args(&self, config: &DebugAdapterConfig) -> Value { json!({"program": config.program}) } diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 47dfd3a25f43d8..5e150c6321a10f 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -25,19 +25,7 @@ impl DebugAdapter for LldbDebugAdapter { Box::new(StdioTransport::new()) } - async fn install_binary( - &self, - _version: AdapterVersion, - _delegate: &dyn DapDelegate, - ) -> Result<()> { - bail!("Install binary is not support for install_binary (yet)") - } - - async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { - bail!("Fetch latest adapter version not implemented for lldb (yet)") - } - - async fn get_installed_binary( + async fn get_binary( &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, @@ -64,6 +52,26 @@ impl DebugAdapter for LldbDebugAdapter { } } + async fn install_binary( + &self, + _version: AdapterVersion, + _delegate: &dyn DapDelegate, + ) -> Result<()> { + bail!("LLDB debug adapter cannot be installed") + } + + async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { + bail!("Fetch latest adapter version not implemented for lldb (yet)") + } + + async fn get_installed_binary( + &self, + _: &dyn DapDelegate, + _: &DebugAdapterConfig, + ) -> Result { + bail!("LLDB debug adapter cannot be installed") + } + fn request_args(&self, config: &DebugAdapterConfig) -> Value { json!({"program": config.program}) } From 5efdaf3ca7dac01c41eab536ba0694c22e724c7e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 1 Nov 2024 17:13:52 -0400 Subject: [PATCH 321/650] Allow users to set debug adapter path to use from settings --- crates/dap/src/adapters.rs | 46 +++++++++++++++++++++++-- crates/dap_adapters/src/custom.rs | 2 ++ crates/dap_adapters/src/dap_adapters.rs | 1 + crates/dap_adapters/src/javascript.rs | 38 +++++++++++++------- crates/dap_adapters/src/lldb.rs | 2 ++ crates/dap_adapters/src/php.rs | 35 +++++++++++++------ crates/dap_adapters/src/python.rs | 37 +++++++++++++------- crates/project/src/dap_store.rs | 18 ++++++++-- crates/project/src/project_settings.rs | 9 +++++ 9 files changed, 147 insertions(+), 41 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 92884cbfed6408..6ca6f4e92f0b6b 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -2,6 +2,7 @@ use crate::transport::Transport; use ::fs::Fs; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; +use gpui::SharedString; use http_client::{github::latest_github_release, HttpClient}; use node_runtime::NodeRuntime; use serde_json::Value; @@ -61,6 +62,12 @@ impl std::fmt::Display for DebugAdapterName { } } +impl From for SharedString { + fn from(name: DebugAdapterName) -> Self { + SharedString::from(name.0) + } +} + #[derive(Debug, Clone)] pub struct DebugAdapterBinary { pub command: String, @@ -191,7 +198,39 @@ pub trait DebugAdapter: 'static + Send + Sync { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, + adapter_path: Option, ) -> Result { + if let Some(adapter_path) = adapter_path { + if adapter_path.exists() { + log::info!( + "Using adapter path from settings\n debug adapter name: {}\n adapter_path: {:?}", + self.name(), + &adapter_path, + ); + + let binary = self + .get_installed_binary(delegate, config, Some(adapter_path)) + .await; + + if binary.is_ok() { + return binary; + } else { + log::info!( + "Failed to get debug adapter path from user's setting.\n adapter_name: {}", + self.name() + ); + } + } else { + log::warn!( + r#"User downloaded adapter path does not exist + Debug Adapter: {}, + User Adapter Path: {:?}"#, + self.name(), + &adapter_path + ) + } + } + if delegate .updated_adapters() .lock() @@ -200,14 +239,14 @@ pub trait DebugAdapter: 'static + Send + Sync { { log::info!("Using cached debug adapter binary {}", self.name()); - return self.get_installed_binary(delegate, config).await; + return self.get_installed_binary(delegate, config, None).await; } log::info!("Getting latest version of debug adapter {}", self.name()); delegate.update_status(self.name(), DapStatus::CheckingForUpdate); let version = self.fetch_latest_adapter_version(delegate).await.ok(); - let mut binary = self.get_installed_binary(delegate, config).await; + let mut binary = self.get_installed_binary(delegate, config, None).await; if let Some(version) = version { if binary @@ -225,7 +264,7 @@ pub trait DebugAdapter: 'static + Send + Sync { delegate.update_status(self.name(), DapStatus::Downloading); self.install_binary(version, delegate).await?; - binary = self.get_installed_binary(delegate, config).await; + binary = self.get_installed_binary(delegate, config, None).await; } else { log::error!( "Failed getting latest version of debug adapter {}", @@ -264,6 +303,7 @@ pub trait DebugAdapter: 'static + Send + Sync { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, + user_installed_path: Option, ) -> Result; /// Should return base configuration to make the debug adapter work diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 5336b3b394248e..279bb8d05d5033 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -43,6 +43,7 @@ impl DebugAdapter for CustomDebugAdapter { &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, + _: Option, ) -> Result { Ok(DebugAdapterBinary { command: self.custom_args.command.clone(), @@ -68,6 +69,7 @@ impl DebugAdapter for CustomDebugAdapter { &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, + _: Option, ) -> Result { bail!("Custom debug adapters cannot be installed") } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index fe18c83eb553e3..9a305d4a7b142c 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -16,6 +16,7 @@ use lldb::LldbDebugAdapter; use php::PhpDebugAdapter; use python::PythonDebugAdapter; use serde_json::{json, Value}; +use std::path::PathBuf; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; pub async fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 4b47243b43fa57..1581b49fa005ea 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,6 +1,6 @@ -use std::net::Ipv4Addr; - use dap::transport::{TcpTransport, Transport}; +use std::net::Ipv4Addr; +use util::maybe; use crate::*; @@ -49,6 +49,7 @@ impl DebugAdapter for JsDebugAdapter { &self, delegate: &dyn DapDelegate, _: &DebugAdapterConfig, + user_installed_path: Option, ) -> Result { let node_runtime = delegate .node_runtime() @@ -57,18 +58,29 @@ impl DebugAdapter for JsDebugAdapter { let adapter_path = paths::debug_adapters_dir().join(self.name()); let file_name_prefix = format!("{}_", self.name()); - let adapter_path = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) + let adapter_info: Result<_> = maybe!(async { + let adapter_path = + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; + + let version = adapter_path + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("PHP debug adapter has invalid file name"))? + .to_string(); + + Ok((adapter_path, version)) }) - .await - .ok_or_else(|| anyhow!("Couldn't find Javascript dap directory"))?; - - let version = adapter_path - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("Javascript debug adapter has invalid file name"))? - .to_string(); + .await; + + let (adapter_path, version) = match user_installed_path { + Some(path) => (path, "N/A".into()), + None => adapter_info?, + }; Ok(DebugAdapterBinary { command: node_runtime diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 5e150c6321a10f..dd2e216f473f86 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -29,6 +29,7 @@ impl DebugAdapter for LldbDebugAdapter { &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, + _: Option, ) -> Result { #[cfg(target_os = "macos")] { @@ -68,6 +69,7 @@ impl DebugAdapter for LldbDebugAdapter { &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, + _: Option, ) -> Result { bail!("LLDB debug adapter cannot be installed") } diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 4e5ccb92d08c9f..339e966893893f 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,5 +1,6 @@ use dap::transport::{TcpTransport, Transport}; use std::net::Ipv4Addr; +use util::maybe; use crate::*; @@ -48,6 +49,7 @@ impl DebugAdapter for PhpDebugAdapter { &self, delegate: &dyn DapDelegate, _: &DebugAdapterConfig, + user_installed_path: Option, ) -> Result { let node_runtime = delegate .node_runtime() @@ -56,18 +58,29 @@ impl DebugAdapter for PhpDebugAdapter { let adapter_path = paths::debug_adapters_dir().join(self.name()); let file_name_prefix = format!("{}_", self.name()); - let adapter_path = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) + let adapter_info: Result<_> = maybe!(async { + let adapter_path = + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; + + let version = adapter_path + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("PHP debug adapter has invalid file name"))? + .to_string(); + + Ok((adapter_path, version)) }) - .await - .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; - - let version = adapter_path - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("PHP debug adapter has invalid file name"))? - .to_string(); + .await; + + let (adapter_path, version) = match user_installed_path { + Some(path) => (path, "N/A".into()), + None => adapter_info?, + }; Ok(DebugAdapterBinary { command: node_runtime diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index f2f2c942e5edab..f36a39287a2338 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,5 +1,6 @@ use dap::transport::{TcpTransport, Transport}; -use std::net::Ipv4Addr; +use std::{net::Ipv4Addr, path::PathBuf}; +use util::maybe; use crate::*; @@ -57,22 +58,34 @@ impl DebugAdapter for PythonDebugAdapter { &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, + user_installed_path: Option, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(self.name()); let file_name_prefix = format!("{}_", self.name()); - let debugpy_dir = util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) + let adapter_info: Result<_> = maybe!(async { + let debugpy_dir = + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Debugpy directory not found"))?; + + let version = debugpy_dir + .file_name() + .and_then(|file_name| file_name.to_str()) + .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) + .ok_or_else(|| anyhow!("Python debug adapter has invalid file name"))? + .to_string(); + + Ok((debugpy_dir, version)) }) - .await - .ok_or_else(|| anyhow!("Debugpy directory not found"))?; - - let version = debugpy_dir - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("Python debug adapter has invalid file name"))? - .to_string(); + .await; + + let (debugpy_dir, version) = match user_installed_path { + Some(path) => (path, "N/A".into()), + None => adapter_info?, + }; Ok(DebugAdapterBinary { command: "python3".to_string(), diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 60549e53a74237..c9a2fda15cef58 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,3 +1,4 @@ +use crate::project_settings::ProjectSettings; use crate::ProjectPath; use anyhow::{anyhow, Context as _, Result}; use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; @@ -29,7 +30,7 @@ use language::{ }; use node_runtime::NodeRuntime; use serde_json::{json, Value}; -use settings::WorktreeId; +use settings::{Settings, WorktreeId}; use smol::lock::Mutex; use std::{ collections::{BTreeMap, HashMap, HashSet}, @@ -245,6 +246,7 @@ impl DapStore { ) { let client_id = self.next_client_id(); let adapter_delegate = self.delegate.clone(); + let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); let client = maybe!(async { @@ -254,7 +256,19 @@ impl DapStore { .context("Creating debug adapter")?, ); - let binary = match adapter.get_binary(adapter_delegate.as_ref(), &config).await { + let path = cx.update(|cx| { + let name = LanguageServerName::from(adapter.name().as_ref()); + + ProjectSettings::get_global(cx) + .dap + .get(&name) + .and_then(|s| s.path.as_ref().map(PathBuf::from)) + })?; + + let binary = match adapter + .get_binary(adapter_delegate.as_ref(), &config, path) + .await + { Err(error) => { adapter_delegate.update_status( adapter.name(), diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index f421f2c0013aec..edb7dd252bcde6 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -41,6 +41,9 @@ pub struct ProjectSettings { #[serde(default)] pub lsp: HashMap, + #[serde(default)] + pub dap: HashMap, + /// Configuration for Git-related features #[serde(default)] pub git: GitSettings, @@ -181,6 +184,12 @@ pub struct LspSettings { pub settings: Option, } +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DapSettings { + pub path: Option, +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct SessionSettings { /// Whether or not to restore unsaved buffers on restart. From d226de323043827f7559e4709b543b3c82d95e43 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 1 Nov 2024 22:47:43 +0100 Subject: [PATCH 322/650] Add current working directory to adapter (#59) * Add current working directory to adapter * Prio use resolve cwd --- crates/dap/src/adapters.rs | 1 + crates/dap/src/transport.rs | 8 ++++++++ crates/dap_adapters/src/custom.rs | 3 ++- crates/dap_adapters/src/javascript.rs | 4 +++- crates/dap_adapters/src/lldb.rs | 3 ++- crates/dap_adapters/src/php.rs | 3 ++- crates/dap_adapters/src/python.rs | 3 ++- crates/task/src/debug_format.rs | 7 +++++++ crates/task/src/lib.rs | 10 +++++++--- crates/task/src/task_template.rs | 1 + 10 files changed, 35 insertions(+), 8 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 6ca6f4e92f0b6b..1c2f7ab148b0ad 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -73,6 +73,7 @@ pub struct DebugAdapterBinary { pub command: String, pub arguments: Option>, pub envs: Option>, + pub cwd: Option, pub version: String, } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index e68d1590c2a349..fe500a77e65ecf 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -404,6 +404,10 @@ impl Transport for TcpTransport { ) -> Result { let mut command = process::Command::new(&binary.command); + if let Some(cwd) = &binary.cwd { + command.current_dir(cwd); + } + if let Some(args) = &binary.arguments { command.args(args); } @@ -484,6 +488,10 @@ impl Transport for StdioTransport { ) -> Result { let mut command = process::Command::new(&binary.command); + if let Some(cwd) = &binary.cwd { + command.current_dir(cwd); + } + if let Some(args) = &binary.arguments { command.args(args); } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 279bb8d05d5033..3a58d02469a3be 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -42,7 +42,7 @@ impl DebugAdapter for CustomDebugAdapter { async fn get_binary( &self, _: &dyn DapDelegate, - _: &DebugAdapterConfig, + config: &DebugAdapterConfig, _: Option, ) -> Result { Ok(DebugAdapterBinary { @@ -52,6 +52,7 @@ impl DebugAdapter for CustomDebugAdapter { .args .clone() .map(|args| args.iter().map(OsString::from).collect()), + cwd: config.cwd.clone(), envs: self.custom_args.envs.clone(), version: "Custom Debug Adapter".to_string(), }) diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 1581b49fa005ea..708fe650809cc8 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -48,7 +48,7 @@ impl DebugAdapter for JsDebugAdapter { async fn get_installed_binary( &self, delegate: &dyn DapDelegate, - _: &DebugAdapterConfig, + config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { let node_runtime = delegate @@ -92,6 +92,7 @@ impl DebugAdapter for JsDebugAdapter { adapter_path.join(Self::ADAPTER_PATH).into(), self.port.to_string().into(), ]), + cwd: config.cwd.clone(), envs: None, version, }) @@ -126,6 +127,7 @@ impl DebugAdapter for JsDebugAdapter { json!({ "program": config.program, "type": "pwa-node", + "cwd": config.cwd, }) } } diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index dd2e216f473f86..4f958dcfddfcbb 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -28,7 +28,7 @@ impl DebugAdapter for LldbDebugAdapter { async fn get_binary( &self, _: &dyn DapDelegate, - _: &DebugAdapterConfig, + config: &DebugAdapterConfig, _: Option, ) -> Result { #[cfg(target_os = "macos")] @@ -42,6 +42,7 @@ impl DebugAdapter for LldbDebugAdapter { command: lldb_dap_path, arguments: None, envs: None, + cwd: config.cwd.clone(), version: "1".into(), }) } diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 339e966893893f..892e1a6c6b4a0c 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -48,7 +48,7 @@ impl DebugAdapter for PhpDebugAdapter { async fn get_installed_binary( &self, delegate: &dyn DapDelegate, - _: &DebugAdapterConfig, + config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { let node_runtime = delegate @@ -92,6 +92,7 @@ impl DebugAdapter for PhpDebugAdapter { adapter_path.join(Self::ADAPTER_PATH).into(), format!("--server={}", self.port).into(), ]), + cwd: config.cwd.clone(), envs: None, version, }) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index f36a39287a2338..c5eabfb1b8b424 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -57,7 +57,7 @@ impl DebugAdapter for PythonDebugAdapter { async fn get_installed_binary( &self, _: &dyn DapDelegate, - _: &DebugAdapterConfig, + config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(self.name()); @@ -94,6 +94,7 @@ impl DebugAdapter for PythonDebugAdapter { format!("--port={}", self.port).into(), format!("--host={}", self.host).into(), ]), + cwd: config.cwd.clone(), envs: None, version, }) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 80823bce128a48..40db6f7957e9e6 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -2,6 +2,7 @@ use schemars::{gen::SchemaSettings, JsonSchema}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::Ipv4Addr; +use std::path::PathBuf; use util::ResultExt; use crate::{TaskTemplate, TaskTemplates, TaskType}; @@ -105,6 +106,8 @@ pub struct DebugAdapterConfig { pub request: DebugRequestType, /// The program that you trying to debug pub program: Option, + /// The current working directory of your project + pub cwd: Option, /// Additional initialization arguments to be sent on DAP initialization pub initialize_args: Option, } @@ -126,6 +129,8 @@ pub struct DebugTaskDefinition { label: String, /// Program to run the debugger on program: Option, + /// The current working directory of your project + cwd: Option, /// Launch | Request depending on the session the adapter should be ran as #[serde(default)] session_type: DebugRequestType, @@ -142,6 +147,7 @@ impl DebugTaskDefinition { kind: self.adapter, request: self.session_type, program: self.program, + cwd: self.cwd.clone().map(PathBuf::from), initialize_args: self.initialize_args, }); @@ -152,6 +158,7 @@ impl DebugTaskDefinition { command, args, task_type, + cwd: self.cwd, ..Default::default() }) } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index faef6d6635be15..ad02b367c09359 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -99,12 +99,16 @@ impl ResolvedTask { match self.original_task.task_type.clone() { TaskType::Script => None, TaskType::Debug(mut adapter_config) => { - let program = match &self.resolved { - None => adapter_config.program, - Some(spawn_in_terminal) => spawn_in_terminal.program.clone(), + let (cwd, program) = match &self.resolved { + None => (adapter_config.cwd, adapter_config.program), + Some(spawn_in_terminal) => ( + spawn_in_terminal.cwd.clone(), + spawn_in_terminal.program.clone(), + ), }; adapter_config.program = program; + adapter_config.cwd = cwd; Some(adapter_config) } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 7879c105ff3965..138a2f7a16a97b 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -95,6 +95,7 @@ mod deserialization_tests { kind: DebugAdapterKind::Python(TCPHost::default()), request: crate::DebugRequestType::Launch, program: Some("main".to_string()), + cwd: None, initialize_args: None, }; let json = json!({ From bb89c1d9135a04da498ae2a859d6c60506ce7698 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 1 Nov 2024 18:25:45 -0400 Subject: [PATCH 323/650] Check if debug task cwd field is a valid path If the path isn't valid the default cwd value is used (users worktree directory) --- crates/task/src/debug_format.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 40db6f7957e9e6..c71b0ee15416bb 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -143,11 +143,13 @@ pub struct DebugTaskDefinition { impl DebugTaskDefinition { fn to_zed_format(self) -> anyhow::Result { let command = "".to_string(); + let cwd = self.cwd.clone().map(PathBuf::from).take_if(|p| p.exists()); + let task_type = TaskType::Debug(DebugAdapterConfig { kind: self.adapter, request: self.session_type, program: self.program, - cwd: self.cwd.clone().map(PathBuf::from), + cwd: cwd.clone(), initialize_args: self.initialize_args, }); @@ -158,7 +160,7 @@ impl DebugTaskDefinition { command, args, task_type, - cwd: self.cwd, + cwd: if cwd.is_some() { self.cwd } else { None }, ..Default::default() }) } From 591f6cc9a2422a1c5cf0aaf9ff3463b1af4f83f8 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 1 Nov 2024 23:36:05 +0100 Subject: [PATCH 324/650] Fix linux clippy error --- crates/dap_adapters/src/lldb.rs | 44 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 4f958dcfddfcbb..c54df216d15177 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -25,33 +25,37 @@ impl DebugAdapter for LldbDebugAdapter { Box::new(StdioTransport::new()) } + #[cfg(target_os = "macos")] async fn get_binary( &self, _: &dyn DapDelegate, config: &DebugAdapterConfig, _: Option, ) -> Result { - #[cfg(target_os = "macos")] - { - let output = std::process::Command::new("xcrun") - .args(&["-f", "lldb-dap"]) - .output()?; - let lldb_dap_path = String::from_utf8(output.stdout)?.trim().to_string(); + let output = std::process::Command::new("xcrun") + .args(&["-f", "lldb-dap"]) + .output()?; + let lldb_dap_path = String::from_utf8(output.stdout)?.trim().to_string(); - Ok(DebugAdapterBinary { - command: lldb_dap_path, - arguments: None, - envs: None, - cwd: config.cwd.clone(), - version: "1".into(), - }) - } - #[cfg(not(target_os = "macos"))] - { - Err(anyhow::anyhow!( - "LLDB-DAP is only supported on macOS (Right now)" - )) - } + Ok(DebugAdapterBinary { + command: lldb_dap_path, + arguments: None, + envs: None, + cwd: config.cwd.clone(), + version: "1".into(), + }) + } + + #[cfg(not(target_os = "macos"))] + async fn get_binary( + &self, + _: &dyn DapDelegate, + _: &DebugAdapterConfig, + _: Option, + ) -> Result { + Err(anyhow::anyhow!( + "LLDB-DAP is only supported on macOS (Right now)" + )) } async fn install_binary( From 65cd774baa6afa5475b18ac386753774134e674b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 2 Nov 2024 22:23:15 +0100 Subject: [PATCH 325/650] Implement Attach debugger + picker (#58) * Rename StopDebugAdapters to ShutdownDebugAdapters * Remove debug config from install methods * Move requests methods to background executor * Wip attach with picker * Move find client to the first line of the method The client should always be there at this point. * Fix correctly determine when to restart While debugging an reverse request issue, the top level client did send terminated event with `restart: false` but we tried to restart the client resulting in the client never being cleaned up by us. Because before this change we always assumed if we got a json value we are going to restart the client, which is wrong. We no try to restart the client if: - restart arg is a boolean and its true - restart arg is a json value but no boolean * Clean up response to adapter * Fix clippy errors * WIP tasks * Simplified debug task schema This changes debug.json to look for adapter: adapter_name instead of and object when a user selects a debug adatper and fixes the default behavior of request (to launch) Co-authored-by: Remco Smits * Make default and flatten work for request * Rename enum case * Remove dbg * Dismiss when candidate is not found * Add docs for why we update the process id on the config * Show error when `attach` request is selected but not supported --------- Co-authored-by: Anthony Eid --- Cargo.lock | 5 + crates/dap/Cargo.toml | 1 + crates/dap/src/adapters.rs | 24 ++- crates/dap/src/client.rs | 37 ++-- crates/dap_adapters/Cargo.toml | 2 + crates/dap_adapters/src/dap_adapters.rs | 6 +- crates/dap_adapters/src/javascript.rs | 34 ++- crates/dap_adapters/src/lldb.rs | 1 - crates/debugger_ui/Cargo.toml | 4 +- crates/debugger_ui/src/attach_modal.rs | 250 ++++++++++++++++++++++ crates/debugger_ui/src/debugger_panel.rs | 70 +++++- crates/debugger_ui/src/lib.rs | 5 +- crates/project/src/dap_store.rs | 260 +++++++++++++---------- crates/project/src/project.rs | 10 +- crates/task/src/debug_format.rs | 39 ++-- crates/task/src/lib.rs | 4 +- crates/workspace/src/workspace.rs | 2 +- 17 files changed, 582 insertions(+), 172 deletions(-) create mode 100644 crates/debugger_ui/src/attach_modal.rs diff --git a/Cargo.lock b/Cargo.lock index 7b72a9d736f745..d2e3261dfc20df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3370,6 +3370,7 @@ dependencies = [ "settings", "smallvec", "smol", + "sysinfo", "task", "util", ] @@ -3392,8 +3393,10 @@ dependencies = [ "async-trait", "dap", "paths", + "regex", "serde", "serde_json", + "sysinfo", "task", "util", ] @@ -3499,10 +3502,12 @@ dependencies = [ "language", "menu", "parking_lot", + "picker", "project", "serde", "serde_json", "settings", + "sysinfo", "task", "tasks_ui", "terminal_view", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index f3a955859869a8..c2f57bf6abeca1 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -26,5 +26,6 @@ serde_json.workspace = true settings.workspace = true smallvec.workspace = true smol.workspace = true +sysinfo.workspace = true task.workspace = true util.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 1c2f7ab148b0ad..542c245402799d 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -15,6 +15,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use sysinfo::{Pid, Process}; use task::DebugAdapterConfig; #[derive(Clone, Debug, PartialEq, Eq)] @@ -210,7 +211,7 @@ pub trait DebugAdapter: 'static + Send + Sync { ); let binary = self - .get_installed_binary(delegate, config, Some(adapter_path)) + .get_installed_binary(delegate, &config, Some(adapter_path)) .await; if binary.is_ok() { @@ -240,14 +241,14 @@ pub trait DebugAdapter: 'static + Send + Sync { { log::info!("Using cached debug adapter binary {}", self.name()); - return self.get_installed_binary(delegate, config, None).await; + return self.get_installed_binary(delegate, &config, None).await; } log::info!("Getting latest version of debug adapter {}", self.name()); delegate.update_status(self.name(), DapStatus::CheckingForUpdate); let version = self.fetch_latest_adapter_version(delegate).await.ok(); - let mut binary = self.get_installed_binary(delegate, config, None).await; + let mut binary = self.get_installed_binary(delegate, &config, None).await; if let Some(version) = version { if binary @@ -265,7 +266,8 @@ pub trait DebugAdapter: 'static + Send + Sync { delegate.update_status(self.name(), DapStatus::Downloading); self.install_binary(version, delegate).await?; - binary = self.get_installed_binary(delegate, config, None).await; + + binary = self.get_installed_binary(delegate, &config, None).await; } else { log::error!( "Failed getting latest version of debug adapter {}", @@ -309,4 +311,18 @@ pub trait DebugAdapter: 'static + Send + Sync { /// Should return base configuration to make the debug adapter work fn request_args(&self, config: &DebugAdapterConfig) -> Value; + + /// Whether the adapter supports `attach` request, + /// if not support and the request is selected we will show an error message + fn supports_attach(&self) -> bool { + false + } + + /// Filters out the processes that the adapter can attach to for debugging + fn attach_processes<'a>( + &self, + _: &'a HashMap, + ) -> Option> { + None + } } diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 672fa17c817c6c..068a5a91e07eb6 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -3,19 +3,17 @@ use crate::{ transport::{IoKind, LogKind, TransportDelegate}, }; use anyhow::{anyhow, Result}; - use dap_types::{ messages::{Message, Response}, requests::Request, }; use gpui::{AppContext, AsyncAppContext}; -use serde_json::Value; use smol::channel::{bounded, Receiver, Sender}; use std::{ hash::Hash, sync::{ atomic::{AtomicU64, Ordering}, - Arc, + Arc, Mutex, }, }; use task::{DebugAdapterConfig, DebugRequestType}; @@ -35,27 +33,26 @@ pub struct DebugAdapterClientId(pub usize); pub struct DebugAdapterClient { id: DebugAdapterClientId, - adapter_id: String, - request_args: Value, sequence_count: AtomicU64, - config: DebugAdapterConfig, + adapter: Arc>, transport_delegate: TransportDelegate, + config: Arc>, } impl DebugAdapterClient { pub fn new( id: DebugAdapterClientId, - request_args: Value, config: DebugAdapterConfig, adapter: Arc>, ) -> Self { + let transport_delegate = TransportDelegate::new(adapter.transport()); + Self { id, - config, - request_args, + adapter, + transport_delegate, sequence_count: AtomicU64::new(1), - adapter_id: adapter.name().to_string(), - transport_delegate: TransportDelegate::new(adapter.transport()), + config: Arc::new(Mutex::new(config)), } } @@ -141,19 +138,23 @@ impl DebugAdapterClient { } pub fn config(&self) -> DebugAdapterConfig { - self.config.clone() + self.config.lock().unwrap().clone() } - pub fn adapter_id(&self) -> String { - self.adapter_id.clone() + pub fn adapter(&self) -> &Arc> { + &self.adapter } - pub fn request_args(&self) -> Value { - self.request_args.clone() + pub fn adapter_id(&self) -> String { + self.adapter.name().to_string() } - pub fn request_type(&self) -> DebugRequestType { - self.config.request.clone() + pub fn set_process_id(&self, process_id: u32) { + let mut config = self.config.lock().unwrap(); + + config.request = DebugRequestType::Attach(task::AttachConfig { + process_id: Some(process_id), + }); } /// Get the next sequence id to be used in a request diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 73d5e8168f5a98..6f390f4e66f46c 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -17,7 +17,9 @@ anyhow.workspace = true async-trait.workspace = true dap.workspace = true paths.workspace = true +regex.workspace = true serde.workspace = true serde_json.workspace = true +sysinfo.workspace = true task.workspace = true util.workspace = true diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 9a305d4a7b142c..3c9949d0ec4caa 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -19,13 +19,13 @@ use serde_json::{json, Value}; use std::path::PathBuf; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; -pub async fn build_adapter(adapter_config: &DebugAdapterConfig) -> Result> { - match &adapter_config.kind { +pub async fn build_adapter(kind: &DebugAdapterKind) -> Result> { + match &kind { DebugAdapterKind::Custom(start_args) => { Ok(Box::new(CustomDebugAdapter::new(start_args.clone()).await?)) } DebugAdapterKind::Python(host) => Ok(Box::new(PythonDebugAdapter::new(host).await?)), - DebugAdapterKind::PHP(host) => Ok(Box::new(PhpDebugAdapter::new(host.clone()).await?)), + DebugAdapterKind::Php(host) => Ok(Box::new(PhpDebugAdapter::new(host.clone()).await?)), DebugAdapterKind::Javascript(host) => { Ok(Box::new(JsDebugAdapter::new(host.clone()).await?)) } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 708fe650809cc8..6e99e1dd4d50ad 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,5 +1,8 @@ use dap::transport::{TcpTransport, Transport}; -use std::net::Ipv4Addr; +use regex::Regex; +use std::{collections::HashMap, net::Ipv4Addr}; +use sysinfo::{Pid, Process}; +use task::DebugRequestType; use util::maybe; use crate::*; @@ -124,10 +127,39 @@ impl DebugAdapter for JsDebugAdapter { } fn request_args(&self, config: &DebugAdapterConfig) -> Value { + let pid = if let DebugRequestType::Attach(attach_config) = &config.request { + attach_config.process_id + } else { + None + }; + json!({ "program": config.program, "type": "pwa-node", + "request": match config.request { + DebugRequestType::Launch => "launch", + DebugRequestType::Attach(_) => "attach", + }, + "processId": pid, "cwd": config.cwd, }) } + + fn supports_attach(&self) -> bool { + true + } + + fn attach_processes<'a>( + &self, + processes: &'a HashMap, + ) -> Option> { + let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap(); + + Some( + processes + .iter() + .filter(|(_, process)| regex.is_match(&process.name().to_string_lossy())) + .collect::>(), + ) + } } diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index c54df216d15177..65f1074d888cef 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -50,7 +50,6 @@ impl DebugAdapter for LldbDebugAdapter { async fn get_binary( &self, _: &dyn DapDelegate, - _: &DebugAdapterConfig, _: Option, ) -> Result { Err(anyhow::anyhow!( diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 370b1e1595c57d..427881ef1af3f2 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -10,8 +10,8 @@ workspace = true [dependencies] anyhow.workspace = true -command_palette_hooks.workspace = true collections.workspace = true +command_palette_hooks.workspace = true dap.workspace = true editor.workspace = true futures.workspace = true @@ -20,10 +20,12 @@ gpui.workspace = true language.workspace = true menu.workspace = true parking_lot.workspace = true +picker.workspace = true project.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +sysinfo.workspace = true task.workspace = true tasks_ui.workspace = true terminal_view.workspace = true diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs new file mode 100644 index 00000000000000..df036e8ccbd832 --- /dev/null +++ b/crates/debugger_ui/src/attach_modal.rs @@ -0,0 +1,250 @@ +use dap::client::DebugAdapterClientId; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{DismissEvent, EventEmitter, FocusableView, Render, View}; +use gpui::{Model, Subscription}; +use picker::{Picker, PickerDelegate}; +use project::dap_store::DapStore; +use std::sync::Arc; +use sysinfo::System; +use ui::{prelude::*, ViewContext}; +use ui::{ListItem, ListItemSpacing}; +use workspace::ModalView; + +#[derive(Debug, Clone)] +struct Candidate { + pid: u32, + name: String, + command: String, +} + +struct AttachModalDelegate { + selected_index: usize, + matches: Vec, + placeholder_text: Arc, + dap_store: Model, + client_id: DebugAdapterClientId, + candidates: Option>, +} + +impl AttachModalDelegate { + pub fn new(client_id: DebugAdapterClientId, dap_store: Model) -> Self { + Self { + client_id, + dap_store, + candidates: None, + selected_index: 0, + matches: Vec::default(), + placeholder_text: Arc::from("Select the process you want to attach the debugger to"), + } + } +} + +pub(crate) struct AttachModal { + _subscription: Subscription, + picker: View>, +} + +impl AttachModal { + pub fn new( + client_id: &DebugAdapterClientId, + dap_store: Model, + cx: &mut ViewContext, + ) -> Self { + let picker = cx.new_view(|cx| { + Picker::uniform_list(AttachModalDelegate::new(*client_id, dap_store), cx) + }); + let _subscription = cx.subscribe(&picker, |_, _, _, cx| { + cx.emit(DismissEvent); + }); + Self { + picker, + _subscription, + } + } +} + +impl Render for AttachModal { + fn render(&mut self, _: &mut ViewContext) -> impl ui::IntoElement { + v_flex() + .key_context("AttachModal") + .w(rems(34.)) + .child(self.picker.clone()) + } +} + +impl EventEmitter for AttachModal {} + +impl FocusableView for AttachModal { + fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + self.picker.read(cx).focus_handle(cx) + } +} + +impl ModalView for AttachModal {} + +impl PickerDelegate for AttachModalDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> std::sync::Arc { + self.placeholder_text.clone() + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + cx.spawn(|this, mut cx| async move { + let Some(processes) = this + .update(&mut cx, |this, cx| { + if let Some(processes) = this.delegate.candidates.clone() { + processes + } else { + let Some(client) = this + .delegate + .dap_store + .read(cx) + .client_by_id(&this.delegate.client_id) + else { + return Vec::new(); + }; + + let system = System::new_all(); + let Some(processes) = + client.adapter().attach_processes(&system.processes()) + else { + return Vec::new(); + }; + + let processes = processes + .into_iter() + .map(|(pid, process)| Candidate { + pid: pid.as_u32(), + name: process.name().to_string_lossy().into_owned(), + command: process + .cmd() + .iter() + .map(|s| s.to_string_lossy()) + .collect::>() + .join(" "), + }) + .collect::>(); + + let _ = this.delegate.candidates.insert(processes.clone()); + + processes + } + }) + .ok() + else { + return; + }; + + let matches = fuzzy::match_strings( + &processes + .iter() + .enumerate() + .map(|(id, candidate)| { + StringMatchCandidate::new( + id, + format!("{} {} {}", candidate.command, candidate.pid, candidate.name), + ) + }) + .collect::>(), + &query, + true, + 100, + &Default::default(), + cx.background_executor().clone(), + ) + .await; + + this.update(&mut cx, |this, _| { + let delegate = &mut this.delegate; + + delegate.matches = matches; + delegate.candidates = Some(processes); + + if delegate.matches.is_empty() { + delegate.selected_index = 0; + } else { + delegate.selected_index = + delegate.selected_index.min(delegate.matches.len() - 1); + } + }) + .ok(); + }) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + let candidate = self + .matches + .get(self.selected_index()) + .and_then(|current_match| { + let ix = current_match.candidate_id; + self.candidates.as_ref().map(|candidates| &candidates[ix]) + }); + let Some(candidate) = candidate else { + return cx.emit(DismissEvent); + }; + + self.dap_store.update(cx, |store, cx| { + store + .attach(&self.client_id, candidate.pid, cx) + .detach_and_log_err(cx); + }); + + cx.emit(DismissEvent); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.selected_index = 0; + self.candidates.take(); + + self.dap_store.update(cx, |store, cx| { + store.shutdown_client(&self.client_id, cx).detach(); + }); + + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut ViewContext>, + ) -> Option { + let candidates = self.candidates.as_ref()?; + let hit = &self.matches[ix]; + let candidate = &candidates.get(hit.candidate_id)?; + + Some( + ListItem::new(SharedString::from(format!("attach-modal-{ix}"))) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child( + v_flex() + .items_start() + .child(Label::new(candidate.command.clone())) + .child( + Label::new(format!("Pid: {}, name: {}", candidate.pid, candidate.name)) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ), + ) + } +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 8fe9ccb2d741e9..47014ccfe8bd86 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,3 +1,4 @@ +use crate::attach_modal::AttachModal; use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use collections::{BTreeMap, HashMap}; @@ -22,6 +23,7 @@ use settings::Settings; use std::any::TypeId; use std::path::PathBuf; use std::u64; +use task::DebugRequestType; use terminal_view::terminal_panel::TerminalPanel; use ui::prelude::*; use workspace::{ @@ -98,6 +100,9 @@ impl DebugPanel { cx.subscribe(&pane, Self::handle_pane_event), cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { + project::Event::DebugClientStarted(client_id) => { + this.handle_debug_client_started(client_id, cx); + } project::Event::DebugClientEvent { message, client_id } => match message { Message::Event(event) => { this.handle_debug_client_events(client_id, event, cx); @@ -255,7 +260,11 @@ impl DebugPanel { let args = if let Some(args) = request_args { serde_json::from_value(args.clone()).ok() } else { - None + return; + }; + + let Some(args) = args else { + return; }; self.dap_store.update(cx, |store, cx| { @@ -375,6 +384,58 @@ impl DebugPanel { .detach_and_log_err(cx); } + fn handle_debug_client_started( + &self, + client_id: &DebugAdapterClientId, + cx: &mut ViewContext, + ) { + let Some(client) = self.dap_store.read(cx).client_by_id(&client_id) else { + return; + }; + + let client_id = *client_id; + let workspace = self.workspace.clone(); + let request_type = client.config().request; + cx.spawn(|this, mut cx| async move { + let task = this.update(&mut cx, |this, cx| { + this.dap_store + .update(cx, |store, cx| store.initialize(&client_id, cx)) + })?; + + task.await?; + + match request_type { + DebugRequestType::Launch => { + let task = this.update(&mut cx, |this, cx| { + this.dap_store + .update(cx, |store, cx| store.launch(&client_id, cx)) + }); + + task?.await + } + DebugRequestType::Attach(config) => { + if let Some(process_id) = config.process_id { + let task = this.update(&mut cx, |this, cx| { + this.dap_store + .update(cx, |store, cx| store.attach(&client_id, process_id, cx)) + })?; + + task.await + } else { + this.update(&mut cx, |this, cx| { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| { + AttachModal::new(&client_id, this.dap_store.clone(), cx) + }) + }) + })? + } + } + } + }) + .detach_and_log_err(cx); + } + fn handle_debug_client_events( &mut self, client_id: &DebugAdapterClientId, @@ -461,7 +522,7 @@ impl DebugPanel { .dap_store .read(cx) .client_by_id(client_id) - .map(|c| c.config().kind) + .map(|client| client.config().kind) else { return; // this can never happen }; @@ -600,7 +661,10 @@ impl DebugPanel { } self.dap_store.update(cx, |store, cx| { - if restart_args.is_some() { + if restart_args + .as_ref() + .is_some_and(|v| v.as_bool().unwrap_or(true)) + { store .restart(&client_id, restart_args, cx) .detach_and_log_err(cx); diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index d7e3e59b4f8b05..f9dbe505f1420e 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -4,10 +4,11 @@ use gpui::AppContext; use settings::Settings; use ui::ViewContext; use workspace::{ - Continue, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, StopDebugAdapters, + Continue, Pause, Restart, ShutdownDebugAdapters, Start, StepInto, StepOut, StepOver, Stop, Workspace, }; +mod attach_modal; mod console; pub mod debugger_panel; mod debugger_panel_item; @@ -28,7 +29,7 @@ pub fn init(cx: &mut AppContext) { .register_action(|workspace: &mut Workspace, _: &Start, cx| { tasks_ui::toggle_modal(workspace, task::TaskModal::DebugModal, cx).detach(); }) - .register_action(|workspace: &mut Workspace, _: &StopDebugAdapters, cx| { + .register_action(|workspace: &mut Workspace, _: &ShutdownDebugAdapters, cx| { workspace.project().update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { store.shutdown_clients(cx).detach(); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index c9a2fda15cef58..ef53b347d465b4 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -6,20 +6,20 @@ use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::messages::{Message, Response}; use dap::requests::{ Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, - LoadedSources, Modules, Next, Pause, Request as _, RunInTerminal, Scopes, SetBreakpoints, - SetExpression, SetVariable, StackTrace, StartDebugging, StepIn, StepOut, Terminate, - TerminateThreads, Variables, + LoadedSources, Modules, Next, Pause, Request as _, Restart, RunInTerminal, Scopes, + SetBreakpoints, SetExpression, SetVariable, StackTrace, StartDebugging, StepIn, StepOut, + Terminate, TerminateThreads, Variables, }; use dap::{ AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, LoadedSourcesArguments, Module, - ModulesArguments, NextArguments, PauseArguments, RunInTerminalResponse, Scope, ScopesArguments, - SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, - SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, - StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, - TerminateThreadsArguments, Variable, VariablesArguments, + ModulesArguments, NextArguments, PauseArguments, RestartArguments, RunInTerminalResponse, + Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, + Source, SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, + StartDebuggingRequestArgumentsRequest, StepInArguments, StepOutArguments, SteppingGranularity, + TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, }; use dap_adapters::build_adapter; use fs::Fs; @@ -29,7 +29,7 @@ use language::{ Buffer, BufferSnapshot, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName, }; use node_runtime::NodeRuntime; -use serde_json::{json, Value}; +use serde_json::Value; use settings::{Settings, WorktreeId}; use smol::lock::Mutex; use std::{ @@ -41,9 +41,9 @@ use std::{ Arc, }, }; -use task::{DebugAdapterConfig, DebugRequestType}; +use task::{AttachConfig, DebugAdapterConfig, DebugRequestType}; use text::Point; -use util::{maybe, merge_json_value_into, ResultExt}; +use util::{maybe, merge_json_value_into, ResultExt as _}; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), @@ -238,12 +238,7 @@ impl DapStore { } } - pub fn start_client( - &mut self, - config: DebugAdapterConfig, - args: Option, - cx: &mut ModelContext, - ) { + pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { let client_id = self.next_client_id(); let adapter_delegate = self.delegate.clone(); @@ -251,11 +246,17 @@ impl DapStore { let dap_store = this.clone(); let client = maybe!(async { let adapter = Arc::new( - build_adapter(&config) + build_adapter(&config.kind) .await .context("Creating debug adapter")?, ); + if !adapter.supports_attach() + && matches!(config.request, DebugRequestType::Attach(_)) + { + return Err(anyhow!("Debug adapter does not support `attach` request")); + } + let path = cx.update(|cx| { let name = LanguageServerName::from(adapter.name().as_ref()); @@ -286,18 +287,7 @@ impl DapStore { } }; - let mut request_args = json!({}); - if let Some(config_args) = config.initialize_args.clone() { - merge_json_value_into(config_args, &mut request_args); - } - - merge_json_value_into(adapter.request_args(&config), &mut request_args); - - if let Some(args) = args { - merge_json_value_into(args.configuration, &mut request_args); - } - - let mut client = DebugAdapterClient::new(client_id, request_args, config, adapter); + let mut client = DebugAdapterClient::new(client_id, config, adapter); client .start( @@ -387,27 +377,58 @@ impl DapStore { store.capabilities.insert(client.id(), capabilities); cx.notify(); - })?; - - // send correct request based on adapter config - match client.config().request { - DebugRequestType::Launch => { - client - .request::(LaunchRequestArguments { - raw: client.request_args(), - }) - .await? - } - DebugRequestType::Attach => { - client - .request::(AttachRequestArguments { - raw: client.request_args(), - }) - .await? - } - } + }) + }) + } - Ok(()) + pub fn launch( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Client was not found"))); + }; + + let mut adapter_args = client.adapter().request_args(&client.config()); + + if let Some(args) = client.config().initialize_args.clone() { + merge_json_value_into(args, &mut adapter_args); + } + + cx.background_executor().spawn(async move { + client + .request::(LaunchRequestArguments { raw: adapter_args }) + .await + }) + } + + pub fn attach( + &mut self, + client_id: &DebugAdapterClientId, + pid: u32, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Client was not found"))); + }; + + // update the process id on the config, so when the `startDebugging` reverse request + // comes in we send another `attach` request with the already selected PID + // If we don't do this the user has to select the process twice if the adapter sends a `startDebugging` request + client.set_process_id(pid); + + let config = client.config(); + let mut adapter_args = client.adapter().request_args(&config); + + if let Some(args) = config.initialize_args.clone() { + merge_json_value_into(args, &mut adapter_args); + } + + cx.background_executor().spawn(async move { + client + .request::(AttachRequestArguments { raw: adapter_args }) + .await }) } @@ -426,7 +447,7 @@ impl DapStore { return Task::ready(Ok(Vec::default())); } - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(ModulesArguments { start_module: None, @@ -455,7 +476,7 @@ impl DapStore { return Task::ready(Ok(Vec::default())); } - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(LoadedSourcesArguments {}) .await? @@ -473,7 +494,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Client was not found"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(StackTraceArguments { thread_id, @@ -496,7 +517,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Client was not found"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(ScopesArguments { frame_id: stack_frame_id, @@ -517,7 +538,7 @@ impl DapStore { let capabilities = self.capabilities_by_id(client_id); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { let support_configuration_done_request = capabilities .supports_configuration_done_request .unwrap_or_default(); @@ -536,7 +557,7 @@ impl DapStore { &self, client_id: &DebugAdapterClientId, seq: u64, - args: Option, + args: StartDebuggingRequestArguments, cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { @@ -554,8 +575,33 @@ impl DapStore { })) .await?; + let config = client.config(); + this.update(&mut cx, |store, cx| { - store.start_client(client.config(), args, cx); + store.start_client( + DebugAdapterConfig { + kind: config.kind.clone(), + request: match args.request { + StartDebuggingRequestArgumentsRequest::Launch => { + DebugRequestType::Launch + } + StartDebuggingRequestArgumentsRequest::Attach => { + DebugRequestType::Attach( + if let DebugRequestType::Attach(attach_config) = config.request + { + attach_config + } else { + AttachConfig::default() + }, + ) + } + }, + program: config.program.clone(), + cwd: config.cwd.clone(), + initialize_args: Some(args.configuration), + }, + cx, + ); }) }) } @@ -572,31 +618,22 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { - if success { - client - .send_message(Message::Response(Response { - seq, - request_seq: seq, - success: true, - command: RunInTerminal::COMMAND.to_string(), - body: Some(serde_json::to_value(RunInTerminalResponse { + cx.background_executor().spawn(async move { + client + .send_message(Message::Response(Response { + seq, + request_seq: seq, + success, + command: RunInTerminal::COMMAND.to_string(), + body: match success { + true => Some(serde_json::to_value(RunInTerminalResponse { process_id: Some(std::process::id() as u64), shell_process_id: shell_pid, })?), - })) - .await - } else { - client - .send_message(Message::Response(Response { - seq, - request_seq: seq, - success: false, - command: RunInTerminal::COMMAND.to_string(), - body: Some(serde_json::to_value(ErrorResponse { error: None })?), - })) - .await - } + false => Some(serde_json::to_value(ErrorResponse { error: None })?), + }, + })) + .await }) } @@ -610,7 +647,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(ContinueArguments { thread_id, @@ -642,7 +679,7 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(NextArguments { thread_id, @@ -673,7 +710,7 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(StepInArguments { thread_id, @@ -705,7 +742,7 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(StepOutArguments { thread_id, @@ -726,7 +763,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(VariablesArguments { variables_reference, @@ -752,7 +789,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(EvaluateArguments { expression: expression.clone(), @@ -779,7 +816,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { Ok(client .request::(CompletionsArguments { frame_id: Some(stack_frame_id), @@ -812,7 +849,7 @@ impl DapStore { .supports_set_expression .unwrap_or_default(); - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { if let Some(evaluate_name) = supports_set_expression.then(|| evaluate_name).flatten() { client .request::(SetExpressionArguments { @@ -847,7 +884,8 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { client.request::(PauseArguments { thread_id }).await }) + cx.background_executor() + .spawn(async move { client.request::(PauseArguments { thread_id }).await }) } pub fn terminate_threads( @@ -866,7 +904,7 @@ impl DapStore { .supports_terminate_threads_request .unwrap_or_default() { - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(TerminateThreadsArguments { thread_ids }) .await @@ -885,7 +923,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(DisconnectArguments { restart: Some(false), @@ -906,28 +944,24 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - let restart_args = args.unwrap_or(Value::Null); + let supports_restart = self + .capabilities_by_id(client_id) + .supports_restart_request + .unwrap_or_default(); - cx.spawn(|_, _| async move { - client - .request::(DisconnectArguments { - restart: Some(true), - terminate_debuggee: Some(false), - suspend_debuggee: Some(false), - }) - .await?; + let raw = args.unwrap_or(Value::Null); - match client.request_type() { - DebugRequestType::Launch => { - client - .request::(LaunchRequestArguments { raw: restart_args }) - .await? - } - DebugRequestType::Attach => { - client - .request::(AttachRequestArguments { raw: restart_args }) - .await? - } + cx.background_executor().spawn(async move { + if supports_restart { + client.request::(RestartArguments { raw }).await?; + } else { + client + .request::(DisconnectArguments { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }) + .await?; } Ok(()) @@ -959,9 +993,7 @@ impl DapStore { let capabilities = self.capabilities.remove(client_id); - cx.notify(); - - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { let client = match client { DebugAdapterClientState::Starting(task) => task.await, DebugAdapterClientState::Running(client) => Some(client), @@ -1026,7 +1058,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not found client"))); }; - cx.spawn(|_, _| async move { + cx.background_executor().spawn(async move { client .request::(SetBreakpointsArguments { source: Source { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 85e0fa4faa844a..6367191365db67 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -85,6 +85,7 @@ use std::{ sync::Arc, time::Duration, }; + use task_store::TaskStore; use terminals::Terminals; use text::{Anchor, BufferId}; @@ -1224,8 +1225,9 @@ impl Project { cx: &mut ModelContext, ) { if let Some(adapter_config) = debug_task.debug_adapter_config() { - self.dap_store - .update(cx, |store, cx| store.start_client(adapter_config, None, cx)); + self.dap_store.update(cx, |store, cx| { + store.start_client(adapter_config, cx); + }); } } @@ -2270,10 +2272,6 @@ impl Project { match event { DapStoreEvent::DebugClientStarted(client_id) => { cx.emit(Event::DebugClientStarted(*client_id)); - - self.dap_store.update(cx, |store, cx| { - store.initialize(client_id, cx).detach_and_log_err(cx); - }); } DapStoreEvent::DebugClientStopped(client_id) => { cx.emit(Event::DebugClientStopped(*client_id)); diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index c71b0ee15416bb..f590e865f32934 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -37,20 +37,28 @@ impl TCPHost { } } +/// Represents the attach request information of the debug adapter +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] +pub struct AttachConfig { + /// The processId to attach to, if left empty we will show a process picker + #[serde(default)] + pub process_id: Option, +} + /// Represents the type that will determine which request to call on the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "lowercase", tag = "request")] pub enum DebugRequestType { /// Call the `launch` request on the debug adapter #[default] Launch, /// Call the `attach` request on the debug adapter - Attach, + Attach(AttachConfig), } /// The Debug adapter to use #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase", tag = "kind")] +#[serde(rename_all = "lowercase", tag = "adapter")] pub enum DebugAdapterKind { /// Manually setup starting a debug adapter /// The argument within is used to start the DAP @@ -58,7 +66,7 @@ pub enum DebugAdapterKind { /// Use debugpy Python(TCPHost), /// Use vscode-php-debug - PHP(TCPHost), + Php(TCPHost), /// Use vscode-js-debug Javascript(TCPHost), /// Use lldb @@ -71,7 +79,7 @@ impl DebugAdapterKind { match self { Self::Custom(_) => "Custom", Self::Python(_) => "Python", - Self::PHP(_) => "PHP", + Self::Php(_) => "PHP", Self::Javascript(_) => "JavaScript", Self::Lldb => "LLDB", } @@ -96,13 +104,11 @@ pub struct CustomArgs { #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugAdapterConfig { - /// Unique id of for the debug adapter, - /// that will be send with the `initialize` request + /// The type of adapter you want to use #[serde(flatten)] pub kind: DebugAdapterKind, - /// The type of connection the adapter should use /// The type of request that should be called on the debug adapter - #[serde(default)] + #[serde(default, flatten)] pub request: DebugRequestType, /// The program that you trying to debug pub program: Option, @@ -125,17 +131,18 @@ pub enum DebugConnectionType { #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugTaskDefinition { - /// Name of the debug tasks + /// Name of the debug task label: String, /// Program to run the debugger on program: Option, /// The current working directory of your project cwd: Option, - /// Launch | Request depending on the session the adapter should be ran as - #[serde(default)] - session_type: DebugRequestType, + /// The type of request that should be called on the debug adapter + #[serde(default, flatten)] + request: DebugRequestType, /// The adapter to run - adapter: DebugAdapterKind, + #[serde(flatten)] + kind: DebugAdapterKind, /// Additional initialization arguments to be sent on DAP initialization initialize_args: Option, } @@ -146,8 +153,8 @@ impl DebugTaskDefinition { let cwd = self.cwd.clone().map(PathBuf::from).take_if(|p| p.exists()); let task_type = TaskType::Debug(DebugAdapterConfig { - kind: self.adapter, - request: self.session_type, + kind: self.kind, + request: self.request, program: self.program, cwd: cwd.clone(), initialize_args: self.initialize_args, diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index ad02b367c09359..e7e05bd5666aec 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -15,8 +15,8 @@ use std::path::PathBuf; use std::str::FromStr; pub use debug_format::{ - CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, DebugRequestType, - DebugTaskFile, TCPHost, + AttachConfig, CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, + DebugRequestType, DebugTaskFile, TCPHost, }; pub use task_template::{ HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, TaskType, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 876306175c1e0d..b4308fbfb88281 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -158,7 +158,7 @@ actions!( ReloadActiveItem, SaveAs, SaveWithoutFormat, - StopDebugAdapters, + ShutdownDebugAdapters, ToggleBottomDock, ToggleCenteredLayout, ToggleLeftDock, From 1b2871aac2ae3ea550843c15247b530e627286c4 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 2 Nov 2024 22:44:03 +0100 Subject: [PATCH 326/650] Fix failing test --- crates/task/src/task_template.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 0bc9229089ea02..241676cbe7ff8d 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -100,7 +100,7 @@ mod deserialization_tests { }; let json = json!({ "type": "debug", - "kind": "python", + "adapter": "python", "request": "launch", "program": "main" }); From 45c4aef0dacf806bd3840d7b17a2fc4fa5360d13 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 3 Nov 2024 12:18:19 +0100 Subject: [PATCH 327/650] Fix non mac os compile error --- crates/dap_adapters/src/lldb.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 65f1074d888cef..c54df216d15177 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -50,6 +50,7 @@ impl DebugAdapter for LldbDebugAdapter { async fn get_binary( &self, _: &dyn DapDelegate, + _: &DebugAdapterConfig, _: Option, ) -> Result { Err(anyhow::anyhow!( From 56df4fbe6e6106a03f71f7e2bb0d7f1d1c8b5051 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:26:55 -0500 Subject: [PATCH 328/650] Get users python binary path based on `which` instead of `python3` & pass shell_env to Adapters (#63) * Add ProjectEnvironment to Dap Store & use get users python3 path from which command * Pass shell env to debug adapters --- Cargo.lock | 1 + crates/dap/Cargo.toml | 1 + crates/dap/src/adapters.rs | 5 +- crates/dap_adapters/src/python.rs | 24 +++++- crates/project/src/dap_store.rs | 83 ++++++++++++++++---- crates/project/src/project.rs | 11 ++- crates/remote_server/src/headless_project.rs | 15 +++- 7 files changed, 114 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2e3261dfc20df..7df2935b73e730 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3355,6 +3355,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "collections", "dap-types", "fs", "futures 0.3.30", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index c2f57bf6abeca1..bfddab846f475f 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true +collections.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b95818130022bfc72bbcd639bdd0c0358c7549fc" } fs.workspace = true futures.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 542c245402799d..3da1f5e87a88aa 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -9,7 +9,7 @@ use serde_json::Value; use smol::{self, fs::File, lock::Mutex, process}; use std::{ collections::{HashMap, HashSet}, - ffi::OsString, + ffi::{OsStr, OsString}, fmt::Debug, ops::Deref, path::{Path, PathBuf}, @@ -26,12 +26,15 @@ pub enum DapStatus { Failed { error: String }, } +#[async_trait(?Send)] pub trait DapDelegate { fn http_client(&self) -> Option>; fn node_runtime(&self) -> Option; fn fs(&self) -> Arc; fn updated_adapters(&self) -> Arc>>; fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus); + fn which(&self, command: &OsStr) -> Option; + async fn shell_env(&self) -> collections::HashMap; } #[derive(PartialEq, Eq, Hash, Debug)] diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index c5eabfb1b8b424..bcd4f492e94cde 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,5 +1,5 @@ use dap::transport::{TcpTransport, Transport}; -use std::{net::Ipv4Addr, path::PathBuf}; +use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; use util::maybe; use crate::*; @@ -56,7 +56,7 @@ impl DebugAdapter for PythonDebugAdapter { async fn get_installed_binary( &self, - _: &dyn DapDelegate, + delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { @@ -87,8 +87,26 @@ impl DebugAdapter for PythonDebugAdapter { None => adapter_info?, }; + let python_cmds = [ + OsStr::new("python3"), + OsStr::new("python"), + OsStr::new("py"), + ]; + let python_path = python_cmds + .iter() + .filter_map(|cmd| { + delegate + .which(cmd) + .and_then(|path| path.to_str().map(|str| str.to_string())) + }) + .find(|_| true); + + let python_path = python_path.ok_or(anyhow!( + "Failed to start debugger because python couldn't be found in PATH" + ))?; + Ok(DebugAdapterBinary { - command: "python3".to_string(), + command: python_path, arguments: Some(vec![ debugpy_dir.join(Self::ADAPTER_PATH).into(), format!("--port={}", self.port).into(), diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index ef53b347d465b4..06639cc3be9be1 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,16 +1,17 @@ -use crate::project_settings::ProjectSettings; -use crate::ProjectPath; +use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectPath}; use anyhow::{anyhow, Context as _, Result}; +use async_trait::async_trait; +use collections::HashMap; use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; -use dap::client::{DebugAdapterClient, DebugAdapterClientId}; -use dap::messages::{Message, Response}; -use dap::requests::{ - Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, - LoadedSources, Modules, Next, Pause, Request as _, Restart, RunInTerminal, Scopes, - SetBreakpoints, SetExpression, SetVariable, StackTrace, StartDebugging, StepIn, StepOut, - Terminate, TerminateThreads, Variables, -}; use dap::{ + client::{DebugAdapterClient, DebugAdapterClientId}, + messages::{Message, Response}, + requests::{ + Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, + LoadedSources, Modules, Next, Pause, Request as _, Restart, RunInTerminal, Scopes, + SetBreakpoints, SetExpression, SetVariable, StackTrace, StartDebugging, StepIn, StepOut, + Terminate, TerminateThreads, Variables, + }, AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, @@ -23,6 +24,8 @@ use dap::{ }; use dap_adapters::build_adapter; use fs::Fs; +use futures::future::Shared; +use futures::FutureExt; use gpui::{EventEmitter, Model, ModelContext, SharedString, Task}; use http_client::HttpClient; use language::{ @@ -33,7 +36,8 @@ use serde_json::Value; use settings::{Settings, WorktreeId}; use smol::lock::Mutex; use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashSet}, + ffi::OsStr, hash::{Hash, Hasher}, path::{Path, PathBuf}, sync::{ @@ -68,10 +72,11 @@ pub struct DebugPosition { pub struct DapStore { next_client_id: AtomicUsize, - delegate: Arc, + delegate: DapAdapterDelegate, breakpoints: BTreeMap>, active_debug_line: Option<(ProjectPath, DebugPosition)>, capabilities: HashMap, + environment: Model, clients: HashMap, } @@ -83,25 +88,46 @@ impl DapStore { node_runtime: Option, fs: Arc, languages: Arc, + environment: Model, cx: &mut ModelContext, ) -> Self { cx.on_app_quit(Self::shutdown_clients).detach(); + let load_shell_env_task = Task::ready(None).shared(); + Self { active_debug_line: None, clients: Default::default(), breakpoints: Default::default(), capabilities: HashMap::default(), next_client_id: Default::default(), - delegate: Arc::new(DapAdapterDelegate::new( + delegate: DapAdapterDelegate::new( http_client.clone(), node_runtime.clone(), fs.clone(), languages.clone(), - )), + load_shell_env_task, + ), + environment, } } + pub fn delegate( + &self, + cwd: &Option, + cx: &mut ModelContext, + ) -> Arc { + let worktree_abs_path = cwd.as_ref().map(|p| Arc::from(p.as_path())); + let task = self.environment.update(cx, |env, cx| { + env.get_environment(None, worktree_abs_path, cx) + }); + + let mut delegate = self.delegate.clone(); + delegate.refresh_shell_env_task(task); + + Arc::new(delegate) + } + pub fn next_client_id(&self) -> DebugAdapterClientId { DebugAdapterClientId(self.next_client_id.fetch_add(1, SeqCst)) } @@ -240,7 +266,7 @@ impl DapStore { pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { let client_id = self.next_client_id(); - let adapter_delegate = self.delegate.clone(); + let adapter_delegate = self.delegate(&config.cwd, cx); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); @@ -280,9 +306,14 @@ impl DapStore { return Err(error); } - Ok(binary) => { + Ok(mut binary) => { adapter_delegate.update_status(adapter.name(), DapStatus::None); + let shell_env = adapter_delegate.shell_env().await; + let mut envs = binary.envs.unwrap_or_default(); + envs.extend(shell_env); + binary.envs = Some(envs); + binary } }; @@ -1305,6 +1336,7 @@ pub struct DapAdapterDelegate { node_runtime: Option, updated_adapters: Arc>>, languages: Arc, + load_shell_env_task: Shared>>>, } impl DapAdapterDelegate { @@ -1313,17 +1345,27 @@ impl DapAdapterDelegate { node_runtime: Option, fs: Arc, languages: Arc, + load_shell_env_task: Shared>>>, ) -> Self { Self { fs, languages, http_client, node_runtime, + load_shell_env_task, updated_adapters: Default::default(), } } + + pub(crate) fn refresh_shell_env_task( + &mut self, + load_shell_env_task: Shared>>>, + ) { + self.load_shell_env_task = load_shell_env_task; + } } +#[async_trait(?Send)] impl dap::adapters::DapDelegate for DapAdapterDelegate { fn http_client(&self) -> Option> { self.http_client.clone() @@ -1353,4 +1395,13 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { self.languages .update_dap_status(LanguageServerName(name), status); } + + fn which(&self, command: &OsStr) -> Option { + which::which(command).ok() + } + + async fn shell_env(&self) -> HashMap { + let task = self.load_shell_env_task.clone(); + task.await.unwrap_or_default() + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6367191365db67..0bbe0d21d236cb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -625,12 +625,15 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); + let environment = ProjectEnvironment::new(&worktree_store, env, cx); + let dap_store = cx.new_model(|cx| { DapStore::new( Some(client.http_client()), Some(node.clone()), fs.clone(), languages.clone(), + environment.clone(), cx, ) }); @@ -651,8 +654,6 @@ impl Project { ) }); - let environment = ProjectEnvironment::new(&worktree_store, env, cx); - let task_store = cx.new_model(|cx| { TaskStore::local( fs.clone(), @@ -804,6 +805,7 @@ impl Project { Some(node.clone()), fs.clone(), languages.clone(), + environment.clone(), cx, ) }); @@ -964,12 +966,15 @@ impl Project { BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx) })?; + let environment = cx.update(|cx| ProjectEnvironment::new(&worktree_store, None, cx))?; + let dap_store = cx.new_model(|cx| { DapStore::new( Some(client.http_client()), None, fs.clone(), languages.clone(), + environment.clone(), cx, ) })?; @@ -1068,7 +1073,7 @@ impl Project { search_history: Self::new_search_history(), search_included_history: Self::new_search_history(), search_excluded_history: Self::new_search_history(), - environment: ProjectEnvironment::new(&worktree_store, None, cx), + environment, remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())), toolchain_store: None, }; diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 1165f2c42f958a..76d9fac8851156 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -73,8 +73,18 @@ impl HeadlessProject { store }); - let dap_store = - cx.new_model(|cx| DapStore::new(None, None, fs.clone(), languages.clone(), cx)); + let environment = project::ProjectEnvironment::new(&worktree_store, None, cx); + + let dap_store = cx.new_model(|cx| { + DapStore::new( + None, + None, + fs.clone(), + languages.clone(), + environment.clone(), + cx, + ) + }); let buffer_store = cx.new_model(|cx| { let mut buffer_store = BufferStore::local(worktree_store.clone(), dap_store.clone(), cx); @@ -91,7 +101,6 @@ impl HeadlessProject { ) }); - let environment = project::ProjectEnvironment::new(&worktree_store, None, cx); let task_store = cx.new_model(|cx| { let mut task_store = TaskStore::local( fs.clone(), From 56f77c31928aeed95f577be550d7c7dda8814b83 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:55:51 -0500 Subject: [PATCH 329/650] Disable the ability to create debug tasks from tasks.json (#61) This was done to simplify the process of setting up a debug task and improve task organization. This commit also improves parsing of debug.json so it's able to ignore misconfiguration debug tasks instead of failing and returning no configured tasks --- crates/project/src/project_settings.rs | 37 +++++++------------------- crates/project/src/project_tests.rs | 1 + crates/project/src/task_inventory.rs | 30 ++++++++++++++------- crates/project/src/task_store.rs | 14 +++++++--- crates/settings/src/settings.rs | 2 +- crates/settings/src/settings_store.rs | 10 +++++-- crates/task/src/debug_format.rs | 4 ++- crates/task/src/lib.rs | 2 +- crates/task/src/task_template.rs | 2 +- crates/tasks_ui/src/modal.rs | 1 - 10 files changed, 55 insertions(+), 48 deletions(-) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index edb7dd252bcde6..2bc43a45cf0255 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -12,14 +12,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, - SettingsSources, SettingsStore, + SettingsSources, SettingsStore, TaskKind, }; use std::{ path::{Path, PathBuf}, sync::Arc, time::Duration, }; -use task::{DebugTaskFile, TaskTemplates, VsCodeTaskFile}; +use task::{TaskTemplates, VsCodeTaskFile}; use util::ResultExt; use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId}; @@ -408,7 +408,7 @@ impl SettingsObserver { ) .unwrap(), ); - (settings_dir, LocalSettingsKind::Tasks) + (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script)) } else if path.ends_with(local_vscode_tasks_file_relative_path()) { let settings_dir = Arc::::from( path.ancestors() @@ -420,7 +420,7 @@ impl SettingsObserver { ) .unwrap(), ); - (settings_dir, LocalSettingsKind::Tasks) + (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script)) } else if path.ends_with(local_debug_file_relative_path()) { let settings_dir = Arc::::from( path.ancestors() @@ -432,12 +432,7 @@ impl SettingsObserver { ) .unwrap(), ); - ( - // Debug task file name has to be unique because it will overwrite tasks - // from .zed/tasks.json file if it is not (It was also being overwritten too) - Arc::from(settings_dir.join("debug").as_path()), - LocalSettingsKind::Tasks, - ) + (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug)) } else if path.ends_with(EDITORCONFIG_NAME) { let Some(settings_dir) = path.parent().map(Arc::from) else { continue; @@ -483,21 +478,6 @@ impl SettingsObserver { "serializing Zed tasks into JSON, file {abs_path:?}" ) }) - } else if abs_path.ends_with(local_debug_file_relative_path()) { - let debug_tasks_content = parse_json_with_comments::(&content).with_context(|| { - format!("Parsing Zed debug tasks, file {abs_path:?}") - })?; - - let zed_debug_tasks = TaskTemplates::try_from(debug_tasks_content) - .with_context(|| { - format!("Converting zed debugger tasks into Zed tasks, file {abs_path:?}") - })?; - - serde_json::to_string(&zed_debug_tasks).with_context(|| { - format!( - "serializing Zed debug tasks into JSON, file {abs_path:?}" - ) - }) } else { Ok(content) } @@ -573,7 +553,7 @@ impl SettingsObserver { } } }), - LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| { + LocalSettingsKind::Tasks(task_kind) => task_store.update(cx, |task_store, cx| { task_store .update_user_tasks( Some(SettingsLocation { @@ -581,6 +561,7 @@ impl SettingsObserver { path: directory.as_ref(), }), file_content.as_deref(), + Some(task_kind), cx, ) .log_err(); @@ -605,7 +586,7 @@ impl SettingsObserver { pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind { match kind { proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings, - proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks, + proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script), proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig, } } @@ -613,7 +594,7 @@ pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSe pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind { match kind { LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings, - LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks, + LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks, LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig, } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 1a0536d067378b..2c299b3ee62c16 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -330,6 +330,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) }]) .to_string(), ), + settings::TaskKind::Script, ) .unwrap(); }); diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index eb982fdcd7d581..8857c978dc3232 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -13,9 +13,10 @@ use collections::{HashMap, HashSet, VecDeque}; use gpui::{AppContext, Context as _, Model}; use itertools::Itertools; use language::{ContextProvider, File, Language, Location}; -use settings::{parse_json_with_comments, SettingsLocation}; +use settings::{parse_json_with_comments, SettingsLocation, TaskKind}; use task::{ - ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName, + DebugTaskDefinition, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, + TaskVariables, VariableName, }; use text::{Point, ToPoint}; use util::{post_inc, NumericPrefixWithSuffix, ResultExt as _}; @@ -33,7 +34,7 @@ pub struct Inventory { #[derive(Debug, Default)] struct ParsedTemplates { global: Vec, - worktree: HashMap, Vec>>, + worktree: HashMap, TaskKind), Vec>>, } /// Kind of a source the tasks are fetched from, used to display more source information in the UI. @@ -261,7 +262,7 @@ impl Inventory { .flat_map(|(directory, templates)| { templates.iter().map(move |template| (directory, template)) }) - .map(move |(directory, template)| { + .map(move |((directory, _task_kind), template)| { ( TaskSourceKind::Worktree { id: worktree, @@ -284,13 +285,19 @@ impl Inventory { &mut self, location: Option>, raw_tasks_json: Option<&str>, + task_kind: TaskKind, ) -> anyhow::Result<()> { let raw_tasks = parse_json_with_comments::>(raw_tasks_json.unwrap_or("[]")) .context("parsing tasks file content as a JSON array")?; - let new_templates = raw_tasks.into_iter().filter_map(|raw_template| { - serde_json::from_value::(raw_template).log_err() - }); + let new_templates = raw_tasks + .into_iter() + .filter_map(|raw_template| match &task_kind { + TaskKind::Script => serde_json::from_value::(raw_template).log_err(), + TaskKind::Debug => serde_json::from_value::(raw_template) + .log_err() + .and_then(|content| content.to_zed_format().log_err()), + }); let parsed_templates = &mut self.templates_from_settings; match location { @@ -300,14 +307,14 @@ impl Inventory { if let Some(worktree_tasks) = parsed_templates.worktree.get_mut(&location.worktree_id) { - worktree_tasks.remove(location.path); + worktree_tasks.remove(&(Arc::from(location.path), task_kind)); } } else { parsed_templates .worktree .entry(location.worktree_id) .or_default() - .insert(Arc::from(location.path), new_templates); + .insert((Arc::from(location.path), task_kind), new_templates); } } None => parsed_templates.global = new_templates.collect(), @@ -584,6 +591,7 @@ mod tests { Some(&mock_tasks_from_names( expected_initial_state.iter().map(|name| name.as_str()), )), + settings::TaskKind::Script, ) .unwrap(); }); @@ -639,6 +647,7 @@ mod tests { .into_iter() .chain(expected_initial_state.iter().map(|name| name.as_str())), )), + settings::TaskKind::Script, ) .unwrap(); }); @@ -763,6 +772,7 @@ mod tests { .iter() .map(|(_, name)| name.as_str()), )), + settings::TaskKind::Script, ) .unwrap(); inventory @@ -774,6 +784,7 @@ mod tests { Some(&mock_tasks_from_names( worktree_1_tasks.iter().map(|(_, name)| name.as_str()), )), + settings::TaskKind::Script, ) .unwrap(); inventory @@ -785,6 +796,7 @@ mod tests { Some(&mock_tasks_from_names( worktree_2_tasks.iter().map(|(_, name)| name.as_str()), )), + settings::TaskKind::Script, ) .unwrap(); }); diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index 45b62697c3df37..acb1a8ebb86267 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -10,7 +10,7 @@ use language::{ ContextProvider as _, Location, }; use rpc::{proto, AnyProtoClient, TypedEnvelope}; -use settings::{watch_config_file, SettingsLocation}; +use settings::{watch_config_file, SettingsLocation, TaskKind}; use task::{TaskContext, TaskVariables, VariableName}; use text::BufferId; use util::ResultExt; @@ -261,6 +261,7 @@ impl TaskStore { &self, location: Option>, raw_tasks_json: Option<&str>, + task_type: Option, cx: &mut ModelContext<'_, Self>, ) -> anyhow::Result<()> { let task_inventory = match self { @@ -272,7 +273,11 @@ impl TaskStore { .filter(|json| !json.is_empty()); task_inventory.update(cx, |inventory, _| { - inventory.update_file_based_tasks(location, raw_tasks_json) + inventory.update_file_based_tasks( + location, + raw_tasks_json, + task_type.unwrap_or(TaskKind::Script), + ) }) } @@ -287,7 +292,7 @@ impl TaskStore { if let Some(user_tasks_content) = user_tasks_content { let Ok(_) = task_store.update(&mut cx, |task_store, cx| { task_store - .update_user_tasks(None, Some(&user_tasks_content), cx) + .update_user_tasks(None, Some(&user_tasks_content), None, cx) .log_err(); }) else { return; @@ -295,7 +300,8 @@ impl TaskStore { } while let Some(user_tasks_content) = user_tasks_file_rx.next().await { let Ok(()) = task_store.update(&mut cx, |task_store, cx| { - let result = task_store.update_user_tasks(None, Some(&user_tasks_content), cx); + let result = + task_store.update_user_tasks(None, Some(&user_tasks_content), None, cx); if let Err(err) = &result { log::error!("Failed to load user tasks: {err}"); cx.emit(crate::Event::Toast { diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 40c371d9951ff1..2fafdfb94b2b88 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -15,7 +15,7 @@ pub use keymap_file::KeymapFile; pub use settings_file::*; pub use settings_store::{ parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, - SettingsSources, SettingsStore, + SettingsSources, SettingsStore, TaskKind, }; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 620055a9712d77..f2613b514caa2b 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -206,10 +206,16 @@ impl FromStr for Editorconfig { #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum LocalSettingsKind { Settings, - Tasks, + Tasks(TaskKind), Editorconfig, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum TaskKind { + Debug, + Script, +} + impl Global for SettingsStore {} #[derive(Debug)] @@ -600,7 +606,7 @@ impl SettingsStore { .map(|content| content.trim()) .filter(|content| !content.is_empty()), ) { - (LocalSettingsKind::Tasks, _) => { + (LocalSettingsKind::Tasks(_), _) => { return Err(InvalidSettingsError::Tasks { message: "Attempted to submit tasks into the settings store".to_string(), }) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index f590e865f32934..1f83d53eb926b3 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -128,6 +128,7 @@ pub enum DebugConnectionType { STDIO, } +/// This struct represent a user created debug task #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugTaskDefinition { @@ -148,7 +149,8 @@ pub struct DebugTaskDefinition { } impl DebugTaskDefinition { - fn to_zed_format(self) -> anyhow::Result { + /// Translate from debug definition to a task template + pub fn to_zed_format(self) -> anyhow::Result { let command = "".to_string(); let cwd = self.cwd.clone().map(PathBuf::from).take_if(|p| p.exists()); diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index e7e05bd5666aec..c81a8b17638407 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -16,7 +16,7 @@ use std::str::FromStr; pub use debug_format::{ AttachConfig, CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, - DebugRequestType, DebugTaskFile, TCPHost, + DebugRequestType, DebugTaskDefinition, DebugTaskFile, TCPHost, }; pub use task_template::{ HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, TaskType, diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 241676cbe7ff8d..ab116d6211fd58 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -52,7 +52,7 @@ pub struct TaskTemplate { #[serde(default)] pub hide: HideStrategy, /// If this task should start a debugger or not - #[serde(default)] + #[serde(default, skip)] pub task_type: TaskType, /// Represents the tags which this template attaches to. Adding this removes this task from other UI. #[serde(default)] diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index ce13e18ba03506..d1ffac93976bc7 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -319,7 +319,6 @@ impl PickerDelegate for TasksModalDelegate { omit_history_entry, cx, ), - // TODO: Should create a schedule_resolved_debug_task function // This would allow users to access to debug history and other issues TaskType::Debug(_) => workspace.project().update(cx, |project, cx| { project.start_debug_adapter_client_from_task(task, cx) From 9772573816914384bda3a446215a0e56fb4356bd Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 6 Nov 2024 19:06:29 -0500 Subject: [PATCH 330/650] Fix adapter completion not showing in debug.json schemars seems to have a bug when generating schema for a struct with more than one flatten field. Only the first flatten field works correctly and the second one doesn't show up in the schema. --- crates/task/src/debug_format.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 1f83d53eb926b3..4816675fa7c650 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -47,7 +47,7 @@ pub struct AttachConfig { /// Represents the type that will determine which request to call on the debug adapter #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase", tag = "request")] +#[serde(rename_all = "lowercase")] pub enum DebugRequestType { /// Call the `launch` request on the debug adapter #[default] @@ -132,18 +132,18 @@ pub enum DebugConnectionType { #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugTaskDefinition { + /// The adapter to run + #[serde(flatten)] + kind: DebugAdapterKind, + /// The type of request that should be called on the debug adapter + #[serde(default)] + request: DebugRequestType, /// Name of the debug task label: String, /// Program to run the debugger on program: Option, /// The current working directory of your project cwd: Option, - /// The type of request that should be called on the debug adapter - #[serde(default, flatten)] - request: DebugRequestType, - /// The adapter to run - #[serde(flatten)] - kind: DebugAdapterKind, /// Additional initialization arguments to be sent on DAP initialization initialize_args: Option, } From bae6edba88270722d7a170bc7861c8b4e4bff55c Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 6 Nov 2024 23:55:14 -0500 Subject: [PATCH 331/650] Fix failing unit test --- crates/task/src/debug_format.rs | 2 +- crates/task/src/task_template.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 4816675fa7c650..04f135fca48c6f 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -108,7 +108,7 @@ pub struct DebugAdapterConfig { #[serde(flatten)] pub kind: DebugAdapterKind, /// The type of request that should be called on the debug adapter - #[serde(default, flatten)] + #[serde(default)] pub request: DebugRequestType, /// The program that you trying to debug pub program: Option, diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index ab116d6211fd58..6e5f67c244b09b 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -101,7 +101,6 @@ mod deserialization_tests { let json = json!({ "type": "debug", "adapter": "python", - "request": "launch", "program": "main" }); From 9e37b4708f2026d403fb822a70d7ed6aacae7bce Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 8 Nov 2024 02:15:07 -0500 Subject: [PATCH 332/650] Go Adapter Implementation (#64) * Go DAP WIP * Start work on getting go adapter working * Get beta version of go adapter working & fix breakpoint line msgs This adapter only works if a user has Go & delve in their PATH. It doesn't automatically download & updates itself because the official download process from the Delve docs would add delve to a user's PATH. I want to discuss with the Zed dev team & Remco if that is behavior we're ok with because typical downloads don't affect a user's PATH. This PR also fixes a bug where some breakpoint line numbers were incorrect when sending to active DAP servers. --- crates/dap_adapters/src/dap_adapters.rs | 3 + crates/dap_adapters/src/go.rs | 93 +++++++++++++++++++++++++ crates/project/src/dap_store.rs | 21 ++++-- crates/task/src/debug_format.rs | 3 + 4 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 crates/dap_adapters/src/go.rs diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 3c9949d0ec4caa..d905ebd0a1c8cf 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -1,4 +1,5 @@ mod custom; +mod go; mod javascript; mod lldb; mod php; @@ -11,6 +12,7 @@ use dap::adapters::{ self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, GithubRepo, }; +use go::GoDebugAdapter; use javascript::JsDebugAdapter; use lldb::LldbDebugAdapter; use php::PhpDebugAdapter; @@ -30,5 +32,6 @@ pub async fn build_adapter(kind: &DebugAdapterKind) -> Result Ok(Box::new(LldbDebugAdapter::new())), + DebugAdapterKind::Go(host) => Ok(Box::new(GoDebugAdapter::new(host).await?)), } } diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs new file mode 100644 index 00000000000000..ae0973eafe17b5 --- /dev/null +++ b/crates/dap_adapters/src/go.rs @@ -0,0 +1,93 @@ +use dap::transport::{TcpTransport, Transport}; +use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; + +use crate::*; + +pub(crate) struct GoDebugAdapter { + port: u16, + host: Ipv4Addr, + timeout: Option, +} + +impl GoDebugAdapter { + const _ADAPTER_NAME: &'static str = "delve"; + // const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; + + pub(crate) async fn new(host: &TCPHost) -> Result { + Ok(GoDebugAdapter { + port: TcpTransport::port(host).await?, + host: host.host(), + timeout: host.timeout, + }) + } +} + +#[async_trait(?Send)] +impl DebugAdapter for GoDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::_ADAPTER_NAME.into()) + } + + fn transport(&self) -> Box { + Box::new(TcpTransport::new(self.host, self.port, self.timeout)) + } + + async fn get_binary( + &self, + delegate: &dyn DapDelegate, + config: &DebugAdapterConfig, + adapter_path: Option, + ) -> Result { + self.get_installed_binary(delegate, config, adapter_path) + .await + } + + async fn fetch_latest_adapter_version( + &self, + _delegate: &dyn DapDelegate, + ) -> Result { + // let github_repo = GithubRepo { + // repo_name: Self::ADAPTER_NAME.into(), + // repo_owner: "go-delve".into(), + // }; + + // adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + unimplemented!("This adapter is used from path for now"); + } + + async fn install_binary( + &self, + version: AdapterVersion, + delegate: &dyn DapDelegate, + ) -> Result<()> { + adapters::download_adapter_from_github(self.name(), version, delegate).await?; + Ok(()) + } + + async fn get_installed_binary( + &self, + delegate: &dyn DapDelegate, + config: &DebugAdapterConfig, + _user_installed_path: Option, + ) -> Result { + let delve_path = delegate + .which(OsStr::new("dlv")) + .and_then(|p| p.to_str().map(|p| p.to_string())) + .ok_or(anyhow!("Dlv not found in path"))?; + + let ip_address = format!("{}:{}", self.host, self.port); + let version = "N/A".into(); + + Ok(DebugAdapterBinary { + command: delve_path, + arguments: Some(vec!["dap".into(), "--listen".into(), ip_address.into()]), + cwd: config.cwd.clone(), + envs: None, + version, + }) + } + + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + json!({"program": config.program, "subProcess": true}) + } +} diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 06639cc3be9be1..e3327830c32faa 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -83,6 +83,8 @@ pub struct DapStore { impl EventEmitter for DapStore {} impl DapStore { + const INDEX_STARTS_AT_ONE: bool = true; + pub fn new( http_client: Option>, node_runtime: Option, @@ -396,8 +398,8 @@ impl DapStore { supports_memory_references: Some(true), supports_progress_reporting: Some(false), supports_invalidated_event: Some(false), - lines_start_at1: Some(false), - columns_start_at1: Some(false), + lines_start_at1: Some(Self::INDEX_STARTS_AT_ONE), + columns_start_at1: Some(Self::INDEX_STARTS_AT_ONE), supports_memory_event: Some(false), supports_args_can_be_interpreted_by_shell: Some(false), supports_start_debugging_request: Some(true), @@ -1082,13 +1084,17 @@ impl DapStore { &self, client_id: &DebugAdapterClientId, absolute_file_path: Arc, - breakpoints: Vec, + mut breakpoints: Vec, cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { return Task::ready(Err(anyhow!("Could not found client"))); }; + if Self::INDEX_STARTS_AT_ONE { + breakpoints.iter_mut().for_each(|bp| bp.line += 1u64) + } + cx.background_executor().spawn(async move { client .request::(SetBreakpointsArguments { @@ -1235,11 +1241,16 @@ impl Breakpoint { .map(|position| buffer.summary_for_anchor::(&position).row) .unwrap_or(self.cache_position) as u64; + let log_message = match &self.kind { + BreakpointKind::Standard => None, + BreakpointKind::Log(message) => Some(message.clone().to_string()), + }; + SourceBreakpoint { line, condition: None, hit_condition: None, - log_message: None, + log_message, column: None, mode: None, } @@ -1290,7 +1301,7 @@ impl Breakpoint { Some(buffer) => SerializedBreakpoint { position: self .active_position - .map(|position| buffer.summary_for_anchor::(&position).row + 1u32) + .map(|position| buffer.summary_for_anchor::(&position).row) .unwrap_or(self.cache_position), path, kind: self.kind.clone(), diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 04f135fca48c6f..c039a987feccbf 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -69,6 +69,8 @@ pub enum DebugAdapterKind { Php(TCPHost), /// Use vscode-js-debug Javascript(TCPHost), + /// Use delve + Go(TCPHost), /// Use lldb Lldb, } @@ -82,6 +84,7 @@ impl DebugAdapterKind { Self::Php(_) => "PHP", Self::Javascript(_) => "JavaScript", Self::Lldb => "LLDB", + Self::Go(_) => "Go", } } } From 9781292cbab6c592954fa3c57220470c6b9bc294 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 6 Nov 2024 23:55:14 -0500 Subject: [PATCH 333/650] Fix breakpoint deserialization when loading workspaces from database --- crates/project/src/dap_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e3327830c32faa..bd7d24de4114ed 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -241,7 +241,7 @@ impl DapStore { .or_default() .insert(Breakpoint { active_position: None, - cache_position: serialize_breakpoint.position.saturating_sub(1u32), + cache_position: serialize_breakpoint.position, kind: serialize_breakpoint.kind, }); } From ffaaadf2b98458f29a030c9d3fcc95f5ce9a3fb5 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 8 Nov 2024 03:03:10 -0500 Subject: [PATCH 334/650] Get lldb-dap working on linux if already installed by user --- crates/dap_adapters/src/lldb.rs | 53 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index c54df216d15177..b79e6d754b2223 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -1,3 +1,5 @@ +use std::ffi::OsStr; + use anyhow::Result; use async_trait::async_trait; use dap::transport::{StdioTransport, Transport}; @@ -25,17 +27,36 @@ impl DebugAdapter for LldbDebugAdapter { Box::new(StdioTransport::new()) } - #[cfg(target_os = "macos")] async fn get_binary( &self, - _: &dyn DapDelegate, + delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - _: Option, + user_installed_path: Option, ) -> Result { - let output = std::process::Command::new("xcrun") - .args(&["-f", "lldb-dap"]) - .output()?; - let lldb_dap_path = String::from_utf8(output.stdout)?.trim().to_string(); + let user_setting_path = user_installed_path + .filter(|p| p.exists()) + .and_then(|p| p.to_str().map(|s| s.to_string())); + + let lldb_dap_path = if cfg!(target_os = "macos") { + std::process::Command::new("xcrun") + .args(&["-f", "lldb-dap"]) + .output() + .ok() + .and_then(|output| String::from_utf8(output.stdout).ok()) + .map(|path| path.trim().to_string()) + .ok_or(anyhow!("Failed to find lldb-dap in user's path")) + } else { + delegate + .which(OsStr::new("lldb-dap")) + .and_then(|p| p.to_str().map(|s| s.to_string())) + .ok_or(anyhow!("Could not find lldb-dap in path")) + }; + + if lldb_dap_path.is_err() && user_setting_path.is_none() { + bail!("Could not find lldb-dap path or it's not installed"); + } + + let lldb_dap_path = user_setting_path.unwrap_or(lldb_dap_path?); Ok(DebugAdapterBinary { command: lldb_dap_path, @@ -46,28 +67,16 @@ impl DebugAdapter for LldbDebugAdapter { }) } - #[cfg(not(target_os = "macos"))] - async fn get_binary( - &self, - _: &dyn DapDelegate, - _: &DebugAdapterConfig, - _: Option, - ) -> Result { - Err(anyhow::anyhow!( - "LLDB-DAP is only supported on macOS (Right now)" - )) - } - async fn install_binary( &self, _version: AdapterVersion, _delegate: &dyn DapDelegate, ) -> Result<()> { - bail!("LLDB debug adapter cannot be installed") + unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)") } async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { - bail!("Fetch latest adapter version not implemented for lldb (yet)") + unimplemented!("Fetch latest adapter version not implemented for lldb (yet)") } async fn get_installed_binary( @@ -76,7 +85,7 @@ impl DebugAdapter for LldbDebugAdapter { _: &DebugAdapterConfig, _: Option, ) -> Result { - bail!("LLDB debug adapter cannot be installed") + unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)") } fn request_args(&self, config: &DebugAdapterConfig) -> Value { From c19c86085b838f74b80b2796be3c47bb2a9ddf2d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 10 Nov 2024 18:20:16 +0100 Subject: [PATCH 335/650] Allow users to toggle ignore breakpoints (#62) * Add allow users to ignore breakpoints * Add different colors for both states * Add source name for breakpoints * Move ignore breakpoints to dap store * Change icon instead of color * Add action for ignore breakpoints for a client * Remove spacing * Fix compile error * Return task instead of detaching itself --- assets/icons/debug_ignore_breakpoints.svg | 1 + crates/debugger_ui/src/debugger_panel.rs | 2 + crates/debugger_ui/src/debugger_panel_item.rs | 31 ++++++++ crates/debugger_ui/src/lib.rs | 15 +++- crates/project/src/dap_store.rs | 36 ++++++--- crates/project/src/project.rs | 77 ++++++++++++++++--- crates/ui/src/components/icon.rs | 1 + crates/workspace/src/workspace.rs | 13 +++- 8 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 assets/icons/debug_ignore_breakpoints.svg diff --git a/assets/icons/debug_ignore_breakpoints.svg b/assets/icons/debug_ignore_breakpoints.svg new file mode 100644 index 00000000000000..ba7074e083c700 --- /dev/null +++ b/assets/icons/debug_ignore_breakpoints.svg @@ -0,0 +1 @@ + diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 47014ccfe8bd86..121f56dfdf14cf 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -32,6 +32,7 @@ use workspace::{ }; use workspace::{ pane, Continue, Disconnect, Pane, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, + ToggleIgnoreBreakpoints, }; pub enum DebugPanelEvent { @@ -174,6 +175,7 @@ impl DebugPanel { TypeId::of::(), TypeId::of::(), TypeId::of::(), + TypeId::of::(), ]; if has_active_session { diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 22aa393aa3780b..62168d1f23d30d 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -477,6 +477,18 @@ impl DebugPanelItem { .detach_and_log_err(cx); }); } + + pub fn toggle_ignore_breakpoints(&mut self, cx: &mut ViewContext) { + self.workspace + .update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project + .toggle_ignore_breakpoints(&self.client_id, cx) + .detach_and_log_err(cx); + }) + }) + .ok(); + } } impl EventEmitter for DebugPanelItem {} @@ -633,6 +645,25 @@ impl Render for DebugPanelItem { || thread_status == ThreadStatus::Ended, ) .tooltip(move |cx| Tooltip::text("Disconnect", cx)), + ) + .child( + IconButton::new( + "debug-ignore-breakpoints", + if self.dap_store.read(cx).ignore_breakpoints(&self.client_id) { + IconName::DebugIgnoreBreakpoints + } else { + IconName::DebugBreakpoint + }, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + this.toggle_ignore_breakpoints(cx); + })) + .disabled( + thread_status == ThreadStatus::Exited + || thread_status == ThreadStatus::Ended, + ) + .tooltip(move |cx| Tooltip::text("Ignore breakpoints", cx)), ), ) .child( diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index f9dbe505f1420e..7e05878aeb451f 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -5,7 +5,7 @@ use settings::Settings; use ui::ViewContext; use workspace::{ Continue, Pause, Restart, ShutdownDebugAdapters, Start, StepInto, StepOut, StepOver, Stop, - Workspace, + ToggleIgnoreBreakpoints, Workspace, }; mod attach_modal; @@ -102,6 +102,19 @@ pub fn init(cx: &mut AppContext) { active_item.update(cx, |item, cx| item.restart_client(cx)) }); }) + .register_action( + |workspace: &mut Workspace, _: &ToggleIgnoreBreakpoints, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) + }); + }, + ) .register_action(|workspace: &mut Workspace, _: &Pause, cx| { let debug_panel = workspace.panel::(cx).unwrap(); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index bd7d24de4114ed..53d0d57dc070ec 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -73,6 +73,7 @@ pub struct DebugPosition { pub struct DapStore { next_client_id: AtomicUsize, delegate: DapAdapterDelegate, + ignore_breakpoints: HashSet, breakpoints: BTreeMap>, active_debug_line: Option<(ProjectPath, DebugPosition)>, capabilities: HashMap, @@ -103,6 +104,7 @@ impl DapStore { breakpoints: Default::default(), capabilities: HashMap::default(), next_client_id: Default::default(), + ignore_breakpoints: Default::default(), delegate: DapAdapterDelegate::new( http_client.clone(), node_runtime.clone(), @@ -192,6 +194,16 @@ impl DapStore { &self.breakpoints } + pub fn ignore_breakpoints(&self, client_id: &DebugAdapterClientId) -> bool { + self.ignore_breakpoints.contains(client_id) + } + + pub fn toggle_ignore_breakpoints(&mut self, client_id: &DebugAdapterClientId) { + if !self.ignore_breakpoints.remove(client_id) { + self.ignore_breakpoints.insert(*client_id); + } + } + pub fn breakpoint_at_row( &self, row: u32, @@ -1024,6 +1036,7 @@ impl DapStore { cx.emit(DapStoreEvent::DebugClientStopped(*client_id)); + self.ignore_breakpoints.remove(client_id); let capabilities = self.capabilities.remove(client_id); cx.background_executor().spawn(async move { @@ -1059,7 +1072,7 @@ impl DapStore { buffer_snapshot: BufferSnapshot, edit_action: BreakpointEditAction, cx: &mut ModelContext, - ) { + ) -> Task> { let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); match edit_action { @@ -1077,7 +1090,6 @@ impl DapStore { cx.notify(); self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) - .detach(); } pub fn send_breakpoints( @@ -1085,6 +1097,7 @@ impl DapStore { client_id: &DebugAdapterClientId, absolute_file_path: Arc, mut breakpoints: Vec, + ignore: bool, cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { @@ -1100,7 +1113,9 @@ impl DapStore { .request::(SetBreakpointsArguments { source: Source { path: Some(String::from(absolute_file_path.to_string_lossy())), - name: None, + name: absolute_file_path + .file_name() + .map(|name| name.to_string_lossy().to_string()), source_reference: None, presentation_hint: None, origin: None, @@ -1108,8 +1123,8 @@ impl DapStore { adapter_data: None, checksums: None, }, - breakpoints: Some(breakpoints), - source_modified: None, + breakpoints: Some(if ignore { Vec::default() } else { breakpoints }), + source_modified: Some(false), lines: None, }) .await?; @@ -1124,15 +1139,15 @@ impl DapStore { buffer_path: PathBuf, buffer_snapshot: BufferSnapshot, cx: &mut ModelContext, - ) -> Task<()> { + ) -> Task> { let clients = self.running_clients().collect::>(); if clients.is_empty() { - return Task::ready(()); + return Task::ready(Ok(())); } let Some(breakpoints) = self.breakpoints.get(project_path) else { - return Task::ready(()); + return Task::ready(Ok(())); }; let source_breakpoints = breakpoints @@ -1146,12 +1161,15 @@ impl DapStore { &client.id(), Arc::from(buffer_path.clone()), source_breakpoints.clone(), + self.ignore_breakpoints(&client.id()), cx, )) } cx.background_executor().spawn(async move { - futures::future::join_all(tasks).await; + futures::future::try_join_all(tasks).await?; + + Ok(()) }) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a6e7794f90ea68..5a4547377f7186 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1243,7 +1243,13 @@ impl Project { .collect::>(); tasks.push(self.dap_store.update(cx, |store, cx| { - store.send_breakpoints(client_id, abs_path, source_breakpoints, cx) + store.send_breakpoints( + client_id, + abs_path, + source_breakpoints, + store.ignore_breakpoints(client_id), + cx, + ) })); } @@ -1337,6 +1343,57 @@ impl Project { result } + pub fn toggle_ignore_breakpoints( + &self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Task> { + let tasks = self.dap_store.update(cx, |store, cx| { + store.toggle_ignore_breakpoints(client_id); + + let mut tasks = Vec::new(); + + for (project_path, breakpoints) in store.breakpoints() { + let Some((buffer, buffer_path)) = maybe!({ + let buffer = self + .buffer_store + .read_with(cx, |store, cx| store.get_by_path(project_path, cx))?; + + let buffer = buffer.read(cx); + let project_path = buffer.project_path(cx)?; + let worktree = self.worktree_for_id(project_path.clone().worktree_id, cx)?; + Some(( + buffer, + worktree.read(cx).absolutize(&project_path.path).ok()?, + )) + }) else { + continue; + }; + + tasks.push( + store.send_breakpoints( + client_id, + Arc::from(buffer_path), + breakpoints + .into_iter() + .map(|breakpoint| breakpoint.to_source_breakpoint(buffer)) + .collect::>(), + store.ignore_breakpoints(client_id), + cx, + ), + ); + } + + tasks + }); + + cx.background_executor().spawn(async move { + try_join_all(tasks).await?; + + Ok(()) + }) + } + /// Sends updated breakpoint information of one file to all active debug adapters /// /// This function is called whenever a breakpoint is toggled, and it doesn't need @@ -1365,14 +1422,16 @@ impl Project { }; self.dap_store.update(cx, |store, cx| { - store.toggle_breakpoint_for_buffer( - &project_path, - breakpoint, - buffer_path, - buffer.read(cx).snapshot(), - edit_action, - cx, - ); + store + .toggle_breakpoint_for_buffer( + &project_path, + breakpoint, + buffer_path, + buffer.read(cx).snapshot(), + edit_action, + cx, + ) + .detach_and_log_err(cx); }); } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index d8be1c27f9091d..0b8997053495f5 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -173,6 +173,7 @@ pub enum IconName { TextSnippet, Dash, DebugBreakpoint, + DebugIgnoreBreakpoints, DebugPause, DebugContinue, DebugStepOver, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0d277fff055ee6..fbded0f7c46e79 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -130,7 +130,18 @@ actions!(assistant, [ShowConfiguration]); actions!( debugger, - [Start, Continue, Disconnect, Pause, Restart, StepInto, StepOver, StepOut, Stop] + [ + Start, + Continue, + Disconnect, + Pause, + Restart, + StepInto, + StepOver, + StepOut, + Stop, + ToggleIgnoreBreakpoints + ] ); actions!( From 0976e85eb0647a1935e59afefcf67689064a29de Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 10 Nov 2024 19:37:40 +0100 Subject: [PATCH 336/650] Fallback to disconnect if client does not support terminate request for shutdown --- crates/project/src/dap_store.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 53d0d57dc070ec..cfdc74ec38d281 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1058,6 +1058,14 @@ impl DapStore { restart: Some(false), }) .await; + } else { + let _ = client + .request::(DisconnectArguments { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }) + .await; } client.shutdown().await From 7cec577daad7dea9f6f432b7a06811b62a9eb352 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 10 Nov 2024 19:48:54 +0100 Subject: [PATCH 337/650] Add request timeout and add more logging for dropping tasks --- crates/dap/src/client.rs | 90 ++++++++++--- crates/dap/src/transport.rs | 221 ++++++++++++++++++++------------ crates/project/src/dap_store.rs | 2 +- 3 files changed, 211 insertions(+), 102 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 068a5a91e07eb6..0618a6f921f321 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -7,17 +7,21 @@ use dap_types::{ messages::{Message, Response}, requests::Request, }; -use gpui::{AppContext, AsyncAppContext}; -use smol::channel::{bounded, Receiver, Sender}; +use futures::{channel::oneshot, select, FutureExt as _}; +use gpui::{AppContext, AsyncAppContext, BackgroundExecutor}; +use smol::channel::{Receiver, Sender}; use std::{ hash::Hash, sync::{ atomic::{AtomicU64, Ordering}, Arc, Mutex, }, + time::Duration, }; use task::{DebugAdapterConfig, DebugRequestType}; +const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(15); + #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ThreadStatus { #[default] @@ -34,6 +38,7 @@ pub struct DebugAdapterClientId(pub usize); pub struct DebugAdapterClient { id: DebugAdapterClientId, sequence_count: AtomicU64, + executor: BackgroundExecutor, adapter: Arc>, transport_delegate: TransportDelegate, config: Arc>, @@ -44,6 +49,7 @@ impl DebugAdapterClient { id: DebugAdapterClientId, config: DebugAdapterConfig, adapter: Arc>, + cx: &AsyncAppContext, ) -> Self { let transport_delegate = TransportDelegate::new(adapter.transport()); @@ -53,6 +59,7 @@ impl DebugAdapterClient { transport_delegate, sequence_count: AtomicU64::new(1), config: Arc::new(Mutex::new(config)), + executor: cx.background_executor().clone(), } } @@ -68,12 +75,16 @@ impl DebugAdapterClient { let (server_rx, server_tx) = self.transport_delegate.start(binary, cx).await?; // start handling events/reverse requests - cx.spawn(|mut cx| async move { - Self::handle_receive_messages(server_rx, server_tx, message_handler, &mut cx).await + cx.update(|cx| { + cx.spawn({ + let server_tx = server_tx.clone(); + |mut cx| async move { + Self::handle_receive_messages(server_rx, server_tx, message_handler, &mut cx) + .await + } + }) + .detach_and_log_err(cx); }) - .detach(); - - Ok(()) } async fn handle_receive_messages( @@ -85,19 +96,26 @@ impl DebugAdapterClient { where F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { - while let Ok(payload) = server_rx.recv().await { - match payload { - Message::Event(ev) => cx.update(|cx| event_handler(Message::Event(ev), cx))?, - Message::Response(_) => unreachable!(), - Message::Request(req) => { - cx.update(|cx| event_handler(Message::Request(req), cx))? - } + let result = loop { + let message = match server_rx.recv().await { + Ok(message) => message, + Err(e) => break Err(e.into()), }; - } + + if let Err(e) = match message { + Message::Event(ev) => cx.update(|cx| event_handler(Message::Event(ev), cx)), + Message::Request(req) => cx.update(|cx| event_handler(Message::Request(req), cx)), + Message::Response(_) => unreachable!(), + } { + break Err(e); + } + }; drop(client_tx); - anyhow::Ok(()) + log::debug!("Handle receive messages dropped"); + + result } /// Send a request to an adapter and get a response back @@ -105,7 +123,7 @@ impl DebugAdapterClient { pub async fn request(&self, arguments: R::Arguments) -> Result { let serialized_arguments = serde_json::to_value(arguments)?; - let (callback_tx, callback_rx) = bounded::>(1); + let (callback_tx, callback_rx) = oneshot::channel::>(); let sequence_id = self.next_sequence_id(); @@ -119,13 +137,43 @@ impl DebugAdapterClient { .add_pending_request(sequence_id, callback_tx) .await; + log::debug!( + "Send `{}` request with sequence_id: {}", + R::COMMAND.to_string(), + sequence_id + ); + self.send_message(Message::Request(request)).await?; - let response = callback_rx.recv().await??; + log::debug!( + "Start receiving response for: `{}` sequence_id: {}", + R::COMMAND.to_string(), + sequence_id + ); + + let mut timeout = self.executor.timer(DAP_REQUEST_TIMEOUT).fuse(); + let command = R::COMMAND.to_string(); + + select! { + response = callback_rx.fuse() => { + log::debug!( + "Received response for: `{}` sequence_id: {}", + command, + sequence_id + ); + + let response = response??; + match response.success { + true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), + false => Err(anyhow!("Request failed")), + } + } - match response.success { - true => Ok(serde_json::from_value(response.body.unwrap_or_default())?), - false => Err(anyhow!("Request failed")), + _ = timeout => { + self.transport_delegate.cancel_pending_request(&sequence_id).await; + log::error!("Cancelled DAP request for {command:?} id {sequence_id} which took over {DAP_REQUEST_TIMEOUT:?}"); + anyhow::bail!("DAP request timeout"); + } } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index fe500a77e65ecf..7ec000f4d08c42 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -4,7 +4,9 @@ use dap_types::{ messages::{Message, Response}, ErrorResponse, }; -use futures::{select, AsyncBufRead, AsyncReadExt as _, AsyncWrite, FutureExt as _}; +use futures::{ + channel::oneshot, select, AsyncBufRead, AsyncReadExt as _, AsyncWrite, FutureExt as _, +}; use gpui::AsyncAppContext; use settings::Settings as _; use smallvec::SmallVec; @@ -23,6 +25,7 @@ use std::{ time::Duration, }; use task::TCPHost; +use util::ResultExt as _; use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; @@ -60,7 +63,7 @@ impl TransportParams { } } -type Requests = Arc>>>>; +type Requests = Arc>>>>; type LogHandlers = Arc>>; pub(crate) struct TransportDelegate { @@ -69,15 +72,15 @@ pub(crate) struct TransportDelegate { pending_requests: Requests, transport: Box, process: Arc>>, - server_tx: Option>, + server_tx: Arc>>>, } impl TransportDelegate { pub fn new(transport: Box) -> Self { Self { transport, - server_tx: None, process: Default::default(), + server_tx: Default::default(), log_handlers: Default::default(), current_requests: Default::default(), pending_requests: Default::default(), @@ -94,39 +97,46 @@ impl TransportDelegate { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); - if let Some(stdout) = params.process.stdout.take() { - cx.background_executor() - .spawn(Self::handle_adapter_log(stdout, self.log_handlers.clone())) - .detach(); - } + cx.update(|cx| { + if let Some(stdout) = params.process.stdout.take() { + cx.background_executor() + .spawn(Self::handle_adapter_log(stdout, self.log_handlers.clone())) + .detach_and_log_err(cx); + } - cx.background_executor() - .spawn(Self::handle_output( - params.output, - client_tx, - self.pending_requests.clone(), - self.log_handlers.clone(), - )) - .detach(); + cx.background_executor() + .spawn(Self::handle_output( + params.output, + client_tx, + self.pending_requests.clone(), + self.log_handlers.clone(), + )) + .detach_and_log_err(cx); + + if let Some(stderr) = params.process.stderr.take() { + cx.background_executor() + .spawn(Self::handle_error(stderr, self.log_handlers.clone())) + .detach_and_log_err(cx); + } - if let Some(stderr) = params.process.stderr.take() { cx.background_executor() - .spawn(Self::handle_error(stderr, self.log_handlers.clone())) - .detach(); - } + .spawn(Self::handle_input( + params.input, + client_rx, + self.current_requests.clone(), + self.pending_requests.clone(), + self.log_handlers.clone(), + )) + .detach_and_log_err(cx); + })?; - cx.background_executor() - .spawn(Self::handle_input( - params.input, - client_rx, - self.current_requests.clone(), - self.pending_requests.clone(), - self.log_handlers.clone(), - )) - .detach(); + { + let mut lock = self.process.lock().await; + *lock = Some(params.process); - self.process = Arc::new(Mutex::new(Some(params.process))); - self.server_tx = Some(server_tx.clone()); + let mut lock = self.server_tx.lock().await; + *lock = Some(server_tx.clone()); + } Ok((server_rx, server_tx)) } @@ -134,18 +144,23 @@ impl TransportDelegate { pub(crate) async fn add_pending_request( &self, sequence_id: u64, - request: Sender>, + request: oneshot::Sender>, ) { let mut pending_requests = self.pending_requests.lock().await; pending_requests.insert(sequence_id, request); } + pub(crate) async fn cancel_pending_request(&self, sequence_id: &u64) { + let mut pending_requests = self.pending_requests.lock().await; + pending_requests.remove(sequence_id); + } + pub(crate) async fn send_message(&self, message: Message) -> Result<()> { - if let Some(server_tx) = self.server_tx.as_ref() { + if let Some(server_tx) = self.server_tx.lock().await.as_ref() { server_tx .send(message) .await - .map_err(|e| anyhow!("Failed to send response back: {}", e)) + .map_err(|e| anyhow!("Failed to send message: {}", e)) } else { Err(anyhow!("Server tx already dropped")) } @@ -154,16 +169,29 @@ impl TransportDelegate { async fn handle_adapter_log(stdout: ChildStdout, log_handlers: LogHandlers) -> Result<()> { let mut reader = BufReader::new(stdout); let mut line = String::new(); - while reader.read_line(&mut line).await? > 0 { + + let result = loop { + line.truncate(0); + + let bytes_read = match reader.read_line(&mut line).await { + Ok(bytes_read) => bytes_read, + Err(e) => break Err(e.into()), + }; + + if bytes_read == 0 { + break Err(anyhow!("Debugger log stream closed")); + } + for (kind, handler) in log_handlers.lock().iter_mut() { if matches!(kind, LogKind::Adapter) { handler(IoKind::StdOut, line.as_str()); } } - line.truncate(0); - } + }; - Ok(()) + log::debug!("Handle adapter log dropped"); + + result } async fn handle_input( @@ -173,31 +201,47 @@ impl TransportDelegate { pending_requests: Requests, log_handlers: LogHandlers, ) -> Result<()> { - while let Ok(payload) = client_rx.recv().await { - if let Message::Request(request) = &payload { - if let Some(sender) = current_requests.lock().await.remove(&request.seq) { - pending_requests.lock().await.insert(request.seq, sender); - } - } + let result = loop { + match client_rx.recv().await { + Ok(message) => { + if let Message::Request(request) = &message { + if let Some(sender) = current_requests.lock().await.remove(&request.seq) { + pending_requests.lock().await.insert(request.seq, sender); + } + } + + let message = match serde_json::to_string(&message) { + Ok(message) => message, + Err(e) => break Err(e.into()), + }; - let message = serde_json::to_string(&payload)?; + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdIn, &message); + } + } + + if let Err(e) = server_stdin + .write_all( + format!("Content-Length: {}\r\n\r\n{}", message.len(), message) + .as_bytes(), + ) + .await + { + break Err(e.into()); + } - for (kind, log_handler) in log_handlers.lock().iter_mut() { - if matches!(kind, LogKind::Rpc) { - log_handler(IoKind::StdIn, &message); + if let Err(e) = server_stdin.flush().await { + break Err(e.into()); + } } + Err(error) => break Err(error.into()), } + }; - server_stdin - .write_all( - format!("Content-Length: {}\r\n\r\n{}", message.len(), message).as_bytes(), - ) - .await?; - - server_stdin.flush().await?; - } + log::debug!("Handle adapter input dropped"); - Ok(()) + result } async fn handle_output( @@ -208,27 +252,33 @@ impl TransportDelegate { ) -> Result<()> { let mut recv_buffer = String::new(); - while let Ok(message) = - Self::receive_server_message(&mut server_stdout, &mut recv_buffer, &log_handlers).await - { + let result = loop { + let message = + Self::receive_server_message(&mut server_stdout, &mut recv_buffer, &log_handlers) + .await; + match message { - Message::Response(res) => { + Ok(Message::Response(res)) => { if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) { - tx.send(Self::process_response(res)).await?; + if let Err(e) = tx.send(Self::process_response(res)) { + break Err(anyhow!("Failed to send response: {:?}", e)); + } } else { client_tx.send(Message::Response(res)).await?; }; } - Message::Request(_) => { - client_tx.send(message).await?; - } - Message::Event(_) => { + Ok(message) => { client_tx.send(message).await?; } + Err(e) => break Err(e), } - } + }; - Ok(()) + drop(client_tx); + + log::debug!("Handle adapter output dropped"); + + result } async fn handle_error(stderr: ChildStderr, log_handlers: LogHandlers) -> Result<()> { @@ -236,18 +286,25 @@ impl TransportDelegate { let mut reader = BufReader::new(stderr); - loop { - buffer.truncate(0); - if reader.read_line(&mut buffer).await? == 0 { - return Err(anyhow!("debugger error stream closed")); - } + let result = loop { + match reader.read_line(&mut buffer).await { + Ok(0) => break Err(anyhow!("debugger error stream closed")), + Ok(_) => { + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Adapter) { + log_handler(IoKind::StdErr, buffer.as_str()); + } + } - for (kind, log_handler) in log_handlers.lock().iter_mut() { - if matches!(kind, LogKind::Adapter) { - log_handler(IoKind::StdErr, buffer.as_str()); + buffer.truncate(0); } + Err(error) => break Err(error.into()), } - } + }; + + log::debug!("Handle adapter error dropped"); + + result } fn process_response(response: Response) -> Result { @@ -318,7 +375,9 @@ impl TransportDelegate { } pub async fn shutdown(&self) -> Result<()> { - if let Some(server_tx) = self.server_tx.as_ref() { + log::debug!("Start shutdown client"); + + if let Some(server_tx) = self.server_tx.lock().await.take().as_ref() { server_tx.close(); } @@ -330,13 +389,15 @@ impl TransportDelegate { pending_requests.clear(); if let Some(mut adapter) = adapter.take() { - adapter.kill()?; + let _ = adapter.kill().log_err(); } drop(current_requests); drop(pending_requests); drop(adapter); + log::debug!("Shutdown client completed"); + anyhow::Ok(()) } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index cfdc74ec38d281..37475b8e787e49 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -332,7 +332,7 @@ impl DapStore { } }; - let mut client = DebugAdapterClient::new(client_id, config, adapter); + let mut client = DebugAdapterClient::new(client_id, config, adapter, &cx); client .start( From 68ee3c747a8c094b5bcf4c66ecaa08b89a73d9be Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 10 Nov 2024 20:06:54 +0100 Subject: [PATCH 338/650] Fix flickering of rpc messages --- crates/debugger_tools/src/dap_log.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 076e0dfc964468..fa928fec738d26 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -212,6 +212,8 @@ impl LogStore { rpc_messages.last_message_kind = Some(kind); } Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx); + + cx.notify(); } fn add_debug_client_log( @@ -225,8 +227,6 @@ impl LogStore { return; }; - let mut log_messages = &mut debug_client_state.log_messages; - let message = match io_kind { IoKind::StdErr => { let mut message = message.clone(); @@ -236,7 +236,14 @@ impl LogStore { _ => message, }; - Self::add_debug_client_entry(&mut log_messages, id, message, LogKind::Adapter, cx); + Self::add_debug_client_entry( + &mut debug_client_state.log_messages, + id, + message, + LogKind::Adapter, + cx, + ); + cx.notify(); } fn add_debug_client_entry( @@ -254,7 +261,6 @@ impl LogStore { log_lines.push_back(message); cx.emit(Event::NewLogEntry { id, entry, kind }); - cx.notify(); } fn add_debug_client( @@ -722,6 +728,7 @@ impl SearchableItem for DapLogView { fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { // Since DAP Log is read-only, it doesn't make sense to support replace operation. } + fn supported_options() -> workspace::searchable::SearchOptions { workspace::searchable::SearchOptions { case: true, From b69d031e158d87082835c6232ce05ea4c7d5e535 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Sun, 10 Nov 2024 14:42:06 -0500 Subject: [PATCH 339/650] Transfer breakpoints on project_panel file rename (#65) --- crates/project/src/dap_store.rs | 6 ++++++ crates/project_panel/src/project_panel.rs | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 37475b8e787e49..67f18f68ff0844 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -190,6 +190,12 @@ impl DapStore { self.active_debug_line.take(); } + pub fn on_file_rename(&mut self, old_project_path: ProjectPath, new_project_path: ProjectPath) { + if let Some(breakpoints) = self.breakpoints.remove(&old_project_path) { + self.breakpoints.insert(new_project_path, breakpoints); + } + } + pub fn breakpoints(&self) -> &BTreeMap> { &self.breakpoints } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d8ad0c58a58b6f..a000b45745fd57 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -871,6 +871,22 @@ impl ProjectPanel { } Ok(CreatedEntry::Included(new_entry)) => { project_panel.update(&mut cx, |project_panel, cx| { + project_panel.project.update(cx, |project, cx| { + let old_path = ProjectPath { + worktree_id, + path: entry.path, + }; + + let new_path = ProjectPath { + worktree_id, + path: new_entry.path.clone() + }; + + project.dap_store().update(cx, |dap_store, _| { + dap_store.on_file_rename(old_path, new_path); + }); + }); + if let Some(selection) = &mut project_panel.selection { if selection.entry_id == edited_entry_id { selection.worktree_id = worktree_id; From ba25aa26c9ee357a762719f84dd21adfb31e9972 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 10 Nov 2024 21:41:01 +0100 Subject: [PATCH 340/650] Fix race condition that would trigger `debug did not stop` warning --- crates/debugger_ui/src/debugger_panel.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 121f56dfdf14cf..37fe5849b81d95 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -541,13 +541,13 @@ impl DebugPanel { .or_insert(cx.new_model(|_| ThreadState::default())) .clone(); - thread_state.update(cx, |thread_state, cx| { + thread_state.update(cx, |thread_state, _| { thread_state.stopped = true; thread_state.status = ThreadStatus::Stopped; - - cx.notify(); }); + cx.notify(); + let existing_item = this .pane .read(cx) @@ -594,8 +594,6 @@ impl DebugPanel { go_to_stack_frame, }); - cx.notify(); - this.workspace.clone() })?; From 964a6e858518b6e2973630fe55a0c00ed9bfff35 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 10 Nov 2024 22:10:32 +0100 Subject: [PATCH 341/650] Fix don't show active debug line for finished session when you reopen a file --- crates/debugger_ui/src/debugger_panel.rs | 4 ++++ crates/debugger_ui/src/debugger_panel_item.rs | 4 ++++ crates/debugger_ui/src/stack_frame_list.rs | 3 ++- crates/editor/src/editor.rs | 2 +- crates/project/src/dap_store.rs | 24 ++++++++++++++++--- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 37fe5849b81d95..549e673374d84d 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -130,6 +130,10 @@ impl DebugPanel { project::Event::DebugClientStopped(client_id) => { cx.emit(DebugPanelEvent::ClientStopped(*client_id)); + this.dap_store.update(cx, |store, cx| { + store.remove_active_debug_line_for_client(client_id, cx); + }); + this.thread_states .retain(|&(client_id_, _), _| client_id_ != *client_id); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 62168d1f23d30d..39cde029049a73 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -322,6 +322,10 @@ impl DebugPanelItem { return; } + self.dap_store.update(cx, |store, cx| { + store.remove_active_debug_line_for_client(client_id, cx); + }); + self.update_thread_state_status(ThreadStatus::Exited, cx); cx.emit(DebugPanelItemEvent::Close); diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 7da06104e29145..9c682c29e3d43d 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -159,6 +159,7 @@ impl StackFrameList { }; cx.spawn({ + let client_id = self.client_id; let workspace = self.workspace.clone(); move |this, mut cx| async move { let task = workspace.update(&mut cx, |workspace, cx| { @@ -169,7 +170,7 @@ impl StackFrameList { this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { - store.set_active_debug_line(&project_path, row, column, cx); + store.set_active_debug_line(&client_id, &project_path, row, column, cx); }) })?; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b4da8fff1d7a0b..79e5e6f4e6b204 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11956,7 +11956,7 @@ impl Editor { return; }; - if let Some((path, position)) = dap_store.read(cx).active_debug_line() { + if let Some((_, path, position)) = dap_store.read(cx).active_debug_line() { if path == project_path { self.go_to_line::( position.row, diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 67f18f68ff0844..b33f40b5d79ed1 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -75,7 +75,7 @@ pub struct DapStore { delegate: DapAdapterDelegate, ignore_breakpoints: HashSet, breakpoints: BTreeMap>, - active_debug_line: Option<(ProjectPath, DebugPosition)>, + active_debug_line: Option<(DebugAdapterClientId, ProjectPath, DebugPosition)>, capabilities: HashMap, environment: Model, clients: HashMap, @@ -170,22 +170,40 @@ impl DapStore { } } - pub fn active_debug_line(&self) -> Option<(ProjectPath, DebugPosition)> { + pub fn active_debug_line(&self) -> Option<(DebugAdapterClientId, ProjectPath, DebugPosition)> { self.active_debug_line.clone() } pub fn set_active_debug_line( &mut self, + client_id: &DebugAdapterClientId, project_path: &ProjectPath, row: u32, column: u32, cx: &mut ModelContext, ) { - self.active_debug_line = Some((project_path.clone(), DebugPosition { row, column })); + self.active_debug_line = Some(( + *client_id, + project_path.clone(), + DebugPosition { row, column }, + )); cx.notify(); } + pub fn remove_active_debug_line_for_client( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) { + if let Some(active_line) = &self.active_debug_line { + if active_line.0 == *client_id { + self.active_debug_line.take(); + cx.notify(); + } + } + } + pub fn remove_active_debug_line(&mut self) { self.active_debug_line.take(); } From c54454fa420afe20dd3efe70d50883d6b9da4bb7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 10 Nov 2024 22:38:03 +0100 Subject: [PATCH 342/650] Remove not needed notify I added this it fix the race issue with session exited without stopping on a breakpoint. Turns out the adapter was sending these events for other threads that where not visible. --- crates/debugger_ui/src/debugger_panel.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 549e673374d84d..b28b59bd994971 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -550,8 +550,6 @@ impl DebugPanel { thread_state.status = ThreadStatus::Stopped; }); - cx.notify(); - let existing_item = this .pane .read(cx) From 15dd1ee22e8a2ce0cac36facef1fe39718be51ac Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 10 Nov 2024 23:48:32 +0100 Subject: [PATCH 343/650] Only clear the active debug line for the editor that belongs to the current debug line Also added a missing `cx.notify()` which fixes a delay when the line is removed visually --- crates/debugger_ui/src/debugger_panel_item.rs | 36 ++++++++++--------- crates/editor/src/editor.rs | 16 +++++---- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 39cde029049a73..7a1528ae48db76 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -22,7 +22,7 @@ use task::DebugAdapterKind; use ui::WindowContext; use ui::{prelude::*, Tooltip}; use workspace::item::{Item, ItemEvent}; -use workspace::Workspace; +use workspace::{ItemHandle, Workspace}; #[derive(Debug)] pub enum DebugPanelItemEvent { @@ -322,12 +322,12 @@ impl DebugPanelItem { return; } + self.update_thread_state_status(ThreadStatus::Exited, cx); + self.dap_store.update(cx, |store, cx| { store.remove_active_debug_line_for_client(client_id, cx); }); - self.update_thread_state_status(ThreadStatus::Exited, cx); - cx.emit(DebugPanelItemEvent::Close); } @@ -357,19 +357,23 @@ impl DebugPanelItem { } fn clear_highlights(&self, cx: &mut ViewContext) { - self.workspace - .update(cx, |workspace, cx| { - let editor_views = workspace - .items_of_type::(cx) - .collect::>>(); - - for editor_view in editor_views { - editor_view.update(cx, |editor, _| { - editor.clear_row_highlights::(); - }); - } - }) - .ok(); + if let Some((_, project_path, _)) = self.dap_store.read(cx).active_debug_line() { + self.workspace + .update(cx, |workspace, cx| { + let editor = workspace + .items_of_type::(cx) + .find(|editor| Some(project_path.clone()) == editor.project_path(cx)); + + if let Some(editor) = editor { + editor.update(cx, |editor, cx| { + editor.clear_row_highlights::(); + + cx.notify(); + }); + } + }) + .ok(); + } } pub fn go_to_current_stack_frame(&self, cx: &mut ViewContext) { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 79e5e6f4e6b204..d95e39da2a74b5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -102,7 +102,7 @@ use language::{ point_to_lsp, BufferRow, CharClassifier, LanguageServerName, Runnable, RunnableRange, }; use linked_editing_ranges::refresh_linked_ranges; -use project::dap_store::BreakpointEditAction; +use project::{dap_store::BreakpointEditAction, ProjectPath}; pub use proposed_changes_editor::{ ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, }; @@ -11943,16 +11943,20 @@ impl Editor { } } + pub fn project_path(&self, cx: &mut ViewContext) -> Option { + if let Some(buffer) = self.buffer.read(cx).as_singleton() { + buffer.read_with(cx, |buffer, cx| buffer.project_path(cx).map(|p| p.into())) + } else { + None + } + } + pub fn go_to_active_debug_line(&mut self, cx: &mut ViewContext) { let Some(dap_store) = self.dap_store.as_ref() else { return; }; - let Some(buffer) = self.buffer.read(cx).as_singleton() else { - return; - }; - - let Some(project_path) = buffer.read_with(cx, |buffer, cx| buffer.project_path(cx)) else { + let Some(project_path) = self.project_path(cx) else { return; }; From 81ca004ee6a99b6fe032f8648de8316c3e704634 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 11 Nov 2024 19:33:02 +0100 Subject: [PATCH 344/650] Fix clippy error --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d95e39da2a74b5..6fca41d629faba 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11945,7 +11945,7 @@ impl Editor { pub fn project_path(&self, cx: &mut ViewContext) -> Option { if let Some(buffer) = self.buffer.read(cx).as_singleton() { - buffer.read_with(cx, |buffer, cx| buffer.project_path(cx).map(|p| p.into())) + buffer.read_with(cx, |buffer, cx| buffer.project_path(cx)) } else { None } From 31e3e48052e2bae37a1f6250391d9ab32012fb12 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 11 Nov 2024 19:33:38 +0100 Subject: [PATCH 345/650] Keep open variable list entries when changing stackframe --- crates/debugger_ui/src/variable_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index ae85b295265de8..b577ed80b79994 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -176,7 +176,7 @@ impl VariableList { ) { match event { StackFrameListEvent::SelectedStackFrameChanged => { - self.build_entries(true, false, cx); + self.build_entries(true, true, cx); } StackFrameListEvent::StackFramesUpdated => { self.fetch_variables(cx); From ce30deac6303873e79b81b2e48ff9ae7f6a9ef37 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 12 Nov 2024 20:14:37 +0100 Subject: [PATCH 346/650] Prevent building variable entries twice while step debugging --- crates/debugger_ui/src/debugger_panel_item.rs | 6 ++---- crates/debugger_ui/src/stack_frame_list.rs | 5 ----- crates/debugger_ui/src/variable_list.rs | 2 ++ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 7a1528ae48db76..35ea7c91e19411 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -134,8 +134,8 @@ impl DebugPanelItem { cx.subscribe( &stack_frame_list, move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { - StackFrameListEvent::SelectedStackFrameChanged => this.clear_highlights(cx), - _ => {} + StackFrameListEvent::SelectedStackFrameChanged + | StackFrameListEvent::StackFramesUpdated => this.clear_highlights(cx), }, ), ]; @@ -261,8 +261,6 @@ impl DebugPanelItem { editor.move_to_end(&editor::actions::MoveToEnd, cx); editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); editor.set_read_only(true); - - cx.notify(); }); } } diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 9c682c29e3d43d..49006e58c424ff 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -109,13 +109,8 @@ impl StackFrameList { let task = this.update(&mut cx, |this, cx| { std::mem::swap(&mut this.stack_frames, &mut stack_frames); - let previous_stack_frame_id = this.current_stack_frame_id; if let Some(stack_frame) = this.stack_frames.first() { this.current_stack_frame_id = stack_frame.id; - - if previous_stack_frame_id != this.current_stack_frame_id { - cx.emit(StackFrameListEvent::SelectedStackFrameChanged); - } } this.list.reset(this.stack_frames.len()); diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index b577ed80b79994..2fc2198d433b6b 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -179,6 +179,8 @@ impl VariableList { self.build_entries(true, true, cx); } StackFrameListEvent::StackFramesUpdated => { + self.entries.clear(); + self.fetch_variables(cx); } } From ca80d0c3bdfc7cd98fd8ab937df4e2b78c75b70f Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 12 Nov 2024 15:34:19 -0500 Subject: [PATCH 347/650] Fix debug client terminate bug where some highlights were not cleared Co-authored-by: Remco Smits --- crates/debugger_ui/src/debugger_panel.rs | 4 ---- crates/debugger_ui/src/debugger_panel_item.rs | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b28b59bd994971..fbec89f14fe1fd 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -130,10 +130,6 @@ impl DebugPanel { project::Event::DebugClientStopped(client_id) => { cx.emit(DebugPanelEvent::ClientStopped(*client_id)); - this.dap_store.update(cx, |store, cx| { - store.remove_active_debug_line_for_client(client_id, cx); - }); - this.thread_states .retain(|&(client_id_, _), _| client_id_ != *client_id); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 35ea7c91e19411..4b7c93b8d7ff6d 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -308,6 +308,10 @@ impl DebugPanelItem { self.update_thread_state_status(ThreadStatus::Stopped, cx); + self.dap_store.update(cx, |store, cx| { + store.remove_active_debug_line_for_client(client_id, cx); + }); + cx.emit(DebugPanelItemEvent::Close); } From 6f0e223dc7817bb0b9de2db0625fa249321c3515 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 12 Nov 2024 16:23:48 -0500 Subject: [PATCH 348/650] Improve debug client not found log error messages --- crates/project/src/dap_store.rs | 108 ++++++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 18 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index b33f40b5d79ed1..6a5b6a3a461199 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -417,7 +417,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.spawn(|this, mut cx| async move { @@ -602,7 +606,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; let capabilities = self.capabilities_by_id(client_id); @@ -630,7 +638,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.spawn(|this, mut cx| async move { @@ -684,7 +696,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.background_executor().spawn(async move { @@ -713,7 +729,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.background_executor().spawn(async move { @@ -736,7 +756,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; let capabilities = self.capabilities_by_id(client_id); @@ -767,7 +791,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; let capabilities = self.capabilities_by_id(client_id); @@ -799,7 +827,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; let capabilities = self.capabilities_by_id(client_id); @@ -829,7 +861,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task>> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.background_executor().spawn(async move { @@ -855,7 +891,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.background_executor().spawn(async move { @@ -882,7 +922,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task>> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.background_executor().spawn(async move { @@ -910,7 +954,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; let supports_set_expression = self @@ -950,7 +998,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.background_executor() @@ -964,7 +1016,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; let capabilities = self.capabilities_by_id(client_id); @@ -989,7 +1045,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.background_executor().spawn(async move { @@ -1010,7 +1070,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; let supports_restart = self @@ -1055,7 +1119,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.clients.remove(&client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; cx.emit(DapStoreEvent::DebugClientStopped(*client_id)); @@ -1133,7 +1201,11 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not found client"))); + return Task::ready(Err(anyhow!( + "Could not find client: file: {}:{}", + file!(), + line!() + ))); }; if Self::INDEX_STARTS_AT_ONE { From 80f775e186e4ea3ed6c896fad1ff8c6069d688cf Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 12 Nov 2024 16:50:21 -0500 Subject: [PATCH 349/650] Add debug console indicator for unread messages --- crates/debugger_ui/src/debugger_panel_item.rs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 4b7c93b8d7ff6d..5379bd5a578139 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -19,8 +19,8 @@ use gpui::{ use project::dap_store::DapStore; use settings::Settings; use task::DebugAdapterKind; -use ui::WindowContext; use ui::{prelude::*, Tooltip}; +use ui::{Indicator, WindowContext}; use workspace::item::{Item, ItemEvent}; use workspace::{ItemHandle, Workspace}; @@ -42,6 +42,7 @@ enum ThreadItem { pub struct DebugPanelItem { thread_id: u64, console: View, + show_console_indicator: bool, focus_handle: FocusHandle, dap_store: Model, output_editor: View, @@ -156,6 +157,7 @@ impl DebugPanelItem { Self { console, + show_console_indicator: false, thread_id, dap_store, workspace, @@ -254,6 +256,10 @@ impl DebugPanelItem { self.console.update(cx, |console, cx| { console.add_message(&event.output, cx); }); + + if !matches!(self.active_thread_item, ThreadItem::Console) { + self.show_console_indicator = true; + } } _ => { self.output_editor.update(cx, |editor, cx| { @@ -392,6 +398,9 @@ impl DebugPanelItem { thread_item: ThreadItem, cx: &mut ViewContext, ) -> AnyElement { + let has_indicator = + matches!(thread_item, ThreadItem::Console) && self.show_console_indicator; + div() .id(label.clone()) .px_2() @@ -401,10 +410,18 @@ impl DebugPanelItem { .when(self.active_thread_item == thread_item, |this| { this.border_color(cx.theme().colors().border) }) - .child(Button::new(label.clone(), label.clone())) + .child( + h_flex() + .child(Button::new(label.clone(), label.clone())) + .when(has_indicator, |this| this.child(Indicator::dot())), + ) .on_click(cx.listener(move |this, _, cx| { this.active_thread_item = thread_item.clone(); + if matches!(this.active_thread_item, ThreadItem::Console) { + this.show_console_indicator = false; + } + cx.notify(); })) .into_any_element() From 977fd87a518b8db9803ccc59b2217d2c2d812619 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 12 Nov 2024 17:08:22 -0500 Subject: [PATCH 350/650] Create basic debug.json user documentation --- docs/src/debugger.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/src/debugger.md diff --git a/docs/src/debugger.md b/docs/src/debugger.md new file mode 100644 index 00000000000000..bd43f16ea7e722 --- /dev/null +++ b/docs/src/debugger.md @@ -0,0 +1,32 @@ +# Debugger + +## Debug Configuration + +To debug a program using Zed you must first create a debug configuration within your project located at `.zed/debug.json` + + +```json +[ + { + "label": "Example Start debugger config" + // The debug adapter to use + // Zed supports javascript, python, lldb, go, and custom out of the box + "adapter": "custom", + // request: defaults to launch + // - launch: Zed will launch the program to be debugged + // - attach: Zed will attach to a running program to debug it + "request": "launch", + // cwd: defaults to the current working directory of your project + // The current working directory to start the debugger from + // accepts zed task variables e.g. $ZED_WORKPLACE_ROOT + "cwd": "$ZED_WORKPLACE_ROOT", + // program: The program to debug + // accepts zed task variables + "program": "path_to_program", + // Additional initialization arguments to be sent on DAP initialization + "initialize_args": { + + } + } +] +``` From ae0d08f36c3566ba259adfd91b6d1e535175d40d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 13 Nov 2024 12:21:49 +0100 Subject: [PATCH 351/650] Format docs with prettier --- docs/src/debugger.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/debugger.md b/docs/src/debugger.md index bd43f16ea7e722..9f52c88a74b791 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -4,7 +4,6 @@ To debug a program using Zed you must first create a debug configuration within your project located at `.zed/debug.json` - ```json [ { From d8f81409655a9c5226468e2108d4bb7d6de02e61 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 13 Nov 2024 12:26:05 +0100 Subject: [PATCH 352/650] Fix compile error --- crates/debugger_ui/src/console.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index a2393a8dc6986e..b34920bcd60431 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -194,7 +194,7 @@ impl Render for Console { .child( div() .child(self.render_query_bar(cx)) - .pt(Spacing::XSmall.rems(cx)), + .pt(DynamicSpacing::Base01.rems(cx)), ) .border_2() } From 2c1f348c493570f02c64b314ff526ef1b1662f76 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 13 Nov 2024 12:31:37 +0100 Subject: [PATCH 353/650] Refine spacing --- crates/debugger_ui/src/console.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index b34920bcd60431..4772b5738ab99e 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -194,7 +194,7 @@ impl Render for Console { .child( div() .child(self.render_query_bar(cx)) - .pt(DynamicSpacing::Base01.rems(cx)), + .pt(DynamicSpacing::Base04.rems(cx)), ) .border_2() } From 3a77d7a6554985d98a45f95f19340c205c25c57a Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:45:10 -0500 Subject: [PATCH 354/650] Fix output that was produced before initial stop was not visible at first stop (#57) * Fix log breakpoint output bug when hit before any breakpoints * Fix always push to output queue * Don't pop output queue We still want all the output in new threads * Fix clippy error --------- Co-authored-by: Remco Smits --- crates/debugger_ui/src/debugger_panel.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index fbec89f14fe1fd..b9c3d70f458c3b 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -21,6 +21,7 @@ use project::terminals::TerminalKind; use serde_json::Value; use settings::Settings; use std::any::TypeId; +use std::collections::VecDeque; use std::path::PathBuf; use std::u64; use task::DebugRequestType; @@ -70,6 +71,7 @@ pub struct DebugPanel { workspace: WeakView, show_did_not_stop_warning: bool, _subscriptions: Vec, + message_queue: HashMap>, thread_states: BTreeMap<(DebugAdapterClientId, u64), Model>, } @@ -130,6 +132,7 @@ impl DebugPanel { project::Event::DebugClientStopped(client_id) => { cx.emit(DebugPanelEvent::ClientStopped(*client_id)); + this.message_queue.remove(client_id); this.thread_states .retain(|&(client_id_, _), _| client_id_ != *client_id); @@ -147,6 +150,7 @@ impl DebugPanel { focus_handle: cx.focus_handle(), show_did_not_stop_warning: false, thread_states: Default::default(), + message_queue: Default::default(), workspace: workspace.weak_handle(), dap_store: project.read(cx).dap_store(), } @@ -575,6 +579,12 @@ impl DebugPanel { pane.add_item(Box::new(tab), true, true, None, cx); }); + + if let Some(message_queue) = this.message_queue.get(&client_id) { + while let Some(output) = message_queue.iter().next() { + cx.emit(DebugPanelEvent::Output((client_id, output.clone()))); + } + } } let go_to_stack_frame = if let Some(item) = this.pane.read(cx).active_item() { @@ -680,6 +690,11 @@ impl DebugPanel { event: &OutputEvent, cx: &mut ViewContext, ) { + self.message_queue + .entry(*client_id) + .or_default() + .push_back(event.clone()); + cx.emit(DebugPanelEvent::Output((*client_id, event.clone()))); } From b6dc3ca86f447826f66fc8fe1893341db7951607 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 15 Nov 2024 23:18:39 +0100 Subject: [PATCH 355/650] Send cwd to PHP and Python adapter --- crates/dap_adapters/src/php.rs | 5 ++++- crates/dap_adapters/src/python.rs | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 892e1a6c6b4a0c..eaea7be7141f10 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -124,6 +124,9 @@ impl DebugAdapter for PhpDebugAdapter { } fn request_args(&self, config: &DebugAdapterConfig) -> Value { - json!({"program": config.program}) + json!({ + "program": config.program, + "cwd": config.cwd, + }) } } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index bcd4f492e94cde..855fa3551b6d30 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -119,6 +119,10 @@ impl DebugAdapter for PythonDebugAdapter { } fn request_args(&self, config: &DebugAdapterConfig) -> Value { - json!({"program": config.program, "subProcess": true}) + json!({ + "program": config.program, + "subProcess": true, + "cwd": config.cwd, + }) } } From 0d848816411f0401ddcaf23311c7756e5a42683f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 15 Nov 2024 23:36:42 +0100 Subject: [PATCH 356/650] Add default tasks for build in adapters --- .gitignore | 1 - .zed/debug.json | 9 ++++++++ assets/settings/initial_debug.json | 23 ++++++++++++++++++++ crates/settings/src/settings.rs | 4 ++++ crates/zed/src/zed.rs | 34 +++++++++++++++++++++++++++--- 5 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 .zed/debug.json create mode 100644 assets/settings/initial_debug.json diff --git a/.gitignore b/.gitignore index e9eb4c3ef124ae..d19c5a102aac8a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ DerivedData/ .vscode .wrangler .flatpak-builder -.zed/debug.json # Don't commit any secrets to the repo. .env.secret.toml diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 00000000000000..d799c13a8e1855 --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,9 @@ +[ + { + "label": "Debug Zed with LLDB", + "adapter": "lldb", + "program": "$ZED_WORKTREE_ROOT/target/debug/zed", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" + } +] diff --git a/assets/settings/initial_debug.json b/assets/settings/initial_debug.json new file mode 100644 index 00000000000000..27bee698c9d6d4 --- /dev/null +++ b/assets/settings/initial_debug.json @@ -0,0 +1,23 @@ +[ + { + "label": "Debug active PHP file", + "adapter": "php", + "program": "$ZED_FILE", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "Debug active Python file", + "adapter": "python", + "program": "$ZED_FILE", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "Debug active JavaScript file", + "adapter": "javascript", + "program": "$ZED_FILE", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" + } +] diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index e1dc033677fff4..65d0257539dea0 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -105,3 +105,7 @@ pub fn initial_keymap_content() -> Cow<'static, str> { pub fn initial_tasks_content() -> Cow<'static, str> { asset_str::("settings/initial_tasks.json") } + +pub fn initial_debug_tasks_content() -> Cow<'static, str> { + asset_str::("settings/initial_debug.json") +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 504d32ff67264e..116157e3c02882 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -37,8 +37,8 @@ use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; use settings::{ - initial_project_settings_content, initial_tasks_content, KeymapFile, Settings, SettingsStore, - DEFAULT_KEYMAP_PATH, + initial_debug_tasks_content, initial_project_settings_content, initial_tasks_content, + KeymapFile, Settings, SettingsStore, DEFAULT_KEYMAP_PATH, }; use std::any::TypeId; use std::path::PathBuf; @@ -47,7 +47,10 @@ use theme::ActiveTheme; use workspace::notifications::NotificationId; use workspace::CloseIntent; -use paths::{local_settings_file_relative_path, local_tasks_file_relative_path}; +use paths::{ + local_debug_file_relative_path, local_settings_file_relative_path, + local_tasks_file_relative_path, +}; use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{asset_str, ResultExt}; use uuid::Uuid; @@ -72,7 +75,9 @@ actions!( OpenDefaultSettings, OpenProjectSettings, OpenProjectTasks, + OpenProjectDebugTasks, OpenTasks, + OpenDebugTasks, ResetDatabase, ShowAll, ToggleFullScreen, @@ -475,8 +480,18 @@ pub fn initialize_workspace( ); }, ) + .register_action( + move |_: &mut Workspace, _: &OpenDebugTasks, cx: &mut ViewContext| { + open_settings_file( + paths::debug_tasks_file(), + || settings::initial_debug_tasks_content().as_ref().into(), + cx, + ); + }, + ) .register_action(open_project_settings_file) .register_action(open_project_tasks_file) + .register_action(open_project_debug_tasks_file) .register_action( move |workspace: &mut Workspace, _: &zed_actions::OpenDefaultKeymap, @@ -940,6 +955,19 @@ fn open_project_tasks_file( ) } +fn open_project_debug_tasks_file( + workspace: &mut Workspace, + _: &OpenProjectDebugTasks, + cx: &mut ViewContext, +) { + open_local_file( + workspace, + local_debug_file_relative_path(), + initial_debug_tasks_content(), + cx, + ) +} + fn open_local_file( workspace: &mut Workspace, settings_relative_path: &'static Path, From 3aaee14eccf88d25294d4a78ca7d11c0a0e9fc15 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 15 Nov 2024 21:12:40 -0500 Subject: [PATCH 357/650] Extract breakpoint context menu into function to use for code action symbols --- crates/editor/src/editor.rs | 155 ++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 76 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aff5ff1098bc5d..3ede1107090a9e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5622,6 +5622,82 @@ impl Editor { breakpoint_display_points } + fn breakpoint_context_menu( + &self, + anchor: text::Anchor, + kind: Arc, + row: DisplayRow, + cx: &mut ViewContext, + ) -> View { + let editor_weak = cx.view().downgrade(); + let editor_weak2 = editor_weak.clone(); + let focus_handle = self.focus_handle(cx); + + let log_message = kind.log_message(); + let second_entry_msg = if log_message.is_some() { + "Edit Log Breakpoint" + } else { + "Toggle Log Breakpoint" + }; + + ui::ContextMenu::build(cx, |menu, _cx| { + menu.on_blur_subscription(Subscription::new(|| {})) + .context(focus_handle) + .entry("Toggle Breakpoint", None, move |cx| { + if let Some(editor) = editor_weak.upgrade() { + editor.update(cx, |this, cx| { + this.edit_breakpoint_at_anchor( + anchor, + BreakpointKind::Standard, + BreakpointEditAction::Toggle, + cx, + ); + }) + } + }) + .entry(second_entry_msg, None, move |cx| { + if let Some(editor) = editor_weak2.clone().upgrade() { + let log_message = log_message.clone(); + editor.update(cx, |this, cx| { + let position = this + .snapshot(cx) + .display_point_to_anchor(DisplayPoint::new(row, 0), Bias::Right); + + let weak_editor = cx.view().downgrade(); + let bp_prompt = cx.new_view(|cx| { + BreakpointPromptEditor::new(weak_editor, anchor, log_message, cx) + }); + + let height = bp_prompt.update(cx, |this, cx| { + this.prompt + .update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2) + }); + let cloned_prompt = bp_prompt.clone(); + let blocks = vec![BlockProperties { + style: BlockStyle::Sticky, + placement: BlockPlacement::Above(position), + height, + render: Arc::new(move |cx| { + *cloned_prompt.read(cx).gutter_dimensions.lock() = + *cx.gutter_dimensions; + cloned_prompt.clone().into_any_element() + }), + priority: 0, + }]; + + let focus_handle = bp_prompt.focus_handle(cx); + cx.focus(&focus_handle); + + let block_ids = this.insert_blocks(blocks, None, cx); + bp_prompt.update(cx, |prompt, _| { + prompt.add_block_ids(block_ids); + }); + }); + } + }) + }) + } + fn render_breakpoint( &self, position: text::Anchor, @@ -5654,7 +5730,7 @@ impl Editor { editor.focus(cx); editor.edit_breakpoint_at_anchor( position, - (*arc_kind).clone(), + arc_kind.as_ref().clone(), BreakpointEditAction::Toggle, cx, ); @@ -5667,82 +5743,9 @@ impl Editor { .anchor_at(Point::new(row.0, 0u32), Bias::Left); let clicked_point = event.down.position; - let focus_handle = editor.focus_handle.clone(); - let editor_weak = cx.view().downgrade(); - let second_weak = editor_weak.clone(); - let log_message = arc_kind2.log_message(); - - let second_entry_msg = if log_message.is_some() { - "Edit Log Breakpoint" - } else { - "Toggle Log Breakpoint" - }; - - let context_menu = ui::ContextMenu::build(cx, move |menu, _cx| { - let anchor = position; - menu.on_blur_subscription(Subscription::new(|| {})) - .context(focus_handle) - .entry("Toggle Breakpoint", None, move |cx| { - if let Some(editor) = editor_weak.upgrade() { - editor.update(cx, |this, cx| { - this.edit_breakpoint_at_anchor( - anchor, - BreakpointKind::Standard, - BreakpointEditAction::Toggle, - cx, - ); - }) - } - }) - .entry(second_entry_msg, None, move |cx| { - if let Some(editor) = second_weak.clone().upgrade() { - let log_message = log_message.clone(); - editor.update(cx, |this, cx| { - let position = this.snapshot(cx).display_point_to_anchor( - DisplayPoint::new(row, 0), - Bias::Right, - ); - - let weak_editor = cx.view().downgrade(); - let bp_prompt = cx.new_view(|cx| { - BreakpointPromptEditor::new( - weak_editor, - anchor, - log_message, - cx, - ) - }); - - let height = bp_prompt.update(cx, |this, cx| { - this.prompt.update(cx, |prompt, cx| { - prompt.max_point(cx).row().0 + 1 + 2 - }) - }); - let cloned_prompt = bp_prompt.clone(); - let blocks = vec![BlockProperties { - style: BlockStyle::Sticky, - placement: BlockPlacement::Above(position), - height, - render: Arc::new(move |cx| { - *cloned_prompt.read(cx).gutter_dimensions.lock() = - *cx.gutter_dimensions; - cloned_prompt.clone().into_any_element() - }), - priority: 0, - }]; - - let focus_handle = bp_prompt.focus_handle(cx); - cx.focus(&focus_handle); - - let block_ids = this.insert_blocks(blocks, None, cx); - bp_prompt.update(cx, |prompt, _| { - prompt.add_block_ids(block_ids); - }); - }); - } - }) - }); + let context_menu = + editor.breakpoint_context_menu(position, arc_kind2.clone(), row, cx); editor.mouse_context_menu = MouseContextMenu::pinned_to_editor( editor, From a3220dc31d7cac831d8e701024e9800e3e12753f Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 15 Nov 2024 22:02:49 -0500 Subject: [PATCH 358/650] Add breakpoint context menu to all gutter symbols on right click --- crates/editor/src/editor.rs | 66 ++++++++++++++++++++++++++++++++++-- crates/editor/src/element.rs | 11 +++--- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3ede1107090a9e..859fbb4e5eeb33 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5480,14 +5480,27 @@ impl Editor { _style: &EditorStyle, row: DisplayRow, is_active: bool, + breakpoint: Option, cx: &mut ViewContext, ) -> Option { + let color = if breakpoint.is_some() { + Color::Debugger + } else { + Color::Muted + }; + + let bp_kind = Arc::new( + breakpoint + .map(|bp| bp.kind) + .unwrap_or(BreakpointKind::Standard), + ); + if self.available_code_actions.is_some() { Some( IconButton::new("code_actions_indicator", ui::IconName::Bolt) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) + .icon_color(color) .selected(is_active) .tooltip({ let focus_handle = self.focus_handle.clone(); @@ -5510,6 +5523,27 @@ impl Editor { }, cx, ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { + let source = editor + .buffer + .read(cx) + .snapshot(cx) + .anchor_at(Point::new(row.0, 0u32), Bias::Left); + + let anchor = source.text_anchor; + + let context_menu = + editor.breakpoint_context_menu(anchor, bp_kind.clone(), row, cx); + + let clicked_point = event.down.position; + editor.mouse_context_menu = MouseContextMenu::pinned_to_editor( + editor, + source, + clicked_point, + context_menu, + cx, + ) })), ) } else { @@ -5888,15 +5922,21 @@ impl Editor { _style: &EditorStyle, is_active: bool, row: DisplayRow, - overlaps_breakpoint: bool, + breakpoint: Option, cx: &mut ViewContext, ) -> IconButton { - let color = if overlaps_breakpoint { + let color = if breakpoint.is_some() { Color::Debugger } else { Color::Muted }; + let bp_kind = Arc::new( + breakpoint + .map(|bp| bp.kind) + .unwrap_or(BreakpointKind::Standard), + ); + IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) @@ -5911,6 +5951,26 @@ impl Editor { cx, ); })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { + let source = editor + .buffer + .read(cx) + .snapshot(cx) + .anchor_at(Point::new(row.0, 0u32), Bias::Left); + + let anchor = source.text_anchor; + + let context_menu = editor.breakpoint_context_menu(anchor, bp_kind.clone(), row, cx); + + let clicked_point = event.down.position; + editor.mouse_context_menu = MouseContextMenu::pinned_to_editor( + editor, + source, + clicked_point, + context_menu, + cx, + ) + })) } pub fn context_menu_visible(&self) -> bool { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6a22d788b7cf88..d07b12c4c37cc5 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1794,7 +1794,7 @@ impl EditorElement { &self.style, Some(display_row) == active_task_indicator_row, display_row, - breakpoints.remove(&display_row).is_some(), + breakpoints.remove(&display_row), cx, ); @@ -1837,15 +1837,12 @@ impl EditorElement { { active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row); }; - button = editor.render_code_actions_indicator(&self.style, row, active, cx); + + let breakpoint = breakpoint_points.remove(&row); + button = editor.render_code_actions_indicator(&self.style, row, active, breakpoint, cx); }); let button = button?; - let button = if breakpoint_points.remove(&row).is_some() { - button.icon_color(Color::Debugger) - } else { - button - }; let button = prepaint_gutter_button( button, From 7e0150790ace296de0827b039ab95e31cb22f3a2 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sat, 16 Nov 2024 00:13:03 -0500 Subject: [PATCH 359/650] Fix debugger hang cause by repeatedly processing the same output event --- crates/dap/src/client.rs | 1 + crates/debugger_ui/src/debugger_panel.rs | 3 ++- crates/project/src/dap_store.rs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 0618a6f921f321..497a6ac09548ba 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -73,6 +73,7 @@ impl DebugAdapterClient { F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { let (server_rx, server_tx) = self.transport_delegate.start(binary, cx).await?; + log::info!("Successfully connected to debug adapter"); // start handling events/reverse requests cx.update(|cx| { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b9c3d70f458c3b..3ca063d0f628b7 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -581,7 +581,8 @@ impl DebugPanel { }); if let Some(message_queue) = this.message_queue.get(&client_id) { - while let Some(output) = message_queue.iter().next() { + let mut message_queue = message_queue.iter(); + while let Some(output) = message_queue.next() { cx.emit(DebugPanelEvent::Output((client_id, output.clone()))); } } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 32cf6eaad9e81e..c368c1900b9316 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -371,6 +371,7 @@ impl DapStore { ) .await?; + log::info!("Client has started"); anyhow::Ok(client) }) .await; From 8c72b99031f7388d14357a359fc390696f0c2091 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 16 Nov 2024 09:33:29 +0100 Subject: [PATCH 360/650] Dont starve the main thread when receiving alot of messages --- Cargo.lock | 1 + crates/dap/src/client.rs | 2 ++ crates/dap/src/transport.rs | 8 ++++++++ crates/debugger_tools/Cargo.toml | 11 ++++++----- crates/debugger_tools/src/dap_log.rs | 4 ++++ crates/debugger_ui/src/debugger_panel.rs | 3 +-- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da04301f999eee..31e3e83899a9b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3527,6 +3527,7 @@ dependencies = [ "futures 0.3.30", "gpui", "project", + "smol", "workspace", ] diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 497a6ac09548ba..855d486ec76d88 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -110,6 +110,8 @@ impl DebugAdapterClient { } { break Err(e); } + + smol::future::yield_now().await; }; drop(client_tx); diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 7ec000f4d08c42..04c3eaf391b11c 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -187,6 +187,8 @@ impl TransportDelegate { handler(IoKind::StdOut, line.as_str()); } } + + smol::future::yield_now().await; }; log::debug!("Handle adapter log dropped"); @@ -237,6 +239,8 @@ impl TransportDelegate { } Err(error) => break Err(error.into()), } + + smol::future::yield_now().await; }; log::debug!("Handle adapter input dropped"); @@ -272,6 +276,8 @@ impl TransportDelegate { } Err(e) => break Err(e), } + + smol::future::yield_now().await; }; drop(client_tx); @@ -300,6 +306,8 @@ impl TransportDelegate { } Err(error) => break Err(error.into()), } + + smol::future::yield_now().await; }; log::debug!("Handle adapter error dropped"); diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml index e139721a614f66..b4594fbe602dd5 100644 --- a/crates/debugger_tools/Cargo.toml +++ b/crates/debugger_tools/Cargo.toml @@ -13,10 +13,11 @@ path = "src/debugger_tools.rs" doctest = false [dependencies] -gpui.workspace = true -workspace.workspace = true -editor.workspace = true -project.workspace = true +anyhow.workspace = true dap.workspace = true +editor.workspace = true futures.workspace = true -anyhow.workspace = true +gpui.workspace = true +project.workspace = true +smol.workspace = true +workspace.workspace = true diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index fa928fec738d26..7d225d3dc3a2bd 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -104,6 +104,8 @@ impl LogStore { this.on_rpc_log(server_id, io_kind, &message, cx); })?; } + + smol::future::yield_now().await; } anyhow::Ok(()) }) @@ -118,6 +120,8 @@ impl LogStore { this.on_adapter_log(server_id, io_kind, &message, cx); })?; } + + smol::future::yield_now().await; } anyhow::Ok(()) }) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3ca063d0f628b7..1ece48e267cb9e 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -581,8 +581,7 @@ impl DebugPanel { }); if let Some(message_queue) = this.message_queue.get(&client_id) { - let mut message_queue = message_queue.iter(); - while let Some(output) = message_queue.next() { + for output in message_queue.iter() { cx.emit(DebugPanelEvent::Output((client_id, output.clone()))); } } From 008bd534afbb7f7788b42c493177e321773c50d3 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 16 Nov 2024 11:07:31 +0100 Subject: [PATCH 361/650] Add debug task file watcher This also fixes that we did not see the initial debug tasks defined in `initial_debug_tasks.json`. So new users should see at least these debug tasks the could run, without having to define them their selfs. --- ...al_debug.json => initial_debug_tasks.json} | 0 crates/paths/src/paths.rs | 6 +- crates/project/src/project_settings.rs | 2 +- crates/project/src/task_inventory.rs | 10 ++- crates/project/src/task_store.rs | 61 +++++++++++++------ crates/settings/src/settings.rs | 2 +- 6 files changed, 56 insertions(+), 25 deletions(-) rename assets/settings/{initial_debug.json => initial_debug_tasks.json} (100%) diff --git a/assets/settings/initial_debug.json b/assets/settings/initial_debug_tasks.json similarity index 100% rename from assets/settings/initial_debug.json rename to assets/settings/initial_debug_tasks.json diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 4ca8716813f94e..c47fff4e9e3960 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -326,14 +326,12 @@ pub fn local_vscode_tasks_file_relative_path() -> &'static Path { /// Returns the relative path to a `launch.json` file within a project. pub fn local_debug_file_relative_path() -> &'static Path { - static LOCAL_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new(); - LOCAL_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".zed/debug.json")) + Path::new(".zed/debug.json") } /// Returns the relative path to a `.vscode/launch.json` file within a project. pub fn local_vscode_launch_file_relative_path() -> &'static Path { - static LOCAL_VSCODE_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new(); - LOCAL_VSCODE_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".vscode/launch.json")) + Path::new(".vscode/launch.json") } /// A default editorconfig file name to use when resolving project settings. diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index bb09901befbb45..8cec28c06ea03a 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -561,7 +561,7 @@ impl SettingsObserver { path: directory.as_ref(), }), file_content.as_deref(), - Some(task_kind), + task_kind, cx, ) .log_err(); diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 09775dc1ed528c..5e56474a54418d 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -247,8 +247,14 @@ impl Inventory { .map(|template| { ( TaskSourceKind::AbsPath { - id_base: Cow::Borrowed("global tasks.json"), - abs_path: paths::tasks_file().clone(), + id_base: match template.task_type { + task::TaskType::Script => Cow::Borrowed("global tasks.json"), + task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"), + }, + abs_path: match template.task_type { + task::TaskType::Script => paths::tasks_file().clone(), + task::TaskType::Debug(_) => paths::debug_tasks_file().clone(), + }, }, template, ) diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index 480f0c188c3cc5..9d7dfa0ef60df5 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -32,7 +32,7 @@ pub struct StoreState { buffer_store: WeakModel, worktree_store: Model, toolchain_store: Arc, - _global_task_config_watcher: Task<()>, + _global_task_config_watchers: (Task<()>, Task<()>), } enum StoreMode { @@ -170,7 +170,20 @@ impl TaskStore { buffer_store, toolchain_store, worktree_store, - _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(fs, cx), + _global_task_config_watchers: ( + Self::subscribe_to_global_task_file_changes( + fs.clone(), + TaskKind::Script, + paths::tasks_file().clone(), + cx, + ), + Self::subscribe_to_global_task_file_changes( + fs.clone(), + TaskKind::Debug, + paths::debug_tasks_file().clone(), + cx, + ), + ), }) } @@ -192,7 +205,20 @@ impl TaskStore { buffer_store, toolchain_store, worktree_store, - _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(fs, cx), + _global_task_config_watchers: ( + Self::subscribe_to_global_task_file_changes( + fs.clone(), + TaskKind::Script, + paths::tasks_file().clone(), + cx, + ), + Self::subscribe_to_global_task_file_changes( + fs.clone(), + TaskKind::Debug, + paths::debug_tasks_file().clone(), + cx, + ), + ), }) } @@ -269,7 +295,7 @@ impl TaskStore { &self, location: Option>, raw_tasks_json: Option<&str>, - task_type: Option, + task_type: TaskKind, cx: &mut ModelContext<'_, Self>, ) -> anyhow::Result<()> { let task_inventory = match self { @@ -281,26 +307,23 @@ impl TaskStore { .filter(|json| !json.is_empty()); task_inventory.update(cx, |inventory, _| { - inventory.update_file_based_tasks( - location, - raw_tasks_json, - task_type.unwrap_or(TaskKind::Script), - ) + inventory.update_file_based_tasks(location, raw_tasks_json, task_type) }) } fn subscribe_to_global_task_file_changes( fs: Arc, + task_kind: TaskKind, + file_path: PathBuf, cx: &mut ModelContext<'_, Self>, ) -> Task<()> { - let mut user_tasks_file_rx = - watch_config_file(&cx.background_executor(), fs, paths::tasks_file().clone()); + let mut user_tasks_file_rx = watch_config_file(&cx.background_executor(), fs, file_path); let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next()); cx.spawn(move |task_store, mut cx| async move { if let Some(user_tasks_content) = user_tasks_content { let Ok(_) = task_store.update(&mut cx, |task_store, cx| { task_store - .update_user_tasks(None, Some(&user_tasks_content), None, cx) + .update_user_tasks(None, Some(&user_tasks_content), task_kind, cx) .log_err(); }) else { return; @@ -308,13 +331,17 @@ impl TaskStore { } while let Some(user_tasks_content) = user_tasks_file_rx.next().await { let Ok(()) = task_store.update(&mut cx, |task_store, cx| { - let result = - task_store.update_user_tasks(None, Some(&user_tasks_content), None, cx); + let result = task_store.update_user_tasks( + None, + Some(&user_tasks_content), + task_kind, + cx, + ); if let Err(err) = &result { - log::error!("Failed to load user tasks: {err}"); + log::error!("Failed to load user {:?} tasks: {err}", task_kind); cx.emit(crate::Event::Toast { - notification_id: "load-user-tasks".into(), - message: format!("Invalid global tasks file\n{err}"), + notification_id: format!("load-user-{:?}-tasks", task_kind).into(), + message: format!("Invalid global {:?} tasks file\n{err}", task_kind), }); } cx.refresh(); diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 65d0257539dea0..e755dc8da9c4f7 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -107,5 +107,5 @@ pub fn initial_tasks_content() -> Cow<'static, str> { } pub fn initial_debug_tasks_content() -> Cow<'static, str> { - asset_str::("settings/initial_debug.json") + asset_str::("settings/initial_debug_tasks.json") } From 8e738ba4d50982d6590cd238b0737278c0fa7e3d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 19 Nov 2024 23:20:16 +0100 Subject: [PATCH 362/650] Reduce request timeout The php adapter hangs when you click to fast on the UI buttons, so waiting 15 seconds before the request timeout was reached is a bit to much. Because after the timeout you can still continue the debug session. --- crates/dap/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 855d486ec76d88..5bcc371f82b6e5 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -20,7 +20,7 @@ use std::{ }; use task::{DebugAdapterConfig, DebugRequestType}; -const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(15); +const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ThreadStatus { From 2c45c57f7b0ebf6147bc865151fedc65b937eccf Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 25 Nov 2024 15:09:56 +0100 Subject: [PATCH 363/650] Collab: Sync breakpoints (#68) * Sync breakpoints on toggle to other client * WIP Seperate local/remote in dap store * WIP initial breakpoints sync Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> * Get zed to compile * Don't remove dap data when you unshare * Add breakpoints table migration * Update collab db when changing breakpoints * Store breakpoints inside collab db when you change them * Clean up * Fix incorrect clearing of breakpoints during collab sync * Get breakpoints to sync correctly on project join We now send SynchronizedBreakpoints within the JoinProjectResponse and use those breakpoints to initialize a remote DapStore. * Set breakpoints from proto method --------- Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Co-authored-by: Anthony Eid --- .../20221109000000_test_schema.sql | 11 + .../20241121185750_add_breakpoints.sql | 11 + crates/collab/src/db.rs | 13 + crates/collab/src/db/queries/projects.rs | 150 +++++-- crates/collab/src/db/queries/rooms.rs | 28 ++ crates/collab/src/db/tables.rs | 1 + crates/collab/src/db/tables/breakpoints.rs | 47 ++ crates/collab/src/db/tables/project.rs | 8 + crates/collab/src/rpc.rs | 39 +- crates/collab/src/tests/test_server.rs | 1 + crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 2 +- crates/project/src/dap_store.rs | 409 ++++++++++++------ crates/project/src/project.rs | 67 +-- crates/proto/build.rs | 3 + crates/proto/proto/zed.proto | 26 +- crates/proto/src/proto.rs | 2 + crates/remote_server/src/headless_project.rs | 8 +- crates/zed/src/zed.rs | 1 + 19 files changed, 634 insertions(+), 195 deletions(-) create mode 100644 crates/collab/migrations/20241121185750_add_breakpoints.sql create mode 100644 crates/collab/src/db/tables/breakpoints.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index c59091d66d0e27..cef67943265d03 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -436,3 +436,14 @@ CREATE TABLE IF NOT EXISTS processed_stripe_events ( ); CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp); + +CREATE TABLE IF NOT EXISTS "breakpoints" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, + "position" INTEGER NOT NULL, + "log_message" TEXT NULL, + "worktree_id" BIGINT NOT NULL, + "path" TEXT NOT NULL, + "kind" VARCHAR NOT NULL +); +CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id"); diff --git a/crates/collab/migrations/20241121185750_add_breakpoints.sql b/crates/collab/migrations/20241121185750_add_breakpoints.sql new file mode 100644 index 00000000000000..4b3071457392f4 --- /dev/null +++ b/crates/collab/migrations/20241121185750_add_breakpoints.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS "breakpoints" ( + "id" SERIAL PRIMARY KEY, + "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, + "position" INTEGER NOT NULL, + "log_message" TEXT NULL, + "worktree_id" BIGINT NOT NULL, + "path" TEXT NOT NULL, + "kind" VARCHAR NOT NULL +); + +CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 81db7158e83ab7..f569265d1cb6e3 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -657,6 +657,7 @@ pub struct RejoinedProject { pub collaborators: Vec, pub worktrees: Vec, pub language_servers: Vec, + pub breakpoints: HashMap>, } impl RejoinedProject { @@ -679,6 +680,17 @@ impl RejoinedProject { .map(|collaborator| collaborator.to_proto()) .collect(), language_servers: self.language_servers.clone(), + breakpoints: self + .breakpoints + .iter() + .map( + |(project_path, breakpoints)| proto::SynchronizeBreakpoints { + project_id: self.id.to_proto(), + breakpoints: breakpoints.iter().cloned().collect(), + project_path: Some(project_path.clone()), + }, + ) + .collect(), } } } @@ -725,6 +737,7 @@ pub struct Project { pub collaborators: Vec, pub worktrees: BTreeMap, pub language_servers: Vec, + pub breakpoints: HashMap>, } pub struct ProjectCollaborator { diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 7ff8aa7a9fbb1f..22a56554fce840 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -1,4 +1,5 @@ use anyhow::Context as _; +use collections::{HashMap, HashSet}; use util::ResultExt; use super::*; @@ -476,6 +477,60 @@ impl Database { .await } + pub async fn update_breakpoints( + &self, + connection_id: ConnectionId, + update: &proto::SynchronizeBreakpoints, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + self.project_transaction(project_id, |tx| async move { + let project_path = update + .project_path + .as_ref() + .ok_or_else(|| anyhow!("invalid project path"))?; + + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + + // remove all existing breakpoints + breakpoints::Entity::delete_many() + .filter(breakpoints::Column::ProjectId.eq(project.id)) + .exec(&*tx) + .await?; + + if !update.breakpoints.is_empty() { + breakpoints::Entity::insert_many(update.breakpoints.iter().map(|breakpoint| { + breakpoints::ActiveModel { + id: ActiveValue::NotSet, + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(project_path.worktree_id as i64), + path: ActiveValue::Set(project_path.path.clone()), + kind: match proto::BreakpointKind::from_i32(breakpoint.kind) { + Some(proto::BreakpointKind::Log) => { + ActiveValue::Set(breakpoints::BreakpointKind::Log) + } + Some(proto::BreakpointKind::Standard) => { + ActiveValue::Set(breakpoints::BreakpointKind::Standard) + } + None => ActiveValue::Set(breakpoints::BreakpointKind::Standard), + }, + log_message: ActiveValue::Set(breakpoint.message.clone()), + position: ActiveValue::Set(breakpoint.cached_position as i32), + } + })) + .exec_without_returning(&*tx) + .await?; + } + + self.internal_project_connection_ids(project_id, connection_id, true, &tx) + .await + }) + .await + } + /// Updates the worktree settings for the given connection. pub async fn update_worktree_settings( &self, @@ -719,6 +774,33 @@ impl Database { } } + let mut breakpoints: HashMap> = + HashMap::default(); + + let db_breakpoints = project.find_related(breakpoints::Entity).all(tx).await?; + + for breakpoint in db_breakpoints.iter() { + let project_path = proto::ProjectPath { + worktree_id: breakpoint.worktree_id as u64, + path: breakpoint.path.clone(), + }; + + breakpoints + .entry(project_path) + .or_default() + .insert(proto::Breakpoint { + position: None, + cached_position: breakpoint.position as u32, + kind: match breakpoint.kind { + breakpoints::BreakpointKind::Standard => { + proto::BreakpointKind::Standard.into() + } + breakpoints::BreakpointKind::Log => proto::BreakpointKind::Log.into(), + }, + message: breakpoint.log_message.clone(), + }); + } + // Populate language servers. let language_servers = project .find_related(language_server::Entity) @@ -746,6 +828,7 @@ impl Database { worktree_id: None, }) .collect(), + breakpoints, }; Ok((project, replica_id as ReplicaId)) } @@ -973,39 +1056,50 @@ impl Database { exclude_dev_server: bool, ) -> Result>> { self.project_transaction(project_id, |tx| async move { - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; + self.internal_project_connection_ids(project_id, connection_id, exclude_dev_server, &tx) + .await + }) + .await + } - let mut collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; + async fn internal_project_connection_ids( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + exclude_dev_server: bool, + tx: &DatabaseTransaction, + ) -> Result> { + let project = project::Entity::find_by_id(project_id) + .one(tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; - let mut connection_ids = HashSet::default(); - if let Some(host_connection) = project.host_connection().log_err() { - if !exclude_dev_server { - connection_ids.insert(host_connection); - } - } + let mut collaborators = project_collaborator::Entity::find() + .filter(project_collaborator::Column::ProjectId.eq(project_id)) + .stream(tx) + .await?; - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - connection_ids.insert(collaborator.connection()); + let mut connection_ids = HashSet::default(); + if let Some(host_connection) = project.host_connection().log_err() { + if !exclude_dev_server { + connection_ids.insert(host_connection); } + } - if connection_ids.contains(&connection_id) - || Some(connection_id) == project.host_connection().ok() - { - Ok(connection_ids) - } else { - Err(anyhow!( - "can only send project updates to a project you're in" - ))? - } - }) - .await + while let Some(collaborator) = collaborators.next().await { + let collaborator = collaborator?; + connection_ids.insert(collaborator.connection()); + } + + if connection_ids.contains(&connection_id) + || Some(connection_id) == project.host_connection().ok() + { + Ok(connection_ids) + } else { + Err(anyhow!( + "can only send project updates to a project you're in" + ))? + } } async fn project_guest_connection_ids( diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 682c4ed38949e2..0dc1683b91405d 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -710,6 +710,33 @@ impl Database { worktrees.push(worktree); } + let mut breakpoints: HashMap> = + HashMap::default(); + + let db_breakpoints = project.find_related(breakpoints::Entity).all(tx).await?; + + for breakpoint in db_breakpoints.iter() { + let project_path = proto::ProjectPath { + worktree_id: breakpoint.worktree_id as u64, + path: breakpoint.path.clone(), + }; + + breakpoints + .entry(project_path) + .or_default() + .insert(proto::Breakpoint { + position: None, + cached_position: breakpoint.position as u32, + kind: match breakpoint.kind { + breakpoints::BreakpointKind::Standard => { + proto::BreakpointKind::Standard.into() + } + breakpoints::BreakpointKind::Log => proto::BreakpointKind::Log.into(), + }, + message: breakpoint.log_message.clone(), + }); + } + let language_servers = project .find_related(language_server::Entity) .all(tx) @@ -779,6 +806,7 @@ impl Database { collaborators, worktrees, language_servers, + breakpoints, })) } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 8a4ec29998ac86..fd4553c52b41c4 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -2,6 +2,7 @@ pub mod access_token; pub mod billing_customer; pub mod billing_preference; pub mod billing_subscription; +pub mod breakpoints; pub mod buffer; pub mod buffer_operation; pub mod buffer_snapshot; diff --git a/crates/collab/src/db/tables/breakpoints.rs b/crates/collab/src/db/tables/breakpoints.rs new file mode 100644 index 00000000000000..c00bfef9d6d477 --- /dev/null +++ b/crates/collab/src/db/tables/breakpoints.rs @@ -0,0 +1,47 @@ +use crate::db::ProjectId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "breakpoints")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(primary_key)] + pub project_id: ProjectId, + pub worktree_id: i64, + pub path: String, + pub kind: BreakpointKind, + pub log_message: Option, + pub position: i32, +} + +#[derive( + Copy, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Default, Hash, serde::Serialize, +)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] +#[serde(rename_all = "snake_case")] +pub enum BreakpointKind { + #[default] + #[sea_orm(string_value = "standard")] + Standard, + #[sea_orm(string_value = "log")] + Log, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::project::Entity", + from = "Column::ProjectId", + to = "super::project::Column::Id" + )] + Project, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/project.rs b/crates/collab/src/db/tables/project.rs index 10e3da50e1dd09..09d20bce625435 100644 --- a/crates/collab/src/db/tables/project.rs +++ b/crates/collab/src/db/tables/project.rs @@ -49,6 +49,8 @@ pub enum Relation { Collaborators, #[sea_orm(has_many = "super::language_server::Entity")] LanguageServers, + #[sea_orm(has_many = "super::breakpoints::Entity")] + Breakpoints, } impl Related for Entity { @@ -81,4 +83,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Breakpoints.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0e977074f77646..02bbe95d9255b3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -406,7 +406,8 @@ impl Server { app_state.config.openai_api_key.clone(), ) } - }); + }) + .add_message_handler(update_breakpoints); Arc::new(server) } @@ -1841,6 +1842,18 @@ fn join_project_internal( .trace_err(); } + let breakpoints = project + .breakpoints + .iter() + .map( + |(project_path, breakpoint_set)| proto::SynchronizeBreakpoints { + project_id: project.id.0 as u64, + breakpoints: breakpoint_set.iter().map(|bp| bp.clone()).collect(), + project_path: Some(project_path.clone()), + }, + ) + .collect(); + // First, we send the metadata associated with each worktree. response.send(proto::JoinProjectResponse { project_id: project.id.0 as u64, @@ -1849,6 +1862,7 @@ fn join_project_internal( collaborators: collaborators.clone(), language_servers: project.language_servers.clone(), role: project.role.into(), + breakpoints, })?; for (worktree_id, worktree) in mem::take(&mut project.worktrees) { @@ -2081,6 +2095,29 @@ async fn update_language_server( Ok(()) } +/// Notify other participants that breakpoints have changed. +async fn update_breakpoints( + request: proto::SynchronizeBreakpoints, + session: Session, +) -> Result<()> { + let guest_connection_ids = session + .db() + .await + .update_breakpoints(session.connection_id, &request) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, request.clone()) + }, + ); + Ok(()) +} + /// forward a project request to the host. These requests should be read only /// as guests are allowed to send them. async fn forward_read_only_project_request( diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 17cd1b51c42cd4..e1893e06aaebc8 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -299,6 +299,7 @@ impl TestServer { settings::KeymapFile::load_asset(os_keymap, cx).unwrap(); language_model::LanguageModelRegistry::test(cx); assistant::context_store::init(&client.clone().into()); + project::dap_store::DapStore::init(&client.clone().into()); }); client diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 859fbb4e5eeb33..325bd138a668c4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6858,7 +6858,7 @@ impl Editor { project.toggle_breakpoint( buffer_id, Breakpoint { - cache_position, + cached_position: cache_position, active_position: Some(breakpoint_position), kind, }, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d07b12c4c37cc5..289aaac7c97404 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -5272,7 +5272,7 @@ impl Element for EditorElement { .display_point_to_breakpoint_anchor(gutter_breakpoint_point) .text_anchor, ), - cache_position: 0, + cached_position: 0, kind: BreakpointKind::Standard, }); } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index c368c1900b9316..e18cfe5f3ca9f3 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,6 +1,7 @@ use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectPath}; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; + use collections::HashMap; use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; use dap::{ @@ -26,11 +27,15 @@ use dap_adapters::build_adapter; use fs::Fs; use futures::future::Shared; use futures::FutureExt; -use gpui::{EventEmitter, Model, ModelContext, SharedString, Task}; +use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task}; use http_client::HttpClient; -use language::{Buffer, BufferSnapshot, LanguageRegistry, LanguageServerBinaryStatus}; +use language::{ + proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, + Buffer, BufferSnapshot, LanguageRegistry, LanguageServerBinaryStatus, +}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; +use rpc::{proto, AnyProtoClient, TypedEnvelope}; use serde_json::Value; use settings::{Settings, WorktreeId}; use smol::lock::Mutex; @@ -56,6 +61,7 @@ pub enum DapStoreEvent { message: Message, }, Notification(String), + BreakpointsChanged, } pub enum DebugAdapterClientState { @@ -69,15 +75,31 @@ pub struct DebugPosition { pub column: u32, } +#[allow(clippy::large_enum_variant)] +pub enum DapStoreMode { + Local(LocalDapStore), // ssh host and collab host + Remote(RemoteDapStore), // collab guest +} + +pub struct LocalDapStore { + delegate: DapAdapterDelegate, + environment: Model, +} + +pub struct RemoteDapStore { + upstream_client: Option, + upstream_project_id: u64, +} + pub struct DapStore { + mode: DapStoreMode, next_client_id: AtomicUsize, - delegate: DapAdapterDelegate, + downstream_client: Option<(AnyProtoClient, u64)>, ignore_breakpoints: HashSet, breakpoints: BTreeMap>, - active_debug_line: Option<(DebugAdapterClientId, ProjectPath, DebugPosition)>, capabilities: HashMap, - environment: Model, clients: HashMap, + active_debug_line: Option<(DebugAdapterClientId, ProjectPath, DebugPosition)>, } impl EventEmitter for DapStore {} @@ -85,9 +107,13 @@ impl EventEmitter for DapStore {} impl DapStore { const INDEX_STARTS_AT_ONE: bool = true; - pub fn new( - http_client: Option>, - node_runtime: Option, + pub fn init(client: &AnyProtoClient) { + client.add_model_message_handler(DapStore::handle_synchronize_breakpoints); + } + + pub fn new_local( + http_client: Arc, + node_runtime: NodeRuntime, fs: Arc, languages: Arc, environment: Model, @@ -95,40 +121,82 @@ impl DapStore { ) -> Self { cx.on_app_quit(Self::shutdown_clients).detach(); - let load_shell_env_task = Task::ready(None).shared(); + Self { + mode: DapStoreMode::Local(LocalDapStore { + environment, + delegate: DapAdapterDelegate::new( + Some(http_client.clone()), + Some(node_runtime.clone()), + fs.clone(), + languages.clone(), + Task::ready(None).shared(), + ), + }), + downstream_client: None, + active_debug_line: None, + clients: HashMap::default(), + breakpoints: Default::default(), + capabilities: HashMap::default(), + next_client_id: Default::default(), + ignore_breakpoints: Default::default(), + } + } + pub fn new_remote( + project_id: u64, + upstream_client: AnyProtoClient, + _: &mut ModelContext, + ) -> Self { Self { + mode: DapStoreMode::Remote(RemoteDapStore { + upstream_client: Some(upstream_client), + upstream_project_id: project_id, + }), + downstream_client: None, active_debug_line: None, - clients: Default::default(), + clients: HashMap::default(), breakpoints: Default::default(), capabilities: HashMap::default(), next_client_id: Default::default(), ignore_breakpoints: Default::default(), - delegate: DapAdapterDelegate::new( - http_client.clone(), - node_runtime.clone(), - fs.clone(), - languages.clone(), - load_shell_env_task, - ), - environment, } } - pub fn delegate( - &self, - cwd: &Option, - cx: &mut ModelContext, - ) -> Arc { - let worktree_abs_path = cwd.as_ref().map(|p| Arc::from(p.as_path())); - let task = self.environment.update(cx, |env, cx| { - env.get_environment(None, worktree_abs_path, cx) - }); + pub fn as_remote(&self) -> Option<&RemoteDapStore> { + match &self.mode { + DapStoreMode::Remote(remote_dap_store) => Some(remote_dap_store), + _ => None, + } + } - let mut delegate = self.delegate.clone(); - delegate.refresh_shell_env_task(task); + pub fn as_local(&self) -> Option<&LocalDapStore> { + match &self.mode { + DapStoreMode::Local(local_dap_store) => Some(local_dap_store), + _ => None, + } + } - Arc::new(delegate) + pub fn as_local_mut(&mut self) -> Option<&mut LocalDapStore> { + match &mut self.mode { + DapStoreMode::Local(local_dap_store) => Some(local_dap_store), + _ => None, + } + } + + pub fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> { + match &self.mode { + DapStoreMode::Remote(RemoteDapStore { + upstream_client: Some(upstream_client), + upstream_project_id, + .. + }) => Some((upstream_client.clone(), *upstream_project_id)), + + DapStoreMode::Remote(RemoteDapStore { + upstream_client: None, + .. + }) => None, + DapStoreMode::Local(_) => None, + } } pub fn next_client_id(&self) -> DebugAdapterClientId { @@ -276,7 +344,7 @@ impl DapStore { .or_default() .insert(Breakpoint { active_position: None, - cache_position: serialize_breakpoint.position, + cached_position: serialize_breakpoint.position, kind: serialize_breakpoint.kind, }); } @@ -289,7 +357,7 @@ impl DapStore { ) { if let Some(breakpoint_set) = self.breakpoints.remove(project_path) { let breakpoint_iter = breakpoint_set.into_iter().map(|mut bp| { - bp.cache_position = bp.point_for_buffer(&buffer).row; + bp.cached_position = bp.point_for_buffer(&buffer).row; bp.active_position = None; bp }); @@ -302,8 +370,18 @@ impl DapStore { } pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { + let Some(local_store) = self.as_local_mut() else { + return; + }; + + let mut adapter_delegate = local_store.delegate.clone(); + let worktree_abs_path = config.cwd.as_ref().map(|p| Arc::from(p.as_path())); + adapter_delegate.refresh_shell_env_task(local_store.environment.update(cx, |env, cx| { + env.get_environment(None, worktree_abs_path, cx) + })); + let adapter_delegate = Arc::new(adapter_delegate); + let client_id = self.next_client_id(); - let adapter_delegate = self.delegate(&config.cwd, cx); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); @@ -417,11 +495,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.spawn(|this, mut cx| async move { @@ -606,11 +680,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); @@ -638,11 +708,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.spawn(|this, mut cx| async move { @@ -696,11 +762,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.background_executor().spawn(async move { @@ -729,11 +791,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.background_executor().spawn(async move { @@ -756,11 +814,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); @@ -791,11 +845,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); @@ -827,11 +877,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); @@ -861,11 +907,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task>> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.background_executor().spawn(async move { @@ -891,11 +933,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.background_executor().spawn(async move { @@ -922,11 +960,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task>> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.background_executor().spawn(async move { @@ -954,11 +988,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let supports_set_expression = self @@ -998,11 +1028,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.background_executor() @@ -1016,11 +1042,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); @@ -1045,11 +1067,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.background_executor().spawn(async move { @@ -1070,11 +1088,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let supports_restart = self @@ -1119,11 +1133,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.clients.remove(&client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; cx.emit(DapStoreEvent::DebugClientStopped(*client_id)); @@ -1164,6 +1174,63 @@ impl DapStore { }) } + pub fn set_breakpoints_from_proto( + &mut self, + breakpoints: Vec, + cx: &mut ModelContext, + ) { + self.breakpoints.clear(); + + for project_breakpoints in breakpoints { + let Some(project_path) = project_breakpoints.project_path else { + continue; + }; + + self.breakpoints.insert( + ProjectPath::from_proto(project_path), + project_breakpoints + .breakpoints + .into_iter() + .filter_map(Breakpoint::from_proto) + .collect::>(), + ); + } + + cx.notify(); + } + + async fn handle_synchronize_breakpoints( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + let project_path = ProjectPath::from_proto( + envelope + .payload + .project_path + .context("Invalid Breakpoint call")?, + ); + + this.update(&mut cx, |store, cx| { + let breakpoints = envelope + .payload + .breakpoints + .into_iter() + .filter_map(Breakpoint::from_proto) + .collect::>(); + + if breakpoints.is_empty() { + store.breakpoints.remove(&project_path); + } else { + store.breakpoints.insert(project_path, breakpoints); + } + + cx.emit(DapStoreEvent::BreakpointsChanged); + + cx.notify(); + }) + } + pub fn toggle_breakpoint_for_buffer( &mut self, project_path: &ProjectPath, @@ -1173,6 +1240,8 @@ impl DapStore { edit_action: BreakpointEditAction, cx: &mut ModelContext, ) -> Task> { + let upstream_client = self.upstream_client(); + let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); match edit_action { @@ -1189,6 +1258,19 @@ impl DapStore { cx.notify(); + if let Some((client, project_id)) = upstream_client.or(self.downstream_client.clone()) { + client + .send(client::proto::SynchronizeBreakpoints { + project_id, + project_path: Some(project_path.to_proto()), + breakpoints: breakpoint_set + .iter() + .filter_map(|breakpoint| breakpoint.to_proto()) + .collect(), + }) + .log_err(); + } + self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) } @@ -1201,11 +1283,7 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!( - "Could not find client: file: {}:{}", - file!(), - line!() - ))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; if Self::INDEX_STARTS_AT_ONE { @@ -1276,6 +1354,34 @@ impl DapStore { Ok(()) }) } + + pub fn shared( + &mut self, + project_id: u64, + downstream_client: AnyProtoClient, + _: &mut ModelContext, + ) { + self.downstream_client = Some((downstream_client.clone(), project_id)); + + for (project_path, breakpoints) in self.breakpoints.iter() { + downstream_client + .send(proto::SynchronizeBreakpoints { + project_id, + project_path: Some(project_path.to_proto()), + breakpoints: breakpoints + .iter() + .filter_map(|breakpoint| breakpoint.to_proto()) + .collect(), + }) + .log_err(); + } + } + + pub fn unshared(&mut self, cx: &mut ModelContext) { + self.downstream_client.take(); + + cx.notify(); + } } type LogMessage = Arc; @@ -1325,7 +1431,7 @@ impl Hash for BreakpointKind { #[derive(Clone, Debug)] pub struct Breakpoint { pub active_position: Option, - pub cache_position: u32, + pub cached_position: u32, pub kind: BreakpointKind, } @@ -1336,7 +1442,7 @@ pub struct Breakpoint { impl PartialEq for Breakpoint { fn eq(&self, other: &Self) -> bool { match (&self.active_position, &other.active_position) { - (None, None) => self.cache_position == other.cache_position, + (None, None) => self.cached_position == other.cached_position, (None, Some(_)) => false, (Some(_), None) => false, (Some(self_position), Some(other_position)) => self_position == other_position, @@ -1351,7 +1457,7 @@ impl Hash for Breakpoint { if self.active_position.is_some() { self.active_position.hash(state); } else { - self.cache_position.hash(state); + self.cached_position.hash(state); } } } @@ -1361,7 +1467,7 @@ impl Breakpoint { let line = self .active_position .map(|position| buffer.summary_for_anchor::(&position).row) - .unwrap_or(self.cache_position) as u64; + .unwrap_or(self.cached_position) as u64; let log_message = match &self.kind { BreakpointKind::Standard => None, @@ -1381,27 +1487,27 @@ impl Breakpoint { pub fn set_active_position(&mut self, buffer: &Buffer) { if self.active_position.is_none() { self.active_position = - Some(buffer.breakpoint_anchor(Point::new(self.cache_position, 0))); + Some(buffer.breakpoint_anchor(Point::new(self.cached_position, 0))); } } pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { self.active_position .map(|position| buffer.summary_for_anchor::(&position)) - .unwrap_or(Point::new(self.cache_position, 0)) + .unwrap_or(Point::new(self.cached_position, 0)) } pub fn point_for_buffer_snapshot(&self, buffer_snapshot: &BufferSnapshot) -> Point { self.active_position .map(|position| buffer_snapshot.summary_for_anchor::(&position)) - .unwrap_or(Point::new(self.cache_position, 0)) + .unwrap_or(Point::new(self.cached_position, 0)) } pub fn source_for_snapshot(&self, snapshot: &BufferSnapshot) -> SourceBreakpoint { let line = self .active_position .map(|position| snapshot.summary_for_anchor::(&position).row) - .unwrap_or(self.cache_position) as u64; + .unwrap_or(self.cached_position) as u64; let log_message = match &self.kind { BreakpointKind::Standard => None, @@ -1424,17 +1530,54 @@ impl Breakpoint { position: self .active_position .map(|position| buffer.summary_for_anchor::(&position).row) - .unwrap_or(self.cache_position), + .unwrap_or(self.cached_position), path, kind: self.kind.clone(), }, None => SerializedBreakpoint { - position: self.cache_position, + position: self.cached_position, path, kind: self.kind.clone(), }, } } + + pub fn to_proto(&self) -> Option { + Some(client::proto::Breakpoint { + position: if let Some(position) = &self.active_position { + Some(serialize_text_anchor(position)) + } else { + None + }, + cached_position: self.cached_position, + kind: match self.kind { + BreakpointKind::Standard => proto::BreakpointKind::Standard.into(), + BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(), + }, + message: if let BreakpointKind::Log(message) = &self.kind { + Some(message.to_string()) + } else { + None + }, + }) + } + + pub fn from_proto(breakpoint: client::proto::Breakpoint) -> Option { + Some(Self { + active_position: if let Some(position) = breakpoint.position.clone() { + deserialize_anchor(position) + } else { + None + }, + cached_position: breakpoint.cached_position, + kind: match proto::BreakpointKind::from_i32(breakpoint.kind) { + Some(proto::BreakpointKind::Log) => { + BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into()) + } + None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard, + }, + }) + } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d838488000b145..e6a0fb76018d0a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -516,6 +516,7 @@ enum EntitySubscription { WorktreeStore(PendingEntitySubscription), LspStore(PendingEntitySubscription), SettingsObserver(PendingEntitySubscription), + DapStore(PendingEntitySubscription), } #[derive(Clone)] @@ -611,6 +612,7 @@ impl Project { SettingsObserver::init(&client); TaskStore::init(Some(&client)); ToolchainStore::init(&client); + DapStore::init(&client); } pub fn local( @@ -634,9 +636,9 @@ impl Project { let environment = ProjectEnvironment::new(&worktree_store, env, cx); let dap_store = cx.new_model(|cx| { - DapStore::new( - Some(client.http_client()), - Some(node.clone()), + DapStore::new_local( + client.http_client(), + node.clone(), fs.clone(), languages.clone(), environment.clone(), @@ -827,16 +829,8 @@ impl Project { }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - let dap_store = cx.new_model(|cx| { - DapStore::new( - Some(client.http_client()), - Some(node.clone()), - fs.clone(), - languages.clone(), - environment.clone(), - cx, - ) - }); + let dap_store = + cx.new_model(|cx| DapStore::new_remote(SSH_PROJECT_ID, client.clone().into(), cx)); cx.subscribe(&ssh, Self::on_ssh_event).detach(); cx.observe(&ssh, |_, _, cx| cx.notify()).detach(); @@ -898,6 +892,7 @@ impl Project { ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); + ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.dap_store); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); ssh_proto.add_model_message_handler(Self::handle_create_buffer_for_peer); @@ -912,6 +907,7 @@ impl Project { SettingsObserver::init(&ssh_proto); TaskStore::init(Some(&ssh_proto)); ToolchainStore::init(&ssh_proto); + DapStore::init(&ssh_proto); this }) @@ -955,6 +951,7 @@ impl Project { EntitySubscription::SettingsObserver( client.subscribe_to_entity::(remote_id)?, ), + EntitySubscription::DapStore(client.subscribe_to_entity::(remote_id)?), ]; let response = client .request_envelope(proto::JoinProject { @@ -977,7 +974,7 @@ impl Project { #[allow(clippy::too_many_arguments)] async fn from_join_project_response( response: TypedEnvelope, - subscriptions: [EntitySubscription; 5], + subscriptions: [EntitySubscription; 6], client: Arc, run_tasks: bool, user_store: Model, @@ -1001,14 +998,10 @@ impl Project { let environment = cx.update(|cx| ProjectEnvironment::new(&worktree_store, None, cx))?; let dap_store = cx.new_model(|cx| { - DapStore::new( - Some(client.http_client()), - None, - fs.clone(), - languages.clone(), - environment.clone(), - cx, - ) + let mut dap_store = DapStore::new_remote(remote_id, client.clone().into(), cx); + + dap_store.set_breakpoints_from_proto(response.payload.breakpoints, cx); + dap_store })?; let lsp_store = cx.new_model(|cx| { @@ -1097,7 +1090,7 @@ impl Project { remote_id, replica_id, }, - dap_store, + dap_store: dap_store.clone(), buffers_needing_diff: Default::default(), git_diff_debouncer: DebouncedDelay::new(), terminals: Terminals { @@ -1134,6 +1127,9 @@ impl Project { EntitySubscription::LspStore(subscription) => { subscription.set_model(&lsp_store, &mut cx) } + EntitySubscription::DapStore(subscription) => { + subscription.set_model(&dap_store, &mut cx) + } }) .collect::>(); @@ -1857,6 +1853,9 @@ impl Project { self.client .subscribe_to_entity(project_id)? .set_model(&self.lsp_store, &mut cx.to_async()), + self.client + .subscribe_to_entity(project_id)? + .set_model(&self.dap_store, &mut cx.to_async()), self.client .subscribe_to_entity(project_id)? .set_model(&self.settings_observer, &mut cx.to_async()), @@ -1871,6 +1870,9 @@ impl Project { self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.shared(project_id, self.client.clone().into(), cx) }); + self.dap_store.update(cx, |dap_store, cx| { + dap_store.shared(project_id, self.client.clone().into(), cx); + }); self.task_store.update(cx, |task_store, cx| { task_store.shared(project_id, self.client.clone().into(), cx); }); @@ -1926,6 +1928,9 @@ impl Project { self.lsp_store.update(cx, |lsp_store, _| { lsp_store.set_language_server_statuses_from_proto(message.language_servers) }); + self.dap_store.update(cx, |dap_store, cx| { + dap_store.set_breakpoints_from_proto(message.breakpoints, cx); + }); self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync) .unwrap(); cx.emit(Event::Rejoined); @@ -1958,6 +1963,9 @@ impl Project { self.task_store.update(cx, |task_store, cx| { task_store.unshared(cx); }); + self.dap_store.update(cx, |dap_store, cx| { + dap_store.unshared(cx); + }); self.settings_observer.update(cx, |settings_observer, cx| { settings_observer.unshared(cx); }); @@ -2410,10 +2418,15 @@ impl Project { message: message.clone(), }); } - DapStoreEvent::Notification(message) => cx.emit(Event::Toast { - notification_id: "dap".into(), - message: message.clone(), - }), + DapStoreEvent::Notification(message) => { + cx.emit(Event::Toast { + notification_id: "dap".into(), + message: message.clone(), + }); + } + DapStoreEvent::BreakpointsChanged => { + cx.notify(); + } } } diff --git a/crates/proto/build.rs b/crates/proto/build.rs index d94d80082aefe8..b16aad1b6909b8 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -2,6 +2,9 @@ fn main() { let mut build = prost_build::Config::new(); build .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") + .type_attribute("ProjectPath", "#[derive(Hash, Eq)]") + .type_attribute("Breakpoint", "#[derive(Hash, Eq)]") + .type_attribute("Anchor", "#[derive(Hash, Eq)]") .compile_protos(&["proto/zed.proto"], &["proto"]) .unwrap(); } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index b86894bae1e361..10e9c41a0d9c2a 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -293,7 +293,9 @@ message Envelope { GetPanicFiles get_panic_files = 280; GetPanicFilesResponse get_panic_files_response = 281; - CancelLanguageServerWork cancel_language_server_work = 282; // current max + CancelLanguageServerWork cancel_language_server_work = 282; + + SynchronizeBreakpoints Synchronize_breakpoints = 283; // current max } reserved 87 to 88; @@ -412,6 +414,7 @@ message RejoinedProject { repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; + repeated SynchronizeBreakpoints breakpoints = 5; } message LeaveRoom {} @@ -549,6 +552,7 @@ message JoinProjectResponse { repeated LanguageServer language_servers = 4; ChannelRole role = 6; reserved 7; + repeated SynchronizeBreakpoints breakpoints = 8; } message LeaveProject { @@ -2366,6 +2370,26 @@ message GetLlmTokenResponse { message RefreshLlmToken {} +// Remote debugging + +enum BreakpointKind { + Standard = 0; + Log = 1; +} + +message Breakpoint { + Anchor position = 1; + uint32 cached_position = 2; + BreakpointKind kind = 3; + optional string message = 4; +} + +message SynchronizeBreakpoints { + uint64 project_id = 1; + ProjectPath project_path = 2; + repeated Breakpoint breakpoints = 3; +} + // Remote FS message AddWorktree { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index e143f24a47a5a3..e83bdf8b042d48 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -366,6 +366,7 @@ messages!( (GetPanicFiles, Background), (GetPanicFilesResponse, Background), (CancelLanguageServerWork, Foreground), + (SynchronizeBreakpoints, Foreground), ); request_messages!( @@ -571,6 +572,7 @@ entity_messages!( ActiveToolchain, GetPathMetadata, CancelLanguageServerWork, + SynchronizeBreakpoints, ); entity_messages!( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index ffe906edc76f92..7ddfde92b206ea 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -76,9 +76,9 @@ impl HeadlessProject { let environment = project::ProjectEnvironment::new(&worktree_store, None, cx); let dap_store = cx.new_model(|cx| { - DapStore::new( - None, - None, + DapStore::new_local( + http_client.clone(), + node_runtime.clone(), fs.clone(), languages.clone(), environment.clone(), @@ -171,6 +171,7 @@ impl HeadlessProject { session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store); session.subscribe_to_entity(SSH_PROJECT_ID, &task_store); session.subscribe_to_entity(SSH_PROJECT_ID, &toolchain_store); + session.subscribe_to_entity(SSH_PROJECT_ID, &dap_store); session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory); @@ -195,6 +196,7 @@ impl HeadlessProject { LspStore::init(&client); TaskStore::init(Some(&client)); ToolchainStore::init(&client); + DapStore::init(&client); HeadlessProject { session: client, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 116157e3c02882..5d1b9cc7624648 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3549,6 +3549,7 @@ mod tests { ); repl::notebook::init(cx); tasks_ui::init(cx); + project::dap_store::DapStore::init(&app_state.client.clone().into()); debugger_ui::init(cx); initialize_workspace(app_state.clone(), prompt_builder, cx); search::init(cx); From 5971d3794230792b8acdcd06c1e9f6bb0c01b203 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 25 Nov 2024 19:03:35 +0100 Subject: [PATCH 364/650] Fix failing tests --- crates/collab/src/tests/test_server.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 7b796b45806b81..c93cce9770e58f 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -300,7 +300,6 @@ impl TestServer { settings::KeymapFile::load_asset(os_keymap, cx).unwrap(); language_model::LanguageModelRegistry::test(cx); assistant::context_store::init(&client.clone().into()); - project::dap_store::DapStore::init(&client.clone().into()); }); client From 35a52a7f9033a3bb738a8f9f54b3e466491bdb24 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 26 Nov 2024 19:07:11 -0500 Subject: [PATCH 365/650] Fix bug where breakpoints weren't render on active lines --- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/element.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9783ec0164ee7d..97f4306f3df7dc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5587,7 +5587,7 @@ impl Editor { _style: &EditorStyle, row: DisplayRow, is_active: bool, - breakpoint: Option, + breakpoint: Option<&Breakpoint>, cx: &mut ViewContext, ) -> Option { let color = if breakpoint.is_some() { @@ -5598,7 +5598,7 @@ impl Editor { let bp_kind = Arc::new( breakpoint - .map(|bp| bp.kind) + .map(|bp| bp.kind.clone()) .unwrap_or(BreakpointKind::Standard), ); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e1dc2b146cf37a..5d2c47acaaa58f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1850,11 +1850,12 @@ impl EditorElement { active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row); }; - let breakpoint = breakpoint_points.remove(&row); + let breakpoint = breakpoint_points.get(&row); button = editor.render_code_actions_indicator(&self.style, row, active, breakpoint, cx); }); let button = button?; + breakpoint_points.remove(&row); let button = prepaint_gutter_button( button, From 6bc5679c863fd46484f17fddea5331cd035f0b4b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 27 Nov 2024 11:18:43 +0100 Subject: [PATCH 366/650] Always send a response back when you receive a startDebugging reverse request --- crates/debugger_ui/src/debugger_panel.rs | 6 +- crates/project/src/dap_store.rs | 89 ++++++++++++++---------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1ece48e267cb9e..7348de45229a31 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -266,11 +266,7 @@ impl DebugPanel { let args = if let Some(args) = request_args { serde_json::from_value(args.clone()).ok() } else { - return; - }; - - let Some(args) = args else { - return; + None }; self.dap_store.update(cx, |store, cx| { diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e18cfe5f3ca9f3..72bb30592547bd 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -704,7 +704,7 @@ impl DapStore { &self, client_id: &DebugAdapterClientId, seq: u64, - args: StartDebuggingRequestArguments, + args: Option, cx: &mut ModelContext, ) -> Task> { let Some(client) = self.client_by_id(client_id) else { @@ -712,44 +712,59 @@ impl DapStore { }; cx.spawn(|this, mut cx| async move { - client - .send_message(Message::Response(Response { - seq, - request_seq: seq, - success: true, - command: StartDebugging::COMMAND.to_string(), - body: None, - })) - .await?; - - let config = client.config(); + if let Some(args) = args { + client + .send_message(Message::Response(Response { + seq, + request_seq: seq, + success: true, + command: StartDebugging::COMMAND.to_string(), + body: None, + })) + .await?; - this.update(&mut cx, |store, cx| { - store.start_client( - DebugAdapterConfig { - kind: config.kind.clone(), - request: match args.request { - StartDebuggingRequestArgumentsRequest::Launch => { - DebugRequestType::Launch - } - StartDebuggingRequestArgumentsRequest::Attach => { - DebugRequestType::Attach( - if let DebugRequestType::Attach(attach_config) = config.request - { - attach_config - } else { - AttachConfig::default() - }, - ) - } + this.update(&mut cx, |store, cx| { + let config = client.config(); + + store.start_client( + DebugAdapterConfig { + kind: config.kind.clone(), + request: match args.request { + StartDebuggingRequestArgumentsRequest::Launch => { + DebugRequestType::Launch + } + StartDebuggingRequestArgumentsRequest::Attach => { + DebugRequestType::Attach( + if let DebugRequestType::Attach(attach_config) = + config.request + { + attach_config + } else { + AttachConfig::default() + }, + ) + } + }, + program: config.program.clone(), + cwd: config.cwd.clone(), + initialize_args: Some(args.configuration), }, - program: config.program.clone(), - cwd: config.cwd.clone(), - initialize_args: Some(args.configuration), - }, - cx, - ); - }) + cx, + ); + }) + } else { + client + .send_message(Message::Response(Response { + seq, + request_seq: seq, + success: false, + command: StartDebugging::COMMAND.to_string(), + body: Some(serde_json::to_value(ErrorResponse { error: None })?), + })) + .await?; + + Ok(()) + } }) } From df71b972e1c32021f7e46a77e3cacc1252fe126b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 27 Nov 2024 17:21:22 +0100 Subject: [PATCH 367/650] Fix a rare panic when selecting a stack frame but new stack frames are being loaded --- crates/debugger_ui/src/console.rs | 4 +- crates/debugger_ui/src/debugger_panel_item.rs | 13 ++++- crates/debugger_ui/src/stack_frame_list.rs | 55 ++++++++----------- crates/editor/src/editor.rs | 6 +- crates/project/src/dap_store.rs | 22 +------- 5 files changed, 40 insertions(+), 60 deletions(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 4772b5738ab99e..c05ff193db8177 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -80,9 +80,7 @@ impl Console { ) { match event { StackFrameListEvent::SelectedStackFrameChanged => cx.notify(), - StackFrameListEvent::StackFramesUpdated => { - // TODO debugger: check if we need to do something here - } + StackFrameListEvent::StackFramesUpdated => {} } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 5379bd5a578139..33c530962014c5 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -386,9 +386,16 @@ impl DebugPanelItem { pub fn go_to_current_stack_frame(&self, cx: &mut ViewContext) { self.stack_frame_list.update(cx, |stack_frame_list, cx| { - stack_frame_list - .go_to_stack_frame(cx) - .detach_and_log_err(cx); + if let Some(stack_frame) = stack_frame_list + .stack_frames() + .iter() + .find(|frame| frame.id == stack_frame_list.current_stack_frame_id()) + .cloned() + { + stack_frame_list + .select_stack_frame(&stack_frame, true, cx) + .detach_and_log_err(cx); + } }); } diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 49006e58c424ff..bb2892b2e836cc 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -109,25 +109,20 @@ impl StackFrameList { let task = this.update(&mut cx, |this, cx| { std::mem::swap(&mut this.stack_frames, &mut stack_frames); - if let Some(stack_frame) = this.stack_frames.first() { - this.current_stack_frame_id = stack_frame.id; - } - this.list.reset(this.stack_frames.len()); - cx.notify(); cx.emit(StackFrameListEvent::StackFramesUpdated); - if go_to_stack_frame { - Some(this.go_to_stack_frame(cx)) - } else { - None - } + let stack_frame = this + .stack_frames + .first() + .cloned() + .ok_or_else(|| anyhow!("No stack frame found to select"))?; + + anyhow::Ok(this.select_stack_frame(&stack_frame, go_to_stack_frame, cx)) })?; - if let Some(task) = task { - task.await?; - } + task?.await?; this.update(&mut cx, |this, _| { this.fetch_stack_frames_task.take(); @@ -135,19 +130,22 @@ impl StackFrameList { })); } - pub fn go_to_stack_frame(&mut self, cx: &mut ViewContext) -> Task> { - let stack_frame = self - .stack_frames - .iter() - .find(|s| s.id == self.current_stack_frame_id) - .cloned(); + pub fn select_stack_frame( + &mut self, + stack_frame: &StackFrame, + go_to_stack_frame: bool, + cx: &mut ViewContext, + ) -> Task> { + self.current_stack_frame_id = stack_frame.id; + + cx.emit(StackFrameListEvent::SelectedStackFrameChanged); + cx.notify(); - let Some(stack_frame) = stack_frame else { - return Task::ready(Ok(())); // this could never happen + if !go_to_stack_frame { + return Task::ready(Ok(())); }; let row = (stack_frame.line.saturating_sub(1)) as u32; - let column = (stack_frame.column.saturating_sub(1)) as u32; let Some(project_path) = self.project_path_from_stack_frame(&stack_frame, cx) else { return Task::ready(Err(anyhow!("Project path not found"))); @@ -165,7 +163,7 @@ impl StackFrameList { this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { - store.set_active_debug_line(&client_id, &project_path, row, column, cx); + store.set_active_debug_line(&client_id, &project_path, row, cx); }) })?; @@ -218,15 +216,10 @@ impl StackFrameList { this.bg(cx.theme().colors().element_hover) }) .on_click(cx.listener({ - let stack_frame_id = stack_frame.id; + let stack_frame = stack_frame.clone(); move |this, _, cx| { - this.current_stack_frame_id = stack_frame_id; - - this.go_to_stack_frame(cx).detach_and_log_err(cx); - - cx.notify(); - - cx.emit(StackFrameListEvent::SelectedStackFrameChanged); + this.select_stack_frame(&stack_frame, true, cx) + .detach_and_log_err(cx); } })) .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 97f4306f3df7dc..9e8b4fff4743d1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10331,14 +10331,13 @@ impl Editor { pub fn go_to_line( &mut self, row: u32, - column: u32, highlight_color: Option, cx: &mut ViewContext, ) { let snapshot = self.snapshot(cx).display_snapshot; let start = snapshot .buffer_snapshot - .clip_point(Point::new(row, column), Bias::Left); + .clip_point(Point::new(row, 0), Bias::Left); let end = start + Point::new(1, 0); let start = snapshot.buffer_snapshot.anchor_before(start); let end = snapshot.buffer_snapshot.anchor_before(end); @@ -12245,8 +12244,7 @@ impl Editor { if let Some((_, path, position)) = dap_store.read(cx).active_debug_line() { if path == project_path { self.go_to_line::( - position.row, - position.column, + position, Some(cx.theme().colors().editor_debugger_active_line_background), cx, ); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 72bb30592547bd..0209b17c3059f7 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -69,12 +69,6 @@ pub enum DebugAdapterClientState { Running(Arc), } -#[derive(Clone, Debug)] -pub struct DebugPosition { - pub row: u32, - pub column: u32, -} - #[allow(clippy::large_enum_variant)] pub enum DapStoreMode { Local(LocalDapStore), // ssh host and collab host @@ -99,7 +93,7 @@ pub struct DapStore { breakpoints: BTreeMap>, capabilities: HashMap, clients: HashMap, - active_debug_line: Option<(DebugAdapterClientId, ProjectPath, DebugPosition)>, + active_debug_line: Option<(DebugAdapterClientId, ProjectPath, u32)>, } impl EventEmitter for DapStore {} @@ -237,7 +231,7 @@ impl DapStore { } } - pub fn active_debug_line(&self) -> Option<(DebugAdapterClientId, ProjectPath, DebugPosition)> { + pub fn active_debug_line(&self) -> Option<(DebugAdapterClientId, ProjectPath, u32)> { self.active_debug_line.clone() } @@ -246,15 +240,9 @@ impl DapStore { client_id: &DebugAdapterClientId, project_path: &ProjectPath, row: u32, - column: u32, cx: &mut ModelContext, ) { - self.active_debug_line = Some(( - *client_id, - project_path.clone(), - DebugPosition { row, column }, - )); - + self.active_debug_line = Some((*client_id, project_path.clone(), row)); cx.notify(); } @@ -271,10 +259,6 @@ impl DapStore { } } - pub fn remove_active_debug_line(&mut self) { - self.active_debug_line.take(); - } - pub fn on_file_rename(&mut self, old_project_path: ProjectPath, new_project_path: ProjectPath) { if let Some(breakpoints) = self.breakpoints.remove(&old_project_path) { self.breakpoints.insert(new_project_path, breakpoints); From 4068960686774cbd3d68fca0af241496863125bb Mon Sep 17 00:00:00 2001 From: jansol Date: Thu, 28 Nov 2024 22:14:27 +0200 Subject: [PATCH 368/650] Add gdb debug adapter implementation (#70) * dap_adapters: add gdb * debug: Add debug zed with gdb task --- .zed/debug.json | 10 +++ crates/dap_adapters/src/dap_adapters.rs | 3 + crates/dap_adapters/src/gdb.rs | 85 +++++++++++++++++++++++++ crates/task/src/debug_format.rs | 3 + 4 files changed, 101 insertions(+) create mode 100644 crates/dap_adapters/src/gdb.rs diff --git a/.zed/debug.json b/.zed/debug.json index d799c13a8e1855..b7646ee3bd7d28 100644 --- a/.zed/debug.json +++ b/.zed/debug.json @@ -5,5 +5,15 @@ "program": "$ZED_WORKTREE_ROOT/target/debug/zed", "request": "launch", "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "Debug Zed with GDB", + "adapter": "gdb", + "program": "$ZED_WORKTREE_ROOT/target/debug/zed", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT", + "initialize_args": { + "stopAtBeginningOfMainSubprogram": true + } } ] diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index d905ebd0a1c8cf..b47e9ed24e31ea 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -1,4 +1,5 @@ mod custom; +mod gdb; mod go; mod javascript; mod lldb; @@ -12,6 +13,7 @@ use dap::adapters::{ self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, GithubRepo, }; +use gdb::GdbDebugAdapter; use go::GoDebugAdapter; use javascript::JsDebugAdapter; use lldb::LldbDebugAdapter; @@ -33,5 +35,6 @@ pub async fn build_adapter(kind: &DebugAdapterKind) -> Result Ok(Box::new(LldbDebugAdapter::new())), DebugAdapterKind::Go(host) => Ok(Box::new(GoDebugAdapter::new(host).await?)), + DebugAdapterKind::Gdb => Ok(Box::new(GdbDebugAdapter::new())), } } diff --git a/crates/dap_adapters/src/gdb.rs b/crates/dap_adapters/src/gdb.rs new file mode 100644 index 00000000000000..d87c1b0d927f3f --- /dev/null +++ b/crates/dap_adapters/src/gdb.rs @@ -0,0 +1,85 @@ +use std::ffi::OsStr; + +use anyhow::Result; +use async_trait::async_trait; +use dap::transport::{StdioTransport, Transport}; +use task::DebugAdapterConfig; + +use crate::*; + +pub(crate) struct GdbDebugAdapter {} + +impl GdbDebugAdapter { + const ADAPTER_NAME: &'static str = "gdb"; + + pub(crate) fn new() -> Self { + GdbDebugAdapter {} + } +} + +#[async_trait(?Send)] +impl DebugAdapter for GdbDebugAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::ADAPTER_NAME.into()) + } + + fn transport(&self) -> Box { + Box::new(StdioTransport::new()) + } + + async fn get_binary( + &self, + delegate: &dyn DapDelegate, + config: &DebugAdapterConfig, + user_installed_path: Option, + ) -> Result { + let user_setting_path = user_installed_path + .filter(|p| p.exists()) + .and_then(|p| p.to_str().map(|s| s.to_string())); + + /* GDB implements DAP natively so just need to */ + let gdb_path = delegate + .which(OsStr::new("gdb")) + .and_then(|p| p.to_str().map(|s| s.to_string())) + .ok_or(anyhow!("Could not find gdb in path")); + + if gdb_path.is_err() && user_setting_path.is_none() { + bail!("Could not find gdb path or it's not installed"); + } + + let gdb_path = user_setting_path.unwrap_or(gdb_path?); + + Ok(DebugAdapterBinary { + command: gdb_path, + arguments: Some(vec!["-i=dap".into()]), + envs: None, + cwd: config.cwd.clone(), + version: "1".into(), + }) + } + + async fn install_binary( + &self, + _version: AdapterVersion, + _delegate: &dyn DapDelegate, + ) -> Result<()> { + unimplemented!("GDB debug adapter cannot be installed by Zed (yet)") + } + + async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { + unimplemented!("Fetch latest GDB version not implemented (yet)") + } + + async fn get_installed_binary( + &self, + _: &dyn DapDelegate, + _: &DebugAdapterConfig, + _: Option, + ) -> Result { + unimplemented!("GDB cannot be installed by Zed (yet)") + } + + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + json!({"program": config.program, "cwd": config.cwd}) + } +} diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index c039a987feccbf..621bd2a9482b4b 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -73,6 +73,8 @@ pub enum DebugAdapterKind { Go(TCPHost), /// Use lldb Lldb, + /// Use GDB's built-in DAP support + Gdb, } impl DebugAdapterKind { @@ -84,6 +86,7 @@ impl DebugAdapterKind { Self::Php(_) => "PHP", Self::Javascript(_) => "JavaScript", Self::Lldb => "LLDB", + Self::Gdb => "GDB", Self::Go(_) => "Go", } } From b8b65f7a8ffa24d251c6b9df74767754d8b92839 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 28 Nov 2024 15:32:07 -0500 Subject: [PATCH 369/650] Disable gdb dap on non x86 platforms Gdb doesn't run on Arm platforms so I'm disabling it to avoid users running into bugs. It will still work on intel macs and x86 devices --- crates/dap_adapters/src/dap_adapters.rs | 3 +++ crates/task/src/debug_format.rs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index b47e9ed24e31ea..357eb6075be6d4 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -1,4 +1,5 @@ mod custom; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod gdb; mod go; mod javascript; @@ -13,6 +14,7 @@ use dap::adapters::{ self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, GithubRepo, }; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] use gdb::GdbDebugAdapter; use go::GoDebugAdapter; use javascript::JsDebugAdapter; @@ -35,6 +37,7 @@ pub async fn build_adapter(kind: &DebugAdapterKind) -> Result Ok(Box::new(LldbDebugAdapter::new())), DebugAdapterKind::Go(host) => Ok(Box::new(GoDebugAdapter::new(host).await?)), + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] DebugAdapterKind::Gdb => Ok(Box::new(GdbDebugAdapter::new())), } } diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 621bd2a9482b4b..f5ca35b68ce5e9 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -74,6 +74,7 @@ pub enum DebugAdapterKind { /// Use lldb Lldb, /// Use GDB's built-in DAP support + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] Gdb, } @@ -86,6 +87,7 @@ impl DebugAdapterKind { Self::Php(_) => "PHP", Self::Javascript(_) => "JavaScript", Self::Lldb => "LLDB", + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] Self::Gdb => "GDB", Self::Go(_) => "Go", } From 386031e6dd7f5edc9166b67c18559437b2636958 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 28 Nov 2024 21:48:36 +0100 Subject: [PATCH 370/650] Collab: Sync active debug line (#71) * Add sync active debug line over collab * Remove unused downcast * Remove unused import --- crates/collab/src/rpc.rs | 6 ++- crates/debugger_ui/src/stack_frame_list.rs | 15 ++---- crates/editor/src/editor.rs | 7 +++ crates/project/src/dap_store.rs | 63 ++++++++++++++++++++++ crates/project/src/project.rs | 4 ++ crates/proto/proto/zed.proto | 15 +++++- crates/proto/src/proto.rs | 4 ++ 7 files changed, 102 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c8691da02cef03..50bbd2ff419c26 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -408,7 +408,11 @@ impl Server { ) } }) - .add_message_handler(update_breakpoints); + .add_message_handler(update_breakpoints) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler( + broadcast_project_message_from_host::, + ); Arc::new(server) } diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index bb2892b2e836cc..1fe57d64b28f17 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -3,7 +3,6 @@ use std::path::Path; use anyhow::{anyhow, Result}; use dap::client::DebugAdapterClientId; use dap::StackFrame; -use editor::Editor; use gpui::{ list, AnyElement, EventEmitter, FocusHandle, ListState, Subscription, Task, View, WeakView, }; @@ -155,20 +154,16 @@ impl StackFrameList { let client_id = self.client_id; let workspace = self.workspace.clone(); move |this, mut cx| async move { - let task = workspace.update(&mut cx, |workspace, cx| { - workspace.open_path_preview(project_path.clone(), None, false, true, cx) - })?; - - let editor = task.await?.downcast::().unwrap(); + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path_preview(project_path.clone(), None, false, true, cx) + })? + .await?; this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { store.set_active_debug_line(&client_id, &project_path, row, cx); }) - })?; - - workspace.update(&mut cx, |_, cx| { - editor.update(cx, |editor, cx| editor.go_to_active_debug_line(cx)) }) } }) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9e8b4fff4743d1..0b37af8e53e838 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2047,6 +2047,8 @@ impl Editor { } } } + } else if let project::Event::ActiveDebugLineChanged = event { + editor.go_to_active_debug_line(cx); } })); if let Some(task_inventory) = project @@ -12248,8 +12250,13 @@ impl Editor { Some(cx.theme().colors().editor_debugger_active_line_background), cx, ); + + return; } } + + self.clear_row_highlights::(); + cx.notify(); } pub fn toggle_git_blame(&mut self, _: &ToggleGitBlame, cx: &mut ViewContext) { diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 0209b17c3059f7..1825682a9ba983 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -62,6 +62,7 @@ pub enum DapStoreEvent { }, Notification(String), BreakpointsChanged, + ActiveDebugLineChanged, } pub enum DebugAdapterClientState { @@ -103,6 +104,8 @@ impl DapStore { pub fn init(client: &AnyProtoClient) { client.add_model_message_handler(DapStore::handle_synchronize_breakpoints); + client.add_model_message_handler(DapStore::handle_set_active_debug_line); + client.add_model_message_handler(DapStore::handle_remove_active_debug_line); } pub fn new_local( @@ -243,7 +246,21 @@ impl DapStore { cx: &mut ModelContext, ) { self.active_debug_line = Some((*client_id, project_path.clone(), row)); + cx.emit(DapStoreEvent::ActiveDebugLineChanged); cx.notify(); + + if let Some((client, project_id)) = + self.upstream_client().or(self.downstream_client.clone()) + { + client + .send(client::proto::SetActiveDebugLine { + row, + project_id, + client_id: client_id.0 as u64, + project_path: Some(project_path.to_proto()), + }) + .log_err(); + } } pub fn remove_active_debug_line_for_client( @@ -254,7 +271,16 @@ impl DapStore { if let Some(active_line) = &self.active_debug_line { if active_line.0 == *client_id { self.active_debug_line.take(); + cx.emit(DapStoreEvent::ActiveDebugLineChanged); cx.notify(); + + if let Some((client, project_id)) = + self.upstream_client().or(self.downstream_client.clone()) + { + client + .send(client::proto::RemoveActiveDebugLine { project_id }) + .log_err(); + } } } } @@ -1230,6 +1256,43 @@ impl DapStore { }) } + async fn handle_set_active_debug_line( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + let project_path = ProjectPath::from_proto( + envelope + .payload + .project_path + .context("Invalid Breakpoint call")?, + ); + + this.update(&mut cx, |store, cx| { + store.active_debug_line = Some(( + DebugAdapterClientId(envelope.payload.client_id as usize), + project_path, + envelope.payload.row, + )); + + cx.emit(DapStoreEvent::ActiveDebugLineChanged); + cx.notify(); + }) + } + + async fn handle_remove_active_debug_line( + this: Model, + _: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |store, cx| { + store.active_debug_line.take(); + + cx.emit(DapStoreEvent::ActiveDebugLineChanged); + cx.notify(); + }) + } + pub fn toggle_breakpoint_for_buffer( &mut self, project_path: &ProjectPath, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d6aca6d40e265f..65b469e7c63ebc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -250,6 +250,7 @@ pub enum Event { LanguageNotFound(Model), DebugClientStarted(DebugAdapterClientId), DebugClientStopped(DebugAdapterClientId), + ActiveDebugLineChanged, DebugClientEvent { client_id: DebugAdapterClientId, message: Message, @@ -2427,6 +2428,9 @@ impl Project { DapStoreEvent::BreakpointsChanged => { cx.notify(); } + DapStoreEvent::ActiveDebugLineChanged => { + cx.emit(Event::ActiveDebugLineChanged); + } } } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index aa83917abc9101..b98f2f1af9ef94 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -303,7 +303,9 @@ message Envelope { SyncExtensionsResponse sync_extensions_response = 286; InstallExtension install_extension = 287; - SynchronizeBreakpoints Synchronize_breakpoints = 288; // current max + SynchronizeBreakpoints Synchronize_breakpoints = 288; + SetActiveDebugLine set_active_debug_line = 289; + RemoveActiveDebugLine remove_active_debug_line = 290; // current max } reserved 87 to 88; @@ -2409,6 +2411,17 @@ message SynchronizeBreakpoints { repeated Breakpoint breakpoints = 3; } +message SetActiveDebugLine { + uint64 project_id = 1; + ProjectPath project_path = 2; + uint64 client_id = 3; + uint32 row = 4; +} + +message RemoveActiveDebugLine { + uint64 project_id = 1; +} + // Remote FS message AddWorktree { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 71c6f340575355..a0e17b680633c1 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -372,6 +372,8 @@ messages!( (SyncExtensionsResponse, Background), (InstallExtension, Background), (SynchronizeBreakpoints, Background), + (SetActiveDebugLine, Background), + (RemoveActiveDebugLine, Background), ); request_messages!( @@ -582,6 +584,8 @@ entity_messages!( GetPathMetadata, CancelLanguageServerWork, SynchronizeBreakpoints, + SetActiveDebugLine, + RemoveActiveDebugLine, ); entity_messages!( From 61e8d0c39bd1171f31952da25708f04c7cbc82bb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 1 Dec 2024 10:57:09 +0100 Subject: [PATCH 371/650] Add resolved cwd to lldb initialize options --- crates/dap_adapters/src/lldb.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index b79e6d754b2223..e7587787644276 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -89,6 +89,9 @@ impl DebugAdapter for LldbDebugAdapter { } fn request_args(&self, config: &DebugAdapterConfig) -> Value { - json!({"program": config.program}) + json!({ + "program": config.program, + "cwd": config.cwd, + }) } } From 2b8ae367c74c5c1049d6a0bef9d163de545def53 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 6 Dec 2024 20:31:21 +0100 Subject: [PATCH 372/650] Rework download adapters and allow multiple download formats (#72) * Rework download adapters and allow multiple download formats * Move version path check up, so we don't have to check always 2 paths * Add user installed binary back * Fix dynamic ports don't work for javascript when using start debugging path --- Cargo.lock | 2 + crates/dap/Cargo.toml | 2 + crates/dap/src/adapters.rs | 243 +++++++++++------------- crates/dap/src/client.rs | 7 + crates/dap_adapters/src/custom.rs | 3 +- crates/dap_adapters/src/dap_adapters.rs | 1 - crates/dap_adapters/src/gdb.rs | 2 - crates/dap_adapters/src/go.rs | 36 ++-- crates/dap_adapters/src/javascript.rs | 99 +++++----- crates/dap_adapters/src/lldb.rs | 19 +- crates/dap_adapters/src/php.rs | 100 +++++----- crates/dap_adapters/src/python.rs | 58 +++--- crates/project/src/dap_store.rs | 201 +++++++++++--------- crates/project/src/project.rs | 2 +- crates/project/src/project_settings.rs | 16 +- crates/util/src/fs.rs | 27 ++- 16 files changed, 411 insertions(+), 407 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05858ebce0480a..c165b80b92f3c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3431,6 +3431,8 @@ name = "dap" version = "0.1.0" dependencies = [ "anyhow", + "async-compression", + "async-tar", "async-trait", "collections", "dap-types", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index bfddab846f475f..2097bea26a6ac0 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -10,6 +10,8 @@ workspace = true [dependencies] anyhow.workspace = true +async-compression.workspace = true +async-tar.workspace = true async-trait.workspace = true collections.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b95818130022bfc72bbcd639bdd0c0358c7549fc" } diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 3da1f5e87a88aa..d6fc9696564d2e 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,10 +1,14 @@ use crate::transport::Transport; use ::fs::Fs; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; use async_trait::async_trait; +use futures::io::BufReader; use gpui::SharedString; -use http_client::{github::latest_github_release, HttpClient}; +pub use http_client::{github::latest_github_release, HttpClient}; use node_runtime::NodeRuntime; +use serde::{Deserialize, Serialize}; use serde_json::Value; use smol::{self, fs::File, lock::Mutex, process}; use std::{ @@ -17,6 +21,7 @@ use std::{ }; use sysinfo::{Pid, Process}; use task::DebugAdapterConfig; +use util::ResultExt; #[derive(Clone, Debug, PartialEq, Eq)] pub enum DapStatus { @@ -37,7 +42,7 @@ pub trait DapDelegate { async fn shell_env(&self) -> collections::HashMap; } -#[derive(PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] pub struct DebugAdapterName(pub Arc); impl Deref for DebugAdapterName { @@ -72,13 +77,18 @@ impl From for SharedString { } } +impl<'a> From<&'a str> for DebugAdapterName { + fn from(str: &'a str) -> DebugAdapterName { + DebugAdapterName(str.to_string().into()) + } +} + #[derive(Debug, Clone)] pub struct DebugAdapterBinary { pub command: String, pub arguments: Option>, pub envs: Option>, pub cwd: Option, - pub version: String, } pub struct AdapterVersion { @@ -86,6 +96,12 @@ pub struct AdapterVersion { pub url: String, } +pub enum DownloadedFileType { + Vsix, + GzipTar, + Zip, +} + pub struct GithubRepo { pub repo_name: String, pub repo_owner: String, @@ -94,89 +110,79 @@ pub struct GithubRepo { pub async fn download_adapter_from_github( adapter_name: DebugAdapterName, github_version: AdapterVersion, + file_type: DownloadedFileType, delegate: &dyn DapDelegate, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(&adapter_name); - let version_dir = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name)); + let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name)); let fs = delegate.fs(); - let http_client = delegate - .http_client() - .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; - - if !adapter_path.exists() { - fs.create_dir(&adapter_path.as_path()).await?; + if version_path.exists() { + return Ok(version_path); } - if version_dir.exists() { - return Ok(version_dir); + if !adapter_path.exists() { + fs.create_dir(&adapter_path.as_path()) + .await + .context("Failed creating adapter path")?; } - let asset_name = format!("{}_{}.zip", &adapter_name, github_version.tag_name); - let zip_path = adapter_path.join(&asset_name); - fs.remove_file( - zip_path.as_path(), - fs::RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await?; + log::debug!( + "Downloading adapter {} from {}", + adapter_name, + &github_version.url, + ); + let http_client = delegate + .http_client() + .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; let mut response = http_client .get(&github_version.url, Default::default(), true) .await .context("Error downloading release")?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } - let mut file = File::create(&zip_path).await?; - futures::io::copy(response.body_mut(), &mut file).await?; - - let old_files: HashSet<_> = util::fs::collect_matching(&adapter_path.as_path(), |file_path| { - file_path != zip_path.as_path() - }) - .await - .into_iter() - .filter_map(|file_path| { - file_path - .file_name() - .and_then(|f| f.to_str()) - .map(|f| f.to_string()) - }) - .collect(); - - let _unzip_status = process::Command::new("unzip") - .current_dir(&adapter_path) - .arg(&zip_path) - .output() - .await? - .status; + match file_type { + DownloadedFileType::GzipTar => { + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(&version_path).await?; + } + DownloadedFileType::Zip | DownloadedFileType::Vsix => { + let zip_path = version_path.with_extension("zip"); + + let mut file = File::create(&zip_path).await?; + futures::io::copy(response.body_mut(), &mut file).await?; + + // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence` + process::Command::new("unzip") + .arg(&zip_path) + .arg("-d") + .arg(&version_path) + .output() + .await?; + + util::fs::remove_matching(&adapter_path, |entry| { + entry + .file_name() + .is_some_and(|file| file.to_string_lossy().ends_with(".zip")) + }) + .await; + } + } - let file_name = util::fs::find_file_name_in_dir(&adapter_path.as_path(), |file_name| { - !file_name.ends_with(".zip") && !old_files.contains(file_name) + // remove older versions + util::fs::remove_matching(&adapter_path, |entry| { + entry.to_string_lossy() != version_path.to_string_lossy() }) - .await - .ok_or_else(|| anyhow!("Unzipped directory not found")); - - let file_name = file_name?; - let downloaded_path = adapter_path - .join(format!("{}_{}", adapter_name, github_version.tag_name)) - .to_owned(); - - fs.rename( - file_name.as_path(), - downloaded_path.as_path(), - Default::default(), - ) - .await?; + .await; - util::fs::remove_matching(&adapter_path, |entry| entry != version_dir).await; - - // if !unzip_status.success() { - // dbg!(unzip_status); - // Err(anyhow!("failed to unzip downloaded dap archive"))?; - // } - - Ok(downloaded_path) + Ok(version_path) } pub async fn fetch_latest_adapter_version_from_github( @@ -186,8 +192,14 @@ pub async fn fetch_latest_adapter_version_from_github( let http_client = delegate .http_client() .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; - let repo_name_with_owner = format!("{}/{}", github_repo.repo_owner, github_repo.repo_name); - let release = latest_github_release(&repo_name_with_owner, false, false, http_client).await?; + + let release = latest_github_release( + &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name), + false, + false, + http_client, + ) + .await?; Ok(AdapterVersion { tag_name: release.tag_name, @@ -203,39 +215,8 @@ pub trait DebugAdapter: 'static + Send + Sync { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - adapter_path: Option, + user_installed_path: Option, ) -> Result { - if let Some(adapter_path) = adapter_path { - if adapter_path.exists() { - log::info!( - "Using adapter path from settings\n debug adapter name: {}\n adapter_path: {:?}", - self.name(), - &adapter_path, - ); - - let binary = self - .get_installed_binary(delegate, &config, Some(adapter_path)) - .await; - - if binary.is_ok() { - return binary; - } else { - log::info!( - "Failed to get debug adapter path from user's setting.\n adapter_name: {}", - self.name() - ); - } - } else { - log::warn!( - r#"User downloaded adapter path does not exist - Debug Adapter: {}, - User Adapter Path: {:?}"#, - self.name(), - &adapter_path - ) - } - } - if delegate .updated_adapters() .lock() @@ -244,49 +225,39 @@ pub trait DebugAdapter: 'static + Send + Sync { { log::info!("Using cached debug adapter binary {}", self.name()); - return self.get_installed_binary(delegate, &config, None).await; + if let Some(binary) = self + .get_installed_binary(delegate, &config, user_installed_path.clone()) + .await + .log_err() + { + return Ok(binary); + } + + log::info!( + "Cached binary {} is corrupt falling back to install", + self.name() + ); } log::info!("Getting latest version of debug adapter {}", self.name()); delegate.update_status(self.name(), DapStatus::CheckingForUpdate); - let version = self.fetch_latest_adapter_version(delegate).await.ok(); - - let mut binary = self.get_installed_binary(delegate, &config, None).await; - - if let Some(version) = version { - if binary - .as_ref() - .is_ok_and(|binary| binary.version == version.tag_name) - { - delegate - .updated_adapters() - .lock_arc() - .await - .insert(self.name()); - - return binary; - } - + if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() { + log::info!( + "Installiing latest version of debug adapter {}", + self.name() + ); delegate.update_status(self.name(), DapStatus::Downloading); self.install_binary(version, delegate).await?; - binary = self.get_installed_binary(delegate, &config, None).await; - } else { - log::error!( - "Failed getting latest version of debug adapter {}", - self.name() - ); + delegate + .updated_adapters() + .lock_arc() + .await + .insert(self.name()); } - let binary = binary?; - - delegate - .updated_adapters() - .lock_arc() + self.get_installed_binary(delegate, &config, user_installed_path) .await - .insert(self.name()); - - Ok(binary) } fn transport(&self) -> Box; diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 5bcc371f82b6e5..c06107e8dcf2af 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -38,6 +38,7 @@ pub struct DebugAdapterClientId(pub usize); pub struct DebugAdapterClient { id: DebugAdapterClientId, sequence_count: AtomicU64, + binary: DebugAdapterBinary, executor: BackgroundExecutor, adapter: Arc>, transport_delegate: TransportDelegate, @@ -49,12 +50,14 @@ impl DebugAdapterClient { id: DebugAdapterClientId, config: DebugAdapterConfig, adapter: Arc>, + binary: DebugAdapterBinary, cx: &AsyncAppContext, ) -> Self { let transport_delegate = TransportDelegate::new(adapter.transport()); Self { id, + binary, adapter, transport_delegate, sequence_count: AtomicU64::new(1), @@ -196,6 +199,10 @@ impl DebugAdapterClient { &self.adapter } + pub fn binary(&self) -> &DebugAdapterBinary { + &self.binary + } + pub fn adapter_id(&self) -> String { self.adapter.name().to_string() } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 3a58d02469a3be..30a21dd7d404d2 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,4 +1,4 @@ -use std::ffi::OsString; +use std::{ffi::OsString, path::PathBuf}; use dap::transport::{StdioTransport, TcpTransport, Transport}; use serde_json::Value; @@ -54,7 +54,6 @@ impl DebugAdapter for CustomDebugAdapter { .map(|args| args.iter().map(OsString::from).collect()), cwd: config.cwd.clone(), envs: self.custom_args.envs.clone(), - version: "Custom Debug Adapter".to_string(), }) } diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 357eb6075be6d4..4fddcd31f2520c 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -22,7 +22,6 @@ use lldb::LldbDebugAdapter; use php::PhpDebugAdapter; use python::PythonDebugAdapter; use serde_json::{json, Value}; -use std::path::PathBuf; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; pub async fn build_adapter(kind: &DebugAdapterKind) -> Result> { diff --git a/crates/dap_adapters/src/gdb.rs b/crates/dap_adapters/src/gdb.rs index d87c1b0d927f3f..4f7ffcf7f621a8 100644 --- a/crates/dap_adapters/src/gdb.rs +++ b/crates/dap_adapters/src/gdb.rs @@ -31,7 +31,6 @@ impl DebugAdapter for GdbDebugAdapter { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - user_installed_path: Option, ) -> Result { let user_setting_path = user_installed_path .filter(|p| p.exists()) @@ -74,7 +73,6 @@ impl DebugAdapter for GdbDebugAdapter { &self, _: &dyn DapDelegate, _: &DebugAdapterConfig, - _: Option, ) -> Result { unimplemented!("GDB cannot be installed by Zed (yet)") } diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index ae0973eafe17b5..587526eec9f056 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -36,9 +36,9 @@ impl DebugAdapter for GoDebugAdapter { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - adapter_path: Option, + user_installed_path: Option, ) -> Result { - self.get_installed_binary(delegate, config, adapter_path) + self.get_installed_binary(delegate, config, user_installed_path) .await } @@ -46,12 +46,6 @@ impl DebugAdapter for GoDebugAdapter { &self, _delegate: &dyn DapDelegate, ) -> Result { - // let github_repo = GithubRepo { - // repo_name: Self::ADAPTER_NAME.into(), - // repo_owner: "go-delve".into(), - // }; - - // adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await unimplemented!("This adapter is used from path for now"); } @@ -60,7 +54,13 @@ impl DebugAdapter for GoDebugAdapter { version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()> { - adapters::download_adapter_from_github(self.name(), version, delegate).await?; + adapters::download_adapter_from_github( + self.name(), + version, + adapters::DownloadedFileType::Zip, + delegate, + ) + .await?; Ok(()) } @@ -68,26 +68,30 @@ impl DebugAdapter for GoDebugAdapter { &self, delegate: &dyn DapDelegate, config: &DebugAdapterConfig, - _user_installed_path: Option, + _: Option, ) -> Result { let delve_path = delegate .which(OsStr::new("dlv")) .and_then(|p| p.to_str().map(|p| p.to_string())) .ok_or(anyhow!("Dlv not found in path"))?; - let ip_address = format!("{}:{}", self.host, self.port); - let version = "N/A".into(); - Ok(DebugAdapterBinary { command: delve_path, - arguments: Some(vec!["dap".into(), "--listen".into(), ip_address.into()]), + arguments: Some(vec![ + "dap".into(), + "--listen".into(), + format!("{}:{}", self.host, self.port).into(), + ]), cwd: config.cwd.clone(), envs: None, - version, }) } fn request_args(&self, config: &DebugAdapterConfig) -> Value { - json!({"program": config.program, "subProcess": true}) + json!({ + "program": config.program, + "cwd": config.cwd, + "subProcess": true, + }) } } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 6e99e1dd4d50ad..805913fc510244 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,9 +1,9 @@ +use adapters::latest_github_release; use dap::transport::{TcpTransport, Transport}; use regex::Regex; -use std::{collections::HashMap, net::Ipv4Addr}; +use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf}; use sysinfo::{Pid, Process}; use task::DebugRequestType; -use util::maybe; use crate::*; @@ -15,7 +15,7 @@ pub(crate) struct JsDebugAdapter { impl JsDebugAdapter { const ADAPTER_NAME: &'static str = "vscode-js-debug"; - const ADAPTER_PATH: &'static str = "src/dapDebugServer.js"; + const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js"; pub(crate) async fn new(host: TCPHost) -> Result { Ok(JsDebugAdapter { @@ -40,12 +40,29 @@ impl DebugAdapter for JsDebugAdapter { &self, delegate: &dyn DapDelegate, ) -> Result { - let github_repo = GithubRepo { - repo_name: Self::ADAPTER_NAME.into(), - repo_owner: "microsoft".to_string(), - }; + let http_client = delegate + .http_client() + .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; + let release = latest_github_release( + &format!("{}/{}", "microsoft", Self::ADAPTER_NAME), + true, + false, + http_client, + ) + .await?; + + let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name); - adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + Ok(AdapterVersion { + tag_name: release.tag_name, + url: release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))? + .browser_download_url + .clone(), + }) } async fn get_installed_binary( @@ -54,37 +71,24 @@ impl DebugAdapter for JsDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let node_runtime = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))?; + let adapter_path = if let Some(user_installed_path) = user_installed_path { + user_installed_path + } else { + let adapter_path = paths::debug_adapters_dir().join(self.name()); - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let file_name_prefix = format!("{}_", self.name()); - - let adapter_info: Result<_> = maybe!(async { - let adapter_path = - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; - - let version = adapter_path - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("PHP debug adapter has invalid file name"))? - .to_string(); - - Ok((adapter_path, version)) - }) - .await; + let file_name_prefix = format!("{}_", self.name()); - let (adapter_path, version) = match user_installed_path { - Some(path) => (path, "N/A".into()), - None => adapter_info?, + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))? }; + let node_runtime = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))?; + Ok(DebugAdapterBinary { command: node_runtime .binary_path() @@ -94,10 +98,10 @@ impl DebugAdapter for JsDebugAdapter { arguments: Some(vec![ adapter_path.join(Self::ADAPTER_PATH).into(), self.port.to_string().into(), + self.host.to_string().into(), ]), cwd: config.cwd.clone(), envs: None, - version, }) } @@ -106,22 +110,13 @@ impl DebugAdapter for JsDebugAdapter { version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()> { - let adapter_path = - adapters::download_adapter_from_github(self.name(), version, delegate).await?; - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "install", &[]) - .await - .ok(); - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["compile"]) - .await - .ok(); + adapters::download_adapter_from_github( + self.name(), + version, + adapters::DownloadedFileType::GzipTar, + delegate, + ) + .await?; return Ok(()); } diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index e7587787644276..8a6bb9ad8b1d10 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -1,4 +1,4 @@ -use std::ffi::OsStr; +use std::{ffi::OsStr, path::PathBuf}; use anyhow::Result; use async_trait::async_trait; @@ -33,10 +33,6 @@ impl DebugAdapter for LldbDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let user_setting_path = user_installed_path - .filter(|p| p.exists()) - .and_then(|p| p.to_str().map(|s| s.to_string())); - let lldb_dap_path = if cfg!(target_os = "macos") { std::process::Command::new("xcrun") .args(&["-f", "lldb-dap"]) @@ -44,26 +40,21 @@ impl DebugAdapter for LldbDebugAdapter { .ok() .and_then(|output| String::from_utf8(output.stdout).ok()) .map(|path| path.trim().to_string()) - .ok_or(anyhow!("Failed to find lldb-dap in user's path")) + .ok_or(anyhow!("Failed to find lldb-dap in user's path"))? + } else if let Some(user_installed_path) = user_installed_path { + user_installed_path.to_string_lossy().into() } else { delegate .which(OsStr::new("lldb-dap")) .and_then(|p| p.to_str().map(|s| s.to_string())) - .ok_or(anyhow!("Could not find lldb-dap in path")) + .ok_or(anyhow!("Could not find lldb-dap in path"))? }; - if lldb_dap_path.is_err() && user_setting_path.is_none() { - bail!("Could not find lldb-dap path or it's not installed"); - } - - let lldb_dap_path = user_setting_path.unwrap_or(lldb_dap_path?); - Ok(DebugAdapterBinary { command: lldb_dap_path, arguments: None, envs: None, cwd: config.cwd.clone(), - version: "1".into(), }) } diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index eaea7be7141f10..db031900150752 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,6 +1,6 @@ +use adapters::latest_github_release; use dap::transport::{TcpTransport, Transport}; -use std::net::Ipv4Addr; -use util::maybe; +use std::{net::Ipv4Addr, path::PathBuf}; use crate::*; @@ -12,7 +12,7 @@ pub(crate) struct PhpDebugAdapter { impl PhpDebugAdapter { const ADAPTER_NAME: &'static str = "vscode-php-debug"; - const ADAPTER_PATH: &'static str = "out/phpDebug.js"; + const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js"; pub(crate) async fn new(host: TCPHost) -> Result { Ok(PhpDebugAdapter { @@ -37,12 +37,29 @@ impl DebugAdapter for PhpDebugAdapter { &self, delegate: &dyn DapDelegate, ) -> Result { - let github_repo = GithubRepo { - repo_name: Self::ADAPTER_NAME.into(), - repo_owner: "xdebug".into(), - }; - - adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await + let http_client = delegate + .http_client() + .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; + let release = latest_github_release( + &format!("{}/{}", "xdebug", Self::ADAPTER_NAME), + true, + false, + http_client, + ) + .await?; + + let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", "")); + + Ok(AdapterVersion { + tag_name: release.tag_name, + url: release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))? + .browser_download_url + .clone(), + }) } async fn get_installed_binary( @@ -51,37 +68,24 @@ impl DebugAdapter for PhpDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let node_runtime = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))?; + let adapter_path = if let Some(user_installed_path) = user_installed_path { + user_installed_path + } else { + let adapter_path = paths::debug_adapters_dir().join(self.name()); - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let file_name_prefix = format!("{}_", self.name()); - - let adapter_info: Result<_> = maybe!(async { - let adapter_path = - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .ok_or_else(|| anyhow!("Couldn't find Php dap directory"))?; - - let version = adapter_path - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("PHP debug adapter has invalid file name"))? - .to_string(); - - Ok((adapter_path, version)) - }) - .await; + let file_name_prefix = format!("{}_", self.name()); - let (adapter_path, version) = match user_installed_path { - Some(path) => (path, "N/A".into()), - None => adapter_info?, + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))? }; + let node_runtime = delegate + .node_runtime() + .ok_or(anyhow!("Couldn't get npm runtime"))?; + Ok(DebugAdapterBinary { command: node_runtime .binary_path() @@ -94,7 +98,6 @@ impl DebugAdapter for PhpDebugAdapter { ]), cwd: config.cwd.clone(), envs: None, - version, }) } @@ -103,22 +106,13 @@ impl DebugAdapter for PhpDebugAdapter { version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()> { - let adapter_path = - adapters::download_adapter_from_github(self.name(), version, delegate).await?; - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "install", &[]) - .await - .is_ok(); - - let _ = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))? - .run_npm_subcommand(&adapter_path, "run", &["build"]) - .await - .is_ok(); + adapters::download_adapter_from_github( + self.name(), + version, + adapters::DownloadedFileType::Vsix, + delegate, + ) + .await?; Ok(()) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 855fa3551b6d30..38e1be293aca66 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,6 +1,5 @@ use dap::transport::{TcpTransport, Transport}; use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; -use util::maybe; use crate::*; @@ -50,7 +49,25 @@ impl DebugAdapter for PythonDebugAdapter { version: AdapterVersion, delegate: &dyn DapDelegate, ) -> Result<()> { - adapters::download_adapter_from_github(self.name(), version, delegate).await?; + let version_path = adapters::download_adapter_from_github( + self.name(), + version, + adapters::DownloadedFileType::Zip, + delegate, + ) + .await?; + + // only needed when you install the latest version for the first time + if let Some(debugpy_dir) = + util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| { + file_name.starts_with("microsoft-debugpy-") + }) + .await + { + util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path()) + .await?; + } + Ok(()) } @@ -60,31 +77,17 @@ impl DebugAdapter for PythonDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let adapter_path = paths::debug_adapters_dir().join(self.name()); - let file_name_prefix = format!("{}_", self.name()); - - let adapter_info: Result<_> = maybe!(async { - let debugpy_dir = - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .ok_or_else(|| anyhow!("Debugpy directory not found"))?; - - let version = debugpy_dir - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| file_name.strip_prefix(&file_name_prefix)) - .ok_or_else(|| anyhow!("Python debug adapter has invalid file name"))? - .to_string(); - - Ok((debugpy_dir, version)) - }) - .await; - - let (debugpy_dir, version) = match user_installed_path { - Some(path) => (path, "N/A".into()), - None => adapter_info?, + let debugpy_dir = if let Some(user_installed_path) = user_installed_path { + user_installed_path + } else { + let adapter_path = paths::debug_adapters_dir().join(self.name()); + let file_name_prefix = format!("{}_", self.name()); + + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .ok_or_else(|| anyhow!("Debugpy directory not found"))? }; let python_cmds = [ @@ -114,7 +117,6 @@ impl DebugAdapter for PythonDebugAdapter { ]), cwd: config.cwd.clone(), envs: None, - version, }) } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 1825682a9ba983..007ae83b99b626 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,9 +1,10 @@ -use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectPath}; +use crate::project_settings::ProjectSettings; +use crate::{ProjectEnvironment, ProjectPath}; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; -use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; +use dap::adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}; use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, messages::{Message, Response}, @@ -37,7 +38,7 @@ use lsp::LanguageServerName; use node_runtime::NodeRuntime; use rpc::{proto, AnyProtoClient, TypedEnvelope}; use serde_json::Value; -use settings::{Settings, WorktreeId}; +use settings::{Settings as _, WorktreeId}; use smol::lock::Mutex; use std::{ collections::{BTreeMap, HashSet}, @@ -51,7 +52,7 @@ use std::{ }; use task::{AttachConfig, DebugAdapterConfig, DebugRequestType}; use text::Point; -use util::{maybe, merge_json_value_into, ResultExt as _}; +use util::{merge_json_value_into, ResultExt as _}; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), @@ -379,102 +380,55 @@ impl DapStore { } } - pub fn start_client(&mut self, config: DebugAdapterConfig, cx: &mut ModelContext) { - let Some(local_store) = self.as_local_mut() else { + pub fn start_client( + &mut self, + adapter: Arc>, + binary: DebugAdapterBinary, + config: DebugAdapterConfig, + cx: &mut ModelContext, + ) { + let Some(_) = self.as_local() else { return; }; - let mut adapter_delegate = local_store.delegate.clone(); - let worktree_abs_path = config.cwd.as_ref().map(|p| Arc::from(p.as_path())); - adapter_delegate.refresh_shell_env_task(local_store.environment.update(cx, |env, cx| { - env.get_environment(None, worktree_abs_path, cx) - })); - let adapter_delegate = Arc::new(adapter_delegate); + if !adapter.supports_attach() && matches!(config.request, DebugRequestType::Attach(_)) { + cx.emit(DapStoreEvent::Notification( + "Debug adapter does not support `attach` request".into(), + )); + + return; + } let client_id = self.next_client_id(); let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); - let client = maybe!(async { - let adapter = Arc::new( - build_adapter(&config.kind) - .await - .context("Creating debug adapter")?, - ); - - if !adapter.supports_attach() - && matches!(config.request, DebugRequestType::Attach(_)) - { - return Err(anyhow!("Debug adapter does not support `attach` request")); - } - - let path = cx.update(|cx| { - let name = LanguageServerName::from(adapter.name().as_ref()); - - ProjectSettings::get_global(cx) - .dap - .get(&name) - .and_then(|s| s.path.as_ref().map(PathBuf::from)) - })?; - - let binary = match adapter - .get_binary(adapter_delegate.as_ref(), &config, path) - .await - { - Err(error) => { - adapter_delegate.update_status( - adapter.name(), - DapStatus::Failed { - error: error.to_string(), - }, - ); - - return Err(error); - } - Ok(mut binary) => { - adapter_delegate.update_status(adapter.name(), DapStatus::None); - - let shell_env = adapter_delegate.shell_env().await; - let mut envs = binary.envs.unwrap_or_default(); - envs.extend(shell_env); - binary.envs = Some(envs); - - binary - } - }; - - let mut client = DebugAdapterClient::new(client_id, config, adapter, &cx); - - client - .start( - &binary, - move |message, cx| { - dap_store - .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) - }) - .log_err(); - }, - &mut cx, - ) - .await?; - - log::info!("Client has started"); - anyhow::Ok(client) - }) - .await; + let mut client = + DebugAdapterClient::new(client_id, config, adapter, binary.clone(), &cx); + + let result = client + .start( + &binary, + move |message, cx| { + dap_store + .update(cx, |_, cx| { + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + }) + .log_err(); + }, + &mut cx, + ) + .await; - let client = match client { - Err(error) => { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(error.to_string())); - }) - .log_err()?; + if let Err(error) = result { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + .log_err()?; + return None; + } - return None; - } - Ok(client) => Arc::new(client), - }; + let client = Arc::new(client); this.update(&mut cx, |store, cx| { let handle = store @@ -499,6 +453,67 @@ impl DapStore { ); } + pub fn start_client_from_debug_config( + &mut self, + config: DebugAdapterConfig, + cx: &mut ModelContext, + ) { + let Some(local_store) = self.as_local_mut() else { + return; + }; + + let mut adapter_delegate = local_store.delegate.clone(); + let worktree_abs_path = config.cwd.as_ref().map(|p| Arc::from(p.as_path())); + adapter_delegate.refresh_shell_env_task(local_store.environment.update(cx, |env, cx| { + env.get_environment(None, worktree_abs_path, cx) + })); + let adapter_delegate = Arc::new(adapter_delegate); + + cx.spawn(|this, mut cx| async move { + let adapter = Arc::new(build_adapter(&config.kind).await?); + + let binary = cx.update(|cx| { + let name = DebugAdapterName::from(adapter.name().as_ref()); + + ProjectSettings::get_global(cx) + .dap + .get(&name) + .and_then(|s| s.binary.as_ref().map(PathBuf::from)) + })?; + + let (adapter, binary) = match adapter + .get_binary(adapter_delegate.as_ref(), &config, binary) + .await + { + Err(error) => { + adapter_delegate.update_status( + adapter.name(), + DapStatus::Failed { + error: error.to_string(), + }, + ); + + return Err(error); + } + Ok(mut binary) => { + adapter_delegate.update_status(adapter.name(), DapStatus::None); + + let shell_env = adapter_delegate.shell_env().await; + let mut envs = binary.envs.unwrap_or_default(); + envs.extend(shell_env); + binary.envs = Some(envs); + + (adapter, binary) + } + }; + + this.update(&mut cx, |store, cx| { + store.start_client(adapter, binary, config, cx); + }) + }) + .detach_and_log_err(cx); + } + pub fn initialize( &mut self, client_id: &DebugAdapterClientId, @@ -737,6 +752,8 @@ impl DapStore { let config = client.config(); store.start_client( + client.adapter().clone(), + client.binary().clone(), DebugAdapterConfig { kind: config.kind.clone(), request: match args.request { @@ -771,9 +788,7 @@ impl DapStore { command: StartDebugging::COMMAND.to_string(), body: Some(serde_json::to_value(ErrorResponse { error: None })?), })) - .await?; - - Ok(()) + .await } }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 65b469e7c63ebc..76c54d28ae25c9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1268,7 +1268,7 @@ impl Project { ) { if let Some(adapter_config) = debug_task.debug_adapter_config() { self.dap_store.update(cx, |store, cx| { - store.start_client(adapter_config, cx); + store.start_client_from_debug_config(adapter_config, cx); }); } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 8cec28c06ea03a..b712d1104249df 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -1,5 +1,6 @@ use anyhow::Context; use collections::HashMap; +use dap::adapters::DebugAdapterName; use fs::Fs; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext}; use lsp::LanguageServerName; @@ -41,8 +42,9 @@ pub struct ProjectSettings { #[serde(default)] pub lsp: HashMap, + /// Configuration for Debugger-related features #[serde(default)] - pub dap: HashMap, + pub dap: HashMap, /// Configuration for Git-related features #[serde(default)] @@ -61,6 +63,12 @@ pub struct ProjectSettings { pub session: SessionSettings, } +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DapSettings { + pub binary: Option, +} + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct NodeBinarySettings { /// The path to the node binary @@ -184,12 +192,6 @@ pub struct LspSettings { pub settings: Option, } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct DapSettings { - pub path: Option, -} - #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct SessionSettings { /// Whether or not to restore unsaved buffers on restart. diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs index 5fe9b055ab3828..d6baf9364b7016 100644 --- a/crates/util/src/fs.rs +++ b/crates/util/src/fs.rs @@ -1,4 +1,5 @@ use crate::ResultExt; +use anyhow::{bail, Result}; use async_fs as fs; use futures_lite::StreamExt; use std::path::{Path, PathBuf}; @@ -56,9 +57,9 @@ where if let Some(file_name) = entry_path .file_name() - .and_then(|file_name| file_name.to_str()) + .map(|file_name| file_name.to_string_lossy()) { - if predicate(file_name) { + if predicate(&file_name) { return Some(entry_path); } } @@ -68,3 +69,25 @@ where None } + +pub async fn move_folder_files_to_folder>( + source_path: P, + target_path: P, +) -> Result<()> { + if !target_path.as_ref().is_dir() { + bail!("Folder not found or is not a directory"); + } + + let mut entries = fs::read_dir(source_path.as_ref()).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + let old_path = entry.path(); + let new_path = target_path.as_ref().join(entry.file_name()); + + fs::rename(&old_path, &new_path).await?; + } + + fs::remove_dir(source_path).await?; + + Ok(()) +} From 1a0ecf0c166377fb12789187828c25164073cfbf Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 9 Dec 2024 20:16:06 +0100 Subject: [PATCH 373/650] Add FakeAdapter and FakeTransport for testing purposes (#73) * Move process to transport struct itself * Add test to make sure we can send request and receive a response * Align other handle methods * Fix issues inside cargo.toml require test features inside the correct *-dependencies * Remove comments that are not needed * Add as_fake instead of downcasting * Clean up * Override get_binary method so we don't fail on install * Fix false positive clippy error This error was a match variant not being covered when the variant wasn't possible dued to a feature flag. I'm pretty sure this is a bug in clippy/rust-analyzer and will open an issue on their repos * Remove not needed clone * Panic when we receive an event/reverse request inside the test * reuse the type of the closure * Add a way to fake receiving events * Oops remove fake event from different test * Clipppyyyy --------- Co-authored-by: Anthony Eid --- Cargo.lock | 3 + crates/dap/Cargo.toml | 17 ++ crates/dap/src/adapters.rs | 72 +++++- crates/dap/src/client.rs | 187 +++++++++++++- crates/dap/src/lib.rs | 3 + crates/dap/src/transport.rs | 321 +++++++++++++++++++----- crates/dap_adapters/Cargo.toml | 11 + crates/dap_adapters/src/custom.rs | 12 +- crates/dap_adapters/src/dap_adapters.rs | 25 +- crates/dap_adapters/src/gdb.rs | 4 +- crates/dap_adapters/src/go.rs | 6 +- crates/dap_adapters/src/javascript.rs | 6 +- crates/dap_adapters/src/lldb.rs | 6 +- crates/dap_adapters/src/php.rs | 6 +- crates/dap_adapters/src/python.rs | 6 +- crates/project/src/dap_store.rs | 10 +- crates/task/Cargo.toml | 6 + crates/task/src/debug_format.rs | 5 + 18 files changed, 591 insertions(+), 115 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c165b80b92f3c0..5a7ed0f60c9b84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3432,10 +3432,13 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", + "async-pipe", "async-tar", "async-trait", "collections", + "ctor", "dap-types", + "env_logger 0.11.5", "fs", "futures 0.3.31", "gpui", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 2097bea26a6ac0..dd101a48647017 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -8,9 +8,18 @@ license = "GPL-3.0-or-later" [lints] workspace = true +[features] +test-support = [ + "gpui/test-support", + "util/test-support", + "task/test-support", + "async-pipe", +] + [dependencies] anyhow.workspace = true async-compression.workspace = true +async-pipe = { workspace = true, optional = true } async-tar.workspace = true async-trait.workspace = true collections.workspace = true @@ -32,3 +41,11 @@ smol.workspace = true sysinfo.workspace = true task.workspace = true util.workspace = true + +[dev-dependencies] +async-pipe.workspace = true +ctor.workspace = true +env_logger.workspace = true +gpui = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } +task = { workspace = true, features = ["test-support"] } diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index d6fc9696564d2e..c51b7f22fc3d8c 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,3 +1,5 @@ +#[cfg(any(test, feature = "test-support"))] +use crate::transport::FakeTransport; use crate::transport::Transport; use ::fs::Fs; use anyhow::{anyhow, Context as _, Result}; @@ -260,7 +262,7 @@ pub trait DebugAdapter: 'static + Send + Sync { .await } - fn transport(&self) -> Box; + fn transport(&self) -> Arc; async fn fetch_latest_adapter_version( &self, @@ -300,3 +302,71 @@ pub trait DebugAdapter: 'static + Send + Sync { None } } + +#[cfg(any(test, feature = "test-support"))] +pub struct FakeAdapter {} + +#[cfg(any(test, feature = "test-support"))] +impl FakeAdapter { + const ADAPTER_NAME: &'static str = "fake-adapter"; + + pub fn new() -> Self { + Self {} + } +} + +#[cfg(any(test, feature = "test-support"))] +#[async_trait(?Send)] +impl DebugAdapter for FakeAdapter { + fn name(&self) -> DebugAdapterName { + DebugAdapterName(Self::ADAPTER_NAME.into()) + } + + fn transport(&self) -> Arc { + Arc::new(FakeTransport::new()) + } + + async fn get_binary( + &self, + _delegate: &dyn DapDelegate, + _config: &DebugAdapterConfig, + _user_installed_path: Option, + ) -> Result { + Ok(DebugAdapterBinary { + command: "command".into(), + arguments: None, + envs: None, + cwd: None, + }) + } + + async fn fetch_latest_adapter_version( + &self, + _delegate: &dyn DapDelegate, + ) -> Result { + unimplemented!("fetch latest adapter version"); + } + + async fn install_binary( + &self, + _version: AdapterVersion, + _delegate: &dyn DapDelegate, + ) -> Result<()> { + unimplemented!("install binary"); + } + + async fn get_installed_binary( + &self, + _delegate: &dyn DapDelegate, + _config: &DebugAdapterConfig, + _user_installed_path: Option, + ) -> Result { + unimplemented!("get installed binary"); + } + + fn request_args(&self, _config: &DebugAdapterConfig) -> Value { + use serde_json::json; + + json!({}) + } +} diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index c06107e8dcf2af..45f2ec1ddd4a84 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -40,7 +40,7 @@ pub struct DebugAdapterClient { sequence_count: AtomicU64, binary: DebugAdapterBinary, executor: BackgroundExecutor, - adapter: Arc>, + adapter: Arc, transport_delegate: TransportDelegate, config: Arc>, } @@ -49,7 +49,7 @@ impl DebugAdapterClient { pub fn new( id: DebugAdapterClientId, config: DebugAdapterConfig, - adapter: Arc>, + adapter: Arc, binary: DebugAdapterBinary, cx: &AsyncAppContext, ) -> Self { @@ -66,16 +66,11 @@ impl DebugAdapterClient { } } - pub async fn start( - &mut self, - binary: &DebugAdapterBinary, - message_handler: F, - cx: &mut AsyncAppContext, - ) -> Result<()> + pub async fn start(&mut self, message_handler: F, cx: &mut AsyncAppContext) -> Result<()> where F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, { - let (server_rx, server_tx) = self.transport_delegate.start(binary, cx).await?; + let (server_rx, server_tx) = self.transport_delegate.start(&self.binary, cx).await?; log::info!("Successfully connected to debug adapter"); // start handling events/reverse requests @@ -195,7 +190,7 @@ impl DebugAdapterClient { self.config.lock().unwrap().clone() } - pub fn adapter(&self) -> &Arc> { + pub fn adapter(&self) -> &Arc { &self.adapter } @@ -234,4 +229,176 @@ impl DebugAdapterClient { { self.transport_delegate.add_log_handler(f, kind); } + + #[cfg(any(test, feature = "test-support"))] + pub async fn on_request(&self, handler: F) + where + F: 'static + Send + FnMut(u64, R::Arguments) -> Result, + { + let transport = self.transport_delegate.transport(); + + transport.as_fake().on_request::(handler).await; + } + + #[cfg(any(test, feature = "test-support"))] + pub async fn fake_event(&self, event: dap_types::messages::Events) { + self.send_message(Message::Event(Box::new(event))) + .await + .unwrap(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{adapters::FakeAdapter, client::DebugAdapterClient}; + use dap_types::{ + messages::Events, requests::Initialize, Capabilities, InitializeRequestArguments, + InitializeRequestArgumentsPathFormat, + }; + use gpui::TestAppContext; + use std::sync::atomic::{AtomicBool, Ordering}; + use task::DebugAdapterConfig; + + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + + #[gpui::test] + pub async fn test_initialize_client(cx: &mut TestAppContext) { + let adapter = Arc::new(FakeAdapter::new()); + + let mut client = DebugAdapterClient::new( + crate::client::DebugAdapterClientId(1), + DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + adapter, + DebugAdapterBinary { + command: "command".into(), + arguments: Default::default(), + envs: Default::default(), + cwd: None, + }, + &mut cx.to_async(), + ); + + client + .on_request::(move |_, _| { + Ok(dap_types::Capabilities { + supports_configuration_done_request: Some(true), + ..Default::default() + }) + }) + .await; + + client + .start( + |_, _| panic!("Did not expect to hit this code path"), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.run_until_parked(); + + let response = client + .request::(InitializeRequestArguments { + client_id: Some("zed".to_owned()), + client_name: Some("Zed".to_owned()), + adapter_id: "fake-adapter".to_owned(), + locale: Some("en-US".to_owned()), + path_format: Some(InitializeRequestArgumentsPathFormat::Path), + supports_variable_type: Some(true), + supports_variable_paging: Some(false), + supports_run_in_terminal_request: Some(true), + supports_memory_references: Some(true), + supports_progress_reporting: Some(false), + supports_invalidated_event: Some(false), + lines_start_at1: Some(true), + columns_start_at1: Some(true), + supports_memory_event: Some(false), + supports_args_can_be_interpreted_by_shell: Some(false), + supports_start_debugging_request: Some(true), + }) + .await + .unwrap(); + + cx.run_until_parked(); + + assert_eq!( + dap_types::Capabilities { + supports_configuration_done_request: Some(true), + ..Default::default() + }, + response + ); + + client.shutdown().await.unwrap(); + } + + #[gpui::test] + pub async fn test_calls_event_handler(cx: &mut TestAppContext) { + let adapter = Arc::new(FakeAdapter::new()); + let was_called = Arc::new(AtomicBool::new(false)); + let was_called_clone = was_called.clone(); + + let mut client = DebugAdapterClient::new( + crate::client::DebugAdapterClientId(1), + DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + adapter, + DebugAdapterBinary { + command: "command".into(), + arguments: Default::default(), + envs: Default::default(), + cwd: None, + }, + &mut cx.to_async(), + ); + + client + .start( + move |event, _| { + was_called_clone.store(true, Ordering::SeqCst); + + assert_eq!( + Message::Event(Box::new(Events::Initialized( + Some(Capabilities::default()) + ))), + event + ); + }, + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.run_until_parked(); + + client + .fake_event(Events::Initialized(Some(Capabilities::default()))) + .await; + + cx.run_until_parked(); + + assert!( + was_called.load(std::sync::atomic::Ordering::SeqCst), + "Event handler was not called" + ); + + client.shutdown().await.unwrap(); + } } diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index df62861da7bf1b..b7a48be87b4d4f 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -3,3 +3,6 @@ pub mod client; pub mod transport; pub use dap_types::*; pub mod debugger_settings; + +#[cfg(any(test, feature = "test-support"))] +pub use adapters::FakeAdapter; diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 04c3eaf391b11c..3051f3f839fcd8 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -4,9 +4,7 @@ use dap_types::{ messages::{Message, Response}, ErrorResponse, }; -use futures::{ - channel::oneshot, select, AsyncBufRead, AsyncReadExt as _, AsyncWrite, FutureExt as _, -}; +use futures::{channel::oneshot, select, AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _}; use gpui::AsyncAppContext; use settings::Settings as _; use smallvec::SmallVec; @@ -15,9 +13,10 @@ use smol::{ io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, lock::Mutex, net::{TcpListener, TcpStream}, - process::{self, Child, ChildStderr, ChildStdout}, + process::{self, Child}, }; use std::{ + any::Any, collections::HashMap, net::{Ipv4Addr, SocketAddrV4}, process::Stdio, @@ -43,22 +42,25 @@ pub enum IoKind { StdErr, } -pub struct TransportParams { - input: Box, - output: Box, - process: Child, +pub struct TransportPipe { + input: Box, + output: Box, + stdout: Option>, + stderr: Option>, } -impl TransportParams { +impl TransportPipe { pub fn new( - input: Box, - output: Box, - process: Child, + input: Box, + output: Box, + stdout: Option>, + stderr: Option>, ) -> Self { - TransportParams { + TransportPipe { input, output, - process, + stdout, + stderr, } } } @@ -70,16 +72,14 @@ pub(crate) struct TransportDelegate { log_handlers: LogHandlers, current_requests: Requests, pending_requests: Requests, - transport: Box, - process: Arc>>, + transport: Arc, server_tx: Arc>>>, } impl TransportDelegate { - pub fn new(transport: Box) -> Self { + pub fn new(transport: Arc) -> Self { Self { transport, - process: Default::default(), server_tx: Default::default(), log_handlers: Default::default(), current_requests: Default::default(), @@ -98,7 +98,7 @@ impl TransportDelegate { let (server_tx, client_rx) = unbounded::(); cx.update(|cx| { - if let Some(stdout) = params.process.stdout.take() { + if let Some(stdout) = params.stdout.take() { cx.background_executor() .spawn(Self::handle_adapter_log(stdout, self.log_handlers.clone())) .detach_and_log_err(cx); @@ -113,7 +113,7 @@ impl TransportDelegate { )) .detach_and_log_err(cx); - if let Some(stderr) = params.process.stderr.take() { + if let Some(stderr) = params.stderr.take() { cx.background_executor() .spawn(Self::handle_error(stderr, self.log_handlers.clone())) .detach_and_log_err(cx); @@ -131,9 +131,6 @@ impl TransportDelegate { })?; { - let mut lock = self.process.lock().await; - *lock = Some(params.process); - let mut lock = self.server_tx.lock().await; *lock = Some(server_tx.clone()); } @@ -166,7 +163,10 @@ impl TransportDelegate { } } - async fn handle_adapter_log(stdout: ChildStdout, log_handlers: LogHandlers) -> Result<()> { + async fn handle_adapter_log(stdout: Stdout, log_handlers: LogHandlers) -> Result<()> + where + Stdout: AsyncRead + Unpin + Send + 'static, + { let mut reader = BufReader::new(stdout); let mut line = String::new(); @@ -196,13 +196,20 @@ impl TransportDelegate { result } - async fn handle_input( - mut server_stdin: Box, + fn build_rpc_message(message: String) -> String { + format!("Content-Length: {}\r\n\r\n{}", message.len(), message) + } + + async fn handle_input( + mut server_stdin: Stdin, client_rx: Receiver, current_requests: Requests, pending_requests: Requests, log_handlers: LogHandlers, - ) -> Result<()> { + ) -> Result<()> + where + Stdin: AsyncWrite + Unpin + Send + 'static, + { let result = loop { match client_rx.recv().await { Ok(message) => { @@ -224,10 +231,7 @@ impl TransportDelegate { } if let Err(e) = server_stdin - .write_all( - format!("Content-Length: {}\r\n\r\n{}", message.len(), message) - .as_bytes(), - ) + .write_all(Self::build_rpc_message(message).as_bytes()) .await { break Err(e.into()); @@ -248,18 +252,21 @@ impl TransportDelegate { result } - async fn handle_output( - mut server_stdout: Box, + async fn handle_output( + server_stdout: Stdout, client_tx: Sender, pending_requests: Requests, log_handlers: LogHandlers, - ) -> Result<()> { + ) -> Result<()> + where + Stdout: AsyncRead + Unpin + Send + 'static, + { let mut recv_buffer = String::new(); + let mut reader = BufReader::new(server_stdout); let result = loop { let message = - Self::receive_server_message(&mut server_stdout, &mut recv_buffer, &log_handlers) - .await; + Self::receive_server_message(&mut reader, &mut recv_buffer, &log_handlers).await; match message { Ok(Message::Response(res)) => { @@ -287,7 +294,10 @@ impl TransportDelegate { result } - async fn handle_error(stderr: ChildStderr, log_handlers: LogHandlers) -> Result<()> { + async fn handle_error(stderr: Stderr, log_handlers: LogHandlers) -> Result<()> + where + Stderr: AsyncRead + Unpin + Send + 'static, + { let mut buffer = String::new(); let mut reader = BufReader::new(stderr); @@ -331,11 +341,14 @@ impl TransportDelegate { } } - async fn receive_server_message( - reader: &mut Box, + async fn receive_server_message( + reader: &mut BufReader, buffer: &mut String, log_handlers: &LogHandlers, - ) -> Result { + ) -> Result + where + Stdout: AsyncRead + Unpin + Send + 'static, + { let mut content_length = None; loop { buffer.truncate(0); @@ -389,20 +402,16 @@ impl TransportDelegate { server_tx.close(); } - let mut adapter = self.process.lock().await.take(); let mut current_requests = self.current_requests.lock().await; let mut pending_requests = self.pending_requests.lock().await; current_requests.clear(); pending_requests.clear(); - if let Some(mut adapter) = adapter.take() { - let _ = adapter.kill().log_err(); - } + let _ = self.transport.kill().await.log_err(); drop(current_requests); drop(pending_requests); - drop(adapter); log::debug!("Shutdown client completed"); @@ -413,6 +422,11 @@ impl TransportDelegate { self.transport.has_adapter_logs() } + #[cfg(any(test, feature = "test-support"))] + pub fn transport(&self) -> &Arc { + &self.transport + } + pub fn add_log_handler(&self, f: F, kind: LogKind) where F: 'static + Send + FnMut(IoKind, &str), @@ -423,23 +437,28 @@ impl TransportDelegate { } #[async_trait(?Send)] -pub trait Transport: 'static + Send + Sync { +pub trait Transport: 'static + Send + Sync + Any { async fn start( - &mut self, + &self, binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Result; + ) -> Result; fn has_adapter_logs(&self) -> bool; - fn clone_box(&self) -> Box; + async fn kill(&self) -> Result<()>; + + #[cfg(any(test, feature = "test-support"))] + fn as_fake(&self) -> &FakeTransport { + panic!("called as_fake on a real adapter"); + } } -#[derive(Clone)] pub struct TcpTransport { port: u16, host: Ipv4Addr, timeout: Option, + process: Arc>>, } impl TcpTransport { @@ -448,6 +467,7 @@ impl TcpTransport { port, host, timeout, + process: Arc::new(Mutex::new(None)), } } @@ -467,10 +487,10 @@ impl TcpTransport { #[async_trait(?Send)] impl Transport for TcpTransport { async fn start( - &mut self, + &self, binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, - ) -> Result { + ) -> Result { let mut command = process::Command::new(&binary.command); if let Some(cwd) = &binary.cwd { @@ -491,7 +511,7 @@ impl Transport for TcpTransport { .stderr(Stdio::piped()) .kill_on_drop(true); - let process = command + let mut process = command .spawn() .with_context(|| "failed to start debug adapter.")?; @@ -523,10 +543,18 @@ impl Transport for TcpTransport { self.port ); - Ok(TransportParams::new( + let stdout = process.stdout.take(); + let stderr = process.stderr.take(); + + { + *self.process.lock().await = Some(process); + } + + Ok(TransportPipe::new( Box::new(tx), Box::new(BufReader::new(rx)), - process, + stdout.map(|s| Box::new(s) as Box), + stderr.map(|s| Box::new(s) as Box), )) } @@ -534,27 +562,34 @@ impl Transport for TcpTransport { true } - fn clone_box(&self) -> Box { - Box::new(self.clone()) + async fn kill(&self) -> Result<()> { + if let Some(mut process) = self.process.lock().await.take() { + process.kill()?; + } + + Ok(()) } } -#[derive(Clone)] -pub struct StdioTransport {} +pub struct StdioTransport { + process: Arc>>, +} impl StdioTransport { pub fn new() -> Self { - Self {} + Self { + process: Arc::new(Mutex::new(None)), + } } } #[async_trait(?Send)] impl Transport for StdioTransport { async fn start( - &mut self, + &self, binary: &DebugAdapterBinary, _: &mut AsyncAppContext, - ) -> Result { + ) -> Result { let mut command = process::Command::new(&binary.command); if let Some(cwd) = &binary.cwd { @@ -587,13 +622,162 @@ impl Transport for StdioTransport { .stdout .take() .ok_or_else(|| anyhow!("Failed to open stdout"))?; + let stderr = process + .stdout + .take() + .ok_or_else(|| anyhow!("Failed to open stderr"))?; log::info!("Debug adapter has connected to stdio adapter"); - Ok(TransportParams::new( + { + *self.process.lock().await = Some(process); + } + + Ok(TransportPipe::new( Box::new(stdin), Box::new(BufReader::new(stdout)), - process, + None, + Some(Box::new(stderr) as Box), + )) + } + + fn has_adapter_logs(&self) -> bool { + false + } + + async fn kill(&self) -> Result<()> { + if let Some(mut process) = self.process.lock().await.take() { + process.kill()?; + } + + Ok(()) + } +} + +#[cfg(any(test, feature = "test-support"))] +type RequestHandler = Box< + dyn Send + + FnMut( + u64, + serde_json::Value, + Arc>, + ) -> std::pin::Pin + Send>>, +>; + +#[cfg(any(test, feature = "test-support"))] +pub struct FakeTransport { + request_handlers: Arc>>, +} + +#[cfg(any(test, feature = "test-support"))] +impl FakeTransport { + pub fn new() -> Self { + Self { + request_handlers: Arc::new(Mutex::new(HashMap::default())), + } + } + + pub async fn on_request(&self, mut handler: F) + where + F: 'static + Send + FnMut(u64, R::Arguments) -> Result, + { + self.request_handlers.lock().await.insert( + R::COMMAND, + Box::new( + move |seq, args, writer: Arc>| { + let response = handler(seq, serde_json::from_value(args).unwrap()).unwrap(); + + let message = Message::Response(Response { + seq: seq + 1, + request_seq: seq, + success: true, + command: R::COMMAND.into(), + body: Some(serde_json::to_value(response).unwrap()), + }); + + let message = serde_json::to_string(&message).unwrap(); + + let writer = writer.clone(); + + Box::pin(async move { + let mut writer = writer.lock().await; + writer + .write_all(TransportDelegate::build_rpc_message(message).as_bytes()) + .await + .unwrap(); + writer.flush().await.unwrap(); + }) + }, + ), + ); + } +} + +#[cfg(any(test, feature = "test-support"))] +#[async_trait(?Send)] +impl Transport for FakeTransport { + async fn start( + &self, + _binary: &DebugAdapterBinary, + cx: &mut AsyncAppContext, + ) -> Result { + let (stdin_writer, stdin_reader) = async_pipe::pipe(); + let (stdout_writer, stdout_reader) = async_pipe::pipe(); + + let handlers = self.request_handlers.clone(); + let stdout_writer = Arc::new(Mutex::new(stdout_writer)); + + cx.background_executor().spawn(async move { + let mut reader = BufReader::new(stdin_reader); + let mut buffer = String::new(); + + loop { + let message = TransportDelegate::receive_server_message( + &mut reader, + &mut buffer, + &Default::default(), + ) + .await; + + match message { + Err(error) => { + break anyhow!(error); + } + Ok(Message::Request(request)) => { + if let Some(mut handle) = + handlers.lock().await.remove(request.command.as_str()) + { + handle( + request.seq, + request.arguments.unwrap(), + stdout_writer.clone(), + ) + .await; + } else { + log::debug!("No handler for {}", request.command); + } + } + Ok(Message::Event(event)) => { + let message = serde_json::to_string(&Message::Event(event)).unwrap(); + + let mut writer = stdout_writer.lock().await; + writer + .write_all(TransportDelegate::build_rpc_message(message).as_bytes()) + .await + .unwrap(); + writer.flush().await.unwrap(); + } + _ => unreachable!("You can only send a request and an event that is redirected to the ouput reader"), + } + } + }) + .detach(); + + Ok(TransportPipe::new( + Box::new(stdin_writer), + Box::new(stdout_reader), + None, + None, )) } @@ -601,7 +785,12 @@ impl Transport for StdioTransport { false } - fn clone_box(&self) -> Box { - Box::new(self.clone()) + async fn kill(&self) -> Result<()> { + Ok(()) + } + + #[cfg(any(test, feature = "test-support"))] + fn as_fake(&self) -> &FakeTransport { + self } } diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 6f390f4e66f46c..a1bb17fb806aaa 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -5,6 +5,13 @@ edition = "2021" publish = false license = "GPL-3.0-or-later" +[features] +test-support = [ + "util/test-support", + "task/test-support", + "dap/test-support", +] + [lints] workspace = true @@ -23,3 +30,7 @@ serde_json.workspace = true sysinfo.workspace = true task.workspace = true util.workspace = true + +[dev-dependencies] +dap = { workspace = true, features = ["test-support"] } +task = { workspace = true, features = ["test-support"] } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 30a21dd7d404d2..ed54a828f882f6 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsString, path::PathBuf}; +use std::{ffi::OsString, path::PathBuf, sync::Arc}; use dap::transport::{StdioTransport, TcpTransport, Transport}; use serde_json::Value; @@ -8,7 +8,7 @@ use crate::*; pub(crate) struct CustomDebugAdapter { custom_args: CustomArgs, - transport: Box, + transport: Arc, } impl CustomDebugAdapter { @@ -17,12 +17,12 @@ impl CustomDebugAdapter { pub(crate) async fn new(custom_args: CustomArgs) -> Result { Ok(CustomDebugAdapter { transport: match &custom_args.connection { - DebugConnectionType::TCP(host) => Box::new(TcpTransport::new( + DebugConnectionType::TCP(host) => Arc::new(TcpTransport::new( host.host(), TcpTransport::port(&host).await?, host.timeout, )), - DebugConnectionType::STDIO => Box::new(StdioTransport::new()), + DebugConnectionType::STDIO => Arc::new(StdioTransport::new()), }, custom_args, }) @@ -35,8 +35,8 @@ impl DebugAdapter for CustomDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Box { - self.transport.clone_box() + fn transport(&self) -> Arc { + self.transport.clone() } async fn get_binary( diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 4fddcd31f2520c..21d8a8cc084094 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -7,6 +7,8 @@ mod lldb; mod php; mod python; +use std::sync::Arc; + use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use custom::CustomDebugAdapter; @@ -24,19 +26,24 @@ use python::PythonDebugAdapter; use serde_json::{json, Value}; use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost}; -pub async fn build_adapter(kind: &DebugAdapterKind) -> Result> { - match &kind { +pub async fn build_adapter(kind: &DebugAdapterKind) -> Result> { + match kind { DebugAdapterKind::Custom(start_args) => { - Ok(Box::new(CustomDebugAdapter::new(start_args.clone()).await?)) + Ok(Arc::new(CustomDebugAdapter::new(start_args.clone()).await?)) } - DebugAdapterKind::Python(host) => Ok(Box::new(PythonDebugAdapter::new(host).await?)), - DebugAdapterKind::Php(host) => Ok(Box::new(PhpDebugAdapter::new(host.clone()).await?)), + DebugAdapterKind::Python(host) => Ok(Arc::new(PythonDebugAdapter::new(host).await?)), + DebugAdapterKind::Php(host) => Ok(Arc::new(PhpDebugAdapter::new(host.clone()).await?)), DebugAdapterKind::Javascript(host) => { - Ok(Box::new(JsDebugAdapter::new(host.clone()).await?)) + Ok(Arc::new(JsDebugAdapter::new(host.clone()).await?)) } - DebugAdapterKind::Lldb => Ok(Box::new(LldbDebugAdapter::new())), - DebugAdapterKind::Go(host) => Ok(Box::new(GoDebugAdapter::new(host).await?)), + DebugAdapterKind::Lldb => Ok(Arc::new(LldbDebugAdapter::new())), + DebugAdapterKind::Go(host) => Ok(Arc::new(GoDebugAdapter::new(host).await?)), #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - DebugAdapterKind::Gdb => Ok(Box::new(GdbDebugAdapter::new())), + DebugAdapterKind::Gdb => Ok(Arc::new(GdbDebugAdapter::new())), + #[cfg(any(test, feature = "test-support"))] + DebugAdapterKind::Fake => Ok(Arc::new(dap::adapters::FakeAdapter::new())), + #[cfg(not(any(test, feature = "test-support")))] + #[allow(unreachable_patterns)] + _ => unreachable!("Fake variant only exists with test-support feature"), } } diff --git a/crates/dap_adapters/src/gdb.rs b/crates/dap_adapters/src/gdb.rs index 4f7ffcf7f621a8..de94d1a1089e74 100644 --- a/crates/dap_adapters/src/gdb.rs +++ b/crates/dap_adapters/src/gdb.rs @@ -23,8 +23,8 @@ impl DebugAdapter for GdbDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Box { - Box::new(StdioTransport::new()) + fn transport(&self) -> Arc { + Arc::new(StdioTransport::new()) } async fn get_binary( diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index 587526eec9f056..b1e8b34c526dc7 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -1,5 +1,5 @@ use dap::transport::{TcpTransport, Transport}; -use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; +use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf, sync::Arc}; use crate::*; @@ -28,8 +28,8 @@ impl DebugAdapter for GoDebugAdapter { DebugAdapterName(Self::_ADAPTER_NAME.into()) } - fn transport(&self) -> Box { - Box::new(TcpTransport::new(self.host, self.port, self.timeout)) + fn transport(&self) -> Arc { + Arc::new(TcpTransport::new(self.host, self.port, self.timeout)) } async fn get_binary( diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 805913fc510244..da425ba70553d1 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,7 +1,7 @@ use adapters::latest_github_release; use dap::transport::{TcpTransport, Transport}; use regex::Regex; -use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf}; +use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf, sync::Arc}; use sysinfo::{Pid, Process}; use task::DebugRequestType; @@ -32,8 +32,8 @@ impl DebugAdapter for JsDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Box { - Box::new(TcpTransport::new(self.host, self.port, self.timeout)) + fn transport(&self) -> Arc { + Arc::new(TcpTransport::new(self.host, self.port, self.timeout)) } async fn fetch_latest_adapter_version( diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 8a6bb9ad8b1d10..b007f0f89b0de4 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsStr, path::PathBuf}; +use std::{ffi::OsStr, path::PathBuf, sync::Arc}; use anyhow::Result; use async_trait::async_trait; @@ -23,8 +23,8 @@ impl DebugAdapter for LldbDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Box { - Box::new(StdioTransport::new()) + fn transport(&self) -> Arc { + Arc::new(StdioTransport::new()) } async fn get_binary( diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index db031900150752..1cba2b4ce2fa38 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,6 +1,6 @@ use adapters::latest_github_release; use dap::transport::{TcpTransport, Transport}; -use std::{net::Ipv4Addr, path::PathBuf}; +use std::{net::Ipv4Addr, path::PathBuf, sync::Arc}; use crate::*; @@ -29,8 +29,8 @@ impl DebugAdapter for PhpDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Box { - Box::new(TcpTransport::new(self.host, self.port, self.timeout)) + fn transport(&self) -> Arc { + Arc::new(TcpTransport::new(self.host, self.port, self.timeout)) } async fn fetch_latest_adapter_version( diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 38e1be293aca66..eb5118eb79eb0c 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,5 +1,5 @@ use dap::transport::{TcpTransport, Transport}; -use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; +use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf, sync::Arc}; use crate::*; @@ -28,8 +28,8 @@ impl DebugAdapter for PythonDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Box { - Box::new(TcpTransport::new(self.host, self.port, self.timeout)) + fn transport(&self) -> Arc { + Arc::new(TcpTransport::new(self.host, self.port, self.timeout)) } async fn fetch_latest_adapter_version( diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 007ae83b99b626..e89ba63c000e6a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -382,7 +382,7 @@ impl DapStore { pub fn start_client( &mut self, - adapter: Arc>, + adapter: Arc, binary: DebugAdapterBinary, config: DebugAdapterConfig, cx: &mut ModelContext, @@ -403,12 +403,10 @@ impl DapStore { let start_client_task = cx.spawn(|this, mut cx| async move { let dap_store = this.clone(); - let mut client = - DebugAdapterClient::new(client_id, config, adapter, binary.clone(), &cx); + let mut client = DebugAdapterClient::new(client_id, config, adapter, binary, &cx); let result = client .start( - &binary, move |message, cx| { dap_store .update(cx, |_, cx| { @@ -470,7 +468,7 @@ impl DapStore { let adapter_delegate = Arc::new(adapter_delegate); cx.spawn(|this, mut cx| async move { - let adapter = Arc::new(build_adapter(&config.kind).await?); + let adapter = build_adapter(&config.kind).await?; let binary = cx.update(|cx| { let name = DebugAdapterName::from(adapter.name().as_ref()); @@ -1181,7 +1179,7 @@ impl DapStore { self.ignore_breakpoints.remove(client_id); let capabilities = self.capabilities.remove(client_id); - cx.background_executor().spawn(async move { + cx.spawn(|_, _| async move { let client = match client { DebugAdapterClientState::Starting(task) => task.await, DebugAdapterClientState::Running(client) => Some(client), diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml index 7a22ea6157e4ea..008516524e9ea1 100644 --- a/crates/task/Cargo.toml +++ b/crates/task/Cargo.toml @@ -5,6 +5,12 @@ edition = "2021" publish = false license = "GPL-3.0-or-later" +[features] +test-support = [ + "gpui/test-support", + "util/test-support" +] + [lints] workspace = true diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index f5ca35b68ce5e9..29cad2b1914abd 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -76,6 +76,9 @@ pub enum DebugAdapterKind { /// Use GDB's built-in DAP support #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] Gdb, + /// Used for integration tests + #[cfg(any(test, feature = "test-support"))] + Fake, } impl DebugAdapterKind { @@ -90,6 +93,8 @@ impl DebugAdapterKind { #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] Self::Gdb => "GDB", Self::Go(_) => "Go", + #[cfg(any(test, feature = "test-support"))] + Self::Fake => "Fake", } } } From 6a4a285ee6aea432b286d11f8ad36177c163a546 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 10 Dec 2024 17:28:47 +0100 Subject: [PATCH 374/650] Add test to validate that you can add/remove breakpoints in collab setting --- crates/collab/src/tests/editor_tests.rs | 198 +++++++++++++++++++++++- crates/editor/src/editor.rs | 2 +- crates/project/src/dap_store.rs | 13 +- 3 files changed, 210 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index beb1ef61ef9886..7e4460e762f2db 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -23,7 +23,7 @@ use language::{ use multi_buffer::MultiBufferRow; use project::{ project_settings::{InlineBlameSettings, ProjectSettings}, - SERVER_PROGRESS_THROTTLE_TIMEOUT, + ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, }; use recent_projects::disconnected_overlay::DisconnectedOverlay; use rpc::RECEIVE_TIMEOUT; @@ -2401,6 +2401,202 @@ fn main() { let foo = other::foo(); }"}; ); } +#[gpui::test] +async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + cx_a.update(editor::init); + cx_b.update(editor::init); + client_a + .fs() + .insert_tree( + "/a", + json!({ + "test.txt": "one\ntwo\nthree\nfour\nfive", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_path = ProjectPath { + worktree_id, + path: Arc::from(Path::new(&"test.txt")), + }; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Client A opens an editor. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens same editor as A. + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + // Client A adds breakpoint on line (1) + editor_a.update(cx_a, |editor, cx| { + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let breakpoints_a = editor_a.update(cx_a, |editor, cx| { + editor + .dap_store + .clone() + .unwrap() + .read(cx) + .breakpoints() + .clone() + }); + let breakpoints_b = editor_b.update(cx_b, |editor, cx| { + editor + .dap_store + .clone() + .unwrap() + .read(cx) + .breakpoints() + .clone() + }); + + assert_eq!(1, breakpoints_a.len()); + assert_eq!(1, breakpoints_a.get(&project_path).unwrap().len()); + assert_eq!(breakpoints_a, breakpoints_b); + + // Client B adds breakpoint on line(2) + editor_b.update(cx_b, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let breakpoints_a = editor_a.update(cx_a, |editor, cx| { + editor + .dap_store + .clone() + .unwrap() + .read(cx) + .breakpoints() + .clone() + }); + let breakpoints_b = editor_b.update(cx_b, |editor, cx| { + editor + .dap_store + .clone() + .unwrap() + .read(cx) + .breakpoints() + .clone() + }); + + assert_eq!(1, breakpoints_a.len()); + assert_eq!(breakpoints_a, breakpoints_b); + assert_eq!(2, breakpoints_a.get(&project_path).unwrap().len()); + + // Client A removes last added breakpoint from client B + editor_a.update(cx_a, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let breakpoints_a = editor_a.update(cx_a, |editor, cx| { + editor + .dap_store + .clone() + .unwrap() + .read(cx) + .breakpoints() + .clone() + }); + let breakpoints_b = editor_b.update(cx_b, |editor, cx| { + editor + .dap_store + .clone() + .unwrap() + .read(cx) + .breakpoints() + .clone() + }); + + assert_eq!(1, breakpoints_a.len()); + assert_eq!(breakpoints_a, breakpoints_b); + assert_eq!(1, breakpoints_a.get(&project_path).unwrap().len()); + + // Client B removes first added breakpoint by client A + editor_b.update(cx_b, |editor, cx| { + editor.move_up(&editor::actions::MoveUp, cx); + editor.move_up(&editor::actions::MoveUp, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let breakpoints_a = editor_a.update(cx_a, |editor, cx| { + editor + .dap_store + .clone() + .unwrap() + .read(cx) + .breakpoints() + .clone() + }); + let breakpoints_b = editor_b.update(cx_b, |editor, cx| { + editor + .dap_store + .clone() + .unwrap() + .read(cx) + .breakpoints() + .clone() + }); + + assert_eq!(0, breakpoints_a.len()); + assert_eq!(breakpoints_a, breakpoints_b); + assert!(breakpoints_a.get(&project_path).is_none()); +} + #[track_caller] fn tab_undo_assert( cx_a: &mut EditorTestContext, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5292d8dcc93561..f8f873525394e5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -657,7 +657,7 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - dap_store: Option>, + pub dap_store: Option>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e89ba63c000e6a..e671f883f4ac4a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1317,7 +1317,11 @@ impl DapStore { ) -> Task> { let upstream_client = self.upstream_client(); - let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); + let mut breakpoint_set = self + .breakpoints + .get(project_path) + .cloned() + .unwrap_or_default(); match edit_action { BreakpointEditAction::Toggle => { @@ -1331,6 +1335,13 @@ impl DapStore { } } + if breakpoint_set.is_empty() { + self.breakpoints.remove(project_path); + } else { + self.breakpoints + .insert(project_path.clone(), breakpoint_set.clone()); + } + cx.notify(); if let Some((client, project_id)) = upstream_client.or(self.downstream_client.clone()) { From 22e52c306a2f362de9067408bf7711e50ebe9371 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 10 Dec 2024 17:36:13 +0100 Subject: [PATCH 375/650] Use util command for windows support --- crates/dap/src/adapters.rs | 4 ++-- crates/dap/src/transport.rs | 6 +++--- crates/dap_adapters/src/lldb.rs | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index c51b7f22fc3d8c..b9a8010abd0870 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -12,7 +12,7 @@ pub use http_client::{github::latest_github_release, HttpClient}; use node_runtime::NodeRuntime; use serde::{Deserialize, Serialize}; use serde_json::Value; -use smol::{self, fs::File, lock::Mutex, process}; +use smol::{self, fs::File, lock::Mutex}; use std::{ collections::{HashMap, HashSet}, ffi::{OsStr, OsString}, @@ -162,7 +162,7 @@ pub async fn download_adapter_from_github( futures::io::copy(response.body_mut(), &mut file).await?; // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence` - process::Command::new("unzip") + util::command::new_smol_command("unzip") .arg(&zip_path) .arg("-d") .arg(&version_path) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 8153736e6924eb..649c68377fc1dc 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -13,7 +13,7 @@ use smol::{ io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader}, lock::Mutex, net::{TcpListener, TcpStream}, - process::{self, Child}, + process::Child, }; use std::{ any::Any, @@ -491,7 +491,7 @@ impl Transport for TcpTransport { binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result { - let mut command = process::Command::new(&binary.command); + let mut command = util::command::new_smol_command(&binary.command); if let Some(cwd) = &binary.cwd { command.current_dir(cwd); @@ -590,7 +590,7 @@ impl Transport for StdioTransport { binary: &DebugAdapterBinary, _: &mut AsyncAppContext, ) -> Result { - let mut command = process::Command::new(&binary.command); + let mut command = util::command::new_smol_command(&binary.command); if let Some(cwd) = &binary.cwd { command.current_dir(cwd); diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index b007f0f89b0de4..d9281a0d546367 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -34,9 +34,10 @@ impl DebugAdapter for LldbDebugAdapter { user_installed_path: Option, ) -> Result { let lldb_dap_path = if cfg!(target_os = "macos") { - std::process::Command::new("xcrun") + util::command::new_smol_command("xcrun") .args(&["-f", "lldb-dap"]) .output() + .await .ok() .and_then(|output| String::from_utf8(output.stdout).ok()) .map(|path| path.trim().to_string()) From 04cd04eb443db1a73abc4abdff4ece0d05cc5fe6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 10 Dec 2024 17:54:38 +0100 Subject: [PATCH 376/650] Move thread status to debug_panel --- crates/dap/src/client.rs | 9 --------- crates/debugger_ui/src/debugger_panel.rs | 11 ++++++++++- crates/debugger_ui/src/debugger_panel_item.rs | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 45f2ec1ddd4a84..367799102c28f5 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -22,15 +22,6 @@ use task::{DebugAdapterConfig, DebugRequestType}; const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ThreadStatus { - #[default] - Running, - Stopped, - Exited, - Ended, -} - #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct DebugAdapterClientId(pub usize); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index d9270aace1077b..ed38d7196e7dd1 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -3,7 +3,7 @@ use crate::debugger_panel_item::DebugPanelItem; use anyhow::Result; use collections::{BTreeMap, HashMap}; use command_palette_hooks::CommandPaletteFilter; -use dap::client::{DebugAdapterClientId, ThreadStatus}; +use dap::client::DebugAdapterClientId; use dap::debugger_settings::DebuggerSettings; use dap::messages::{Events, Message}; use dap::requests::{Request, RunInTerminal, StartDebugging}; @@ -63,6 +63,15 @@ pub struct ThreadState { pub stopped: bool, } +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ThreadStatus { + #[default] + Running, + Stopped, + Exited, + Ended, +} + pub struct DebugPanel { size: Pixels, pane: View, diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 33c530962014c5..b89ab445cd01e8 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,11 +1,11 @@ use crate::console::Console; -use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState}; +use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState, ThreadStatus}; use crate::loaded_source_list::LoadedSourceList; use crate::module_list::ModuleList; use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; -use dap::client::{DebugAdapterClientId, ThreadStatus}; +use dap::client::DebugAdapterClientId; use dap::debugger_settings::DebuggerSettings; use dap::{ Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, From 490d42599b4a47e0b47c13066fb602dd02d0b9b3 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 11 Dec 2024 05:20:50 -0500 Subject: [PATCH 377/650] Fix GDB Debug Adapter on linux & Clippy The GDB adapter was failing because an stderr stream failed to connect to Zed from the adapter. We now log the error and continue the debugging sequence if std input and output streams connect. --- crates/collab/src/tests/editor_tests.rs | 2 +- crates/dap/src/transport.rs | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 7e4460e762f2db..61c73e56450aaf 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2594,7 +2594,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte assert_eq!(0, breakpoints_a.len()); assert_eq!(breakpoints_a, breakpoints_b); - assert!(breakpoints_a.get(&project_path).is_none()); + assert!(breakpoints_a.contains_key(&project_path)); } #[track_caller] diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 649c68377fc1dc..3ab20e58b86e6b 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -329,7 +329,7 @@ impl TransportDelegate { if response.success { Ok(response) } else { - if let Some(body) = response.body { + if let Some(body) = response.body.clone() { if let Ok(error) = serde_json::from_value::(body) { if let Some(message) = error.error { return Err(anyhow!(message.format)); @@ -337,7 +337,10 @@ impl TransportDelegate { }; } - Err(anyhow!("Received error response from adapter")) + Err(anyhow!( + "Received error response from adapter. Response: {:?}", + response.clone() + )) } } @@ -625,7 +628,14 @@ impl Transport for StdioTransport { let stderr = process .stdout .take() - .ok_or_else(|| anyhow!("Failed to open stderr"))?; + .map(|io_err| Box::new(io_err) as Box); + + if stderr.is_none() { + log::error!( + "Failed to connect to stderr for debug adapter command {}", + &binary.command + ); + } log::info!("Debug adapter has connected to stdio adapter"); @@ -637,7 +647,7 @@ impl Transport for StdioTransport { Box::new(stdin), Box::new(BufReader::new(stdout)), None, - Some(Box::new(stderr) as Box), + stderr, )) } From 29a9eaf5fc0912a0d820164b5340d4910470a2a2 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 14 Dec 2024 19:00:33 +0100 Subject: [PATCH 378/650] Fix RunInTerminal not working due env variable values get wrapped with `"` --- crates/debugger_ui/src/debugger_panel.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index ed38d7196e7dd1..259d49ab10b1c3 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -308,21 +308,13 @@ impl DebugPanel { let mut envs: HashMap = Default::default(); if let Some(Value::Object(env)) = request_args.env { - // Special handling for VSCODE_INSPECTOR_OPTIONS: - // The JavaScript debug adapter expects this value to be a valid JSON object. - // However, it's often passed as an escaped string, which the adapter can't parse. - // We need to unescape it and reformat it so the adapter can read it correctly. for (key, value) in env { let value_str = match (key.as_str(), value) { - ("VSCODE_INSPECTOR_OPTIONS", Value::String(value)) => { - serde_json::from_str::(&value[3..]) - .map(|json| format!(":::{}", json)) - .unwrap_or_else(|_| value) - } - (_, value) => value.to_string(), + (_, Value::String(value)) => value, + _ => continue, }; - envs.insert(key, value_str.trim_matches('"').to_string()); + envs.insert(key, value_str); } } From aefef9c58ca62f4328175d38a4f80882dbabb0c8 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 14 Dec 2024 19:59:41 +0100 Subject: [PATCH 379/650] Log event names for debugging --- Cargo.lock | 2 +- crates/dap/Cargo.toml | 2 +- crates/dap/src/client.rs | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8aac6c6a9dccea..6c008e0c3c375d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3598,7 +3598,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=b95818130022bfc72bbcd639bdd0c0358c7549fc#b95818130022bfc72bbcd639bdd0c0358c7549fc" +source = "git+https://github.com/zed-industries/dap-types?rev=b186b35bd6cfe63a7befcd4137c142ce9dd9c754#b186b35bd6cfe63a7befcd4137c142ce9dd9c754" dependencies = [ "schemars", "serde", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index dd101a48647017..1abcb9e5d0a0d2 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -23,7 +23,7 @@ async-pipe = { workspace = true, optional = true } async-tar.workspace = true async-trait.workspace = true collections.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b95818130022bfc72bbcd639bdd0c0358c7549fc" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b186b35bd6cfe63a7befcd4137c142ce9dd9c754" } fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 367799102c28f5..e59a7b0f036cff 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -93,7 +93,11 @@ impl DebugAdapterClient { }; if let Err(e) = match message { - Message::Event(ev) => cx.update(|cx| event_handler(Message::Event(ev), cx)), + Message::Event(ev) => { + log::debug!("Received event `{}`", &ev); + + cx.update(|cx| event_handler(Message::Event(ev), cx)) + } Message::Request(req) => cx.update(|cx| event_handler(Message::Request(req), cx)), Message::Response(_) => unreachable!(), } { From 980f9c7353f2beed34f1399e236441274654c277 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 14 Dec 2024 20:04:39 +0100 Subject: [PATCH 380/650] Correctly reconnect to adapter for `StartDebugging` reverse request Before this change, we always started a new adapter but this was causing some issues: - Infinite client starts - Cannot start client port already in use - Not able to start a debug session Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/dap/src/client.rs | 20 +++ crates/dap/src/transport.rs | 63 ++++++++- crates/project/src/dap_store.rs | 224 ++++++++++++++++++++------------ 3 files changed, 219 insertions(+), 88 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index e59a7b0f036cff..d6b1403a5ba19e 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -57,6 +57,26 @@ impl DebugAdapterClient { } } + pub async fn reconnect(&mut self, message_handler: F, cx: &mut AsyncAppContext) -> Result<()> + where + F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, + { + let (server_rx, server_tx) = self.transport_delegate.reconnect(cx).await?; + log::info!("Successfully reconnected to debug adapter"); + + // start handling events/reverse requests + cx.update(|cx| { + cx.spawn({ + let server_tx = server_tx.clone(); + |mut cx| async move { + Self::handle_receive_messages(server_rx, server_tx, message_handler, &mut cx) + .await + } + }) + .detach_and_log_err(cx); + }) + } + pub async fn start(&mut self, message_handler: F, cx: &mut AsyncAppContext) -> Result<()> where F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 3ab20e58b86e6b..95d68ff42ede76 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; use dap_types::{ messages::{Message, Response}, @@ -87,13 +87,28 @@ impl TransportDelegate { } } + pub(crate) async fn reconnect( + &mut self, + cx: &mut AsyncAppContext, + ) -> Result<(Receiver, Sender)> { + self.start_handlers(self.transport.reconnect(cx).await?, cx) + .await + } + pub(crate) async fn start( &mut self, binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result<(Receiver, Sender)> { - let mut params = self.transport.start(binary, cx).await?; + self.start_handlers(self.transport.start(binary, cx).await?, cx) + .await + } + async fn start_handlers( + &mut self, + mut params: TransportPipe, + cx: &mut AsyncAppContext, + ) -> Result<(Receiver, Sender)> { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); @@ -451,9 +466,13 @@ pub trait Transport: 'static + Send + Sync + Any { async fn kill(&self) -> Result<()>; + async fn reconnect(&self, _: &mut AsyncAppContext) -> Result { + bail!("Cannot reconnect to adapter") + } + #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeTransport { - panic!("called as_fake on a real adapter"); + panic!("Called as_fake on a real adapter"); } } @@ -489,6 +508,44 @@ impl TcpTransport { #[async_trait(?Send)] impl Transport for TcpTransport { + async fn reconnect(&self, cx: &mut AsyncAppContext) -> Result { + let address = SocketAddrV4::new(self.host, self.port); + + let timeout = self.timeout.unwrap_or_else(|| { + cx.update(|cx| DebuggerSettings::get_global(cx).timeout) + .unwrap_or(2000u64) + }); + + let (rx, tx) = select! { + _ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => { + return Err(anyhow!(format!("Reconnect to TCP DAP timeout {}:{}", self.host, self.port))) + }, + result = cx.spawn(|cx| async move { + loop { + match TcpStream::connect(address).await { + Ok(stream) => return stream.split(), + Err(_) => { + cx.background_executor().timer(Duration::from_millis(100)).await; + } + } + } + }).fuse() => result + }; + + log::info!( + "Debug adapter has reconnected to TCP server {}:{}", + self.host, + self.port + ); + + Ok(TransportPipe::new( + Box::new(tx), + Box::new(BufReader::new(rx)), + None, + None, + )) + } + async fn start( &self, binary: &DebugAdapterBinary, diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e671f883f4ac4a..c8b10308833479 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -2,7 +2,6 @@ use crate::project_settings::ProjectSettings; use crate::{ProjectEnvironment, ProjectPath}; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; - use collections::HashMap; use dap::adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}; use dap::{ @@ -380,75 +379,56 @@ impl DapStore { } } - pub fn start_client( + fn reconnect_client( &mut self, adapter: Arc, binary: DebugAdapterBinary, config: DebugAdapterConfig, cx: &mut ModelContext, - ) { + ) -> Task> { let Some(_) = self.as_local() else { - return; + return Task::ready(Err(anyhow!( + "starting a debug client is not supported in remote setting" + ))); }; if !adapter.supports_attach() && matches!(config.request, DebugRequestType::Attach(_)) { - cx.emit(DapStoreEvent::Notification( - "Debug adapter does not support `attach` request".into(), - )); - - return; + return Task::ready(Err(anyhow!( + "Debug adapter does not support `attach` request" + ))); } let client_id = self.next_client_id(); - let start_client_task = cx.spawn(|this, mut cx| async move { - let dap_store = this.clone(); + cx.spawn(|dap_store, mut cx| async move { let mut client = DebugAdapterClient::new(client_id, config, adapter, binary, &cx); - let result = client - .start( - move |message, cx| { - dap_store - .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) - }) - .log_err(); + client + .reconnect( + { + let dap_store = dap_store.clone(); + move |message, cx| { + dap_store + .update(cx, |_, cx| { + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + }) + .log_err(); + } }, &mut cx, ) - .await; - - if let Err(error) = result { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(error.to_string())); - }) - .log_err()?; - return None; - } + .await?; let client = Arc::new(client); - this.update(&mut cx, |store, cx| { - let handle = store + dap_store.update(&mut cx, |store, cx| { + store .clients - .get_mut(&client_id) - .with_context(|| "Failed to find starting debug client")?; - - *handle = DebugAdapterClientState::Running(client.clone()); + .insert(client_id, DebugAdapterClientState::Running(client)); cx.emit(DapStoreEvent::DebugClientStarted(client_id)); - - anyhow::Ok(()) }) - .log_err(); - - Some(client) - }); - - self.clients.insert( - client_id, - DebugAdapterClientState::Starting(start_client_task), - ); + }) } pub fn start_client_from_debug_config( @@ -467,17 +447,40 @@ impl DapStore { })); let adapter_delegate = Arc::new(adapter_delegate); - cx.spawn(|this, mut cx| async move { - let adapter = build_adapter(&config.kind).await?; + let client_id = self.next_client_id(); + + let start_client_task = cx.spawn(|this, mut cx| async move { + let adapter = match build_adapter(&config.kind).await { + Ok(adapter) => adapter, + Err(error) => { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + .log_err()?; + return None; + } + }; - let binary = cx.update(|cx| { - let name = DebugAdapterName::from(adapter.name().as_ref()); + if !adapter.supports_attach() && matches!(config.request, DebugRequestType::Attach(_)) { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification( + "Debug adapter does not support `attach` request".to_string(), + )); + }) + .log_err()?; + return None; + } - ProjectSettings::get_global(cx) - .dap - .get(&name) - .and_then(|s| s.binary.as_ref().map(PathBuf::from)) - })?; + let binary = cx + .update(|cx| { + let name = DebugAdapterName::from(adapter.name().as_ref()); + + ProjectSettings::get_global(cx) + .dap + .get(&name) + .and_then(|s| s.binary.as_ref().map(PathBuf::from)) + }) + .log_err()?; let (adapter, binary) = match adapter .get_binary(adapter_delegate.as_ref(), &config, binary) @@ -491,7 +494,7 @@ impl DapStore { }, ); - return Err(error); + return None; } Ok(mut binary) => { adapter_delegate.update_status(adapter.name(), DapStatus::None); @@ -505,11 +508,55 @@ impl DapStore { } }; + let mut client = DebugAdapterClient::new(client_id, config, adapter, binary, &cx); + + let result = client + .start( + { + let dap_store = this.clone(); + move |message, cx| { + dap_store + .update(cx, |_, cx| { + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + }) + .log_err(); + } + }, + &mut cx, + ) + .await; + + if let Err(error) = result { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + .log_err()?; + return None; + } + + let client = Arc::new(client); + this.update(&mut cx, |store, cx| { - store.start_client(adapter, binary, config, cx); + let handle = store + .clients + .get_mut(&client_id) + .with_context(|| "Failed to find starting debug client")?; + + *handle = DebugAdapterClientState::Running(client.clone()); + + cx.emit(DapStoreEvent::DebugClientStarted(client_id)); + + anyhow::Ok(()) }) - }) - .detach_and_log_err(cx); + .log_err(); + + Some(client) + }); + + self.clients.insert( + client_id, + DebugAdapterClientState::Starting(start_client_task), + ); } pub fn initialize( @@ -724,7 +771,7 @@ impl DapStore { } pub fn respond_to_start_debugging( - &self, + &mut self, client_id: &DebugAdapterClientId, seq: u64, args: Option, @@ -735,21 +782,15 @@ impl DapStore { }; cx.spawn(|this, mut cx| async move { - if let Some(args) = args { - client - .send_message(Message::Response(Response { - seq, - request_seq: seq, - success: true, - command: StartDebugging::COMMAND.to_string(), - body: None, - })) - .await?; - - this.update(&mut cx, |store, cx| { + let (success, body) = if let Some(args) = args { + let task = this.update(&mut cx, |store, cx| { let config = client.config(); - store.start_client( + // Merge the new configuration over the existing configuration + let mut initialize_args = client.config().initialize_args.unwrap_or_default(); + merge_json_value_into(args.configuration, &mut initialize_args); + + store.reconnect_client( client.adapter().clone(), client.binary().clone(), DebugAdapterConfig { @@ -772,22 +813,35 @@ impl DapStore { }, program: config.program.clone(), cwd: config.cwd.clone(), - initialize_args: Some(args.configuration), + initialize_args: Some(initialize_args), }, cx, - ); - }) + ) + }); + + match task { + Ok(task) => match task.await { + Ok(_) => (true, None), + Err(_) => (false, None), + }, + Err(_) => (false, None), + } } else { - client - .send_message(Message::Response(Response { - seq, - request_seq: seq, - success: false, - command: StartDebugging::COMMAND.to_string(), - body: Some(serde_json::to_value(ErrorResponse { error: None })?), - })) - .await - } + ( + false, + Some(serde_json::to_value(ErrorResponse { error: None })?), + ) + }; + + client + .send_message(Message::Response(Response { + seq, + body, + success, + request_seq: seq, + command: StartDebugging::COMMAND.to_string(), + })) + .await }) } From bea29464f95e54cfb813230f74b6469a0cb21bdb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 14 Dec 2024 20:06:29 +0100 Subject: [PATCH 381/650] Show client id inside dap log This makes it easier to see which adapter was started first. --- crates/debugger_tools/src/dap_log.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 7d225d3dc3a2bd..80bec483f80993 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -367,8 +367,9 @@ impl Render for DapLogToolbarItemView { current_client .map(|row| { Cow::Owned(format!( - "{} - {}", + "{}({}) - {}", row.client_name, + row.client_id.0, match row.selected_entry { LogKind::Adapter => ADAPTER_LOGS, LogKind::Rpc => RPC_MESSAGES, @@ -382,7 +383,7 @@ impl Render for DapLogToolbarItemView { let menu_rows = menu_rows.clone(); ContextMenu::build(cx, move |mut menu, cx| { for row in menu_rows.into_iter() { - menu = menu.header(row.client_name.to_string()); + menu = menu.header(format!("{}({})", row.client_name, row.client_id.0)); if row.has_adapter_logs { menu = menu.entry( From 65789d3925c2ce83c5ce4cff99c9c0efdfc9fe3e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 14 Dec 2024 23:14:11 +0100 Subject: [PATCH 382/650] Fix don't serialize terminal view when it's a debug terminal --- crates/terminal_view/src/terminal_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7a83e530feb89a..9a092fcc5bdffa 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1195,7 +1195,7 @@ impl SerializableItem for TerminalView { cx: &mut ViewContext, ) -> Option>> { let terminal = self.terminal().read(cx); - if terminal.task().is_some() { + if terminal.task().is_some() || terminal.debug_terminal() { return None; } From d7b5132bd9da7ea938232941bb1adb572af6afd5 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 16 Dec 2024 12:14:53 -0500 Subject: [PATCH 383/650] Update dap-types version The past version always returned an error when attempting to deserialize custom DAP events. Causing Zed to stop recving messages from the debug adapter --- Cargo.lock | 2 +- crates/dap/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c008e0c3c375d..8afcacaf56b875 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3598,7 +3598,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=b186b35bd6cfe63a7befcd4137c142ce9dd9c754#b186b35bd6cfe63a7befcd4137c142ce9dd9c754" +source = "git+https://github.com/zed-industries/dap-types?rev=9aa5155d631bb1ac8faba5446244d98f855ff97f#9aa5155d631bb1ac8faba5446244d98f855ff97f" dependencies = [ "schemars", "serde", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 1abcb9e5d0a0d2..ff2d19f2187127 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -23,7 +23,7 @@ async-pipe = { workspace = true, optional = true } async-tar.workspace = true async-trait.workspace = true collections.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b186b35bd6cfe63a7befcd4137c142ce9dd9c754" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "9aa5155d631bb1ac8faba5446244d98f855ff97f" } fs.workspace = true futures.workspace = true gpui.workspace = true From 9a802b9133dc29e6e644a7fa98d2487bcf46177a Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 16 Dec 2024 12:30:30 -0500 Subject: [PATCH 384/650] Add dap settings to disable dap logs & format dap messages within logs --- Cargo.lock | 3 ++ crates/dap/src/debugger_settings.rs | 10 +++++ crates/dap/src/transport.rs | 57 +++++++++++++++++++--------- crates/debugger_tools/Cargo.toml | 3 ++ crates/debugger_tools/src/dap_log.rs | 21 ++++++++-- 5 files changed, 73 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8afcacaf56b875..5ec20bf4d0b51a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3704,7 +3704,10 @@ dependencies = [ "futures 0.3.31", "gpui", "project", + "serde_json", + "settings", "smol", + "util", "workspace", ] diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index 4594a1785039e5..657cd3b9b125b9 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -23,6 +23,14 @@ pub struct DebuggerSettings { /// /// Default: 2000ms pub timeout: u64, + /// Whether to log messages between active debug adapters and Zed + /// + /// Default: true + pub log_dap_communications: bool, + /// Whether to format dap messages in when adding them to debug adapter logger + /// + /// Default: true + pub format_dap_log_messages: bool, } impl Default for DebuggerSettings { @@ -32,6 +40,8 @@ impl Default for DebuggerSettings { save_breakpoints: true, stepping_granularity: SteppingGranularity::Line, timeout: 2000, + log_dap_communications: true, + format_dap_log_messages: true, } } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 95d68ff42ede76..249f15397d9423 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -112,10 +112,21 @@ impl TransportDelegate { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); + let log_dap_communications = + cx.update(|cx| DebuggerSettings::get_global(cx).log_dap_communications) + .with_context(|| "Failed to get Debugger Setting log dap communications error in transport::start_handlers. Defaulting to false") + .unwrap_or(false); + + let log_handler = if log_dap_communications { + Some(self.log_handlers.clone()) + } else { + None + }; + cx.update(|cx| { if let Some(stdout) = params.stdout.take() { cx.background_executor() - .spawn(Self::handle_adapter_log(stdout, self.log_handlers.clone())) + .spawn(Self::handle_adapter_log(stdout, log_handler.clone())) .detach_and_log_err(cx); } @@ -124,7 +135,7 @@ impl TransportDelegate { params.output, client_tx, self.pending_requests.clone(), - self.log_handlers.clone(), + log_handler.clone(), )) .detach_and_log_err(cx); @@ -140,7 +151,7 @@ impl TransportDelegate { client_rx, self.current_requests.clone(), self.pending_requests.clone(), - self.log_handlers.clone(), + log_handler.clone(), )) .detach_and_log_err(cx); })?; @@ -178,7 +189,10 @@ impl TransportDelegate { } } - async fn handle_adapter_log(stdout: Stdout, log_handlers: LogHandlers) -> Result<()> + async fn handle_adapter_log( + stdout: Stdout, + log_handlers: Option, + ) -> Result<()> where Stdout: AsyncRead + Unpin + Send + 'static, { @@ -197,9 +211,11 @@ impl TransportDelegate { break Err(anyhow!("Debugger log stream closed")); } - for (kind, handler) in log_handlers.lock().iter_mut() { - if matches!(kind, LogKind::Adapter) { - handler(IoKind::StdOut, line.as_str()); + if let Some(log_handlers) = log_handlers.as_ref() { + for (kind, handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Adapter) { + handler(IoKind::StdOut, line.as_str()); + } } } @@ -220,7 +236,7 @@ impl TransportDelegate { client_rx: Receiver, current_requests: Requests, pending_requests: Requests, - log_handlers: LogHandlers, + log_handlers: Option, ) -> Result<()> where Stdin: AsyncWrite + Unpin + Send + 'static, @@ -239,9 +255,11 @@ impl TransportDelegate { Err(e) => break Err(e.into()), }; - for (kind, log_handler) in log_handlers.lock().iter_mut() { - if matches!(kind, LogKind::Rpc) { - log_handler(IoKind::StdIn, &message); + if let Some(log_handlers) = log_handlers.as_ref() { + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdIn, &message); + } } } @@ -271,7 +289,7 @@ impl TransportDelegate { server_stdout: Stdout, client_tx: Sender, pending_requests: Requests, - log_handlers: LogHandlers, + log_handlers: Option, ) -> Result<()> where Stdout: AsyncRead + Unpin + Send + 'static, @@ -281,7 +299,8 @@ impl TransportDelegate { let result = loop { let message = - Self::receive_server_message(&mut reader, &mut recv_buffer, &log_handlers).await; + Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref()) + .await; match message { Ok(Message::Response(res)) => { @@ -362,7 +381,7 @@ impl TransportDelegate { async fn receive_server_message( reader: &mut BufReader, buffer: &mut String, - log_handlers: &LogHandlers, + log_handlers: Option<&LogHandlers>, ) -> Result where Stdout: AsyncRead + Unpin + Send + 'static, @@ -404,9 +423,11 @@ impl TransportDelegate { let message = std::str::from_utf8(&content).context("invalid utf8 from server")?; - for (kind, log_handler) in log_handlers.lock().iter_mut() { - if matches!(kind, LogKind::Rpc) { - log_handler(IoKind::StdOut, &message); + if let Some(log_handlers) = log_handlers { + for (kind, log_handler) in log_handlers.lock().iter_mut() { + if matches!(kind, LogKind::Rpc) { + log_handler(IoKind::StdOut, &message); + } } } @@ -802,7 +823,7 @@ impl Transport for FakeTransport { let message = TransportDelegate::receive_server_message( &mut reader, &mut buffer, - &Default::default(), + None, ) .await; diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml index b4594fbe602dd5..a062b749f281a8 100644 --- a/crates/debugger_tools/Cargo.toml +++ b/crates/debugger_tools/Cargo.toml @@ -19,5 +19,8 @@ editor.workspace = true futures.workspace = true gpui.workspace = true project.workspace = true +serde_json.workspace = true +settings.workspace = true smol.workspace = true +util.workspace = true workspace.workspace = true diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 80bec483f80993..7c4725a5005721 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -1,5 +1,6 @@ use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, + debugger_settings::DebuggerSettings, transport::{IoKind, LogKind}, }; use editor::{Editor, EditorEvent}; @@ -13,11 +14,13 @@ use gpui::{ View, ViewContext, VisualContext, WeakModel, WindowContext, }; use project::{search::SearchQuery, Project}; +use settings::Settings as _; use std::{ borrow::Cow, collections::{HashMap, VecDeque}, sync::Arc, }; +use util::maybe; use workspace::{ item::Item, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, @@ -260,9 +263,21 @@ impl LogStore { while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { log_lines.pop_front(); } - let entry: &str = message.as_ref(); - let entry = entry.to_string(); - log_lines.push_back(message); + + let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages; + + let entry = if format_messages { + maybe!({ + serde_json::to_string_pretty::( + &serde_json::from_str(&message).ok()?, + ) + .ok() + }) + .unwrap_or(message) + } else { + message + }; + log_lines.push_back(entry.clone()); cx.emit(Event::NewLogEntry { id, entry, kind }); } From 28c6012ff8164a0715cd10bca34ad35187ec491c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 16 Dec 2024 20:03:05 +0100 Subject: [PATCH 385/650] Update dap types to correctly fix other event case --- Cargo.lock | 2 +- crates/dap/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ec20bf4d0b51a..20febe5e655bc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3598,7 +3598,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=9aa5155d631bb1ac8faba5446244d98f855ff97f#9aa5155d631bb1ac8faba5446244d98f855ff97f" +source = "git+https://github.com/zed-industries/dap-types?rev=0f4da80b6f4713d268922556425b669627373b3e#0f4da80b6f4713d268922556425b669627373b3e" dependencies = [ "schemars", "serde", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index ff2d19f2187127..98b17426cfd03a 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -23,7 +23,7 @@ async-pipe = { workspace = true, optional = true } async-tar.workspace = true async-trait.workspace = true collections.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "9aa5155d631bb1ac8faba5446244d98f855ff97f" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "0f4da80b6f4713d268922556425b669627373b3e" } fs.workspace = true futures.workspace = true gpui.workspace = true From 3474750588ef92547b6b59b00e8fc010678bd265 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 17 Dec 2024 18:38:05 +0100 Subject: [PATCH 386/650] Fix don't panic when we receive a response after the request timeout was exceeded --- crates/dap/src/client.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d6b1403a5ba19e..c959e8473e668f 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -20,7 +20,7 @@ use std::{ }; use task::{DebugAdapterConfig, DebugRequestType}; -const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] @@ -119,7 +119,11 @@ impl DebugAdapterClient { cx.update(|cx| event_handler(Message::Event(ev), cx)) } Message::Request(req) => cx.update(|cx| event_handler(Message::Request(req), cx)), - Message::Response(_) => unreachable!(), + Message::Response(response) => { + log::debug!("Received response after request timeout: {:#?}", response); + + Ok(()) + } } { break Err(e); } From 473ebbb6c3893a8025ca7af020336f836a16bf39 Mon Sep 17 00:00:00 2001 From: Gaauwe Rombouts Date: Tue, 17 Dec 2024 23:21:32 +0100 Subject: [PATCH 387/650] Continue with successful variable requests (#75) --- crates/debugger_ui/src/variable_list.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 2fc2198d433b6b..3a3be6d6178d78 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -5,7 +5,6 @@ use editor::{ actions::{self, SelectAll}, Editor, EditorEvent, }; -use futures::future::try_join_all; use gpui::{ anchored, deferred, list, AnyElement, ClipboardItem, DismissEvent, FocusHandle, FocusableView, ListState, Model, MouseDownEvent, Point, Subscription, Task, View, @@ -451,7 +450,8 @@ impl VariableList { ); } - let result = try_join_all(tasks).await?; + let tasks_results = futures::future::join_all(tasks).await; + let result = tasks_results.into_iter().filter_map(|result| result.ok()); this.update(&mut cx, |this, cx| { let mut new_variables = BTreeMap::new(); From 7f4f7b056e8deb2a645e20f0a870ab4ed707553a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 18 Dec 2024 10:47:50 +0100 Subject: [PATCH 388/650] Add basic debug panel flow test (#74) * Add debug panel flow tests * Wip debug * Wip fix test Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> * WIP Get test_show_debug_panel to run while the test now runs without panicing dued to parked with nothing left to run the debug panel item is not being spawned * Get test_show_debug_panel to pass & clean up * Make clippy pass * Assert debug panel item is removed when client shutdown * Move send event back to dapstore --------- Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Co-authored-by: Anthony Eid --- Cargo.lock | 2 + crates/dap/src/transport.rs | 4 +- crates/dap_adapters/Cargo.toml | 1 + crates/debugger_ui/Cargo.toml | 12 ++ crates/debugger_ui/src/lib.rs | 3 + crates/debugger_ui/src/tests.rs | 1 + .../debugger_ui/src/tests/debugger_panel.rs | 180 ++++++++++++++++++ crates/project/Cargo.toml | 8 +- crates/project/src/dap_store.rs | 56 ++++++ 9 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 crates/debugger_ui/src/tests.rs create mode 100644 crates/debugger_ui/src/tests/debugger_panel.rs diff --git a/Cargo.lock b/Cargo.lock index 20febe5e655bc3..26a3454fd195d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3720,6 +3720,7 @@ dependencies = [ "command_palette_hooks", "dap", "editor", + "env_logger 0.11.5", "futures 0.3.31", "fuzzy", "gpui", @@ -3737,6 +3738,7 @@ dependencies = [ "terminal_view", "theme", "ui", + "unindent", "workspace", ] diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 249f15397d9423..e3d7315e9a4b56 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -809,6 +809,8 @@ impl Transport for FakeTransport { _binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result { + use serde_json::json; + let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); @@ -837,7 +839,7 @@ impl Transport for FakeTransport { { handle( request.seq, - request.arguments.unwrap(), + request.arguments.unwrap_or(json!({})), stdout_writer.clone(), ) .await; diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index a1bb17fb806aaa..1672050ed720ad 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -34,3 +34,4 @@ util.workspace = true [dev-dependencies] dap = { workspace = true, features = ["test-support"] } task = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 427881ef1af3f2..cc766339cd6399 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -8,6 +8,15 @@ license = "GPL-3.0-or-later" [lints] workspace = true +[features] +test-support = [ + "dap/test-support", + "editor/test-support", + "gpui/test-support", + "project/test-support", + "workspace/test-support", +] + [dependencies] anyhow.workspace = true collections.workspace = true @@ -34,7 +43,10 @@ ui.workspace = true workspace.workspace = true [dev-dependencies] +dap = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } +env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } +unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 7e05878aeb451f..aefe3b557bddbd 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -17,6 +17,9 @@ mod module_list; mod stack_frame_list; mod variable_list; +#[cfg(test)] +mod tests; + pub fn init(cx: &mut AppContext) { DebuggerSettings::register(cx); diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs new file mode 100644 index 00000000000000..4160835280b7f8 --- /dev/null +++ b/crates/debugger_ui/src/tests.rs @@ -0,0 +1 @@ +mod debugger_panel; diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs new file mode 100644 index 00000000000000..b68d79b634e4ed --- /dev/null +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -0,0 +1,180 @@ +use crate::*; +use dap::requests::{Disconnect, Initialize, Launch, StackTrace}; +use gpui::{BackgroundExecutor, Model, TestAppContext, VisualTestContext, WindowHandle}; +use project::{FakeFs, Project}; +use serde_json::json; +use settings::SettingsStore; +use unindent::Unindent as _; +use workspace::{dock::Panel, Workspace}; + +pub fn init_test(cx: &mut gpui::TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::try_init().ok(); + } + + cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(theme::LoadThemes::JustBase, cx); + command_palette_hooks::init(cx); + language::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + crate::init(cx); + editor::init(cx); + }); +} + +async fn add_debugger_panel( + project: &Model, + cx: &mut TestAppContext, +) -> WindowHandle { + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + let debugger_panel = window + .update(cx, |_, cx| cx.spawn(DebugPanel::load)) + .unwrap() + .await + .expect("Failed to load debug panel"); + + window + .update(cx, |workspace, cx| { + workspace.add_panel(debugger_panel, cx); + }) + .unwrap(); + window +} + +#[gpui::test] +async fn test_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let file_contents = r#" + // print goodbye + fn main() { + println!("goodbye world"); + } + "# + .unindent(); + + fs.insert_tree( + "/dir", + json!({ + "src": { + "main.rs": file_contents, + } + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + // assert we don't have a debug panel item yet + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }) + .unwrap(); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // assert we added a debug panel item + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), debug_panel_item.read(cx).client_id()); + assert_eq!(1, debug_panel_item.read(cx).thread_id()); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + // If we don't end session client will still be awaiting to recv messages + // from fake transport that will never be transmitted, thus resulting in + // a "panic: parked with nothing to run" + shutdown_client.await.unwrap(); + + // assert we don't have a debug panel item anymore because the client shutdown + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }) + .unwrap(); +} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index c19e85f5b9575f..85b7a68df20b5b 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -21,6 +21,8 @@ test-support = [ "prettier/test-support", "worktree/test-support", "gpui/test-support", + "dap/test-support", + "dap_adapters/test-support", ] [dependencies] @@ -78,17 +80,19 @@ fancy-regex.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } +dap = { workspace = true, features = ["test-support"] } +dap_adapters = { workspace = true, features = ["test-support"] } env_logger.workspace = true fs = { workspace = true, features = ["test-support"] } git2.workspace = true gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } -release_channel.workspace = true lsp = { workspace = true, features = ["test-support"] } prettier = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true -worktree = { workspace = true, features = ["test-support"] } +release_channel.workspace = true rpc = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } unindent.workspace = true util = { workspace = true, features = ["test-support"] } +worktree = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index c8b10308833479..508f234a8c123f 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -431,6 +431,62 @@ impl DapStore { }) } + #[cfg(any(test, feature = "test-support"))] + pub fn start_test_client( + &mut self, + config: DebugAdapterConfig, + cx: &mut ModelContext, + ) -> Task>> { + use task::DebugAdapterKind; + + let client_id = self.next_client_id(); + + cx.spawn(|this, mut cx| async move { + let adapter = build_adapter(&DebugAdapterKind::Fake).await?; + + let mut client = DebugAdapterClient::new( + client_id, + config, + adapter, + DebugAdapterBinary { + command: "command".into(), + arguments: None, + envs: None, + cwd: None, + }, + &cx, + ); + + client + .start( + { + let dap_store = this.clone(); + move |message, cx| { + dap_store + .update(cx, |_, cx| { + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + }) + .log_err(); + } + }, + &mut cx, + ) + .await?; + + let client = Arc::new(client); + + this.update(&mut cx, |store, cx| { + store + .clients + .insert(client_id, DebugAdapterClientState::Running(client.clone())); + + cx.emit(DapStoreEvent::DebugClientStarted(client_id)); + + client + }) + }) + } + pub fn start_client_from_debug_config( &mut self, config: DebugAdapterConfig, From 6ed42285d6595ed5cc32bea3d3520038bb860856 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 18 Dec 2024 14:12:37 +0100 Subject: [PATCH 389/650] Remove unused dep from debugger_ui crate --- Cargo.lock | 1 - crates/debugger_ui/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2968bfdd5e47e..c0b8a551574026 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3725,7 +3725,6 @@ dependencies = [ "gpui", "language", "menu", - "parking_lot", "picker", "project", "serde", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index cc766339cd6399..35e675cb93cadf 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -28,7 +28,6 @@ fuzzy.workspace = true gpui.workspace = true language.workspace = true menu.workspace = true -parking_lot.workspace = true picker.workspace = true project.workspace = true serde.workspace = true From e3cb3d143e709f4d9cb4809f7cbcb02988971d1a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 18 Dec 2024 14:36:16 +0100 Subject: [PATCH 390/650] Fix failing test --- Cargo.lock | 1 - crates/collab/src/tests/editor_tests.rs | 1 - crates/dap/Cargo.toml | 5 +++-- crates/dap/src/client.rs | 20 ++++++++++++++++---- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0b8a551574026..6cf722dbd97d63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3572,7 +3572,6 @@ dependencies = [ "async-tar", "async-trait", "collections", - "ctor", "dap-types", "env_logger 0.11.5", "fs", diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 4509ae3bee8f69..e39be89576c3f2 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2597,7 +2597,6 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte assert_eq!(0, breakpoints_a.len()); assert_eq!(breakpoints_a, breakpoints_b); - assert!(breakpoints_a.contains_key(&project_path)); } #[track_caller] diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 98b17426cfd03a..9dc7d72a32c53a 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -14,6 +14,7 @@ test-support = [ "util/test-support", "task/test-support", "async-pipe", + "settings/test-support", ] [dependencies] @@ -44,8 +45,8 @@ util.workspace = true [dev-dependencies] async-pipe.workspace = true -ctor.workspace = true env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } +settings = { workspace = true, features = ["test-support"] } task = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index c959e8473e668f..cb7a135069c3ac 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -270,24 +270,34 @@ impl DebugAdapterClient { #[cfg(test)] mod tests { use super::*; - use crate::{adapters::FakeAdapter, client::DebugAdapterClient}; + use crate::{ + adapters::FakeAdapter, client::DebugAdapterClient, debugger_settings::DebuggerSettings, + }; use dap_types::{ messages::Events, requests::Initialize, Capabilities, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, }; use gpui::TestAppContext; + use settings::{Settings, SettingsStore}; use std::sync::atomic::{AtomicBool, Ordering}; use task::DebugAdapterConfig; - #[ctor::ctor] - fn init_logger() { + pub fn init_test(cx: &mut gpui::TestAppContext) { if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); + env_logger::try_init().ok(); } + + cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + DebuggerSettings::register(cx); + }); } #[gpui::test] pub async fn test_initialize_client(cx: &mut TestAppContext) { + init_test(cx); + let adapter = Arc::new(FakeAdapter::new()); let mut client = DebugAdapterClient::new( @@ -365,6 +375,8 @@ mod tests { #[gpui::test] pub async fn test_calls_event_handler(cx: &mut TestAppContext) { + init_test(cx); + let adapter = Arc::new(FakeAdapter::new()); let was_called = Arc::new(AtomicBool::new(false)); let was_called_clone = was_called.clone(); From ab4973fbdb4684a39db8e3b9f4f9a3511f995cd5 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 18 Dec 2024 15:46:49 +0100 Subject: [PATCH 391/650] Add more tests for creating a debug panel item per client & thread --- crates/debugger_ui/src/debugger_panel.rs | 31 +- .../debugger_ui/src/tests/debugger_panel.rs | 344 +++++++++++++++++- 2 files changed, 359 insertions(+), 16 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 259d49ab10b1c3..cec26e630e715a 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -215,6 +215,23 @@ impl DebugPanel { .and_then(|panel| panel.downcast::()) } + pub fn debug_panel_item_by_client( + &self, + client_id: &DebugAdapterClientId, + thread_id: u64, + cx: &mut ViewContext, + ) -> Option> { + self.pane + .read(cx) + .items() + .filter_map(|item| item.downcast::()) + .find(|item| { + let item = item.read(cx); + + &item.client_id() == client_id && item.thread_id() == thread_id + }) + } + fn handle_pane_event( &mut self, _: View, @@ -547,18 +564,8 @@ impl DebugPanel { thread_state.status = ThreadStatus::Stopped; }); - let existing_item = this - .pane - .read(cx) - .items() - .filter_map(|item| item.downcast::()) - .any(|item| { - let item = item.read(cx); - - item.client_id() == client_id && item.thread_id() == thread_id - }); - - if !existing_item { + let existing_item = this.debug_panel_item_by_client(&client_id, thread_id, cx); + if existing_item.is_none() { let debug_panel = cx.view().clone(); this.pane.update(cx, |pane, cx| { let tab = cx.new_view(|cx| { diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index b68d79b634e4ed..3956c2591fc23e 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -46,7 +46,7 @@ async fn add_debugger_panel( } #[gpui::test] -async fn test_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) { +async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(executor.clone()); @@ -142,7 +142,7 @@ async fn test_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppCon workspace .update(cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); - let debug_panel_item = debug_panel + let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) .unwrap(); @@ -150,8 +150,344 @@ async fn test_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppCon 1, debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); - assert_eq!(client.id(), debug_panel_item.read(cx).client_id()); - assert_eq!(1, debug_panel_item.read(cx).thread_id()); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + // If we don't end session client will still be awaiting to recv messages + // from fake transport that will never be transmitted, thus resulting in + // a "panic: parked with nothing to run" + shutdown_client.await.unwrap(); + + // assert we don't have a debug panel item anymore because the client shutdown + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }) + .unwrap(); +} + +#[gpui::test] +async fn test_we_can_only_have_one_panel_per_debug_thread( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let file_contents = r#" + // print goodbye + fn main() { + println!("goodbye world"); + } + "# + .unindent(); + + fs.insert_tree( + "/dir", + json!({ + "src": { + "main.rs": file_contents, + } + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + // assert we don't have a debug panel item yet + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }) + .unwrap(); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // assert we added a debug panel item + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + }) + .unwrap(); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // assert we added a debug panel item + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + // If we don't end session client will still be awaiting to recv messages + // from fake transport that will never be transmitted, thus resulting in + // a "panic: parked with nothing to run" + shutdown_client.await.unwrap(); + + // assert we don't have a debug panel item anymore because the client shutdown + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }) + .unwrap(); +} + +#[gpui::test] +async fn test_client_can_open_multiple_thread_panels( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let file_contents = r#" + // print goodbye + fn main() { + println!("goodbye world"); + } + "# + .unindent(); + + fs.insert_tree( + "/dir", + json!({ + "src": { + "main.rs": file_contents, + } + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + // assert we don't have a debug panel item yet + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }) + .unwrap(); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // assert we added a debug panel item + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + }) + .unwrap(); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(2), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // assert we added a debug panel item and the new one is active + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 2, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(2, active_debug_panel_item.read(cx).thread_id()); }) .unwrap(); From 5fe2da3e72881b81b5179df4895b6511d5bf4281 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 18 Dec 2024 17:27:28 +0100 Subject: [PATCH 392/650] Minimize delay when clear entries and build entries --- crates/debugger_ui/src/variable_list.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 653e8c50277a71..5f11dcdc8f186c 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -178,8 +178,6 @@ impl VariableList { self.build_entries(true, true, cx); } StackFrameListEvent::StackFramesUpdated => { - self.entries.clear(); - self.fetch_variables(cx); } } @@ -472,6 +470,7 @@ impl VariableList { std::mem::swap(&mut this.variables, &mut new_variables); std::mem::swap(&mut this.scopes, &mut new_scopes); + this.entries.clear(); this.build_entries(true, true, cx); this.fetch_variables_task.take(); From 6aba39874c382a973fd97d0938d14cd822878c42 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 18 Dec 2024 17:28:36 +0100 Subject: [PATCH 393/650] Add test for stack frame list This covers the initial fetch and opening of the editor that belongs to the current stack frame (always the first one) --- crates/dap/src/client.rs | 4 + crates/debugger_ui/src/debugger_panel_item.rs | 5 + crates/debugger_ui/src/tests.rs | 46 ++++ .../debugger_ui/src/tests/debugger_panel.rs | 106 +-------- .../debugger_ui/src/tests/stack_frame_list.rs | 207 ++++++++++++++++++ 5 files changed, 268 insertions(+), 100 deletions(-) create mode 100644 crates/debugger_ui/src/tests/stack_frame_list.rs diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index cb7a135069c3ac..c2e5396bdf0210 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -20,6 +20,10 @@ use std::{ }; use task::{DebugAdapterConfig, DebugRequestType}; +#[cfg(debug_assertions)] +const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2); + +#[cfg(not(debug_assertions))] const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index b89ab445cd01e8..572d6cdaa00361 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -359,6 +359,11 @@ impl DebugPanelItem { self.thread_id } + #[cfg(any(test, feature = "test-support"))] + pub fn stack_frame_list(&self) -> &View { + &self.stack_frame_list + } + pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { self.dap_store .read_with(cx, |store, _| store.capabilities_by_id(&self.client_id)) diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 4160835280b7f8..4d21f8ee27b107 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -1 +1,47 @@ +use gpui::{Model, TestAppContext, WindowHandle}; +use project::Project; +use settings::SettingsStore; +use workspace::Workspace; + +use crate::debugger_panel::DebugPanel; + mod debugger_panel; +mod stack_frame_list; + +pub fn init_test(cx: &mut gpui::TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::try_init().ok(); + } + + cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(theme::LoadThemes::JustBase, cx); + command_palette_hooks::init(cx); + language::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + crate::init(cx); + editor::init(cx); + }); +} + +pub async fn add_debugger_panel( + project: &Model, + cx: &mut TestAppContext, +) -> WindowHandle { + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + let debugger_panel = window + .update(cx, |_, cx| cx.spawn(DebugPanel::load)) + .unwrap() + .await + .expect("Failed to load debug panel"); + + window + .update(cx, |workspace, cx| { + workspace.add_panel(debugger_panel, cx); + }) + .unwrap(); + window +} diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 3956c2591fc23e..ed2c668a0d7767 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1,49 +1,9 @@ use crate::*; use dap::requests::{Disconnect, Initialize, Launch, StackTrace}; -use gpui::{BackgroundExecutor, Model, TestAppContext, VisualTestContext, WindowHandle}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; -use serde_json::json; -use settings::SettingsStore; -use unindent::Unindent as _; -use workspace::{dock::Panel, Workspace}; - -pub fn init_test(cx: &mut gpui::TestAppContext) { - if std::env::var("RUST_LOG").is_ok() { - env_logger::try_init().ok(); - } - - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - theme::init(theme::LoadThemes::JustBase, cx); - command_palette_hooks::init(cx); - language::init(cx); - workspace::init_settings(cx); - Project::init_settings(cx); - crate::init(cx); - editor::init(cx); - }); -} - -async fn add_debugger_panel( - project: &Model, - cx: &mut TestAppContext, -) -> WindowHandle { - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - - let debugger_panel = window - .update(cx, |_, cx| cx.spawn(DebugPanel::load)) - .unwrap() - .await - .expect("Failed to load debug panel"); - - window - .update(cx, |workspace, cx| { - workspace.add_panel(debugger_panel, cx); - }) - .unwrap(); - window -} +use tests::{add_debugger_panel, init_test}; +use workspace::dock::Panel; #[gpui::test] async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) { @@ -51,25 +11,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let fs = FakeFs::new(executor.clone()); - let file_contents = r#" - // print goodbye - fn main() { - println!("goodbye world"); - } - "# - .unindent(); - - fs.insert_tree( - "/dir", - json!({ - "src": { - "main.rs": file_contents, - } - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [], cx).await; let workspace = add_debugger_panel(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -188,25 +130,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( let fs = FakeFs::new(executor.clone()); - let file_contents = r#" - // print goodbye - fn main() { - println!("goodbye world"); - } - "# - .unindent(); - - fs.insert_tree( - "/dir", - json!({ - "src": { - "main.rs": file_contents, - } - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [], cx).await; let workspace = add_debugger_panel(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -356,25 +280,7 @@ async fn test_client_can_open_multiple_thread_panels( let fs = FakeFs::new(executor.clone()); - let file_contents = r#" - // print goodbye - fn main() { - println!("goodbye world"); - } - "# - .unindent(); - - fs.insert_tree( - "/dir", - json!({ - "src": { - "main.rs": file_contents, - } - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [], cx).await; let workspace = add_debugger_panel(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs new file mode 100644 index 00000000000000..e4092597cbdaa6 --- /dev/null +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -0,0 +1,207 @@ +use crate::{ + debugger_panel::DebugPanel, + tests::{add_debugger_panel, init_test}, +}; +use dap::{ + requests::{Disconnect, Initialize, Launch, StackTrace}, + StackFrame, +}; +use editor::{Editor, ToPoint as _}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use project::{FakeFs, Project}; +use serde_json::json; +use std::sync::Arc; +use unindent::Unindent as _; + +#[gpui::test] +async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + import { SOME_VALUE } './module.js'; + + console.log(SOME_VALUE); + "# + .unindent(); + + let module_file_content = r#" + export SOME_VALUE = 'some value'; + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + "module.js": module_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![ + StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + StackFrame { + id: 2, + name: "Stack Frame 2".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + ]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + }); + + let editors = workspace.items_of_type::(cx).collect::>(); + assert_eq!(1, editors.len()); + + let project_path = editors[0] + .update(cx, |editor, cx| editor.project_path(cx)) + .unwrap(); + assert_eq!("src/test.js", project_path.path.to_string_lossy()); + assert_eq!(test_file_content, editors[0].read(cx).text(cx)); + assert_eq!( + vec![2..3], + editors[0].update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + editor + .highlighted_rows::() + .map(|(range, _)| { + let start = range.start.to_point(&snapshot.buffer_snapshot); + let end = range.end.to_point(&snapshot.buffer_snapshot); + start.row..end.row + }) + .collect::>() + }) + ); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + // If we don't end session client will still be awaiting to recv messages + // from fake transport that will never be transmitted, thus resulting in + // a "panic: parked with nothing to run" + shutdown_client.await.unwrap(); +} From d7fa7c208dc076d26b69dbbdb14258f4c05f56b0 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:45:21 -0500 Subject: [PATCH 394/650] WIP Implement Debugger Collab Functionality (#69) * Initial WIP for impl FollowableItem for DebugPanelItem * Implment DebuggerThreadState proto functions * Add Debug panel item variable list definition to zed.proto * Add more debug panel item messages to zed.proto * WIP * Fix compile errors Co-authored-by: Remco Smits * More WIP Co-authored-by: Remco Smits * Further WIP lol * Start working on fake adapter WIP Co-authored-by: Remco Smits Co-authored-by: Mikayla Maki * Merge with Remco's mock debug adapter client branch * Fix false positive clippy error This error was a match variant not being covered when the variant wasn't possible dued to a feature flag. I'm pretty sure this is a bug in clippy/rust-analyzer and will open an issue on their repos * Add todo to change in dap adapter downloads * WIP Get variable to send during variable list * Get variable list from/to_proto working Note: For some reason variable entries aren't rendering even though everything is being sent * Fix warning messages * Fix typo * Impl stack from list from/to_proto for debug panel item * Change order of set_from_proto for debug panel item * Impl Variable list variables to/from proto funcs * Start work on Set Debugger Panel Item event * WIP with remco Co-authored-by: Remco Smits * Get SetDebugPanelItem proto message sending and handled Co-authored-by: Remco Smits * Setup UpdateDebugAdapter collab message & send live stack frame list updates * Use proto enums instead of hardcoded integers * Use read instead of update * Send variable list update message each time we build the entries * Send stack frame message when we selected the active stackframe * Add more mappings * Remove debug and rename method to be more inline with others * Use the correct entries to reset * Add tests to validate we can go and from proto ScopeVariableIndex * Rename test * Create UpdateAdapter ModuleList variant WIP * Change proto conversion trait to have some types return Result enums * Get clippy to pass I removed some proto message we used in DebugPanelItem FollowableItem implmentation because they were causing clippy errors and will need to be change in the near future anyway --------- Co-authored-by: Remco Smits Co-authored-by: Mikayla Maki --- Cargo.lock | 4 + crates/collab/src/rpc.rs | 4 +- crates/collab/src/tests.rs | 1 + crates/collab/src/tests/debug_panel_tests.rs | 90 ++++ crates/dap/Cargo.toml | 1 + crates/dap/src/adapters.rs | 6 +- crates/dap/src/client.rs | 10 + crates/dap/src/lib.rs | 4 +- crates/dap/src/proto_conversions.rs | 333 ++++++++++++++ crates/dap_adapters/src/python.rs | 2 + crates/debugger_ui/Cargo.toml | 5 + crates/debugger_ui/src/debugger_panel.rs | 216 ++++++++- crates/debugger_ui/src/debugger_panel_item.rs | 211 +++++++-- crates/debugger_ui/src/lib.rs | 5 + crates/debugger_ui/src/module_list.rs | 44 +- crates/debugger_ui/src/stack_frame_list.rs | 43 ++ crates/debugger_ui/src/variable_list.rs | 430 +++++++++++++++++- crates/project/src/dap_store.rs | 45 ++ crates/project/src/project.rs | 4 +- crates/proto/proto/zed.proto | 263 ++++++++++- crates/proto/src/proto.rs | 4 + crates/workspace/src/workspace.rs | 1 - 22 files changed, 1666 insertions(+), 60 deletions(-) create mode 100644 crates/collab/src/tests/debug_panel_tests.rs create mode 100644 crates/dap/src/proto_conversions.rs diff --git a/Cargo.lock b/Cargo.lock index 6cf722dbd97d63..e369c2af7536fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3571,6 +3571,7 @@ dependencies = [ "async-pipe", "async-tar", "async-trait", + "client", "collections", "dap-types", "env_logger 0.11.5", @@ -3714,6 +3715,7 @@ name = "debugger_ui" version = "0.1.0" dependencies = [ "anyhow", + "client", "collections", "command_palette_hooks", "dap", @@ -3726,6 +3728,7 @@ dependencies = [ "menu", "picker", "project", + "rpc", "serde", "serde_json", "settings", @@ -3736,6 +3739,7 @@ dependencies = [ "theme", "ui", "unindent", + "util", "workspace", ] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b68b456f0bcfb4..1736506fb93eaf 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -416,7 +416,9 @@ impl Server { .add_message_handler(broadcast_project_message_from_host::) .add_message_handler( broadcast_project_message_from_host::, - ); + ) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::); Arc::new(server) } diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 2ce69efc9b4069..4dbee3032b66af 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -11,6 +11,7 @@ mod channel_buffer_tests; mod channel_guest_tests; mod channel_message_tests; mod channel_tests; +mod debug_panel_tests; mod editor_tests; mod following_tests; mod integration_tests; diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs new file mode 100644 index 00000000000000..16a59de63b5230 --- /dev/null +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -0,0 +1,90 @@ +use call::ActiveCall; +use editor::Editor; +use gpui::TestAppContext; +use serde_json::json; + +use super::TestServer; + +#[gpui::test] +async fn test_debug_panel_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + // TODO: Make these good files for debugging + json!({ + "test.txt": "one\ntwo\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Client A opens an editor. + let _pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "test.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let peer_id_a = client_a.peer_id().unwrap(); + + // Client B follows A + workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + + let _editor_b2 = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + + // Start a fake debugging session in a (see: other tests which setup fake language servers for a model) + // Add a breakpoint + editor_a.update(cx_a, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.select_right(&editor::actions::SelectRight, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + // Start debugging + + // TODO: + // 2. Sanity check: make sure a looks right + // 3. Check that b looks right +} diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 9dc7d72a32c53a..0a4dd1b349a98e 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -23,6 +23,7 @@ async-compression.workspace = true async-pipe = { workspace = true, optional = true } async-tar.workspace = true async-trait.workspace = true +client.workspace = true collections.workspace = true dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "0f4da80b6f4713d268922556425b669627373b3e" } fs.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index b9a8010abd0870..bb70be285f1395 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -2,7 +2,7 @@ use crate::transport::FakeTransport; use crate::transport::Transport; use ::fs::Fs; -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{anyhow, Context as _, Ok, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; @@ -365,8 +365,6 @@ impl DebugAdapter for FakeAdapter { } fn request_args(&self, _config: &DebugAdapterConfig) -> Value { - use serde_json::json; - - json!({}) + Value::Null } } diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index c2e5396bdf0210..d269348a2b1b1c 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -30,6 +30,16 @@ const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); #[repr(transparent)] pub struct DebugAdapterClientId(pub usize); +impl DebugAdapterClientId { + pub fn from_proto(client_id: u64) -> Self { + Self(client_id as usize) + } + + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } +} + pub struct DebugAdapterClient { id: DebugAdapterClientId, sequence_count: AtomicU64, diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index b7a48be87b4d4f..19eacd6dfd0379 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -1,8 +1,10 @@ pub mod adapters; pub mod client; +pub mod debugger_settings; +pub mod proto_conversions; pub mod transport; + pub use dap_types::*; -pub mod debugger_settings; #[cfg(any(test, feature = "test-support"))] pub use adapters::FakeAdapter; diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs new file mode 100644 index 00000000000000..0ccd4fe1ac5542 --- /dev/null +++ b/crates/dap/src/proto_conversions.rs @@ -0,0 +1,333 @@ +use anyhow::{anyhow, Result}; +use client::proto::{ + self, DapChecksum, DapChecksumAlgorithm, DapModule, DapScope, DapScopePresentationHint, + DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable, +}; +use dap_types::{ScopePresentationHint, Source}; + +pub trait ProtoConversion { + type ProtoType; + type Output; + + fn to_proto(&self) -> Self::ProtoType; + fn from_proto(payload: Self::ProtoType) -> Self::Output; +} + +impl ProtoConversion for Vec +where + T: ProtoConversion, +{ + type ProtoType = Vec; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + self.iter().map(|item| item.to_proto()).collect() + } + + fn from_proto(payload: Self::ProtoType) -> Self { + payload + .into_iter() + .map(|item| T::from_proto(item)) + .collect() + } +} + +impl ProtoConversion for dap_types::Scope { + type ProtoType = DapScope; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + Self::ProtoType { + name: self.name.clone(), + presentation_hint: self + .presentation_hint + .as_ref() + .map(|hint| hint.to_proto().into()), + variables_reference: self.variables_reference, + named_variables: self.named_variables, + indexed_variables: self.indexed_variables, + expensive: self.expensive, + source: self.source.as_ref().map(Source::to_proto), + line: self.line, + end_line: self.end_line, + column: self.column, + end_column: self.end_column, + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + let presentation_hint = payload + .presentation_hint + .and_then(DapScopePresentationHint::from_i32); + Self { + name: payload.name, + presentation_hint: presentation_hint.map(ScopePresentationHint::from_proto), + variables_reference: payload.variables_reference, + named_variables: payload.named_variables, + indexed_variables: payload.indexed_variables, + expensive: payload.expensive, + source: payload.source.map(dap_types::Source::from_proto), + line: payload.line, + end_line: payload.end_line, + column: payload.column, + end_column: payload.end_column, + } + } +} + +impl ProtoConversion for dap_types::Variable { + type ProtoType = DapVariable; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + Self::ProtoType { + name: self.name.clone(), + value: self.value.clone(), + r#type: self.type_.clone(), + evaluate_name: self.evaluate_name.clone(), + variables_reference: self.variables_reference, + named_variables: self.named_variables, + indexed_variables: self.indexed_variables, + memory_reference: self.memory_reference.clone(), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + Self { + name: payload.name, + value: payload.value, + type_: payload.r#type, + evaluate_name: payload.evaluate_name, + presentation_hint: None, // TODO Debugger Collab Add this + variables_reference: payload.variables_reference, + named_variables: payload.named_variables, + indexed_variables: payload.indexed_variables, + memory_reference: payload.memory_reference, + } + } +} + +impl ProtoConversion for dap_types::ScopePresentationHint { + type ProtoType = DapScopePresentationHint; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + match self { + dap_types::ScopePresentationHint::Locals => DapScopePresentationHint::Locals, + dap_types::ScopePresentationHint::Arguments => DapScopePresentationHint::Arguments, + dap_types::ScopePresentationHint::Registers => DapScopePresentationHint::Registers, + dap_types::ScopePresentationHint::ReturnValue => DapScopePresentationHint::ReturnValue, + dap_types::ScopePresentationHint::Unknown => DapScopePresentationHint::ScopeUnknown, + &_ => unreachable!(), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + match payload { + DapScopePresentationHint::Locals => dap_types::ScopePresentationHint::Locals, + DapScopePresentationHint::Arguments => dap_types::ScopePresentationHint::Arguments, + DapScopePresentationHint::Registers => dap_types::ScopePresentationHint::Registers, + DapScopePresentationHint::ReturnValue => dap_types::ScopePresentationHint::ReturnValue, + DapScopePresentationHint::ScopeUnknown => dap_types::ScopePresentationHint::Unknown, + } + } +} + +impl ProtoConversion for dap_types::SourcePresentationHint { + type ProtoType = DapSourcePresentationHint; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + match self { + dap_types::SourcePresentationHint::Normal => DapSourcePresentationHint::SourceNormal, + dap_types::SourcePresentationHint::Emphasize => DapSourcePresentationHint::Emphasize, + dap_types::SourcePresentationHint::Deemphasize => { + DapSourcePresentationHint::Deemphasize + } + dap_types::SourcePresentationHint::Unknown => DapSourcePresentationHint::SourceUnknown, + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + match payload { + DapSourcePresentationHint::SourceNormal => dap_types::SourcePresentationHint::Normal, + DapSourcePresentationHint::Emphasize => dap_types::SourcePresentationHint::Emphasize, + DapSourcePresentationHint::Deemphasize => { + dap_types::SourcePresentationHint::Deemphasize + } + DapSourcePresentationHint::SourceUnknown => dap_types::SourcePresentationHint::Unknown, + } + } +} + +impl ProtoConversion for dap_types::Checksum { + type ProtoType = DapChecksum; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + DapChecksum { + algorithm: self.algorithm.to_proto().into(), + checksum: self.checksum.clone(), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + Self { + algorithm: dap_types::ChecksumAlgorithm::from_proto(payload.algorithm()), + checksum: payload.checksum, + } + } +} + +impl ProtoConversion for dap_types::ChecksumAlgorithm { + type ProtoType = DapChecksumAlgorithm; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + match self { + dap_types::ChecksumAlgorithm::Md5 => DapChecksumAlgorithm::Md5, + dap_types::ChecksumAlgorithm::Sha1 => DapChecksumAlgorithm::Sha1, + dap_types::ChecksumAlgorithm::Sha256 => DapChecksumAlgorithm::Sha256, + dap_types::ChecksumAlgorithm::Timestamp => DapChecksumAlgorithm::Timestamp, + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + match payload { + DapChecksumAlgorithm::Md5 => dap_types::ChecksumAlgorithm::Md5, + DapChecksumAlgorithm::Sha1 => dap_types::ChecksumAlgorithm::Sha1, + DapChecksumAlgorithm::Sha256 => dap_types::ChecksumAlgorithm::Sha256, + DapChecksumAlgorithm::Timestamp => dap_types::ChecksumAlgorithm::Timestamp, + DapChecksumAlgorithm::ChecksumAlgorithmUnspecified => unreachable!(), + } + } +} + +impl ProtoConversion for dap_types::Source { + type ProtoType = DapSource; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + Self::ProtoType { + name: self.name.clone(), + path: self.path.clone(), + source_reference: self.source_reference, + presentation_hint: self.presentation_hint.map(|hint| hint.to_proto().into()), + origin: self.origin.clone(), + sources: self + .sources + .clone() + .map(|src| src.to_proto()) + .unwrap_or_default(), + adapter_data: Default::default(), // TODO Debugger Collab + checksums: self + .checksums + .clone() + .map(|c| c.to_proto()) + .unwrap_or_default(), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + Self { + name: payload.name.clone(), + path: payload.path.clone(), + source_reference: payload.source_reference, + presentation_hint: payload + .presentation_hint + .and_then(DapSourcePresentationHint::from_i32) + .map(dap_types::SourcePresentationHint::from_proto), + origin: payload.origin.clone(), + sources: Some(Vec::::from_proto(payload.sources)), + checksums: Some(Vec::::from_proto(payload.checksums)), + adapter_data: None, // TODO Debugger Collab + } + } +} + +impl ProtoConversion for dap_types::StackFrame { + type ProtoType = DapStackFrame; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + Self::ProtoType { + id: self.id, + name: self.name.clone(), + source: self.source.as_ref().map(|src| src.to_proto()), + line: self.line, + column: self.column, + end_line: self.end_line, + end_column: self.end_column, + can_restart: self.can_restart, + instruction_pointer_reference: self.instruction_pointer_reference.clone(), + module_id: None, // TODO Debugger Collab + presentation_hint: None, // TODO Debugger Collab + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + Self { + id: payload.id, + name: payload.name, + source: payload.source.map(dap_types::Source::from_proto), + line: payload.line, + column: payload.column, + end_line: payload.end_line, + end_column: payload.end_column, + can_restart: payload.can_restart, + instruction_pointer_reference: payload.instruction_pointer_reference, + module_id: None, // TODO Debugger Collab + presentation_hint: None, // TODO Debugger Collab + } + } +} + +impl ProtoConversion for dap_types::Module { + type ProtoType = DapModule; + type Output = Result; + + fn to_proto(&self) -> Self::ProtoType { + let id = match &self.id { + dap_types::ModuleId::Number(num) => proto::dap_module_id::Id::Number(*num), + dap_types::ModuleId::String(string) => proto::dap_module_id::Id::String(string.clone()), + }; + + DapModule { + id: Some(proto::DapModuleId { id: Some(id) }), + name: self.name.clone(), + path: self.path.clone(), + is_optimized: self.is_optimized, + is_user_code: self.is_user_code, + version: self.version.clone(), + symbol_status: self.symbol_status.clone(), + symbol_file_path: self.symbol_file_path.clone(), + date_time_stamp: self.date_time_stamp.clone(), + address_range: self.address_range.clone(), + } + } + + fn from_proto(payload: Self::ProtoType) -> Result { + let id = match payload + .id + .ok_or(anyhow!("All DapModule proto messages must have an id"))? + .id + .ok_or(anyhow!("All DapModuleID proto messages must have an id"))? + { + proto::dap_module_id::Id::String(string) => dap_types::ModuleId::String(string), + proto::dap_module_id::Id::Number(num) => dap_types::ModuleId::Number(num), + }; + + Ok(Self { + id, + name: payload.name, + path: payload.path, + is_optimized: payload.is_optimized, + is_user_code: payload.is_user_code, + version: payload.version, + symbol_status: payload.symbol_status, + symbol_file_path: payload.symbol_file_path, + date_time_stamp: payload.date_time_stamp, + address_range: payload.address_range, + }) + } +} diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index eb5118eb79eb0c..d930383c1531f9 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -64,6 +64,8 @@ impl DebugAdapter for PythonDebugAdapter { }) .await { + // TODO Debugger: Rename folder instead of moving all files to another folder + // We're doing uncessary IO work right now util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path()) .await?; } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 35e675cb93cadf..196976c9247208 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -14,11 +14,13 @@ test-support = [ "editor/test-support", "gpui/test-support", "project/test-support", + "util/test-support", "workspace/test-support", ] [dependencies] anyhow.workspace = true +client.workspace = true collections.workspace = true command_palette_hooks.workspace = true dap.workspace = true @@ -30,6 +32,7 @@ language.workspace = true menu.workspace = true picker.workspace = true project.workspace = true +rpc.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true @@ -39,6 +42,7 @@ tasks_ui.workspace = true terminal_view.workspace = true theme.workspace = true ui.workspace = true +util.workspace = true workspace.workspace = true [dev-dependencies] @@ -47,5 +51,6 @@ editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index cec26e630e715a..070e2edffa0f95 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,13 +1,13 @@ -use crate::attach_modal::AttachModal; -use crate::debugger_panel_item::DebugPanelItem; +use crate::{attach_modal::AttachModal, debugger_panel_item::DebugPanelItem}; use anyhow::Result; +use client::proto; use collections::{BTreeMap, HashMap}; use command_palette_hooks::CommandPaletteFilter; -use dap::client::DebugAdapterClientId; -use dap::debugger_settings::DebuggerSettings; -use dap::messages::{Events, Message}; -use dap::requests::{Request, RunInTerminal, StartDebugging}; use dap::{ + client::DebugAdapterClientId, + debugger_settings::DebuggerSettings, + messages::{Events, Message}, + requests::{Request, RunInTerminal, StartDebugging}, Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, RunInTerminalRequestArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, @@ -16,24 +16,18 @@ use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, FontWeight, Model, Subscription, Task, View, ViewContext, WeakView, }; -use project::dap_store::DapStore; -use project::terminals::TerminalKind; +use project::{dap_store::DapStore, terminals::TerminalKind}; +use rpc::proto::{SetDebuggerPanelItem, UpdateDebugAdapter}; use serde_json::Value; use settings::Settings; -use std::any::TypeId; -use std::collections::VecDeque; -use std::path::PathBuf; -use std::u64; +use std::{any::TypeId, collections::VecDeque, path::PathBuf, u64}; use task::DebugRequestType; use terminal_view::terminal_panel::TerminalPanel; use ui::prelude::*; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - Workspace, -}; -use workspace::{ pane, Continue, Disconnect, Pane, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, - ToggleIgnoreBreakpoints, + ToggleIgnoreBreakpoints, Workspace, }; pub enum DebugPanelEvent { @@ -63,6 +57,26 @@ pub struct ThreadState { pub stopped: bool, } +impl ThreadState { + pub fn from_proto(thread_state: proto::DebuggerThreadState) -> Self { + let status = ThreadStatus::from_proto(thread_state.thread_status()); + + Self { + status, + stopped: thread_state.stopped, + } + } + + pub fn to_proto(&self) -> proto::DebuggerThreadState { + let status = self.status.to_proto(); + + proto::DebuggerThreadState { + thread_status: status, + stopped: self.stopped, + } + } +} + #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ThreadStatus { #[default] @@ -72,6 +86,26 @@ pub enum ThreadStatus { Ended, } +impl ThreadStatus { + pub fn from_proto(status: proto::DebuggerThreadStatus) -> Self { + match status { + proto::DebuggerThreadStatus::Running => Self::Running, + proto::DebuggerThreadStatus::Stopped => Self::Stopped, + proto::DebuggerThreadStatus::Exited => Self::Exited, + proto::DebuggerThreadStatus::Ended => Self::Ended, + } + } + + pub fn to_proto(&self) -> i32 { + match self { + Self::Running => proto::DebuggerThreadStatus::Running.into(), + Self::Stopped => proto::DebuggerThreadStatus::Stopped.into(), + Self::Exited => proto::DebuggerThreadStatus::Exited.into(), + Self::Ended => proto::DebuggerThreadStatus::Ended.into(), + } + } +} + pub struct DebugPanel { size: Pixels, pane: View, @@ -106,10 +140,12 @@ impl DebugPanel { }); let project = workspace.project().clone(); + let dap_store = project.read(cx).dap_store(); let _subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe(&pane, Self::handle_pane_event), + cx.subscribe(&dap_store, Self::on_dap_store_event), cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { project::Event::DebugClientStarted(client_id) => { @@ -147,6 +183,9 @@ impl DebugPanel { cx.notify(); } + project::Event::SetDebugClient(set_debug_client) => { + let _res = this.handle_set_debug_panel_item(set_debug_client, cx); + } _ => {} } }), @@ -549,6 +588,8 @@ impl DebugPanel { let client_id = *client_id; + let client_name = SharedString::from(client_kind.display_name().to_string()); + cx.spawn({ let event = event.clone(); |this, mut cx| async move { @@ -575,7 +616,7 @@ impl DebugPanel { this.dap_store.clone(), thread_state.clone(), &client_id, - &client_kind, + client_name, thread_id, cx, ) @@ -702,6 +743,100 @@ impl DebugPanel { cx.emit(DebugPanelEvent::Output((*client_id, event.clone()))); } + fn on_dap_store_event( + &mut self, + _: Model, + event: &project::dap_store::DapStoreEvent, + cx: &mut ViewContext, + ) { + //handle the even + match event { + project::dap_store::DapStoreEvent::SetDebugPanelItem(set_debug_panel_item) => { + self.handle_set_debug_panel_item(set_debug_panel_item, cx); + } + project::dap_store::DapStoreEvent::UpdateDebugAdapter(debug_adapter_update) => { + self.handle_debug_adapter_update(debug_adapter_update, cx); + } + _ => {} + } + } + + pub(crate) fn handle_debug_adapter_update( + &mut self, + update: &UpdateDebugAdapter, + cx: &mut ViewContext, + ) { + let client_id = DebugAdapterClientId::from_proto(update.client_id); + let thread_id = update.thread_id; + + let existing_item = self + .pane + .read(cx) + .items() + .filter_map(|item| item.downcast::()) + .find(|item| { + let item = item.read(cx); + + item.client_id() == client_id + && thread_id.map(|id| id == item.thread_id()).unwrap_or(true) + }); + + if let Some(debug_panel_item) = existing_item { + debug_panel_item.update(cx, |this, cx| { + this.update_adapter(update, cx); + }); + } + } + + pub(crate) fn handle_set_debug_panel_item( + &mut self, + payload: &SetDebuggerPanelItem, + cx: &mut ViewContext, + ) { + let client_id = DebugAdapterClientId::from_proto(payload.client_id); + let thread_id = payload.thread_id; + let thread_state = payload.thread_state.clone().unwrap(); + let thread_state = cx.new_model(|_| ThreadState::from_proto(thread_state)); + + let mut existing_item = self + .pane + .read(cx) + .items() + .filter_map(|item| item.downcast::()) + .find(|item| { + let item = item.read(cx); + + item.client_id() == client_id && item.thread_id() == thread_id + }); + + let debug_panel_item = existing_item.get_or_insert_with(|| { + let debug_panel = cx.view().clone(); + let debug_panel_item = self.pane.update(cx, |pane, cx| { + let debug_panel_item = cx.new_view(|cx| { + DebugPanelItem::new( + debug_panel, + self.workspace.clone(), + self.dap_store.clone(), + thread_state, + &client_id, + payload.client_name.clone().into(), + thread_id, + cx, + ) + }); + + pane.add_item(Box::new(debug_panel_item.clone()), true, true, None, cx); + debug_panel_item + }); + + debug_panel_item + }); + + debug_panel_item.update(cx, |this, cx| { + this.from_proto(payload, cx); + }); + } + fn handle_module_event( &mut self, client_id: &DebugAdapterClientId, @@ -733,6 +868,49 @@ impl DebugPanel { cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id)); } + pub fn open_remote_debug_panel_item( + &self, + client_id: DebugAdapterClientId, + thread_id: u64, + cx: &mut ViewContext, + ) -> View { + let existing_item = self.pane.read(cx).items().find_map(|item| { + let item = item.downcast::()?; + let item_ref = item.read(cx); + + if item_ref.client_id() == client_id && item_ref.thread_id() == thread_id { + Some(item) + } else { + None + } + }); + + if let Some(existing_item) = existing_item { + return existing_item; + } + + let debug_panel = cx.view().clone(); + + let debug_panel_item = cx.new_view(|cx| { + DebugPanelItem::new( + debug_panel, + self.workspace.clone(), + self.dap_store.clone(), + cx.new_model(|_| Default::default()), // change this + &client_id, + SharedString::from("test"), // change this + thread_id, + cx, + ) + }); + + self.pane.update(cx, |pane, cx| { + pane.add_item(Box::new(debug_panel_item.clone()), true, true, None, cx); + }); + + debug_panel_item + } + fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement { const TITLE: &'static str = "Debug session exited without hitting any breakpoints"; const DESCRIPTION: &'static str = @@ -813,6 +991,10 @@ impl Panel for DebugPanel { self.size = size.unwrap(); } + fn remote_id() -> Option { + Some(proto::PanelId::DebugPanel) + } + fn icon(&self, _cx: &WindowContext) -> Option { Some(IconName::Debug) } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 572d6cdaa00361..310421a54a492a 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -5,24 +5,25 @@ use crate::module_list::ModuleList; use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; -use dap::client::DebugAdapterClientId; -use dap::debugger_settings::DebuggerSettings; use dap::{ - Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, - StoppedEvent, ThreadEvent, + client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, + ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, + ThreadEvent, }; use editor::Editor; use gpui::{ - AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, View, - WeakView, + AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, + View, WeakView, }; use project::dap_store::DapStore; +use rpc::proto::{self, PeerId, SetDebuggerPanelItem, UpdateDebugAdapter}; use settings::Settings; -use task::DebugAdapterKind; -use ui::{prelude::*, Tooltip}; -use ui::{Indicator, WindowContext}; -use workspace::item::{Item, ItemEvent}; -use workspace::{ItemHandle, Workspace}; +use ui::{prelude::*, Indicator, Tooltip, WindowContext}; +use util::ResultExt as _; +use workspace::{ + item::{self, Item, ItemEvent}, + FollowableItem, ItemHandle, ViewId, Workspace, +}; #[derive(Debug)] pub enum DebugPanelItemEvent { @@ -39,15 +40,38 @@ enum ThreadItem { Variables, } +impl ThreadItem { + fn to_proto(&self) -> proto::DebuggerThreadItem { + match self { + ThreadItem::Console => proto::DebuggerThreadItem::Console, + ThreadItem::LoadedSource => proto::DebuggerThreadItem::LoadedSource, + ThreadItem::Modules => proto::DebuggerThreadItem::Modules, + ThreadItem::Output => proto::DebuggerThreadItem::Output, + ThreadItem::Variables => proto::DebuggerThreadItem::Variables, + } + } + + fn from_proto(active_thread_item: proto::DebuggerThreadItem) -> Self { + match active_thread_item { + proto::DebuggerThreadItem::Console => ThreadItem::Console, + proto::DebuggerThreadItem::LoadedSource => ThreadItem::LoadedSource, + proto::DebuggerThreadItem::Modules => ThreadItem::Modules, + proto::DebuggerThreadItem::Output => ThreadItem::Output, + proto::DebuggerThreadItem::Variables => ThreadItem::Variables, + } + } +} + pub struct DebugPanelItem { thread_id: u64, + remote_id: Option, console: View, show_console_indicator: bool, focus_handle: FocusHandle, dap_store: Model, output_editor: View, module_list: View, - client_kind: DebugAdapterKind, + client_name: SharedString, active_thread_item: ThreadItem, workspace: WeakView, client_id: DebugAdapterClientId, @@ -66,7 +90,7 @@ impl DebugPanelItem { dap_store: Model, thread_state: Model, client_id: &DebugAdapterClientId, - client_kind: &DebugAdapterKind, + client_name: SharedString, thread_id: u64, cx: &mut ViewContext, ) -> Self { @@ -157,24 +181,68 @@ impl DebugPanelItem { Self { console, - show_console_indicator: false, thread_id, dap_store, workspace, + client_name, module_list, thread_state, focus_handle, output_editor, variable_list, _subscriptions, + remote_id: None, stack_frame_list, loaded_source_list, client_id: *client_id, - client_kind: client_kind.clone(), + show_console_indicator: false, active_thread_item: ThreadItem::Variables, } } + pub(crate) fn to_proto(&self, cx: &ViewContext, project_id: u64) -> SetDebuggerPanelItem { + let thread_state = Some(self.thread_state.read_with(cx, |this, _| this.to_proto())); + let module_list = Some(self.module_list.read(cx).to_proto()); + let variable_list = Some(self.variable_list.read(cx).to_proto()); + let stack_frame_list = Some(self.stack_frame_list.read(cx).to_proto()); + + SetDebuggerPanelItem { + project_id, + client_id: self.client_id.to_proto(), + thread_id: self.thread_id, + console: None, + module_list, + active_thread_item: self.active_thread_item.to_proto().into(), + thread_state, + variable_list, + stack_frame_list, + loaded_source_list: None, + client_name: self.client_name.to_string(), + } + } + + pub(crate) fn from_proto(&mut self, state: &SetDebuggerPanelItem, cx: &mut ViewContext) { + self.active_thread_item = ThreadItem::from_proto(state.active_thread_item()); + + if let Some(stack_frame_list) = state.stack_frame_list.as_ref() { + self.stack_frame_list.update(cx, |this, cx| { + this.set_from_proto(stack_frame_list.clone(), cx); + }); + } + + if let Some(variable_list_state) = state.variable_list.as_ref() { + self.variable_list + .update(cx, |this, cx| this.set_from_proto(variable_list_state, cx)); + } + + if let Some(module_list_state) = state.module_list.as_ref() { + self.module_list + .update(cx, |this, cx| this.set_from_proto(module_list_state, cx)); + } + + cx.notify(); + } + pub fn update_thread_state_status(&mut self, status: ThreadStatus, cx: &mut ViewContext) { self.thread_state.update(cx, |thread_state, cx| { thread_state.status = status; @@ -219,6 +287,12 @@ impl DebugPanelItem { } cx.emit(DebugPanelItemEvent::Stopped { go_to_stack_frame }); + + if let Some((downstream_client, project_id)) = self.dap_store.read(cx).downstream_client() { + downstream_client + .send(self.to_proto(cx, *project_id)) + .log_err(); + } } fn handle_thread_event( @@ -351,6 +425,35 @@ impl DebugPanelItem { cx.notify(); } + pub(crate) fn update_adapter( + &mut self, + update: &UpdateDebugAdapter, + cx: &mut ViewContext, + ) { + if let Some(update_variant) = update.variant.as_ref() { + match update_variant { + proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => { + self.stack_frame_list.update(cx, |this, cx| { + this.set_from_proto(stack_frame_list.clone(), cx) + }) + } + proto::update_debug_adapter::Variant::ThreadState(thread_state) => { + self.thread_state.update(cx, |this, _| { + *this = ThreadState::from_proto(thread_state.clone()); + }) + } + proto::update_debug_adapter::Variant::VariableList(variable_list) => self + .variable_list + .update(cx, |this, cx| this.set_from_proto(variable_list, cx)), + proto::update_debug_adapter::Variant::Modules(module_list) => { + self.module_list.update(cx, |this, cx| { + this.set_from_proto(module_list, cx); + }) + } + } + } + } + pub fn client_id(&self) -> DebugAdapterClientId { self.client_id } @@ -546,23 +649,19 @@ impl Item for DebugPanelItem { params: workspace::item::TabContentParams, _: &WindowContext, ) -> AnyElement { - Label::new(format!( - "{} - Thread {}", - self.client_kind.display_name(), - self.thread_id - )) - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() + Label::new(format!("{} - Thread {}", self.client_name, self.thread_id)) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(SharedString::from(format!( "{} Thread {} - {:?}", - self.client_kind.display_name(), + self.client_name, self.thread_id, self.thread_state.read(cx).status, ))) @@ -576,6 +675,64 @@ impl Item for DebugPanelItem { } } +impl FollowableItem for DebugPanelItem { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, _cx: &WindowContext) -> Option { + None + } + + fn from_state_proto( + _workspace: View, + _remote_id: ViewId, + _state: &mut Option, + _cx: &mut WindowContext, + ) -> Option>>> { + None + } + + fn add_event_to_update_proto( + &self, + _event: &Self::Event, + _update: &mut Option, + _cx: &WindowContext, + ) -> bool { + // update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default())); + + true + } + + fn apply_update_proto( + &mut self, + _project: &Model, + _message: proto::update_view::Variant, + _cx: &mut ViewContext, + ) -> gpui::Task> { + Task::ready(Ok(())) + } + + fn set_leader_peer_id(&mut self, _leader_peer_id: Option, _cx: &mut ViewContext) { + } + + fn to_follow_event(_event: &Self::Event) -> Option { + None + } + + fn dedup(&self, existing: &Self, _cx: &WindowContext) -> Option { + if existing.client_id == self.client_id && existing.thread_id == self.thread_id { + Some(item::Dedup::KeepExisting) + } else { + None + } + } + + fn is_project_item(&self, _cx: &WindowContext) -> bool { + true + } +} + impl Render for DebugPanelItem { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let thread_status = self.thread_state.read(cx).status; diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 76761606a2ce95..51009adf59dcf0 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,5 +1,6 @@ use dap::debugger_settings::DebuggerSettings; use debugger_panel::{DebugPanel, ToggleFocus}; +use debugger_panel_item::DebugPanelItem; use gpui::AppContext; use settings::Settings; use ui::ViewContext; @@ -22,6 +23,10 @@ mod tests; pub fn init(cx: &mut AppContext) { DebuggerSettings::register(cx); + workspace::FollowableViewRegistry::register::(cx); + + // let client: AnyProtoClient = client.clone().into(); + // client.add_model_message_handler(DebugPanel::handle_set_debug_panel_item); cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 5973cfd7682b53..0ab25734795425 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,8 +1,10 @@ use anyhow::Result; -use dap::{client::DebugAdapterClientId, Module, ModuleEvent}; +use dap::{client::DebugAdapterClientId, proto_conversions::ProtoConversion, Module, ModuleEvent}; use gpui::{list, AnyElement, FocusHandle, FocusableView, ListState, Model, Task}; use project::dap_store::DapStore; +use rpc::proto::{DebuggerModuleList, UpdateDebugAdapter}; use ui::prelude::*; +use util::ResultExt; pub struct ModuleList { list: ListState, @@ -41,6 +43,34 @@ impl ModuleList { this } + pub(crate) fn set_from_proto( + &mut self, + module_list: &DebuggerModuleList, + cx: &mut ViewContext, + ) { + self.modules = module_list + .modules + .iter() + .filter_map(|payload| Module::from_proto(payload.clone()).log_err()) + .collect(); + + self.client_id = DebugAdapterClientId::from_proto(module_list.client_id); + + self.list.reset(self.modules.len()); + cx.notify(); + } + + pub(crate) fn to_proto(&self) -> DebuggerModuleList { + DebuggerModuleList { + client_id: self.client_id.to_proto(), + modules: self + .modules + .iter() + .map(|module| module.to_proto()) + .collect(), + } + } + pub fn on_module_event(&mut self, event: &ModuleEvent, cx: &mut ViewContext) { match event.reason { dap::ModuleEventReason::New => self.modules.push(event.module.clone()), @@ -68,6 +98,18 @@ impl ModuleList { std::mem::swap(&mut this.modules, &mut modules); this.list.reset(this.modules.len()); + if let Some((client, id)) = this.dap_store.read(cx).downstream_client() { + let request = UpdateDebugAdapter { + client_id: this.client_id.to_proto(), + project_id: *id, + thread_id: None, + variant: Some(rpc::proto::update_debug_adapter::Variant::Modules( + this.to_proto(), + )), + }; + + client.send(request).log_err(); + } cx.notify(); }) }) diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 1fe57d64b28f17..298f0d6791784b 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -2,6 +2,7 @@ use std::path::Path; use anyhow::{anyhow, Result}; use dap::client::DebugAdapterClientId; +use dap::proto_conversions::ProtoConversion; use dap::StackFrame; use gpui::{ list, AnyElement, EventEmitter, FocusHandle, ListState, Subscription, Task, View, WeakView, @@ -9,8 +10,10 @@ use gpui::{ use gpui::{FocusableView, Model}; use project::dap_store::DapStore; use project::ProjectPath; +use rpc::proto::{DebuggerStackFrameList, UpdateDebugAdapter}; use ui::ViewContext; use ui::{prelude::*, Tooltip}; +use util::ResultExt; use workspace::Workspace; use crate::debugger_panel_item::DebugPanelItemEvent::Stopped; @@ -71,6 +74,33 @@ impl StackFrameList { } } + pub(crate) fn thread_id(&self) -> u64 { + self.thread_id + } + + pub(crate) fn to_proto(&self) -> DebuggerStackFrameList { + DebuggerStackFrameList { + thread_id: self.thread_id, + client_id: self.client_id.to_proto(), + current_stack_frame: self.current_stack_frame_id, + stack_frames: self.stack_frames.to_proto(), + } + } + + pub(crate) fn set_from_proto( + &mut self, + stack_frame_list: DebuggerStackFrameList, + cx: &mut ViewContext, + ) { + self.thread_id = stack_frame_list.thread_id; + self.client_id = DebugAdapterClientId::from_proto(stack_frame_list.client_id); + self.current_stack_frame_id = stack_frame_list.current_stack_frame; + self.stack_frames = Vec::from_proto(stack_frame_list.stack_frames); + self.list.reset(self.stack_frames.len()); + + cx.notify(); + } + pub fn stack_frames(&self) -> &Vec { &self.stack_frames } @@ -140,6 +170,19 @@ impl StackFrameList { cx.emit(StackFrameListEvent::SelectedStackFrameChanged); cx.notify(); + if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { + let request = UpdateDebugAdapter { + client_id: self.client_id.to_proto(), + project_id: *id, + thread_id: Some(self.thread_id), + variant: Some(rpc::proto::update_debug_adapter::Variant::StackFrameList( + self.to_proto(), + )), + }; + + client.send(request).log_err(); + } + if !go_to_stack_frame { return Task::ready(Ok(())); }; diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 5f11dcdc8f186c..39ccfe7bcd4592 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,6 +1,9 @@ use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; -use anyhow::Result; -use dap::{client::DebugAdapterClientId, Scope, Variable}; +use anyhow::{anyhow, Result}; +use dap::{ + client::DebugAdapterClientId, proto_conversions::ProtoConversion, Scope, ScopePresentationHint, + Variable, +}; use editor::{ actions::{self, SelectAll}, Editor, EditorEvent, @@ -11,11 +14,17 @@ use gpui::{ }; use menu::Confirm; use project::dap_store::DapStore; +use proto::debugger_variable_list_entry::Entry; +use rpc::proto::{ + self, DebuggerScopeVariableIndex, DebuggerVariableContainer, UpdateDebugAdapter, + VariableListEntries, VariableListScopes, VariableListVariables, +}; use std::{ collections::{BTreeMap, HashMap, HashSet}, sync::Arc, }; use ui::{prelude::*, ContextMenu, ListItem}; +use util::ResultExt; #[derive(Debug, Clone, PartialEq, Eq)] pub struct VariableContainer { @@ -24,6 +33,29 @@ pub struct VariableContainer { pub depth: usize, } +impl ProtoConversion for VariableContainer { + type ProtoType = DebuggerVariableContainer; + type Output = Result; + + fn to_proto(&self) -> Self::ProtoType { + DebuggerVariableContainer { + container_reference: self.container_reference, + depth: self.depth as u64, + variable: Some(self.variable.to_proto()), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self::Output { + Ok(Self { + container_reference: payload.container_reference, + variable: payload.variable.map(Variable::from_proto).ok_or(anyhow!( + "DebuggerVariableContainer proto message didn't contain DapVariable variable field" + ))?, + depth: payload.depth as usize, + }) + } +} + #[derive(Debug, Clone)] pub struct SetVariableState { name: String, @@ -34,12 +66,104 @@ pub struct SetVariableState { parent_variables_reference: u64, } +impl SetVariableState { + fn from_proto(payload: proto::DebuggerSetVariableState) -> Option { + let scope = payload.scope.map(|scope| { + let proto_hint = scope + .presentation_hint + .unwrap_or(proto::DapScopePresentationHint::ScopeUnknown.into()); + + let presentation_hint = match proto::DapScopePresentationHint::from_i32(proto_hint) { + Some(proto::DapScopePresentationHint::Arguments) => { + Some(ScopePresentationHint::Arguments) + } + Some(proto::DapScopePresentationHint::Locals) => { + Some(ScopePresentationHint::Locals) + } + Some(proto::DapScopePresentationHint::Registers) => { + Some(ScopePresentationHint::Registers) + } + Some(proto::DapScopePresentationHint::ReturnValue) => { + Some(ScopePresentationHint::ReturnValue) + } + _ => Some(ScopePresentationHint::Unknown), + }; + + Scope { + name: scope.name, + presentation_hint, + variables_reference: scope.variables_reference, + named_variables: scope.named_variables, + indexed_variables: scope.indexed_variables, + expensive: scope.expensive, + source: None, + line: scope.line, + column: scope.column, + end_line: scope.end_line, + end_column: scope.end_column, + } + })?; + + Some(SetVariableState { + name: payload.name, + scope, + value: payload.value, + stack_frame_id: payload.stack_frame_id, + evaluate_name: payload.evaluate_name.clone(), + parent_variables_reference: payload.parent_variables_reference, + }) + } + + fn to_proto(&self) -> proto::DebuggerSetVariableState { + proto::DebuggerSetVariableState { + name: self.name.clone(), + scope: Some(self.scope.to_proto()), + value: self.value.clone(), + stack_frame_id: self.stack_frame_id, + evaluate_name: self.evaluate_name.clone(), + parent_variables_reference: self.parent_variables_reference, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] enum OpenEntry { Scope { name: String }, Variable { name: String, depth: usize }, } +impl OpenEntry { + pub(crate) fn from_proto(open_entry: &proto::VariableListOpenEntry) -> Option { + match open_entry.entry.as_ref()? { + proto::variable_list_open_entry::Entry::Scope(state) => Some(Self::Scope { + name: state.name.clone(), + }), + proto::variable_list_open_entry::Entry::Variable(state) => Some(Self::Variable { + name: state.name.clone(), + depth: state.depth as usize, + }), + } + } + + pub(crate) fn to_proto(&self) -> proto::VariableListOpenEntry { + let entry = match self { + OpenEntry::Scope { name } => { + proto::variable_list_open_entry::Entry::Scope(proto::DebuggerOpenEntryScope { + name: name.clone(), + }) + } + OpenEntry::Variable { name, depth } => { + proto::variable_list_open_entry::Entry::Variable(proto::DebuggerOpenEntryVariable { + name: name.clone(), + depth: *depth as u64, + }) + } + }; + + proto::VariableListOpenEntry { entry: Some(entry) } + } +} + #[derive(Debug, Clone)] pub enum VariableListEntry { Scope(Scope), @@ -56,12 +180,81 @@ pub enum VariableListEntry { }, } +impl VariableListEntry { + pub(crate) fn to_proto(&self) -> proto::DebuggerVariableListEntry { + let entry = match &self { + VariableListEntry::Scope(scope) => Entry::Scope(scope.to_proto()), + VariableListEntry::Variable { + depth, + scope, + variable, + has_children, + container_reference, + } => Entry::Variable(proto::VariableListEntryVariable { + depth: *depth as u64, + scope: Some(scope.to_proto()), + variable: Some(variable.to_proto()), + has_children: *has_children, + container_reference: *container_reference, + }), + VariableListEntry::SetVariableEditor { depth, state } => { + Entry::SetVariableEditor(proto::VariableListEntrySetState { + depth: *depth as u64, + state: Some(state.to_proto()), + }) + } + }; + + proto::DebuggerVariableListEntry { entry: Some(entry) } + } + + pub(crate) fn from_proto(entry: proto::DebuggerVariableListEntry) -> Option { + match entry.entry? { + Entry::Scope(scope) => Some(Self::Scope(Scope::from_proto(scope))), + Entry::Variable(var) => Some(Self::Variable { + depth: var.depth as usize, + scope: Arc::new(Scope::from_proto(var.scope?)), + variable: Arc::new(Variable::from_proto(var.variable?)), + has_children: var.has_children, + container_reference: var.container_reference, + }), + Entry::SetVariableEditor(set_state) => Some(Self::SetVariableEditor { + depth: set_state.depth as usize, + state: SetVariableState::from_proto(set_state.state?)?, + }), + } + } +} + #[derive(Debug)] struct ScopeVariableIndex { fetched_ids: HashSet, variables: Vec, } +impl ProtoConversion for ScopeVariableIndex { + type ProtoType = DebuggerScopeVariableIndex; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + DebuggerScopeVariableIndex { + fetched_ids: self.fetched_ids.iter().copied().collect(), + variables: self.variables.iter().map(|var| var.to_proto()).collect(), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + Self { + fetched_ids: payload.fetched_ids.iter().copied().collect(), + variables: payload + .variables + .iter() + .filter_map(|var| VariableContainer::from_proto(var.clone()).log_err()) + .collect(), + } + } +} + impl ScopeVariableIndex { pub fn new() -> Self { Self { @@ -99,21 +292,23 @@ impl ScopeVariableIndex { } } +type StackFrameId = u64; +type ScopeId = u64; + pub struct VariableList { list: ListState, focus_handle: FocusHandle, dap_store: Model, open_entries: Vec, client_id: DebugAdapterClientId, - scopes: HashMap>, + scopes: HashMap>, set_variable_editor: View, _subscriptions: Vec, stack_frame_list: View, set_variable_state: Option, - entries: HashMap>, + entries: HashMap>, fetch_variables_task: Option>>, - // (stack_frame_id, scope_id) -> VariableIndex - variables: BTreeMap<(u64, u64), ScopeVariableIndex>, + variables: BTreeMap<(StackFrameId, ScopeId), ScopeVariableIndex>, open_context_menu: Option<(View, Point, Subscription)>, } @@ -167,6 +362,119 @@ impl VariableList { } } + pub(crate) fn to_proto(&self) -> proto::DebuggerVariableList { + let open_entries = self.open_entries.iter().map(OpenEntry::to_proto).collect(); + let set_variable_state = self + .set_variable_state + .as_ref() + .map(SetVariableState::to_proto); + + let variables = self + .variables + .iter() + .map( + |((stack_frame_id, scope_id), scope_variable_index)| VariableListVariables { + scope_id: *scope_id, + stack_frame_id: *stack_frame_id, + variables: Some(scope_variable_index.to_proto()), + }, + ) + .collect(); + + let entries = self + .entries + .iter() + .map(|(key, entries)| VariableListEntries { + stack_frame_id: *key, + entries: entries + .clone() + .iter() + .map(|entry| entry.to_proto()) + .collect(), + }) + .collect(); + + let scopes = self + .scopes + .iter() + .map(|(key, scopes)| VariableListScopes { + stack_frame_id: *key, + scopes: scopes.to_proto(), + }) + .collect(); + + proto::DebuggerVariableList { + open_entries, + scopes, + set_variable_state, + entries, + variables, + } + } + + pub(crate) fn set_from_proto( + &mut self, + state: &proto::DebuggerVariableList, + cx: &mut ViewContext, + ) { + self.variables = state + .variables + .iter() + .filter_map(|variable| { + Some(( + (variable.stack_frame_id, variable.stack_frame_id), + ScopeVariableIndex::from_proto(variable.variables.clone()?), + )) + }) + .collect(); + + self.open_entries = state + .open_entries + .iter() + .filter_map(OpenEntry::from_proto) + .collect(); + + self.set_variable_state = state + .set_variable_state + .clone() + .and_then(SetVariableState::from_proto); + + self.entries = state + .entries + .iter() + .map(|entry| { + ( + entry.stack_frame_id, + entry + .entries + .clone() + .into_iter() + .filter_map(VariableListEntry::from_proto) + .collect(), + ) + }) + .collect(); + + self.scopes = state + .scopes + .iter() + .map(|scope| { + ( + scope.stack_frame_id, + scope + .scopes + .clone() + .into_iter() + .map(Scope::from_proto) + .collect(), + ) + }) + .collect(); + + self.list.reset(self.entries.len()); + cx.notify(); + } + fn handle_stack_frame_list_events( &mut self, _: View, @@ -341,6 +649,19 @@ impl VariableList { self.list.reset(len); cx.notify(); + + if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { + let request = UpdateDebugAdapter { + client_id: self.client_id.to_proto(), + thread_id: Some(self.stack_frame_list.read(cx).thread_id()), + project_id: *project_id, + variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( + self.to_proto(), + )), + }; + + client.send(request).log_err(); + } } fn fetch_nested_variables( @@ -424,6 +745,10 @@ impl VariableList { } fn fetch_variables(&mut self, cx: &mut ViewContext) { + if self.dap_store.read(cx).upstream_client().is_some() { + return; + } + let stack_frames = self.stack_frame_list.read(cx).stack_frames().clone(); self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { @@ -856,6 +1181,7 @@ mod tests { let mut index = ScopeVariableIndex::new(); assert_eq!(index.variables(), &[]); + assert_eq!(index.fetched_ids, HashSet::default()); let variable1 = VariableContainer { variable: Variable { @@ -914,6 +1240,7 @@ mod tests { index.variables(), &[variable1.clone(), variable2.clone(), variable3.clone()] ); + assert_eq!(index.fetched_ids, HashSet::from([1])); } /// This covers when you click on a variable that has a nested variable @@ -981,6 +1308,7 @@ mod tests { index.variables(), &[variable1.clone(), variable2.clone(), variable3.clone()] ); + assert_eq!(index.fetched_ids, HashSet::from([1])); let variable4 = VariableContainer { variable: Variable { @@ -1026,5 +1354,95 @@ mod tests { variable3.clone(), ] ); + assert_eq!(index.fetched_ids, HashSet::from([1, 2])); + } + + #[test] + fn test_can_serialize_to_and_from_proto() { + let mut index = ScopeVariableIndex::new(); + + let variable1 = VariableContainer { + variable: Variable { + name: "First variable".into(), + value: "First variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + let variable2 = VariableContainer { + variable: Variable { + name: "Second variable with child".into(), + value: "Second variable with child".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + index.add_variables(1, vec![variable1.clone(), variable2.clone()]); + + let variable3 = VariableContainer { + variable: Variable { + name: "Third variable".into(), + value: "Third variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + let variable4 = VariableContainer { + variable: Variable { + name: "Four variable".into(), + value: "Four variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + depth: 1, + container_reference: 1, + }; + + index.add_variables(2, vec![variable3.clone(), variable4.clone()]); + + assert_eq!( + index.variables(), + &[ + variable1.clone(), + variable3.clone(), + variable4.clone(), + variable2.clone(), + ] + ); + assert_eq!(index.fetched_ids, HashSet::from([1, 2])); + + let from_proto = ScopeVariableIndex::from_proto(index.to_proto()); + + assert_eq!(index.variables(), from_proto.variables()); + assert_eq!(index.fetched_ids, from_proto.fetched_ids); } } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index ddde9b708ff37e..e7b701101523ac 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -35,6 +35,7 @@ use language::{ }; use lsp::LanguageServerName; use node_runtime::NodeRuntime; +use rpc::proto::{SetDebuggerPanelItem, UpdateDebugAdapter}; use rpc::{proto, AnyProtoClient, TypedEnvelope}; use serde_json::Value; use settings::{Settings as _, WorktreeId}; @@ -63,6 +64,8 @@ pub enum DapStoreEvent { Notification(String), BreakpointsChanged, ActiveDebugLineChanged, + SetDebugPanelItem(SetDebuggerPanelItem), + UpdateDebugAdapter(UpdateDebugAdapter), } pub enum DebugAdapterClientState { @@ -106,6 +109,8 @@ impl DapStore { client.add_model_message_handler(DapStore::handle_synchronize_breakpoints); client.add_model_message_handler(DapStore::handle_set_active_debug_line); client.add_model_message_handler(DapStore::handle_remove_active_debug_line); + client.add_model_message_handler(DapStore::handle_set_debug_panel_item); + client.add_model_message_handler(DapStore::handle_update_debug_adapter); } pub fn new_local( @@ -196,6 +201,10 @@ impl DapStore { } } + pub fn downstream_client(&self) -> Option<&(AnyProtoClient, u64)> { + self.downstream_client.as_ref() + } + pub fn next_client_id(&self) -> DebugAdapterClientId { DebugAdapterClientId(self.next_client_id.fetch_add(1, SeqCst)) } @@ -958,6 +967,16 @@ impl DapStore { }) } + // TODO Debugger Collab + fn _send_proto_client_request( + &self, + _client_id: &DebugAdapterClientId, + _message: Message, + _cx: &mut ModelContext, + ) { + // + } + pub fn step_over( &self, client_id: &DebugAdapterClientId, @@ -965,6 +984,12 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { + if let Some(remote) = self.as_remote() { + if let Some(_client) = &remote.upstream_client { + // + } + } + let Some(client) = self.client_by_id(client_id) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1383,6 +1408,26 @@ impl DapStore { }) } + async fn handle_set_debug_panel_item( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::SetDebugPanelItem(envelope.payload)); + }) + } + + async fn handle_update_debug_adapter( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::UpdateDebugAdapter(envelope.payload)); + }) + } + async fn handle_set_active_debug_line( this: Model, envelope: TypedEnvelope, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8b4a21cfd0305a..1bcc164495b68d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -71,7 +71,7 @@ pub use prettier_store::PrettierStore; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; use remote::{SshConnectionOptions, SshRemoteClient}; use rpc::{ - proto::{LanguageServerPromptResponse, SSH_PROJECT_ID}, + proto::{LanguageServerPromptResponse, SetDebuggerPanelItem, SSH_PROJECT_ID}, AnyProtoClient, ErrorCode, }; use search::{SearchInputKind, SearchQuery, SearchResult}; @@ -253,6 +253,7 @@ pub enum Event { LanguageNotFound(Model), DebugClientStarted(DebugAdapterClientId), DebugClientStopped(DebugAdapterClientId), + SetDebugClient(SetDebuggerPanelItem), ActiveDebugLineChanged, DebugClientEvent { client_id: DebugAdapterClientId, @@ -2519,6 +2520,7 @@ impl Project { DapStoreEvent::ActiveDebugLineChanged => { cx.emit(Event::ActiveDebugLineChanged); } + _ => {} } } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 5d429916f9d235..1e2628820fa28a 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -310,7 +310,9 @@ message Envelope { SynchronizeBreakpoints Synchronize_breakpoints = 291; SetActiveDebugLine set_active_debug_line = 292; - RemoveActiveDebugLine remove_active_debug_line = 293; // current max + RemoveActiveDebugLine remove_active_debug_line = 293; + SetDebuggerPanelItem set_debugger_panel_item = 294; + UpdateDebugAdapter update_debug_adapter = 295; // current max } reserved 87 to 88; @@ -1689,6 +1691,7 @@ message UpdateActiveView { enum PanelId { AssistantPanel = 0; + DebugPanel = 1; } message UpdateView { @@ -1743,6 +1746,7 @@ message View { } } + message Collaborator { PeerId peer_id = 1; uint32 replica_id = 2; @@ -2437,6 +2441,263 @@ message RemoveActiveDebugLine { uint64 project_id = 1; } +enum DebuggerThreadItem { + Console = 0; + LoadedSource = 1; + Modules = 2; + Output = 3; + Variables = 4; +} + +message DebuggerSetVariableState { + string name = 1; + DapScope scope = 2; + string value = 3; + uint64 stack_frame_id = 4; + optional string evaluate_name = 5; + uint64 parent_variables_reference = 6; +} + +message VariableListOpenEntry { + oneof entry { + DebuggerOpenEntryScope scope = 1; + DebuggerOpenEntryVariable variable = 2; + } +} + +message DebuggerOpenEntryScope { + string name = 1; +} + +message DebuggerOpenEntryVariable { + string name = 1; + uint64 depth = 2; +} + +message DebuggerVariableListEntry { + oneof entry { + DapScope scope = 1; + VariableListEntrySetState set_variable_editor = 2; + VariableListEntryVariable variable = 3; + } +} + +message VariableListEntrySetState { + uint64 depth = 1; + DebuggerSetVariableState state = 2; +} + +message VariableListEntryVariable { + uint64 depth = 1; + DapScope scope = 2; + DapVariable variable = 3; + bool has_children = 4; + uint64 container_reference = 5; +} + +message DebuggerScopeVariableIndex { + repeated uint64 fetched_ids = 1; + repeated DebuggerVariableContainer variables = 2; +} + +message DebuggerVariableContainer { + uint64 container_reference = 1; + DapVariable variable = 2; + uint64 depth = 3; +} + +message DebuggerThreadState { + DebuggerThreadStatus thread_status = 1; + bool stopped = 2; +} + +enum DebuggerThreadStatus { + Running = 0; + Stopped = 1; + Exited = 2; + Ended = 3; +} + +message VariableListScopes { + uint64 stack_frame_id = 1; + repeated DapScope scopes = 2; +} + +message VariableListEntries { + uint64 stack_frame_id = 1; + repeated DebuggerVariableListEntry entries = 2; +} + +message VariableListVariables { + uint64 stack_frame_id = 1; + uint64 scope_id = 2; + DebuggerScopeVariableIndex variables = 3; +} + +message DebuggerVariableList { + repeated VariableListOpenEntry open_entries = 1; + repeated VariableListScopes scopes = 2; + // Editor set_variable_editor = 3; + DebuggerSetVariableState set_variable_state = 4; + repeated VariableListEntries entries = 5; + repeated VariableListVariables variables = 6; +} + +message DebuggerStackFrameList { + uint64 thread_id = 1; + uint64 client_id = 2; + uint64 current_stack_frame = 3; + repeated DapStackFrame stack_frames = 4; +} + +message DapStackFrame { + uint64 id = 1; + string name = 2; + optional DapSource source = 3; + uint64 line = 4; + uint64 column = 5; + optional uint64 end_line = 6; + optional uint64 end_column = 7; + optional bool can_restart = 8; + optional string instruction_pointer_reference = 9; + optional DapModuleId module_id = 10; + optional DapStackPresentationHint presentation_hint = 11; +} + +message DebuggerLoadedSourceList { + uint64 client_id = 1; + repeated DapSource sources = 2; +} + +message DebuggerConsole { + // Editor console = 1; + // Editor query_bar = 2; +} + +message DebuggerModuleList { + repeated DapModule modules = 1; + uint64 client_id = 2; +} + +message SetDebuggerPanelItem { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + DebuggerConsole console = 4; + DebuggerModuleList module_list = 5; + DebuggerThreadItem active_thread_item = 6; + DebuggerThreadState thread_state = 7; + DebuggerVariableList variable_list = 8; + DebuggerStackFrameList stack_frame_list = 9; + DebuggerLoadedSourceList loaded_source_list = 10; + string client_name = 11; +} + +message UpdateDebugAdapter { + uint64 project_id = 1; + uint64 client_id = 2; + optional uint64 thread_id = 3; + oneof variant { + DebuggerThreadState thread_state = 4; + DebuggerStackFrameList stack_frame_list = 5; + DebuggerVariableList variable_list = 6; + DebuggerModuleList modules = 7; + } +} + +// Remote Debugging: Dap Types +message DapVariable { + string name = 1; + string value = 2; + optional string type = 3; + // optional DapVariablePresentationHint presentation_hint = 4; + optional string evaluate_name = 5; + uint64 variables_reference = 6; + optional uint64 named_variables = 7; + optional uint64 indexed_variables = 8; + optional string memory_reference = 9; +} + +message DapScope { + string name = 1; + optional DapScopePresentationHint presentation_hint = 2; + uint64 variables_reference = 3; + optional uint64 named_variables = 4; + optional uint64 indexed_variables = 5; + bool expensive = 6; + optional DapSource source = 7; + optional uint64 line = 8; + optional uint64 column = 9; + optional uint64 end_line = 10; + optional uint64 end_column = 11; +} + +message DapSource { + optional string name = 1; + optional string path = 2; + optional uint64 source_reference = 3; + optional DapSourcePresentationHint presentation_hint = 4; + optional string origin = 5; + repeated DapSource sources = 6; + optional bytes adapter_data = 7; + repeated DapChecksum checksums = 8; +} + +enum DapChecksumAlgorithm { + CHECKSUM_ALGORITHM_UNSPECIFIED = 0; + MD5 = 1; + SHA1 = 2; + SHA256 = 3; + TIMESTAMP = 4; +} + +message DapChecksum { + DapChecksumAlgorithm algorithm = 1; + string checksum = 2; +} + +enum DapScopePresentationHint { + Arguments = 0; + Locals = 1; + Registers = 2; + ReturnValue = 3; + ScopeUnknown = 4; +} + +enum DapSourcePresentationHint { + SourceNormal = 0; + Emphasize = 1; + Deemphasize = 2; + SourceUnknown = 3; +} + +enum DapStackPresentationHint { + StackNormal = 0; + Label = 1; + Subtle = 2; + StackUnknown = 3; +} + +message DapModule { + DapModuleId id = 1; + string name = 2; + optional string path = 3; + optional bool is_optimized = 4; + optional bool is_user_code = 5; + optional string version = 6; + optional string symbol_status = 7; + optional string symbol_file_path = 8; + optional string date_time_stamp = 9; + optional string address_range = 10; +} + +message DapModuleId { + oneof id { + uint32 number = 1; + string string = 2; + } +} + // Remote FS message AddWorktree { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 8b9bac3867e18d..d27a864d2cf7fd 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -307,6 +307,7 @@ messages!( (UpdateFollowers, Foreground), (UpdateInviteInfo, Foreground), (UpdateLanguageServer, Foreground), + (UpdateDebugAdapter, Foreground), (UpdateParticipantLocation, Foreground), (UpdateProject, Foreground), (UpdateProjectCollaborator, Foreground), @@ -377,6 +378,7 @@ messages!( (SynchronizeBreakpoints, Background), (SetActiveDebugLine, Background), (RemoveActiveDebugLine, Background), + (SetDebuggerPanelItem, Background), ); request_messages!( @@ -568,6 +570,7 @@ entity_messages!( UpdateProjectCollaborator, UpdateWorktree, UpdateWorktreeSettings, + UpdateDebugAdapter, LspExtExpandMacro, LspExtOpenDocs, AdvertiseContexts, @@ -593,6 +596,7 @@ entity_messages!( SynchronizeBreakpoints, SetActiveDebugLine, RemoveActiveDebugLine, + SetDebuggerPanelItem, ); entity_messages!( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 16fe7ba748532d..4e77e09ab4a142 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3628,7 +3628,6 @@ impl Workspace { cx: &mut ViewContext, ) -> proto::FollowResponse { let active_view = self.active_view_for_follower(follower_project_id, cx); - cx.notify(); proto::FollowResponse { // TODO: Remove after version 0.145.x stabilizes. From 5dbadab1acc2b41d385711647aa12fd4af5b09db Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 18 Dec 2024 13:25:22 -0500 Subject: [PATCH 395/650] Fix failing test_debug_panel_following We don't have followableItem impl for debug_panel_item yet so this test will always fail. I commented out some lines and put a todo that we'll get to after standard collab works for the debugger --- crates/collab/src/tests/debug_panel_tests.rs | 34 +++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 16a59de63b5230..09dadb830f3078 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -48,11 +48,11 @@ async fn test_debug_panel_following(cx_a: &mut TestAppContext, cx_b: &mut TestAp .unwrap(); let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let (_workspace_b, _cx_b) = client_b.build_workspace(&project_b, cx_b); // Client A opens an editor. let _pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); - let editor_a = workspace_a + let _editor_a = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "test.txt"), None, true, cx) }) @@ -61,26 +61,28 @@ async fn test_debug_panel_following(cx_a: &mut TestAppContext, cx_b: &mut TestAp .downcast::() .unwrap(); - let peer_id_a = client_a.peer_id().unwrap(); + let _peer_id_a = client_a.peer_id().unwrap(); // Client B follows A - workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + // workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); - let _editor_b2 = workspace_b.update(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); + // TODO Debugger: FollowableItem implementation test + + // let _editor_b2 = workspace_b.update(cx_b, |workspace, cx| { + // workspace + // .active_item(cx) + // .unwrap() + // .downcast::() + // .unwrap() + // }); // Start a fake debugging session in a (see: other tests which setup fake language servers for a model) // Add a breakpoint - editor_a.update(cx_a, |editor, cx| { - editor.move_down(&editor::actions::MoveDown, cx); - editor.select_right(&editor::actions::SelectRight, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); - }); + // editor_a.update(cx_a, |editor, cx| { + // editor.move_down(&editor::actions::MoveDown, cx); + // editor.select_right(&editor::actions::SelectRight, cx); + // editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + // }); // Start debugging From 1b1d37484bb0b4b1ff67f5ff4c9ccb7412d9518b Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:39:21 -0500 Subject: [PATCH 396/650] Add DAP Step back support (#78) * Add step back support for DAP The step back button is hidden because most dap implementations don't support it. * Add step back as global action - Thanks Remco for the advice! * Filter step back action when not avaliable --- assets/icons/debug_step_back.svg | 1 + crates/debugger_ui/src/debugger_panel.rs | 28 +++++++++-- crates/debugger_ui/src/debugger_panel_item.rs | 23 +++++++++ crates/debugger_ui/src/lib.rs | 15 +++++- crates/project/src/dap_store.rs | 49 +++++++++++++++++-- crates/ui/src/components/icon.rs | 1 + crates/workspace/src/workspace.rs | 1 + 7 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 assets/icons/debug_step_back.svg diff --git a/assets/icons/debug_step_back.svg b/assets/icons/debug_step_back.svg new file mode 100644 index 00000000000000..bc7c9b8444cda2 --- /dev/null +++ b/assets/icons/debug_step_back.svg @@ -0,0 +1 @@ + diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 070e2edffa0f95..5b7acd5e48de44 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -26,8 +26,8 @@ use terminal_view::terminal_panel::TerminalPanel; use ui::prelude::*; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - pane, Continue, Disconnect, Pane, Pause, Restart, Start, StepInto, StepOut, StepOver, Stop, - ToggleIgnoreBreakpoints, Workspace, + pane, Continue, Disconnect, Pane, Pause, Restart, Start, StepBack, StepInto, StepOut, StepOver, + Stop, ToggleIgnoreBreakpoints, Workspace, }; pub enum DebugPanelEvent { @@ -214,8 +214,19 @@ impl DebugPanel { let debug_panel = DebugPanel::new(workspace, cx); cx.observe(&debug_panel, |_, debug_panel, cx| { - let has_active_session = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx).is_some()); + let (has_active_session, support_step_back) = + debug_panel.update(cx, |this, cx| { + this.active_debug_panel_item(cx) + .map(|item| { + ( + true, + item.update(cx, |this, cx| this.capabilities(cx)) + .supports_step_back + .unwrap_or(false), + ) + }) + .unwrap_or((false, false)) + }); let filter = CommandPaletteFilter::global_mut(cx); let debugger_action_types = [ @@ -230,11 +241,20 @@ impl DebugPanel { TypeId::of::(), ]; + let step_back_action_type = [TypeId::of::()]; + if has_active_session { filter.show_action_types(debugger_action_types.iter()); + + if support_step_back { + filter.show_action_types(step_back_action_type.iter()); + } else { + filter.hide_action_types(&step_back_action_type); + } } else { // show only the `debug: start` filter.hide_action_types(&debugger_action_types); + filter.hide_action_types(&step_back_action_type); } }) .detach(); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 310421a54a492a..e4c53ea59ce22c 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -588,6 +588,18 @@ impl DebugPanelItem { }); } + pub fn step_back(&mut self, cx: &mut ViewContext) { + self.update_thread_state_status(ThreadStatus::Running, cx); + + let granularity = DebuggerSettings::get_global(cx).stepping_granularity; + + self.dap_store.update(cx, |store, cx| { + store + .step_back(&self.client_id, self.thread_id, granularity, cx) + .detach_and_log_err(cx); + }); + } + pub fn restart_client(&self, cx: &mut ViewContext) { self.dap_store.update(cx, |store, cx| { store @@ -780,6 +792,17 @@ impl Render for DebugPanelItem { ) } }) + .when(capabilities.supports_step_back.unwrap_or(false), |this| { + this.child( + IconButton::new("debug-step-back", IconName::DebugStepBack) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + this.step_back(cx); + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |cx| Tooltip::text("Step back", cx)), + ) + }) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) .icon_size(IconSize::Small) diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 51009adf59dcf0..8176f36d43edd7 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -5,8 +5,8 @@ use gpui::AppContext; use settings::Settings; use ui::ViewContext; use workspace::{ - Continue, Pause, Restart, ShutdownDebugAdapters, Start, StepInto, StepOut, StepOver, Stop, - ToggleIgnoreBreakpoints, Workspace, + Continue, Pause, Restart, ShutdownDebugAdapters, Start, StepBack, StepInto, StepOut, StepOver, + Stop, ToggleIgnoreBreakpoints, Workspace, }; mod attach_modal; @@ -78,6 +78,17 @@ pub fn init(cx: &mut AppContext) { active_item.update(cx, |item, cx| item.step_in(cx)) }); }) + .register_action(|workspace: &mut Workspace, _: &StepBack, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.step_back(cx)) + }); + }) .register_action(|workspace: &mut Workspace, _: &StepOut, cx| { let debug_panel = workspace.panel::(cx).unwrap(); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e7b701101523ac..27d284427da25a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -3,15 +3,15 @@ use crate::{ProjectEnvironment, ProjectItem as _, ProjectPath}; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; -use dap::adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}; use dap::{ + adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}, client::{DebugAdapterClient, DebugAdapterClientId}, messages::{Message, Response}, requests::{ Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, LoadedSources, Modules, Next, Pause, Request as _, Restart, RunInTerminal, Scopes, - SetBreakpoints, SetExpression, SetVariable, StackTrace, StartDebugging, StepIn, StepOut, - Terminate, TerminateThreads, Variables, + SetBreakpoints, SetExpression, SetVariable, StackTrace, StartDebugging, StepBack, StepIn, + StepOut, Terminate, TerminateThreads, Variables, }, AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, @@ -20,8 +20,9 @@ use dap::{ ModulesArguments, NextArguments, PauseArguments, RestartArguments, RunInTerminalResponse, Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, - StartDebuggingRequestArgumentsRequest, StepInArguments, StepOutArguments, SteppingGranularity, - TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, + StartDebuggingRequestArgumentsRequest, StepBackArguments, StepInArguments, StepOutArguments, + SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, + VariablesArguments, }; use dap_adapters::build_adapter; use fs::Fs; @@ -1077,6 +1078,44 @@ impl DapStore { }) } + pub fn step_back( + &self, + client_id: &DebugAdapterClientId, + thread_id: u64, + granularity: SteppingGranularity, + cx: &mut ModelContext, + ) -> Task> { + let Some(client) = self.client_by_id(client_id) else { + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + }; + + let capabilities = self.capabilities_by_id(client_id); + + if capabilities.supports_step_back.unwrap_or(false) { + return Task::ready(Err(anyhow!( + "Step back request isn't support for client_id: {:?}", + client_id + ))); + } + + let supports_single_thread_execution_requests = capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(); + let supports_stepping_granularity = capabilities + .supports_stepping_granularity + .unwrap_or_default(); + + cx.background_executor().spawn(async move { + client + .request::(StepBackArguments { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + }) + .await + }) + } + pub fn variables( &self, client_id: &DebugAdapterClientId, diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 71bb647f5da3d8..08bee40a019973 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -168,6 +168,7 @@ pub enum IconName { DebugStepOver, DebugStepInto, DebugStepOut, + DebugStepBack, DebugRestart, Debug, DebugStop, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4e77e09ab4a142..5d22b99e0276ac 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -135,6 +135,7 @@ actions!( StepInto, StepOver, StepOut, + StepBack, Stop, ToggleIgnoreBreakpoints ] From 4f8c19a93ad41d8188a3cbcd7a4627db47550b65 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 19 Dec 2024 22:38:54 +0100 Subject: [PATCH 397/650] Fix don't remove request handler for tests This fixes an issue that we couldn't handle a request multiple times. --- crates/dap/src/transport.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index e3d7315e9a4b56..c062c869952afd 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -834,8 +834,8 @@ impl Transport for FakeTransport { break anyhow!(error); } Ok(Message::Request(request)) => { - if let Some(mut handle) = - handlers.lock().await.remove(request.command.as_str()) + if let Some(handle) = + handlers.lock().await.get_mut(request.command.as_str()) { handle( request.seq, From 39e70354c1fa60cc55a907ebf77c24169056d107 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 19 Dec 2024 22:40:54 +0100 Subject: [PATCH 398/650] Add test for selecting stackframe and open correct editor --- crates/debugger_ui/src/stack_frame_list.rs | 10 +- .../debugger_ui/src/tests/debugger_panel.rs | 9 - .../debugger_ui/src/tests/stack_frame_list.rs | 225 +++++++++++++++++- 3 files changed, 227 insertions(+), 17 deletions(-) diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 298f0d6791784b..62b2e70c47f373 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -195,13 +195,13 @@ impl StackFrameList { cx.spawn({ let client_id = self.client_id; - let workspace = self.workspace.clone(); move |this, mut cx| async move { - workspace - .update(&mut cx, |workspace, cx| { + this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |workspace, cx| { workspace.open_path_preview(project_path.clone(), None, false, true, cx) - })? - .await?; + }) + })?? + .await?; this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index ed2c668a0d7767..75901633624c4d 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -103,9 +103,6 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test }) }); - // If we don't end session client will still be awaiting to recv messages - // from fake transport that will never be transmitted, thus resulting in - // a "panic: parked with nothing to run" shutdown_client.await.unwrap(); // assert we don't have a debug panel item anymore because the client shutdown @@ -253,9 +250,6 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( }) }); - // If we don't end session client will still be awaiting to recv messages - // from fake transport that will never be transmitted, thus resulting in - // a "panic: parked with nothing to run" shutdown_client.await.unwrap(); // assert we don't have a debug panel item anymore because the client shutdown @@ -403,9 +397,6 @@ async fn test_client_can_open_multiple_thread_panels( }) }); - // If we don't end session client will still be awaiting to recv messages - // from fake transport that will never be transmitted, thus resulting in - // a "panic: parked with nothing to run" shutdown_client.await.unwrap(); // assert we don't have a debug panel item anymore because the client shutdown diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index e4092597cbdaa6..0c67077f84554e 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -154,6 +154,169 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( cx.run_until_parked(); + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + }); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} + +#[gpui::test] +async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + import { SOME_VALUE } './module.js'; + + console.log(SOME_VALUE); + "# + .unindent(); + + let module_file_content = r#" + export SOME_VALUE = 'some value'; + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + "module.js": module_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![ + StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + StackFrame { + id: 2, + name: "Stack Frame 2".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + ]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + workspace .update(cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); @@ -194,14 +357,70 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( }) .unwrap(); + let stack_frame_list = workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.read(cx).stack_frame_list().clone() + }) + .unwrap(); + + // select second stack frame + stack_frame_list + .update(cx, |stack_frame_list, cx| { + stack_frame_list.select_stack_frame(&stack_frames[1], true, cx) + }) + .await + .unwrap(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + + assert_eq!(2, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + }); + + let editors = workspace.items_of_type::(cx).collect::>(); + assert_eq!(1, editors.len()); + + let project_path = editors[0] + .update(cx, |editor, cx| editor.project_path(cx)) + .unwrap(); + assert_eq!("src/module.js", project_path.path.to_string_lossy()); + assert_eq!(module_file_content, editors[0].read(cx).text(cx)); + assert_eq!( + vec![0..1], + editors[0].update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + editor + .highlighted_rows::() + .map(|(range, _)| { + let start = range.start.to_point(&snapshot.buffer_snapshot); + let end = range.end.to_point(&snapshot.buffer_snapshot); + start.row..end.row + }) + .collect::>() + }) + ); + }) + .unwrap(); + let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_client(&client.id(), cx) }) }); - // If we don't end session client will still be awaiting to recv messages - // from fake transport that will never be transmitted, thus resulting in - // a "panic: parked with nothing to run" shutdown_client.await.unwrap(); } From f8fe1652a71a681ff8d00bce3ab7e42e2b074b70 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 19 Dec 2024 22:41:31 +0100 Subject: [PATCH 399/650] Add basic flow tests for variable list --- crates/debugger_ui/src/debugger_panel_item.rs | 5 + crates/debugger_ui/src/tests.rs | 1 + crates/debugger_ui/src/tests/variable_list.rs | 503 ++++++++++++++++++ crates/debugger_ui/src/variable_list.rs | 17 +- 4 files changed, 524 insertions(+), 2 deletions(-) create mode 100644 crates/debugger_ui/src/tests/variable_list.rs diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index e4c53ea59ce22c..ec3de93c650087 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -467,6 +467,11 @@ impl DebugPanelItem { &self.stack_frame_list } + #[cfg(any(test, feature = "test-support"))] + pub fn variable_list(&self) -> &View { + &self.variable_list + } + pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { self.dap_store .read_with(cx, |store, _| store.capabilities_by_id(&self.client_id)) diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 4d21f8ee27b107..779c6413a73846 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -7,6 +7,7 @@ use crate::debugger_panel::DebugPanel; mod debugger_panel; mod stack_frame_list; +mod variable_list; pub fn init_test(cx: &mut gpui::TestAppContext) { if std::env::var("RUST_LOG").is_ok() { diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs new file mode 100644 index 00000000000000..4603272a830eab --- /dev/null +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -0,0 +1,503 @@ +use std::sync::Arc; + +use crate::{ + debugger_panel::DebugPanel, + tests::{add_debugger_panel, init_test}, + variable_list::VariableContainer, +}; +use collections::HashMap; +use dap::{ + requests::{Disconnect, Initialize, Launch, Scopes, StackTrace, Variables}, + Scope, StackFrame, Variable, +}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use project::{FakeFs, Project}; +use serde_json::json; +use unindent::Unindent as _; + +/// This only tests fetching one scope and 2 variables for a single stackframe +#[gpui::test] +async fn test_basic_fetch_initial_scope_and_variables( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + console.log("Some value"); + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + let scopes = vec![Scope { + name: "Scope 1".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }]; + + client + .on_request::({ + let scopes = Arc::new(scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*scopes).clone(), + }) + } + }) + .await; + + let variables = vec![ + Variable { + name: "Variable 1".into(), + value: "Value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "Variable 2".into(), + value: "Value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + client + .on_request::({ + let variables = Arc::new(variables.clone()); + move |_, args| { + assert_eq!(2, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*variables).clone(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!( + vec![ + VariableContainer { + container_reference: 2, + variable: variables[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: 2, + variable: variables[1].clone(), + depth: 1, + }, + ], + variable_list.variables(cx) + ); + }); + }); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} + +/// This tests fetching multiple scopes and variables for them with a single stackframe +#[gpui::test] +async fn test_fetch_variables_for_multiple_scopes( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + console.log("Some value"); + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + let scopes = vec![ + Scope { + name: "Scope 1".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + Scope { + name: "Scope 2".into(), + presentation_hint: None, + variables_reference: 3, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + ]; + + client + .on_request::({ + let scopes = Arc::new(scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*scopes).clone(), + }) + } + }) + .await; + + let mut variables = HashMap::default(); + variables.insert( + 2, + vec![ + Variable { + name: "Variable 1".into(), + value: "Value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "Variable 2".into(), + value: "Value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ], + ); + variables.insert( + 3, + vec![Variable { + name: "Variable 3".into(), + value: "Value 3".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }], + ); + + client + .on_request::({ + let variables = Arc::new(variables.clone()); + move |_, args| { + Ok(dap::VariablesResponse { + variables: variables.get(&args.variables_reference).unwrap().clone(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + debug_panel_item + .variable_list() + .update(cx, |variable_list, _| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + + // scope 1 + assert_eq!( + [ + VariableContainer { + container_reference: 2, + variable: variables.get(&2).unwrap()[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: 2, + variable: variables.get(&2).unwrap()[1].clone(), + depth: 1, + }, + ], + variable_list.variables_by_scope(1, 2).unwrap().variables() + ); + + // scope 2 + assert_eq!( + [VariableContainer { + container_reference: 3, + variable: variables.get(&3).unwrap()[0].clone(), + depth: 1, + }], + variable_list.variables_by_scope(1, 3).unwrap().variables() + ); + }); + }); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 39ccfe7bcd4592..472c68d22b4592 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -227,7 +227,7 @@ impl VariableListEntry { } #[derive(Debug)] -struct ScopeVariableIndex { +pub struct ScopeVariableIndex { fetched_ids: HashSet, variables: Vec, } @@ -491,6 +491,19 @@ impl VariableList { } } + #[cfg(any(test, feature = "test-support"))] + pub fn scopes(&self) -> &HashMap> { + &self.scopes + } + + pub fn variables_by_scope( + &self, + stack_frame_id: StackFrameId, + scope_id: ScopeId, + ) -> Option<&ScopeVariableIndex> { + self.variables.get(&(stack_frame_id, scope_id)) + } + pub fn variables(&self, cx: &mut ViewContext) -> Vec { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); @@ -999,7 +1012,7 @@ impl VariableList { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); - let Some(index) = self.variables.get(&(stack_frame_id, scope_id)) else { + let Some(index) = self.variables_by_scope(stack_frame_id, scope_id) else { return; }; From f6d26afb13dcd98c0d12674e7231c36fa3d2048e Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:42:00 -0500 Subject: [PATCH 400/650] Collab: Sync DAP Client Capabilities (#79) * Start work on getting dap clients to sync capabilities * Add notify when syncing capabilities * Remove client capabilities in handle shutdown debug client --- crates/collab/src/rpc.rs | 6 +- crates/dap/src/proto_conversions.rs | 47 ++++++++++++++- crates/debugger_ui/src/debugger_panel.rs | 2 + crates/debugger_ui/src/debugger_panel_item.rs | 13 +++++ crates/project/src/dap_store.rs | 58 ++++++++++++++++++- crates/proto/proto/zed.proto | 22 ++++++- crates/proto/src/proto.rs | 4 ++ 7 files changed, 146 insertions(+), 6 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1736506fb93eaf..61ff8756bfe115 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -418,7 +418,11 @@ impl Server { broadcast_project_message_from_host::, ) .add_message_handler(broadcast_project_message_from_host::) - .add_message_handler(broadcast_project_message_from_host::); + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler( + broadcast_project_message_from_host::, + ) + .add_message_handler(broadcast_project_message_from_host::); Arc::new(server) } diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index 0ccd4fe1ac5542..6d50fe00a62123 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -1,9 +1,9 @@ use anyhow::{anyhow, Result}; use client::proto::{ self, DapChecksum, DapChecksumAlgorithm, DapModule, DapScope, DapScopePresentationHint, - DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable, + DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable, SetDebugClientCapabilities, }; -use dap_types::{ScopePresentationHint, Source}; +use dap_types::{Capabilities, ScopePresentationHint, Source}; pub trait ProtoConversion { type ProtoType; @@ -331,3 +331,46 @@ impl ProtoConversion for dap_types::Module { }) } } + +pub fn capabilities_from_proto(payload: &SetDebugClientCapabilities) -> Capabilities { + Capabilities { + supports_loaded_sources_request: Some(payload.supports_loaded_sources_request), + supports_modules_request: Some(payload.supports_modules_request), + supports_restart_request: Some(payload.supports_restart_request), + supports_set_expression: Some(payload.supports_set_expression), + supports_single_thread_execution_requests: Some( + payload.supports_single_thread_execution_requests, + ), + supports_step_back: Some(payload.supports_step_back), + supports_stepping_granularity: Some(payload.supports_stepping_granularity), + supports_terminate_threads_request: Some(payload.supports_terminate_threads_request), + ..Default::default() + } +} + +pub fn capabilities_to_proto( + capabilities: &Capabilities, + project_id: u64, + client_id: u64, +) -> SetDebugClientCapabilities { + SetDebugClientCapabilities { + client_id, + project_id, + supports_loaded_sources_request: capabilities + .supports_loaded_sources_request + .unwrap_or_default(), + supports_modules_request: capabilities.supports_modules_request.unwrap_or_default(), + supports_restart_request: capabilities.supports_restart_request.unwrap_or_default(), + supports_set_expression: capabilities.supports_set_expression.unwrap_or_default(), + supports_single_thread_execution_requests: capabilities + .supports_single_thread_execution_requests + .unwrap_or_default(), + supports_step_back: capabilities.supports_step_back.unwrap_or_default(), + supports_stepping_granularity: capabilities + .supports_stepping_granularity + .unwrap_or_default(), + supports_terminate_threads_request: capabilities + .supports_terminate_threads_request + .unwrap_or_default(), + } +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 5b7acd5e48de44..de63f816bfea68 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -855,6 +855,8 @@ impl DebugPanel { debug_panel_item.update(cx, |this, cx| { this.from_proto(payload, cx); }); + + cx.notify(); } fn handle_module_event( diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index ec3de93c650087..dcaaffe699e768 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -5,6 +5,7 @@ use crate::module_list::ModuleList; use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; +use dap::proto_conversions; use dap::{ client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, @@ -422,6 +423,18 @@ impl DebugPanelItem { return; } + self.dap_store.update(cx, |dap_store, _| { + if let Some((downstream_client, project_id)) = dap_store.downstream_client() { + let message = proto_conversions::capabilities_to_proto( + &dap_store.capabilities_by_id(client_id), + *project_id, + client_id.to_proto(), + ); + + downstream_client.send(message).log_err(); + } + }); + cx.notify(); } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 27d284427da25a..6c0ebf41de45ff 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -107,10 +107,12 @@ impl DapStore { const INDEX_STARTS_AT_ONE: bool = true; pub fn init(client: &AnyProtoClient) { - client.add_model_message_handler(DapStore::handle_synchronize_breakpoints); - client.add_model_message_handler(DapStore::handle_set_active_debug_line); client.add_model_message_handler(DapStore::handle_remove_active_debug_line); + client.add_model_message_handler(DapStore::handle_shutdown_debug_client); + client.add_model_message_handler(DapStore::handle_set_active_debug_line); + client.add_model_message_handler(DapStore::handle_set_debug_client_capabilities); client.add_model_message_handler(DapStore::handle_set_debug_panel_item); + client.add_model_message_handler(DapStore::handle_synchronize_breakpoints); client.add_model_message_handler(DapStore::handle_update_debug_adapter); } @@ -661,6 +663,15 @@ impl DapStore { .await?; this.update(&mut cx, |store, cx| { + if let Some((downstream_client, project_id)) = store.downstream_client.as_ref() { + let message = dap::proto_conversions::capabilities_to_proto( + &capabilities.clone(), + *project_id, + client.id().to_proto(), + ); + + downstream_client.send(message).log_err(); + } store.capabilities.insert(client.id(), capabilities); cx.notify(); @@ -1357,6 +1368,15 @@ impl DapStore { self.ignore_breakpoints.remove(client_id); let capabilities = self.capabilities.remove(client_id); + if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { + let request = proto::ShutdownDebugClient { + client_id: client_id.to_proto(), + project_id: *project_id, + }; + + downstream_client.send(request).log_err(); + } + cx.spawn(|_, _| async move { let client = match client { DebugAdapterClientState::Starting(task) => task.await, @@ -1467,6 +1487,40 @@ impl DapStore { }) } + async fn handle_set_debug_client_capabilities( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |dap_store, cx| { + if dap_store.upstream_client().is_some() { + *dap_store + .capabilities + .entry(DebugAdapterClientId::from_proto(envelope.payload.client_id)) + .or_default() = + dap::proto_conversions::capabilities_from_proto(&envelope.payload); + + cx.notify(); + } + }) + } + + async fn handle_shutdown_debug_client( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |dap_store, cx| { + if matches!(dap_store.mode, DapStoreMode::Remote(_)) { + dap_store.clients.remove(&DebugAdapterClientId::from_proto( + envelope.payload.client_id, + )); + + cx.notify(); + } + }) + } + async fn handle_set_active_debug_line( this: Model, envelope: TypedEnvelope, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 1e2628820fa28a..672a46433fbaad 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -312,7 +312,9 @@ message Envelope { SetActiveDebugLine set_active_debug_line = 292; RemoveActiveDebugLine remove_active_debug_line = 293; SetDebuggerPanelItem set_debugger_panel_item = 294; - UpdateDebugAdapter update_debug_adapter = 295; // current max + UpdateDebugAdapter update_debug_adapter = 295; + ShutdownDebugClient shutdown_debug_client = 296; + SetDebugClientCapabilities set_debug_client_capabilities = 297; // current max } reserved 87 to 88; @@ -2417,6 +2419,24 @@ enum BreakpointKind { Log = 1; } +message ShutdownDebugClient { + uint64 client_id = 1; + uint64 project_id = 2; +} + +message SetDebugClientCapabilities { +uint64 client_id = 1; +uint64 project_id = 2; +bool supports_loaded_sources_request = 3; +bool supports_modules_request = 4; +bool supports_restart_request = 5; +bool supports_set_expression = 6; +bool supports_single_thread_execution_requests = 7; +bool supports_step_back = 8; +bool supports_stepping_granularity = 9; +bool supports_terminate_threads_request = 10; +} + message Breakpoint { Anchor position = 1; uint32 cached_position = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index d27a864d2cf7fd..2efe63c546b092 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -379,6 +379,8 @@ messages!( (SetActiveDebugLine, Background), (RemoveActiveDebugLine, Background), (SetDebuggerPanelItem, Background), + (ShutdownDebugClient, Background), + (SetDebugClientCapabilities, Background), ); request_messages!( @@ -597,6 +599,8 @@ entity_messages!( SetActiveDebugLine, RemoveActiveDebugLine, SetDebuggerPanelItem, + ShutdownDebugClient, + SetDebugClientCapabilities, ); entity_messages!( From cea5cc7cd03333e14761214716148722e52738e6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 20 Dec 2024 21:38:15 +0100 Subject: [PATCH 401/650] Add basic test for collab show debug panel Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- Cargo.lock | 3 + crates/collab/Cargo.toml | 5 +- crates/collab/src/tests/debug_panel_tests.rs | 172 +++++++++++++------ crates/dap/src/lib.rs | 1 + crates/debugger_ui/src/lib.rs | 17 +- crates/project/src/dap_store.rs | 17 +- 6 files changed, 144 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e369c2af7536fd..e6439f8cb28d08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2671,9 +2671,12 @@ dependencies = [ "clock", "collab_ui", "collections", + "command_palette_hooks", "context_server", "ctor", + "dap", "dashmap 6.1.0", + "debugger_ui", "derive_more", "editor", "env_logger 0.11.5", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 996ee398431fcb..5d6c03dd02a2b6 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -79,7 +79,6 @@ uuid.workspace = true [dev-dependencies] assistant = { workspace = true, features = ["test-support"] } assistant_tool.workspace = true -context_server.workspace = true async-trait.workspace = true audio.workspace = true call = { workspace = true, features = ["test-support"] } @@ -87,7 +86,11 @@ channel.workspace = true client = { workspace = true, features = ["test-support"] } collab_ui = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } +command_palette_hooks.workspace = true +context_server.workspace = true ctor.workspace = true +dap = { workspace = true, features = ["test-support"] } +debugger_ui = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true extension.workspace = true diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 09dadb830f3078..086e7da00f0dcf 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1,37 +1,58 @@ use call::ActiveCall; -use editor::Editor; -use gpui::TestAppContext; -use serde_json::json; +use dap::requests::{Disconnect, Initialize, Launch, StackTrace}; +use debugger_ui::debugger_panel::DebugPanel; +use gpui::{TestAppContext, View, VisualTestContext}; +use workspace::{dock::Panel, Workspace}; use super::TestServer; +pub fn init_test(cx: &mut gpui::TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::try_init().ok(); + } + + cx.update(|cx| { + theme::init(theme::LoadThemes::JustBase, cx); + command_palette_hooks::init(cx); + language::init(cx); + workspace::init_settings(cx); + project::Project::init_settings(cx); + debugger_ui::init(cx); + editor::init(cx); + }); +} + +pub async fn add_debugger_panel(workspace: &View, cx: &mut VisualTestContext) { + let debugger_panel = workspace + .update(cx, |_, cx| cx.spawn(DebugPanel::load)) + .await + .unwrap(); + + workspace.update(cx, |workspace, cx| { + workspace.add_panel(debugger_panel, cx); + }); +} + #[gpui::test] -async fn test_debug_panel_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { +async fn test_debug_panel_item_opens_on_remote( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { let executor = cx_a.executor(); let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + init_test(cx_a); + init_test(cx_b); + server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/a", - // TODO: Make these good files for debugging - json!({ - "test.txt": "one\ntwo\nthree", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -48,45 +69,96 @@ async fn test_debug_panel_following(cx_a: &mut TestAppContext, cx_b: &mut TestAp .unwrap(); let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (_workspace_b, _cx_b) = client_b.build_workspace(&project_b, cx_b); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - // Client A opens an editor. - let _pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); - let _editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "test.txt"), None, true, cx) + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + dap::DebugAdapterConfig { + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }) - .await - .unwrap() - .downcast::() - .unwrap(); + }); - let _peer_id_a = client_a.peer_id().unwrap(); + let client = task.await.unwrap(); - // Client B follows A - // workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; - // TODO Debugger: FollowableItem implementation test + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + }); + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); - // let _editor_b2 = workspace_b.update(cx_b, |workspace, cx| { - // workspace - // .active_item(cx) - // .unwrap() - // .downcast::() - // .unwrap() - // }); + shutdown_client.await.unwrap(); - // Start a fake debugging session in a (see: other tests which setup fake language servers for a model) - // Add a breakpoint - // editor_a.update(cx_a, |editor, cx| { - // editor.move_down(&editor::actions::MoveDown, cx); - // editor.select_right(&editor::actions::SelectRight, cx); - // editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); - // }); + cx_b.run_until_parked(); - // Start debugging + // assert we don't have a debug panel item anymore because the client shutdown + workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); - // TODO: - // 2. Sanity check: make sure a looks right - // 3. Check that b looks right + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }); } diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index 19eacd6dfd0379..fc458dc8763c88 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -5,6 +5,7 @@ pub mod proto_conversions; pub mod transport; pub use dap_types::*; +pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType}; #[cfg(any(test, feature = "test-support"))] pub use adapters::FakeAdapter; diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 8176f36d43edd7..6465b20246fe38 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -9,14 +9,14 @@ use workspace::{ Stop, ToggleIgnoreBreakpoints, Workspace, }; -mod attach_modal; -mod console; +pub mod attach_modal; +pub mod console; pub mod debugger_panel; -mod debugger_panel_item; -mod loaded_source_list; -mod module_list; -mod stack_frame_list; -mod variable_list; +pub mod debugger_panel_item; +pub mod loaded_source_list; +pub mod module_list; +pub mod stack_frame_list; +pub mod variable_list; #[cfg(test)] mod tests; @@ -25,9 +25,6 @@ pub fn init(cx: &mut AppContext) { DebuggerSettings::register(cx); workspace::FollowableViewRegistry::register::(cx); - // let client: AnyProtoClient = client.clone().into(); - // client.add_model_message_handler(DebugPanel::handle_set_debug_panel_item); - cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 6c0ebf41de45ff..48047cfc79fddc 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -261,14 +261,12 @@ impl DapStore { cx.emit(DapStoreEvent::ActiveDebugLineChanged); cx.notify(); - if let Some((client, project_id)) = - self.upstream_client().or(self.downstream_client.clone()) - { + if let Some((client, project_id)) = self.downstream_client.clone() { client .send(client::proto::SetActiveDebugLine { row, project_id, - client_id: client_id.0 as u64, + client_id: client_id.to_proto(), project_path: Some(project_path.to_proto()), }) .log_err(); @@ -286,9 +284,7 @@ impl DapStore { cx.emit(DapStoreEvent::ActiveDebugLineChanged); cx.notify(); - if let Some((client, project_id)) = - self.upstream_client().or(self.downstream_client.clone()) - { + if let Some((client, project_id)) = self.downstream_client.clone() { client .send(client::proto::RemoveActiveDebugLine { project_id }) .log_err(); @@ -1512,10 +1508,11 @@ impl DapStore { ) -> Result<()> { this.update(&mut cx, |dap_store, cx| { if matches!(dap_store.mode, DapStoreMode::Remote(_)) { - dap_store.clients.remove(&DebugAdapterClientId::from_proto( - envelope.payload.client_id, - )); + let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); + + dap_store.capabilities.remove(&client_id); + cx.emit(DapStoreEvent::DebugClientStopped(client_id)); cx.notify(); } }) From a91faaceecb765568021caae1ce70382f4a5cde2 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 20 Dec 2024 21:43:11 +0100 Subject: [PATCH 402/650] Rename DebugClientStopped event to DebugClientShutdown Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 6 +++--- crates/debugger_ui/src/debugger_panel_item.rs | 6 +++--- crates/project/src/dap_store.rs | 6 +++--- crates/project/src/project.rs | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 8a14b4b9299395..91b4b669f367f0 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -173,7 +173,7 @@ impl LogStore { project.read(cx).debug_client_for_id(client_id, cx), ); } - project::Event::DebugClientStopped(id) => { + project::Event::DebugClientShutdown(id) => { this.remove_debug_client(*id, cx); } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index de63f816bfea68..f6c7307de5ee81 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -43,7 +43,7 @@ pub enum DebugPanelEvent { Output((DebugAdapterClientId, OutputEvent)), Module((DebugAdapterClientId, ModuleEvent)), LoadedSource((DebugAdapterClientId, LoadedSourceEvent)), - ClientStopped(DebugAdapterClientId), + ClientShutdown(DebugAdapterClientId), CapabilitiesChanged(DebugAdapterClientId), } @@ -174,8 +174,8 @@ impl DebugPanel { } _ => unreachable!(), }, - project::Event::DebugClientStopped(client_id) => { - cx.emit(DebugPanelEvent::ClientStopped(*client_id)); + project::Event::DebugClientShutdown(client_id) => { + cx.emit(DebugPanelEvent::ClientShutdown(*client_id)); this.message_queue.remove(client_id); this.thread_states diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index dcaaffe699e768..2e56d1b4837aac 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -141,8 +141,8 @@ impl DebugPanelItem { DebugPanelEvent::LoadedSource((client_id, event)) => { this.handle_loaded_source_event(client_id, event, cx) } - DebugPanelEvent::ClientStopped(client_id) => { - this.handle_client_stopped_event(client_id, cx) + DebugPanelEvent::ClientShutdown(client_id) => { + this.handle_client_shutdown_event(client_id, cx) } DebugPanelEvent::Continued((client_id, event)) => { this.handle_thread_continued_event(client_id, event, cx); @@ -378,7 +378,7 @@ impl DebugPanelItem { }); } - fn handle_client_stopped_event( + fn handle_client_shutdown_event( &mut self, client_id: &DebugAdapterClientId, cx: &mut ViewContext, diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 48047cfc79fddc..5727e9d424ff3a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -57,7 +57,7 @@ use util::{merge_json_value_into, ResultExt as _}; pub enum DapStoreEvent { DebugClientStarted(DebugAdapterClientId), - DebugClientStopped(DebugAdapterClientId), + DebugClientShutdown(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, message: Message, @@ -1359,7 +1359,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; - cx.emit(DapStoreEvent::DebugClientStopped(*client_id)); + cx.emit(DapStoreEvent::DebugClientShutdown(*client_id)); self.ignore_breakpoints.remove(client_id); let capabilities = self.capabilities.remove(client_id); @@ -1512,7 +1512,7 @@ impl DapStore { dap_store.capabilities.remove(&client_id); - cx.emit(DapStoreEvent::DebugClientStopped(client_id)); + cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); cx.notify(); } }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1bcc164495b68d..dd2aafc97fd462 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -252,7 +252,7 @@ pub enum Event { LanguageServerPrompt(LanguageServerPromptRequest), LanguageNotFound(Model), DebugClientStarted(DebugAdapterClientId), - DebugClientStopped(DebugAdapterClientId), + DebugClientShutdown(DebugAdapterClientId), SetDebugClient(SetDebuggerPanelItem), ActiveDebugLineChanged, DebugClientEvent { @@ -2499,8 +2499,8 @@ impl Project { DapStoreEvent::DebugClientStarted(client_id) => { cx.emit(Event::DebugClientStarted(*client_id)); } - DapStoreEvent::DebugClientStopped(client_id) => { - cx.emit(Event::DebugClientStopped(*client_id)); + DapStoreEvent::DebugClientShutdown(client_id) => { + cx.emit(Event::DebugClientShutdown(*client_id)); } DapStoreEvent::DebugClientEvent { client_id, message } => { cx.emit(Event::DebugClientEvent { From ad1e51f64afade3e913a961e527191ab11b86663 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 21 Dec 2024 00:46:13 +0100 Subject: [PATCH 403/650] Add tests for variable list toggle scope & variables This also covers fetching the variables, and validating the visual entries --- crates/debugger_ui/src/tests/variable_list.rs | 608 +++++++++++++++++- crates/debugger_ui/src/variable_list.rs | 43 +- 2 files changed, 624 insertions(+), 27 deletions(-) diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 4603272a830eab..a2a91af2767d7c 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use crate::{ debugger_panel::DebugPanel, tests::{add_debugger_panel, init_test}, - variable_list::VariableContainer, + variable_list::{VariableContainer, VariableListEntry}, }; use collections::HashMap; use dap::{ @@ -211,18 +211,40 @@ async fn test_basic_fetch_initial_scope_and_variables( assert_eq!( vec![ VariableContainer { - container_reference: 2, + container_reference: scopes[0].variables_reference, variable: variables[0].clone(), depth: 1, }, VariableContainer { - container_reference: 2, + container_reference: scopes[0].variables_reference, variable: variables[1].clone(), depth: 1, }, ], variable_list.variables(cx) ); + + // assert visual entries + assert_eq!( + vec![ + VariableListEntry::Scope(scopes[0].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(variables[0].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(variables[1].clone()), + container_reference: scopes[0].variables_reference, + }, + ], + variable_list.entries().get(&1).unwrap().clone() + ); }); }); }) @@ -466,12 +488,12 @@ async fn test_fetch_variables_for_multiple_scopes( assert_eq!( [ VariableContainer { - container_reference: 2, + container_reference: scopes[0].variables_reference, variable: variables.get(&2).unwrap()[0].clone(), depth: 1, }, VariableContainer { - container_reference: 2, + container_reference: scopes[0].variables_reference, variable: variables.get(&2).unwrap()[1].clone(), depth: 1, }, @@ -482,12 +504,586 @@ async fn test_fetch_variables_for_multiple_scopes( // scope 2 assert_eq!( [VariableContainer { - container_reference: 3, + container_reference: scopes[1].variables_reference, variable: variables.get(&3).unwrap()[0].clone(), depth: 1, }], variable_list.variables_by_scope(1, 3).unwrap().variables() ); + + // assert visual entries + assert_eq!( + vec![ + VariableListEntry::Scope(scopes[0].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new( + variables.get(&scopes[0].variables_reference).unwrap()[0] + .clone() + ), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new( + variables.get(&scopes[0].variables_reference).unwrap()[1] + .clone() + ), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Scope(scopes[1].clone()), + ], + variable_list.entries().get(&1).unwrap().clone() + ); + }); + }); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} + +// tests that toggling a variable will fetch its children and show it +#[gpui::test] +async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + console.log("Some value"); + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + let scopes = vec![ + Scope { + name: "Scope 1".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + Scope { + name: "Scope 2".into(), + presentation_hint: None, + variables_reference: 4, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + ]; + + client + .on_request::({ + let scopes = Arc::new(scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*scopes).clone(), + }) + } + }) + .await; + + let scope1_variables = vec![ + Variable { + name: "Variable 1".into(), + value: "Value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 3, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "Variable 2".into(), + value: "Value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + let nested_variables = vec![ + Variable { + name: "Nested Variable 1".into(), + value: "Nested Value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "Nested Variable 2".into(), + value: "Nested Value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + let scope2_variables = vec![Variable { + name: "Variable 1".into(), + value: "Value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }]; + + client + .on_request::({ + let scope1_variables = Arc::new(scope1_variables.clone()); + let nested_variables = Arc::new(nested_variables.clone()); + let scope2_variables = Arc::new(scope2_variables.clone()); + move |_, args| match args.variables_reference { + 4 => Ok(dap::VariablesResponse { + variables: (*scope2_variables).clone(), + }), + 3 => Ok(dap::VariablesResponse { + variables: (*nested_variables).clone(), + }), + 2 => Ok(dap::VariablesResponse { + variables: (*scope1_variables).clone(), + }), + id => unreachable!("unexpected variables reference {id}"), + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, _| { + // scope 1 + assert_eq!( + [ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[1].clone(), + depth: 1, + }, + ], + variable_list.variables_by_scope(1, 2).unwrap().variables() + ); + + // scope 2 + assert_eq!( + [VariableContainer { + container_reference: scopes[1].variables_reference, + variable: scope2_variables[0].clone(), + depth: 1, + }], + variable_list.variables_by_scope(1, 4).unwrap().variables() + ); + + // assert visual entries + assert_eq!( + vec![ + VariableListEntry::Scope(scopes[0].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: true, + variable: Arc::new(scope1_variables[0].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(scope1_variables[1].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Scope(scopes[1].clone()), + ], + variable_list.entries().get(&1).unwrap().clone() + ); + }); + }); + }) + .unwrap(); + + // toggle nested variables for scope 1 + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.on_toggle_variable( + scopes[0].variables_reference, // scope id + &crate::variable_list::OpenEntry::Variable { + name: scope1_variables[0].name.clone(), + depth: 1, + }, + scope1_variables[0].variables_reference, + 1, // depth + Some(false), + cx, + ); + }); + }); + }) + .unwrap(); + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, _| { + // scope 1 + assert_eq!( + [ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[0].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[1].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[1].clone(), + depth: 1, + }, + ], + variable_list.variables_by_scope(1, 2).unwrap().variables() + ); + + // scope 2 + assert_eq!( + [VariableContainer { + container_reference: scopes[1].variables_reference, + variable: scope2_variables[0].clone(), + depth: 1, + }], + variable_list.variables_by_scope(1, 4).unwrap().variables() + ); + + // assert visual entries + assert_eq!( + vec![ + VariableListEntry::Scope(scopes[0].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: true, + variable: Arc::new(scope1_variables[0].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 2, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(nested_variables[0].clone()), + container_reference: scope1_variables[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 2, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(nested_variables[1].clone()), + container_reference: scope1_variables[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(scope1_variables[1].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Scope(scopes[1].clone()), + ], + variable_list.entries().get(&1).unwrap().clone() + ); + }); + }); + }) + .unwrap(); + + // toggle scope 2 to show variables + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.toggle_entry( + &crate::variable_list::OpenEntry::Scope { + name: scopes[1].name.clone(), + }, + cx, + ); + }); + }); + }) + .unwrap(); + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, _| { + // scope 1 + assert_eq!( + [ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[0].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[1].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[1].clone(), + depth: 1, + }, + ], + variable_list.variables_by_scope(1, 2).unwrap().variables() + ); + + // scope 2 + assert_eq!( + [VariableContainer { + container_reference: scopes[1].variables_reference, + variable: scope2_variables[0].clone(), + depth: 1, + }], + variable_list.variables_by_scope(1, 4).unwrap().variables() + ); + + // assert visual entries + assert_eq!( + vec![ + VariableListEntry::Scope(scopes[0].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: true, + variable: Arc::new(scope1_variables[0].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 2, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(nested_variables[0].clone()), + container_reference: scope1_variables[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 2, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(nested_variables[1].clone()), + container_reference: scope1_variables[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(scope1_variables[1].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Scope(scopes[1].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[1].clone()), + has_children: false, + variable: Arc::new(scope2_variables[0].clone()), + container_reference: scopes[1].variables_reference, + }, + ], + variable_list.entries().get(&1).unwrap().clone() + ); }); }); }) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 472c68d22b4592..fc920a61cb6b12 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -56,7 +56,7 @@ impl ProtoConversion for VariableContainer { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SetVariableState { name: String, scope: Scope, @@ -127,7 +127,7 @@ impl SetVariableState { } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -enum OpenEntry { +pub enum OpenEntry { Scope { name: String }, Variable { name: String, depth: usize }, } @@ -164,7 +164,7 @@ impl OpenEntry { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum VariableListEntry { Scope(Scope), SetVariableEditor { @@ -496,6 +496,11 @@ impl VariableList { &self.scopes } + #[cfg(any(test, feature = "test-support"))] + pub fn entries(&self) -> &HashMap> { + &self.entries + } + pub fn variables_by_scope( &self, stack_frame_id: StackFrameId, @@ -542,7 +547,7 @@ impl VariableList { } } - fn toggle_entry(&mut self, entry_id: &OpenEntry, cx: &mut ViewContext) { + pub fn toggle_entry(&mut self, entry_id: &OpenEntry, cx: &mut ViewContext) { match self.open_entries.binary_search(&entry_id) { Ok(ix) => { self.open_entries.remove(ix); @@ -996,20 +1001,15 @@ impl VariableList { } #[allow(clippy::too_many_arguments)] - fn on_toggle_variable( + pub fn on_toggle_variable( &mut self, scope_id: u64, entry_id: &OpenEntry, variable_reference: u64, depth: usize, - has_children: bool, disclosed: Option, cx: &mut ViewContext, ) { - if !has_children { - return; - } - let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); let Some(index) = self.variables_by_scope(stack_frame_id, scope_id) else { @@ -1089,17 +1089,18 @@ impl VariableList { .indent_step_size(px(20.)) .always_show_disclosure_icon(true) .toggle(disclosed) - .on_toggle(cx.listener(move |this, _, cx| { - this.on_toggle_variable( - scope_id, - &entry_id, - variable_reference, - depth, - has_children, - disclosed, - cx, - ) - })) + .when(has_children, |list_item| { + list_item.on_toggle(cx.listener(move |this, _, cx| { + this.on_toggle_variable( + scope_id, + &entry_id, + variable_reference, + depth, + disclosed, + cx, + ) + })) + }) .on_secondary_mouse_down(cx.listener({ let scope = scope.clone(); let variable = variable.clone(); From 3e9139eeb153c71bda971f87f95cebd17c6a1d87 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 21 Dec 2024 15:43:53 +0100 Subject: [PATCH 404/650] Add basic flow tests for console/output editor --- crates/debugger_ui/src/console.rs | 12 +- crates/debugger_ui/src/debugger_panel.rs | 5 + crates/debugger_ui/src/debugger_panel_item.rs | 10 ++ crates/debugger_ui/src/module_list.rs | 2 +- crates/debugger_ui/src/tests.rs | 1 + crates/debugger_ui/src/tests/console.rs | 163 ++++++++++++++++++ .../debugger_ui/src/tests/debugger_panel.rs | 155 +++++++++++++++++ crates/debugger_ui/src/variable_list.rs | 32 ++-- 8 files changed, 358 insertions(+), 22 deletions(-) create mode 100644 crates/debugger_ui/src/tests/console.rs diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index f0f33842eb2c2a..12812566a1f417 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -71,6 +71,16 @@ impl Console { } } + #[cfg(any(test, feature = "test-support"))] + pub fn editor(&self) -> &View { + &self.console + } + + #[cfg(any(test, feature = "test-support"))] + pub fn query_bar(&self) -> &View { + &self.query_bar + } + fn handle_stack_frame_list_events( &mut self, _: View, @@ -92,7 +102,7 @@ impl Console { }); } - fn evaluate(&mut self, _: &Confirm, cx: &mut ViewContext) { + pub fn evaluate(&mut self, _: &Confirm, cx: &mut ViewContext) { let expression = self.query_bar.update(cx, |editor, cx| { let expression = editor.text(cx); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index f6c7307de5ee81..0fabca3997a21b 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -264,6 +264,11 @@ impl DebugPanel { }) } + #[cfg(any(test, feature = "test-support"))] + pub fn message_queue(&self) -> &HashMap> { + &self.message_queue + } + pub fn active_debug_panel_item( &self, cx: &mut ViewContext, diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 2e56d1b4837aac..7dbe670ab6ab67 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -480,6 +480,16 @@ impl DebugPanelItem { &self.stack_frame_list } + #[cfg(any(test, feature = "test-support"))] + pub fn output_editor(&self) -> &View { + &self.output_editor + } + + #[cfg(any(test, feature = "test-support"))] + pub fn console(&self) -> &View { + &self.console + } + #[cfg(any(test, feature = "test-support"))] pub fn variable_list(&self) -> &View { &self.variable_list diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 0ab25734795425..72e51f96d877ec 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -97,6 +97,7 @@ impl ModuleList { this.update(&mut cx, |this, cx| { std::mem::swap(&mut this.modules, &mut modules); this.list.reset(this.modules.len()); + cx.notify(); if let Some((client, id)) = this.dap_store.read(cx).downstream_client() { let request = UpdateDebugAdapter { @@ -110,7 +111,6 @@ impl ModuleList { client.send(request).log_err(); } - cx.notify(); }) }) } diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 779c6413a73846..f14e12122df173 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -5,6 +5,7 @@ use workspace::Workspace; use crate::debugger_panel::DebugPanel; +mod console; mod debugger_panel; mod stack_frame_list; mod variable_list; diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs new file mode 100644 index 00000000000000..a6acac7a3114c0 --- /dev/null +++ b/crates/debugger_ui/src/tests/console.rs @@ -0,0 +1,163 @@ +use crate::*; +use dap::requests::{Disconnect, Evaluate, Initialize, Launch, StackTrace}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use project::{FakeFs, Project}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use tests::{add_debugger_panel, init_test}; + +#[gpui::test] +async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let was_called = Arc::new(AtomicBool::new(false)); + let was_called_clone = was_called.clone(); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client + .on_request::(move |_, args| { + was_called_clone.store(true, Ordering::SeqCst); + + assert_eq!("print_r($variable, true);", args.expression); + assert_eq!(Some(0), args.frame_id); + assert_eq!(Some(dap::EvaluateArgumentsContext::Variables), args.context); + + Ok(dap::EvaluateResponse { + result: "['key' => 'value']".into(), + type_: None, + presentation_hint: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |item, cx| { + item.console().update(cx, |console, cx| { + console.query_bar().update(cx, |query_bar, cx| { + query_bar.set_text("print_r($variable, true);", cx); + }); + + console.evaluate(&menu::Confirm, cx); + }); + }); + }) + .unwrap(); + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + "", + active_debug_panel_item + .read(cx) + .console() + .read(cx) + .query_bar() + .read(cx) + .text(cx) + .as_str() + ); + + assert_eq!( + "['key' => 'value']\n", + active_debug_panel_item + .read(cx) + .console() + .read(cx) + .editor() + .read(cx) + .text(cx) + .as_str() + ); + }) + .unwrap(); + + assert!( + was_called.load(std::sync::atomic::Ordering::SeqCst), + "Expected evaluate request to be called" + ); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 75901633624c4d..598084b277238b 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -411,3 +411,158 @@ async fn test_client_can_open_multiple_thread_panels( }) .unwrap(); } + +#[gpui::test] +async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = add_debugger_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_test_client( + task::DebugAdapterConfig { + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let client = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: None, + output: "First console output line before thread stopped!".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First output line before thread stopped!".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second output line after thread stopped!".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Console), + output: "Second console output line after thread stopped!".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + cx.run_until_parked(); + + // assert we have output from before and after the thread stopped + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!(1, debug_panel.read(cx).message_queue().len()); + + assert_eq!( + "First output line before thread stopped!\nSecond output line after thread stopped!\n", + active_debug_panel_item.read(cx).output_editor().read(cx).text(cx).as_str() + ); + + assert_eq!( + "First console output line before thread stopped!\nSecond console output line after thread stopped!\n", + active_debug_panel_item.read(cx).console().read(cx).editor().read(cx).text(cx).as_str() + ); + }) + .unwrap(); + + let shutdown_client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_client(&client.id(), cx) + }) + }); + + shutdown_client.await.unwrap(); + + // assert output queue is empty + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + assert!(debug_panel.read(cx).message_queue().is_empty()); + }) + .unwrap(); +} diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index fc920a61cb6b12..2965bab415693a 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -944,28 +944,20 @@ impl VariableList { return cx.notify(); } - let client_id = self.client_id; - let variables_reference = state.parent_variables_reference; - let name = state.name; - let evaluate_name = state.evaluate_name; - let stack_frame_id = state.stack_frame_id; + let set_value_task = self.dap_store.update(cx, |store, cx| { + store.set_variable_value( + &self.client_id, + state.stack_frame_id, + state.parent_variables_reference, + state.name, + new_variable_value, + state.evaluate_name, + cx, + ) + }); cx.spawn(|this, mut cx| async move { - let set_value_task = this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - store.set_variable_value( - &client_id, - stack_frame_id, - variables_reference, - name, - new_variable_value, - evaluate_name, - cx, - ) - }) - }); - - set_value_task?.await?; + set_value_task.await?; this.update(&mut cx, |this, cx| { this.build_entries(false, true, cx); From ed7443db6ecd2f8a678ed7de3f0f427371d305ed Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 22 Dec 2024 14:23:39 +0100 Subject: [PATCH 405/650] Update console evaluate test to validate we invalidate variables --- crates/dap/src/client.rs | 26 +- crates/debugger_ui/src/debugger_panel.rs | 4 +- crates/debugger_ui/src/tests/console.rs | 357 ++++++++++++++++-- crates/debugger_ui/src/tests/variable_list.rs | 57 +-- 4 files changed, 378 insertions(+), 66 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index d269348a2b1b1c..879d53143f0297 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -392,8 +392,7 @@ mod tests { init_test(cx); let adapter = Arc::new(FakeAdapter::new()); - let was_called = Arc::new(AtomicBool::new(false)); - let was_called_clone = was_called.clone(); + let called_event_handler = Arc::new(AtomicBool::new(false)); let mut client = DebugAdapterClient::new( crate::client::DebugAdapterClientId(1), @@ -416,15 +415,18 @@ mod tests { client .start( - move |event, _| { - was_called_clone.store(true, Ordering::SeqCst); - - assert_eq!( - Message::Event(Box::new(Events::Initialized( - Some(Capabilities::default()) - ))), - event - ); + { + let called_event_handler = called_event_handler.clone(); + move |event, _| { + called_event_handler.store(true, Ordering::SeqCst); + + assert_eq!( + Message::Event(Box::new(Events::Initialized(Some( + Capabilities::default() + )))), + event + ); + } }, &mut cx.to_async(), ) @@ -440,7 +442,7 @@ mod tests { cx.run_until_parked(); assert!( - was_called.load(std::sync::atomic::Ordering::SeqCst), + called_event_handler.load(std::sync::atomic::Ordering::SeqCst), "Event handler was not called" ); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 0fabca3997a21b..9d797f5b2bff23 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -923,9 +923,9 @@ impl DebugPanel { debug_panel, self.workspace.clone(), self.dap_store.clone(), - cx.new_model(|_| Default::default()), // change this + cx.new_model(|_| Default::default()), // TODO debugger: change this &client_id, - SharedString::from("test"), // change this + SharedString::from("test"), // TODO debugger: change this thread_id, cx, ) diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index a6acac7a3114c0..f44d9ea087f6ea 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -1,23 +1,50 @@ use crate::*; -use dap::requests::{Disconnect, Evaluate, Initialize, Launch, StackTrace}; +use dap::{ + requests::{Disconnect, Evaluate, Initialize, Launch, Scopes, StackTrace, Variables}, + Scope, StackFrame, Variable, +}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; +use serde_json::json; use std::sync::{ atomic::{AtomicBool, Ordering}, - Arc, + Arc, Mutex, }; use tests::{add_debugger_panel, init_test}; +use unindent::Unindent as _; +use variable_list::{VariableContainer, VariableListEntry}; #[gpui::test] async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); - let was_called = Arc::new(AtomicBool::new(false)); - let was_called_clone = was_called.clone(); + const NEW_VALUE: &str = "{nested1: \"Nested 1 updated\", nested2: \"Nested 2 updated\"}"; + + let called_evaluate = Arc::new(AtomicBool::new(false)); let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + let test_file_content = r#" + const variable1 = { + nested1: "Nested 1", + nested2: "Nested 2", + }; + const variable2 = "Value 2"; + const variable3 = "Value 3"; + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = add_debugger_panel(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -49,32 +76,190 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp client.on_request::(move |_, _| Ok(())).await; + let stack_frames = vec![StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }]; + client - .on_request::(move |_, _| { - Ok(dap::StackTraceResponse { - stack_frames: Vec::default(), - total_frames: None, - }) + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } }) .await; + let scopes = vec![ + Scope { + name: "Scope 1".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + Scope { + name: "Scope 2".into(), + presentation_hint: None, + variables_reference: 4, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }, + ]; + client - .on_request::(move |_, args| { - was_called_clone.store(true, Ordering::SeqCst); - - assert_eq!("print_r($variable, true);", args.expression); - assert_eq!(Some(0), args.frame_id); - assert_eq!(Some(dap::EvaluateArgumentsContext::Variables), args.context); - - Ok(dap::EvaluateResponse { - result: "['key' => 'value']".into(), - type_: None, - presentation_hint: None, - variables_reference: 0, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }) + .on_request::({ + let scopes = Arc::new(scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*scopes).clone(), + }) + } + }) + .await; + + let scope1_variables = Arc::new(Mutex::new(vec![ + Variable { + name: "variable1".into(), + value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 3, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "variable2".into(), + value: "Value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ])); + + let nested_variables = vec![ + Variable { + name: "nested1".into(), + value: "Nested 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "nested2".into(), + value: "Nested 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + let scope2_variables = vec![Variable { + name: "variable3".into(), + value: "Value 3".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }]; + + client + .on_request::({ + let scope1_variables = scope1_variables.clone(); + let nested_variables = Arc::new(nested_variables.clone()); + let scope2_variables = Arc::new(scope2_variables.clone()); + move |_, args| match args.variables_reference { + 4 => Ok(dap::VariablesResponse { + variables: (*scope2_variables).clone(), + }), + 3 => Ok(dap::VariablesResponse { + variables: (*nested_variables).clone(), + }), + 2 => Ok(dap::VariablesResponse { + variables: scope1_variables.lock().unwrap().clone(), + }), + id => unreachable!("unexpected variables reference {id}"), + } + }) + .await; + + client + .on_request::({ + let called_evaluate = called_evaluate.clone(); + let scope1_variables = scope1_variables.clone(); + move |_, args| { + called_evaluate.store(true, Ordering::SeqCst); + + assert_eq!(format!("$variable1 = {}", NEW_VALUE), args.expression); + assert_eq!(Some(1), args.frame_id); + assert_eq!(Some(dap::EvaluateArgumentsContext::Variables), args.context); + + scope1_variables.lock().unwrap()[0].value = NEW_VALUE.to_string(); + + Ok(dap::EvaluateResponse { + result: NEW_VALUE.into(), + type_: None, + presentation_hint: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }) + } }) .await; @@ -94,6 +279,38 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp cx.run_until_parked(); + // toggle nested variables for scope 1 + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + let scope1_variables = scope1_variables.lock().unwrap().clone(); + + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.on_toggle_variable( + scopes[0].variables_reference, // scope id + &crate::variable_list::OpenEntry::Variable { + name: scope1_variables[0].name.clone(), + depth: 1, + }, + scope1_variables[0].variables_reference, + 1, // depth + Some(false), + cx, + ); + }); + }); + }) + .unwrap(); + + cx.run_until_parked(); + workspace .update(cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); @@ -104,7 +321,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp active_debug_panel_item.update(cx, |item, cx| { item.console().update(cx, |console, cx| { console.query_bar().update(cx, |query_bar, cx| { - query_bar.set_text("print_r($variable, true);", cx); + query_bar.set_text(format!("$variable1 = {}", NEW_VALUE), cx); }); console.evaluate(&menu::Confirm, cx); @@ -135,7 +352,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp ); assert_eq!( - "['key' => 'value']\n", + format!("{}\n", NEW_VALUE), active_debug_panel_item .read(cx) .console() @@ -145,11 +362,93 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp .text(cx) .as_str() ); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, _| { + let scope1_variables = scope1_variables.lock().unwrap().clone(); + + // scope 1 + assert_eq!( + [ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[0].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[1].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[1].clone(), + depth: 1, + }, + ], + variable_list.variables_by_scope(1, 2).unwrap().variables() + ); + + // scope 2 + assert_eq!( + [VariableContainer { + container_reference: scopes[1].variables_reference, + variable: scope2_variables[0].clone(), + depth: 1, + }], + variable_list.variables_by_scope(1, 4).unwrap().variables() + ); + + // assert visual entries + assert_eq!( + vec![ + VariableListEntry::Scope(scopes[0].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: true, + variable: Arc::new(scope1_variables[0].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 2, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(nested_variables[0].clone()), + container_reference: scope1_variables[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 2, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(nested_variables[1].clone()), + container_reference: scope1_variables[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(scope1_variables[1].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Scope(scopes[1].clone()), + ], + variable_list.entries().get(&1).unwrap().clone() + ); + }); + }); }) .unwrap(); assert!( - was_called.load(std::sync::atomic::Ordering::SeqCst), + called_evaluate.load(std::sync::atomic::Ordering::SeqCst), "Expected evaluate request to be called" ); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index a2a91af2767d7c..d8e87aafc01cb9 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -26,7 +26,8 @@ async fn test_basic_fetch_initial_scope_and_variables( let fs = FakeFs::new(executor.clone()); let test_file_content = r#" - console.log("Some value"); + const variable1 = "Value 1"; + const variable2 = "Value 2"; "# .unindent(); @@ -85,7 +86,7 @@ async fn test_basic_fetch_initial_scope_and_variables( adapter_data: None, checksums: None, }), - line: 3, + line: 1, column: 1, end_line: None, end_column: None, @@ -138,8 +139,8 @@ async fn test_basic_fetch_initial_scope_and_variables( let variables = vec![ Variable { - name: "Variable 1".into(), - value: "Value 1".into(), + name: "variable1".into(), + value: "value 1".into(), type_: None, presentation_hint: None, evaluate_name: None, @@ -149,8 +150,8 @@ async fn test_basic_fetch_initial_scope_and_variables( memory_reference: None, }, Variable { - name: "Variable 2".into(), - value: "Value 2".into(), + name: "variable2".into(), + value: "value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, @@ -270,7 +271,12 @@ async fn test_fetch_variables_for_multiple_scopes( let fs = FakeFs::new(executor.clone()); let test_file_content = r#" - console.log("Some value"); + const variable1 = { + nested1: "Nested 1", + nested2: "Nested 2", + }; + const variable2 = "Value 2"; + const variable3 = "Value 3"; "# .unindent(); @@ -329,7 +335,7 @@ async fn test_fetch_variables_for_multiple_scopes( adapter_data: None, checksums: None, }), - line: 3, + line: 1, column: 1, end_line: None, end_column: None, @@ -400,8 +406,8 @@ async fn test_fetch_variables_for_multiple_scopes( 2, vec![ Variable { - name: "Variable 1".into(), - value: "Value 1".into(), + name: "variable1".into(), + value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(), type_: None, presentation_hint: None, evaluate_name: None, @@ -411,7 +417,7 @@ async fn test_fetch_variables_for_multiple_scopes( memory_reference: None, }, Variable { - name: "Variable 2".into(), + name: "variable2".into(), value: "Value 2".into(), type_: None, presentation_hint: None, @@ -426,7 +432,7 @@ async fn test_fetch_variables_for_multiple_scopes( variables.insert( 3, vec![Variable { - name: "Variable 3".into(), + name: "variable3".into(), value: "Value 3".into(), type_: None, presentation_hint: None, @@ -561,7 +567,12 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T let fs = FakeFs::new(executor.clone()); let test_file_content = r#" - console.log("Some value"); + const variable1 = { + nested1: "Nested 1", + nested2: "Nested 2", + }; + const variable2 = "Value 2"; + const variable3 = "Value 3"; "# .unindent(); @@ -620,7 +631,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T adapter_data: None, checksums: None, }), - line: 3, + line: 1, column: 1, end_line: None, end_column: None, @@ -688,8 +699,8 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T let scope1_variables = vec![ Variable { - name: "Variable 1".into(), - value: "Value 1".into(), + name: "variable1".into(), + value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(), type_: None, presentation_hint: None, evaluate_name: None, @@ -699,7 +710,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T memory_reference: None, }, Variable { - name: "Variable 2".into(), + name: "variable2".into(), value: "Value 2".into(), type_: None, presentation_hint: None, @@ -713,8 +724,8 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T let nested_variables = vec![ Variable { - name: "Nested Variable 1".into(), - value: "Nested Value 1".into(), + name: "nested1".into(), + value: "Nested 1".into(), type_: None, presentation_hint: None, evaluate_name: None, @@ -724,8 +735,8 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T memory_reference: None, }, Variable { - name: "Nested Variable 2".into(), - value: "Nested Value 2".into(), + name: "nested2".into(), + value: "Nested 2".into(), type_: None, presentation_hint: None, evaluate_name: None, @@ -737,8 +748,8 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T ]; let scope2_variables = vec![Variable { - name: "Variable 1".into(), - value: "Value 1".into(), + name: "variable3".into(), + value: "Value 3".into(), type_: None, presentation_hint: None, evaluate_name: None, From 1929dec4a06b6c501fcde8f44c90ea84b3af45ef Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 22 Dec 2024 22:28:22 +0100 Subject: [PATCH 406/650] Use task label for debug panel item tab content --- crates/collab/src/tests/debug_panel_tests.rs | 1 + crates/dap/src/client.rs | 2 ++ crates/debugger_ui/src/debugger_panel.rs | 6 +++--- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/tests/console.rs | 1 + crates/debugger_ui/src/tests/debugger_panel.rs | 4 ++++ crates/debugger_ui/src/tests/stack_frame_list.rs | 2 ++ crates/debugger_ui/src/tests/variable_list.rs | 3 +++ crates/project/src/dap_store.rs | 1 + crates/task/src/debug_format.rs | 3 +++ crates/task/src/task_template.rs | 1 + 11 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 086e7da00f0dcf..5aa8a8b994a164 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -78,6 +78,7 @@ async fn test_debug_panel_item_opens_on_remote( project.dap_store().update(cx, |store, cx| { store.start_test_client( dap::DebugAdapterConfig { + label: "test config".into(), kind: dap::DebugAdapterKind::Fake, request: dap::DebugRequestType::Launch, program: None, diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 879d53143f0297..78af4f9586b45d 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -317,6 +317,7 @@ mod tests { let mut client = DebugAdapterClient::new( crate::client::DebugAdapterClientId(1), DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, @@ -397,6 +398,7 @@ mod tests { let mut client = DebugAdapterClient::new( crate::client::DebugAdapterClientId(1), DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9d797f5b2bff23..9f657beeb328b2 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -602,18 +602,18 @@ impl DebugPanel { return; }; - let Some(client_kind) = self + let Some(client_name) = self .dap_store .read(cx) .client_by_id(client_id) - .map(|client| client.config().kind) + .map(|client| client.config().label) else { return; // this can never happen }; let client_id = *client_id; - let client_name = SharedString::from(client_kind.display_name().to_string()); + let client_name = SharedString::from(client_name); cx.spawn({ let event = event.clone(); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 7dbe670ab6ab67..dfcd6f09632f6e 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -202,7 +202,7 @@ impl DebugPanelItem { } pub(crate) fn to_proto(&self, cx: &ViewContext, project_id: u64) -> SetDebuggerPanelItem { - let thread_state = Some(self.thread_state.read_with(cx, |this, _| this.to_proto())); + let thread_state = Some(self.thread_state.read(cx).to_proto()); let module_list = Some(self.module_list.read(cx).to_proto()); let variable_list = Some(self.variable_list.read(cx).to_proto()); let stack_frame_list = Some(self.stack_frame_list.read(cx).to_proto()); diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index f44d9ea087f6ea..bd6684ed3a763e 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -52,6 +52,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 598084b277238b..a9c14a97335e55 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -19,6 +19,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, @@ -135,6 +136,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, @@ -282,6 +284,7 @@ async fn test_client_can_open_multiple_thread_panels( project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, @@ -426,6 +429,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 0c67077f84554e..316160e8b02c35 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -53,6 +53,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, @@ -216,6 +217,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index d8e87aafc01cb9..5f9de616458e3a 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -49,6 +49,7 @@ async fn test_basic_fetch_initial_scope_and_variables( project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, @@ -298,6 +299,7 @@ async fn test_fetch_variables_for_multiple_scopes( project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, @@ -594,6 +596,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T project.dap_store().update(cx, |store, cx| { store.start_test_client( task::DebugAdapterConfig { + label: "test config".into(), kind: task::DebugAdapterKind::Fake, request: task::DebugRequestType::Launch, program: None, diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 5727e9d424ff3a..4bd739043dfed1 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -871,6 +871,7 @@ impl DapStore { client.adapter().clone(), client.binary().clone(), DebugAdapterConfig { + label: config.label.clone(), kind: config.kind.clone(), request: match args.request { StartDebuggingRequestArgumentsRequest::Launch => { diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 29cad2b1914abd..83f91047d940b1 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -117,6 +117,8 @@ pub struct CustomArgs { #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] #[serde(rename_all = "snake_case")] pub struct DebugAdapterConfig { + /// Name of the debug task + pub label: String, /// The type of adapter you want to use #[serde(flatten)] pub kind: DebugAdapterKind, @@ -168,6 +170,7 @@ impl DebugTaskDefinition { let cwd = self.cwd.clone().map(PathBuf::from).take_if(|p| p.exists()); let task_type = TaskType::Debug(DebugAdapterConfig { + label: self.label.clone(), kind: self.kind, request: self.request, program: self.program, diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 427bab64234a19..1dac7e160262ed 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -105,6 +105,7 @@ mod deserialization_tests { #[test] fn deserialize_task_type_debug() { let adapter_config = DebugAdapterConfig { + label: "test config".into(), kind: DebugAdapterKind::Python(TCPHost::default()), request: crate::DebugRequestType::Launch, program: Some("main".to_string()), From ccacce9ccbeef68c80335685402810014aadcb79 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 26 Dec 2024 16:09:54 +0100 Subject: [PATCH 407/650] introduce debug session model (#80) * WIP introduce debug session model * Wip more refactor towards session model * Fix some compile errors * Move sub menu item into own struct * Don't show client id inside selected value * Remove unused session_id from shutdown event * Remove clients from dap store use from sessions instead * Fix clippy * Add client id to received event log * Move reconnect client work again * Move sessions to local dap store * Move ingore breakpoints to session model * Move next session/client id to local store * Remove duplicated test support only method * Show client id first in dap log menu entry * Show sub menu better * Sort clients by their id * Sort sessions by their id * Fix configuration done race condition with sending breakpoints @Anthony-Eid I think this fixed the race condition you noticed with the configuration done request. So the issue is when the task is created it directly start the execution of the task itself. So the `configuration_done` request is most of the times finished before the breakpoints are send if you don't have a lot of breakpoints. So instead of creating both tasks before the task is created we now create the 2 tasks right after eachother, so we can be sure that the `configuration_done` request is send after the breakpoints are send. ``` [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 received event `Initialized` [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 3 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 4 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 send `configurationDone` request with sequence_id: 8 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 9 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 7 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 5 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 3 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 4 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 6 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 received response for: `configurationDone` sequence_id: 8 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 9 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 7 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 5 [2024-12-25T21:51:09+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 6 ``` ``` [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 received event `Initialized` [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 4 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 5 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 6 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 7 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 8 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 send `setBreakpoints` request with sequence_id: 3 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 4 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 5 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 6 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 7 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 8 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 received response for: `setBreakpoints` sequence_id: 3 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 send `configurationDone` request with sequence_id: 9 [2024-12-25T21:49:51+01:00 DEBUG dap::client] Client 0 received response for: `configurationDone` sequence_id: 9 ``` * Move capabilities back to dapstore itself * Fix failing remote debug panel test * Remove capabilities on remote when client shutdown * Move client_by_session to local store and impl multi client shutdown * Remove unused code * Fix clippyyy * Rename merge capabilities method As we don't only merge we also insert if there is no capabilities for the client yet. * Use resolved label for debug session name * Notify errors when start client to user * Notify reconnect error to user * Always shutdown all clients We should always shutdown all clients from a single debug session when one is shutdown/terminated. --- crates/collab/src/tests/debug_panel_tests.rs | 6 +- crates/dap/src/client.rs | 74 +- crates/dap/src/lib.rs | 1 + crates/dap/src/proto_conversions.rs | 2 + crates/dap/src/session.rs | 99 +++ crates/debugger_tools/src/dap_log.rs | 198 +++-- crates/debugger_ui/src/attach_modal.rs | 28 +- crates/debugger_ui/src/console.rs | 14 +- crates/debugger_ui/src/debugger_panel.rs | 169 ++--- crates/debugger_ui/src/debugger_panel_item.rs | 65 +- crates/debugger_ui/src/lib.rs | 2 +- crates/debugger_ui/src/tests/console.rs | 6 +- .../debugger_ui/src/tests/debugger_panel.rs | 24 +- .../debugger_ui/src/tests/stack_frame_list.rs | 12 +- crates/debugger_ui/src/tests/variable_list.rs | 18 +- crates/debugger_ui/src/variable_list.rs | 12 +- crates/project/src/dap_store.rs | 714 ++++++++++-------- crates/project/src/project.rs | 38 +- crates/proto/proto/zed.proto | 47 +- crates/task/src/lib.rs | 14 +- 20 files changed, 874 insertions(+), 669 deletions(-) create mode 100644 crates/dap/src/session.rs diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 5aa8a8b994a164..fb8c69312c14f8 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -76,7 +76,7 @@ async fn test_debug_panel_item_opens_on_remote( let task = project_a.update(cx_a, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), kind: dap::DebugAdapterKind::Fake, @@ -90,7 +90,7 @@ async fn test_debug_panel_item_opens_on_remote( }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -145,7 +145,7 @@ async fn test_debug_panel_item_opens_on_remote( let shutdown_client = project_a.update(cx_a, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 78af4f9586b45d..85e7d875e1b0d6 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -14,11 +14,10 @@ use std::{ hash::Hash, sync::{ atomic::{AtomicU64, Ordering}, - Arc, Mutex, + Arc, }, time::Duration, }; -use task::{DebugAdapterConfig, DebugRequestType}; #[cfg(debug_assertions)] const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2); @@ -47,13 +46,11 @@ pub struct DebugAdapterClient { executor: BackgroundExecutor, adapter: Arc, transport_delegate: TransportDelegate, - config: Arc>, } impl DebugAdapterClient { pub fn new( id: DebugAdapterClientId, - config: DebugAdapterConfig, adapter: Arc, binary: DebugAdapterBinary, cx: &AsyncAppContext, @@ -66,7 +63,6 @@ impl DebugAdapterClient { adapter, transport_delegate, sequence_count: AtomicU64::new(1), - config: Arc::new(Mutex::new(config)), executor: cx.background_executor().clone(), } } @@ -78,13 +74,21 @@ impl DebugAdapterClient { let (server_rx, server_tx) = self.transport_delegate.reconnect(cx).await?; log::info!("Successfully reconnected to debug adapter"); + let client_id = self.id; + // start handling events/reverse requests cx.update(|cx| { cx.spawn({ let server_tx = server_tx.clone(); |mut cx| async move { - Self::handle_receive_messages(server_rx, server_tx, message_handler, &mut cx) - .await + Self::handle_receive_messages( + client_id, + server_rx, + server_tx, + message_handler, + &mut cx, + ) + .await } }) .detach_and_log_err(cx); @@ -98,13 +102,21 @@ impl DebugAdapterClient { let (server_rx, server_tx) = self.transport_delegate.start(&self.binary, cx).await?; log::info!("Successfully connected to debug adapter"); + let client_id = self.id; + // start handling events/reverse requests cx.update(|cx| { cx.spawn({ let server_tx = server_tx.clone(); |mut cx| async move { - Self::handle_receive_messages(server_rx, server_tx, message_handler, &mut cx) - .await + Self::handle_receive_messages( + client_id, + server_rx, + server_tx, + message_handler, + &mut cx, + ) + .await } }) .detach_and_log_err(cx); @@ -112,6 +124,7 @@ impl DebugAdapterClient { } async fn handle_receive_messages( + client_id: DebugAdapterClientId, server_rx: Receiver, client_tx: Sender, mut event_handler: F, @@ -128,7 +141,7 @@ impl DebugAdapterClient { if let Err(e) = match message { Message::Event(ev) => { - log::debug!("Received event `{}`", &ev); + log::debug!("Client {} received event `{}`", client_id.0, &ev); cx.update(|cx| event_handler(Message::Event(ev), cx)) } @@ -172,26 +185,22 @@ impl DebugAdapterClient { .await; log::debug!( - "Send `{}` request with sequence_id: {}", + "Client {} send `{}` request with sequence_id: {}", + self.id.0, R::COMMAND.to_string(), sequence_id ); self.send_message(Message::Request(request)).await?; - log::debug!( - "Start receiving response for: `{}` sequence_id: {}", - R::COMMAND.to_string(), - sequence_id - ); - let mut timeout = self.executor.timer(DAP_REQUEST_TIMEOUT).fuse(); let command = R::COMMAND.to_string(); select! { response = callback_rx.fuse() => { log::debug!( - "Received response for: `{}` sequence_id: {}", + "Client {} received response for: `{}` sequence_id: {}", + self.id.0, command, sequence_id ); @@ -219,10 +228,6 @@ impl DebugAdapterClient { self.id } - pub fn config(&self) -> DebugAdapterConfig { - self.config.lock().unwrap().clone() - } - pub fn adapter(&self) -> &Arc { &self.adapter } @@ -235,14 +240,6 @@ impl DebugAdapterClient { self.adapter.name().to_string() } - pub fn set_process_id(&self, process_id: u32) { - let mut config = self.config.lock().unwrap(); - - config.request = DebugRequestType::Attach(task::AttachConfig { - process_id: Some(process_id), - }); - } - /// Get the next sequence id to be used in a request pub fn next_sequence_id(&self) -> u64 { self.sequence_count.fetch_add(1, Ordering::Relaxed) @@ -294,7 +291,6 @@ mod tests { use gpui::TestAppContext; use settings::{Settings, SettingsStore}; use std::sync::atomic::{AtomicBool, Ordering}; - use task::DebugAdapterConfig; pub fn init_test(cx: &mut gpui::TestAppContext) { if std::env::var("RUST_LOG").is_ok() { @@ -316,14 +312,6 @@ mod tests { let mut client = DebugAdapterClient::new( crate::client::DebugAdapterClientId(1), - DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, adapter, DebugAdapterBinary { command: "command".into(), @@ -397,14 +385,6 @@ mod tests { let mut client = DebugAdapterClient::new( crate::client::DebugAdapterClientId(1), - DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, adapter, DebugAdapterBinary { command: "command".into(), diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index fc458dc8763c88..d87f24038d5225 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -2,6 +2,7 @@ pub mod adapters; pub mod client; pub mod debugger_settings; pub mod proto_conversions; +pub mod session; pub mod transport; pub use dap_types::*; diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index 6d50fe00a62123..cdca5a964b918b 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -351,9 +351,11 @@ pub fn capabilities_from_proto(payload: &SetDebugClientCapabilities) -> Capabili pub fn capabilities_to_proto( capabilities: &Capabilities, project_id: u64, + session_id: u64, client_id: u64, ) -> SetDebugClientCapabilities { SetDebugClientCapabilities { + session_id, client_id, project_id, supports_loaded_sources_request: capabilities diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs new file mode 100644 index 00000000000000..ee3ac0943d0d66 --- /dev/null +++ b/crates/dap/src/session.rs @@ -0,0 +1,99 @@ +use collections::HashMap; +use gpui::ModelContext; +use std::sync::Arc; +use task::DebugAdapterConfig; + +use crate::client::{DebugAdapterClient, DebugAdapterClientId}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct DebugSessionId(pub usize); + +impl DebugSessionId { + pub fn from_proto(client_id: u64) -> Self { + Self(client_id as usize) + } + + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } +} + +pub struct DebugSession { + id: DebugSessionId, + ignore_breakpoints: bool, + configuration: DebugAdapterConfig, + clients: HashMap>, +} + +impl DebugSession { + pub fn new(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { + Self { + id, + configuration, + ignore_breakpoints: false, + clients: HashMap::default(), + } + } + + pub fn id(&self) -> DebugSessionId { + self.id + } + + pub fn name(&self) -> String { + self.configuration.label.clone() + } + + pub fn configuration(&self) -> &DebugAdapterConfig { + &self.configuration + } + + pub fn ignore_breakpoints(&self) -> bool { + self.ignore_breakpoints + } + + pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut ModelContext) { + self.ignore_breakpoints = ignore; + cx.notify(); + } + + pub fn update_configuration( + &mut self, + f: impl FnOnce(&mut DebugAdapterConfig), + cx: &mut ModelContext, + ) { + f(&mut self.configuration); + cx.notify(); + } + + pub fn add_client(&mut self, client: Arc, cx: &mut ModelContext) { + self.clients.insert(client.id(), client); + cx.notify(); + } + + pub fn remove_client( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Option> { + let client = self.clients.remove(client_id); + cx.notify(); + + client + } + + pub fn client_by_id( + &self, + client_id: &DebugAdapterClientId, + ) -> Option> { + self.clients.get(client_id).cloned() + } + + pub fn has_clients(&self) -> bool { + !self.clients.is_empty() + } + + pub fn clients(&self) -> impl Iterator> + '_ { + self.clients.values().cloned() + } +} diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 91b4b669f367f0..8bb58529d7a4fb 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -1,6 +1,7 @@ use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, debugger_settings::DebuggerSettings, + session::{DebugSession, DebugSessionId}, transport::{IoKind, LogKind}, }; use editor::{Editor, EditorEvent}; @@ -9,9 +10,9 @@ use futures::{ StreamExt, }; use gpui::{ - actions, div, AppContext, Context, EventEmitter, FocusHandle, FocusableView, IntoElement, - Model, ModelContext, ParentElement, Render, SharedString, Styled, Subscription, View, - ViewContext, VisualContext, WeakModel, WindowContext, + actions, div, AppContext, Context, Empty, EventEmitter, FocusHandle, FocusableView, + IntoElement, Model, ModelContext, ParentElement, Render, SharedString, Styled, Subscription, + View, ViewContext, VisualContext, WeakModel, WindowContext, }; use project::{search::SearchQuery, Project}; use settings::Settings as _; @@ -24,7 +25,7 @@ use util::maybe; use workspace::{ item::Item, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, - ui::{h_flex, Button, Clickable, ContextMenu, Label, PopoverMenu}, + ui::{h_flex, Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu}, ToolbarItemEvent, ToolbarItemView, Workspace, }; @@ -167,14 +168,18 @@ impl LogStore { this.projects.remove(&weak_project); }), cx.subscribe(project, |this, project, event, cx| match event { - project::Event::DebugClientStarted(client_id) => { + project::Event::DebugClientStarted((_, client_id)) => { this.add_debug_client( *client_id, - project.read(cx).debug_client_for_id(client_id, cx), + project.update(cx, |project, cx| { + project + .dap_store() + .update(cx, |store, cx| store.client_by_id(client_id, cx)) + }), ); } - project::Event::DebugClientShutdown(id) => { - this.remove_debug_client(*id, cx); + project::Event::DebugClientShutdown(client_id) => { + this.remove_debug_client(*client_id, cx); } _ => {} @@ -285,14 +290,14 @@ impl LogStore { fn add_debug_client( &mut self, client_id: DebugAdapterClientId, - client: Option>, + session_and_client: Option<(Model, Arc)>, ) -> Option<&mut DebugAdapterState> { let client_state = self .debug_clients .entry(client_id) .or_insert_with(DebugAdapterState::new); - if let Some(client) = client { + if let Some((_, client)) = session_and_client { let io_tx = self.rpc_tx.clone(); client.add_log_handler( @@ -318,8 +323,12 @@ impl LogStore { Some(client_state) } - fn remove_debug_client(&mut self, id: DebugAdapterClientId, cx: &mut ModelContext) { - self.debug_clients.remove(&id); + fn remove_debug_client( + &mut self, + client_id: DebugAdapterClientId, + cx: &mut ModelContext, + ) { + self.debug_clients.remove(&client_id); cx.notify(); } @@ -357,7 +366,7 @@ impl DapLogToolbarItemView { impl Render for DapLogToolbarItemView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let Some(log_view) = self.log_view.clone() else { - return div(); + return Empty.into_any_element(); }; let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| { @@ -368,11 +377,11 @@ impl Render for DapLogToolbarItemView { }); let current_client = current_client_id.and_then(|current_client_id| { - if let Ok(ix) = menu_rows.binary_search_by_key(¤t_client_id, |e| e.client_id) { - Some(&menu_rows[ix]) - } else { - None - } + menu_rows.iter().find_map(|row| { + row.clients + .iter() + .find(|sub_item| sub_item.client_id == current_client_id) + }) }); let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") @@ -380,12 +389,12 @@ impl Render for DapLogToolbarItemView { .trigger(Button::new( "debug_server_menu_header", current_client - .map(|row| { + .map(|sub_item| { Cow::Owned(format!( - "{}({}) - {}", - row.client_name, - row.client_id.0, - match row.selected_entry { + "{} ({}) - {}", + sub_item.client_name, + sub_item.client_id.0, + match sub_item.selected_entry { LogKind::Adapter => ADAPTER_LOGS, LogKind::Rpc => RPC_MESSAGES, } @@ -398,55 +407,80 @@ impl Render for DapLogToolbarItemView { let menu_rows = menu_rows.clone(); ContextMenu::build(cx, move |mut menu, cx| { for row in menu_rows.into_iter() { - menu = menu.header(format!("{}({})", row.client_name, row.client_id.0)); + menu = menu.header(format!("{}. {}", row.session_id.0, row.session_name)); - if row.has_adapter_logs { - menu = menu.entry( - ADAPTER_LOGS, - None, + for sub_item in row.clients.into_iter() { + menu = menu.custom_row(move |_| { + div() + .w_full() + .pl_2() + .child( + Label::new(format!( + "{}. {}", + sub_item.client_id.0, sub_item.client_name, + )) + .color(workspace::ui::Color::Muted), + ) + .into_any_element() + }); + + if sub_item.has_adapter_logs { + menu = menu.custom_entry( + move |_| { + div() + .w_full() + .pl_4() + .child(Label::new(ADAPTER_LOGS)) + .into_any_element() + }, + cx.handler_for(&log_view, move |view, cx| { + view.show_log_messages_for_server(sub_item.client_id, cx); + }), + ); + } + + menu = menu.custom_entry( + move |_| { + div() + .w_full() + .pl_4() + .child(Label::new(RPC_MESSAGES)) + .into_any_element() + }, cx.handler_for(&log_view, move |view, cx| { - view.show_log_messages_for_server(row.client_id, cx); + view.show_rpc_trace_for_server(sub_item.client_id, cx); }), ); } - - menu = menu.custom_entry( - move |_| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .into_any_element() - }, - cx.handler_for(&log_view, move |view, cx| { - view.show_rpc_trace_for_server(row.client_id, cx); - }), - ); } menu }) .into() }); - h_flex().size_full().child(dap_menu).child( - div() - .child( - Button::new("clear_log_button", "Clear").on_click(cx.listener( - |this, _, cx| { - if let Some(log_view) = this.log_view.as_ref() { - log_view.update(cx, |log_view, cx| { - log_view.editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.clear(cx); - editor.set_read_only(true); - }); - }) - } - }, - )), - ) - .ml_2(), - ) + h_flex() + .size_full() + .child(dap_menu) + .child( + div() + .child( + Button::new("clear_log_button", "Clear").on_click(cx.listener( + |this, _, cx| { + if let Some(log_view) = this.log_view.as_ref() { + log_view.update(cx, |log_view, cx| { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + }) + } + }, + )), + ) + .ml_2(), + ) + .into_any_element() } } @@ -540,20 +574,35 @@ impl DapLogView { } fn menu_items(&self, cx: &AppContext) -> Option> { - let mut rows = self + let mut menu_items = self .project .read(cx) - .debug_clients(cx) - .map(|client| DapMenuItem { - client_id: client.id(), - client_name: client.config().kind.display_name().into(), - selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), - has_adapter_logs: client.has_adapter_logs(), + .dap_store() + .read(cx) + .sessions() + .map(|session| DapMenuItem { + session_id: session.read(cx).id(), + session_name: session.read(cx).name(), + clients: { + let mut clients = session + .read(cx) + .clients() + .map(|client| DapMenuSubItem { + client_id: client.id(), + client_name: client.adapter_id(), + has_adapter_logs: client.has_adapter_logs(), + selected_entry: self + .current_view + .map_or(LogKind::Adapter, |(_, kind)| kind), + }) + .collect::>(); + clients.sort_by_key(|item| item.client_id.0); + clients + }, }) .collect::>(); - rows.sort_by_key(|row| row.client_id); - rows.dedup_by_key(|row| row.client_id); - Some(rows) + menu_items.sort_by_key(|item| item.session_id.0); + Some(menu_items) } fn show_rpc_trace_for_server( @@ -639,6 +688,13 @@ fn log_contents(lines: &VecDeque) -> String { #[derive(Clone, PartialEq)] pub(crate) struct DapMenuItem { + pub session_id: DebugSessionId, + pub session_name: String, + pub clients: Vec, +} + +#[derive(Clone, PartialEq)] +pub(crate) struct DapMenuSubItem { pub client_id: DebugAdapterClientId, pub client_name: String, pub has_adapter_logs: bool, diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index b2770b671b70b5..97605ce4a8173f 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -1,4 +1,5 @@ use dap::client::DebugAdapterClientId; +use dap::session::DebugSessionId; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{DismissEvent, EventEmitter, FocusableView, Render, View}; use gpui::{Model, Subscription}; @@ -20,6 +21,7 @@ struct Candidate { struct AttachModalDelegate { selected_index: usize, matches: Vec, + session_id: DebugSessionId, placeholder_text: Arc, dap_store: Model, client_id: DebugAdapterClientId, @@ -27,10 +29,15 @@ struct AttachModalDelegate { } impl AttachModalDelegate { - pub fn new(client_id: DebugAdapterClientId, dap_store: Model) -> Self { + pub fn new( + session_id: DebugSessionId, + client_id: DebugAdapterClientId, + dap_store: Model, + ) -> Self { Self { client_id, dap_store, + session_id, candidates: None, selected_index: 0, matches: Vec::default(), @@ -46,12 +53,16 @@ pub(crate) struct AttachModal { impl AttachModal { pub fn new( + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, dap_store: Model, cx: &mut ViewContext, ) -> Self { let picker = cx.new_view(|cx| { - Picker::uniform_list(AttachModalDelegate::new(*client_id, dap_store), cx) + Picker::uniform_list( + AttachModalDelegate::new(*session_id, *client_id, dap_store), + cx, + ) }); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { cx.emit(DismissEvent); @@ -112,12 +123,9 @@ impl PickerDelegate for AttachModalDelegate { if let Some(processes) = this.delegate.candidates.clone() { processes } else { - let Some(client) = this - .delegate - .dap_store - .read(cx) - .client_by_id(&this.delegate.client_id) - else { + let Some((_, client)) = this.delegate.dap_store.update(cx, |store, cx| { + store.client_by_id(&this.delegate.client_id, cx) + }) else { return Vec::new(); }; @@ -203,7 +211,7 @@ impl PickerDelegate for AttachModalDelegate { self.dap_store.update(cx, |store, cx| { store - .attach(&self.client_id, candidate.pid, cx) + .attach(&self.session_id, &self.client_id, candidate.pid, cx) .detach_and_log_err(cx); }); @@ -215,7 +223,7 @@ impl PickerDelegate for AttachModalDelegate { self.candidates.take(); self.dap_store.update(cx, |store, cx| { - store.shutdown_client(&self.client_id, cx).detach(); + store.shutdown_session(&self.session_id, cx).detach(); }); cx.emit(DismissEvent); diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 12812566a1f417..de20f079d6cc04 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -221,13 +221,13 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { return Task::ready(Ok(Vec::new())); }; - let support_completions = console.update(cx, |this, cx| { - this.dap_store - .read(cx) - .capabilities_by_id(&this.client_id) - .supports_completions_request - .unwrap_or_default() - }); + let support_completions = console + .read(cx) + .dap_store + .read(cx) + .capabilities_by_id(&console.read(cx).client_id) + .supports_completions_request + .unwrap_or_default(); if support_completions { self.client_completions(&console, buffer, buffer_position, cx) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9f657beeb328b2..94344ac6b13be8 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -8,6 +8,7 @@ use dap::{ debugger_settings::DebuggerSettings, messages::{Events, Message}, requests::{Request, RunInTerminal, StartDebugging}, + session::DebugSessionId, Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, RunInTerminalRequestArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, @@ -148,16 +149,21 @@ impl DebugPanel { cx.subscribe(&dap_store, Self::on_dap_store_event), cx.subscribe(&project, { move |this: &mut Self, _, event, cx| match event { - project::Event::DebugClientStarted(client_id) => { - this.handle_debug_client_started(client_id, cx); + project::Event::DebugClientStarted((session_id, client_id)) => { + this.handle_debug_client_started(session_id, client_id, cx); } - project::Event::DebugClientEvent { message, client_id } => match message { + project::Event::DebugClientEvent { + session_id, + client_id, + message, + } => match message { Message::Event(event) => { - this.handle_debug_client_events(client_id, event, cx); + this.handle_debug_client_events(session_id, client_id, event, cx); } Message::Request(request) => { if StartDebugging::COMMAND == request.command { this.handle_start_debugging_request( + session_id, client_id, request.seq, request.arguments.clone(), @@ -165,6 +171,7 @@ impl DebugPanel { ); } else if RunInTerminal::COMMAND == request.command { this.handle_run_in_terminal_request( + session_id, client_id, request.seq, request.arguments.clone(), @@ -307,6 +314,7 @@ impl DebugPanel { let thread_panel = item.downcast::().unwrap(); let thread_id = thread_panel.read(cx).thread_id(); + let session_id = thread_panel.read(cx).session_id(); let client_id = thread_panel.read(cx).client_id(); self.thread_states.remove(&(client_id, thread_id)); @@ -315,7 +323,7 @@ impl DebugPanel { self.dap_store.update(cx, |store, cx| { store - .terminate_threads(&client_id, Some(vec![thread_id; 1]), cx) + .terminate_threads(&session_id, &client_id, Some(vec![thread_id; 1]), cx) .detach() }); } @@ -348,6 +356,7 @@ impl DebugPanel { fn handle_start_debugging_request( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, seq: u64, request_args: Option, @@ -361,13 +370,14 @@ impl DebugPanel { self.dap_store.update(cx, |store, cx| { store - .respond_to_start_debugging(client_id, seq, args, cx) + .respond_to_start_debugging(session_id, client_id, seq, args, cx) .detach_and_log_err(cx); }); } fn handle_run_in_terminal_request( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, seq: u64, request_args: Option, @@ -376,7 +386,7 @@ impl DebugPanel { let Some(request_args) = request_args else { self.dap_store.update(cx, |store, cx| { store - .respond_to_run_in_terminal(client_id, false, seq, None, cx) + .respond_to_run_in_terminal(session_id, client_id, false, seq, None, cx) .detach_and_log_err(cx); }); @@ -439,6 +449,7 @@ impl DebugPanel { }) }); + let session_id = *session_id; let client_id = *client_id; cx.spawn(|this, mut cx| async move { // Ensure a response is always sent, even in error cases, @@ -454,6 +465,7 @@ impl DebugPanel { let respond_task = this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { store.respond_to_run_in_terminal( + &session_id, &client_id, success, seq, @@ -470,20 +482,23 @@ impl DebugPanel { fn handle_debug_client_started( &self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, cx: &mut ViewContext, ) { - let Some(client) = self.dap_store.read(cx).client_by_id(&client_id) else { + let Some(session) = self.dap_store.read(cx).session_by_id(session_id) else { return; }; + let session_id = *session_id; let client_id = *client_id; let workspace = self.workspace.clone(); - let request_type = client.config().request; + let request_type = session.read(cx).configuration().request.clone(); cx.spawn(|this, mut cx| async move { let task = this.update(&mut cx, |this, cx| { - this.dap_store - .update(cx, |store, cx| store.initialize(&client_id, cx)) + this.dap_store.update(cx, |store, cx| { + store.initialize(&session_id, &client_id, cx) + }) })?; task.await?; @@ -492,7 +507,7 @@ impl DebugPanel { DebugRequestType::Launch => { let task = this.update(&mut cx, |this, cx| { this.dap_store - .update(cx, |store, cx| store.launch(&client_id, cx)) + .update(cx, |store, cx| store.launch(&session_id, &client_id, cx)) }); task?.await @@ -500,8 +515,9 @@ impl DebugPanel { DebugRequestType::Attach(config) => { if let Some(process_id) = config.process_id { let task = this.update(&mut cx, |this, cx| { - this.dap_store - .update(cx, |store, cx| store.attach(&client_id, process_id, cx)) + this.dap_store.update(cx, |store, cx| { + store.attach(&session_id, &client_id, process_id, cx) + }) })?; task.await @@ -509,7 +525,12 @@ impl DebugPanel { this.update(&mut cx, |this, cx| { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(cx, |cx| { - AttachModal::new(&client_id, this.dap_store.clone(), cx) + AttachModal::new( + &session_id, + &client_id, + this.dap_store.clone(), + cx, + ) }) }) })? @@ -522,23 +543,28 @@ impl DebugPanel { fn handle_debug_client_events( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, event: &Events, cx: &mut ViewContext, ) { match event { - Events::Initialized(event) => self.handle_initialized_event(&client_id, event, cx), - Events::Stopped(event) => self.handle_stopped_event(&client_id, event, cx), + Events::Initialized(event) => { + self.handle_initialized_event(&session_id, &client_id, event, cx) + } + Events::Stopped(event) => self.handle_stopped_event(&session_id, &client_id, event, cx), Events::Continued(event) => self.handle_continued_event(&client_id, event, cx), Events::Exited(event) => self.handle_exited_event(&client_id, event, cx), - Events::Terminated(event) => self.handle_terminated_event(&client_id, event, cx), + Events::Terminated(event) => { + self.handle_terminated_event(&session_id, &client_id, event, cx) + } Events::Thread(event) => self.handle_thread_event(&client_id, event, cx), Events::Output(event) => self.handle_output_event(&client_id, event, cx), Events::Breakpoint(_) => {} Events::Module(event) => self.handle_module_event(&client_id, event, cx), Events::LoadedSource(event) => self.handle_loaded_source_event(&client_id, event, cx), Events::Capabilities(event) => { - self.handle_capabilities_changed_event(client_id, event, cx); + self.handle_capabilities_changed_event(session_id, client_id, event, cx); } Events::Memory(_) => {} Events::Process(_) => {} @@ -552,35 +578,40 @@ impl DebugPanel { fn handle_initialized_event( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, capabilities: &Option, cx: &mut ViewContext, ) { if let Some(capabilities) = capabilities { self.dap_store.update(cx, |store, cx| { - store.merge_capabilities_for_client(&client_id, capabilities, cx); + store.update_capabilities_for_client(&session_id, &client_id, capabilities, cx); }); cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id)); } - let send_breakpoints_task = self.workspace.update(cx, |workspace, cx| { - workspace - .project() - .update(cx, |project, cx| project.send_breakpoints(&client_id, cx)) - }); - - let configuration_done_task = self - .dap_store - .update(cx, |store, cx| store.configuration_done(&client_id, cx)); + let session_id = *session_id; + let client_id = *client_id; - cx.background_executor() - .spawn(async move { - send_breakpoints_task?.await; + cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |debug_panel, cx| { + debug_panel.workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.send_breakpoints(&session_id, &client_id, cx) + }) + }) + })?? + .await; - configuration_done_task.await - }) - .detach_and_log_err(cx); + this.update(&mut cx, |debug_panel, cx| { + debug_panel + .dap_store + .update(cx, |store, cx| store.configuration_done(&client_id, cx)) + })? + .await + }) + .detach_and_log_err(cx); } fn handle_continued_event( @@ -594,6 +625,7 @@ impl DebugPanel { fn handle_stopped_event( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, event: &StoppedEvent, cx: &mut ViewContext, @@ -602,18 +634,19 @@ impl DebugPanel { return; }; - let Some(client_name) = self + let Some(session_name) = self .dap_store .read(cx) - .client_by_id(client_id) - .map(|client| client.config().label) + .session_by_id(session_id) + .map(|session| session.read(cx).name()) else { return; // this can never happen }; + let session_id = *session_id; let client_id = *client_id; - let client_name = SharedString::from(client_name); + let session_name = SharedString::from(session_name); cx.spawn({ let event = event.clone(); @@ -640,8 +673,9 @@ impl DebugPanel { this.workspace.clone(), this.dap_store.clone(), thread_state.clone(), + &session_id, &client_id, - client_name, + session_name, thread_id, cx, ) @@ -721,6 +755,7 @@ impl DebugPanel { fn handle_terminated_event( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, event: &Option, cx: &mut ViewContext, @@ -747,7 +782,9 @@ impl DebugPanel { .restart(&client_id, restart_args, cx) .detach_and_log_err(cx); } else { - store.shutdown_client(&client_id, cx).detach_and_log_err(cx); + store + .shutdown_session(&session_id, cx) + .detach_and_log_err(cx); } }); @@ -818,6 +855,7 @@ impl DebugPanel { payload: &SetDebuggerPanelItem, cx: &mut ViewContext, ) { + let session_id = DebugSessionId::from_proto(payload.session_id); let client_id = DebugAdapterClientId::from_proto(payload.client_id); let thread_id = payload.thread_id; let thread_state = payload.thread_state.clone().unwrap(); @@ -843,8 +881,9 @@ impl DebugPanel { self.workspace.clone(), self.dap_store.clone(), thread_state, + &session_id, &client_id, - payload.client_name.clone().into(), + payload.session_name.clone().into(), thread_id, cx, ) @@ -884,60 +923,18 @@ impl DebugPanel { fn handle_capabilities_changed_event( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, event: &CapabilitiesEvent, cx: &mut ViewContext, ) { self.dap_store.update(cx, |store, cx| { - store.merge_capabilities_for_client(client_id, &event.capabilities, cx); + store.update_capabilities_for_client(session_id, client_id, &event.capabilities, cx); }); cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id)); } - pub fn open_remote_debug_panel_item( - &self, - client_id: DebugAdapterClientId, - thread_id: u64, - cx: &mut ViewContext, - ) -> View { - let existing_item = self.pane.read(cx).items().find_map(|item| { - let item = item.downcast::()?; - let item_ref = item.read(cx); - - if item_ref.client_id() == client_id && item_ref.thread_id() == thread_id { - Some(item) - } else { - None - } - }); - - if let Some(existing_item) = existing_item { - return existing_item; - } - - let debug_panel = cx.view().clone(); - - let debug_panel_item = cx.new_view(|cx| { - DebugPanelItem::new( - debug_panel, - self.workspace.clone(), - self.dap_store.clone(), - cx.new_model(|_| Default::default()), // TODO debugger: change this - &client_id, - SharedString::from("test"), // TODO debugger: change this - thread_id, - cx, - ) - }); - - self.pane.update(cx, |pane, cx| { - pane.add_item(Box::new(debug_panel_item.clone()), true, true, None, cx); - }); - - debug_panel_item - } - fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement { const TITLE: &'static str = "Debug session exited without hitting any breakpoints"; const DESCRIPTION: &'static str = diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index dfcd6f09632f6e..238d439fca5957 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -6,6 +6,7 @@ use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; use dap::proto_conversions; +use dap::session::DebugSessionId; use dap::{ client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, @@ -65,14 +66,15 @@ impl ThreadItem { pub struct DebugPanelItem { thread_id: u64, - remote_id: Option, console: View, - show_console_indicator: bool, focus_handle: FocusHandle, + remote_id: Option, + session_name: SharedString, dap_store: Model, + session_id: DebugSessionId, output_editor: View, + show_console_indicator: bool, module_list: View, - client_name: SharedString, active_thread_item: ThreadItem, workspace: WeakView, client_id: DebugAdapterClientId, @@ -90,8 +92,9 @@ impl DebugPanelItem { workspace: WeakView, dap_store: Model, thread_state: Model, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, - client_name: SharedString, + session_name: SharedString, thread_id: u64, cx: &mut ViewContext, ) -> Self { @@ -185,7 +188,7 @@ impl DebugPanelItem { thread_id, dap_store, workspace, - client_name, + session_name, module_list, thread_state, focus_handle, @@ -196,6 +199,7 @@ impl DebugPanelItem { stack_frame_list, loaded_source_list, client_id: *client_id, + session_id: *session_id, show_console_indicator: false, active_thread_item: ThreadItem::Variables, } @@ -209,6 +213,7 @@ impl DebugPanelItem { SetDebuggerPanelItem { project_id, + session_id: self.session_id.to_proto(), client_id: self.client_id.to_proto(), thread_id: self.thread_id, console: None, @@ -218,7 +223,7 @@ impl DebugPanelItem { variable_list, stack_frame_list, loaded_source_list: None, - client_name: self.client_name.to_string(), + session_name: self.session_name.to_string(), } } @@ -423,19 +428,19 @@ impl DebugPanelItem { return; } - self.dap_store.update(cx, |dap_store, _| { - if let Some((downstream_client, project_id)) = dap_store.downstream_client() { - let message = proto_conversions::capabilities_to_proto( - &dap_store.capabilities_by_id(client_id), - *project_id, - client_id.to_proto(), - ); - - downstream_client.send(message).log_err(); - } - }); - + // notify the view that the capabilities have changed cx.notify(); + + if let Some((downstream_client, project_id)) = self.dap_store.read(cx).downstream_client() { + let message = proto_conversions::capabilities_to_proto( + &self.dap_store.read(cx).capabilities_by_id(client_id), + *project_id, + self.session_id.to_proto(), + self.client_id.to_proto(), + ); + + downstream_client.send(message).log_err(); + } } pub(crate) fn update_adapter( @@ -467,6 +472,10 @@ impl DebugPanelItem { } } + pub fn session_id(&self) -> DebugSessionId { + self.session_id + } + pub fn client_id(&self) -> DebugAdapterClientId { self.client_id } @@ -496,8 +505,7 @@ impl DebugPanelItem { } pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { - self.dap_store - .read_with(cx, |store, _| store.capabilities_by_id(&self.client_id)) + self.dap_store.read(cx).capabilities_by_id(&self.client_id) } fn clear_highlights(&self, cx: &mut ViewContext) { @@ -647,7 +655,12 @@ impl DebugPanelItem { pub fn stop_thread(&self, cx: &mut ViewContext) { self.dap_store.update(cx, |store, cx| { store - .terminate_threads(&self.client_id, Some(vec![self.thread_id; 1]), cx) + .terminate_threads( + &self.session_id, + &self.client_id, + Some(vec![self.thread_id; 1]), + cx, + ) .detach_and_log_err(cx) }); } @@ -665,7 +678,7 @@ impl DebugPanelItem { .update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { project - .toggle_ignore_breakpoints(&self.client_id, cx) + .toggle_ignore_breakpoints(&self.session_id, &self.client_id, cx) .detach_and_log_err(cx); }) }) @@ -689,7 +702,7 @@ impl Item for DebugPanelItem { params: workspace::item::TabContentParams, _: &WindowContext, ) -> AnyElement { - Label::new(format!("{} - Thread {}", self.client_name, self.thread_id)) + Label::new(format!("{} - Thread {}", self.session_name, self.thread_id)) .color(if params.selected { Color::Default } else { @@ -701,7 +714,7 @@ impl Item for DebugPanelItem { fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(SharedString::from(format!( "{} Thread {} - {:?}", - self.client_name, + self.session_name, self.thread_id, self.thread_state.read(cx).status, ))) @@ -896,7 +909,9 @@ impl Render for DebugPanelItem { .child( IconButton::new( "debug-ignore-breakpoints", - if self.dap_store.read(cx).ignore_breakpoints(&self.client_id) { + if self.dap_store.update(cx, |store, cx| { + store.ignore_breakpoints(&self.session_id, cx) + }) { IconName::DebugIgnoreBreakpoints } else { IconName::DebugBreakpoint diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 6465b20246fe38..fe6e1340a67fec 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -38,7 +38,7 @@ pub fn init(cx: &mut AppContext) { .register_action(|workspace: &mut Workspace, _: &ShutdownDebugAdapters, cx| { workspace.project().update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.shutdown_clients(cx).detach(); + store.shutdown_sessions(cx).detach(); }) }) }) diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index bd6684ed3a763e..a5e200bd2601b4 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -50,7 +50,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -64,7 +64,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -455,7 +455,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index a9c14a97335e55..2409926ad57ff1 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -17,7 +17,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -31,7 +31,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -100,7 +100,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -134,7 +134,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -148,7 +148,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -248,7 +248,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -282,7 +282,7 @@ async fn test_client_can_open_multiple_thread_panels( let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -296,7 +296,7 @@ async fn test_client_can_open_multiple_thread_panels( }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -396,7 +396,7 @@ async fn test_client_can_open_multiple_thread_panels( let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -427,7 +427,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -441,7 +441,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -555,7 +555,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 316160e8b02c35..2875b2e08368e7 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -51,7 +51,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -65,7 +65,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -173,7 +173,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -215,7 +215,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -229,7 +229,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -420,7 +420,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 5f9de616458e3a..5a59846d8ddb3c 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -47,7 +47,7 @@ async fn test_basic_fetch_initial_scope_and_variables( let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -61,7 +61,7 @@ async fn test_basic_fetch_initial_scope_and_variables( }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -254,7 +254,7 @@ async fn test_basic_fetch_initial_scope_and_variables( let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -297,7 +297,7 @@ async fn test_fetch_variables_for_multiple_scopes( let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -311,7 +311,7 @@ async fn test_fetch_variables_for_multiple_scopes( }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -554,7 +554,7 @@ async fn test_fetch_variables_for_multiple_scopes( let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -594,7 +594,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T let task = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.start_test_client( + store.start_debug_session( task::DebugAdapterConfig { label: "test config".into(), kind: task::DebugAdapterKind::Fake, @@ -608,7 +608,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T }) }); - let client = task.await.unwrap(); + let (session, client) = task.await.unwrap(); client .on_request::(move |_, _| { @@ -1105,7 +1105,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T let shutdown_client = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&client.id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 2965bab415693a..4486172e7d1591 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -831,12 +831,12 @@ impl VariableList { ) { let this = cx.view().clone(); - let support_set_variable = self.dap_store.read_with(cx, |store, _| { - store - .capabilities_by_id(&self.client_id) - .supports_set_variable - .unwrap_or_default() - }); + let support_set_variable = self + .dap_store + .read(cx) + .capabilities_by_id(&self.client_id) + .supports_set_variable + .unwrap_or_default(); let context_menu = ContextMenu::build(cx, |menu, cx| { menu.entry( diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 4bd739043dfed1..783dbff000223c 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,8 +1,9 @@ use crate::project_settings::ProjectSettings; use crate::{ProjectEnvironment, ProjectItem as _, ProjectPath}; -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; +use dap::session::{DebugSession, DebugSessionId}; use dap::{ adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}, client::{DebugAdapterClient, DebugAdapterClientId}, @@ -28,7 +29,7 @@ use dap_adapters::build_adapter; use fs::Fs; use futures::future::Shared; use futures::FutureExt; -use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task}; +use gpui::{AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task}; use http_client::HttpClient; use language::{ proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, @@ -56,9 +57,10 @@ use text::Point; use util::{merge_json_value_into, ResultExt as _}; pub enum DapStoreEvent { - DebugClientStarted(DebugAdapterClientId), + DebugClientStarted((DebugSessionId, DebugAdapterClientId)), DebugClientShutdown(DebugAdapterClientId), DebugClientEvent { + session_id: DebugSessionId, client_id: DebugAdapterClientId, message: Message, }, @@ -69,11 +71,6 @@ pub enum DapStoreEvent { UpdateDebugAdapter(UpdateDebugAdapter), } -pub enum DebugAdapterClientState { - Starting(Task>>), - Running(Arc), -} - #[allow(clippy::large_enum_variant)] pub enum DapStoreMode { Local(LocalDapStore), // ssh host and collab host @@ -81,8 +78,31 @@ pub enum DapStoreMode { } pub struct LocalDapStore { + next_client_id: AtomicUsize, + next_session_id: AtomicUsize, delegate: DapAdapterDelegate, environment: Model, + sessions: HashMap>, + client_by_session: HashMap, +} + +impl LocalDapStore { + fn next_client_id(&self) -> DebugAdapterClientId { + DebugAdapterClientId(self.next_client_id.fetch_add(1, SeqCst)) + } + + fn next_session_id(&self) -> DebugSessionId { + DebugSessionId(self.next_session_id.fetch_add(1, SeqCst)) + } + + pub fn session_by_client_id( + &self, + client_id: &DebugAdapterClientId, + ) -> Option> { + self.sessions + .get(self.client_by_session.get(client_id)?) + .cloned() + } } pub struct RemoteDapStore { @@ -92,12 +112,9 @@ pub struct RemoteDapStore { pub struct DapStore { mode: DapStoreMode, - next_client_id: AtomicUsize, downstream_client: Option<(AnyProtoClient, u64)>, - ignore_breakpoints: HashSet, breakpoints: BTreeMap>, capabilities: HashMap, - clients: HashMap, active_debug_line: Option<(DebugAdapterClientId, ProjectPath, u32)>, } @@ -124,11 +141,14 @@ impl DapStore { environment: Model, cx: &mut ModelContext, ) -> Self { - cx.on_app_quit(Self::shutdown_clients).detach(); + cx.on_app_quit(Self::shutdown_sessions).detach(); Self { mode: DapStoreMode::Local(LocalDapStore { environment, + sessions: HashMap::default(), + next_client_id: Default::default(), + next_session_id: Default::default(), delegate: DapAdapterDelegate::new( Some(http_client.clone()), Some(node_runtime.clone()), @@ -136,14 +156,12 @@ impl DapStore { languages.clone(), Task::ready(None).shared(), ), + client_by_session: Default::default(), }), downstream_client: None, active_debug_line: None, - clients: HashMap::default(), breakpoints: Default::default(), - capabilities: HashMap::default(), - next_client_id: Default::default(), - ignore_breakpoints: Default::default(), + capabilities: Default::default(), } } @@ -159,11 +177,8 @@ impl DapStore { }), downstream_client: None, active_debug_line: None, - clients: HashMap::default(), breakpoints: Default::default(), - capabilities: HashMap::default(), - next_client_id: Default::default(), - ignore_breakpoints: Default::default(), + capabilities: Default::default(), } } @@ -208,22 +223,32 @@ impl DapStore { self.downstream_client.as_ref() } - pub fn next_client_id(&self) -> DebugAdapterClientId { - DebugAdapterClientId(self.next_client_id.fetch_add(1, SeqCst)) + pub fn sessions(&self) -> impl Iterator> + '_ { + self.as_local().unwrap().sessions.values().cloned() } - pub fn running_clients(&self) -> impl Iterator> + '_ { - self.clients.values().filter_map(|state| match state { - DebugAdapterClientState::Starting(_) => None, - DebugAdapterClientState::Running(client) => Some(client.clone()), - }) + pub fn session_by_id(&self, session_id: &DebugSessionId) -> Option> { + self.as_local() + .and_then(|store| store.sessions.get(session_id).cloned()) } - pub fn client_by_id(&self, id: &DebugAdapterClientId) -> Option> { - self.clients.get(id).and_then(|state| match state { - DebugAdapterClientState::Starting(_) => None, - DebugAdapterClientState::Running(client) => Some(client.clone()), - }) + pub fn session_by_client_id( + &self, + client_id: &DebugAdapterClientId, + ) -> Option> { + self.as_local() + .and_then(|store| store.session_by_client_id(client_id)) + } + + pub fn client_by_id( + &self, + client_id: &DebugAdapterClientId, + cx: &mut ModelContext, + ) -> Option<(Model, Arc)> { + let session = self.session_by_client_id(client_id)?; + let client = session.read(cx).client_by_id(client_id)?; + + Some((session, client)) } pub fn capabilities_by_id(&self, client_id: &DebugAdapterClientId) -> Capabilities { @@ -233,16 +258,30 @@ impl DapStore { .unwrap_or_default() } - pub fn merge_capabilities_for_client( + pub fn update_capabilities_for_client( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, - other: &Capabilities, + capabilities: &Capabilities, cx: &mut ModelContext, ) { - if let Some(capabilities) = self.capabilities.get_mut(client_id) { - *capabilities = capabilities.merge(other.clone()); + if let Some(old_capabilities) = self.capabilities.get_mut(client_id) { + *old_capabilities = old_capabilities.merge(capabilities.clone()); + } else { + self.capabilities.insert(*client_id, capabilities.clone()); + } - cx.notify(); + cx.notify(); + + if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { + downstream_client + .send(dap::proto_conversions::capabilities_to_proto( + &capabilities, + *project_id, + session_id.to_proto(), + client_id.to_proto(), + )) + .log_err(); } } @@ -303,13 +342,21 @@ impl DapStore { &self.breakpoints } - pub fn ignore_breakpoints(&self, client_id: &DebugAdapterClientId) -> bool { - self.ignore_breakpoints.contains(client_id) + pub fn ignore_breakpoints(&self, session_id: &DebugSessionId, cx: &ModelContext) -> bool { + self.session_by_id(session_id) + .map(|session| session.read(cx).ignore_breakpoints()) + .unwrap_or_default() } - pub fn toggle_ignore_breakpoints(&mut self, client_id: &DebugAdapterClientId) { - if !self.ignore_breakpoints.remove(client_id) { - self.ignore_breakpoints.insert(*client_id); + pub fn toggle_ignore_breakpoints( + &mut self, + session_id: &DebugSessionId, + cx: &mut ModelContext, + ) { + if let Some(session) = self.session_by_id(session_id) { + session.update(cx, |session, cx| { + session.set_ignore_breakpoints(!session.ignore_breakpoints(), cx); + }); } } @@ -393,27 +440,23 @@ impl DapStore { fn reconnect_client( &mut self, + session_id: &DebugSessionId, adapter: Arc, binary: DebugAdapterBinary, config: DebugAdapterConfig, cx: &mut ModelContext, ) -> Task> { - let Some(_) = self.as_local() else { - return Task::ready(Err(anyhow!( - "starting a debug client is not supported in remote setting" - ))); - }; - if !adapter.supports_attach() && matches!(config.request, DebugRequestType::Attach(_)) { return Task::ready(Err(anyhow!( "Debug adapter does not support `attach` request" ))); } - let client_id = self.next_client_id(); + let session_id = *session_id; + let client_id = self.as_local().unwrap().next_client_id(); cx.spawn(|dap_store, mut cx| async move { - let mut client = DebugAdapterClient::new(client_id, config, adapter, binary, &cx); + let mut client = DebugAdapterClient::new(client_id, adapter, binary, &cx); client .reconnect( @@ -422,7 +465,11 @@ impl DapStore { move |message, cx| { dap_store .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + cx.emit(DapStoreEvent::DebugClientEvent { + session_id, + client_id, + message, + }) }) .log_err(); } @@ -431,81 +478,41 @@ impl DapStore { ) .await?; - let client = Arc::new(client); - dap_store.update(&mut cx, |store, cx| { store - .clients - .insert(client_id, DebugAdapterClientState::Running(client)); - - cx.emit(DapStoreEvent::DebugClientStarted(client_id)); - }) - }) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn start_test_client( - &mut self, - config: DebugAdapterConfig, - cx: &mut ModelContext, - ) -> Task>> { - use task::DebugAdapterKind; - - let client_id = self.next_client_id(); - - cx.spawn(|this, mut cx| async move { - let adapter = build_adapter(&DebugAdapterKind::Fake).await?; - - let mut client = DebugAdapterClient::new( - client_id, - config, - adapter, - DebugAdapterBinary { - command: "command".into(), - arguments: None, - envs: None, - cwd: None, - }, - &cx, - ); - - client - .start( - { - let dap_store = this.clone(); - move |message, cx| { - dap_store - .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) - }) - .log_err(); - } - }, - &mut cx, - ) - .await?; + .as_local_mut() + .unwrap() + .client_by_session + .insert(client_id, session_id); - let client = Arc::new(client); + let session = store.session_by_id(&session_id).unwrap(); - this.update(&mut cx, |store, cx| { - store - .clients - .insert(client_id, DebugAdapterClientState::Running(client.clone())); + let client = Arc::new(client); - cx.emit(DapStoreEvent::DebugClientStarted(client_id)); + session.update(cx, |session, cx| { + session.update_configuration( + |old_config| { + *old_config = config.clone(); + }, + cx, + ); + session.add_client(client.clone(), cx); + }); - client + cx.emit(DapStoreEvent::DebugClientStarted((session_id, client_id))); + cx.notify(); }) }) } - pub fn start_client_from_debug_config( + fn start_client_internal( &mut self, + session_id: DebugSessionId, config: DebugAdapterConfig, cx: &mut ModelContext, - ) { + ) -> Task>> { let Some(local_store) = self.as_local_mut() else { - return; + return Task::ready(Err(anyhow!("cannot start client on remote side"))); }; let mut adapter_delegate = local_store.delegate.clone(); @@ -515,40 +522,23 @@ impl DapStore { })); let adapter_delegate = Arc::new(adapter_delegate); - let client_id = self.next_client_id(); + let client_id = self.as_local().unwrap().next_client_id(); - let start_client_task = cx.spawn(|this, mut cx| async move { - let adapter = match build_adapter(&config.kind).await { - Ok(adapter) => adapter, - Err(error) => { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(error.to_string())); - }) - .log_err()?; - return None; - } - }; + cx.spawn(|this, mut cx| async move { + let adapter = build_adapter(&config.kind).await?; if !adapter.supports_attach() && matches!(config.request, DebugRequestType::Attach(_)) { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::Notification( - "Debug adapter does not support `attach` request".to_string(), - )); - }) - .log_err()?; - return None; + bail!("Debug adapter does not support `attach` request"); } - let binary = cx - .update(|cx| { - let name = DebugAdapterName::from(adapter.name().as_ref()); + let binary = cx.update(|cx| { + let name = DebugAdapterName::from(adapter.name().as_ref()); - ProjectSettings::get_global(cx) - .dap - .get(&name) - .and_then(|s| s.binary.as_ref().map(PathBuf::from)) - }) - .log_err()?; + ProjectSettings::get_global(cx) + .dap + .get(&name) + .and_then(|s| s.binary.as_ref().map(PathBuf::from)) + })?; let (adapter, binary) = match adapter .get_binary(adapter_delegate.as_ref(), &config, binary) @@ -562,7 +552,7 @@ impl DapStore { }, ); - return None; + return Err(error); } Ok(mut binary) => { adapter_delegate.update_status(adapter.name(), DapStatus::None); @@ -576,66 +566,95 @@ impl DapStore { } }; - let mut client = DebugAdapterClient::new(client_id, config, adapter, binary, &cx); + let mut client = DebugAdapterClient::new(client_id, adapter, binary, &cx); - let result = client + client .start( { let dap_store = this.clone(); move |message, cx| { dap_store .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + cx.emit(DapStoreEvent::DebugClientEvent { + session_id, + client_id, + message, + }) }) .log_err(); } }, &mut cx, ) - .await; + .await?; - if let Err(error) = result { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(error.to_string())); - }) - .log_err()?; - return None; - } + Ok(Arc::new(client)) + }) + } - let client = Arc::new(client); + pub fn start_debug_session( + &mut self, + config: DebugAdapterConfig, + cx: &mut ModelContext, + ) -> Task, Arc)>> { + let Some(local_store) = self.as_local() else { + return Task::ready(Err(anyhow!("cannot start session on remote side"))); + }; - this.update(&mut cx, |store, cx| { - let handle = store - .clients - .get_mut(&client_id) - .with_context(|| "Failed to find starting debug client")?; + let session_id = local_store.next_session_id(); + let start_client_task = self.start_client_internal(session_id, config.clone(), cx); - *handle = DebugAdapterClientState::Running(client.clone()); + cx.spawn(|this, mut cx| async move { + let session = cx.new_model(|_| DebugSession::new(session_id, config))?; - cx.emit(DapStoreEvent::DebugClientStarted(client_id)); + let client = match start_client_task.await { + Ok(client) => client, + Err(error) => { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + .log_err(); - anyhow::Ok(()) - }) - .log_err(); + return Err(error); + } + }; - Some(client) - }); + this.update(&mut cx, |store, cx| { + session.update(cx, |session, cx| { + session.add_client(client.clone(), cx); + }); - self.clients.insert( - client_id, - DebugAdapterClientState::Starting(start_client_task), - ); + let client_id = client.id(); + + let local_store = store.as_local_mut().unwrap(); + local_store.client_by_session.insert(client_id, session_id); + local_store.sessions.insert(session_id, session.clone()); + + cx.emit(DapStoreEvent::DebugClientStarted((session_id, client_id))); + cx.notify(); + + (session, client) + }) + }) } pub fn initialize( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + let Some((_, client)) = self.client_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!( + "Could not find debug client: {:?} for session {:?}", + client_id, + session_id + ))); }; + let session_id = *session_id; + let client_id = *client_id; + cx.spawn(|this, mut cx| async move { let capabilities = client .request::(InitializeRequestArguments { @@ -659,34 +678,28 @@ impl DapStore { .await?; this.update(&mut cx, |store, cx| { - if let Some((downstream_client, project_id)) = store.downstream_client.as_ref() { - let message = dap::proto_conversions::capabilities_to_proto( - &capabilities.clone(), - *project_id, - client.id().to_proto(), - ); - - downstream_client.send(message).log_err(); - } - store.capabilities.insert(client.id(), capabilities); - - cx.notify(); + store.update_capabilities_for_client(&session_id, &client_id, &capabilities, cx); }) }) } pub fn launch( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Client was not found"))); + let Some((session, client)) = self.client_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!( + "Could not find debug client: {:?} for session {:?}", + client_id, + session_id + ))); }; - let mut adapter_args = client.adapter().request_args(&client.config()); - - if let Some(args) = client.config().initialize_args.clone() { + let config = session.read(cx).configuration(); + let mut adapter_args = client.adapter().request_args(&config); + if let Some(args) = config.initialize_args.clone() { merge_json_value_into(args, &mut adapter_args); } @@ -699,20 +712,34 @@ impl DapStore { pub fn attach( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, - pid: u32, + process_id: u32, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Client was not found"))); + let Some((session, client)) = self.client_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!( + "Could not find debug client: {:?} for session {:?}", + client_id, + session_id + ))); }; // update the process id on the config, so when the `startDebugging` reverse request // comes in we send another `attach` request with the already selected PID // If we don't do this the user has to select the process twice if the adapter sends a `startDebugging` request - client.set_process_id(pid); + session.update(cx, |session, cx| { + session.update_configuration( + |config| { + config.request = DebugRequestType::Attach(task::AttachConfig { + process_id: Some(process_id), + }); + }, + cx, + ); + }); - let config = client.config(); + let config = session.read(cx).configuration(); let mut adapter_args = client.adapter().request_args(&config); if let Some(args) = config.initialize_args.clone() { @@ -731,13 +758,15 @@ impl DapStore { client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task>> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Client was not found"))); }; - let capabilities = self.capabilities_by_id(client_id); - - if !capabilities.supports_modules_request.unwrap_or_default() { + if !self + .capabilities_by_id(client_id) + .supports_modules_request + .unwrap_or_default() + { return Task::ready(Ok(Vec::default())); } @@ -757,13 +786,12 @@ impl DapStore { client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task>> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Client was not found"))); }; - let capabilities = self.capabilities_by_id(client_id); - - if !capabilities + if !self + .capabilities_by_id(client_id) .supports_loaded_sources_request .unwrap_or_default() { @@ -784,7 +812,7 @@ impl DapStore { thread_id: u64, cx: &mut ModelContext, ) -> Task>> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Client was not found"))); }; @@ -807,7 +835,7 @@ impl DapStore { stack_frame_id: u64, cx: &mut ModelContext, ) -> Task>> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Client was not found"))); }; @@ -826,48 +854,53 @@ impl DapStore { client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; - let capabilities = self.capabilities_by_id(client_id); - - cx.background_executor().spawn(async move { - let support_configuration_done_request = capabilities - .supports_configuration_done_request - .unwrap_or_default(); - - if support_configuration_done_request { + if self + .capabilities_by_id(client_id) + .supports_configuration_done_request + .unwrap_or_default() + { + cx.background_executor().spawn(async move { client .request::(ConfigurationDoneArguments) .await - } else { - Ok(()) - } - }) + }) + } else { + Task::ready(Ok(())) + } } pub fn respond_to_start_debugging( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, seq: u64, args: Option, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + let Some((session, client)) = self.client_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!( + "Could not find debug client: {:?} for session {:?}", + client_id, + session_id + ))); }; + let session_id = *session_id; + let config = session.read(cx).configuration().clone(); + cx.spawn(|this, mut cx| async move { let (success, body) = if let Some(args) = args { let task = this.update(&mut cx, |store, cx| { - let config = client.config(); - // Merge the new configuration over the existing configuration - let mut initialize_args = client.config().initialize_args.unwrap_or_default(); + let mut initialize_args = config.initialize_args.unwrap_or_default(); merge_json_value_into(args.configuration, &mut initialize_args); store.reconnect_client( + &session_id, client.adapter().clone(), client.binary().clone(), DebugAdapterConfig { @@ -900,7 +933,14 @@ impl DapStore { match task { Ok(task) => match task.await { Ok(_) => (true, None), - Err(_) => (false, None), + Err(error) => { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + .log_err(); + + (false, None) + } }, Err(_) => (false, None), } @@ -925,14 +965,19 @@ impl DapStore { pub fn respond_to_run_in_terminal( &self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, success: bool, seq: u64, shell_pid: Option, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + let Some((_, client)) = self.client_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!( + "Could not find debug client: {:?} for session {:?}", + client_id, + session_id + ))); }; cx.background_executor().spawn(async move { @@ -960,7 +1005,7 @@ impl DapStore { thread_id: u64, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -993,18 +1038,11 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { - if let Some(remote) = self.as_remote() { - if let Some(_client) = &remote.upstream_client { - // - } - } - - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); - let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests .unwrap_or_default(); @@ -1030,12 +1068,11 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); - let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests .unwrap_or_default(); @@ -1062,12 +1099,11 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); - let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests .unwrap_or_default(); @@ -1093,19 +1129,11 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; let capabilities = self.capabilities_by_id(client_id); - - if capabilities.supports_step_back.unwrap_or(false) { - return Task::ready(Err(anyhow!( - "Step back request isn't support for client_id: {:?}", - client_id - ))); - } - let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests .unwrap_or_default(); @@ -1113,15 +1141,19 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.background_executor().spawn(async move { - client - .request::(StepBackArguments { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - }) + if capabilities.supports_step_back.unwrap_or_default() { + cx.background_executor().spawn(async move { + client + .request::(StepBackArguments { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + }) + .await + }) + } else { + Task::ready(Ok(())) + } } pub fn variables( @@ -1130,7 +1162,7 @@ impl DapStore { variables_reference: u64, cx: &mut ModelContext, ) -> Task>> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1156,7 +1188,7 @@ impl DapStore { context: EvaluateArgumentsContext, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1183,7 +1215,7 @@ impl DapStore { completion_column: u64, cx: &mut ModelContext, ) -> Task>> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1211,7 +1243,7 @@ impl DapStore { evaluate_name: Option, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1251,7 +1283,7 @@ impl DapStore { thread_id: u64, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1261,17 +1293,17 @@ impl DapStore { pub fn terminate_threads( &mut self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, thread_ids: Option>, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; - let capabilities = self.capabilities_by_id(client_id); - - if capabilities + if self + .capabilities_by_id(client_id) .supports_terminate_threads_request .unwrap_or_default() { @@ -1281,7 +1313,7 @@ impl DapStore { .await }) } else { - self.shutdown_client(client_id, cx) + self.shutdown_session(session_id, cx) } } @@ -1290,7 +1322,7 @@ impl DapStore { client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1311,7 +1343,7 @@ impl DapStore { args: Option, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1339,11 +1371,15 @@ impl DapStore { }) } - pub fn shutdown_clients(&mut self, cx: &mut ModelContext) -> Task<()> { + pub fn shutdown_sessions(&mut self, cx: &mut ModelContext) -> Task<()> { + let Some(local_store) = self.as_local() else { + return Task::ready(()); + }; + let mut tasks = Vec::new(); - for client_id in self.clients.keys().cloned().collect::>() { - tasks.push(self.shutdown_client(&client_id, cx)); + for session_id in local_store.sessions.keys().cloned().collect::>() { + tasks.push(self.shutdown_session(&session_id, cx)); } cx.background_executor().spawn(async move { @@ -1351,48 +1387,65 @@ impl DapStore { }) } - pub fn shutdown_client( + pub fn shutdown_session( &mut self, - client_id: &DebugAdapterClientId, + session_id: &DebugSessionId, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.clients.remove(&client_id) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + let Some(local_store) = self.as_local_mut() else { + return Task::ready(Err(anyhow!("Cannot shutdown session on remote side"))); }; - cx.emit(DapStoreEvent::DebugClientShutdown(*client_id)); + let Some(session) = local_store.sessions.remove(session_id) else { + return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); + }; - self.ignore_breakpoints.remove(client_id); - let capabilities = self.capabilities.remove(client_id); + let mut tasks = Vec::new(); + for client in session.read(cx).clients().collect::>() { + tasks.push(self.shutdown_client(&session, client, cx)); + } - if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { - let request = proto::ShutdownDebugClient { - client_id: client_id.to_proto(), - project_id: *project_id, - }; + cx.background_executor().spawn(async move { + futures::future::join_all(tasks).await; + Ok(()) + }) + } - downstream_client.send(request).log_err(); - } + fn shutdown_client( + &mut self, + session: &Model, + client: Arc, + cx: &mut ModelContext, + ) -> Task> { + let Some(local_store) = self.as_local_mut() else { + return Task::ready(Err(anyhow!("Cannot shutdown client on remote side"))); + }; - cx.spawn(|_, _| async move { - let client = match client { - DebugAdapterClientState::Starting(task) => task.await, - DebugAdapterClientState::Running(client) => Some(client), - }; + let client_id = client.id(); - let Some(client) = client else { - return Ok(()); - }; + cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); - if capabilities - .and_then(|c| c.supports_terminate_request) - .unwrap_or_default() - { + local_store.client_by_session.remove(&client_id); + let capabilities = self.capabilities.remove(&client_id).unwrap_or_default(); + + if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { + downstream_client + .send(proto::ShutdownDebugClient { + session_id: session.read(cx).id().to_proto(), + client_id: client_id.to_proto(), + project_id: *project_id, + }) + .log_err(); + } + + cx.spawn(|_, _| async move { + if capabilities.supports_terminate_request.unwrap_or_default() { let _ = client .request::(TerminateArguments { restart: Some(false), }) - .await; + .await + .log_err(); } else { let _ = client .request::(DisconnectArguments { @@ -1400,7 +1453,8 @@ impl DapStore { terminate_debuggee: Some(true), suspend_debuggee: Some(false), }) - .await; + .await + .log_err(); } client.shutdown().await @@ -1490,15 +1544,12 @@ impl DapStore { mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |dap_store, cx| { - if dap_store.upstream_client().is_some() { - *dap_store - .capabilities - .entry(DebugAdapterClientId::from_proto(envelope.payload.client_id)) - .or_default() = - dap::proto_conversions::capabilities_from_proto(&envelope.payload); - - cx.notify(); - } + dap_store.update_capabilities_for_client( + &DebugSessionId::from_proto(envelope.payload.session_id), + &DebugAdapterClientId::from_proto(envelope.payload.client_id), + &dap::proto_conversions::capabilities_from_proto(&envelope.payload), + cx, + ); }) } @@ -1508,14 +1559,12 @@ impl DapStore { mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |dap_store, cx| { - if matches!(dap_store.mode, DapStoreMode::Remote(_)) { - let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); + let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); - dap_store.capabilities.remove(&client_id); + dap_store.capabilities.remove(&client_id); - cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); - cx.notify(); - } + cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); + cx.notify(); }) } @@ -1533,7 +1582,7 @@ impl DapStore { this.update(&mut cx, |store, cx| { store.active_debug_line = Some(( - DebugAdapterClientId(envelope.payload.client_id as usize), + DebugAdapterClientId::from_proto(envelope.payload.client_id), project_path, envelope.payload.row, )); @@ -1618,7 +1667,7 @@ impl DapStore { ignore: bool, cx: &mut ModelContext, ) -> Task> { - let Some(client) = self.client_by_id(client_id) else { + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1658,11 +1707,9 @@ impl DapStore { buffer_snapshot: BufferSnapshot, cx: &mut ModelContext, ) -> Task> { - let clients = self.running_clients().collect::>(); - - if clients.is_empty() { - return Task::ready(Ok(())); - } + let Some(local_store) = self.as_local() else { + return Task::ready(Err(anyhow!("cannot start session on remote side"))); + }; let Some(breakpoints) = self.breakpoints.get(project_path) else { return Task::ready(Ok(())); @@ -1674,19 +1721,26 @@ impl DapStore { .collect::>(); let mut tasks = Vec::new(); - for client in clients { - tasks.push(self.send_breakpoints( - &client.id(), - Arc::from(buffer_path.clone()), - source_breakpoints.clone(), - self.ignore_breakpoints(&client.id()), - cx, - )) + for session in local_store.sessions.values() { + let session = session.read(cx); + let ignore_breakpoints = self.ignore_breakpoints(&session.id(), cx); + for client in session.clients().collect::>() { + tasks.push(self.send_breakpoints( + &client.id(), + Arc::from(buffer_path.clone()), + source_breakpoints.clone(), + ignore_breakpoints, + cx, + )); + } } - cx.background_executor().spawn(async move { - futures::future::try_join_all(tasks).await?; + if tasks.is_empty() { + return Task::ready(Ok(())); + } + cx.background_executor().spawn(async move { + futures::future::join_all(tasks).await; Ok(()) }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index dd2aafc97fd462..2ce5178f3f9fb7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -34,6 +34,7 @@ use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, debugger_settings::DebuggerSettings, messages::Message, + session::DebugSessionId, }; use collections::{BTreeSet, HashMap, HashSet}; @@ -251,11 +252,12 @@ pub enum Event { }, LanguageServerPrompt(LanguageServerPromptRequest), LanguageNotFound(Model), - DebugClientStarted(DebugAdapterClientId), + DebugClientStarted((DebugSessionId, DebugAdapterClientId)), DebugClientShutdown(DebugAdapterClientId), SetDebugClient(SetDebuggerPanelItem), ActiveDebugLineChanged, DebugClientEvent { + session_id: DebugSessionId, client_id: DebugAdapterClientId, message: Message, }, @@ -1240,6 +1242,7 @@ impl Project { pub fn send_breakpoints( &self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task<()> { @@ -1256,7 +1259,7 @@ impl Project { client_id, abs_path, source_breakpoints, - store.ignore_breakpoints(client_id), + store.ignore_breakpoints(session_id, cx), cx, ) })); @@ -1272,9 +1275,9 @@ impl Project { debug_task: task::ResolvedTask, cx: &mut ModelContext, ) { - if let Some(adapter_config) = debug_task.debug_adapter_config() { + if let Some(config) = debug_task.debug_adapter_config() { self.dap_store.update(cx, |store, cx| { - store.start_client_from_debug_config(adapter_config, cx); + store.start_debug_session(config, cx).detach_and_log_err(cx); }); } } @@ -1354,11 +1357,12 @@ impl Project { pub fn toggle_ignore_breakpoints( &self, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task> { let tasks = self.dap_store.update(cx, |store, cx| { - store.toggle_ignore_breakpoints(client_id); + store.toggle_ignore_breakpoints(session_id, cx); let mut tasks = Vec::new(); @@ -1387,7 +1391,7 @@ impl Project { .into_iter() .map(|breakpoint| breakpoint.to_source_breakpoint(buffer)) .collect::>(), - store.ignore_breakpoints(client_id), + store.ignore_breakpoints(session_id, cx), cx, ), ); @@ -2502,8 +2506,13 @@ impl Project { DapStoreEvent::DebugClientShutdown(client_id) => { cx.emit(Event::DebugClientShutdown(*client_id)); } - DapStoreEvent::DebugClientEvent { client_id, message } => { + DapStoreEvent::DebugClientEvent { + session_id, + client_id, + message, + } => { cx.emit(Event::DebugClientEvent { + session_id: *session_id, client_id: *client_id, message: message.clone(), }); @@ -4593,21 +4602,6 @@ impl Project { .language_servers_for_local_buffer(buffer, cx) } - pub fn debug_clients<'a>( - &'a self, - cx: &'a AppContext, - ) -> impl 'a + Iterator> { - self.dap_store.read(cx).running_clients() - } - - pub fn debug_client_for_id( - &self, - id: &DebugAdapterClientId, - cx: &AppContext, - ) -> Option> { - self.dap_store.read(cx).client_by_id(id) - } - pub fn buffer_store(&self) -> &Model { &self.buffer_store } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 672a46433fbaad..83f5cfca47c150 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2420,21 +2420,23 @@ enum BreakpointKind { } message ShutdownDebugClient { - uint64 client_id = 1; - uint64 project_id = 2; + uint64 session_id = 1; + uint64 client_id = 2; + uint64 project_id = 3; } message SetDebugClientCapabilities { -uint64 client_id = 1; -uint64 project_id = 2; -bool supports_loaded_sources_request = 3; -bool supports_modules_request = 4; -bool supports_restart_request = 5; -bool supports_set_expression = 6; -bool supports_single_thread_execution_requests = 7; -bool supports_step_back = 8; -bool supports_stepping_granularity = 9; -bool supports_terminate_threads_request = 10; + uint64 session_id = 1; + uint64 client_id = 2; + uint64 project_id = 3; + bool supports_loaded_sources_request = 4; + bool supports_modules_request = 5; + bool supports_restart_request = 6; + bool supports_set_expression = 7; + bool supports_single_thread_execution_requests = 8; + bool supports_step_back = 9; + bool supports_stepping_granularity = 10; + bool supports_terminate_threads_request = 11; } message Breakpoint { @@ -2601,16 +2603,17 @@ message DebuggerModuleList { message SetDebuggerPanelItem { uint64 project_id = 1; - uint64 client_id = 2; - uint64 thread_id = 3; - DebuggerConsole console = 4; - DebuggerModuleList module_list = 5; - DebuggerThreadItem active_thread_item = 6; - DebuggerThreadState thread_state = 7; - DebuggerVariableList variable_list = 8; - DebuggerStackFrameList stack_frame_list = 9; - DebuggerLoadedSourceList loaded_source_list = 10; - string client_name = 11; + uint64 session_id = 2; + uint64 client_id = 3; + uint64 thread_id = 4; + string session_name = 5; + DebuggerConsole console = 6; + DebuggerModuleList module_list = 7; + DebuggerThreadItem active_thread_item = 8; + DebuggerThreadState thread_state = 9; + DebuggerVariableList variable_list = 10; + DebuggerStackFrameList stack_frame_list = 11; + DebuggerLoadedSourceList loaded_source_list = 12; } message UpdateDebugAdapter { diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index cb3b4017d8893e..190f5a19d2f642 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -106,16 +106,12 @@ impl ResolvedTask { match self.original_task.task_type.clone() { TaskType::Script => None, TaskType::Debug(mut adapter_config) => { - let (cwd, program) = match &self.resolved { - None => (adapter_config.cwd, adapter_config.program), - Some(spawn_in_terminal) => ( - spawn_in_terminal.cwd.clone(), - spawn_in_terminal.program.clone(), - ), - }; + if let Some(resolved) = &self.resolved { + adapter_config.label = resolved.label.clone(); + adapter_config.program = resolved.program.clone().or(adapter_config.program); + adapter_config.cwd = resolved.cwd.clone().or(adapter_config.cwd); + } - adapter_config.program = program; - adapter_config.cwd = cwd; Some(adapter_config) } } From b8c89d3d8a98a1aefd39dbbddc1cc310c9ff18fd Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 26 Dec 2024 16:56:35 +0100 Subject: [PATCH 408/650] Fix formatting --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bca605deb79e6a..da10da145fdd9b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -130,8 +130,8 @@ use multi_buffer::{ }; use parking_lot::Mutex; use project::{ - dap_store::{Breakpoint, BreakpointKind, DapStore}, buffer_store::BufferChangeSet, + dap_store::{Breakpoint, BreakpointKind, DapStore}, lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle}, project_settings::{GitGutterSetting, ProjectSettings}, CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, From b911fb1c9a81d3a9fdb122160ec4f730c329e53b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 26 Dec 2024 17:08:30 +0100 Subject: [PATCH 409/650] Fix failing debug task test --- crates/task/src/task_template.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 1dac7e160262ed..bfd0329de7e8a7 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -113,6 +113,7 @@ mod deserialization_tests { initialize_args: None, }; let json = json!({ + "label": "test config", "type": "debug", "adapter": "python", "program": "main" From 569f500b52a2b98677723c756724713d35166c93 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 26 Dec 2024 18:32:59 +0100 Subject: [PATCH 410/650] Remove not needed hash derive --- crates/editor/src/display_map.rs | 2 +- crates/editor/src/display_map/block_map.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 1e7fd9104faa2a..468cd09263b7cb 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1325,7 +1325,7 @@ impl DisplaySnapshot { } } -#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq, Hash)] +#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct DisplayPoint(BlockPoint); impl Debug for DisplayPoint { diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 67c0e87ce0c787..33242e6c8c53d7 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -70,7 +70,7 @@ impl From for ElementId { } } -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct BlockPoint(pub Point); #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] From ca970dd77deace30866d0feb8be182f00c1d2a23 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 26 Dec 2024 23:04:57 +0100 Subject: [PATCH 411/650] Add test option for faking reverse requests --- crates/dap/src/client.rs | 85 +++++++++++++++++++++++++++++- crates/dap/src/transport.rs | 101 +++++++++++++++++++++--------------- 2 files changed, 143 insertions(+), 43 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 85e7d875e1b0d6..2f5fa0d0ae5146 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -270,6 +270,17 @@ impl DebugAdapterClient { transport.as_fake().on_request::(handler).await; } + #[cfg(any(test, feature = "test-support"))] + pub async fn fake_reverse_request(&self, args: R::Arguments) { + self.send_message(Message::Request(dap_types::messages::Request { + seq: self.sequence_count.load(Ordering::Relaxed), + command: R::COMMAND.into(), + arguments: serde_json::to_value(args).ok(), + })) + .await + .unwrap(); + } + #[cfg(any(test, feature = "test-support"))] pub async fn fake_event(&self, event: dap_types::messages::Events) { self.send_message(Message::Event(Box::new(event))) @@ -285,10 +296,13 @@ mod tests { adapters::FakeAdapter, client::DebugAdapterClient, debugger_settings::DebuggerSettings, }; use dap_types::{ - messages::Events, requests::Initialize, Capabilities, InitializeRequestArguments, - InitializeRequestArgumentsPathFormat, + messages::Events, + requests::{Initialize, Request, RunInTerminal}, + Capabilities, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, + RunInTerminalRequestArguments, }; use gpui::TestAppContext; + use serde_json::json; use settings::{Settings, SettingsStore}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -430,4 +444,71 @@ mod tests { client.shutdown().await.unwrap(); } + + #[gpui::test] + pub async fn test_calls_event_handler_for_reverse_request(cx: &mut TestAppContext) { + init_test(cx); + + let adapter = Arc::new(FakeAdapter::new()); + let called_event_handler = Arc::new(AtomicBool::new(false)); + + let mut client = DebugAdapterClient::new( + crate::client::DebugAdapterClientId(1), + adapter, + DebugAdapterBinary { + command: "command".into(), + arguments: Default::default(), + envs: Default::default(), + cwd: None, + }, + &mut cx.to_async(), + ); + + client + .start( + { + let called_event_handler = called_event_handler.clone(); + move |event, _| { + called_event_handler.store(true, Ordering::SeqCst); + + assert_eq!( + Message::Request(dap_types::messages::Request { + seq: 1, + command: RunInTerminal::COMMAND.into(), + arguments: Some(json!({ + "cwd": "/project/path/src", + "args": ["node", "test.js"], + })) + }), + event + ); + } + }, + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.run_until_parked(); + + client + .fake_reverse_request::(RunInTerminalRequestArguments { + kind: None, + title: None, + cwd: "/project/path/src".into(), + args: vec!["node".into(), "test.js".into()], + env: None, + args_can_be_interpreted_by_shell: None, + }) + .await; + + cx.run_until_parked(); + + assert!( + called_event_handler.load(std::sync::atomic::Ordering::SeqCst), + "Event handler was not called" + ); + + client.shutdown().await.unwrap(); + } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index c062c869952afd..f54b9115ee09a1 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -809,6 +809,7 @@ impl Transport for FakeTransport { _binary: &DebugAdapterBinary, cx: &mut AsyncAppContext, ) -> Result { + use dap_types::requests::{Request, RunInTerminal, StartDebugging}; use serde_json::json; let (stdin_writer, stdin_reader) = async_pipe::pipe(); @@ -817,51 +818,69 @@ impl Transport for FakeTransport { let handlers = self.request_handlers.clone(); let stdout_writer = Arc::new(Mutex::new(stdout_writer)); - cx.background_executor().spawn(async move { - let mut reader = BufReader::new(stdin_reader); - let mut buffer = String::new(); - - loop { - let message = TransportDelegate::receive_server_message( - &mut reader, - &mut buffer, - None, - ) - .await; - - match message { - Err(error) => { - break anyhow!(error); - } - Ok(Message::Request(request)) => { - if let Some(handle) = - handlers.lock().await.get_mut(request.command.as_str()) - { - handle( - request.seq, - request.arguments.unwrap_or(json!({})), - stdout_writer.clone(), - ) + cx.background_executor() + .spawn(async move { + let mut reader = BufReader::new(stdin_reader); + let mut buffer = String::new(); + + loop { + let message = + TransportDelegate::receive_server_message(&mut reader, &mut buffer, None) .await; - } else { - log::debug!("No handler for {}", request.command); - } - } - Ok(Message::Event(event)) => { - let message = serde_json::to_string(&Message::Event(event)).unwrap(); - let mut writer = stdout_writer.lock().await; - writer - .write_all(TransportDelegate::build_rpc_message(message).as_bytes()) - .await - .unwrap(); - writer.flush().await.unwrap(); + match message { + Err(error) => { + break anyhow!(error); + } + Ok(message) => { + if let Message::Request(request) = message { + // redirect reverse requests to stdout writer/reader + if request.command == RunInTerminal::COMMAND + || request.command == StartDebugging::COMMAND + { + let message = + serde_json::to_string(&Message::Request(request)).unwrap(); + + let mut writer = stdout_writer.lock().await; + writer + .write_all( + TransportDelegate::build_rpc_message(message) + .as_bytes(), + ) + .await + .unwrap(); + writer.flush().await.unwrap(); + } else { + if let Some(handle) = + handlers.lock().await.get_mut(request.command.as_str()) + { + handle( + request.seq, + request.arguments.unwrap_or(json!({})), + stdout_writer.clone(), + ) + .await; + } else { + log::debug!("No handler for {}", request.command); + } + } + } else if let Message::Event(event) = message { + let message = serde_json::to_string(&Message::Event(event)).unwrap(); + + let mut writer = stdout_writer.lock().await; + writer + .write_all(TransportDelegate::build_rpc_message(message).as_bytes()) + .await + .unwrap(); + writer.flush().await.unwrap(); + } else { + unreachable!("You can only send a request and an event that is redirected to the output reader") + } + } } - _ => unreachable!("You can only send a request and an event that is redirected to the output reader"), } - } - }) - .detach(); + }) + .detach(); Ok(TransportPipe::new( Box::new(stdin_writer), From fba00c728e3e50efe2c98251fb39aa21aa369ace Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 27 Dec 2024 00:12:05 +0100 Subject: [PATCH 412/650] Notify spawn terminal errors to user --- crates/debugger_ui/src/debugger_panel.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 94344ac6b13be8..9fe4de2adad4dd 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -17,7 +17,10 @@ use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, FontWeight, Model, Subscription, Task, View, ViewContext, WeakView, }; -use project::{dap_store::DapStore, terminals::TerminalKind}; +use project::{ + dap_store::{DapStore, DapStoreEvent}, + terminals::TerminalKind, +}; use rpc::proto::{SetDebuggerPanelItem, UpdateDebugAdapter}; use serde_json::Value; use settings::Settings; @@ -25,6 +28,7 @@ use std::{any::TypeId, collections::VecDeque, path::PathBuf, u64}; use task::DebugRequestType; use terminal_view::terminal_panel::TerminalPanel; use ui::prelude::*; +use util::ResultExt as _; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, pane, Continue, Disconnect, Pane, Pause, Restart, Start, StepBack, StepInto, StepOut, StepOver, @@ -457,7 +461,16 @@ impl DebugPanel { let (success, pid) = match terminal_task { Ok(pid_task) => match pid_task.await { Ok(pid) => (true, pid), - Err(_) => (false, None), + Err(error) => { + this.update(&mut cx, |this, cx| { + this.dap_store.update(cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + }) + .log_err(); + + (false, None) + } }, Err(_) => (false, None), }; From 7ced61d798b2f1baed484d10cc1b4f170eb69255 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 27 Dec 2024 00:51:28 +0100 Subject: [PATCH 413/650] Add basic test for RunInTerminal reverse request to spawn terminal This commit also adds a way to add a handler when a response is received from a specific reverse request. --- crates/dap/src/client.rs | 10 ++ crates/dap/src/transport.rs | 96 ++++++++---- crates/debugger_ui/src/tests.rs | 12 +- crates/debugger_ui/src/tests/console.rs | 8 +- .../debugger_ui/src/tests/debugger_panel.rs | 26 ++-- .../debugger_ui/src/tests/run_in_terminal.rs | 142 ++++++++++++++++++ .../debugger_ui/src/tests/stack_frame_list.rs | 14 +- crates/debugger_ui/src/tests/variable_list.rs | 20 +-- 8 files changed, 265 insertions(+), 63 deletions(-) create mode 100644 crates/debugger_ui/src/tests/run_in_terminal.rs diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 2f5fa0d0ae5146..564feefb90752a 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -281,6 +281,16 @@ impl DebugAdapterClient { .unwrap(); } + #[cfg(any(test, feature = "test-support"))] + pub async fn on_response(&self, handler: F) + where + F: 'static + Send + Fn(Response), + { + let transport = self.transport_delegate.transport(); + + transport.as_fake().on_response::(handler).await; + } + #[cfg(any(test, feature = "test-support"))] pub async fn fake_event(&self, event: dap_types::messages::Events) { self.send_message(Message::Event(Box::new(event))) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index f54b9115ee09a1..d509b6dd95b28c 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -752,9 +752,15 @@ type RequestHandler = Box< ) -> std::pin::Pin + Send>>, >; +#[cfg(any(test, feature = "test-support"))] +type ResponseHandler = Box; + #[cfg(any(test, feature = "test-support"))] pub struct FakeTransport { + // for sending fake response back from adapter side request_handlers: Arc>>, + // for reverse request responses + response_handlers: Arc>>, } #[cfg(any(test, feature = "test-support"))] @@ -762,6 +768,7 @@ impl FakeTransport { pub fn new() -> Self { Self { request_handlers: Arc::new(Mutex::new(HashMap::default())), + response_handlers: Arc::new(Mutex::new(HashMap::default())), } } @@ -799,6 +806,16 @@ impl FakeTransport { ), ); } + + pub async fn on_response(&self, handler: F) + where + F: 'static + Send + Fn(Response), + { + self.response_handlers + .lock() + .await + .insert(R::COMMAND, Box::new(handler)); + } } #[cfg(any(test, feature = "test-support"))] @@ -815,7 +832,8 @@ impl Transport for FakeTransport { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); - let handlers = self.request_handlers.clone(); + let request_handlers = self.request_handlers.clone(); + let response_handlers = self.response_handlers.clone(); let stdout_writer = Arc::new(Mutex::new(stdout_writer)); cx.background_executor() @@ -833,13 +851,48 @@ impl Transport for FakeTransport { break anyhow!(error); } Ok(message) => { - if let Message::Request(request) = message { - // redirect reverse requests to stdout writer/reader - if request.command == RunInTerminal::COMMAND - || request.command == StartDebugging::COMMAND - { + match message { + Message::Request(request) => { + // redirect reverse requests to stdout writer/reader + if request.command == RunInTerminal::COMMAND + || request.command == StartDebugging::COMMAND + { + let message = + serde_json::to_string(&Message::Request(request)) + .unwrap(); + + let mut writer = stdout_writer.lock().await; + writer + .write_all( + TransportDelegate::build_rpc_message(message) + .as_bytes(), + ) + .await + .unwrap(); + writer.flush().await.unwrap(); + } else { + if let Some(handle) = request_handlers + .lock() + .await + .get_mut(request.command.as_str()) + { + handle( + request.seq, + request.arguments.unwrap_or(json!({})), + stdout_writer.clone(), + ) + .await; + } else { + log::error!( + "No request handler for {}", + request.command + ); + } + } + } + Message::Event(event) => { let message = - serde_json::to_string(&Message::Request(request)).unwrap(); + serde_json::to_string(&Message::Event(event)).unwrap(); let mut writer = stdout_writer.lock().await; writer @@ -850,31 +903,18 @@ impl Transport for FakeTransport { .await .unwrap(); writer.flush().await.unwrap(); - } else { - if let Some(handle) = - handlers.lock().await.get_mut(request.command.as_str()) + } + Message::Response(response) => { + if let Some(handle) = response_handlers + .lock() + .await + .get(response.command.as_str()) { - handle( - request.seq, - request.arguments.unwrap_or(json!({})), - stdout_writer.clone(), - ) - .await; + handle(response); } else { - log::debug!("No handler for {}", request.command); + log::error!("No response handler for {}", response.command); } } - } else if let Message::Event(event) = message { - let message = serde_json::to_string(&Message::Event(event)).unwrap(); - - let mut writer = stdout_writer.lock().await; - writer - .write_all(TransportDelegate::build_rpc_message(message).as_bytes()) - .await - .unwrap(); - writer.flush().await.unwrap(); - } else { - unreachable!("You can only send a request and an event that is redirected to the output reader") } } } diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index f14e12122df173..722e8eeb07d352 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -1,12 +1,14 @@ use gpui::{Model, TestAppContext, WindowHandle}; use project::Project; use settings::SettingsStore; +use terminal_view::terminal_panel::TerminalPanel; use workspace::Workspace; use crate::debugger_panel::DebugPanel; mod console; mod debugger_panel; +mod run_in_terminal; mod stack_frame_list; mod variable_list; @@ -18,6 +20,7 @@ pub fn init_test(cx: &mut gpui::TestAppContext) { cx.update(|cx| { let settings = SettingsStore::test(cx); cx.set_global(settings); + terminal_view::init(cx); theme::init(theme::LoadThemes::JustBase, cx); command_palette_hooks::init(cx); language::init(cx); @@ -28,7 +31,7 @@ pub fn init_test(cx: &mut gpui::TestAppContext) { }); } -pub async fn add_debugger_panel( +pub async fn init_test_workspace( project: &Model, cx: &mut TestAppContext, ) -> WindowHandle { @@ -40,9 +43,16 @@ pub async fn add_debugger_panel( .await .expect("Failed to load debug panel"); + let terminal_panel = window + .update(cx, |_, cx| cx.spawn(TerminalPanel::load)) + .unwrap() + .await + .expect("Failed to load terminal panel"); + window .update(cx, |workspace, cx| { workspace.add_panel(debugger_panel, cx); + workspace.add_panel(terminal_panel, cx); }) .unwrap(); window diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index a5e200bd2601b4..64486f25c6ddb8 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -10,7 +10,7 @@ use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }; -use tests::{add_debugger_panel, init_test}; +use tests::{init_test, init_test_workspace}; use unindent::Unindent as _; use variable_list::{VariableContainer, VariableListEntry}; @@ -45,7 +45,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp .await; let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -453,11 +453,11 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp "Expected evaluate request to be called" ); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); } diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 2409926ad57ff1..4da68de14c24a3 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -2,7 +2,7 @@ use crate::*; use dap::requests::{Disconnect, Initialize, Launch, StackTrace}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; -use tests::{add_debugger_panel, init_test}; +use tests::{init_test, init_test_workspace}; use workspace::dock::Panel; #[gpui::test] @@ -12,7 +12,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let fs = FakeFs::new(executor.clone()); let project = Project::test(fs, [], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -98,13 +98,13 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); // assert we don't have a debug panel item anymore because the client shutdown workspace @@ -129,7 +129,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( let fs = FakeFs::new(executor.clone()); let project = Project::test(fs, [], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -246,13 +246,13 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); // assert we don't have a debug panel item anymore because the client shutdown workspace @@ -277,7 +277,7 @@ async fn test_client_can_open_multiple_thread_panels( let fs = FakeFs::new(executor.clone()); let project = Project::test(fs, [], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -394,13 +394,13 @@ async fn test_client_can_open_multiple_thread_panels( }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); // assert we don't have a debug panel item anymore because the client shutdown workspace @@ -422,7 +422,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp let fs = FakeFs::new(executor.clone()); let project = Project::test(fs, [], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -553,13 +553,13 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); // assert output queue is empty workspace diff --git a/crates/debugger_ui/src/tests/run_in_terminal.rs b/crates/debugger_ui/src/tests/run_in_terminal.rs new file mode 100644 index 00000000000000..9ee803c46403f7 --- /dev/null +++ b/crates/debugger_ui/src/tests/run_in_terminal.rs @@ -0,0 +1,142 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use crate::*; +use dap::{ + requests::{Disconnect, Initialize, Launch, RunInTerminal, StackTrace}, + RunInTerminalRequestArguments, +}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use project::{FakeFs, Project}; +use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; +use tests::{init_test, init_test_workspace}; +use workspace::dock::Panel; + +#[gpui::test] +async fn test_handle_run_in_terminal_reverse_request( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let send_response = Arc::new(AtomicBool::new(false)); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + client + .on_response::({ + let send_response = send_response.clone(); + move |response| { + send_response.store(true, Ordering::SeqCst); + + assert!(response.success); + assert!(response.body.is_some()); + } + }) + .await; + + client + .fake_reverse_request::(RunInTerminalRequestArguments { + kind: None, + title: None, + cwd: std::env::temp_dir().to_string_lossy().to_string(), + args: vec![], + env: None, + args_can_be_interpreted_by_shell: None, + }) + .await; + + cx.run_until_parked(); + + assert!( + send_response.load(std::sync::atomic::Ordering::SeqCst), + "Expected to receive response from reverse request" + ); + + workspace + .update(cx, |workspace, cx| { + let terminal_panel = workspace.panel::(cx).unwrap(); + + let panel = terminal_panel.read(cx).pane().unwrap().read(cx); + + assert_eq!(1, panel.items_len()); + assert!(panel + .active_item() + .unwrap() + .downcast::() + .unwrap() + .read(cx) + .terminal() + .read(cx) + .debug_terminal()); + }) + .unwrap(); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 2875b2e08368e7..220cb4184eacd8 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -1,6 +1,6 @@ use crate::{ debugger_panel::DebugPanel, - tests::{add_debugger_panel, init_test}, + tests::{init_test, init_test_workspace}, }; use dap::{ requests::{Disconnect, Initialize, Launch, StackTrace}, @@ -46,7 +46,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( .await; let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -171,13 +171,13 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); } #[gpui::test] @@ -210,7 +210,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC .await; let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -418,11 +418,11 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); } diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 5a59846d8ddb3c..7fc35392d6b31a 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::{ debugger_panel::DebugPanel, - tests::{add_debugger_panel, init_test}, + tests::{init_test, init_test_workspace}, variable_list::{VariableContainer, VariableListEntry}, }; use collections::HashMap; @@ -42,7 +42,7 @@ async fn test_basic_fetch_initial_scope_and_variables( .await; let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -252,13 +252,13 @@ async fn test_basic_fetch_initial_scope_and_variables( }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); } /// This tests fetching multiple scopes and variables for them with a single stackframe @@ -292,7 +292,7 @@ async fn test_fetch_variables_for_multiple_scopes( .await; let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -552,13 +552,13 @@ async fn test_fetch_variables_for_multiple_scopes( }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); } // tests that toggling a variable will fetch its children and show it @@ -589,7 +589,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T .await; let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = add_debugger_panel(&project, cx).await; + let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { @@ -1103,11 +1103,11 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T }) .unwrap(); - let shutdown_client = project.update(cx, |project, cx| { + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); - shutdown_client.await.unwrap(); + shutdown_session.await.unwrap(); } From ea18dfbe9478d721625ead8f2844d0918c79dfa3 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 27 Dec 2024 15:26:33 +0100 Subject: [PATCH 414/650] Add unhappy path test for handle RunInTerminal reverse request --- .../debugger_ui/src/tests/run_in_terminal.rs | 132 +++++++++++++++++- crates/project/src/dap_store.rs | 7 +- 2 files changed, 130 insertions(+), 9 deletions(-) diff --git a/crates/debugger_ui/src/tests/run_in_terminal.rs b/crates/debugger_ui/src/tests/run_in_terminal.rs index 9ee803c46403f7..c193da5a2c478e 100644 --- a/crates/debugger_ui/src/tests/run_in_terminal.rs +++ b/crates/debugger_ui/src/tests/run_in_terminal.rs @@ -1,8 +1,3 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; - use crate::*; use dap::{ requests::{Disconnect, Initialize, Launch, RunInTerminal, StackTrace}, @@ -10,12 +5,16 @@ use dap::{ }; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use tests::{init_test, init_test_workspace}; use workspace::dock::Panel; #[gpui::test] -async fn test_handle_run_in_terminal_reverse_request( +async fn test_handle_successful_run_in_terminal_reverse_request( executor: BackgroundExecutor, cx: &mut TestAppContext, ) { @@ -140,3 +139,124 @@ async fn test_handle_run_in_terminal_reverse_request( shutdown_session.await.unwrap(); } + +// covers that we always send a response back, if something when wrong, +// while spawning the terminal +#[gpui::test] +async fn test_handle_error_run_in_terminal_reverse_request( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let send_response = Arc::new(AtomicBool::new(false)); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + client + .on_response::({ + let send_response = send_response.clone(); + move |response| { + send_response.store(true, Ordering::SeqCst); + + assert!(!response.success); + assert!(response.body.is_none()); + } + }) + .await; + + client + .fake_reverse_request::(RunInTerminalRequestArguments { + kind: None, + title: None, + cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail + args: vec![], + env: None, + args_can_be_interpreted_by_shell: None, + }) + .await; + + cx.run_until_parked(); + + assert!( + send_response.load(std::sync::atomic::Ordering::SeqCst), + "Expected to receive response from reverse request" + ); + + workspace + .update(cx, |workspace, cx| { + let terminal_panel = workspace.panel::(cx).unwrap(); + + assert_eq!( + 0, + terminal_panel.read(cx).pane().unwrap().read(cx).items_len() + ); + }) + .unwrap(); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 783dbff000223c..27940e0e11783f 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -988,11 +988,12 @@ impl DapStore { success, command: RunInTerminal::COMMAND.to_string(), body: match success { - true => Some(serde_json::to_value(RunInTerminalResponse { + true => serde_json::to_value(RunInTerminalResponse { process_id: Some(std::process::id() as u64), shell_process_id: shell_pid, - })?), - false => Some(serde_json::to_value(ErrorResponse { error: None })?), + }) + .ok(), + false => serde_json::to_value(ErrorResponse { error: None }).ok(), }, })) .await From 43defed0a47cc004594ab0b21dea6e8f918ce952 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 27 Dec 2024 16:59:06 +0100 Subject: [PATCH 415/650] Move run in terminal tests to debug panel module --- crates/debugger_ui/src/tests.rs | 1 - .../debugger_ui/src/tests/debugger_panel.rs | 212 +++++++++++++- .../debugger_ui/src/tests/run_in_terminal.rs | 262 ------------------ 3 files changed, 211 insertions(+), 264 deletions(-) delete mode 100644 crates/debugger_ui/src/tests/run_in_terminal.rs diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 722e8eeb07d352..a9f21f4a55eadb 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -8,7 +8,6 @@ use crate::debugger_panel::DebugPanel; mod console; mod debugger_panel; -mod run_in_terminal; mod stack_frame_list; mod variable_list; diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 4da68de14c24a3..9c5e0fe94de7ef 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1,7 +1,15 @@ use crate::*; -use dap::requests::{Disconnect, Initialize, Launch, StackTrace}; +use dap::{ + requests::{Disconnect, Initialize, Launch, RunInTerminal, StackTrace}, + RunInTerminalRequestArguments, +}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use tests::{init_test, init_test_workspace}; use workspace::dock::Panel; @@ -570,3 +578,205 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp }) .unwrap(); } + +#[gpui::test] +async fn test_handle_successful_run_in_terminal_reverse_request( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let send_response = Arc::new(AtomicBool::new(false)); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_response::({ + let send_response = send_response.clone(); + move |response| { + send_response.store(true, Ordering::SeqCst); + + assert!(response.success); + assert!(response.body.is_some()); + } + }) + .await; + + client + .fake_reverse_request::(RunInTerminalRequestArguments { + kind: None, + title: None, + cwd: std::env::temp_dir().to_string_lossy().to_string(), + args: vec![], + env: None, + args_can_be_interpreted_by_shell: None, + }) + .await; + + cx.run_until_parked(); + + assert!( + send_response.load(std::sync::atomic::Ordering::SeqCst), + "Expected to receive response from reverse request" + ); + + workspace + .update(cx, |workspace, cx| { + let terminal_panel = workspace.panel::(cx).unwrap(); + + let panel = terminal_panel.read(cx).pane().unwrap().read(cx); + + assert_eq!(1, panel.items_len()); + assert!(panel + .active_item() + .unwrap() + .downcast::() + .unwrap() + .read(cx) + .terminal() + .read(cx) + .debug_terminal()); + }) + .unwrap(); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} + +// covers that we always send a response back, if something when wrong, +// while spawning the terminal +#[gpui::test] +async fn test_handle_error_run_in_terminal_reverse_request( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let send_response = Arc::new(AtomicBool::new(false)); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_response::({ + let send_response = send_response.clone(); + move |response| { + send_response.store(true, Ordering::SeqCst); + + assert!(!response.success); + assert!(response.body.is_none()); + } + }) + .await; + + client + .fake_reverse_request::(RunInTerminalRequestArguments { + kind: None, + title: None, + cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail + args: vec![], + env: None, + args_can_be_interpreted_by_shell: None, + }) + .await; + + cx.run_until_parked(); + + assert!( + send_response.load(std::sync::atomic::Ordering::SeqCst), + "Expected to receive response from reverse request" + ); + + workspace + .update(cx, |workspace, cx| { + let terminal_panel = workspace.panel::(cx).unwrap(); + + assert_eq!( + 0, + terminal_panel.read(cx).pane().unwrap().read(cx).items_len() + ); + }) + .unwrap(); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/debugger_ui/src/tests/run_in_terminal.rs b/crates/debugger_ui/src/tests/run_in_terminal.rs deleted file mode 100644 index c193da5a2c478e..00000000000000 --- a/crates/debugger_ui/src/tests/run_in_terminal.rs +++ /dev/null @@ -1,262 +0,0 @@ -use crate::*; -use dap::{ - requests::{Disconnect, Initialize, Launch, RunInTerminal, StackTrace}, - RunInTerminalRequestArguments, -}; -use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; -use project::{FakeFs, Project}; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; -use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; -use tests::{init_test, init_test_workspace}; -use workspace::dock::Panel; - -#[gpui::test] -async fn test_handle_successful_run_in_terminal_reverse_request( - executor: BackgroundExecutor, - cx: &mut TestAppContext, -) { - init_test(cx); - - let send_response = Arc::new(AtomicBool::new(false)); - - let fs = FakeFs::new(executor.clone()); - - let project = Project::test(fs, [], cx).await; - let workspace = init_test_workspace(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) - }); - - let (session, client) = task.await.unwrap(); - - client - .on_request::(move |_, _| { - Ok(dap::Capabilities { - supports_step_back: Some(false), - ..Default::default() - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - client - .on_request::(move |_, _| { - Ok(dap::StackTraceResponse { - stack_frames: Vec::default(), - total_frames: None, - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - client - .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { - reason: dap::StoppedEventReason::Pause, - description: None, - thread_id: Some(1), - preserve_focus_hint: None, - text: None, - all_threads_stopped: None, - hit_breakpoint_ids: None, - })) - .await; - - cx.run_until_parked(); - - client - .on_response::({ - let send_response = send_response.clone(); - move |response| { - send_response.store(true, Ordering::SeqCst); - - assert!(response.success); - assert!(response.body.is_some()); - } - }) - .await; - - client - .fake_reverse_request::(RunInTerminalRequestArguments { - kind: None, - title: None, - cwd: std::env::temp_dir().to_string_lossy().to_string(), - args: vec![], - env: None, - args_can_be_interpreted_by_shell: None, - }) - .await; - - cx.run_until_parked(); - - assert!( - send_response.load(std::sync::atomic::Ordering::SeqCst), - "Expected to receive response from reverse request" - ); - - workspace - .update(cx, |workspace, cx| { - let terminal_panel = workspace.panel::(cx).unwrap(); - - let panel = terminal_panel.read(cx).pane().unwrap().read(cx); - - assert_eq!(1, panel.items_len()); - assert!(panel - .active_item() - .unwrap() - .downcast::() - .unwrap() - .read(cx) - .terminal() - .read(cx) - .debug_terminal()); - }) - .unwrap(); - - let shutdown_session = project.update(cx, |project, cx| { - project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) - }) - }); - - shutdown_session.await.unwrap(); -} - -// covers that we always send a response back, if something when wrong, -// while spawning the terminal -#[gpui::test] -async fn test_handle_error_run_in_terminal_reverse_request( - executor: BackgroundExecutor, - cx: &mut TestAppContext, -) { - init_test(cx); - - let send_response = Arc::new(AtomicBool::new(false)); - - let fs = FakeFs::new(executor.clone()); - - let project = Project::test(fs, [], cx).await; - let workspace = init_test_workspace(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) - }); - - let (session, client) = task.await.unwrap(); - - client - .on_request::(move |_, _| { - Ok(dap::Capabilities { - supports_step_back: Some(false), - ..Default::default() - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - client - .on_request::(move |_, _| { - Ok(dap::StackTraceResponse { - stack_frames: Vec::default(), - total_frames: None, - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - client - .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { - reason: dap::StoppedEventReason::Pause, - description: None, - thread_id: Some(1), - preserve_focus_hint: None, - text: None, - all_threads_stopped: None, - hit_breakpoint_ids: None, - })) - .await; - - cx.run_until_parked(); - - client - .on_response::({ - let send_response = send_response.clone(); - move |response| { - send_response.store(true, Ordering::SeqCst); - - assert!(!response.success); - assert!(response.body.is_none()); - } - }) - .await; - - client - .fake_reverse_request::(RunInTerminalRequestArguments { - kind: None, - title: None, - cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail - args: vec![], - env: None, - args_can_be_interpreted_by_shell: None, - }) - .await; - - cx.run_until_parked(); - - assert!( - send_response.load(std::sync::atomic::Ordering::SeqCst), - "Expected to receive response from reverse request" - ); - - workspace - .update(cx, |workspace, cx| { - let terminal_panel = workspace.panel::(cx).unwrap(); - - assert_eq!( - 0, - terminal_panel.read(cx).pane().unwrap().read(cx).items_len() - ); - }) - .unwrap(); - - let shutdown_session = project.update(cx, |project, cx| { - project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) - }) - }); - - shutdown_session.await.unwrap(); -} From f4669e89657920897b503a24cd337a21f35d3e7f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 27 Dec 2024 16:59:45 +0100 Subject: [PATCH 416/650] Add basic test for attach flow --- crates/dap/src/adapters.rs | 28 +++++- crates/debugger_ui/src/tests.rs | 1 + crates/debugger_ui/src/tests/attach_modal.rs | 91 ++++++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 crates/debugger_ui/src/tests/attach_modal.rs diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index bb70be285f1395..3fbdf1ed673721 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -364,7 +364,31 @@ impl DebugAdapter for FakeAdapter { unimplemented!("get installed binary"); } - fn request_args(&self, _config: &DebugAdapterConfig) -> Value { - Value::Null + fn request_args(&self, config: &DebugAdapterConfig) -> Value { + use serde_json::json; + use task::DebugRequestType; + + json!({ + "request": match config.request { + DebugRequestType::Launch => "launch", + DebugRequestType::Attach(_) => "attach", + }, + "process_id": if let DebugRequestType::Attach(attach_config) = &config.request { + attach_config.process_id + } else { + None + }, + }) + } + + fn supports_attach(&self) -> bool { + true + } + + fn attach_processes<'a>( + &self, + processes: &'a HashMap, + ) -> Option> { + Some(processes.iter().collect::>()) } } diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index a9f21f4a55eadb..2561fbf0b484bb 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -6,6 +6,7 @@ use workspace::Workspace; use crate::debugger_panel::DebugPanel; +mod attach_modal; mod console; mod debugger_panel; mod stack_frame_list; diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs new file mode 100644 index 00000000000000..167d25a37661a4 --- /dev/null +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -0,0 +1,91 @@ +use crate::*; +use attach_modal::AttachModal; +use dap::requests::{Attach, Disconnect, Initialize}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use project::{FakeFs, Project}; +use serde_json::json; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use task::AttachConfig; +use tests::{init_test, init_test_workspace}; + +#[gpui::test] +async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let send_attach_request = Arc::new(AtomicBool::new(false)); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Attach(AttachConfig { + process_id: Some(10), + }), + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client + .on_request::({ + let send_attach_request = send_attach_request.clone(); + move |_, args| { + send_attach_request.store(true, Ordering::SeqCst); + + assert_eq!(json!({"request": "attach", "process_id": 10}), args.raw); + + Ok(()) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + cx.run_until_parked(); + + assert!( + send_attach_request.load(std::sync::atomic::Ordering::SeqCst), + "Expected to send attach request, because we passed in the processId" + ); + + // assert we didn't show the attach modal + workspace + .update(cx, |workspace, cx| { + assert!(workspace.active_modal::(cx).is_none()); + }) + .unwrap(); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} From e0891a1df70a7abec99df920eabc9f297a6ccad4 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 27 Dec 2024 19:31:57 +0100 Subject: [PATCH 417/650] Add test for attach select process flow --- crates/dap/src/adapters.rs | 7 +- crates/debugger_ui/src/attach_modal.rs | 20 +++- crates/debugger_ui/src/tests/attach_modal.rs | 106 +++++++++++++++++++ 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 3fbdf1ed673721..e7eee938b3ff03 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -389,6 +389,11 @@ impl DebugAdapter for FakeAdapter { &self, processes: &'a HashMap, ) -> Option> { - Some(processes.iter().collect::>()) + Some( + processes + .iter() + .filter(|(pid, _)| pid.as_u32() == std::process::id()) + .collect::>(), + ) } } diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 97605ce4a8173f..ea3162f0390ee1 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -18,7 +18,7 @@ struct Candidate { command: String, } -struct AttachModalDelegate { +pub(crate) struct AttachModalDelegate { selected_index: usize, matches: Vec, session_id: DebugSessionId, @@ -48,7 +48,7 @@ impl AttachModalDelegate { pub(crate) struct AttachModal { _subscription: Subscription, - picker: View>, + pub(crate) picker: View>, } impl AttachModal { @@ -257,3 +257,19 @@ impl PickerDelegate for AttachModalDelegate { ) } } + +#[allow(dead_code)] +#[cfg(any(test, feature = "test-support"))] +pub(crate) fn procss_names( + modal: &AttachModal, + cx: &mut gpui::ViewContext, +) -> Vec { + modal.picker.update(cx, |picker, _| { + picker + .delegate + .matches + .iter() + .map(|hit| hit.string.clone()) + .collect::>() + }) +} diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 167d25a37661a4..297ed202bbf8ca 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -2,6 +2,7 @@ use crate::*; use attach_modal::AttachModal; use dap::requests::{Attach, Disconnect, Initialize}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use menu::Confirm; use project::{FakeFs, Project}; use serde_json::json; use std::sync::{ @@ -89,3 +90,108 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_show_attach_modal_and_select_process( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let send_attach_request = Arc::new(AtomicBool::new(false)); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Attach(AttachConfig { process_id: None }), + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client + .on_request::({ + let send_attach_request = send_attach_request.clone(); + move |_, args| { + send_attach_request.store(true, Ordering::SeqCst); + + assert_eq!( + json!({ + "request": "attach", + // note we filterd out all processes in FakeAdapter::attach_processes, + // that is not equal to the current process id + "process_id": std::process::id(), + }), + args.raw + ); + + Ok(()) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + cx.run_until_parked(); + + // assert we show the attach modal + workspace + .update(cx, |workspace, cx| { + let attach_modal = workspace.active_modal::(cx).unwrap(); + + let names = attach_modal.update(cx, |modal, cx| attach_modal::procss_names(&modal, cx)); + + // we filterd out all processes that are not the current process(zed itself) + assert_eq!(1, names.len()); + }) + .unwrap(); + + // select the only existing process + cx.dispatch_action(Confirm); + + cx.run_until_parked(); + + // assert attach modal was dismissed + workspace + .update(cx, |workspace, cx| { + assert!(workspace.active_modal::(cx).is_none()); + }) + .unwrap(); + + assert!( + send_attach_request.load(std::sync::atomic::Ordering::SeqCst), + "Expected to send attach request, because we passed in the processId" + ); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} From bfdf12c51c881f77a9ac73c16d25ae9a593a2e1e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 27 Dec 2024 20:13:25 +0100 Subject: [PATCH 418/650] Fix typo --- crates/debugger_ui/src/tests/attach_modal.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 297ed202bbf8ca..a410e65ce413b4 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -142,7 +142,7 @@ async fn test_show_attach_modal_and_select_process( assert_eq!( json!({ "request": "attach", - // note we filterd out all processes in FakeAdapter::attach_processes, + // note we filtered out all processes in FakeAdapter::attach_processes, // that is not equal to the current process id "process_id": std::process::id(), }), @@ -165,7 +165,7 @@ async fn test_show_attach_modal_and_select_process( let names = attach_modal.update(cx, |modal, cx| attach_modal::procss_names(&modal, cx)); - // we filterd out all processes that are not the current process(zed itself) + // we filtered out all processes that are not the current process(zed itself) assert_eq!(1, names.len()); }) .unwrap(); From b5b877bd26fa98cf46aeed222161d6b51d6bdd0d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 27 Dec 2024 20:19:58 +0100 Subject: [PATCH 419/650] Add test to validate we shutdown session when attach modal is dismissed --- crates/debugger_ui/src/tests/attach_modal.rs | 96 +++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index a410e65ce413b4..dfdbfd0bddf52d 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -2,7 +2,7 @@ use crate::*; use attach_modal::AttachModal; use dap::requests::{Attach, Disconnect, Initialize}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; -use menu::Confirm; +use menu::{Cancel, Confirm}; use project::{FakeFs, Project}; use serde_json::json; use std::sync::{ @@ -195,3 +195,97 @@ async fn test_show_attach_modal_and_select_process( shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_shutdown_session_when_modal_is_dismissed( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let send_attach_request = Arc::new(AtomicBool::new(false)); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Attach(AttachConfig { process_id: None }), + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client + .on_request::({ + let send_attach_request = send_attach_request.clone(); + move |_, _| { + send_attach_request.store(true, Ordering::SeqCst); + + Ok(()) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + cx.run_until_parked(); + + // assert we show the attach modal + workspace + .update(cx, |workspace, cx| { + let attach_modal = workspace.active_modal::(cx).unwrap(); + + let names = attach_modal.update(cx, |modal, cx| attach_modal::procss_names(&modal, cx)); + + // we filtered out all processes that are not the current process(zed itself) + assert_eq!(1, names.len()); + }) + .unwrap(); + + // close the modal + cx.dispatch_action(Cancel); + + cx.run_until_parked(); + + // assert attach modal was dismissed + workspace + .update(cx, |workspace, cx| { + assert!(workspace.active_modal::(cx).is_none()); + }) + .unwrap(); + + assert!( + !send_attach_request.load(std::sync::atomic::Ordering::SeqCst), + "Didn't expected to send attach request, because we closed the modal" + ); + + // assert debug session is shutdown + project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + assert!(dap_store.session_by_id(&session.read(cx).id()).is_none()) + }); + }); +} From 1f0304f00293dd7106a7663ee564c92827e578c3 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 29 Dec 2024 03:37:00 -0500 Subject: [PATCH 420/650] Fix debug_panel_item::to_proto function signature I switched it to use AppContext to improve the function's flexibility --- crates/debugger_ui/src/debugger_panel_item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 238d439fca5957..3dcadf3435a0d9 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -205,7 +205,7 @@ impl DebugPanelItem { } } - pub(crate) fn to_proto(&self, cx: &ViewContext, project_id: u64) -> SetDebuggerPanelItem { + pub(crate) fn to_proto(&self, project_id: u64, cx: &AppContext) -> SetDebuggerPanelItem { let thread_state = Some(self.thread_state.read(cx).to_proto()); let module_list = Some(self.module_list.read(cx).to_proto()); let variable_list = Some(self.variable_list.read(cx).to_proto()); @@ -296,7 +296,7 @@ impl DebugPanelItem { if let Some((downstream_client, project_id)) = self.dap_store.read(cx).downstream_client() { downstream_client - .send(self.to_proto(cx, *project_id)) + .send(self.to_proto(*project_id, cx)) .log_err(); } } From e2206b876bbc312de6be48619e5665252947249d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 29 Dec 2024 16:02:06 +0100 Subject: [PATCH 421/650] Move variable index to SumTree This change replaces the Vec-based implementation with SumTree, significantly improving performance for frequent insertions. While the impact may not be immediately noticeable to users, it eliminates the need for costly vector shifts during each insert operation. --- Cargo.lock | 1 + crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/tests/console.rs | 4 +- crates/debugger_ui/src/tests/variable_list.rs | 16 +-- crates/debugger_ui/src/variable_list.rs | 127 +++++++++++++----- 5 files changed, 108 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6e3fa05bfdfe4..c225a5a947f142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3711,6 +3711,7 @@ dependencies = [ "serde", "serde_json", "settings", + "sum_tree", "sysinfo", "task", "tasks_ui", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 196976c9247208..5634651b602f39 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -36,6 +36,7 @@ rpc.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +sum_tree.workspace = true sysinfo.workspace = true task.workspace = true tasks_ui.workspace = true diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 64486f25c6ddb8..510b531234a068 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -372,7 +372,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp // scope 1 assert_eq!( - [ + vec![ VariableContainer { container_reference: scopes[0].variables_reference, variable: scope1_variables[0].clone(), @@ -399,7 +399,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp // scope 2 assert_eq!( - [VariableContainer { + vec![VariableContainer { container_reference: scopes[1].variables_reference, variable: scope2_variables[0].clone(), depth: 1, diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 7fc35392d6b31a..74dab83d0ffb8f 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -494,7 +494,7 @@ async fn test_fetch_variables_for_multiple_scopes( // scope 1 assert_eq!( - [ + vec![ VariableContainer { container_reference: scopes[0].variables_reference, variable: variables.get(&2).unwrap()[0].clone(), @@ -511,7 +511,7 @@ async fn test_fetch_variables_for_multiple_scopes( // scope 2 assert_eq!( - [VariableContainer { + vec![VariableContainer { container_reference: scopes[1].variables_reference, variable: variables.get(&3).unwrap()[0].clone(), depth: 1, @@ -811,7 +811,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T .update(cx, |variable_list, _| { // scope 1 assert_eq!( - [ + vec![ VariableContainer { container_reference: scopes[0].variables_reference, variable: scope1_variables[0].clone(), @@ -828,7 +828,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T // scope 2 assert_eq!( - [VariableContainer { + vec![VariableContainer { container_reference: scopes[1].variables_reference, variable: scope2_variables[0].clone(), depth: 1, @@ -906,7 +906,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T .update(cx, |variable_list, _| { // scope 1 assert_eq!( - [ + vec![ VariableContainer { container_reference: scopes[0].variables_reference, variable: scope1_variables[0].clone(), @@ -933,7 +933,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T // scope 2 assert_eq!( - [VariableContainer { + vec![VariableContainer { container_reference: scopes[1].variables_reference, variable: scope2_variables[0].clone(), depth: 1, @@ -1020,7 +1020,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T .update(cx, |variable_list, _| { // scope 1 assert_eq!( - [ + vec![ VariableContainer { container_reference: scopes[0].variables_reference, variable: scope1_variables[0].clone(), @@ -1047,7 +1047,7 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T // scope 2 assert_eq!( - [VariableContainer { + vec![VariableContainer { container_reference: scopes[1].variables_reference, variable: scope2_variables[0].clone(), depth: 1, diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 4486172e7d1591..832f247414960e 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -23,6 +23,7 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, sync::Arc, }; +use sum_tree::{Dimension, Item, SumTree, Summary}; use ui::{prelude::*, ContextMenu, ListItem}; use util::ResultExt; @@ -229,7 +230,50 @@ impl VariableListEntry { #[derive(Debug)] pub struct ScopeVariableIndex { fetched_ids: HashSet, - variables: Vec, + variables: SumTree, +} + +#[derive(Clone, Debug, Default)] +pub struct ScopeVariableSummary { + count: usize, + max_depth: usize, + container_reference: u64, +} + +impl Item for VariableContainer { + type Summary = ScopeVariableSummary; + + fn summary(&self, _cx: &()) -> Self::Summary { + ScopeVariableSummary { + count: 1, + max_depth: self.depth, + container_reference: self.container_reference, + } + } +} + +impl<'a> Dimension<'a, ScopeVariableSummary> for usize { + fn zero(_cx: &()) -> Self { + 0 + } + + fn add_summary(&mut self, summary: &'a ScopeVariableSummary, _cx: &()) { + *self += summary.count; + } +} + +impl Summary for ScopeVariableSummary { + type Context = (); + + fn zero(_: &Self::Context) -> Self { + Self::default() + } + + fn add_summary(&mut self, other: &Self, _: &Self::Context) { + self.count += other.count; + self.max_depth = self.max_depth.max(other.max_depth); + self.container_reference = self.container_reference.max(other.container_reference); + } } impl ProtoConversion for ScopeVariableIndex { @@ -246,11 +290,13 @@ impl ProtoConversion for ScopeVariableIndex { fn from_proto(payload: Self::ProtoType) -> Self { Self { fetched_ids: payload.fetched_ids.iter().copied().collect(), - variables: payload - .variables - .iter() - .filter_map(|var| VariableContainer::from_proto(var.clone()).log_err()) - .collect(), + variables: SumTree::from_iter( + payload + .variables + .iter() + .filter_map(|var| VariableContainer::from_proto(var.clone()).log_err()), + &(), + ), } } } @@ -258,7 +304,7 @@ impl ProtoConversion for ScopeVariableIndex { impl ScopeVariableIndex { pub fn new() -> Self { Self { - variables: Vec::new(), + variables: SumTree::default(), fetched_ids: HashSet::default(), } } @@ -269,26 +315,45 @@ impl ScopeVariableIndex { /// All the variables should have the same depth and the same container reference pub fn add_variables(&mut self, container_reference: u64, variables: Vec) { - let position = self - .variables - .iter() - .position(|v| v.variable.variables_reference == container_reference); - self.fetched_ids.insert(container_reference); - if let Some(position) = position { - self.variables.splice(position + 1..=position, variables); - } else { - self.variables.extend(variables); + let mut new_variables = SumTree::new(&()); + let mut cursor = self.variables.cursor::(&()); + let mut found_insertion_point = false; + + cursor.seek(&0, editor::Bias::Left, &()); + while let Some(variable) = cursor.item() { + if variable.variable.variables_reference == container_reference { + found_insertion_point = true; + + let start = cursor.start().clone(); + new_variables.push(variable.clone(), &()); + new_variables.append(cursor.slice(&start, editor::Bias::Left, &()), &()); + new_variables.extend(variables.iter().cloned(), &()); + + cursor.next(&()); + new_variables.append(cursor.suffix(&()), &()); + + break; + } + new_variables.push(variable.clone(), &()); + cursor.next(&()); + } + drop(cursor); + + if !found_insertion_point { + new_variables.extend(variables.iter().cloned(), &()); } + + self.variables = new_variables; } pub fn is_empty(&self) -> bool { self.variables.is_empty() } - pub fn variables(&self) -> &[VariableContainer] { - &self.variables + pub fn variables(&self) -> Vec { + self.variables.iter().cloned().collect() } } @@ -1186,7 +1251,7 @@ mod tests { fn test_add_initial_variables_to_index() { let mut index = ScopeVariableIndex::new(); - assert_eq!(index.variables(), &[]); + assert_eq!(index.variables(), vec![]); assert_eq!(index.fetched_ids, HashSet::default()); let variable1 = VariableContainer { @@ -1243,10 +1308,10 @@ mod tests { ); assert_eq!( + vec![variable1.clone(), variable2.clone(), variable3.clone()], index.variables(), - &[variable1.clone(), variable2.clone(), variable3.clone()] ); - assert_eq!(index.fetched_ids, HashSet::from([1])); + assert_eq!(HashSet::from([1]), index.fetched_ids,); } /// This covers when you click on a variable that has a nested variable @@ -1255,7 +1320,7 @@ mod tests { fn test_add_sub_variables_to_index() { let mut index = ScopeVariableIndex::new(); - assert_eq!(index.variables(), &[]); + assert_eq!(index.variables(), vec![]); let variable1 = VariableContainer { variable: Variable { @@ -1311,10 +1376,10 @@ mod tests { ); assert_eq!( + vec![variable1.clone(), variable2.clone(), variable3.clone()], index.variables(), - &[variable1.clone(), variable2.clone(), variable3.clone()] ); - assert_eq!(index.fetched_ids, HashSet::from([1])); + assert_eq!(HashSet::from([1]), index.fetched_ids); let variable4 = VariableContainer { variable: Variable { @@ -1351,14 +1416,14 @@ mod tests { index.add_variables(2, vec![variable4.clone(), variable5.clone()]); assert_eq!( - index.variables(), - &[ + vec![ variable1.clone(), variable2.clone(), variable4.clone(), variable5.clone(), variable3.clone(), - ] + ], + index.variables(), ); assert_eq!(index.fetched_ids, HashSet::from([1, 2])); } @@ -1436,15 +1501,15 @@ mod tests { index.add_variables(2, vec![variable3.clone(), variable4.clone()]); assert_eq!( - index.variables(), - &[ + vec![ variable1.clone(), variable3.clone(), variable4.clone(), variable2.clone(), - ] + ], + index.variables(), ); - assert_eq!(index.fetched_ids, HashSet::from([1, 2])); + assert_eq!(HashSet::from([1, 2]), index.fetched_ids); let from_proto = ScopeVariableIndex::from_proto(index.to_proto()); From 030ee04e54d5553d3b99aea2c691aee5af72758b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 29 Dec 2024 16:02:53 +0100 Subject: [PATCH 422/650] Swap memory for breakpoints instead of manual clear --- crates/project/src/dap_store.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 27940e0e11783f..40798262262de5 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -934,6 +934,7 @@ impl DapStore { Ok(task) => match task.await { Ok(_) => (true, None), Err(error) => { + dbg!(&error); this.update(&mut cx, |_, cx| { cx.emit(DapStoreEvent::Notification(error.to_string())); }) @@ -1467,14 +1468,13 @@ impl DapStore { breakpoints: Vec, cx: &mut ModelContext, ) { - self.breakpoints.clear(); - + let mut new_breakpoints = BTreeMap::new(); for project_breakpoints in breakpoints { let Some(project_path) = project_breakpoints.project_path else { continue; }; - self.breakpoints.insert( + new_breakpoints.insert( ProjectPath::from_proto(project_path), project_breakpoints .breakpoints @@ -1484,6 +1484,7 @@ impl DapStore { ); } + std::mem::swap(&mut self.breakpoints, &mut new_breakpoints); cx.notify(); } From b3aa18d70cad33f55c29e611cccfb6847af9a7b7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 29 Dec 2024 16:33:50 +0100 Subject: [PATCH 423/650] Update variable list test to cover hide sub variables --- crates/debugger_ui/src/tests/variable_list.rs | 110 +++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 74dab83d0ffb8f..f4bc90244dc1f5 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -561,7 +561,7 @@ async fn test_fetch_variables_for_multiple_scopes( shutdown_session.await.unwrap(); } -// tests that toggling a variable will fetch its children and show it +// tests that toggling a variable will fetch its children and shows it #[gpui::test] async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); @@ -1103,6 +1103,114 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T }) .unwrap(); + // toggle variable that has child variables to hide variables + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.toggle_entry( + &crate::variable_list::OpenEntry::Variable { + name: scope1_variables[0].name.clone(), + depth: 1, + }, + cx, + ); + }); + }); + }) + .unwrap(); + + cx.run_until_parked(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, _| { + // scope 1 + assert_eq!( + vec![ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[0].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[1].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[1].clone(), + depth: 1, + }, + ], + variable_list.variables_by_scope(1, 2).unwrap().variables() + ); + + // scope 2 + assert_eq!( + vec![VariableContainer { + container_reference: scopes[1].variables_reference, + variable: scope2_variables[0].clone(), + depth: 1, + }], + variable_list.variables_by_scope(1, 4).unwrap().variables() + ); + + // assert visual entries + assert_eq!( + vec![ + VariableListEntry::Scope(scopes[0].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: true, + variable: Arc::new(scope1_variables[0].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(scope1_variables[1].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Scope(scopes[1].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[1].clone()), + has_children: false, + variable: Arc::new(scope2_variables[0].clone()), + container_reference: scopes[1].variables_reference, + }, + ], + variable_list.entries().get(&1).unwrap().clone() + ); + }); + }); + }) + .unwrap(); + let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) From f682077ffd72b1d1d892b7049b4a917e8ed6da86 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 29 Dec 2024 11:34:22 -0500 Subject: [PATCH 424/650] Remove dbg! statement We already notify the user of the error and now log it too" --- crates/project/src/dap_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 40798262262de5..87fef6bdeaae92 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -934,7 +934,7 @@ impl DapStore { Ok(task) => match task.await { Ok(_) => (true, None), Err(error) => { - dbg!(&error); + log::error!("Failed to reconnect to debug adapter client with error message: {error}"); this.update(&mut cx, |_, cx| { cx.emit(DapStoreEvent::Notification(error.to_string())); }) From 9cd7e072fe2c91569fde8d5f99cb49ac8fd5445a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 29 Dec 2024 17:54:23 +0100 Subject: [PATCH 425/650] Keep scroll position in variable list when opening nested variables (#82) * Keep scroll position when opening nested variables * Remove debug statement * Fix clippy * Update determine scroll position to cover more cases --- crates/debugger_ui/src/variable_list.rs | 45 +++++++++++++++++++++++-- crates/project/src/dap_store.rs | 1 - 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 832f247414960e..088c369ed90cd0 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -10,7 +10,7 @@ use editor::{ }; use gpui::{ anchored, deferred, list, AnyElement, ClipboardItem, DismissEvent, FocusHandle, FocusableView, - ListState, Model, MouseDownEvent, Point, Subscription, Task, View, + ListOffset, ListState, Model, MouseDownEvent, Point, Subscription, Task, View, }; use menu::Confirm; use project::dap_store::DapStore; @@ -326,7 +326,7 @@ impl ScopeVariableIndex { if variable.variable.variables_reference == container_reference { found_insertion_point = true; - let start = cursor.start().clone(); + let start = *cursor.start(); new_variables.push(variable.clone(), &()); new_variables.append(cursor.slice(&start, editor::Bias::Left, &()), &()); new_variables.extend(variables.iter().cloned(), &()); @@ -727,10 +727,49 @@ impl VariableList { } } + let old_entries = self.entries.get(&stack_frame_id).cloned(); + let old_scroll_top = self.list.logical_scroll_top(); + let len = entries.len(); - self.entries.insert(stack_frame_id, entries); + self.entries.insert(stack_frame_id, entries.clone()); self.list.reset(len); + if let Some(old_entries) = old_entries.as_ref() { + if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { + let new_scroll_top = old_entries + .iter() + .position(|entry| entry == old_top_entry) + .map(|item_ix| ListOffset { + item_ix, + offset_in_item: old_scroll_top.offset_in_item, + }) + .or_else(|| { + let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; + let item_ix = entries + .iter() + .position(|entry| entry == entry_after_old_top)?; + Some(ListOffset { + item_ix, + offset_in_item: Pixels::ZERO, + }) + }) + .or_else(|| { + let entry_before_old_top = + old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; + let item_ix = entries + .iter() + .position(|entry| entry == entry_before_old_top)?; + Some(ListOffset { + item_ix, + offset_in_item: Pixels::ZERO, + }) + }); + + self.list + .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); + } + } + cx.notify(); if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 87fef6bdeaae92..6704c8e2e0c70a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -934,7 +934,6 @@ impl DapStore { Ok(task) => match task.await { Ok(_) => (true, None), Err(error) => { - log::error!("Failed to reconnect to debug adapter client with error message: {error}"); this.update(&mut cx, |_, cx| { cx.emit(DapStoreEvent::Notification(error.to_string())); }) From f0947c11976f270e0277c493a9cd2b8ba7bf651a Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 30 Dec 2024 04:34:45 -0500 Subject: [PATCH 426/650] Fix bug where untoggling last breakpoint in buffer wouldn't send to debug sessions The bug was caused by removing the breakpoint_set associated to a project_paths when removing the last breakpoint in the set and there were active debug sessions. Causing Zed to never tell active DAPs that the last breakpoint was removed. We now only remove breakpoint_set if there's no active sessions. A better solution would be removing breakpoint_set after sending all breakpoint requests to active DAPs. Also, we only send initial breakpoint requests for project paths that contain breakpoints now. --- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/project/src/dap_store.rs | 28 +++++++++++++----------- crates/project/src/project.rs | 8 +++++-- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9fe4de2adad4dd..8ee858318101e7 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -611,7 +611,7 @@ impl DebugPanel { this.update(&mut cx, |debug_panel, cx| { debug_panel.workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - project.send_breakpoints(&session_id, &client_id, cx) + project.initial_send_breakpoints(&session_id, &client_id, cx) }) }) })?? diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 6704c8e2e0c70a..22c2014c5256b6 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1617,11 +1617,7 @@ impl DapStore { ) -> Task> { let upstream_client = self.upstream_client(); - let mut breakpoint_set = self - .breakpoints - .get(project_path) - .cloned() - .unwrap_or_default(); + let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); match edit_action { BreakpointEditAction::Toggle => { @@ -1635,13 +1631,6 @@ impl DapStore { } } - if breakpoint_set.is_empty() { - self.breakpoints.remove(project_path); - } else { - self.breakpoints - .insert(project_path.clone(), breakpoint_set.clone()); - } - cx.notify(); if let Some((client, project_id)) = upstream_client.or(self.downstream_client.clone()) { @@ -1657,7 +1646,20 @@ impl DapStore { .log_err(); } - self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) + let remove_breakpoint_set = breakpoint_set.is_empty(); + + if self + .as_local() + .is_some_and(|local| !local.sessions.is_empty()) + { + self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) + } else { + if remove_breakpoint_set { + self.breakpoints.remove(project_path); + } + + Task::ready(Ok(())) + } } pub fn send_breakpoints( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f5967df9a4373d..9b843083c2adcf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1241,7 +1241,7 @@ impl Project { all_breakpoints } - pub fn send_breakpoints( + pub fn initial_send_breakpoints( &self, session_id: &DebugSessionId, client_id: &DebugAdapterClientId, @@ -1249,7 +1249,11 @@ impl Project { ) -> Task<()> { let mut tasks = Vec::new(); - for (abs_path, serialized_breakpoints) in self.all_breakpoints(true, cx) { + for (abs_path, serialized_breakpoints) in self + .all_breakpoints(true, cx) + .into_iter() + .filter(|(_, bps)| !bps.is_empty()) + { let source_breakpoints = serialized_breakpoints .iter() .map(|bp| bp.to_source_breakpoint()) From 0b68944397988fa25a2b610c319d04d327c16e06 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 30 Dec 2024 04:55:03 -0500 Subject: [PATCH 427/650] Fix bug where toggling breakpoints on line 1 wouldn't work This only affected breakpoints that were being toggled from a code runner/actions symbol right click --- crates/editor/src/editor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 214ba8bf471db9..60545973cbfa95 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4965,7 +4965,7 @@ impl Editor { .buffer .read(cx) .snapshot(cx) - .anchor_at(Point::new(row.0, 0u32), Bias::Left); + .breakpoint_anchor(Point::new(row.0, 0u32)); let anchor = source.text_anchor; @@ -5210,7 +5210,7 @@ impl Editor { .buffer .read(cx) .snapshot(cx) - .anchor_at(Point::new(row.0, 0u32), Bias::Left); + .breakpoint_anchor(Point::new(row.0, 0u32)); let clicked_point = event.down.position; @@ -5392,7 +5392,7 @@ impl Editor { .buffer .read(cx) .snapshot(cx) - .anchor_at(Point::new(row.0, 0u32), Bias::Left); + .breakpoint_anchor(Point::new(row.0, 0u32)); let anchor = source.text_anchor; From dd5083ad62dc5f367ab4c6b6c76915c5fa7266ff Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 30 Dec 2024 05:15:20 -0500 Subject: [PATCH 428/650] Remove empty log breakpoints when confirming log message --- crates/project/src/dap_store.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 22c2014c5256b6..535d388ae30e61 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1627,7 +1627,14 @@ impl DapStore { } BreakpointEditAction::EditLogMessage => { breakpoint_set.remove(&breakpoint); - breakpoint_set.insert(breakpoint); + + if breakpoint + .kind + .log_message() + .is_some_and(|msg| !msg.is_empty()) + { + breakpoint_set.insert(breakpoint); + } } } From acb3ee23a803666957a1df3674a1e914fbf4b83c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 30 Dec 2024 20:10:03 +0100 Subject: [PATCH 429/650] Clean up remove breakpoint code Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/project/src/dap_store.rs | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 535d388ae30e61..c4bae21d724ccf 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1638,8 +1638,6 @@ impl DapStore { } } - cx.notify(); - if let Some((client, project_id)) = upstream_client.or(self.downstream_client.clone()) { client .send(client::proto::SynchronizeBreakpoints { @@ -1653,20 +1651,13 @@ impl DapStore { .log_err(); } - let remove_breakpoint_set = breakpoint_set.is_empty(); + if breakpoint_set.is_empty() { + self.breakpoints.remove(project_path); + } - if self - .as_local() - .is_some_and(|local| !local.sessions.is_empty()) - { - self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) - } else { - if remove_breakpoint_set { - self.breakpoints.remove(project_path); - } + cx.notify(); - Task::ready(Ok(())) - } + self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) } pub fn send_breakpoints( @@ -1721,11 +1712,11 @@ impl DapStore { return Task::ready(Err(anyhow!("cannot start session on remote side"))); }; - let Some(breakpoints) = self.breakpoints.get(project_path) else { - return Task::ready(Ok(())); - }; - - let source_breakpoints = breakpoints + let source_breakpoints = self + .breakpoints + .get(project_path) + .cloned() + .unwrap_or_default() .iter() .map(|bp| bp.source_for_snapshot(&buffer_snapshot)) .collect::>(); From 3afda611b484203a115fbafabb4a630b89ab113a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 30 Dec 2024 20:10:18 +0100 Subject: [PATCH 430/650] Add editor toggle breakpoint test Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/editor/src/editor_tests.rs | 122 ++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 7377b6809b71a7..ef6e61c5caccb9 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -28,6 +28,7 @@ use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; use project::{buffer_store::BufferChangeSet, FakeFs}; use project::{ + dap_store::BreakpointKind, lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT, project_settings::{LspSettings, ProjectSettings}, }; @@ -14732,6 +14733,127 @@ fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) { } } +#[gpui::test] +async fn test_breakpoint_toggling(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + #[track_caller] + fn assert_breakpoint( + breakpoints: &BTreeMap>, + project_path: &ProjectPath, + expected: Vec<(u32, BreakpointKind)>, + ) { + if expected.len() == 0usize { + assert!(!breakpoints.contains_key(project_path)); + } else { + let mut breakpoint = breakpoints + .get(project_path) + .unwrap() + .into_iter() + .map(|breakpoint| (breakpoint.cached_position, breakpoint.kind.clone())) + .collect::>(); + + breakpoint.sort_by_key(|(cached_position, _)| *cached_position); + + assert_eq!(expected, breakpoint); + } + } + + let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/a", + json!({ + "main.rs": sample_text, + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + + let window = cx.add_window(|cx| { + Editor::new( + EditorMode::Full, + MultiBuffer::build_from_buffer(buffer, cx), + Some(project), + true, + cx, + ) + }); + + let cx = &mut VisualTestContext::from_window(*window, cx); + let project_path = window + .update(cx, |editor, cx| editor.project_path(cx).unwrap()) + .unwrap(); + + // assert we can add breakpoint on the first line + window + .update(cx, |editor, cx| { + editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); + }) + .unwrap(); + + let breakpoints = window + .update(cx, |editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .dap_store() + .read(cx) + .breakpoints() + .clone() + }) + .unwrap(); + + assert_eq!(1, breakpoints.len()); + assert_breakpoint( + &breakpoints, + &project_path, + vec![(0, BreakpointKind::Standard)], + ); + + window + .update(cx, |editor, cx| { + editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); + }) + .unwrap(); + + let breakpoints = window + .update(cx, |editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .dap_store() + .read(cx) + .breakpoints() + .clone() + }) + .unwrap(); + + assert_eq!(0, breakpoints.len()); + assert_breakpoint(&breakpoints, &project_path, vec![]); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point From 7223bf93baadc49db9593ce13d2c7dbb623acaa6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 30 Dec 2024 20:16:30 +0100 Subject: [PATCH 431/650] Update toggle breakpoint test so cover more cases Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/editor/src/editor_tests.rs | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ef6e61c5caccb9..40d04580a27a95 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -14806,6 +14806,8 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { window .update(cx, |editor, cx| { editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); + editor.move_to_end(&MoveToEnd, cx); + editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); }) .unwrap(); @@ -14827,11 +14829,40 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { assert_breakpoint( &breakpoints, &project_path, - vec![(0, BreakpointKind::Standard)], + vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)], ); window .update(cx, |editor, cx| { + editor.move_to_beginning(&MoveToBeginning, cx); + editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); + }) + .unwrap(); + + let breakpoints = window + .update(cx, |editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .dap_store() + .read(cx) + .breakpoints() + .clone() + }) + .unwrap(); + + assert_eq!(1, breakpoints.len()); + assert_breakpoint( + &breakpoints, + &project_path, + vec![(3, BreakpointKind::Standard)], + ); + + window + .update(cx, |editor, cx| { + editor.move_to_end(&MoveToEnd, cx); editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); }) .unwrap(); From 42d1b484bdca2bdac2bddc7403ca9889b48a4df2 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 30 Dec 2024 23:17:54 +0100 Subject: [PATCH 432/650] Testing: Allow sending error response inside request handler --- crates/dap/src/client.rs | 4 +++- crates/dap/src/transport.rs | 13 ++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 564feefb90752a..1fc7c90b6adeec 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -263,7 +263,9 @@ impl DebugAdapterClient { #[cfg(any(test, feature = "test-support"))] pub async fn on_request(&self, handler: F) where - F: 'static + Send + FnMut(u64, R::Arguments) -> Result, + F: 'static + + Send + + FnMut(u64, R::Arguments) -> Result, { let transport = self.transport_delegate.transport(); diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index d509b6dd95b28c..4b4033acea54a3 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -774,23 +774,22 @@ impl FakeTransport { pub async fn on_request(&self, mut handler: F) where - F: 'static + Send + FnMut(u64, R::Arguments) -> Result, + F: 'static + Send + FnMut(u64, R::Arguments) -> Result, { self.request_handlers.lock().await.insert( R::COMMAND, Box::new( move |seq, args, writer: Arc>| { - let response = handler(seq, serde_json::from_value(args).unwrap()).unwrap(); + let response = handler(seq, serde_json::from_value(args).unwrap()); - let message = Message::Response(Response { + let message = serde_json::to_string(&Message::Response(Response { seq: seq + 1, request_seq: seq, - success: true, + success: response.is_ok(), command: R::COMMAND.into(), body: Some(serde_json::to_value(response).unwrap()), - }); - - let message = serde_json::to_string(&message).unwrap(); + })) + .unwrap(); let writer = writer.clone(); From 1c59d2203bd62e8a0e72b02eb4868a063c1b3f29 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 30 Dec 2024 23:24:52 +0100 Subject: [PATCH 433/650] Send error response message back for StartDebugging request --- crates/project/src/dap_store.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index c4bae21d724ccf..0275234b5a255d 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -939,10 +939,36 @@ impl DapStore { }) .log_err(); - (false, None) + ( + false, + Some(serde_json::to_value(ErrorResponse { + error: Some(dap::Message { + id: seq, + format: error.to_string(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + })?), + ) } }, - Err(_) => (false, None), + Err(error) => ( + false, + Some(serde_json::to_value(ErrorResponse { + error: Some(dap::Message { + id: seq, + format: error.to_string(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + })?), + ), } } else { ( From 1698965317d204d3ad58d98da0d29c072d6ff509 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 2 Jan 2025 03:11:42 -0500 Subject: [PATCH 434/650] Create set breakpoint context menu > > We repeated the same code for creating a breakpoint context menu in three places so I exported > it to a function. --- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 140 +++++++++++++++++------------------ 2 files changed, 71 insertions(+), 70 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 25f99e34d2de7e..43383240b245b9 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -366,6 +366,7 @@ gpui::actions!( Tab, TabPrev, ToggleBreakpoint, + ToggleLogBreakpoint, ToggleAutoSignatureHelp, ToggleGitBlame, ToggleGitBlameInline, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 60545973cbfa95..2bdcddbd8e1372 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4925,6 +4925,7 @@ impl Editor { Color::Muted }; + let position = breakpoint.as_ref().and_then(|bp| bp.active_position); let bp_kind = Arc::new( breakpoint .map(|bp| bp.kind.clone()) @@ -4961,25 +4962,13 @@ impl Editor { ); })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { - let source = editor - .buffer - .read(cx) - .snapshot(cx) - .breakpoint_anchor(Point::new(row.0, 0u32)); - - let anchor = source.text_anchor; - - let context_menu = - editor.breakpoint_context_menu(anchor, bp_kind.clone(), row, cx); - - let clicked_point = event.down.position; - editor.mouse_context_menu = MouseContextMenu::pinned_to_editor( - editor, - source, - clicked_point, - context_menu, + editor.set_breakpoint_context_menu( + row, + position, + bp_kind.clone(), + event.down.position, cx, - ) + ); })), ) } else { @@ -5206,24 +5195,13 @@ impl Editor { ); })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { - let source = editor - .buffer - .read(cx) - .snapshot(cx) - .breakpoint_anchor(Point::new(row.0, 0u32)); - - let clicked_point = event.down.position; - - let context_menu = - editor.breakpoint_context_menu(position, arc_kind2.clone(), row, cx); - - editor.mouse_context_menu = MouseContextMenu::pinned_to_editor( - editor, - source, - clicked_point, - context_menu, + editor.set_breakpoint_context_menu( + row, + Some(position), + arc_kind2.clone(), + event.down.position, cx, - ) + ); })) } @@ -5367,6 +5345,7 @@ impl Editor { Color::Muted }; + let position = breakpoint.as_ref().and_then(|bp| bp.active_position); let bp_kind = Arc::new( breakpoint .map(|bp| bp.kind) @@ -5388,24 +5367,13 @@ impl Editor { ); })) .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { - let source = editor - .buffer - .read(cx) - .snapshot(cx) - .breakpoint_anchor(Point::new(row.0, 0u32)); - - let anchor = source.text_anchor; - - let context_menu = editor.breakpoint_context_menu(anchor, bp_kind.clone(), row, cx); - - let clicked_point = event.down.position; - editor.mouse_context_menu = MouseContextMenu::pinned_to_editor( - editor, - source, - clicked_point, - context_menu, + editor.set_breakpoint_context_menu( + row, + position, + bp_kind.clone(), + event.down.position, cx, - ) + ); })) } @@ -6323,7 +6291,31 @@ impl Editor { } } - pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { + fn set_breakpoint_context_menu( + &mut self, + row: DisplayRow, + position: Option, + kind: Arc, + clicked_point: gpui::Point, + cx: &mut ViewContext, + ) { + let source = self + .buffer + .read(cx) + .snapshot(cx) + .breakpoint_anchor(Point::new(row.0, 0u32)); + + let context_menu = + self.breakpoint_context_menu(position.unwrap_or(source.text_anchor), kind, row, cx); + + self.mouse_context_menu = + MouseContextMenu::pinned_to_editor(self, source, clicked_point, context_menu, cx); + } + + pub(crate) fn breakpoint_at_cursor_head( + &mut self, + cx: &mut ViewContext, + ) -> Option<(text::Anchor, BreakpointKind)> { let cursor_position: Point = self.selections.newest(cx).head(); // We Set the column position to zero so this function interacts correctly @@ -6339,31 +6331,39 @@ impl Editor { let project = self.project.clone(); - let found_bp = maybe!({ - let buffer_id = breakpoint_position.buffer_id?; - let buffer = - project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?; - let (buffer_snapshot, project_path) = ( - buffer.read(cx).snapshot(), - buffer.read(cx).project_path(cx)?, - ); + let buffer_id = breakpoint_position.buffer_id?; + let buffer = project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?; + let (buffer_snapshot, project_path) = ( + buffer.read(cx).snapshot(), + buffer.read(cx).project_path(cx)?, + ); - let row = buffer_snapshot - .summary_for_anchor::(&breakpoint_position) - .row; + let row = buffer_snapshot + .summary_for_anchor::(&breakpoint_position) + .row; - let bp = self.dap_store.clone()?.read_with(cx, |store, _cx| { - store.breakpoint_at_row(row, &project_path, buffer_snapshot) - })?; + let bp = self.dap_store.clone()?.read_with(cx, |store, _cx| { + store.breakpoint_at_row(row, &project_path, buffer_snapshot) + })?; - Some((bp.active_position?, bp.kind)) - }); + Some((bp.active_position?, bp.kind)) + } + pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { let edit_action = BreakpointEditAction::Toggle; - if let Some((anchor, kind)) = found_bp { + if let Some((anchor, kind)) = self.breakpoint_at_cursor_head(cx) { self.edit_breakpoint_at_anchor(anchor, kind, edit_action, cx); } else { + let cursor_position: Point = self.selections.newest(cx).head(); + + let breakpoint_position = self + .snapshot(cx) + .display_snapshot + .buffer_snapshot + .breakpoint_anchor(Point::new(cursor_position.row, 0)) + .text_anchor; + self.edit_breakpoint_at_anchor( breakpoint_position, BreakpointKind::Standard, From f9c88fb50fe7cc77c7f7426bd451bd5e4c2d043e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 2 Jan 2025 17:12:41 -0500 Subject: [PATCH 435/650] Add edit log breakpoint action I also fixed two bugs. Where not adding a log message to a standard breakpoint would remove it (That shouldn't happen) and the breakpoint prompt editor not always returning focus to the editor --- crates/editor/src/actions.rs | 2 +- crates/editor/src/editor.rs | 138 ++++++++++++++++++++------------ crates/editor/src/element.rs | 1 + crates/project/src/dap_store.rs | 19 +++-- 4 files changed, 100 insertions(+), 60 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 43383240b245b9..8faaee90262d4a 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -366,7 +366,7 @@ gpui::actions!( Tab, TabPrev, ToggleBreakpoint, - ToggleLogBreakpoint, + EditLogBreakpoint, ToggleAutoSignatureHelp, ToggleGitBlame, ToggleGitBlameInline, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2bdcddbd8e1372..da07cdd45fcb2a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5004,13 +5004,13 @@ impl Editor { let snapshot = self.snapshot(cx); - let opened_breakpoints = dap_store.read(cx).breakpoints(); + let breakpoints = dap_store.read(cx).breakpoints(); if let Some(buffer) = self.buffer.read(cx).as_singleton() { let buffer = buffer.read(cx); if let Some(project_path) = buffer.project_path(cx) { - if let Some(breakpoints) = opened_breakpoints.get(&project_path) { + if let Some(breakpoints) = breakpoints.get(&project_path) { for breakpoint in breakpoints { let point = breakpoint.point_for_buffer(&buffer); @@ -5060,8 +5060,8 @@ impl Editor { continue; }; - if let Some(breakpoints) = opened_breakpoints.get(&project_path) { - for breakpoint in breakpoints { + if let Some(breakpoint_set) = breakpoints.get(&project_path) { + for breakpoint in breakpoint_set { let breakpoint_position = breakpoint.point_for_buffer_snapshot(&info.buffer); @@ -5092,11 +5092,10 @@ impl Editor { let editor_weak2 = editor_weak.clone(); let focus_handle = self.focus_handle(cx); - let log_message = kind.log_message(); - let second_entry_msg = if log_message.is_some() { + let second_entry_msg = if kind.log_message().is_some() { "Edit Log Breakpoint" } else { - "Toggle Log Breakpoint" + "Add Log Breakpoint" }; ui::ContextMenu::build(cx, |menu, _cx| { @@ -5116,41 +5115,8 @@ impl Editor { }) .entry(second_entry_msg, None, move |cx| { if let Some(editor) = editor_weak2.clone().upgrade() { - let log_message = log_message.clone(); editor.update(cx, |this, cx| { - let position = this - .snapshot(cx) - .display_point_to_anchor(DisplayPoint::new(row, 0), Bias::Right); - - let weak_editor = cx.view().downgrade(); - let bp_prompt = cx.new_view(|cx| { - BreakpointPromptEditor::new(weak_editor, anchor, log_message, cx) - }); - - let height = bp_prompt.update(cx, |this, cx| { - this.prompt - .update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2) - }); - let cloned_prompt = bp_prompt.clone(); - let blocks = vec![BlockProperties { - style: BlockStyle::Sticky, - placement: BlockPlacement::Above(position), - height, - render: Arc::new(move |cx| { - *cloned_prompt.read(cx).gutter_dimensions.lock() = - *cx.gutter_dimensions; - cloned_prompt.clone().into_any_element() - }), - priority: 0, - }]; - - let focus_handle = bp_prompt.focus_handle(cx); - cx.focus(&focus_handle); - - let block_ids = this.insert_blocks(blocks, None, cx); - bp_prompt.update(cx, |prompt, _| { - prompt.add_block_ids(block_ids); - }); + this.add_edit_breakpoint_block(row, anchor, kind.as_ref(), cx); }); } }) @@ -6312,6 +6278,46 @@ impl Editor { MouseContextMenu::pinned_to_editor(self, source, clicked_point, context_menu, cx); } + fn add_edit_breakpoint_block( + &mut self, + row: DisplayRow, + anchor: text::Anchor, + kind: &BreakpointKind, + cx: &mut ViewContext, + ) { + let position = self + .snapshot(cx) + .display_point_to_anchor(DisplayPoint::new(row, 0), Bias::Right); + + let weak_editor = cx.view().downgrade(); + let bp_prompt = + cx.new_view(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), cx)); + + let height = bp_prompt.update(cx, |this, cx| { + this.prompt + .update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2) + }); + let cloned_prompt = bp_prompt.clone(); + let blocks = vec![BlockProperties { + style: BlockStyle::Sticky, + placement: BlockPlacement::Above(position), + height, + render: Arc::new(move |cx| { + *cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions; + cloned_prompt.clone().into_any_element() + }), + priority: 0, + }]; + + let focus_handle = bp_prompt.focus_handle(cx); + cx.focus(&focus_handle); + + let block_ids = self.insert_blocks(blocks, None, cx); + bp_prompt.update(cx, |prompt, _| { + prompt.add_block_ids(block_ids); + }); + } + pub(crate) fn breakpoint_at_cursor_head( &mut self, cx: &mut ViewContext, @@ -6349,6 +6355,37 @@ impl Editor { Some((bp.active_position?, bp.kind)) } + pub fn edit_log_breakpoint(&mut self, _: &EditLogBreakpoint, cx: &mut ViewContext) { + let (anchor, kind) = self.breakpoint_at_cursor_head(cx).unwrap_or_else(|| { + let cursor_position: Point = self.selections.newest(cx).head(); + + let breakpoint_position = self + .snapshot(cx) + .display_snapshot + .buffer_snapshot + .breakpoint_anchor(Point::new(cursor_position.row, 0)) + .text_anchor; + + let kind = BreakpointKind::Standard; + + (breakpoint_position, kind) + }); + + if let Some(buffer) = self + .buffer() + .read(cx) + .as_singleton() + .map(|buffer| buffer.read(cx)) + { + let row = buffer + .summary_for_anchor::(&anchor) + .to_display_point(&self.snapshot(cx)) + .row(); + + self.add_edit_breakpoint_block(row, anchor, &kind, cx); + } + } + pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { let edit_action = BreakpointEditAction::Toggle; @@ -15473,6 +15510,7 @@ struct BreakpointPromptEditor { pub(crate) prompt: View, editor: WeakView, breakpoint_anchor: text::Anchor, + kind: BreakpointKind, block_ids: HashSet, gutter_dimensions: Arc>, _subscriptions: Vec, @@ -15484,12 +15522,14 @@ impl BreakpointPromptEditor { fn new( editor: WeakView, breakpoint_anchor: text::Anchor, - log_message: Option>, + kind: BreakpointKind, cx: &mut ViewContext, ) -> Self { let buffer = cx.new_model(|cx| { Buffer::local( - log_message.map(|msg| msg.to_string()).unwrap_or_default(), + kind.log_message() + .map(|msg| msg.to_string()) + .unwrap_or_default(), cx, ) }); @@ -15506,10 +15546,7 @@ impl BreakpointPromptEditor { cx, ); prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - // Since the prompt editors for all inline assistants are linked, - // always show the cursor (even when it isn't focused) because - // typing in one will make what you typed appear in all of them. - prompt.set_show_cursor_when_unfocused(true, cx); + prompt.set_show_cursor_when_unfocused(false, cx); prompt.set_placeholder_text( "Message to log when breakpoint is hit. Expressions within {} are interpolated.", cx, @@ -15522,6 +15559,7 @@ impl BreakpointPromptEditor { prompt, editor, breakpoint_anchor, + kind, gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), block_ids: Default::default(), _subscriptions: vec![], @@ -15548,12 +15586,13 @@ impl BreakpointPromptEditor { editor.update(cx, |editor, cx| { editor.edit_breakpoint_at_anchor( self.breakpoint_anchor, - BreakpointKind::Log(log_message.into()), - BreakpointEditAction::EditLogMessage, + self.kind.clone(), + BreakpointEditAction::EditLogMessage(log_message.into()), cx, ); editor.remove_blocks(self.block_ids.clone(), None, cx); + editor.focus(cx); }); } } @@ -15562,6 +15601,7 @@ impl BreakpointPromptEditor { if let Some(editor) = self.editor.upgrade() { editor.update(cx, |editor, cx| { editor.remove_blocks(self.block_ids.clone(), None, cx); + editor.focus(cx); }); } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 12acdd41970d78..c02b72e1628609 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -461,6 +461,7 @@ impl EditorElement { register_action(view, cx, Editor::apply_selected_diff_hunks); register_action(view, cx, Editor::open_active_item_in_terminal); register_action(view, cx, Editor::toggle_breakpoint); + register_action(view, cx, Editor::edit_log_breakpoint); register_action(view, cx, Editor::reload_file); register_action(view, cx, Editor::spawn_nearest_task); register_action(view, cx, Editor::insert_uuid_v4); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 0275234b5a255d..fca65be207ac7b 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1635,7 +1635,7 @@ impl DapStore { pub fn toggle_breakpoint_for_buffer( &mut self, project_path: &ProjectPath, - breakpoint: Breakpoint, + mut breakpoint: Breakpoint, buffer_path: PathBuf, buffer_snapshot: BufferSnapshot, edit_action: BreakpointEditAction, @@ -1651,14 +1651,13 @@ impl DapStore { breakpoint_set.insert(breakpoint); } } - BreakpointEditAction::EditLogMessage => { - breakpoint_set.remove(&breakpoint); - - if breakpoint - .kind - .log_message() - .is_some_and(|msg| !msg.is_empty()) - { + BreakpointEditAction::EditLogMessage(log_message) => { + if matches!(&breakpoint.kind, BreakpointKind::Log(_)) { + breakpoint_set.remove(&breakpoint); + } + + if !log_message.is_empty() { + breakpoint.kind = BreakpointKind::Log(log_message.clone()); breakpoint_set.insert(breakpoint); } } @@ -1806,7 +1805,7 @@ type LogMessage = Arc; #[derive(Clone, Debug)] pub enum BreakpointEditAction { Toggle, - EditLogMessage, + EditLogMessage(LogMessage), } #[derive(Clone, Debug)] From 331aafde2306ba6713d7a5d5a9b254fbe02d7348 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 2 Jan 2025 18:04:16 -0500 Subject: [PATCH 436/650] Add log breakpoint tests --- crates/editor/src/editor_tests.rs | 257 +++++++++++++++++++++++++++--- crates/project/src/dap_store.rs | 7 +- 2 files changed, 238 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 40d04580a27a95..f051ee38551877 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -14733,31 +14733,59 @@ fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) { } } -#[gpui::test] -async fn test_breakpoint_toggling(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - #[track_caller] - fn assert_breakpoint( - breakpoints: &BTreeMap>, - project_path: &ProjectPath, - expected: Vec<(u32, BreakpointKind)>, - ) { - if expected.len() == 0usize { - assert!(!breakpoints.contains_key(project_path)); - } else { - let mut breakpoint = breakpoints - .get(project_path) - .unwrap() - .into_iter() - .map(|breakpoint| (breakpoint.cached_position, breakpoint.kind.clone())) - .collect::>(); +#[track_caller] +fn assert_breakpoint( + breakpoints: &BTreeMap>, + project_path: &ProjectPath, + expected: Vec<(u32, BreakpointKind)>, +) { + if expected.len() == 0usize { + assert!(!breakpoints.contains_key(project_path)); + } else { + let mut breakpoint = breakpoints + .get(project_path) + .unwrap() + .into_iter() + .map(|breakpoint| (breakpoint.cached_position, breakpoint.kind.clone())) + .collect::>(); - breakpoint.sort_by_key(|(cached_position, _)| *cached_position); + breakpoint.sort_by_key(|(cached_position, _)| *cached_position); - assert_eq!(expected, breakpoint); - } + assert_eq!(expected, breakpoint); } +} + +fn add_log_breakpoint_at_cursor( + editor: &mut Editor, + log_message: &str, + cx: &mut ViewContext, +) { + let (anchor, kind) = editor.breakpoint_at_cursor_head(cx).unwrap_or_else(|| { + let cursor_position: Point = editor.selections.newest(cx).head(); + + let breakpoint_position = editor + .snapshot(cx) + .display_snapshot + .buffer_snapshot + .breakpoint_anchor(Point::new(cursor_position.row, 0)) + .text_anchor; + + let kind = BreakpointKind::Standard; + + (breakpoint_position, kind) + }); + + editor.edit_breakpoint_at_anchor( + anchor, + kind, + BreakpointEditAction::EditLogMessage(log_message.into()), + cx, + ); +} + +#[gpui::test] +async fn test_breakpoint_toggling(cx: &mut TestAppContext) { + init_test(cx, |_| {}); let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string(); @@ -14885,6 +14913,191 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { assert_breakpoint(&breakpoints, &project_path, vec![]); } +#[gpui::test] +async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/a", + json!({ + "main.rs": sample_text, + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + + let window = cx.add_window(|cx| { + Editor::new( + EditorMode::Full, + MultiBuffer::build_from_buffer(buffer, cx), + Some(project), + true, + cx, + ) + }); + + let cx = &mut VisualTestContext::from_window(*window, cx); + let project_path = window + .update(cx, |editor, cx| editor.project_path(cx).unwrap()) + .unwrap(); + + window + .update(cx, |editor, cx| { + add_log_breakpoint_at_cursor(editor, "hello world", cx); + }) + .unwrap(); + + let breakpoints = window + .update(cx, |editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .dap_store() + .read(cx) + .breakpoints() + .clone() + }) + .unwrap(); + + assert_breakpoint( + &breakpoints, + &project_path, + vec![(0, BreakpointKind::Log("hello world".into()))], + ); + + // Removing a log message from a log breakpoint should remove it + window + .update(cx, |editor, cx| { + add_log_breakpoint_at_cursor(editor, "", cx); + }) + .unwrap(); + + let breakpoints = window + .update(cx, |editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .dap_store() + .read(cx) + .breakpoints() + .clone() + }) + .unwrap(); + + assert_breakpoint(&breakpoints, &project_path, vec![]); + + window + .update(cx, |editor, cx| { + editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); + editor.move_to_end(&MoveToEnd, cx); + editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); + // Not adding a log message to a standard breakpoint shouldn't remove it + add_log_breakpoint_at_cursor(editor, "", cx); + }) + .unwrap(); + + let breakpoints = window + .update(cx, |editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .dap_store() + .read(cx) + .breakpoints() + .clone() + }) + .unwrap(); + + assert_breakpoint( + &breakpoints, + &project_path, + vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)], + ); + + window + .update(cx, |editor, cx| { + add_log_breakpoint_at_cursor(editor, "hello world", cx); + }) + .unwrap(); + + let breakpoints = window + .update(cx, |editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .dap_store() + .read(cx) + .breakpoints() + .clone() + }) + .unwrap(); + + assert_breakpoint( + &breakpoints, + &project_path, + vec![ + (0, BreakpointKind::Standard), + (3, BreakpointKind::Log("hello world".into())), + ], + ); + + window + .update(cx, |editor, cx| { + add_log_breakpoint_at_cursor(editor, "hello Earth!!", cx); + }) + .unwrap(); + + let breakpoints = window + .update(cx, |editor, cx| { + editor + .project + .as_ref() + .unwrap() + .read(cx) + .dap_store() + .read(cx) + .breakpoints() + .clone() + }) + .unwrap(); + + assert_breakpoint( + &breakpoints, + &project_path, + vec![ + (0, BreakpointKind::Standard), + (3, BreakpointKind::Log("hello Earth !!".into())), + ], + ); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index fca65be207ac7b..02ccb2dd3309fa 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1652,13 +1652,12 @@ impl DapStore { } } BreakpointEditAction::EditLogMessage(log_message) => { - if matches!(&breakpoint.kind, BreakpointKind::Log(_)) { - breakpoint_set.remove(&breakpoint); - } - if !log_message.is_empty() { breakpoint.kind = BreakpointKind::Log(log_message.clone()); + breakpoint_set.remove(&breakpoint); breakpoint_set.insert(breakpoint); + } else if matches!(&breakpoint.kind, BreakpointKind::Log(_)) { + breakpoint_set.remove(&breakpoint); } } } From 9497987438a278864ab5e6a1d22b406643a41ffd Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 2 Jan 2025 18:17:00 -0500 Subject: [PATCH 437/650] Add workspace breakpoint persistence test --- crates/workspace/src/persistence.rs | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 10a8230e0a313c..d391e0de1c30f2 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1357,6 +1357,71 @@ mod tests { use db::open_test_db; use gpui::{self}; + #[gpui::test] + async fn test_breakpoints() { + env_logger::try_init().ok(); + + let db = WorkspaceDb(open_test_db("test_breakpoints").await); + let id = db.next_id().await.unwrap(); + + let path = Path::new("/tmp/test.rs"); + let worktree = Path::new("/tmp"); + + let breakpoint = Breakpoint { + position: 123, + kind: BreakpointKind::Standard, + }; + + let log_breakpoint = Breakpoint { + position: 456, + kind: BreakpointKind::Log("Test log message".into()), + }; + + let workspace = SerializedWorkspace { + id, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), + center_group: Default::default(), + window_bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + centered_layout: false, + session_id: None, + breakpoints: { + let mut map = HashMap::default(); + map.insert( + Arc::from(worktree), + vec![ + SerializedBreakpoint { + position: breakpoint.position, + path: Arc::from(path), + kind: breakpoint.kind.clone(), + }, + SerializedBreakpoint { + position: log_breakpoint.position, + path: Arc::from(path), + kind: log_breakpoint.kind.clone(), + }, + ], + ); + map + }, + window_id: None, + }; + + db.save_workspace(workspace.clone()).await; + + let loaded = db.workspace_for_roots(&["/tmp"]).unwrap(); + let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(worktree)).unwrap(); + + assert_eq!(loaded_breakpoints.len(), 2); + assert_eq!(loaded_breakpoints[0].position, breakpoint.position); + assert_eq!(loaded_breakpoints[0].kind, breakpoint.kind); + assert_eq!(loaded_breakpoints[1].position, log_breakpoint.position); + assert_eq!(loaded_breakpoints[1].kind, log_breakpoint.kind); + assert_eq!(loaded_breakpoints[0].path, Arc::from(path)); + assert_eq!(loaded_breakpoints[1].path, Arc::from(path)); + } + #[gpui::test] async fn test_next_id_stability() { env_logger::try_init().ok(); From cbbf2daa2fe4b1a122eaeccb2c23b0044cd1d0d3 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 2 Jan 2025 18:36:37 -0500 Subject: [PATCH 438/650] Update debugger docs with more basic information --- docs/src/debugger.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/src/debugger.md b/docs/src/debugger.md index 9f52c88a74b791..0b580341f960c8 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -29,3 +29,20 @@ To debug a program using Zed you must first create a debug configuration within } ] ``` + +## Breakpoints + +Zed currently supports these types of breakpoints + +- Log Breakpoints: Output a log message instead of stopping at the breakpoint when it's hit +- Standard Breakpoints: Stop at the breakpoint when it's hit + +Standard breakpoints can be toggled by left clicking on the editor gutter or using the Toggle Breakpoint action. Right clicking on a breakpoint, code action symbol, or code runner symbol brings up the breakpoint context menu. That has options for toggling breakpoints and editing log breakpoints. + +Log breakpoints can also be edited/added through the edit log breakpoint action + +## Starting a Debugger Session + +A debugger session can be started by the Start Debugging action or clicking the "Choose Debugger" button in the debugger panel when there are no active sessions. + +Zed supports having multiple sessions From 020623a19d669ea1e5b935c9ecd54350b0b89f32 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 2 Jan 2025 22:06:05 -0500 Subject: [PATCH 439/650] Fix regression causing some debugger tests to fail After fn on_response() was created we added a test feature that allowed us to return error responses. We serialized responses wrapped around a Result type, causing serde_json::from_value(R::Response) to fail when attempting to get a response and returning a default response instead. --- crates/dap/src/transport.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 4b4033acea54a3..de5d0e77c0060e 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -24,7 +24,7 @@ use std::{ time::Duration, }; use task::TCPHost; -use util::ResultExt as _; +use util::{maybe, ResultExt as _}; use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; @@ -785,9 +785,9 @@ impl FakeTransport { let message = serde_json::to_string(&Message::Response(Response { seq: seq + 1, request_seq: seq, - success: response.is_ok(), + success: response.as_ref().is_ok(), command: R::COMMAND.into(), - body: Some(serde_json::to_value(response).unwrap()), + body: maybe!({ serde_json::to_value(response.ok()?).ok() }), })) .unwrap(); From f0cd2bfa61d44a43331f50d9fd6814a91b217613 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 2 Jan 2025 22:21:15 -0500 Subject: [PATCH 440/650] Fix unused test import --- crates/dap/src/transport.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index de5d0e77c0060e..8b2953d90362f0 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -24,7 +24,7 @@ use std::{ time::Duration, }; use task::TCPHost; -use util::{maybe, ResultExt as _}; +use util::ResultExt as _; use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; @@ -787,7 +787,7 @@ impl FakeTransport { request_seq: seq, success: response.as_ref().is_ok(), command: R::COMMAND.into(), - body: maybe!({ serde_json::to_value(response.ok()?).ok() }), + body: util::maybe!({ serde_json::to_value(response.ok()?).ok() }), })) .unwrap(); From f2722db366e28817e99096f0a49f5d8370a4a3f9 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 3 Jan 2025 21:48:00 +0100 Subject: [PATCH 441/650] Fix crash with RunInTerminal request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This occurs when you start a JavaScript debug session, that tells Zed to spawn a debug terminal, without having a breakpoint for the session. The debug adapter terminates the terminal but also terminates Zed because we send the Zed process ID to the debug adapter to keep track of the terminal as extra information, because we already send the pid of the spawned terminal. So removing the process_id of Zed itself fixes the crash😀 You can reproduce this by using the following config: ```json { "label": "JavaScript debug terminal", "adapter": "javascript", "request": "launch", "cwd": "$ZED_WORKTREE_ROOT", // "program": "$ZED_FILE", // this is optional, but will also crash "initialize_args": { "console": "integratedTerminal" } } ``` --- assets/settings/initial_debug_tasks.json | 9 +++++++++ crates/project/src/dap_store.rs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/assets/settings/initial_debug_tasks.json b/assets/settings/initial_debug_tasks.json index 27bee698c9d6d4..e77d7c872767d8 100644 --- a/assets/settings/initial_debug_tasks.json +++ b/assets/settings/initial_debug_tasks.json @@ -19,5 +19,14 @@ "program": "$ZED_FILE", "request": "launch", "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "JavaScript debug terminal", + "adapter": "javascript", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT", + "initialize_args": { + "console": "integratedTerminal" + } } ] diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 02ccb2dd3309fa..98ef410703fd36 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1015,7 +1015,7 @@ impl DapStore { command: RunInTerminal::COMMAND.to_string(), body: match success { true => serde_json::to_value(RunInTerminalResponse { - process_id: Some(std::process::id() as u64), + process_id: None, shell_process_id: shell_pid, }) .ok(), From 78d342d582d26dfe75ad2d798db9e330bb98aa84 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 3 Jan 2025 22:10:01 +0100 Subject: [PATCH 442/650] Fix CWD is not correctly respected when appending child paths Right now when you add `"cwd": "$ZED_WORKTREE_ROOT/some/path"` to your config, it won't work as expected. Because we only added the `cwd` to the task template when the cwd was a valid path. But when you add `"cwd": "$ZED_WORKTREE_ROOT` to your config, it will never be a valid path, so the `cwd` will never be added to the task template and fallback to the `$ZED_WORKTREE_ROOT` already. So by just always adding it to the task template, we will try to resolve it and otherwise fallback to the `$ZED_WORKTREE_ROOT`. --- crates/task/src/debug_format.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 83f91047d940b1..1cd8492c542d7e 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -185,7 +185,7 @@ impl DebugTaskDefinition { command, args, task_type, - cwd: if cwd.is_some() { self.cwd } else { None }, + cwd: self.cwd, ..Default::default() }) } From 2551a0fe13a90390ebac779df5a063ad7c826967 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 8 Jan 2025 05:31:06 -0500 Subject: [PATCH 443/650] Fix variable list for participants during collab debugging --- crates/debugger_ui/src/variable_list.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 088c369ed90cd0..623f25e4119e9a 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -487,7 +487,7 @@ impl VariableList { .iter() .filter_map(|variable| { Some(( - (variable.stack_frame_id, variable.stack_frame_id), + (variable.stack_frame_id, variable.scope_id), ScopeVariableIndex::from_proto(variable.variables.clone()?), )) }) @@ -536,7 +536,7 @@ impl VariableList { }) .collect(); - self.list.reset(self.entries.len()); + self.build_entries(true, true, cx); cx.notify(); } From 72f13890f0024c982e4966084221c71414736a87 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 8 Jan 2025 11:48:11 +0100 Subject: [PATCH 444/650] Make respond to start debugging code more readable --- crates/project/src/dap_store.rs | 89 ++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 98ef410703fd36..ab04126278580a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -892,45 +892,57 @@ impl DapStore { let session_id = *session_id; let config = session.read(cx).configuration().clone(); + let request_args = args.unwrap_or_else(|| StartDebuggingRequestArguments { + configuration: config.initialize_args.clone().unwrap_or_default(), + request: match config.request { + DebugRequestType::Launch => StartDebuggingRequestArgumentsRequest::Launch, + DebugRequestType::Attach(_) => StartDebuggingRequestArgumentsRequest::Attach, + }, + }); + + // Merge the new configuration over the existing configuration + let mut initialize_args = config.initialize_args.unwrap_or_default(); + merge_json_value_into(request_args.configuration, &mut initialize_args); + + let new_config = DebugAdapterConfig { + label: config.label.clone(), + kind: config.kind.clone(), + request: match request_args.request { + StartDebuggingRequestArgumentsRequest::Launch => DebugRequestType::Launch, + StartDebuggingRequestArgumentsRequest::Attach => DebugRequestType::Attach( + if let DebugRequestType::Attach(attach_config) = config.request { + attach_config + } else { + AttachConfig::default() + }, + ), + }, + program: config.program.clone(), + cwd: config.cwd.clone(), + initialize_args: Some(initialize_args), + }; + cx.spawn(|this, mut cx| async move { - let (success, body) = if let Some(args) = args { - let task = this.update(&mut cx, |store, cx| { - // Merge the new configuration over the existing configuration - let mut initialize_args = config.initialize_args.unwrap_or_default(); - merge_json_value_into(args.configuration, &mut initialize_args); - - store.reconnect_client( - &session_id, - client.adapter().clone(), - client.binary().clone(), - DebugAdapterConfig { - label: config.label.clone(), - kind: config.kind.clone(), - request: match args.request { - StartDebuggingRequestArgumentsRequest::Launch => { - DebugRequestType::Launch - } - StartDebuggingRequestArgumentsRequest::Attach => { - DebugRequestType::Attach( - if let DebugRequestType::Attach(attach_config) = - config.request - { - attach_config - } else { - AttachConfig::default() - }, - ) - } - }, - program: config.program.clone(), - cwd: config.cwd.clone(), - initialize_args: Some(initialize_args), - }, - cx, - ) + let (success, body) = { + let reconnect_task = this.update(&mut cx, |store, cx| { + if !client.adapter().supports_attach() + && matches!(new_config.request, DebugRequestType::Attach(_)) + { + Task::ready(Err(anyhow!( + "Debug adapter does not support `attach` request" + ))) + } else { + store.reconnect_client( + &session_id, + client.adapter().clone(), + client.binary().clone(), + new_config, + cx, + ) + } }); - match task { + match reconnect_task { Ok(task) => match task.await { Ok(_) => (true, None), Err(error) => { @@ -970,11 +982,6 @@ impl DapStore { })?), ), } - } else { - ( - false, - Some(serde_json::to_value(ErrorResponse { error: None })?), - ) }; client From d99653fd1578867d0ef45c17b065a77e4d6ebd47 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 8 Jan 2025 11:49:23 +0100 Subject: [PATCH 445/650] Add basic test to validate StartDebugging reverse request works --- crates/dap/src/session.rs | 5 +- crates/dap/src/transport.rs | 13 ++ .../debugger_ui/src/tests/debugger_panel.rs | 137 +++++++++++++++++- crates/project/src/dap_store.rs | 11 +- 4 files changed, 158 insertions(+), 8 deletions(-) diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs index ee3ac0943d0d66..735adb5bfd3d61 100644 --- a/crates/dap/src/session.rs +++ b/crates/dap/src/session.rs @@ -89,8 +89,9 @@ impl DebugSession { self.clients.get(client_id).cloned() } - pub fn has_clients(&self) -> bool { - !self.clients.is_empty() + #[cfg(any(test, feature = "test-support"))] + pub fn clients_len(&self) -> usize { + self.clients.len() } pub fn clients(&self) -> impl Iterator> + '_ { diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 8b2953d90362f0..d0c7fa64d0af34 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -820,6 +820,19 @@ impl FakeTransport { #[cfg(any(test, feature = "test-support"))] #[async_trait(?Send)] impl Transport for FakeTransport { + async fn reconnect(&self, cx: &mut AsyncAppContext) -> Result { + self.start( + &DebugAdapterBinary { + command: "command".into(), + arguments: None, + envs: None, + cwd: None, + }, + cx, + ) + .await + } + async fn start( &self, _binary: &DebugAdapterBinary, diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 9c5e0fe94de7ef..ae7c2a63edeb1d 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1,10 +1,13 @@ use crate::*; use dap::{ - requests::{Disconnect, Initialize, Launch, RunInTerminal, StackTrace}, - RunInTerminalRequestArguments, + client::DebugAdapterClientId, + requests::{Disconnect, Initialize, Launch, RunInTerminal, StackTrace, StartDebugging}, + RunInTerminalRequestArguments, StartDebuggingRequestArguments, + StartDebuggingRequestArgumentsRequest, }; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; +use serde_json::json; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -780,3 +783,133 @@ async fn test_handle_error_run_in_terminal_reverse_request( shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_handle_start_debugging_reverse_request( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let send_response = Arc::new(AtomicBool::new(false)); + let send_launch = Arc::new(AtomicBool::new(false)); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_response::({ + let send_response = send_response.clone(); + move |response| { + send_response.store(true, Ordering::SeqCst); + + assert!(response.success); + assert!(response.body.is_some()); + } + }) + .await; + + cx.run_until_parked(); + + client + .fake_reverse_request::(StartDebuggingRequestArguments { + configuration: json!({}), + request: StartDebuggingRequestArgumentsRequest::Launch, + }) + .await; + + cx.run_until_parked(); + + project.update(cx, |_, cx| { + assert_eq!(2, session.read(cx).clients_len()); + }); + assert!( + send_response.load(std::sync::atomic::Ordering::SeqCst), + "Expected to receive response from reverse request" + ); + + let second_client = project.update(cx, |_, cx| { + session + .read(cx) + .client_by_id(&DebugAdapterClientId(1)) + .unwrap() + }); + + project.update(cx, |_, cx| { + cx.emit(project::Event::DebugClientStarted(( + session.read(cx).id(), + second_client.id(), + ))); + }); + + second_client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + second_client + .on_request::({ + let send_launch = send_launch.clone(); + move |_, _| { + send_launch.store(true, Ordering::SeqCst); + + Ok(()) + } + }) + .await; + second_client + .on_request::(move |_, _| Ok(())) + .await; + + cx.run_until_parked(); + + assert!( + send_launch.load(std::sync::atomic::Ordering::SeqCst), + "Expected to send launch request on second client" + ); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index ab04126278580a..e08409bdfcd048 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -487,8 +487,6 @@ impl DapStore { let session = store.session_by_id(&session_id).unwrap(); - let client = Arc::new(client); - session.update(cx, |session, cx| { session.update_configuration( |old_config| { @@ -496,10 +494,15 @@ impl DapStore { }, cx, ); - session.add_client(client.clone(), cx); + session.add_client(Arc::new(client), cx); }); - cx.emit(DapStoreEvent::DebugClientStarted((session_id, client_id))); + // don't emit this event ourself in tests, so we can add request, + // response and event handlers for this client + if !cfg!(any(test, feature = "test-support")) { + cx.emit(DapStoreEvent::DebugClientStarted((session_id, client_id))); + } + cx.notify(); }) }) From 404f77357d69d4545c5a503c0d2771e804b04e34 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 8 Jan 2025 12:15:28 +0100 Subject: [PATCH 446/650] Use title from adapter when spawning a debug terminal --- crates/debugger_ui/src/debugger_panel.rs | 1 + crates/project/src/terminals.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c38326f51efcc9..1aff0b22095d87 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -436,6 +436,7 @@ impl DebugPanel { args, envs, cwd: PathBuf::from(request_args.cwd), + title: request_args.title, }, task::RevealStrategy::Always, cx, diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index ed3cce0b9e0d7e..c770fed74f3934 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -41,6 +41,7 @@ pub enum TerminalKind { args: Vec, envs: HashMap, cwd: PathBuf, + title: Option, }, } @@ -280,6 +281,7 @@ impl Project { command, args, envs, + title, .. } => { env.extend(envs); @@ -288,7 +290,7 @@ impl Project { Shell::WithArguments { program, args, - title_override: Some("debugger".into()), + title_override: Some(title.unwrap_or("Debug Terminal".into()).into()), } } else { settings.shell.clone() From 8911c96399672b11175714ebe92f20edfe75f1de Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 8 Jan 2025 12:28:01 +0100 Subject: [PATCH 447/650] Send error response back for RunInTerminal response for debugging --- crates/debugger_ui/src/debugger_panel.rs | 81 ++++++++++++++++++++---- crates/project/src/dap_store.rs | 19 ++---- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1aff0b22095d87..6b970aced06b0b 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -9,9 +9,9 @@ use dap::{ messages::{Events, Message}, requests::{Request, RunInTerminal, StartDebugging}, session::DebugSessionId, - Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent, LoadedSourceEvent, ModuleEvent, - OutputEvent, RunInTerminalRequestArguments, StoppedEvent, TerminatedEvent, ThreadEvent, - ThreadEventReason, + Capabilities, CapabilitiesEvent, ContinuedEvent, ErrorResponse, ExitedEvent, LoadedSourceEvent, + ModuleEvent, OutputEvent, RunInTerminalRequestArguments, RunInTerminalResponse, StoppedEvent, + TerminatedEvent, ThreadEvent, ThreadEventReason, }; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, @@ -387,21 +387,39 @@ impl DebugPanel { request_args: Option, cx: &mut ViewContext, ) { + let request_args = request_args.and_then(|request_args| { + serde_json::from_value::(request_args).ok() + }); let Some(request_args) = request_args else { self.dap_store.update(cx, |store, cx| { store - .respond_to_run_in_terminal(session_id, client_id, false, seq, None, cx) + .respond_to_run_in_terminal( + session_id, + client_id, + false, + seq, + serde_json::to_value(ErrorResponse { + error: Some(dap::Message { + id: seq, + format: + "Request arguments must be provided when spawnng debug terminal" + .into(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + .ok(), + cx, + ) .detach_and_log_err(cx); }); - return; }; - let request_args: RunInTerminalRequestArguments = - serde_json::from_value(request_args).unwrap(); - let mut envs: HashMap = Default::default(); - if let Some(Value::Object(env)) = request_args.env { for (key, value) in env { let value_str = match (key.as_str(), value) { @@ -459,9 +477,16 @@ impl DebugPanel { cx.spawn(|this, mut cx| async move { // Ensure a response is always sent, even in error cases, // to maintain proper communication with the debug adapter - let (success, pid) = match terminal_task { + let (success, body) = match terminal_task { Ok(pid_task) => match pid_task.await { - Ok(pid) => (true, pid), + Ok(pid) => ( + true, + serde_json::to_value(RunInTerminalResponse { + process_id: None, + shell_process_id: pid.map(|pid| pid.as_u32() as u64), + }) + .ok(), + ), Err(error) => { this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |_, cx| { @@ -470,10 +495,38 @@ impl DebugPanel { }) .log_err(); - (false, None) + ( + false, + serde_json::to_value(ErrorResponse { + error: Some(dap::Message { + id: seq, + format: error.to_string(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + .ok(), + ) } }, - Err(_) => (false, None), + Err(error) => ( + false, + serde_json::to_value(ErrorResponse { + error: Some(dap::Message { + id: seq, + format: error.to_string(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + .ok(), + ), }; let respond_task = this.update(&mut cx, |this, cx| { @@ -483,7 +536,7 @@ impl DebugPanel { &client_id, success, seq, - pid.map(|pid| pid.as_u32() as u64), + body, cx, ) }) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index e08409bdfcd048..3c86d1a283d025 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -18,9 +18,9 @@ use dap::{ ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, LoadedSourcesArguments, Module, - ModulesArguments, NextArguments, PauseArguments, RestartArguments, RunInTerminalResponse, - Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, - Source, SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, + ModulesArguments, NextArguments, PauseArguments, RestartArguments, Scope, ScopesArguments, + SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, + SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, StepBackArguments, StepInArguments, StepOutArguments, SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, VariablesArguments, @@ -1005,7 +1005,7 @@ impl DapStore { client_id: &DebugAdapterClientId, success: bool, seq: u64, - shell_pid: Option, + body: Option, cx: &mut ModelContext, ) -> Task> { let Some((_, client)) = self.client_by_id(client_id, cx) else { @@ -1020,17 +1020,10 @@ impl DapStore { client .send_message(Message::Response(Response { seq, - request_seq: seq, + body, success, + request_seq: seq, command: RunInTerminal::COMMAND.to_string(), - body: match success { - true => serde_json::to_value(RunInTerminalResponse { - process_id: None, - shell_process_id: shell_pid, - }) - .ok(), - false => serde_json::to_value(ErrorResponse { error: None }).ok(), - }, })) .await }) From dd4a1f7d309393b625b17e00c875a6078fe8f65d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 8 Jan 2025 15:29:16 +0100 Subject: [PATCH 448/650] Only change the request timeout for tests --- crates/dap/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 1fc7c90b6adeec..2071a6a71b121f 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -19,10 +19,10 @@ use std::{ time::Duration, }; -#[cfg(debug_assertions)] +#[cfg(any(test, feature = "test-support"))] const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2); -#[cfg(not(debug_assertions))] +#[cfg(not(any(test, feature = "test-support")))] const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] From 01648b9370cc947aa3d7eef72da8fac6c08ff91e Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:17:50 -0500 Subject: [PATCH 449/650] Integrate Debugger within Collab DB (#81) This PR integrates Zed's Debugger with the Collab database, enabling Zed to guarantee that clients joining a project with active debug sessions in progress will receive and set up those sessions. * Add test for setting active debug panel items on project join * Add DebuggerSession proto message * Modify debugger session * Get collab server to build * Get collab test to compile * Add proto messages * Set up message handler for get debugger sessions * Send set debug panel requests when handling get debugger sessions msg * Get request to send and set debug panel * Setup ground work for debug sessions collab db table * Set up debug_client table for collab db * Remove up GetDebuggerSession proto request code * Get proto::DebuggerSession during join_project_internal * Remove debug_sessions table from collab db * Add migration for debug_client and fix some bugs * Create dap store event queue for remote daps When creating a project in from_join_project_response(...) the debug panel hasn't been initialized yet so it can't handle dap_store events. The solution to this is creating an event queue that debug panel takes from dap store if it's remote and then handles all the events * Fix debug panel join project during session test * Add debug_panel_items table to collab db * Integrate debug_panel_item table into collab msg handlers * Finialize debug_panel_item table refactor for collab db * Integrate UpdateDebugAdapter RPC with collab DB * Handle ShutdownDebugClient RPC for collab db * Fix clippy --- .../20221109000000_test_schema.sql | 34 +++ .../20250107082721_create_debugger_tables.sql | 33 +++ crates/collab/src/db.rs | 1 + crates/collab/src/db/ids.rs | 3 + crates/collab/src/db/queries/projects.rs | 239 ++++++++++++++++++ crates/collab/src/db/tables.rs | 2 + crates/collab/src/db/tables/debug_clients.rs | 101 ++++++++ .../collab/src/db/tables/debug_panel_items.rs | 155 ++++++++++++ crates/collab/src/db/tables/project.rs | 16 ++ crates/collab/src/rpc.rs | 102 +++++++- crates/collab/src/tests/debug_panel_tests.rs | 136 ++++++++++ crates/debugger_ui/src/debugger_panel.rs | 19 +- crates/debugger_ui/src/debugger_panel_item.rs | 18 +- crates/debugger_ui/src/module_list.rs | 9 +- crates/debugger_ui/src/stack_frame_list.rs | 5 + crates/debugger_ui/src/variable_list.rs | 8 +- crates/project/src/dap_store.rs | 43 ++++ crates/project/src/project.rs | 1 + crates/proto/proto/zed.proto | 23 +- 19 files changed, 927 insertions(+), 21 deletions(-) create mode 100644 crates/collab/migrations/20250107082721_create_debugger_tables.sql create mode 100644 crates/collab/src/db/tables/debug_clients.rs create mode 100644 crates/collab/src/db/tables/debug_panel_items.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 62ab6a1d3b2f23..13e35e393ea946 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -463,3 +463,37 @@ CREATE TABLE IF NOT EXISTS "breakpoints" ( "kind" VARCHAR NOT NULL ); CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id"); + +CREATE TABLE IF NOT EXISTS "debug_clients" ( + id BIGINT NOT NULL, + project_id INTEGER NOT NULL, + session_id BIGINT NOT NULL, + capabilities INTEGER NOT NULL, + PRIMARY KEY (id, project_id, session_id), + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE +); + +CREATE INDEX "index_debug_client_on_project_id" ON "debug_clients" ("project_id"); + +CREATE TABLE IF NOT EXISTS "debug_panel_items" ( + id BIGINT NOT NULL, + project_id INTEGER NOT NULL, + thread_id BIGINT NOT NULL, + session_id BIGINT NOT NULL, + active_thread_item INTEGER NOT NULL, + seassion_name TEXT NOT NULL, + console BYTEA NOT NULL, + module_list BYTEA NOT NULL, + thread_state BYTEA NOT NULL, + variable_list BYTEA NOT NULL, + stack_frame_list BYTEA NOT NULL, + loaded_source_list BYTEA NOT NULL, + PRIMARY KEY (id, project_id, session_id, thread_id), + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (id, project_id, session_id) REFERENCES debug_clients (id, project_id, session_id) ON DELETE CASCADE +); + +CREATE INDEX "index_debug_panel_items_on_project_id" ON "debug_panel_items" ("project_id"); +CREATE INDEX "index_debug_panel_items_on_session_id" ON "debug_panel_items" ("session_id"); +CREATE INDEX "index_debug_panel_items_on_thread_id" ON "debug_panel_items" ("thread_id"); +CREATE INDEX "index_debug_panel_items_on_debug_client" ON "debug_panel_items" ("id", "project_id", "session_id"); diff --git a/crates/collab/migrations/20250107082721_create_debugger_tables.sql b/crates/collab/migrations/20250107082721_create_debugger_tables.sql new file mode 100644 index 00000000000000..79c94faa566b62 --- /dev/null +++ b/crates/collab/migrations/20250107082721_create_debugger_tables.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS "debug_clients" ( + id BIGINT NOT NULL, + project_id INTEGER NOT NULL, + session_id BIGINT NOT NULL, + capabilities INTEGER NOT NULL, + PRIMARY KEY (id, project_id, session_id), + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE +); + +CREATE INDEX "index_debug_client_on_project_id" ON "debug_clients" ("project_id"); + +CREATE TABLE IF NOT EXISTS "debug_panel_items" ( + id BIGINT NOT NULL, + project_id INTEGER NOT NULL, + thread_id BIGINT NOT NULL, + session_id BIGINT NOT NULL, + active_thread_item INTEGER NOT NULL, + seassion_name TEXT NOT NULL, + console BYTEA NOT NULL, + module_list BYTEA NOT NULL, + thread_state BYTEA NOT NULL, + variable_list BYTEA NOT NULL, + stack_frame_list BYTEA NOT NULL, + loaded_source_list BYTEA NOT NULL, + PRIMARY KEY (id, project_id, session_id, thread_id), + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (id, project_id, session_id) REFERENCES debug_clients (id, project_id, session_id) ON DELETE CASCADE +); + +CREATE INDEX "index_debug_panel_items_on_project_id" ON "debug_panel_items" ("project_id"); +CREATE INDEX "index_debug_panel_items_on_session_id" ON "debug_panel_items" ("session_id"); +CREATE INDEX "index_debug_panel_items_on_thread_id" ON "debug_panel_items" ("thread_id"); +CREATE INDEX "index_debug_panel_items_on_debug_client" ON "debug_panel_items" ("id", "project_id", "session_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f569265d1cb6e3..94768530be092f 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -738,6 +738,7 @@ pub struct Project { pub worktrees: BTreeMap, pub language_servers: Vec, pub breakpoints: HashMap>, + pub debug_sessions: Vec, } pub struct ProjectCollaborator { diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 698b1c5693337e..36f3dad9e2fb77 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -94,6 +94,9 @@ id_type!(RoomParticipantId); id_type!(ServerId); id_type!(SignupId); id_type!(UserId); +id_type!(DebugClientId); +id_type!(SessionId); +id_type!(ThreadId); /// ChannelRole gives you permissions for both channels and calls. #[derive( diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index b16763fd8f4bed..eb13eb81bba822 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -549,6 +549,202 @@ impl Database { .await } + pub async fn update_debug_adapter( + &self, + connection_id: ConnectionId, + update: &proto::UpdateDebugAdapter, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + self.project_transaction(project_id, |tx| async move { + let mut debug_panel_items = debug_panel_items::Entity::find() + .filter( + Condition::all() + .add(debug_panel_items::Column::ProjectId.eq(project_id)) + .add(debug_panel_items::Column::SessionId.eq(update.session_id)) + .add(debug_panel_items::Column::Id.eq(update.client_id)), + ) + .all(&*tx) + .await?; + + if let Some(thread_id) = update.thread_id { + debug_panel_items.retain(|item| item.thread_id == thread_id as i64); + } + + for mut item in debug_panel_items { + item.update_panel_item(&update)?; + + debug_panel_items::Entity::update(debug_panel_items::ActiveModel { + id: ActiveValue::Unchanged(item.id), + project_id: ActiveValue::Unchanged(item.project_id), + session_id: ActiveValue::Unchanged(item.session_id), + thread_id: ActiveValue::Unchanged(item.thread_id), + active_thread_item: ActiveValue::Unchanged(item.active_thread_item), + seassion_name: ActiveValue::Unchanged(item.seassion_name), + console: ActiveValue::Unchanged(item.console), + module_list: ActiveValue::Set(item.module_list), + thread_state: ActiveValue::Set(item.thread_state), + variable_list: ActiveValue::Set(item.variable_list), + stack_frame_list: ActiveValue::Set(item.stack_frame_list), + loaded_source_list: ActiveValue::Unchanged(item.loaded_source_list), + }) + .exec(&*tx) + .await?; + } + + self.internal_project_connection_ids(project_id, connection_id, true, &tx) + .await + }) + .await + } + + pub async fn shutdown_debug_client( + &self, + connection_id: ConnectionId, + update: &proto::ShutdownDebugClient, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + self.project_transaction(project_id, |tx| async move { + debug_clients::Entity::delete_by_id(( + update.client_id as i64, + ProjectId::from_proto(update.project_id), + update.session_id as i64, + )) + .exec(&*tx) + .await?; + + self.internal_project_connection_ids(project_id, connection_id, true, &tx) + .await + }) + .await + } + + pub async fn set_debug_client_panel_item( + &self, + connection_id: ConnectionId, + update: &proto::SetDebuggerPanelItem, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + self.project_transaction(project_id, |tx| async move { + let debug_client = debug_clients::Entity::find() + .filter( + Condition::all() + .add(debug_clients::Column::ProjectId.eq(project_id)) + .add(debug_clients::Column::SessionId.eq(update.session_id)), + ) + .one(&*tx) + .await?; + + if debug_client.is_none() { + let new_debug_client = debug_clients::ActiveModel { + id: ActiveValue::Set(update.client_id as i64), + project_id: ActiveValue::Set(project_id), + session_id: ActiveValue::Set(update.session_id as i64), + capabilities: ActiveValue::Set(0), + }; + new_debug_client.insert(&*tx).await?; + } + + let mut debug_panel_item = debug_panel_items::Entity::find() + .filter( + Condition::all() + .add(debug_panel_items::Column::ProjectId.eq(project_id)) + .add(debug_panel_items::Column::SessionId.eq(update.session_id as i64)) + .add(debug_panel_items::Column::ThreadId.eq(update.thread_id as i64)) + .add(debug_panel_items::Column::Id.eq(update.client_id as i64)), + ) + .one(&*tx) + .await?; + + if debug_panel_item.is_none() { + let new_debug_panel_item = debug_panel_items::ActiveModel { + id: ActiveValue::Set(update.client_id as i64), + project_id: ActiveValue::Set(project_id), + session_id: ActiveValue::Set(update.session_id as i64), + thread_id: ActiveValue::Set(update.thread_id as i64), + seassion_name: ActiveValue::Set(update.session_name.clone()), + active_thread_item: ActiveValue::Set(0), + console: ActiveValue::Set(Vec::new()), + module_list: ActiveValue::Set(Vec::new()), + thread_state: ActiveValue::Set(Vec::new()), + variable_list: ActiveValue::Set(Vec::new()), + stack_frame_list: ActiveValue::Set(Vec::new()), + loaded_source_list: ActiveValue::Set(Vec::new()), + }; + + debug_panel_item = Some(new_debug_panel_item.insert(&*tx).await?); + }; + + let mut debug_panel_item = debug_panel_item.unwrap(); + + debug_panel_item.set_panel_item(&update); + debug_panel_items::Entity::update(debug_panel_items::ActiveModel { + id: ActiveValue::Unchanged(debug_panel_item.id), + project_id: ActiveValue::Unchanged(debug_panel_item.project_id), + session_id: ActiveValue::Unchanged(debug_panel_item.session_id), + thread_id: ActiveValue::Unchanged(debug_panel_item.thread_id), + seassion_name: ActiveValue::Unchanged(debug_panel_item.seassion_name), + active_thread_item: ActiveValue::Set(debug_panel_item.active_thread_item), + console: ActiveValue::Set(debug_panel_item.console), + module_list: ActiveValue::Set(debug_panel_item.module_list), + thread_state: ActiveValue::Set(debug_panel_item.thread_state), + variable_list: ActiveValue::Set(debug_panel_item.variable_list), + stack_frame_list: ActiveValue::Set(debug_panel_item.stack_frame_list), + loaded_source_list: ActiveValue::Set(debug_panel_item.loaded_source_list), + }) + .exec(&*tx) + .await?; + + self.internal_project_connection_ids(project_id, connection_id, true, &tx) + .await + }) + .await + } + + pub async fn update_debug_client_capabilities( + &self, + connection_id: ConnectionId, + update: &proto::SetDebugClientCapabilities, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + self.project_transaction(project_id, |tx| async move { + let mut debug_client = debug_clients::Entity::find() + .filter( + Condition::all() + .add(debug_clients::Column::ProjectId.eq(project_id)) + .add(debug_clients::Column::SessionId.eq(update.session_id)), + ) + .one(&*tx) + .await?; + + if debug_client.is_none() { + let new_debug_client = debug_clients::ActiveModel { + id: ActiveValue::Set(update.client_id as i64), + project_id: ActiveValue::Set(project_id), + session_id: ActiveValue::Set(update.session_id as i64), + capabilities: ActiveValue::Set(0), + }; + debug_client = Some(new_debug_client.insert(&*tx).await?); + } + + let mut debug_client = debug_client.unwrap(); + + debug_client.set_capabilities(update); + + debug_clients::Entity::update(debug_clients::ActiveModel { + id: ActiveValue::Unchanged(debug_client.id), + project_id: ActiveValue::Unchanged(debug_client.project_id), + session_id: ActiveValue::Unchanged(debug_client.session_id), + capabilities: ActiveValue::Set(debug_client.capabilities), + }) + .exec(&*tx) + .await?; + + self.internal_project_connection_ids(project_id, connection_id, true, &tx) + .await + }) + .await + } + pub async fn update_breakpoints( &self, connection_id: ConnectionId, @@ -871,6 +1067,48 @@ impl Database { } } + let project_id = project.id.to_proto(); + let debug_clients = project.find_related(debug_clients::Entity).all(tx).await?; + let mut debug_sessions: HashMap<_, Vec<_>> = HashMap::default(); + + for debug_client in debug_clients { + debug_sessions + .entry(debug_client.session_id) + .or_default() + .push(debug_client); + } + + let mut sessions: Vec<_> = Vec::default(); + + for (session_id, clients) in debug_sessions.into_iter() { + let mut debug_clients = Vec::default(); + + for debug_client in clients.into_iter() { + let debug_panel_items = debug_client + .find_related(debug_panel_items::Entity) + .all(tx) + .await? + .into_iter() + .map(|item| item.panel_item()) + .collect(); + + debug_clients.push(proto::DebugClient { + client_id: debug_client.id as u64, + capabilities: Some(debug_client.capabilities()), + debug_panel_items, + active_debug_line: None, + }); + } + + sessions.push(proto::DebuggerSession { + project_id, + session_id: session_id as u64, + clients: debug_clients, + }); + } + + let debug_sessions = sessions; + let mut breakpoints: HashMap> = HashMap::default(); @@ -926,6 +1164,7 @@ impl Database { }) .collect(), breakpoints, + debug_sessions, }; Ok((project, replica_id as ReplicaId)) } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index fd4553c52b41c4..a81bc108aad498 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -14,6 +14,8 @@ pub mod channel_message; pub mod channel_message_mention; pub mod contact; pub mod contributor; +pub mod debug_clients; +pub mod debug_panel_items; pub mod embedding; pub mod extension; pub mod extension_version; diff --git a/crates/collab/src/db/tables/debug_clients.rs b/crates/collab/src/db/tables/debug_clients.rs new file mode 100644 index 00000000000000..e27febacbba831 --- /dev/null +++ b/crates/collab/src/db/tables/debug_clients.rs @@ -0,0 +1,101 @@ +use crate::db::ProjectId; +use anyhow::Result; +use rpc::proto::SetDebugClientCapabilities; +use sea_orm::entity::prelude::*; + +const SUPPORTS_LOADED_SOURCES_REQUEST_BIT: u32 = 0; +const SUPPORTS_MODULES_REQUEST_BIT: u32 = 1; +const SUPPORTS_RESTART_REQUEST_BIT: u32 = 2; +const SUPPORTS_SET_EXPRESSION_BIT: u32 = 3; +const SUPPORTS_SINGLE_THREAD_EXECUTION_REQUESTS_BIT: u32 = 4; +const SUPPORTS_STEP_BACK_BIT: u32 = 5; +const SUPPORTS_STEPPING_GRANULARITY_BIT: u32 = 6; +const SUPPORTS_TERMINATE_THREADS_REQUEST_BIT: u32 = 7; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "debug_clients")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + #[sea_orm(primary_key)] + pub project_id: ProjectId, + #[sea_orm(primary_key)] + pub session_id: i64, + #[sea_orm(column_type = "Integer")] + pub capabilities: i32, +} + +impl Model { + pub fn capabilities(&self) -> SetDebugClientCapabilities { + SetDebugClientCapabilities { + session_id: self.session_id as u64, + client_id: self.id as u64, + project_id: ProjectId::to_proto(self.project_id), + supports_loaded_sources_request: (self.capabilities + & (1 << SUPPORTS_LOADED_SOURCES_REQUEST_BIT)) + != 0, + supports_modules_request: (self.capabilities & (1 << SUPPORTS_MODULES_REQUEST_BIT)) + != 0, + supports_restart_request: (self.capabilities & (1 << SUPPORTS_RESTART_REQUEST_BIT)) + != 0, + supports_single_thread_execution_requests: (self.capabilities + & (1 << SUPPORTS_SINGLE_THREAD_EXECUTION_REQUESTS_BIT)) + != 0, + supports_set_expression: (self.capabilities & (1 << SUPPORTS_SET_EXPRESSION_BIT)) != 0, + supports_step_back: (self.capabilities & (1 << SUPPORTS_STEP_BACK_BIT)) != 0, + supports_stepping_granularity: (self.capabilities + & (1 << SUPPORTS_STEPPING_GRANULARITY_BIT)) + != 0, + supports_terminate_threads_request: (self.capabilities + & (1 << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT)) + != 0, + } + } + + pub fn set_capabilities(&mut self, capabilities: &SetDebugClientCapabilities) { + let mut capabilities_bit_mask = 0i32; + capabilities_bit_mask |= (capabilities.supports_loaded_sources_request as i32) + << SUPPORTS_LOADED_SOURCES_REQUEST_BIT; + capabilities_bit_mask |= + (capabilities.supports_modules_request as i32) << SUPPORTS_MODULES_REQUEST_BIT; + capabilities_bit_mask |= + (capabilities.supports_restart_request as i32) << SUPPORTS_RESTART_REQUEST_BIT; + capabilities_bit_mask |= + (capabilities.supports_set_expression as i32) << SUPPORTS_SET_EXPRESSION_BIT; + capabilities_bit_mask |= (capabilities.supports_single_thread_execution_requests as i32) + << SUPPORTS_SINGLE_THREAD_EXECUTION_REQUESTS_BIT; + capabilities_bit_mask |= (capabilities.supports_step_back as i32) << SUPPORTS_STEP_BACK_BIT; + capabilities_bit_mask |= (capabilities.supports_stepping_granularity as i32) + << SUPPORTS_STEPPING_GRANULARITY_BIT; + capabilities_bit_mask |= (capabilities.supports_terminate_threads_request as i32) + << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT; + + self.capabilities = capabilities_bit_mask; + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::project::Entity", + from = "Column::ProjectId", + to = "super::project::Column::Id" + )] + Project, + #[sea_orm(has_many = "super::debug_panel_items::Entity")] + DebugPanelItems, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::DebugPanelItems.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/debug_panel_items.rs b/crates/collab/src/db/tables/debug_panel_items.rs new file mode 100644 index 00000000000000..d4d91f7d29f805 --- /dev/null +++ b/crates/collab/src/db/tables/debug_panel_items.rs @@ -0,0 +1,155 @@ +use crate::db::ProjectId; +use anyhow::{anyhow, Result}; +use prost::Message; +use rpc::{proto, proto::SetDebuggerPanelItem}; +use sea_orm::entity::prelude::*; +use util::ResultExt; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "debug_panel_items")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + #[sea_orm(primary_key)] + pub project_id: ProjectId, + #[sea_orm(primary_key)] + pub session_id: i64, + #[sea_orm(primary_key)] + pub thread_id: i64, + // Below are fields for a debug panel item + pub active_thread_item: i32, + pub seassion_name: String, + pub console: Vec, + pub module_list: Vec, + pub thread_state: Vec, + pub variable_list: Vec, + pub stack_frame_list: Vec, + pub loaded_source_list: Vec, +} + +impl Model { + pub fn set_panel_item(&mut self, item: &SetDebuggerPanelItem) { + let mut buf = Vec::new(); + + self.active_thread_item = item.active_thread_item; + + if let Some(console) = item.console.as_ref() { + if let Some(()) = console.encode(&mut buf).log_err() { + self.console.clone_from(&buf); + } + } + + buf.clear(); + if let Some(module_list) = item.module_list.as_ref() { + if let Some(()) = module_list.encode(&mut buf).log_err() { + self.module_list.clone_from(&buf); + } + } + + buf.clear(); + if let Some(thread_state) = item.thread_state.as_ref() { + if let Some(()) = thread_state.encode(&mut buf).log_err() { + self.thread_state.clone_from(&buf); + } + } + + buf.clear(); + if let Some(variable_list) = item.variable_list.as_ref() { + if let Some(()) = variable_list.encode(&mut buf).log_err() { + self.variable_list.clone_from(&buf); + } + } + + buf.clear(); + if let Some(stack_frame_list) = item.stack_frame_list.as_ref() { + if let Some(()) = stack_frame_list.encode(&mut buf).log_err() { + self.stack_frame_list.clone_from(&buf); + } + } + + buf.clear(); + if let Some(loaded_source_list) = item.loaded_source_list.as_ref() { + if let Some(()) = loaded_source_list.encode(&mut buf).log_err() { + self.loaded_source_list.clone_from(&buf); + } + } + } + + pub fn panel_item(&self) -> SetDebuggerPanelItem { + SetDebuggerPanelItem { + project_id: self.project_id.to_proto(), + session_id: self.session_id as u64, + client_id: self.id as u64, + thread_id: self.thread_id as u64, + session_name: self.seassion_name.clone(), + active_thread_item: self.active_thread_item, + console: proto::DebuggerConsole::decode(&self.console[..]).log_err(), + module_list: proto::DebuggerModuleList::decode(&self.module_list[..]).log_err(), + thread_state: proto::DebuggerThreadState::decode(&self.thread_state[..]).log_err(), + variable_list: proto::DebuggerVariableList::decode(&self.variable_list[..]).log_err(), + stack_frame_list: proto::DebuggerStackFrameList::decode(&self.stack_frame_list[..]) + .log_err(), + loaded_source_list: proto::DebuggerLoadedSourceList::decode( + &self.loaded_source_list[..], + ) + .log_err(), + } + } + + pub fn update_panel_item(&mut self, update: &proto::UpdateDebugAdapter) -> Result<()> { + match update + .variant + .as_ref() + .ok_or(anyhow!("All update debug adapter RPCs must have a variant"))? + { + proto::update_debug_adapter::Variant::ThreadState(thread_state) => { + let encoded = thread_state.encode_to_vec(); + self.thread_state = encoded; + } + proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => { + let encoded = stack_frame_list.encode_to_vec(); + self.stack_frame_list = encoded; + } + proto::update_debug_adapter::Variant::VariableList(variable_list) => { + let encoded = variable_list.encode_to_vec(); + self.variable_list = encoded; + } + proto::update_debug_adapter::Variant::Modules(module_list) => { + let encoded = module_list.encode_to_vec(); + self.module_list = encoded; + } + } + + Ok(()) + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::project::Entity", + from = "Column::ProjectId", + to = "super::project::Column::Id" + )] + Project, + #[sea_orm( + belongs_to = "super::debug_clients::Entity", + from = "(Column::Id, Column::ProjectId, Column::SessionId)", + to = "(super::debug_clients::Column::Id, super::debug_clients::Column::ProjectId, super::debug_clients::Column::SessionId)" + )] + DebugClient, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::DebugClient.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/project.rs b/crates/collab/src/db/tables/project.rs index 09d20bce625435..b87dd70a1dbb6a 100644 --- a/crates/collab/src/db/tables/project.rs +++ b/crates/collab/src/db/tables/project.rs @@ -51,6 +51,10 @@ pub enum Relation { LanguageServers, #[sea_orm(has_many = "super::breakpoints::Entity")] Breakpoints, + #[sea_orm(has_many = "super::debug_clients::Entity")] + DebugClients, + #[sea_orm(has_many = "super::debug_panel_items::Entity")] + DebugPanelItems, } impl Related for Entity { @@ -89,4 +93,16 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::DebugClients.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::DebugPanelItems.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 61ff8756bfe115..14ef0c31821a47 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -417,12 +417,10 @@ impl Server { .add_message_handler( broadcast_project_message_from_host::, ) - .add_message_handler(broadcast_project_message_from_host::) - .add_message_handler(broadcast_project_message_from_host::) - .add_message_handler( - broadcast_project_message_from_host::, - ) - .add_message_handler(broadcast_project_message_from_host::); + .add_message_handler(set_debug_client_panel_item) + .add_message_handler(update_debug_adapter) + .add_message_handler(update_debug_client_capabilities) + .add_message_handler(shutdown_debug_client); Arc::new(server) } @@ -1883,6 +1881,7 @@ fn join_project_internal( language_servers: project.language_servers.clone(), role: project.role.into(), breakpoints, + debug_sessions: project.debug_sessions.clone(), // Todo(Debugger) Figure out how to avoid cloning })?; for (worktree_id, worktree) in mem::take(&mut project.worktrees) { @@ -2069,7 +2068,7 @@ async fn update_worktree_settings( Ok(()) } -/// Notify other participants that a language server has started. +/// Notify other participants that a language server has started. async fn start_language_server( request: proto::StartLanguageServer, session: Session, @@ -2115,6 +2114,95 @@ async fn update_language_server( Ok(()) } +/// Notify other participants that a debug client has shutdown +async fn shutdown_debug_client( + request: proto::ShutdownDebugClient, + session: Session, +) -> Result<()> { + let guest_connection_ids = session + .db() + .await + .shutdown_debug_client(session.connection_id, &request) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, request.clone()) + }, + ); + Ok(()) +} + +/// Notify other participants that a debug panel item has been updated +async fn update_debug_adapter(request: proto::UpdateDebugAdapter, session: Session) -> Result<()> { + let guest_connection_ids = session + .db() + .await + .update_debug_adapter(session.connection_id, &request) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, request.clone()) + }, + ); + Ok(()) +} + +/// Notify other participants that there's a new debug panel item +async fn set_debug_client_panel_item( + request: proto::SetDebuggerPanelItem, + session: Session, +) -> Result<()> { + let guest_connection_ids = session + .db() + .await + .set_debug_client_panel_item(session.connection_id, &request) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, request.clone()) + }, + ); + Ok(()) +} + +/// Notify other participants that a debug client's capabilities has been created or updated +async fn update_debug_client_capabilities( + request: proto::SetDebugClientCapabilities, + session: Session, +) -> Result<()> { + let guest_connection_ids = session + .db() + .await + .update_debug_client_capabilities(session.connection_id, &request) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, request.clone()) + }, + ); + Ok(()) +} + /// Notify other participants that breakpoints have changed. async fn update_breakpoints( request: proto::SynchronizeBreakpoints, diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index fb8c69312c14f8..20d1b3317cceef 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -163,3 +163,139 @@ async fn test_debug_panel_item_opens_on_remote( }); }); } + +#[gpui::test] +async fn test_active_debug_panel_item_set_on_join_project( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + init_test(cx_a); + init_test(cx_b); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + + add_debugger_panel(&workspace_a, cx_a).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + // Give client_a time to send a debug panel item to collab server + cx_a.run_until_parked(); + + let project_b = client_b.join_remote_project(project_id, cx_b).await; + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + add_debugger_panel(&workspace_b, cx_b).await; + + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + }); + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); + + cx_b.run_until_parked(); + + // assert we don't have a debug panel item anymore because the client shutdown + workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }); +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 6b970aced06b0b..83e3a61981424f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -202,7 +202,9 @@ impl DebugPanel { }), ]; - Self { + let dap_store = project.read(cx).dap_store(); + + let mut debug_panel = Self { pane, size: px(300.), _subscriptions, @@ -211,8 +213,20 @@ impl DebugPanel { thread_states: Default::default(), message_queue: Default::default(), workspace: workspace.weak_handle(), - dap_store: project.read(cx).dap_store(), + dap_store: dap_store.clone(), + }; + + if let Some(mut dap_event_queue) = debug_panel + .dap_store + .clone() + .update(cx, |this, _| this.remote_event_queue()) + { + while let Some(dap_event) = dap_event_queue.pop_front() { + debug_panel.on_dap_store_event(debug_panel.dap_store.clone(), &dap_event, cx); + } } + + debug_panel }) } @@ -878,7 +892,6 @@ impl DebugPanel { event: &project::dap_store::DapStoreEvent, cx: &mut ViewContext, ) { - //handle the even match event { project::dap_store::DapStoreEvent::SetDebugPanelItem(set_debug_panel_item) => { self.handle_set_debug_panel_item(set_debug_panel_item, cx); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 3dcadf3435a0d9..88423c41cf5095 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -102,13 +102,23 @@ impl DebugPanelItem { let this = cx.view().clone(); let stack_frame_list = cx.new_view(|cx| { - StackFrameList::new(&workspace, &this, &dap_store, client_id, thread_id, cx) + StackFrameList::new( + &workspace, &this, &dap_store, client_id, session_id, thread_id, cx, + ) }); - let variable_list = cx - .new_view(|cx| VariableList::new(&stack_frame_list, dap_store.clone(), &client_id, cx)); + let variable_list = cx.new_view(|cx| { + VariableList::new( + &stack_frame_list, + dap_store.clone(), + &client_id, + session_id, + cx, + ) + }); - let module_list = cx.new_view(|cx| ModuleList::new(dap_store.clone(), &client_id, cx)); + let module_list = + cx.new_view(|cx| ModuleList::new(dap_store.clone(), &client_id, &session_id, cx)); let loaded_source_list = cx.new_view(|cx| LoadedSourceList::new(&this, dap_store.clone(), &client_id, cx)); diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 72e51f96d877ec..7c48da056f2c41 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,5 +1,8 @@ use anyhow::Result; -use dap::{client::DebugAdapterClientId, proto_conversions::ProtoConversion, Module, ModuleEvent}; +use dap::{ + client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, + Module, ModuleEvent, +}; use gpui::{list, AnyElement, FocusHandle, FocusableView, ListState, Model, Task}; use project::dap_store::DapStore; use rpc::proto::{DebuggerModuleList, UpdateDebugAdapter}; @@ -12,12 +15,14 @@ pub struct ModuleList { focus_handle: FocusHandle, dap_store: Model, client_id: DebugAdapterClientId, + session_id: DebugSessionId, } impl ModuleList { pub fn new( dap_store: Model, client_id: &DebugAdapterClientId, + session_id: &DebugSessionId, cx: &mut ViewContext, ) -> Self { let weakview = cx.view().downgrade(); @@ -35,6 +40,7 @@ impl ModuleList { dap_store, focus_handle, client_id: *client_id, + session_id: *session_id, modules: Vec::default(), }; @@ -101,6 +107,7 @@ impl ModuleList { if let Some((client, id)) = this.dap_store.read(cx).downstream_client() { let request = UpdateDebugAdapter { + session_id: this.session_id.to_proto(), client_id: this.client_id.to_proto(), project_id: *id, thread_id: None, diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 62b2e70c47f373..cec4c5bc394863 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -3,6 +3,7 @@ use std::path::Path; use anyhow::{anyhow, Result}; use dap::client::DebugAdapterClientId; use dap::proto_conversions::ProtoConversion; +use dap::session::DebugSessionId; use dap::StackFrame; use gpui::{ list, AnyElement, EventEmitter, FocusHandle, ListState, Subscription, Task, View, WeakView, @@ -34,6 +35,7 @@ pub struct StackFrameList { stack_frames: Vec, workspace: WeakView, client_id: DebugAdapterClientId, + session_id: DebugSessionId, _subscriptions: Vec, fetch_stack_frames_task: Option>>, } @@ -44,6 +46,7 @@ impl StackFrameList { debug_panel_item: &View, dap_store: &Model, client_id: &DebugAdapterClientId, + session_id: &DebugSessionId, thread_id: u64, cx: &mut ViewContext, ) -> Self { @@ -66,6 +69,7 @@ impl StackFrameList { focus_handle, _subscriptions, client_id: *client_id, + session_id: *session_id, workspace: workspace.clone(), dap_store: dap_store.clone(), fetch_stack_frames_task: None, @@ -173,6 +177,7 @@ impl StackFrameList { if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { let request = UpdateDebugAdapter { client_id: self.client_id.to_proto(), + session_id: self.session_id.to_proto(), project_id: *id, thread_id: Some(self.thread_id), variant: Some(rpc::proto::update_debug_adapter::Variant::StackFrameList( diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 623f25e4119e9a..6444938169fc4b 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,8 +1,8 @@ use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, Scope, ScopePresentationHint, - Variable, + client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, + Scope, ScopePresentationHint, Variable, }; use editor::{ actions::{self, SelectAll}, @@ -366,6 +366,7 @@ pub struct VariableList { dap_store: Model, open_entries: Vec, client_id: DebugAdapterClientId, + session_id: DebugSessionId, scopes: HashMap>, set_variable_editor: View, _subscriptions: Vec, @@ -382,6 +383,7 @@ impl VariableList { stack_frame_list: &View, dap_store: Model, client_id: &DebugAdapterClientId, + session_id: &DebugSessionId, cx: &mut ViewContext, ) -> Self { let weakview = cx.view().downgrade(); @@ -416,6 +418,7 @@ impl VariableList { _subscriptions, set_variable_editor, client_id: *client_id, + session_id: *session_id, open_context_menu: None, set_variable_state: None, fetch_variables_task: None, @@ -775,6 +778,7 @@ impl VariableList { if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { let request = UpdateDebugAdapter { client_id: self.client_id.to_proto(), + session_id: self.session_id.to_proto(), thread_id: Some(self.stack_frame_list.read(cx).thread_id()), project_id: *project_id, variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 3c86d1a283d025..d52d62342dd229 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -42,6 +42,7 @@ use rpc::{proto, AnyProtoClient, TypedEnvelope}; use serde_json::Value; use settings::{Settings as _, WorktreeId}; use smol::lock::Mutex; +use std::collections::VecDeque; use std::{ collections::{BTreeMap, HashSet}, ffi::OsStr, @@ -108,6 +109,7 @@ impl LocalDapStore { pub struct RemoteDapStore { upstream_client: Option, upstream_project_id: u64, + event_queue: Option>, } pub struct DapStore { @@ -174,6 +176,7 @@ impl DapStore { mode: DapStoreMode::Remote(RemoteDapStore { upstream_client: Some(upstream_client), upstream_project_id: project_id, + event_queue: Some(VecDeque::default()), }), downstream_client: None, active_debug_line: None, @@ -189,6 +192,14 @@ impl DapStore { } } + pub fn remote_event_queue(&mut self) -> Option> { + if let DapStoreMode::Remote(remote) = &mut self.mode { + remote.event_queue.take() + } else { + None + } + } + pub fn as_local(&self) -> Option<&LocalDapStore> { match &self.mode { DapStoreMode::Local(local_dap_store) => Some(local_dap_store), @@ -1491,6 +1502,38 @@ impl DapStore { }) } + pub fn set_debug_sessions_from_proto( + &mut self, + debug_sessions: Vec, + cx: &mut ModelContext, + ) { + for (session_id, debug_clients) in debug_sessions + .into_iter() + .map(|session| (session.session_id, session.clients)) + { + for debug_client in debug_clients { + if let DapStoreMode::Remote(remote) = &mut self.mode { + if let Some(queue) = &mut remote.event_queue { + debug_client.debug_panel_items.into_iter().for_each(|item| { + queue.push_back(DapStoreEvent::SetDebugPanelItem(item)); + }); + } + } + + self.update_capabilities_for_client( + &DebugSessionId::from_proto(session_id), + &DebugAdapterClientId::from_proto(debug_client.client_id), + &dap::proto_conversions::capabilities_from_proto( + &debug_client.capabilities.unwrap_or_default(), + ), + cx, + ); + } + } + + cx.notify(); + } + pub fn set_breakpoints_from_proto( &mut self, breakpoints: Vec, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 31adffa591da98..6537210ccd77e3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1013,6 +1013,7 @@ impl Project { let mut dap_store = DapStore::new_remote(remote_id, client.clone().into(), cx); dap_store.set_breakpoints_from_proto(response.payload.breakpoints, cx); + dap_store.set_debug_sessions_from_proto(response.payload.debug_sessions, cx); dap_store })?; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 7f7e0ceb471491..d845dc242e774f 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -572,6 +572,7 @@ message JoinProjectResponse { ChannelRole role = 6; reserved 7; repeated SynchronizeBreakpoints breakpoints = 8; + repeated DebuggerSession debug_sessions = 9; } message LeaveProject { @@ -2423,6 +2424,19 @@ enum BreakpointKind { Log = 1; } +message DebuggerSession { + uint64 session_id = 1; + uint64 project_id = 2; + repeated DebugClient clients = 3; +} + +message DebugClient { + uint64 client_id = 1; + SetDebugClientCapabilities capabilities = 2; + SetActiveDebugLine active_debug_line = 3; + repeated SetDebuggerPanelItem debug_panel_items = 4; +} + message ShutdownDebugClient { uint64 session_id = 1; uint64 client_id = 2; @@ -2624,11 +2638,12 @@ message UpdateDebugAdapter { uint64 project_id = 1; uint64 client_id = 2; optional uint64 thread_id = 3; + uint64 session_id = 4; oneof variant { - DebuggerThreadState thread_state = 4; - DebuggerStackFrameList stack_frame_list = 5; - DebuggerVariableList variable_list = 6; - DebuggerModuleList modules = 7; + DebuggerThreadState thread_state = 5; + DebuggerStackFrameList stack_frame_list = 6; + DebuggerVariableList variable_list = 7; + DebuggerModuleList modules = 8; } } From fa177373324314ac93135c102023c717fd2111fc Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 9 Jan 2025 16:49:41 -0500 Subject: [PATCH 450/650] Correctly set thread state in SetDebuggerPanelItem handler --- crates/debugger_ui/src/debugger_panel_item.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 88423c41cf5095..7d3fc9d1951cdc 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -18,7 +18,7 @@ use gpui::{ View, WeakView, }; use project::dap_store::DapStore; -use rpc::proto::{self, PeerId, SetDebuggerPanelItem, UpdateDebugAdapter}; +use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem, UpdateDebugAdapter}; use settings::Settings; use ui::{prelude::*, Indicator, Tooltip, WindowContext}; use util::ResultExt as _; @@ -238,6 +238,18 @@ impl DebugPanelItem { } pub(crate) fn from_proto(&mut self, state: &SetDebuggerPanelItem, cx: &mut ViewContext) { + self.thread_state.update(cx, |thread_state, _| { + let (status, stopped) = state + .thread_state + .as_ref() + .map_or((DebuggerThreadStatus::Stopped, true), |thread_state| { + (thread_state.thread_status(), true) + }); + + thread_state.status = ThreadStatus::from_proto(status); + thread_state.stopped = stopped; + }); + self.active_thread_item = ThreadItem::from_proto(state.active_thread_item()); if let Some(stack_frame_list) = state.stack_frame_list.as_ref() { From 73627a384348f6c80d82f10b7f7581e211588592 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 10 Jan 2025 04:06:08 -0500 Subject: [PATCH 451/650] Get downstream collab clients to set their own active debug line Before this commit downstream clients in active debug sessions relied on the host to send them the active debug line. This had three main limitations (Which are solved by this commit) 1. Downstream clients didn't have the ability to click on their own stack frame list and go to that frame's location 2. Downstream clients would always follow the host when checking out stack frames even from a different debug adapter or thread 3. If a user joins an active debug session they wouldn't have an active debug line until the host's debug adapter sent another stop event --- crates/debugger_ui/src/debugger_panel.rs | 30 +++++++++++++++---- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/stack_frame_list.rs | 6 ++++ crates/project/src/dap_store.rs | 11 ------- crates/project/src/project.rs | 6 +--- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 83e3a61981424f..cb52a06779a43b 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -911,21 +911,41 @@ impl DebugPanel { let client_id = DebugAdapterClientId::from_proto(update.client_id); let thread_id = update.thread_id; - let existing_item = self + let active_item = self + .pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()); + + let search = self .pane .read(cx) .items() .filter_map(|item| item.downcast::()) - .find(|item| { - let item = item.read(cx); + .find_map(|item_view| { + let item = item_view.read(cx); - item.client_id() == client_id + if item.client_id() == client_id && thread_id.map(|id| id == item.thread_id()).unwrap_or(true) + { + Some(( + item_view.clone(), + active_item + .as_ref() + .map_or(false, |this| this == &item_view), + )) + } else { + None + } }); - if let Some(debug_panel_item) = existing_item { + if let Some((debug_panel_item, is_active_item)) = search { debug_panel_item.update(cx, |this, cx| { this.update_adapter(update, cx); + + if is_active_item { + this.go_to_current_stack_frame(cx); + } }); } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 7d3fc9d1951cdc..bfd29ef2b9e003 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -474,7 +474,7 @@ impl DebugPanelItem { match update_variant { proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => { self.stack_frame_list.update(cx, |this, cx| { - this.set_from_proto(stack_frame_list.clone(), cx) + this.set_from_proto(stack_frame_list.clone(), cx); }) } proto::update_debug_adapter::Variant::ThreadState(thread_state) => { diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index cec4c5bc394863..52e81ab9f822cf 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -132,6 +132,12 @@ impl StackFrameList { } fn fetch_stack_frames(&mut self, go_to_stack_frame: bool, cx: &mut ViewContext) { + // If this is a remote debug session we never need to fetch stack frames ourselves + // because the host will fetch and send us stack frames whenever there's a stop event + if self.dap_store.read(cx).as_remote().is_some() { + return; + } + let task = self.dap_store.update(cx, |store, cx| { store.stack_frames(&self.client_id, self.thread_id, cx) }); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index d52d62342dd229..f297a0e2628642 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -310,17 +310,6 @@ impl DapStore { self.active_debug_line = Some((*client_id, project_path.clone(), row)); cx.emit(DapStoreEvent::ActiveDebugLineChanged); cx.notify(); - - if let Some((client, project_id)) = self.downstream_client.clone() { - client - .send(client::proto::SetActiveDebugLine { - row, - project_id, - client_id: client_id.to_proto(), - project_path: Some(project_path.to_proto()), - }) - .log_err(); - } } pub fn remove_active_debug_line_for_client( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6537210ccd77e3..84bf7eafbef073 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3914,11 +3914,7 @@ impl Project { let trees = self.worktrees(cx); for tree in trees { - if let Some(relative_path) = tree - .read(cx) - .as_local() - .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok()) - { + if let Some(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()).ok() { return Some((tree.clone(), relative_path.into())); } } From 46b72f6e3b0e78f24edd608b7a50a269ac7257a4 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 10 Jan 2025 17:53:17 -0500 Subject: [PATCH 452/650] Reset thread status to stop after failed step{over,in,out,back} & continue requests When thread status is not Stopped users are unable to click buttons. So the thread status needs to be reset if any of the above requests fail or else a user loses to ability to click any of the debug buttons related to those requests --- crates/debugger_ui/src/debugger_panel_item.rs | 94 ++++++++--- .../debugger_ui/src/tests/debugger_panel.rs | 152 +++++++++++++++++- 2 files changed, 220 insertions(+), 26 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index bfd29ef2b9e003..16d9b186036257 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -526,6 +526,11 @@ impl DebugPanelItem { &self.variable_list } + #[cfg(any(test, feature = "test-support"))] + pub fn thread_status(&self, cx: &ViewContext) -> ThreadStatus { + self.thread_state.read(cx).status + } + pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { self.dap_store.read(cx).capabilities_by_id(&self.client_id) } @@ -603,59 +608,100 @@ impl DebugPanelItem { pub fn continue_thread(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); - self.dap_store.update(cx, |store, cx| { - store - .continue_thread(&self.client_id, self.thread_id, cx) - .detach_and_log_err(cx); + let task = self.dap_store.update(cx, |store, cx| { + store.continue_thread(&self.client_id, self.thread_id, cx) }); + + let task = cx.spawn(|weak, mut cx| async move { + if let Err(_) = task.await { + weak.update(&mut cx, |this, cx| { + this.update_thread_state_status(ThreadStatus::Stopped, cx); + }) + .log_err(); + } + }); + + cx.background_executor().spawn(task).detach(); } pub fn step_over(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); - let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - self.dap_store.update(cx, |store, cx| { - store - .step_over(&self.client_id, self.thread_id, granularity, cx) - .detach_and_log_err(cx); + let task = self.dap_store.update(cx, |store, cx| { + store.step_over(&self.client_id, self.thread_id, granularity, cx) }); + + let task = cx.spawn(|weak, mut cx| async move { + if let Err(_) = task.await { + weak.update(&mut cx, |this, cx| { + this.update_thread_state_status(ThreadStatus::Stopped, cx); + }) + .log_err(); + } + }); + + cx.background_executor().spawn(task).detach(); } pub fn step_in(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); - let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - self.dap_store.update(cx, |store, cx| { - store - .step_in(&self.client_id, self.thread_id, granularity, cx) - .detach_and_log_err(cx); + let task = self.dap_store.update(cx, |store, cx| { + store.step_in(&self.client_id, self.thread_id, granularity, cx) }); + + let task = cx.spawn(|weak, mut cx| async move { + if let Err(_) = task.await { + weak.update(&mut cx, |this, cx| { + this.update_thread_state_status(ThreadStatus::Stopped, cx); + }) + .log_err(); + } + }); + + cx.background_executor().spawn(task).detach(); } pub fn step_out(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); - let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - self.dap_store.update(cx, |store, cx| { - store - .step_out(&self.client_id, self.thread_id, granularity, cx) - .detach_and_log_err(cx); + let task = self.dap_store.update(cx, |store, cx| { + store.step_out(&self.client_id, self.thread_id, granularity, cx) + }); + + let task = cx.spawn(|weak, mut cx| async move { + if let Err(_) = task.await { + weak.update(&mut cx, |this, cx| { + this.update_thread_state_status(ThreadStatus::Stopped, cx); + }) + .log_err(); + } }); + + cx.background_executor().spawn(task).detach(); } pub fn step_back(&mut self, cx: &mut ViewContext) { self.update_thread_state_status(ThreadStatus::Running, cx); - let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - self.dap_store.update(cx, |store, cx| { - store - .step_back(&self.client_id, self.thread_id, granularity, cx) - .detach_and_log_err(cx); + let task = self.dap_store.update(cx, |store, cx| { + store.step_back(&self.client_id, self.thread_id, granularity, cx) }); + + let task = cx.spawn(|weak, mut cx| async move { + if let Err(_) = task.await { + weak.update(&mut cx, |this, cx| { + this.update_thread_state_status(ThreadStatus::Stopped, cx); + }) + .log_err(); + } + }); + + cx.background_executor().spawn(task).detach(); } pub fn restart_client(&self, cx: &mut ViewContext) { diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index ae7c2a63edeb1d..fd5ca5b740e3d4 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1,8 +1,11 @@ use crate::*; use dap::{ client::DebugAdapterClientId, - requests::{Disconnect, Initialize, Launch, RunInTerminal, StackTrace, StartDebugging}, - RunInTerminalRequestArguments, StartDebuggingRequestArguments, + requests::{ + Continue, Disconnect, Initialize, Launch, Next, RunInTerminal, StackTrace, StartDebugging, + StepBack, StepIn, StepOut, + }, + ErrorResponse, RunInTerminalRequestArguments, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, }; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; @@ -913,3 +916,148 @@ async fn test_handle_start_debugging_reverse_request( shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_debug_panel_item_thread_status_reset_on_failure( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client + .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .await; + + client + .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .await; + + client + .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .await; + + client + .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .await; + + client + .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + cx.run_until_parked(); + + let debug_panel_item = workspace + .update(cx, |workspace, cx| { + workspace + .panel::(cx) + .unwrap() + .update(cx, |panel, cx| panel.active_debug_panel_item(cx)) + .unwrap() + }) + .unwrap(); + + for operation in &[ + "step_over", + "continue_thread", + "step_back", + "step_in", + "step_out", + ] { + debug_panel_item.update(cx, |item, cx| match *operation { + "step_over" => item.step_over(cx), + "continue_thread" => item.continue_thread(cx), + "step_back" => item.step_back(cx), + "step_in" => item.step_in(cx), + "step_out" => item.step_out(cx), + _ => unreachable!(), + }); + + cx.run_until_parked(); + + debug_panel_item.update(cx, |item, cx| { + assert_eq!( + item.thread_status(cx), + debugger_panel::ThreadStatus::Stopped, + "Thread status not reset to Stopped after failed {}", + operation + ); + }); + } + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); + + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }) + .unwrap(); +} From 14a2f7526b1dafa295204f899a2f629a49d5fad7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 11 Jan 2025 15:47:41 +0100 Subject: [PATCH 453/650] Variable list keyboard navigation (#84) * WIP * Add assert helper for variable list visual entries * Wip rework toggle entry (scope/variable) code * Remove commented code * Move colors to struct * Add entry to selection if you click on them * Add selected option to visual entries assert * Use pretty assertions for visual entries assert helper * Use focus handle method to give focus handle * Register select first and last actions * Correctly format selected entry * Add tests helper to get active debug panel item * Add tests for keyboard navigation * Remove not needed comment * Move other tests to test helper This also removes a test that is duplicated with the keyboard navigation tests, it covers the same * Update console test to use new test helper * Fix failing test I forgot to update the test, because we now always send a body back in a error case. * Fix clippyyyy --- Cargo.lock | 1 + assets/keymaps/default-macos.json | 8 + crates/debugger_ui/Cargo.toml | 3 +- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/tests.rs | 18 +- crates/debugger_ui/src/tests/console.rs | 284 +++-- .../debugger_ui/src/tests/debugger_panel.rs | 2 +- crates/debugger_ui/src/tests/variable_list.rs | 971 ++++++++---------- crates/debugger_ui/src/variable_list.rs | 492 +++++++-- crates/proto/proto/zed.proto | 5 +- 10 files changed, 989 insertions(+), 797 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62819fea3e7e83..763e9d0d157054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3651,6 +3651,7 @@ dependencies = [ "language", "menu", "picker", + "pretty_assertions", "project", "rpc", "serde", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ac4b27f2b02b8a..655063e5504653 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -682,6 +682,14 @@ "space": "project_panel::Open" } }, + { + "context": "VariableList", + "use_key_equivalents": true, + "bindings": { + "left": "variable_list::CollapseSelectedEntry", + "right": "variable_list::ExpandSelectedEntry" + } + }, { "context": "CollabPanel && not_editing", "use_key_equivalents": true, diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 5634651b602f39..d3ad8d5827fbd2 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -31,6 +31,7 @@ gpui.workspace = true language.workspace = true menu.workspace = true picker.workspace = true +pretty_assertions.workspace = true project.workspace = true rpc.workspace = true serde.workspace = true @@ -52,6 +53,6 @@ editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true gpui = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } unindent.workspace = true +util = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 16d9b186036257..a6b63b58f24c55 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -863,7 +863,7 @@ impl Render for DebugPanelItem { h_flex() .key_context("DebugPanelItem") - .track_focus(&self.focus_handle) + .track_focus(&self.focus_handle(cx)) .size_full() .items_start() .child( diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 2561fbf0b484bb..2ab4e9740da399 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -1,10 +1,10 @@ -use gpui::{Model, TestAppContext, WindowHandle}; +use gpui::{Model, TestAppContext, View, WindowHandle}; use project::Project; use settings::SettingsStore; use terminal_view::terminal_panel::TerminalPanel; use workspace::Workspace; -use crate::debugger_panel::DebugPanel; +use crate::{debugger_panel::DebugPanel, debugger_panel_item::DebugPanelItem}; mod attach_modal; mod console; @@ -57,3 +57,17 @@ pub async fn init_test_workspace( .unwrap(); window } + +pub fn active_debug_panel_item( + workspace: WindowHandle, + cx: &mut TestAppContext, +) -> View { + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap() + }) + .unwrap() +} diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 510b531234a068..0dd5a00c272f30 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -10,7 +10,7 @@ use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }; -use tests::{init_test, init_test_workspace}; +use tests::{active_debug_panel_item, init_test, init_test_workspace}; use unindent::Unindent as _; use variable_list::{VariableContainer, VariableListEntry}; @@ -281,172 +281,140 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp cx.run_until_parked(); // toggle nested variables for scope 1 - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - let scope1_variables = scope1_variables.lock().unwrap().clone(); - - debug_panel_item - .variable_list() - .update(cx, |variable_list, cx| { - variable_list.on_toggle_variable( - scopes[0].variables_reference, // scope id - &crate::variable_list::OpenEntry::Variable { - name: scope1_variables[0].name.clone(), - depth: 1, - }, - scope1_variables[0].variables_reference, - 1, // depth - Some(false), - cx, - ); - }); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let scope1_variables = scope1_variables.lock().unwrap().clone(); + + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.toggle_entry( + &variable_list::OpenEntry::Variable { + scope_id: scopes[0].variables_reference, + name: scope1_variables[0].name.clone(), + depth: 1, + }, + cx, + ); }); - }) - .unwrap(); + }); cx.run_until_parked(); - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |item, cx| { - item.console().update(cx, |console, cx| { - console.query_bar().update(cx, |query_bar, cx| { - query_bar.set_text(format!("$variable1 = {}", NEW_VALUE), cx); - }); - - console.evaluate(&menu::Confirm, cx); - }); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item.console().update(cx, |console, cx| { + console.query_bar().update(cx, |query_bar, cx| { + query_bar.set_text(format!("$variable1 = {}", NEW_VALUE), cx); }); - }) - .unwrap(); + + console.evaluate(&menu::Confirm, cx); + }); + }); cx.run_until_parked(); - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - assert_eq!( - "", - active_debug_panel_item - .read(cx) - .console() - .read(cx) - .query_bar() - .read(cx) - .text(cx) - .as_str() - ); - - assert_eq!( - format!("{}\n", NEW_VALUE), - active_debug_panel_item - .read(cx) - .console() - .read(cx) - .editor() - .read(cx) - .text(cx) - .as_str() - ); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - debug_panel_item - .variable_list() - .update(cx, |variable_list, _| { - let scope1_variables = scope1_variables.lock().unwrap().clone(); - - // scope 1 - assert_eq!( - vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[0].clone(), - depth: 1, - }, - VariableContainer { - container_reference: scope1_variables[0].variables_reference, - variable: nested_variables[0].clone(), - depth: 2, - }, - VariableContainer { - container_reference: scope1_variables[0].variables_reference, - variable: nested_variables[1].clone(), - depth: 2, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[1].clone(), - depth: 1, - }, - ], - variable_list.variables_by_scope(1, 2).unwrap().variables() - ); - - // scope 2 - assert_eq!( - vec![VariableContainer { - container_reference: scopes[1].variables_reference, - variable: scope2_variables[0].clone(), - depth: 1, - }], - variable_list.variables_by_scope(1, 4).unwrap().variables() - ); - - // assert visual entries - assert_eq!( - vec![ - VariableListEntry::Scope(scopes[0].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: true, - variable: Arc::new(scope1_variables[0].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 2, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(nested_variables[0].clone()), - container_reference: scope1_variables[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 2, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(nested_variables[1].clone()), - container_reference: scope1_variables[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(scope1_variables[1].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Scope(scopes[1].clone()), - ], - variable_list.entries().get(&1).unwrap().clone() - ); - }); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + assert_eq!( + "", + debug_panel_item + .console() + .read(cx) + .query_bar() + .read(cx) + .text(cx) + .as_str() + ); + + assert_eq!( + format!("{}\n", NEW_VALUE), + debug_panel_item + .console() + .read(cx) + .editor() + .read(cx) + .text(cx) + .as_str() + ); + + debug_panel_item + .variable_list() + .update(cx, |variable_list, _| { + let scope1_variables = scope1_variables.lock().unwrap().clone(); + + // scope 1 + assert_eq!( + vec![ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[0].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scope1_variables[0].variables_reference, + variable: nested_variables[1].clone(), + depth: 2, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: scope1_variables[1].clone(), + depth: 1, + }, + ], + variable_list.variables_by_scope(1, 2).unwrap().variables() + ); + + // scope 2 + assert_eq!( + vec![VariableContainer { + container_reference: scopes[1].variables_reference, + variable: scope2_variables[0].clone(), + depth: 1, + }], + variable_list.variables_by_scope(1, 4).unwrap().variables() + ); + + // assert visual entries + assert_eq!( + vec![ + VariableListEntry::Scope(scopes[0].clone()), + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: true, + variable: Arc::new(scope1_variables[0].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 2, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(nested_variables[0].clone()), + container_reference: scope1_variables[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 2, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(nested_variables[1].clone()), + container_reference: scope1_variables[0].variables_reference, + }, + VariableListEntry::Variable { + depth: 1, + scope: Arc::new(scopes[0].clone()), + has_children: false, + variable: Arc::new(scope1_variables[1].clone()), + container_reference: scopes[0].variables_reference, + }, + VariableListEntry::Scope(scopes[1].clone()), + ], + variable_list.entries().get(&1).unwrap().clone() + ); }); - }) - .unwrap(); + }); assert!( called_evaluate.load(std::sync::atomic::Ordering::SeqCst), diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index fd5ca5b740e3d4..1b3e27c3e3e8a4 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -744,7 +744,7 @@ async fn test_handle_error_run_in_terminal_reverse_request( send_response.store(true, Ordering::SeqCst); assert!(!response.success); - assert!(response.body.is_none()); + assert!(response.body.is_some()); } }) .await; diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index f4bc90244dc1f5..6c75bd76eae20d 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -1,9 +1,8 @@ use std::sync::Arc; use crate::{ - debugger_panel::DebugPanel, - tests::{init_test, init_test_workspace}, - variable_list::{VariableContainer, VariableListEntry}, + tests::{active_debug_panel_item, init_test, init_test_workspace}, + variable_list::{CollapseSelectedEntry, ExpandSelectedEntry, VariableContainer}, }; use collections::HashMap; use dap::{ @@ -11,6 +10,7 @@ use dap::{ Scope, StackFrame, Variable, }; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use menu::{SelectFirst, SelectNext}; use project::{FakeFs, Project}; use serde_json::json; use unindent::Unindent as _; @@ -192,65 +192,39 @@ async fn test_basic_fetch_initial_scope_and_variables( cx.run_until_parked(); - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); - - debug_panel_item - .variable_list() - .update(cx, |variable_list, cx| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - assert_eq!( - vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variables[0].clone(), - depth: 1, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variables[1].clone(), - depth: 1, - }, - ], - variable_list.variables(cx) - ); - - // assert visual entries - assert_eq!( - vec![ - VariableListEntry::Scope(scopes[0].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(variables[0].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(variables[1].clone()), - container_reference: scopes[0].variables_reference, - }, - ], - variable_list.entries().get(&1).unwrap().clone() - ); - }); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!( + vec![ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variables[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variables[1].clone(), + depth: 1, + }, + ], + variable_list.variables(cx) + ); + + variable_list.assert_visual_entries( + vec!["v Scope 1", " > variable1", " > variable2"], + cx, + ); }); - }) - .unwrap(); + }); let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { @@ -473,84 +447,56 @@ async fn test_fetch_variables_for_multiple_scopes( cx.run_until_parked(); - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); - - debug_panel_item - .variable_list() - .update(cx, |variable_list, _| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - - // scope 1 - assert_eq!( - vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variables.get(&2).unwrap()[0].clone(), - depth: 1, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variables.get(&2).unwrap()[1].clone(), - depth: 1, - }, - ], - variable_list.variables_by_scope(1, 2).unwrap().variables() - ); - - // scope 2 - assert_eq!( - vec![VariableContainer { - container_reference: scopes[1].variables_reference, - variable: variables.get(&3).unwrap()[0].clone(), - depth: 1, - }], - variable_list.variables_by_scope(1, 3).unwrap().variables() - ); - - // assert visual entries - assert_eq!( - vec![ - VariableListEntry::Scope(scopes[0].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new( - variables.get(&scopes[0].variables_reference).unwrap()[0] - .clone() - ), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new( - variables.get(&scopes[0].variables_reference).unwrap()[1] - .clone() - ), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Scope(scopes[1].clone()), - ], - variable_list.entries().get(&1).unwrap().clone() - ); - }); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + + // scope 1 + assert_eq!( + vec![ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variables.get(&2).unwrap()[0].clone(), + depth: 1, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variables.get(&2).unwrap()[1].clone(), + depth: 1, + }, + ], + variable_list.variables_by_scope(1, 2).unwrap().variables() + ); + + // scope 2 + assert_eq!( + vec![VariableContainer { + container_reference: scopes[1].variables_reference, + variable: variables.get(&3).unwrap()[0].clone(), + depth: 1, + }], + variable_list.variables_by_scope(1, 3).unwrap().variables() + ); + + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " > variable1", + " > variable2", + "> Scope 2", + ], + cx, + ); }); - }) - .unwrap(); + }); let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { @@ -563,7 +509,7 @@ async fn test_fetch_variables_for_multiple_scopes( // tests that toggling a variable will fetch its children and shows it #[gpui::test] -async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut TestAppContext) { +async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(executor.clone()); @@ -798,418 +744,373 @@ async fn test_toggle_scope_and_variable(executor: BackgroundExecutor, cx: &mut T cx.run_until_parked(); - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - debug_panel_item - .variable_list() - .update(cx, |variable_list, _| { - // scope 1 - assert_eq!( - vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[0].clone(), - depth: 1, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[1].clone(), - depth: 1, - }, - ], - variable_list.variables_by_scope(1, 2).unwrap().variables() - ); - - // scope 2 - assert_eq!( - vec![VariableContainer { - container_reference: scopes[1].variables_reference, - variable: scope2_variables[0].clone(), - depth: 1, - }], - variable_list.variables_by_scope(1, 4).unwrap().variables() - ); - - // assert visual entries - assert_eq!( - vec![ - VariableListEntry::Scope(scopes[0].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: true, - variable: Arc::new(scope1_variables[0].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(scope1_variables[1].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Scope(scopes[1].clone()), - ], - variable_list.entries().get(&1).unwrap().clone() - ); - }); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item.variable_list().focus_handle(cx).focus(cx); + }); + + cx.dispatch_action(SelectFirst); + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1 <=== selected", + " > variable1", + " > variable2", + "> Scope 2", + ], + cx, + ); }); - }) - .unwrap(); - - // toggle nested variables for scope 1 - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - debug_panel_item - .variable_list() - .update(cx, |variable_list, cx| { - variable_list.on_toggle_variable( - scopes[0].variables_reference, // scope id - &crate::variable_list::OpenEntry::Variable { - name: scope1_variables[0].name.clone(), - depth: 1, - }, - scope1_variables[0].variables_reference, - 1, // depth - Some(false), - cx, - ); - }); + }); + + // select the first variable of scope 1 + cx.dispatch_action(SelectNext); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " > variable1 <=== selected", + " > variable2", + "> Scope 2", + ], + cx, + ); }); - }) - .unwrap(); + }); + // expand the nested variables of variable 1 + cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1 <=== selected", + " > nested1", + " > nested2", + " > variable2", + "> Scope 2", + ], + cx, + ); + }); + }); - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - debug_panel_item - .variable_list() - .update(cx, |variable_list, _| { - // scope 1 - assert_eq!( - vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[0].clone(), - depth: 1, - }, - VariableContainer { - container_reference: scope1_variables[0].variables_reference, - variable: nested_variables[0].clone(), - depth: 2, - }, - VariableContainer { - container_reference: scope1_variables[0].variables_reference, - variable: nested_variables[1].clone(), - depth: 2, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[1].clone(), - depth: 1, - }, - ], - variable_list.variables_by_scope(1, 2).unwrap().variables() - ); - - // scope 2 - assert_eq!( - vec![VariableContainer { - container_reference: scopes[1].variables_reference, - variable: scope2_variables[0].clone(), - depth: 1, - }], - variable_list.variables_by_scope(1, 4).unwrap().variables() - ); - - // assert visual entries - assert_eq!( - vec![ - VariableListEntry::Scope(scopes[0].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: true, - variable: Arc::new(scope1_variables[0].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 2, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(nested_variables[0].clone()), - container_reference: scope1_variables[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 2, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(nested_variables[1].clone()), - container_reference: scope1_variables[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(scope1_variables[1].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Scope(scopes[1].clone()), - ], - variable_list.entries().get(&1).unwrap().clone() - ); - }); + // select the first nested variable of variable 1 + cx.dispatch_action(SelectNext); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1 <=== selected", + " > nested2", + " > variable2", + "> Scope 2", + ], + cx, + ); }); - }) - .unwrap(); - - // toggle scope 2 to show variables - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - debug_panel_item - .variable_list() - .update(cx, |variable_list, cx| { - variable_list.toggle_entry( - &crate::variable_list::OpenEntry::Scope { - name: scopes[1].name.clone(), - }, - cx, - ); - }); + }); + + // select the second nested variable of variable 1 + cx.dispatch_action(SelectNext); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2 <=== selected", + " > variable2", + "> Scope 2", + ], + cx, + ); }); - }) - .unwrap(); + }); + // select variable 2 of scope 1 + cx.dispatch_action(SelectNext); cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2", + " > variable2 <=== selected", + "> Scope 2", + ], + cx, + ); + }); + }); - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - debug_panel_item - .variable_list() - .update(cx, |variable_list, _| { - // scope 1 - assert_eq!( - vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[0].clone(), - depth: 1, - }, - VariableContainer { - container_reference: scope1_variables[0].variables_reference, - variable: nested_variables[0].clone(), - depth: 2, - }, - VariableContainer { - container_reference: scope1_variables[0].variables_reference, - variable: nested_variables[1].clone(), - depth: 2, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[1].clone(), - depth: 1, - }, - ], - variable_list.variables_by_scope(1, 2).unwrap().variables() - ); - - // scope 2 - assert_eq!( - vec![VariableContainer { - container_reference: scopes[1].variables_reference, - variable: scope2_variables[0].clone(), - depth: 1, - }], - variable_list.variables_by_scope(1, 4).unwrap().variables() - ); - - // assert visual entries - assert_eq!( - vec![ - VariableListEntry::Scope(scopes[0].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: true, - variable: Arc::new(scope1_variables[0].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 2, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(nested_variables[0].clone()), - container_reference: scope1_variables[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 2, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(nested_variables[1].clone()), - container_reference: scope1_variables[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(scope1_variables[1].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Scope(scopes[1].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[1].clone()), - has_children: false, - variable: Arc::new(scope2_variables[0].clone()), - container_reference: scopes[1].variables_reference, - }, - ], - variable_list.entries().get(&1).unwrap().clone() - ); - }); + // select scope 2 + cx.dispatch_action(SelectNext); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2", + " > variable2", + "> Scope 2 <=== selected", + ], + cx, + ); }); - }) - .unwrap(); - - // toggle variable that has child variables to hide variables - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - debug_panel_item - .variable_list() - .update(cx, |variable_list, cx| { - variable_list.toggle_entry( - &crate::variable_list::OpenEntry::Variable { - name: scope1_variables[0].name.clone(), - depth: 1, - }, - cx, - ); - }); + }); + + // expand the nested variables of scope 2 + cx.dispatch_action(ExpandSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2", + " > variable2", + "v Scope 2 <=== selected", + " > variable3", + ], + cx, + ); }); - }) - .unwrap(); + }); + // select variable 3 of scope 2 + cx.dispatch_action(SelectNext); cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2", + " > variable2", + "v Scope 2", + " > variable3 <=== selected", + ], + cx, + ); + }); + }); - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - active_debug_panel_item.update(cx, |debug_panel_item, cx| { - debug_panel_item - .variable_list() - .update(cx, |variable_list, _| { - // scope 1 - assert_eq!( - vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[0].clone(), - depth: 1, - }, - VariableContainer { - container_reference: scope1_variables[0].variables_reference, - variable: nested_variables[0].clone(), - depth: 2, - }, - VariableContainer { - container_reference: scope1_variables[0].variables_reference, - variable: nested_variables[1].clone(), - depth: 2, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: scope1_variables[1].clone(), - depth: 1, - }, - ], - variable_list.variables_by_scope(1, 2).unwrap().variables() - ); - - // scope 2 - assert_eq!( - vec![VariableContainer { - container_reference: scopes[1].variables_reference, - variable: scope2_variables[0].clone(), - depth: 1, - }], - variable_list.variables_by_scope(1, 4).unwrap().variables() - ); - - // assert visual entries - assert_eq!( - vec![ - VariableListEntry::Scope(scopes[0].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: true, - variable: Arc::new(scope1_variables[0].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[0].clone()), - has_children: false, - variable: Arc::new(scope1_variables[1].clone()), - container_reference: scopes[0].variables_reference, - }, - VariableListEntry::Scope(scopes[1].clone()), - VariableListEntry::Variable { - depth: 1, - scope: Arc::new(scopes[1].clone()), - has_children: false, - variable: Arc::new(scope2_variables[0].clone()), - container_reference: scopes[1].variables_reference, - }, - ], - variable_list.entries().get(&1).unwrap().clone() - ); - }); + // select scope 2 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2", + " > variable2", + "v Scope 2 <=== selected", + " > variable3", + ], + cx, + ); }); - }) - .unwrap(); + }); + + // collapse variables of scope 2 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2", + " > variable2", + "> Scope 2 <=== selected", + ], + cx, + ); + }); + }); + + // select variable 2 of scope 1 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2", + " > variable2 <=== selected", + "> Scope 2", + ], + cx, + ); + }); + }); + + // select nested2 of variable 1 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1", + " > nested2 <=== selected", + " > variable2", + "> Scope 2", + ], + cx, + ); + }); + }); + + // select nested1 of variable 1 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1", + " > nested1 <=== selected", + " > nested2", + " > variable2", + "> Scope 2", + ], + cx, + ); + }); + }); + + // select variable 1 of scope 1 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable1 <=== selected", + " > nested1", + " > nested2", + " > variable2", + "> Scope 2", + ], + cx, + ); + }); + }); + + // collapse variables of variable 1 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " > variable1 <=== selected", + " > variable2", + "> Scope 2", + ], + cx, + ); + }); + }); + + // select scope 1 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list.assert_visual_entries( + vec![ + "v Scope 1 <=== selected", + " > variable1", + " > variable2", + "> Scope 2", + ], + cx, + ); + }); + }); + + // collapse variables of scope 1 + cx.dispatch_action(CollapseSelectedEntry); + cx.run_until_parked(); + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .variable_list() + .update(cx, |variable_list, cx| { + variable_list + .assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"], cx); + }); + }); let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 6444938169fc4b..4f69894262a0e2 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -4,15 +4,13 @@ use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, Scope, ScopePresentationHint, Variable, }; -use editor::{ - actions::{self, SelectAll}, - Editor, EditorEvent, -}; +use editor::{actions::SelectAll, Editor, EditorEvent}; use gpui::{ - anchored, deferred, list, AnyElement, ClipboardItem, DismissEvent, FocusHandle, FocusableView, - ListOffset, ListState, Model, MouseDownEvent, Point, Subscription, Task, View, + actions, anchored, deferred, list, AnyElement, ClipboardItem, DismissEvent, FocusHandle, + FocusableView, Hsla, ListOffset, ListState, Model, MouseDownEvent, Point, Subscription, Task, + View, }; -use menu::Confirm; +use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; use project::dap_store::DapStore; use proto::debugger_variable_list_entry::Entry; use rpc::proto::{ @@ -27,6 +25,8 @@ use sum_tree::{Dimension, Item, SumTree, Summary}; use ui::{prelude::*, ContextMenu, ListItem}; use util::ResultExt; +actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]); + #[derive(Debug, Clone, PartialEq, Eq)] pub struct VariableContainer { pub container_reference: u64, @@ -129,8 +129,14 @@ impl SetVariableState { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum OpenEntry { - Scope { name: String }, - Variable { name: String, depth: usize }, + Scope { + name: String, + }, + Variable { + scope_id: u64, + name: String, + depth: usize, + }, } impl OpenEntry { @@ -142,6 +148,7 @@ impl OpenEntry { proto::variable_list_open_entry::Entry::Variable(state) => Some(Self::Variable { name: state.name.clone(), depth: state.depth as usize, + scope_id: state.scope_id, }), } } @@ -153,10 +160,15 @@ impl OpenEntry { name: name.clone(), }) } - OpenEntry::Variable { name, depth } => { + OpenEntry::Variable { + name, + depth, + scope_id, + } => { proto::variable_list_open_entry::Entry::Variable(proto::DebuggerOpenEntryVariable { name: name.clone(), depth: *depth as u64, + scope_id: *scope_id, }) } }; @@ -364,16 +376,17 @@ pub struct VariableList { list: ListState, focus_handle: FocusHandle, dap_store: Model, + session_id: DebugSessionId, open_entries: Vec, client_id: DebugAdapterClientId, - session_id: DebugSessionId, - scopes: HashMap>, set_variable_editor: View, _subscriptions: Vec, + selection: Option, stack_frame_list: View, + scopes: HashMap>, set_variable_state: Option, - entries: HashMap>, fetch_variables_task: Option>>, + entries: HashMap>, variables: BTreeMap<(StackFrameId, ScopeId), ScopeVariableIndex>, open_context_menu: Option<(View, Point, Subscription)>, } @@ -416,6 +429,7 @@ impl VariableList { dap_store, focus_handle, _subscriptions, + selection: None, set_variable_editor, client_id: *client_id, session_id: *session_id, @@ -593,8 +607,11 @@ impl VariableList { return div().into_any_element(); }; - match &entries[ix] { - VariableListEntry::Scope(scope) => self.render_scope(scope, cx), + let entry = &entries[ix]; + match entry { + VariableListEntry::Scope(scope) => { + self.render_scope(scope, Some(entry) == self.selection.as_ref(), cx) + } VariableListEntry::SetVariableEditor { depth, state } => { self.render_set_variable_editor(*depth, state, cx) } @@ -610,11 +627,72 @@ impl VariableList { scope, *depth, *has_children, + Some(entry) == self.selection.as_ref(), cx, ), } } + fn toggle_variable( + &mut self, + scope_id: u64, + variable: &Variable, + depth: usize, + cx: &mut ViewContext, + ) { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + + let Some(variable_index) = self.variables_by_scope(stack_frame_id, scope_id) else { + return; + }; + + let entry_id = OpenEntry::Variable { + depth, + scope_id, + name: variable.name.clone(), + }; + + let has_children = variable.variables_reference > 0; + let disclosed = has_children.then(|| self.open_entries.binary_search(&entry_id).is_ok()); + + // if we already opened the variable/we already fetched it + // we can just toggle it because we already have the nested variable + if disclosed.unwrap_or(true) || variable_index.fetched(&variable.variables_reference) { + return self.toggle_entry(&entry_id, cx); + } + + let fetch_variables_task = self.dap_store.update(cx, |store, cx| { + store.variables(&self.client_id, variable.variables_reference, cx) + }); + + let container_reference = variable.variables_reference; + let entry_id = entry_id.clone(); + + self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { + let new_variables = fetch_variables_task.await?; + + this.update(&mut cx, |this, cx| { + let Some(index) = this.variables.get_mut(&(stack_frame_id, scope_id)) else { + return; + }; + + index.add_variables( + container_reference, + new_variables + .into_iter() + .map(|variable| VariableContainer { + variable, + depth: depth + 1, + container_reference, + }) + .collect::>(), + ); + + this.toggle_entry(&entry_id, cx); + }) + })) + } + pub fn toggle_entry(&mut self, entry_id: &OpenEntry, cx: &mut ViewContext) { match self.open_entries.binary_search(&entry_id) { Ok(ix) => { @@ -698,8 +776,9 @@ impl VariableList { if self .open_entries .binary_search(&OpenEntry::Variable { - name: variable.name.clone(), depth, + name: variable.name.clone(), + scope_id: scope.variables_reference, }) .is_err() { @@ -792,33 +871,32 @@ impl VariableList { fn fetch_nested_variables( &self, - variables_reference: u64, + container_reference: u64, depth: usize, open_entries: &Vec, cx: &mut ViewContext, ) -> Task>> { + let variables_task = self.dap_store.update(cx, |store, cx| { + store.variables(&self.client_id, container_reference, cx) + }); + cx.spawn({ let open_entries = open_entries.clone(); |this, mut cx| async move { - let variables_task = this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - store.variables(&this.client_id, variables_reference, cx) - }) - })?; - let mut variables = Vec::new(); for variable in variables_task.await? { variables.push(VariableContainer { - variable: variable.clone(), - container_reference: variables_reference, depth, + container_reference, + variable: variable.clone(), }); if open_entries - .binary_search(&&OpenEntry::Variable { - name: variable.name.clone(), + .binary_search(&OpenEntry::Variable { depth, + scope_id: container_reference, + name: variable.name.clone(), }) .is_ok() { @@ -1081,6 +1159,134 @@ impl VariableList { }); } + fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + if let Some(entries) = self.entries.get(&stack_frame_id) { + self.selection = entries.first().cloned(); + cx.notify(); + }; + } + + fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + if let Some(entries) = self.entries.get(&stack_frame_id) { + self.selection = entries.last().cloned(); + cx.notify(); + }; + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(selection) = &self.selection { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + if let Some(entries) = self.entries.get(&stack_frame_id) { + if let Some(ix) = entries.iter().position(|entry| entry == selection) { + self.selection = entries.get(ix.saturating_sub(1)).cloned(); + cx.notify(); + } + } + } else { + self.select_first(&SelectFirst, cx); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(selection) = &self.selection { + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + if let Some(entries) = self.entries.get(&stack_frame_id) { + if let Some(ix) = entries.iter().position(|entry| entry == selection) { + self.selection = entries.get(ix + 1).cloned(); + cx.notify(); + } + } + } else { + self.select_first(&SelectFirst, cx); + } + } + + fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { + if let Some(selection) = &self.selection { + match selection { + VariableListEntry::Scope(scope) => { + let entry_id = &OpenEntry::Scope { + name: scope.name.clone(), + }; + + if self.open_entries.binary_search(entry_id).is_err() { + self.select_prev(&SelectPrev, cx); + } else { + self.toggle_entry(entry_id, cx); + } + } + VariableListEntry::Variable { + depth, + variable, + scope, + .. + } => { + let entry_id = &OpenEntry::Variable { + depth: *depth, + name: variable.name.clone(), + scope_id: scope.variables_reference, + }; + + if self.open_entries.binary_search(entry_id).is_err() { + self.select_prev(&SelectPrev, cx); + } else { + self.toggle_variable( + scope.variables_reference, + &variable.clone(), + *depth, + cx, + ); + } + } + VariableListEntry::SetVariableEditor { .. } => {} + } + } + } + + fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { + if let Some(selection) = &self.selection { + match selection { + VariableListEntry::Scope(scope) => { + let entry_id = &OpenEntry::Scope { + name: scope.name.clone(), + }; + + if self.open_entries.binary_search(entry_id).is_ok() { + self.select_next(&SelectNext, cx); + } else { + self.toggle_entry(entry_id, cx); + } + } + VariableListEntry::Variable { + depth, + variable, + scope, + .. + } => { + let entry_id = &OpenEntry::Variable { + depth: *depth, + name: variable.name.clone(), + scope_id: scope.variables_reference, + }; + + if self.open_entries.binary_search(entry_id).is_ok() { + self.select_next(&SelectNext, cx); + } else { + self.toggle_variable( + scope.variables_reference, + &variable.clone(), + *depth, + cx, + ); + } + } + VariableListEntry::SetVariableEditor { .. } => {} + } + } + } + fn render_set_variable_editor( &self, depth: usize, @@ -1100,105 +1306,142 @@ impl VariableList { .into_any_element() } - #[allow(clippy::too_many_arguments)] - pub fn on_toggle_variable( - &mut self, - scope_id: u64, - entry_id: &OpenEntry, - variable_reference: u64, - depth: usize, - disclosed: Option, - cx: &mut ViewContext, - ) { - let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); - - let Some(index) = self.variables_by_scope(stack_frame_id, scope_id) else { - return; - }; - - // if we already opened the variable/we already fetched it - // we can just toggle it because we already have the nested variable - if disclosed.unwrap_or(true) || index.fetched(&variable_reference) { - return self.toggle_entry(&entry_id, cx); - } - - let fetch_variables_task = self.dap_store.update(cx, |store, cx| { - store.variables(&self.client_id, variable_reference, cx) - }); - - let entry_id = entry_id.clone(); - cx.spawn(|this, mut cx| async move { - let new_variables = fetch_variables_task.await?; - - this.update(&mut cx, |this, cx| { - let Some(index) = this.variables.get_mut(&(stack_frame_id, scope_id)) else { - return; - }; + #[track_caller] + #[cfg(any(test, feature = "test-support"))] + pub fn assert_visual_entries(&self, expected: Vec<&str>, cx: &ViewContext) { + const INDENT: &'static str = " "; - index.add_variables( - variable_reference, - new_variables - .into_iter() - .map(|variable| VariableContainer { - container_reference: variable_reference, - variable, - depth: depth + 1, + let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + let entries = self.entries.get(&stack_frame_id).unwrap(); + + let mut visual_entries = Vec::with_capacity(entries.len()); + for entry in entries { + let is_selected = Some(entry) == self.selection.as_ref(); + + match entry { + VariableListEntry::Scope(scope) => { + let is_expanded = self + .open_entries + .binary_search(&OpenEntry::Scope { + name: scope.name.clone(), }) - .collect::>(), - ); + .is_ok(); + + visual_entries.push(format!( + "{} {}{}", + if is_expanded { "v" } else { ">" }, + scope.name, + if is_selected { " <=== selected" } else { "" } + )); + } + VariableListEntry::SetVariableEditor { depth, state } => { + visual_entries.push(format!( + "{} [EDITOR: {}]{}", + INDENT.repeat(*depth), + state.name, + if is_selected { " <=== selected" } else { "" } + )); + } + VariableListEntry::Variable { + depth, + variable, + scope, + .. + } => { + let is_expanded = self + .open_entries + .binary_search(&OpenEntry::Variable { + depth: *depth, + name: variable.name.clone(), + scope_id: scope.variables_reference, + }) + .is_ok(); + + visual_entries.push(format!( + "{}{} {}{}", + INDENT.repeat(*depth), + if is_expanded { "v" } else { ">" }, + variable.name, + if is_selected { " <=== selected" } else { "" } + )); + } + }; + } - this.toggle_entry(&entry_id, cx); - }) - }) - .detach_and_log_err(cx); + pretty_assertions::assert_eq!(expected, visual_entries); } #[allow(clippy::too_many_arguments)] fn render_variable( &self, - parent_variables_reference: u64, - variable: &Variable, - scope: &Scope, + container_reference: u64, + variable: &Arc, + scope: &Arc, depth: usize, has_children: bool, + is_selected: bool, cx: &mut ViewContext, ) -> AnyElement { let scope_id = scope.variables_reference; - let variable_reference = variable.variables_reference; - let entry_id = OpenEntry::Variable { - name: variable.name.clone(), depth, + scope_id, + name: variable.name.clone(), }; let disclosed = has_children.then(|| self.open_entries.binary_search(&entry_id).is_ok()); + let colors = get_entry_color(cx); + let bg_hover_color = if !is_selected { + colors.hover + } else { + colors.default + }; + let border_color = if is_selected { + colors.marked_active + } else { + colors.default + }; + div() .id(SharedString::from(format!( "variable-{}-{}-{}", scope.variables_reference, variable.name, depth ))) - .group("") + .group("variable_list_entry") + .border_1() + .border_r_2() + .border_color(border_color) .h_4() .size_full() + .hover(|style| style.bg(bg_hover_color)) + .on_click(cx.listener({ + let scope = scope.clone(); + let variable = variable.clone(); + move |this, _, cx| { + this.selection = Some(VariableListEntry::Variable { + depth, + has_children, + container_reference, + scope: scope.clone(), + variable: variable.clone(), + }); + cx.notify(); + } + })) .child( ListItem::new(SharedString::from(format!( "variable-item-{}-{}-{}", scope.variables_reference, variable.name, depth ))) + .selectable(false) .indent_level(depth + 1) .indent_step_size(px(20.)) .always_show_disclosure_icon(true) .toggle(disclosed) .when(has_children, |list_item| { - list_item.on_toggle(cx.listener(move |this, _, cx| { - this.on_toggle_variable( - scope_id, - &entry_id, - variable_reference, - depth, - disclosed, - cx, - ) + list_item.on_toggle(cx.listener({ + let variable = variable.clone(); + move |this, _, cx| this.toggle_variable(scope_id, &variable, depth, cx) })) }) .on_secondary_mouse_down(cx.listener({ @@ -1206,7 +1449,7 @@ impl VariableList { let variable = variable.clone(); move |this, event: &MouseDownEvent, cx| { this.deploy_variable_context_menu( - parent_variables_reference, + container_reference, &scope, &variable, event.position, @@ -1230,7 +1473,12 @@ impl VariableList { .into_any() } - fn render_scope(&self, scope: &Scope, cx: &mut ViewContext) -> AnyElement { + fn render_scope( + &self, + scope: &Scope, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { let element_id = scope.variables_reference; let entry_id = OpenEntry::Scope { @@ -1238,17 +1486,41 @@ impl VariableList { }; let disclosed = self.open_entries.binary_search(&entry_id).is_ok(); + let colors = get_entry_color(cx); + let bg_hover_color = if !is_selected { + colors.hover + } else { + colors.default + }; + let border_color = if is_selected { + colors.marked_active + } else { + colors.default + }; + div() .id(element_id as usize) - .group("") + .group("variable_list_entry") + .border_1() + .border_r_2() + .border_color(border_color) .flex() .w_full() .h_full() + .hover(|style| style.bg(bg_hover_color)) + .on_click(cx.listener({ + let scope = scope.clone(); + move |this, _, cx| { + this.selection = Some(VariableListEntry::Scope(scope.clone())); + cx.notify(); + } + })) .child( ListItem::new(SharedString::from(format!( "scope-{}", scope.variables_reference ))) + .selectable(false) .indent_level(1) .indent_step_size(px(20.)) .always_show_disclosure_icon(true) @@ -1269,10 +1541,20 @@ impl FocusableView for VariableList { impl Render for VariableList { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() + .key_context("VariableList") + .id("variable-list") + .group("variable-list") .size_full() - .on_action( - cx.listener(|this, _: &actions::Cancel, cx| this.cancel_set_variable_value(cx)), - ) + .track_focus(&self.focus_handle(cx)) + .on_action(cx.listener(Self::select_first)) + .on_action(cx.listener(Self::select_last)) + .on_action(cx.listener(Self::select_prev)) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::expand_selected_entry)) + .on_action(cx.listener(Self::collapse_selected_entry)) + .on_action(cx.listener(|this, _: &editor::actions::Cancel, cx| { + this.cancel_set_variable_value(cx) + })) .child(list(self.list.clone()).gap_1_5().size_full()) .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { deferred( @@ -1286,6 +1568,22 @@ impl Render for VariableList { } } +struct EntryColors { + default: Hsla, + hover: Hsla, + marked_active: Hsla, +} + +fn get_entry_color(cx: &ViewContext) -> EntryColors { + let colors = cx.theme().colors(); + + EntryColors { + default: colors.panel_background, + hover: colors.ghost_element_hover, + marked_active: colors.ghost_element_selected, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index d845dc242e774f..d876cb062b7c52 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2510,8 +2510,9 @@ message DebuggerOpenEntryScope { } message DebuggerOpenEntryVariable { - string name = 1; - uint64 depth = 2; + uint64 scope_id = 1; + string name = 2; + uint64 depth = 3; } message DebuggerVariableListEntry { From c91f51dd6865b2f4864dc09e597b15a3f795b504 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 11 Jan 2025 17:20:07 +0100 Subject: [PATCH 454/650] Log errors ins stepping tasks, and update tests I updated the tests so we don't reuse the debug panel item for each operation, instead we request a new instance each time so we can ensure the status actually changed. --- crates/debugger_ui/src/debugger_panel_item.rs | 69 +++++----- .../debugger_ui/src/tests/debugger_panel.rs | 125 ++++++++++++------ 2 files changed, 120 insertions(+), 74 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index a6b63b58f24c55..88424074b0b0c0 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -527,8 +527,8 @@ impl DebugPanelItem { } #[cfg(any(test, feature = "test-support"))] - pub fn thread_status(&self, cx: &ViewContext) -> ThreadStatus { - self.thread_state.read(cx).status + pub fn thread_state(&self) -> &Model { + &self.thread_state } pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { @@ -612,16 +612,15 @@ impl DebugPanelItem { store.continue_thread(&self.client_id, self.thread_id, cx) }); - let task = cx.spawn(|weak, mut cx| async move { - if let Err(_) = task.await { - weak.update(&mut cx, |this, cx| { - this.update_thread_state_status(ThreadStatus::Stopped, cx); + cx.spawn(|this, mut cx| async move { + if task.await.log_err().is_none() { + this.update(&mut cx, |debug_panel_item, cx| { + debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); }) .log_err(); } - }); - - cx.background_executor().spawn(task).detach(); + }) + .detach(); } pub fn step_over(&mut self, cx: &mut ViewContext) { @@ -632,16 +631,15 @@ impl DebugPanelItem { store.step_over(&self.client_id, self.thread_id, granularity, cx) }); - let task = cx.spawn(|weak, mut cx| async move { - if let Err(_) = task.await { - weak.update(&mut cx, |this, cx| { - this.update_thread_state_status(ThreadStatus::Stopped, cx); + cx.spawn(|this, mut cx| async move { + if task.await.log_err().is_none() { + this.update(&mut cx, |debug_panel_item, cx| { + debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); }) .log_err(); } - }); - - cx.background_executor().spawn(task).detach(); + }) + .detach(); } pub fn step_in(&mut self, cx: &mut ViewContext) { @@ -652,16 +650,15 @@ impl DebugPanelItem { store.step_in(&self.client_id, self.thread_id, granularity, cx) }); - let task = cx.spawn(|weak, mut cx| async move { - if let Err(_) = task.await { - weak.update(&mut cx, |this, cx| { - this.update_thread_state_status(ThreadStatus::Stopped, cx); + cx.spawn(|this, mut cx| async move { + if task.await.log_err().is_none() { + this.update(&mut cx, |debug_panel_item, cx| { + debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); }) .log_err(); } - }); - - cx.background_executor().spawn(task).detach(); + }) + .detach(); } pub fn step_out(&mut self, cx: &mut ViewContext) { @@ -672,16 +669,15 @@ impl DebugPanelItem { store.step_out(&self.client_id, self.thread_id, granularity, cx) }); - let task = cx.spawn(|weak, mut cx| async move { - if let Err(_) = task.await { - weak.update(&mut cx, |this, cx| { - this.update_thread_state_status(ThreadStatus::Stopped, cx); + cx.spawn(|this, mut cx| async move { + if task.await.log_err().is_none() { + this.update(&mut cx, |debug_panel_item, cx| { + debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); }) .log_err(); } - }); - - cx.background_executor().spawn(task).detach(); + }) + .detach(); } pub fn step_back(&mut self, cx: &mut ViewContext) { @@ -692,16 +688,15 @@ impl DebugPanelItem { store.step_back(&self.client_id, self.thread_id, granularity, cx) }); - let task = cx.spawn(|weak, mut cx| async move { - if let Err(_) = task.await { - weak.update(&mut cx, |this, cx| { - this.update_thread_state_status(ThreadStatus::Stopped, cx); + cx.spawn(|this, mut cx| async move { + if task.await.log_err().is_none() { + this.update(&mut cx, |debug_panel_item, cx| { + debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); }) .log_err(); } - }); - - cx.background_executor().spawn(task).detach(); + }) + .detach(); } pub fn restart_client(&self, cx: &mut ViewContext) { diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 1b3e27c3e3e8a4..db29446e5e9348 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -8,6 +8,7 @@ use dap::{ ErrorResponse, RunInTerminalRequestArguments, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, }; +use debugger_panel::ThreadStatus; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; use serde_json::json; @@ -16,7 +17,7 @@ use std::sync::{ Arc, }; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; -use tests::{init_test, init_test_workspace}; +use tests::{active_debug_panel_item, init_test, init_test_workspace}; use workspace::dock::Panel; #[gpui::test] @@ -969,23 +970,83 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( .await; client - .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .on_request::(move |_, _| { + Err(ErrorResponse { + error: Some(dap::Message { + id: 1, + format: "error".into(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + }) .await; client - .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .on_request::(move |_, _| { + Err(ErrorResponse { + error: Some(dap::Message { + id: 1, + format: "error".into(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + }) .await; client - .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .on_request::(move |_, _| { + Err(ErrorResponse { + error: Some(dap::Message { + id: 1, + format: "error".into(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + }) .await; client - .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .on_request::(move |_, _| { + Err(ErrorResponse { + error: Some(dap::Message { + id: 1, + format: "error".into(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + }) .await; client - .on_request::(move |_, _| Err(ErrorResponse { error: None })) + .on_request::(move |_, _| { + Err(ErrorResponse { + error: Some(dap::Message { + id: 1, + format: "error".into(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + }) .await; client @@ -1004,16 +1065,6 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( cx.run_until_parked(); - let debug_panel_item = workspace - .update(cx, |workspace, cx| { - workspace - .panel::(cx) - .unwrap() - .update(cx, |panel, cx| panel.active_debug_panel_item(cx)) - .unwrap() - }) - .unwrap(); - for operation in &[ "step_over", "continue_thread", @@ -1021,24 +1072,35 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( "step_in", "step_out", ] { - debug_panel_item.update(cx, |item, cx| match *operation { - "step_over" => item.step_over(cx), - "continue_thread" => item.continue_thread(cx), - "step_back" => item.step_back(cx), - "step_in" => item.step_in(cx), - "step_out" => item.step_out(cx), - _ => unreachable!(), - }); + active_debug_panel_item(workspace, cx).update( + cx, + |debug_panel_item, cx| match *operation { + "step_over" => debug_panel_item.step_over(cx), + "continue_thread" => debug_panel_item.continue_thread(cx), + "step_back" => debug_panel_item.step_back(cx), + "step_in" => debug_panel_item.step_in(cx), + "step_out" => debug_panel_item.step_out(cx), + _ => unreachable!(), + }, + ); cx.run_until_parked(); - debug_panel_item.update(cx, |item, cx| { + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { assert_eq!( - item.thread_status(cx), + debug_panel_item.thread_state().read(cx).status, debugger_panel::ThreadStatus::Stopped, "Thread status not reset to Stopped after failed {}", operation ); + + // update state to running, so we can test it actually changes the status back to stopped + debug_panel_item + .thread_state() + .update(cx, |thread_state, cx| { + thread_state.status = ThreadStatus::Running; + cx.notify(); + }); }); } @@ -1049,15 +1111,4 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( }); shutdown_session.await.unwrap(); - - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |this, cx| { - assert!(this.active_debug_panel_item(cx).is_none()); - assert_eq!(0, this.pane().unwrap().read(cx).items_len()); - }); - }) - .unwrap(); } From 887e2a65e129ba186f2b6f2e0a69caf907ebbddb Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:26:49 -0500 Subject: [PATCH 455/650] Use Toast Notification for Debug Session Warning (#83) * Switch debug session exited without hitting breakpoint to toast notification * Move notify below the thread event, so we register the thread is added --------- Co-authored-by: Remco Smits --- crates/debugger_ui/src/debugger_panel.rs | 57 +++--------------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9ed11f8d2fc1ed..66a553d181f741 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -15,7 +15,7 @@ use dap::{ }; use gpui::{ actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, - FontWeight, Model, Subscription, Task, View, ViewContext, WeakView, + Model, Subscription, Task, View, ViewContext, WeakView, }; use project::{ dap_store::{DapStore, DapStoreEvent}, @@ -117,7 +117,6 @@ pub struct DebugPanel { focus_handle: FocusHandle, dap_store: Model, workspace: WeakView, - show_did_not_stop_warning: bool, _subscriptions: Vec, message_queue: HashMap>, thread_states: BTreeMap<(DebugAdapterClientId, u64), Model>, @@ -209,7 +208,6 @@ impl DebugPanel { size: px(300.), _subscriptions, focus_handle: cx.focus_handle(), - show_did_not_stop_warning: false, thread_states: Default::default(), message_queue: Default::default(), workspace: workspace.weak_handle(), @@ -810,8 +808,11 @@ impl DebugPanel { if let Some(thread_state) = self.thread_states.get(&(*client_id, thread_id)) { if !thread_state.read(cx).stopped && event.reason == ThreadEventReason::Exited { - self.show_did_not_stop_warning = true; - cx.notify(); + const MESSAGE: &'static str = "Debug session exited without hitting breakpoints\n\nTry adding a breakpoint, or define the correct path mapping for your debugger."; + + self.dap_store.update(cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(MESSAGE.into())); + }); }; } @@ -823,6 +824,7 @@ impl DebugPanel { } cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); + cx.notify(); } fn handle_exited_event( @@ -1034,48 +1036,6 @@ impl DebugPanel { cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id)); } - - fn render_did_not_stop_warning(&self, cx: &mut ViewContext) -> impl IntoElement { - const TITLE: &'static str = "Debug session exited without hitting any breakpoints"; - const DESCRIPTION: &'static str = - "Try adding a breakpoint, or define the correct path mapping for your debugger."; - - div() - .absolute() - .right_3() - .bottom_12() - .max_w_96() - .py_2() - .px_3() - .elevation_2(cx) - .occlude() - .child( - v_flex() - .gap_0p5() - .child( - h_flex() - .gap_1p5() - .items_center() - .child(Icon::new(IconName::Warning).color(Color::Conflict)) - .child(Label::new(TITLE).weight(FontWeight::MEDIUM)), - ) - .child( - Label::new(DESCRIPTION) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - h_flex().justify_end().mt_1().child( - Button::new("dismiss", "Dismiss") - .color(Color::Muted) - .on_click(cx.listener(|this, _, cx| { - this.show_did_not_stop_warning = false; - cx.notify(); - })), - ), - ), - ) - } } impl EventEmitter for DebugPanel {} @@ -1146,9 +1106,6 @@ impl Render for DebugPanel { .key_context("DebugPanel") .track_focus(&self.focus_handle) .size_full() - .when(self.show_did_not_stop_warning, |this| { - this.child(self.render_did_not_stop_warning(cx)) - }) .map(|this| { if self.pane.read(cx).items_len() == 0 { this.child( From 943609f1ac93b6b29e86b396a8eb4c3abf4d12dc Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 11 Jan 2025 18:46:00 +0100 Subject: [PATCH 456/650] Fix failing editor test --- crates/editor/src/editor_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 568ed027996d51..e4e16f8c4bdfeb 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10253,6 +10253,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { cx.update_editor(|editor, cx| { editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) }); + cx.run_until_parked(); cx.assert_editor_state(after); }; From 4e13a00c44a4742d0d0546d545fe5ff55a69cd0d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 11 Jan 2025 19:02:32 +0100 Subject: [PATCH 457/650] Add missing action handler for OpenDebugTasks --- crates/zed/src/zed.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ae19ca63efba63..359e63abbc0133 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -602,6 +602,15 @@ fn register_actions( ); }, ) + .register_action( + move |_: &mut Workspace, _: &OpenDebugTasks, cx: &mut ViewContext| { + open_settings_file( + paths::debug_tasks_file(), + || settings::initial_debug_tasks_content().as_ref().into(), + cx, + ); + }, + ) .register_action(open_project_settings_file) .register_action(open_project_tasks_file) .register_action(open_project_debug_tasks_file) From 4baa7f77422c720e096affa346362023d8085d7a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 12 Jan 2025 15:47:43 +0100 Subject: [PATCH 458/650] Don't show Telemetry data in output console --- crates/debugger_ui/src/debugger_panel_item.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 88424074b0b0c0..63f1b524237fdf 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -346,13 +346,16 @@ impl DebugPanelItem { return; } - // The default value of an event category is console - // so we assume that is the output type if it doesn't exist let output_category = event .category .as_ref() .unwrap_or(&OutputEventCategory::Console); + // skip telementry output as it pollutes the users output view + if output_category == &OutputEventCategory::Telemetry { + return; + } + match output_category { OutputEventCategory::Console => { self.console.update(cx, |console, cx| { From 5aa816e85b3990ee33302dbffccba1930f029b30 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 12 Jan 2025 15:26:25 -0500 Subject: [PATCH 459/650] Fix telemetry spelling error --- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 63f1b524237fdf..a81b0f5ddba508 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -351,7 +351,7 @@ impl DebugPanelItem { .as_ref() .unwrap_or(&OutputEventCategory::Console); - // skip telementry output as it pollutes the users output view + // skip telemetry output as it pollutes the users output view if output_category == &OutputEventCategory::Telemetry { return; } From 2736b2f477276756f40af1b495eb683143aa4c97 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Sun, 12 Jan 2025 23:42:51 -0500 Subject: [PATCH 460/650] Update rust-embed version to fix cicd build error (#87) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1a3b8e788810f7..b05e181e3d0ce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -450,7 +450,7 @@ runtimelib = { version = "0.25.0", default-features = false, features = [ "async-dispatcher-runtime", ] } rustc-demangle = "0.1.23" -rust-embed = { version = "8.4", features = ["include-exclude"] } +rust-embed = { version = "8.5", features = ["include-exclude"] } rustc-hash = "2.1.0" rustls = "0.21.12" rustls-native-certs = "0.8.0" From f9f28107f5edb3a93450dc823ad36b5cd2900bb2 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 13 Jan 2025 22:59:28 -0500 Subject: [PATCH 461/650] Add launch delay for gdb debugger --- crates/project/src/dap_store.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index f297a0e2628642..c49544dc053f37 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -706,7 +706,22 @@ impl DapStore { merge_json_value_into(args, &mut adapter_args); } + // TODO(debugger): GDB starts the debuggee program on launch instead of configurationDone + // causing our sent breakpoints to not be valid. This delay should eventually be taken out + let delay = if &client.adapter_id() == "gdb" { + Some( + cx.background_executor() + .timer(std::time::Duration::from_millis(20u64)), + ) + } else { + None + }; + cx.background_executor().spawn(async move { + if let Some(delay) = delay { + delay.await; + } + client .request::(LaunchRequestArguments { raw: adapter_args }) .await From a7e26bbfb529a2b6899eebf53a9d982cec31d6b8 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 14 Jan 2025 01:08:31 -0500 Subject: [PATCH 462/650] Proxy dap requests to upstream clients (#77) * WIP Start work to send all dap client requests with request_dap * Continue work on converting dap client requests * WIP setup dap command for proto dap requests * WIP dap command Co-authored-by: Remco Smits * revert "WIP dap command" This reverts commit fd2a6832b667aa23caf588c3ab55243319bc1654. Co-authored-by: Remco Smits * More WIP with Dap Command trait Co-authored-by: Remco Smits * Get step over command to work with remote dap clients Co-authored-by: Remco Smits * Fix thread status not being set to stop on remote debug panel items * Create a inner wrapper type to use for dap remote step requests * Implement step in,back,out for remote debugger sessions * Add Continue Command * Add more dap command impls TerminateThreads, Pause, and Disconnect. As well as a shutdown session request downstream clients can send to host * Add Disconnect & Terminate dap command impls * Add basic dap proxy test over collab * Fix clippy error * Start work on syncing breakpoint thread status Co-authored-by: Remco Smits Co-authored-by: Carter Canedy * WIP Fix thread status not syncing * Add thread state model's to remote debug panels when setting panel items * Sync thread state on step out command --------- Co-authored-by: Remco Smits Co-authored-by: Carter Canedy --- crates/collab/src/rpc.rs | 16 +- crates/collab/src/tests/debug_panel_tests.rs | 376 +++++++- crates/dap/src/proto_conversions.rs | 21 + crates/debugger_ui/src/debugger_panel.rs | 23 + crates/debugger_ui/src/debugger_panel_item.rs | 1 + crates/project/src/dap_command.rs | 800 ++++++++++++++++++ crates/project/src/dap_store.rs | 357 +++++--- crates/project/src/project.rs | 1 + crates/proto/proto/zed.proto | 112 ++- crates/proto/src/proto.rs | 204 +++-- 10 files changed, 1679 insertions(+), 232 deletions(-) create mode 100644 crates/project/src/dap_command.rs diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 14ef0c31821a47..357cbb0489c568 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -420,7 +420,21 @@ impl Server { .add_message_handler(set_debug_client_panel_item) .add_message_handler(update_debug_adapter) .add_message_handler(update_debug_client_capabilities) - .add_message_handler(shutdown_debug_client); + .add_message_handler(shutdown_debug_client) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler( + forward_mutating_project_request::, + ) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_message_handler(broadcast_project_message_from_host::); Arc::new(server) } diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 20d1b3317cceef..c1d1425f2d6946 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -150,18 +150,6 @@ async fn test_debug_panel_item_opens_on_remote( }); shutdown_client.await.unwrap(); - - cx_b.run_until_parked(); - - // assert we don't have a debug panel item anymore because the client shutdown - workspace_b.update(cx_b, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |this, cx| { - assert!(this.active_debug_panel_item(cx).is_none()); - assert_eq!(0, this.pane().unwrap().read(cx).items_len()); - }); - }); } #[gpui::test] @@ -299,3 +287,367 @@ async fn test_active_debug_panel_item_set_on_join_project( }); }); } + +#[gpui::test] +async fn test_debug_panel_remote_button_presses( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + init_test(cx_a); + init_test(cx_b); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (_, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + client + .on_request::(move |_, _| { + Ok(dap::ContinueResponse { + all_threads_continued: Some(true), + }) + }) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let remote_debug_item = workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + let local_debug_item = workspace_a.update(cx_a, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + remote_debug_item.update(cx_b, |this, cx| { + this.continue_thread(cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + local_debug_item.update(cx_a, |debug_panel_item, cx| { + assert_eq!( + debugger_ui::debugger_panel::ThreadStatus::Running, + debug_panel_item.thread_state().read(cx).status, + ); + }); + + remote_debug_item.update(cx_b, |debug_panel_item, cx| { + assert_eq!( + debugger_ui::debugger_panel::ThreadStatus::Running, + debug_panel_item.thread_state().read(cx).status, + ); + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + local_debug_item.update(cx_a, |debug_panel_item, cx| { + assert_eq!( + debugger_ui::debugger_panel::ThreadStatus::Stopped, + debug_panel_item.thread_state().read(cx).status, + ); + }); + + remote_debug_item.update(cx_b, |debug_panel_item, cx| { + assert_eq!( + debugger_ui::debugger_panel::ThreadStatus::Stopped, + debug_panel_item.thread_state().read(cx).status, + ); + }); + + client + .on_request::(move |_, _| { + Ok(dap::ContinueResponse { + all_threads_continued: Some(true), + }) + }) + .await; + + local_debug_item.update(cx_a, |this, cx| { + this.continue_thread(cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + local_debug_item.update(cx_a, |debug_panel_item, cx| { + assert_eq!( + debugger_ui::debugger_panel::ThreadStatus::Running, + debug_panel_item.thread_state().read(cx).status, + ); + }); + + remote_debug_item.update(cx_b, |debug_panel_item, cx| { + assert_eq!( + debugger_ui::debugger_panel::ThreadStatus::Running, + debug_panel_item.thread_state().read(cx).status, + ); + }); + + client + .on_request::(move |_, _| Ok(())) + .await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + remote_debug_item.update(cx_b, |this, cx| { + this.pause_thread(cx); + }); + + cx_b.run_until_parked(); + cx_a.run_until_parked(); + + client + .on_request::(move |_, _| Ok(())) + .await; + + remote_debug_item.update(cx_b, |this, cx| { + this.step_out(cx); + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_b.run_until_parked(); + cx_a.run_until_parked(); + + client + .on_request::(move |_, _| Ok(())) + .await; + + remote_debug_item.update(cx_b, |this, cx| { + this.step_over(cx); + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_b.run_until_parked(); + cx_a.run_until_parked(); + + client + .on_request::(move |_, _| Ok(())) + .await; + + remote_debug_item.update(cx_b, |this, cx| { + this.step_in(cx); + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_b.run_until_parked(); + cx_a.run_until_parked(); + + client + .on_request::(move |_, _| Ok(())) + .await; + + remote_debug_item.update(cx_b, |this, cx| { + this.step_back(cx); + }); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_b.run_until_parked(); + cx_a.run_until_parked(); + + remote_debug_item.update(cx_b, |this, cx| { + this.stop_thread(cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + // assert we don't have a debug panel item anymore because the client shutdown + workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |this, cx| { + assert!(this.active_debug_panel_item(cx).is_none()); + assert_eq!(0, this.pane().unwrap().read(cx).items_len()); + }); + }); +} diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index cdca5a964b918b..2532060e0cc822 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -376,3 +376,24 @@ pub fn capabilities_to_proto( .unwrap_or_default(), } } + +impl ProtoConversion for dap_types::SteppingGranularity { + type ProtoType = proto::SteppingGranularity; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + match self { + dap_types::SteppingGranularity::Statement => proto::SteppingGranularity::Statement, + dap_types::SteppingGranularity::Line => proto::SteppingGranularity::Line, + dap_types::SteppingGranularity::Instruction => proto::SteppingGranularity::Instruction, + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + match payload { + proto::SteppingGranularity::Line => dap_types::SteppingGranularity::Line, + proto::SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction, + proto::SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement, + } + } +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 66a553d181f741..9378a4970ec306 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -901,10 +901,30 @@ impl DebugPanel { project::dap_store::DapStoreEvent::UpdateDebugAdapter(debug_adapter_update) => { self.handle_debug_adapter_update(debug_adapter_update, cx); } + project::dap_store::DapStoreEvent::UpdateThreadStatus(thread_status_update) => { + self.handle_thread_status_update(thread_status_update, cx); + } _ => {} } } + pub(crate) fn handle_thread_status_update( + &mut self, + update: &proto::UpdateThreadStatus, + cx: &mut ViewContext, + ) { + if let Some(thread_state) = self.thread_states.get_mut(&( + DebugAdapterClientId::from_proto(update.client_id), + update.thread_id, + )) { + thread_state.update(cx, |thread_state, _| { + thread_state.status = ThreadStatus::from_proto(update.status()); + }); + + cx.notify(); + } + } + pub(crate) fn handle_debug_adapter_update( &mut self, update: &UpdateDebugAdapter, @@ -975,6 +995,9 @@ impl DebugPanel { }); let debug_panel_item = existing_item.get_or_insert_with(|| { + self.thread_states + .insert((client_id, thread_id), thread_state.clone()); + let debug_panel = cx.view().clone(); let debug_panel_item = self.pane.update(cx, |pane, cx| { let debug_panel_item = cx.new_view(|cx| { diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index a81b0f5ddba508..54dd0f84648415 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -251,6 +251,7 @@ impl DebugPanelItem { }); self.active_thread_item = ThreadItem::from_proto(state.active_thread_item()); + // self.update_thread_state_status(ThreadStatus::Stopped, cx); // This is a band aid fix for thread status not being sent correctly all the time if let Some(stack_frame_list) = state.stack_frame_list.as_ref() { self.stack_frame_list.update(cx, |this, cx| { diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs new file mode 100644 index 00000000000000..79022cdfe3eb6e --- /dev/null +++ b/crates/project/src/dap_command.rs @@ -0,0 +1,800 @@ +use anyhow::{Ok, Result}; +use dap::{ + client::DebugAdapterClientId, + proto_conversions::ProtoConversion, + requests::{Continue, Next}, + ContinueArguments, NextArguments, StepInArguments, StepOutArguments, SteppingGranularity, +}; +use gpui::{AsyncAppContext, WeakModel}; +use rpc::proto; +use util::ResultExt; + +use crate::dap_store::DapStore; + +pub trait DapCommand: 'static + Sized + Send + std::fmt::Debug { + type Response: 'static + Send + std::fmt::Debug; + type DapRequest: 'static + Send + dap::requests::Request; + type ProtoRequest: 'static + Send + proto::RequestMessage; + + fn handle_response( + &self, + _dap_store: WeakModel, + _client_id: &DebugAdapterClientId, + response: Result, + _cx: &mut AsyncAppContext, + ) -> Result { + response + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId; + + fn from_proto(request: &Self::ProtoRequest) -> Self; + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest; + + fn response_to_proto( + debug_client_id: &DebugAdapterClientId, + message: Self::Response, + ) -> ::Response; + + fn response_from_proto( + self, + message: ::Response, + ) -> Result; + + fn to_dap(&self) -> ::Arguments; + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result; +} + +#[derive(Debug)] +pub struct StepCommand { + pub thread_id: u64, + pub granularity: Option, + pub single_thread: Option, +} + +impl StepCommand { + fn from_proto(message: proto::DapNextRequest) -> Self { + const LINE: i32 = proto::SteppingGranularity::Line as i32; + const INSTRUCTION: i32 = proto::SteppingGranularity::Instruction as i32; + + let granularity = message.granularity.map(|granularity| match granularity { + LINE => SteppingGranularity::Line, + INSTRUCTION => SteppingGranularity::Instruction, + _ => SteppingGranularity::Statement, + }); + + Self { + thread_id: message.thread_id, + granularity, + single_thread: message.single_thread, + } + } +} + +#[derive(Debug)] +pub(crate) struct NextCommand { + pub inner: StepCommand, +} + +impl DapCommand for NextCommand { + type Response = ::Response; + type DapRequest = Next; + type ProtoRequest = proto::DapNextRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + inner: StepCommand::from_proto(request.clone()), + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapNextRequest { + proto::DapNextRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), + } + } + + fn to_dap(&self) -> ::Arguments { + NextArguments { + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct StepInCommand { + pub inner: StepCommand, +} + +impl DapCommand for StepInCommand { + type Response = ::Response; + type DapRequest = dap::requests::StepIn; + type ProtoRequest = proto::DapStepInRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + inner: StepCommand::from_proto(proto::DapNextRequest { + project_id: request.project_id, + client_id: request.client_id, + thread_id: request.thread_id, + single_thread: request.single_thread, + granularity: request.granularity, + }), + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapStepInRequest { + proto::DapStepInRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), + target_id: None, + } + } + + fn to_dap(&self) -> ::Arguments { + StepInArguments { + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + target_id: None, + granularity: self.inner.granularity, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct StepOutCommand { + pub inner: StepCommand, +} + +impl DapCommand for StepOutCommand { + type Response = ::Response; + type DapRequest = dap::requests::StepOut; + type ProtoRequest = proto::DapStepOutRequest; + + fn handle_response( + &self, + dap_store: WeakModel, + client_id: &DebugAdapterClientId, + response: Result, + cx: &mut AsyncAppContext, + ) -> Result { + if response.is_ok() { + dap_store + .update(cx, |this, cx| { + if let Some((client, project_id)) = this.downstream_client() { + let thread_message = proto::UpdateThreadStatus { + project_id: *project_id, + client_id: client_id.to_proto(), + thread_id: self.inner.thread_id, + status: proto::DebuggerThreadStatus::Running.into(), + }; + + cx.emit(crate::dap_store::DapStoreEvent::UpdateThreadStatus( + thread_message.clone(), + )); + + client.send(thread_message).log_err(); + } + }) + .log_err(); + } + response + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + inner: StepCommand::from_proto(proto::DapNextRequest { + project_id: request.project_id, + client_id: request.client_id, + thread_id: request.thread_id, + single_thread: request.single_thread, + granularity: request.granularity, + }), + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapStepOutRequest { + proto::DapStepOutRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), + } + } + + fn to_dap(&self) -> ::Arguments { + StepOutArguments { + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct StepBackCommand { + pub inner: StepCommand, +} + +impl DapCommand for StepBackCommand { + type Response = ::Response; + type DapRequest = dap::requests::StepBack; + type ProtoRequest = proto::DapStepBackRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + inner: StepCommand::from_proto(proto::DapNextRequest { + project_id: request.project_id, + client_id: request.client_id, + thread_id: request.thread_id, + single_thread: request.single_thread, + granularity: request.granularity, + }), + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapStepBackRequest { + proto::DapStepBackRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), + } + } + + fn to_dap(&self) -> ::Arguments { + dap::StepBackArguments { + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct ContinueCommand { + pub args: ContinueArguments, +} + +impl DapCommand for ContinueCommand { + type Response = ::Response; + type DapRequest = Continue; + type ProtoRequest = proto::DapContinueRequest; + + fn handle_response( + &self, + dap_store: WeakModel, + client_id: &DebugAdapterClientId, + response: Result, + cx: &mut AsyncAppContext, + ) -> Result { + if response.is_ok() { + dap_store + .update(cx, |this, cx| { + if let Some((client, project_id)) = this.downstream_client() { + let thread_message = proto::UpdateThreadStatus { + project_id: *project_id, + client_id: client_id.to_proto(), + thread_id: self.args.thread_id, + status: proto::DebuggerThreadStatus::Running.into(), + }; + + cx.emit(crate::dap_store::DapStoreEvent::UpdateThreadStatus( + thread_message.clone(), + )); + + client.send(thread_message).log_err(); + } + }) + .log_err(); + } + response + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapContinueRequest { + proto::DapContinueRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.args.thread_id, + single_thread: self.args.single_thread, + } + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + args: ContinueArguments { + thread_id: request.thread_id, + single_thread: request.single_thread, + }, + } + } + + fn to_dap(&self) -> ::Arguments { + self.args.clone() + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message) + } + + fn response_from_proto( + self, + message: ::Response, + ) -> Result { + Ok(Self::Response { + all_threads_continued: message.all_threads_continued, + }) + } + + fn response_to_proto( + debug_client_id: &DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapContinueResponse { + client_id: debug_client_id.to_proto(), + all_threads_continued: message.all_threads_continued, + } + } +} + +#[derive(Debug)] +pub(crate) struct PauseCommand { + pub thread_id: u64, +} + +impl DapCommand for PauseCommand { + type Response = ::Response; + type DapRequest = dap::requests::Pause; + type ProtoRequest = proto::DapPauseRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + thread_id: request.thread_id, + } + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapPauseRequest { + proto::DapPauseRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.thread_id, + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_dap(&self) -> ::Arguments { + dap::PauseArguments { + thread_id: self.thread_id, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct DisconnectCommand { + pub restart: Option, + pub terminate_debuggee: Option, + pub suspend_debuggee: Option, +} + +impl DapCommand for DisconnectCommand { + type Response = ::Response; + type DapRequest = dap::requests::Disconnect; + type ProtoRequest = proto::DapDisconnectRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + restart: request.restart, + terminate_debuggee: request.terminate_debuggee, + suspend_debuggee: request.suspend_debuggee, + } + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapDisconnectRequest { + proto::DapDisconnectRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + restart: self.restart, + terminate_debuggee: self.terminate_debuggee, + suspend_debuggee: self.suspend_debuggee, + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_dap(&self) -> ::Arguments { + dap::DisconnectArguments { + restart: self.restart, + terminate_debuggee: self.terminate_debuggee, + suspend_debuggee: self.suspend_debuggee, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct TerminateThreadsCommand { + pub thread_ids: Option>, +} + +impl DapCommand for TerminateThreadsCommand { + type Response = ::Response; + type DapRequest = dap::requests::TerminateThreads; + type ProtoRequest = proto::DapTerminateThreadsRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + let thread_ids = if request.thread_ids.is_empty() { + None + } else { + Some(request.thread_ids.clone()) + }; + + Self { thread_ids } + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapTerminateThreadsRequest { + proto::DapTerminateThreadsRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_ids: self.thread_ids.clone().unwrap_or_default(), + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_dap(&self) -> ::Arguments { + dap::TerminateThreadsArguments { + thread_ids: self.thread_ids.clone(), + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct TerminateCommand { + pub restart: Option, +} + +impl DapCommand for TerminateCommand { + type Response = ::Response; + type DapRequest = dap::requests::Terminate; + type ProtoRequest = proto::DapTerminateRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + restart: request.restart, + } + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapTerminateRequest { + proto::DapTerminateRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + restart: self.restart, + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_dap(&self) -> ::Arguments { + dap::TerminateArguments { + restart: self.restart, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct RestartCommand { + pub raw: serde_json::Value, +} + +impl DapCommand for RestartCommand { + type Response = ::Response; + type DapRequest = dap::requests::Restart; + type ProtoRequest = proto::DapRestartRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + raw: serde_json::from_slice(&request.raw_args) + .log_err() + .unwrap_or(serde_json::Value::Null), + } + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapRestartRequest { + let raw_args = serde_json::to_vec(&self.raw).log_err().unwrap_or_default(); + + proto::DapRestartRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + raw_args, + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_dap(&self) -> ::Arguments { + dap::RestartArguments { + raw: self.raw.clone(), + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index c49544dc053f37..9fdcbc08442f6d 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,29 +1,36 @@ -use crate::project_settings::ProjectSettings; -use crate::{ProjectEnvironment, ProjectItem as _, ProjectPath}; +use crate::{ + dap_command::{ + ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, + StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, + TerminateThreadsCommand, + }, + project_settings::ProjectSettings, + ProjectEnvironment, ProjectItem as _, ProjectPath, +}; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; -use dap::session::{DebugSession, DebugSessionId}; use dap::{ adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}, client::{DebugAdapterClient, DebugAdapterClientId}, messages::{Message, Response}, requests::{ - Attach, Completions, ConfigurationDone, Continue, Disconnect, Evaluate, Initialize, Launch, - LoadedSources, Modules, Next, Pause, Request as _, Restart, RunInTerminal, Scopes, - SetBreakpoints, SetExpression, SetVariable, StackTrace, StartDebugging, StepBack, StepIn, - StepOut, Terminate, TerminateThreads, Variables, + Attach, Completions, ConfigurationDone, Disconnect, Evaluate, Initialize, Launch, + LoadedSources, Modules, Request as _, RunInTerminal, Scopes, SetBreakpoints, SetExpression, + SetVariable, StackTrace, StartDebugging, Terminate, Variables, }, AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments, LoadedSourcesArguments, Module, - ModulesArguments, NextArguments, PauseArguments, RestartArguments, Scope, ScopesArguments, - SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, - SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, - StartDebuggingRequestArgumentsRequest, StepBackArguments, StepInArguments, StepOutArguments, - SteppingGranularity, TerminateArguments, TerminateThreadsArguments, Variable, - VariablesArguments, + ModulesArguments, Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, + SetVariableArguments, Source, SourceBreakpoint, StackFrame, StackTraceArguments, + StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, SteppingGranularity, + TerminateArguments, Variable, VariablesArguments, +}; +use dap::{ + session::{DebugSession, DebugSessionId}, + ContinueResponse, }; use dap_adapters::build_adapter; use fs::Fs; @@ -37,8 +44,10 @@ use language::{ }; use lsp::LanguageServerName; use node_runtime::NodeRuntime; -use rpc::proto::{SetDebuggerPanelItem, UpdateDebugAdapter}; -use rpc::{proto, AnyProtoClient, TypedEnvelope}; +use rpc::{ + proto::{self, SetDebuggerPanelItem, UpdateDebugAdapter, UpdateThreadStatus}, + AnyProtoClient, TypedEnvelope, +}; use serde_json::Value; use settings::{Settings as _, WorktreeId}; use smol::lock::Mutex; @@ -70,6 +79,7 @@ pub enum DapStoreEvent { ActiveDebugLineChanged, SetDebugPanelItem(SetDebuggerPanelItem), UpdateDebugAdapter(UpdateDebugAdapter), + UpdateThreadStatus(UpdateThreadStatus), } #[allow(clippy::large_enum_variant)] @@ -133,6 +143,19 @@ impl DapStore { client.add_model_message_handler(DapStore::handle_set_debug_panel_item); client.add_model_message_handler(DapStore::handle_synchronize_breakpoints); client.add_model_message_handler(DapStore::handle_update_debug_adapter); + client.add_model_message_handler(DapStore::handle_update_thread_status); + + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_shutdown_session); } pub fn new_local( @@ -1049,31 +1072,65 @@ impl DapStore { client_id: &DebugAdapterClientId, thread_id: u64, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { + let command = ContinueCommand { + args: ContinueArguments { + thread_id, + single_thread: Some(true), + }, + }; + + self.request_dap(client_id, command, cx) + } + + fn request_dap( + &self, + client_id: &DebugAdapterClientId, + request: R, + cx: &mut ModelContext, + ) -> Task> + where + ::Response: 'static, + ::Arguments: 'static, + { + if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { + return self.send_proto_client_request::( + upstream_client, + upstream_project_id, + client_id, + request, + cx, + ); + } + let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; - cx.background_executor().spawn(async move { - client - .request::(ContinueArguments { - thread_id, - single_thread: Some(true), - }) - .await?; + let client_id = *client_id; - Ok(()) - }) + let task = cx.spawn(|this, mut cx| async move { + let args = request.to_dap(); + let response = request.response_from_dap(client.request::(args).await?); + request.handle_response(this, &client_id, response, &mut cx) + }); + + cx.background_executor().spawn(task) } - // TODO Debugger Collab - fn _send_proto_client_request( + fn send_proto_client_request( &self, - _client_id: &DebugAdapterClientId, - _message: Message, - _cx: &mut ModelContext, - ) { - // + upstream_client: AnyProtoClient, + upstream_project_id: u64, + client_id: &DebugAdapterClientId, + request: R, + cx: &mut ModelContext, + ) -> Task> { + let message = request.to_proto(&client_id, upstream_project_id); + cx.background_executor().spawn(async move { + let response = upstream_client.request(message).await?; + request.response_from_proto(response) + }) } pub fn step_over( @@ -1083,10 +1140,6 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - let capabilities = self.capabilities_by_id(client_id); let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests @@ -1095,15 +1148,15 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.background_executor().spawn(async move { - client - .request::(NextArguments { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - }) + let command = NextCommand { + inner: StepCommand { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + }, + }; + + self.request_dap(client_id, command, cx) } pub fn step_in( @@ -1113,10 +1166,6 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - let capabilities = self.capabilities_by_id(client_id); let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests @@ -1125,16 +1174,15 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.background_executor().spawn(async move { - client - .request::(StepInArguments { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - target_id: None, - }) - .await - }) + let command = StepInCommand { + inner: StepCommand { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + }, + }; + + self.request_dap(client_id, command, cx) } pub fn step_out( @@ -1144,10 +1192,6 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - let capabilities = self.capabilities_by_id(client_id); let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests @@ -1156,15 +1200,15 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - cx.background_executor().spawn(async move { - client - .request::(StepOutArguments { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - }) + let command = StepOutCommand { + inner: StepCommand { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + }, + }; + + self.request_dap(client_id, command, cx) } pub fn step_back( @@ -1174,11 +1218,11 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut ModelContext, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - let capabilities = self.capabilities_by_id(client_id); + if !capabilities.supports_step_back.unwrap_or_default() { + return Task::ready(Ok(())); + } + let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests .unwrap_or_default(); @@ -1186,19 +1230,15 @@ impl DapStore { .supports_stepping_granularity .unwrap_or_default(); - if capabilities.supports_step_back.unwrap_or_default() { - cx.background_executor().spawn(async move { - client - .request::(StepBackArguments { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }) - .await - }) - } else { - Task::ready(Ok(())) - } + let command = StepBackCommand { + inner: StepCommand { + thread_id, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests.then(|| true), + }, + }; + + self.request_dap(client_id, command, cx) } pub fn variables( @@ -1328,12 +1368,7 @@ impl DapStore { thread_id: u64, cx: &mut ModelContext, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - - cx.background_executor() - .spawn(async move { client.request::(PauseArguments { thread_id }).await }) + self.request_dap(client_id, PauseCommand { thread_id }, cx) } pub fn terminate_threads( @@ -1343,20 +1378,12 @@ impl DapStore { thread_ids: Option>, cx: &mut ModelContext, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - if self .capabilities_by_id(client_id) .supports_terminate_threads_request .unwrap_or_default() { - cx.background_executor().spawn(async move { - client - .request::(TerminateThreadsArguments { thread_ids }) - .await - }) + self.request_dap(client_id, TerminateThreadsCommand { thread_ids }, cx) } else { self.shutdown_session(session_id, cx) } @@ -1367,19 +1394,13 @@ impl DapStore { client_id: &DebugAdapterClientId, cx: &mut ModelContext, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + let command = DisconnectCommand { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), }; - cx.background_executor().spawn(async move { - client - .request::(DisconnectArguments { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }) - .await - }) + self.request_dap(client_id, command, cx) } pub fn restart( @@ -1388,36 +1409,43 @@ impl DapStore { args: Option, cx: &mut ModelContext, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - let supports_restart = self .capabilities_by_id(client_id) .supports_restart_request .unwrap_or_default(); - let raw = args.unwrap_or(Value::Null); + if supports_restart { + let command = RestartCommand { + raw: args.unwrap_or(Value::Null), + }; - cx.background_executor().spawn(async move { - if supports_restart { - client.request::(RestartArguments { raw }).await?; - } else { - client - .request::(DisconnectArguments { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }) - .await?; - } + self.request_dap(client_id, command, cx) + } else { + let command = DisconnectCommand { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }; - Ok(()) - }) + self.request_dap(client_id, command, cx) + } } pub fn shutdown_sessions(&mut self, cx: &mut ModelContext) -> Task<()> { let Some(local_store) = self.as_local() else { + if let Some((upstream_client, project_id)) = self.upstream_client() { + return cx.background_executor().spawn(async move { + upstream_client + .request(proto::DapShutdownSession { + project_id, + session_id: None, + }) + .await + .log_err(); + + () + }); + } return Task::ready(()); }; @@ -1438,6 +1466,17 @@ impl DapStore { cx: &mut ModelContext, ) -> Task> { let Some(local_store) = self.as_local_mut() else { + if let Some((upstream_client, project_id)) = self.upstream_client() { + let future = upstream_client.request(proto::DapShutdownSession { + project_id, + session_id: Some(session_id.to_proto()), + }); + + return cx + .background_executor() + .spawn(async move { future.await.map(|_| ()) }); + } + return Task::ready(Err(anyhow!("Cannot shutdown session on remote side"))); }; @@ -1563,6 +1602,46 @@ impl DapStore { cx.notify(); } + async fn handle_shutdown_session( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + if let Some(session_id) = envelope.payload.session_id { + this.update(&mut cx, |dap_store, cx| { + dap_store.shutdown_session(&DebugSessionId::from_proto(session_id), cx) + })? + .await?; + } else { + this.update(&mut cx, |dap_store, cx| dap_store.shutdown_sessions(cx))? + .await; + } + + Ok(proto::Ack {}) + } + + async fn handle_dap_command( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<::Response> + where + ::Arguments: Send, + ::Response: Send, + { + let _sender_id = envelope.original_sender_id().unwrap_or_default(); + let client_id = T::client_id_from_proto(&envelope.payload); + + let request = T::from_proto(&envelope.payload); + let response = this + .update(&mut cx, |this, cx| { + this.request_dap::(&client_id, request, cx) + })? + .await?; + + Ok(T::response_to_proto(&client_id, response)) + } + async fn handle_synchronize_breakpoints( this: Model, envelope: TypedEnvelope, @@ -1615,6 +1694,16 @@ impl DapStore { }) } + async fn handle_update_thread_status( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::UpdateThreadStatus(envelope.payload)); + }) + } + async fn handle_set_debug_client_capabilities( this: Model, envelope: TypedEnvelope, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 629e4c059d9fb6..187df244d9d196 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,6 +1,7 @@ pub mod buffer_store; mod color_extractor; pub mod connection_manager; +pub mod dap_command; pub mod dap_store; pub mod debounced_delay; pub mod image_store; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index d876cb062b7c52..a0397314d36c51 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -314,7 +314,21 @@ message Envelope { SetDebuggerPanelItem set_debugger_panel_item = 294; UpdateDebugAdapter update_debug_adapter = 295; ShutdownDebugClient shutdown_debug_client = 296; - SetDebugClientCapabilities set_debug_client_capabilities = 297; // current max + SetDebugClientCapabilities set_debug_client_capabilities = 297; + + DapNextRequest dap_next_request = 298; + DapStepInRequest dap_step_in_request = 299; + DapStepOutRequest dap_step_out_request = 300; + DapStepBackRequest dap_step_back_request = 301; + DapContinueRequest dap_continue_request = 302; + DapContinueResponse dap_continue_response = 303; + DapPauseRequest dap_pause_request = 304; + DapDisconnectRequest dap_disconnect_request = 305; + DapTerminateThreadsRequest dap_terminate_threads_request = 306; + DapTerminateRequest dap_terminate_request = 307; + DapRestartRequest dap_restart_request = 308; + DapShutdownSession dap_shutdown_session = 309; + UpdateThreadStatus update_thread_status = 310; // current max } reserved 87 to 88; @@ -2552,6 +2566,13 @@ message DebuggerThreadState { bool stopped = 2; } +message UpdateThreadStatus { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + DebuggerThreadStatus status = 4; +} + enum DebuggerThreadStatus { Running = 0; Stopped = 1; @@ -2591,6 +2612,95 @@ message DebuggerStackFrameList { repeated DapStackFrame stack_frames = 4; } +enum SteppingGranularity { + Statement = 0; + Line = 1; + Instruction = 2; +} + +message DapPauseRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; +} + +message DapDisconnectRequest { + uint64 project_id = 1; + uint64 client_id = 2; + optional bool restart = 3; + optional bool terminate_debuggee = 4; + optional bool suspend_debuggee = 5; +} + +message DapTerminateThreadsRequest { + uint64 project_id = 1; + uint64 client_id = 2; + repeated uint64 thread_ids = 3; +} + +message DapTerminateRequest { + uint64 project_id = 1; + uint64 client_id = 2; + optional bool restart = 3; +} + +message DapRestartRequest { + uint64 project_id = 1; + uint64 client_id = 2; + bytes raw_args = 3; +} + + +message DapShutdownSession { + uint64 project_id = 1; + optional uint64 session_id = 2; // Shutdown all sessions if this is None +} + +message DapNextRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + optional bool single_thread = 4; + optional SteppingGranularity granularity = 5; +} + +message DapStepInRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + optional uint64 target_id = 4; + optional bool single_thread = 5; + optional SteppingGranularity granularity = 6; +} + +message DapStepOutRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + optional bool single_thread = 4; + optional SteppingGranularity granularity = 5; +} + +message DapStepBackRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + optional bool single_thread = 4; + optional SteppingGranularity granularity = 5; +} + +message DapContinueRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + optional bool single_thread = 4; +} + +message DapContinueResponse { + uint64 client_id = 1; + optional bool all_threads_continued = 2; +} + message DapStackFrame { uint64 id = 1; string name = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index ce607520b1ad3e..5b55bdbb0ec885 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -143,37 +143,65 @@ messages!( (Ack, Foreground), (AckBufferOperation, Background), (AckChannelMessage, Background), + (ActivateToolchain, Foreground), + (ActiveToolchain, Foreground), + (ActiveToolchainResponse, Foreground), (AddNotification, Foreground), (AddProjectCollaborator, Foreground), + (AddWorktree, Foreground), + (AddWorktreeResponse, Foreground), + (AdvertiseContexts, Foreground), (ApplyCodeAction, Background), (ApplyCodeActionResponse, Background), (ApplyCompletionAdditionalEdits, Background), (ApplyCompletionAdditionalEditsResponse, Background), + (BlameBuffer, Foreground), + (BlameBufferResponse, Foreground), (BufferReloaded, Foreground), (BufferSaved, Foreground), (Call, Foreground), (CallCanceled, Foreground), (CancelCall, Foreground), + (CancelLanguageServerWork, Foreground), (ChannelMessageSent, Foreground), (ChannelMessageUpdate, Foreground), + (CloseBuffer, Foreground), (ComputeEmbeddings, Background), (ComputeEmbeddingsResponse, Background), (CopyProjectEntry, Foreground), + (CountLanguageModelTokens, Background), + (CountLanguageModelTokensResponse, Background), (CreateBufferForPeer, Foreground), (CreateChannel, Foreground), (CreateChannelResponse, Foreground), + (CreateContext, Foreground), + (CreateContextResponse, Foreground), (CreateProjectEntry, Foreground), (CreateRoom, Foreground), (CreateRoomResponse, Foreground), + (DapContinueRequest, Background), + (DapContinueResponse, Background), + (DapDisconnectRequest, Background), + (DapNextRequest, Background), + (DapPauseRequest, Background), + (DapRestartRequest, Background), + (DapShutdownSession, Background), + (DapStepBackRequest, Background), + (DapStepInRequest, Background), + (DapStepOutRequest, Background), + (DapTerminateRequest, Background), + (DapTerminateThreadsRequest, Background), (DeclineCall, Foreground), (DeleteChannel, Foreground), (DeleteNotification, Foreground), - (UpdateNotification, Foreground), (DeleteProjectEntry, Foreground), (EndStream, Foreground), (Error, Foreground), (ExpandProjectEntry, Foreground), (ExpandProjectEntryResponse, Foreground), + (FindSearchCandidates, Background), + (FindSearchCandidatesResponse, Background), + (FlushBufferedMessages, Foreground), (Follow, Foreground), (FollowResponse, Foreground), (FormatBuffers, Foreground), @@ -190,16 +218,26 @@ messages!( (GetCodeActionsResponse, Background), (GetCompletions, Background), (GetCompletionsResponse, Background), - (GetDefinition, Background), - (GetDefinitionResponse, Background), (GetDeclaration, Background), (GetDeclarationResponse, Background), + (GetDefinition, Background), + (GetDefinitionResponse, Background), (GetDocumentHighlights, Background), (GetDocumentHighlightsResponse, Background), (GetHover, Background), (GetHoverResponse, Background), + (GetImplementation, Background), + (GetImplementationResponse, Background), + (GetLlmToken, Background), + (GetLlmTokenResponse, Background), (GetNotifications, Foreground), (GetNotificationsResponse, Foreground), + (GetPanicFiles, Background), + (GetPanicFilesResponse, Background), + (GetPathMetadata, Background), + (GetPathMetadataResponse, Background), + (GetPermalinkToLine, Foreground), + (GetPermalinkToLineResponse, Foreground), (GetPrivateUserInfo, Foreground), (GetPrivateUserInfoResponse, Foreground), (GetProjectSymbols, Background), @@ -208,21 +246,21 @@ messages!( (GetReferencesResponse, Background), (GetSignatureHelp, Background), (GetSignatureHelpResponse, Background), + (GetStagedText, Foreground), + (GetStagedTextResponse, Foreground), (GetSupermavenApiKey, Background), (GetSupermavenApiKeyResponse, Background), (GetTypeDefinition, Background), (GetTypeDefinitionResponse, Background), - (GetImplementation, Background), - (GetImplementationResponse, Background), - (GetLlmToken, Background), - (GetLlmTokenResponse, Background), - (GetStagedText, Foreground), - (GetStagedTextResponse, Foreground), (GetUsers, Foreground), + (GitBranches, Background), + (GitBranchesResponse, Background), (Hello, Foreground), + (HideToast, Background), (IncomingCall, Foreground), (InlayHints, Background), (InlayHintsResponse, Background), + (InstallExtension, Background), (InviteChannelMember, Foreground), (JoinChannel, Foreground), (JoinChannelBuffer, Foreground), @@ -233,12 +271,29 @@ messages!( (JoinProjectResponse, Foreground), (JoinRoom, Foreground), (JoinRoomResponse, Foreground), + (LanguageServerLog, Foreground), + (LanguageServerPromptRequest, Foreground), + (LanguageServerPromptResponse, Foreground), (LeaveChannelBuffer, Background), (LeaveChannelChat, Foreground), (LeaveProject, Foreground), (LeaveRoom, Foreground), + (LinkedEditingRange, Background), + (LinkedEditingRangeResponse, Background), + (ListRemoteDirectory, Background), + (ListRemoteDirectoryResponse, Background), + (ListToolchains, Foreground), + (ListToolchainsResponse, Foreground), + (LspExtExpandMacro, Background), + (LspExtExpandMacroResponse, Background), + (LspExtOpenDocs, Background), + (LspExtOpenDocsResponse, Background), + (LspExtSwitchSourceHeader, Background), + (LspExtSwitchSourceHeaderResponse, Background), (MarkNotificationRead, Foreground), (MoveChannel, Foreground), + (MultiLspQuery, Background), + (MultiLspQueryResponse, Background), (OnTypeFormatting, Background), (OnTypeFormattingResponse, Background), (OpenBufferById, Background), @@ -246,27 +301,33 @@ messages!( (OpenBufferForSymbol, Background), (OpenBufferForSymbolResponse, Background), (OpenBufferResponse, Background), + (OpenContext, Foreground), + (OpenContextResponse, Foreground), + (OpenNewBuffer, Foreground), + (OpenServerSettings, Foreground), (PerformRename, Background), (PerformRenameResponse, Background), (Ping, Foreground), (PrepareRename, Background), (PrepareRenameResponse, Background), (ProjectEntryResponse, Foreground), - (CountLanguageModelTokens, Background), - (CountLanguageModelTokensResponse, Background), - (RefreshLlmToken, Background), (RefreshInlayHints, Foreground), + (RefreshLlmToken, Background), + (RegisterBufferWithLanguageServers, Background), (RejoinChannelBuffers, Foreground), (RejoinChannelBuffersResponse, Foreground), + (RejoinRemoteProjects, Foreground), + (RejoinRemoteProjectsResponse, Foreground), (RejoinRoom, Foreground), (RejoinRoomResponse, Foreground), (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), + (RemoveActiveDebugLine, Background), (RemoveChannelMember, Foreground), (RemoveChannelMessage, Foreground), - (UpdateChannelMessage, Foreground), (RemoveContact, Foreground), (RemoveProjectCollaborator, Foreground), + (RemoveWorktree, Foreground), (RenameChannel, Foreground), (RenameChannelResponse, Foreground), (RenameProjectEntry, Foreground), @@ -277,110 +338,62 @@ messages!( (ResolveInlayHintResponse, Background), (RespondToChannelInvite, Foreground), (RespondToContactRequest, Foreground), + (RestartLanguageServers, Foreground), (RoomUpdated, Foreground), (SaveBuffer, Foreground), - (SetChannelMemberRole, Foreground), - (SetChannelVisibility, Foreground), (SendChannelMessage, Background), (SendChannelMessageResponse, Background), + (SetActiveDebugLine, Background), + (SetChannelMemberRole, Foreground), + (SetChannelVisibility, Foreground), + (SetDebugClientCapabilities, Background), + (SetDebuggerPanelItem, Background), + (SetRoomParticipantRole, Foreground), (ShareProject, Foreground), (ShareProjectResponse, Foreground), (ShowContacts, Foreground), + (ShutdownDebugClient, Background), + (ShutdownRemoteServer, Foreground), (StartLanguageServer, Foreground), (SubscribeToChannels, Foreground), + (SyncExtensions, Background), + (SyncExtensionsResponse, Background), + (SynchronizeBreakpoints, Background), (SynchronizeBuffers, Foreground), (SynchronizeBuffersResponse, Foreground), - (TaskContextForLocation, Background), + (SynchronizeContexts, Foreground), + (SynchronizeContextsResponse, Foreground), (TaskContext, Background), + (TaskContextForLocation, Background), (Test, Foreground), + (Toast, Background), (Unfollow, Foreground), (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateChannelBuffer, Foreground), (UpdateChannelBufferCollaborators, Foreground), + (UpdateChannelMessage, Foreground), (UpdateChannels, Foreground), - (UpdateUserChannels, Foreground), (UpdateContacts, Foreground), + (UpdateContext, Foreground), + (UpdateDebugAdapter, Foreground), (UpdateDiagnosticSummary, Foreground), (UpdateDiffBase, Foreground), (UpdateFollowers, Foreground), + (UpdateGitBranch, Background), (UpdateInviteInfo, Foreground), (UpdateLanguageServer, Foreground), - (UpdateDebugAdapter, Foreground), + (UpdateNotification, Foreground), (UpdateParticipantLocation, Foreground), (UpdateProject, Foreground), (UpdateProjectCollaborator, Foreground), + (UpdateThreadStatus, Background), + (UpdateUserChannels, Foreground), (UpdateUserPlan, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeSettings, Foreground), (UsersResponse, Foreground), - (LspExtExpandMacro, Background), - (LspExtExpandMacroResponse, Background), - (LspExtOpenDocs, Background), - (LspExtOpenDocsResponse, Background), - (SetRoomParticipantRole, Foreground), - (BlameBuffer, Foreground), - (BlameBufferResponse, Foreground), - (RejoinRemoteProjects, Foreground), - (RejoinRemoteProjectsResponse, Foreground), - (MultiLspQuery, Background), - (MultiLspQueryResponse, Background), - (ListRemoteDirectory, Background), - (ListRemoteDirectoryResponse, Background), - (OpenNewBuffer, Foreground), - (RestartLanguageServers, Foreground), - (LinkedEditingRange, Background), - (LinkedEditingRangeResponse, Background), - (AdvertiseContexts, Foreground), - (OpenContext, Foreground), - (OpenContextResponse, Foreground), - (CreateContext, Foreground), - (CreateContextResponse, Foreground), - (UpdateContext, Foreground), - (SynchronizeContexts, Foreground), - (SynchronizeContextsResponse, Foreground), - (LspExtSwitchSourceHeader, Background), - (LspExtSwitchSourceHeaderResponse, Background), - (AddWorktree, Foreground), - (AddWorktreeResponse, Foreground), - (FindSearchCandidates, Background), - (FindSearchCandidatesResponse, Background), - (CloseBuffer, Foreground), - (ShutdownRemoteServer, Foreground), - (RemoveWorktree, Foreground), - (LanguageServerLog, Foreground), - (Toast, Background), - (HideToast, Background), - (OpenServerSettings, Foreground), - (GetPermalinkToLine, Foreground), - (GetPermalinkToLineResponse, Foreground), - (FlushBufferedMessages, Foreground), - (LanguageServerPromptRequest, Foreground), - (LanguageServerPromptResponse, Foreground), - (GitBranches, Background), - (GitBranchesResponse, Background), - (UpdateGitBranch, Background), - (ListToolchains, Foreground), - (ListToolchainsResponse, Foreground), - (ActivateToolchain, Foreground), - (ActiveToolchain, Foreground), - (ActiveToolchainResponse, Foreground), - (GetPathMetadata, Background), - (GetPathMetadataResponse, Background), - (GetPanicFiles, Background), - (GetPanicFilesResponse, Background), - (CancelLanguageServerWork, Foreground), - (SyncExtensions, Background), - (SyncExtensionsResponse, Background), - (InstallExtension, Background), - (RegisterBufferWithLanguageServers, Background), - (SynchronizeBreakpoints, Background), - (SetActiveDebugLine, Background), - (RemoveActiveDebugLine, Background), - (SetDebuggerPanelItem, Background), - (ShutdownDebugClient, Background), - (SetDebugClientCapabilities, Background), ); request_messages!( @@ -508,6 +521,17 @@ request_messages!( (SyncExtensions, SyncExtensionsResponse), (InstallExtension, Ack), (RegisterBufferWithLanguageServers, Ack), + (DapNextRequest, Ack), + (DapStepInRequest, Ack), + (DapStepOutRequest, Ack), + (DapStepBackRequest, Ack), + (DapContinueRequest, DapContinueResponse), + (DapPauseRequest, Ack), + (DapDisconnectRequest, Ack), + (DapTerminateThreadsRequest, Ack), + (DapTerminateRequest, Ack), + (DapRestartRequest, Ack), + (DapShutdownSession, Ack), ); entity_messages!( @@ -601,6 +625,18 @@ entity_messages!( SetDebuggerPanelItem, ShutdownDebugClient, SetDebugClientCapabilities, + DapNextRequest, + DapStepInRequest, + DapStepOutRequest, + DapStepBackRequest, + DapContinueRequest, + DapPauseRequest, + DapDisconnectRequest, + DapTerminateThreadsRequest, + DapTerminateRequest, + DapRestartRequest, + DapShutdownSession, + UpdateThreadStatus, ); entity_messages!( From 8ecd5479aca35a5c02900e5ff8797a480ee70d74 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 14 Jan 2025 19:38:17 +0100 Subject: [PATCH 463/650] Debug output grouping (#86) * Remove output editor * Implement output grouping * Remove OutputGroup when we found the end position * Fix make gutter smaller * Render placeholder * Show group end on the same level as group start * Add tests * Add support for collapsed grouped output * Fix crease placeholder is not showing up * Don't trim output multiple times * Update tests * Fix clippy --- crates/debugger_ui/src/console.rs | 141 +++++- crates/debugger_ui/src/debugger_panel_item.rs | 57 +-- crates/debugger_ui/src/tests/console.rs | 440 ++++++++++++++++++ .../debugger_ui/src/tests/debugger_panel.rs | 156 ------- crates/proto/proto/zed.proto | 3 +- 5 files changed, 580 insertions(+), 217 deletions(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 1906a33d6ca39d..456a94e51e0005 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -2,19 +2,31 @@ use crate::{ stack_frame_list::{StackFrameList, StackFrameListEvent}, variable_list::VariableList, }; -use dap::client::DebugAdapterClientId; -use editor::{CompletionProvider, Editor, EditorElement, EditorStyle}; +use dap::{client::DebugAdapterClientId, OutputEvent, OutputEventGroup}; +use editor::{ + display_map::{Crease, CreaseId}, + Anchor, CompletionProvider, Editor, EditorElement, EditorStyle, FoldPlaceholder, +}; use fuzzy::StringMatchCandidate; use gpui::{Model, Render, Subscription, Task, TextStyle, View, ViewContext, WeakView}; use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16}; use menu::Confirm; use project::{dap_store::DapStore, Completion}; use settings::Settings; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; use theme::ThemeSettings; -use ui::prelude::*; +use ui::{prelude::*, ButtonLike, Disclosure, ElevationIndex}; + +pub struct OutputGroup { + pub start: Anchor, + pub collapsed: bool, + pub end: Option, + pub crease_ids: Vec, + pub placeholder: SharedString, +} pub struct Console { + groups: Vec, console: View, query_bar: View, dap_store: Model, @@ -36,7 +48,13 @@ impl Console { let mut editor = Editor::multi_line(cx); editor.move_to_end(&editor::actions::MoveToEnd, cx); editor.set_read_only(true); - editor.set_show_gutter(false, cx); + editor.set_show_gutter(true, cx); + editor.set_show_runnables(false, cx); + editor.set_show_code_actions(false, cx); + editor.set_show_line_numbers(false, cx); + editor.set_show_git_diff_gutter(false, cx); + editor.set_autoindent(false); + editor.set_input_enabled(false); editor.set_use_autoclose(false); editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); @@ -67,6 +85,7 @@ impl Console { variable_list, _subscriptions, client_id: *client_id, + groups: Vec::default(), stack_frame_list: stack_frame_list.clone(), } } @@ -93,15 +112,109 @@ impl Console { } } - pub fn add_message(&mut self, message: &str, cx: &mut ViewContext) { + pub fn add_message(&mut self, event: OutputEvent, cx: &mut ViewContext) { self.console.update(cx, |console, cx| { + let output = event.output.trim_end().to_string(); + + let snapshot = console.buffer().read(cx).snapshot(cx); + + let start = snapshot.anchor_before(snapshot.max_point()); + + let mut indent_size = self + .groups + .iter() + .filter(|group| group.end.is_none()) + .count(); + if Some(OutputEventGroup::End) == event.group { + indent_size = indent_size.saturating_sub(1); + } + + let indent = if indent_size > 0 { + " ".repeat(indent_size) + } else { + "".to_string() + }; + console.set_read_only(false); console.move_to_end(&editor::actions::MoveToEnd, cx); - console.insert(format!("{}\n", message.trim_end()).as_str(), cx); + console.insert(format!("{}{}\n", indent, output).as_str(), cx); console.set_read_only(true); + + let end = snapshot.anchor_before(snapshot.max_point()); + + match event.group { + Some(OutputEventGroup::Start) => { + self.groups.push(OutputGroup { + start, + end: None, + collapsed: false, + placeholder: output.clone().into(), + crease_ids: console.insert_creases( + vec![Self::create_crease(output.into(), start, end)], + cx, + ), + }); + } + Some(OutputEventGroup::StartCollapsed) => { + self.groups.push(OutputGroup { + start, + end: None, + collapsed: true, + placeholder: output.clone().into(), + crease_ids: console.insert_creases( + vec![Self::create_crease(output.into(), start, end)], + cx, + ), + }); + } + Some(OutputEventGroup::End) => { + if let Some(index) = self.groups.iter().rposition(|group| group.end.is_none()) { + let group = self.groups.remove(index); + + console.remove_creases(group.crease_ids.clone(), cx); + + let creases = + vec![Self::create_crease(group.placeholder, group.start, end)]; + console.insert_creases(creases.clone(), cx); + + if group.collapsed { + console.fold_creases(creases, false, cx); + } + } + } + None => {} + } + + cx.notify(); }); } + fn create_crease(placeholder: SharedString, start: Anchor, end: Anchor) -> Crease { + Crease::inline( + start..end, + FoldPlaceholder { + render: Arc::new({ + let placeholder = placeholder.clone(); + move |_id, _range, _cx| { + ButtonLike::new("output-group-placeholder") + .style(ButtonStyle::Transparent) + .layer(ElevationIndex::ElevatedSurface) + .child(Label::new(placeholder.clone()).single_line()) + .into_any_element() + } + }), + ..Default::default() + }, + move |row, is_folded, fold, _cx| { + Disclosure::new(("output-group", row.0 as u64), !is_folded) + .toggle_state(is_folded) + .on_click(move |_event, cx| fold(!is_folded, cx)) + .into_any_element() + }, + move |_id, _range, _cx| gpui::Empty.into_any_element(), + ) + } + pub fn evaluate(&mut self, _: &Confirm, cx: &mut ViewContext) { let expression = self.query_bar.update(cx, |editor, cx| { let expression = editor.text(cx); @@ -125,7 +238,19 @@ impl Console { let response = evaluate_task.await?; this.update(&mut cx, |console, cx| { - console.add_message(&response.result, cx); + console.add_message( + OutputEvent { + category: None, + output: response.result, + group: None, + variables_reference: Some(response.variables_reference), + source: None, + line: None, + column: None, + data: None, + }, + cx, + ); console.variable_list.update(cx, |variable_list, cx| { variable_list.invalidate(cx); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 54dd0f84648415..437a3de8654d86 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -38,7 +38,6 @@ enum ThreadItem { Console, LoadedSource, Modules, - Output, Variables, } @@ -48,7 +47,6 @@ impl ThreadItem { ThreadItem::Console => proto::DebuggerThreadItem::Console, ThreadItem::LoadedSource => proto::DebuggerThreadItem::LoadedSource, ThreadItem::Modules => proto::DebuggerThreadItem::Modules, - ThreadItem::Output => proto::DebuggerThreadItem::Output, ThreadItem::Variables => proto::DebuggerThreadItem::Variables, } } @@ -58,7 +56,6 @@ impl ThreadItem { proto::DebuggerThreadItem::Console => ThreadItem::Console, proto::DebuggerThreadItem::LoadedSource => ThreadItem::LoadedSource, proto::DebuggerThreadItem::Modules => ThreadItem::Modules, - proto::DebuggerThreadItem::Output => ThreadItem::Output, proto::DebuggerThreadItem::Variables => ThreadItem::Variables, } } @@ -72,7 +69,6 @@ pub struct DebugPanelItem { session_name: SharedString, dap_store: Model, session_id: DebugSessionId, - output_editor: View, show_console_indicator: bool, module_list: View, active_thread_item: ThreadItem, @@ -179,20 +175,6 @@ impl DebugPanelItem { ), ]; - let output_editor = cx.new_view(|cx| { - let mut editor = Editor::multi_line(cx); - editor.set_placeholder_text("Debug adapter and script output", cx); - editor.set_read_only(true); - editor.set_show_inline_completions(Some(false), cx); - editor.set_searchable(false); - editor.set_auto_replace_emoji_shortcode(false); - editor.set_show_indent_guides(false, cx); - editor.set_autoindent(false); - editor.set_show_gutter(false, cx); - editor.set_show_line_numbers(false, cx); - editor - }); - Self { console, thread_id, @@ -202,7 +184,6 @@ impl DebugPanelItem { module_list, thread_state, focus_handle, - output_editor, variable_list, _subscriptions, remote_id: None, @@ -347,6 +328,7 @@ impl DebugPanelItem { return; } + // skip telemetry output as it pollutes the users output view let output_category = event .category .as_ref() @@ -357,25 +339,11 @@ impl DebugPanelItem { return; } - match output_category { - OutputEventCategory::Console => { - self.console.update(cx, |console, cx| { - console.add_message(&event.output, cx); - }); - - if !matches!(self.active_thread_item, ThreadItem::Console) { - self.show_console_indicator = true; - } - } - _ => { - self.output_editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.move_to_end(&editor::actions::MoveToEnd, cx); - editor.insert(format!("{}\n", &event.output.trim_end()).as_str(), cx); - editor.set_read_only(true); - }); - } - } + self.console.update(cx, |console, cx| { + console.add_message(event.clone(), cx); + }); + self.show_console_indicator = true; + cx.notify(); } fn handle_module_event( @@ -515,11 +483,6 @@ impl DebugPanelItem { &self.stack_frame_list } - #[cfg(any(test, feature = "test-support"))] - pub fn output_editor(&self) -> &View { - &self.output_editor - } - #[cfg(any(test, feature = "test-support"))] pub fn console(&self) -> &View { &self.console @@ -1046,11 +1009,6 @@ impl Render for DebugPanelItem { &SharedString::from("Console"), ThreadItem::Console, cx, - )) - .child(self.render_entry_button( - &SharedString::from("Output"), - ThreadItem::Output, - cx, )), ) .when(*active_thread_item == ThreadItem::Variables, |this| { @@ -1062,9 +1020,6 @@ impl Render for DebugPanelItem { .when(*active_thread_item == ThreadItem::LoadedSource, |this| { this.size_full().child(self.loaded_source_list.clone()) }) - .when(*active_thread_item == ThreadItem::Output, |this| { - this.child(self.output_editor.clone()) - }) .when(*active_thread_item == ThreadItem::Console, |this| { this.child(self.console.clone()) }), diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 0dd5a00c272f30..8e9cf6912e5a01 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -14,6 +14,446 @@ use tests::{active_debug_panel_item, init_test, init_test_workspace}; use unindent::Unindent as _; use variable_list::{VariableContainer, VariableListEntry}; +#[gpui::test] +async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: None, + output: "First console output line before thread stopped!".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First output line before thread stopped!".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + // assert we have output from before the thread stopped + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!(1, debug_panel.read(cx).message_queue().len()); + + assert_eq!( + "First console output line before thread stopped!\nFirst output line before thread stopped!\n", + active_debug_panel_item.read(cx).console().read(cx).editor().read(cx).text(cx).as_str() + ); + }) + .unwrap(); + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second output line after thread stopped!".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Console), + output: "Second console output line after thread stopped!".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + cx.run_until_parked(); + + // assert we have output from before and after the thread stopped + workspace + .update(cx, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert!(debug_panel.read(cx).message_queue().is_empty()); + + assert_eq!( + "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", + active_debug_panel_item.read(cx).console().read(cx).editor().read(cx).text(cx).as_str() + ); + }) + .unwrap(); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} + +#[gpui::test] +async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: None, + output: "First line".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First group".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::Start), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First item in group 1".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second item in group 1".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second group".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::Start), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First item in group 2".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second item in group 2".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "End group 2".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::End), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Third group".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::StartCollapsed), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First item in group 3".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second item in group 3".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "End group 3".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::End), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Third item in group 1".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second item".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::End), + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item.console().update(cx, |console, cx| { + console.editor().update(cx, |editor, cx| { + pretty_assertions::assert_eq!( + " + First line + First group + First item in group 1 + Second item in group 1 + Second group + First item in group 2 + Second item in group 2 + End group 2 + ⋯ End group 3 + Third item in group 1 + Second item + " + .unindent(), + editor.display_text(cx) + ); + }) + }); + }); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} + #[gpui::test] async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index db29446e5e9348..32fa8168b8d5b6 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -430,162 +430,6 @@ async fn test_client_can_open_multiple_thread_panels( .unwrap(); } -#[gpui::test] -async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(executor.clone()); - - let project = Project::test(fs, [], cx).await; - let workspace = init_test_workspace(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) - }); - - let (session, client) = task.await.unwrap(); - - client - .on_request::(move |_, _| { - Ok(dap::Capabilities { - supports_step_back: Some(false), - ..Default::default() - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - client - .on_request::(move |_, _| { - Ok(dap::StackTraceResponse { - stack_frames: Vec::default(), - total_frames: None, - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - client - .fake_event(dap::messages::Events::Output(dap::OutputEvent { - category: None, - output: "First console output line before thread stopped!".to_string(), - data: None, - variables_reference: None, - source: None, - line: None, - column: None, - group: None, - })) - .await; - - client - .fake_event(dap::messages::Events::Output(dap::OutputEvent { - category: Some(dap::OutputEventCategory::Stdout), - output: "First output line before thread stopped!".to_string(), - data: None, - variables_reference: None, - source: None, - line: None, - column: None, - group: None, - })) - .await; - - client - .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { - reason: dap::StoppedEventReason::Pause, - description: None, - thread_id: Some(1), - preserve_focus_hint: None, - text: None, - all_threads_stopped: None, - hit_breakpoint_ids: None, - })) - .await; - - client - .fake_event(dap::messages::Events::Output(dap::OutputEvent { - category: Some(dap::OutputEventCategory::Stdout), - output: "Second output line after thread stopped!".to_string(), - data: None, - variables_reference: None, - source: None, - line: None, - column: None, - group: None, - })) - .await; - - client - .fake_event(dap::messages::Events::Output(dap::OutputEvent { - category: Some(dap::OutputEventCategory::Console), - output: "Second console output line after thread stopped!".to_string(), - data: None, - variables_reference: None, - source: None, - line: None, - column: None, - group: None, - })) - .await; - - cx.run_until_parked(); - - // assert we have output from before and after the thread stopped - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - assert_eq!(1, debug_panel.read(cx).message_queue().len()); - - assert_eq!( - "First output line before thread stopped!\nSecond output line after thread stopped!\n", - active_debug_panel_item.read(cx).output_editor().read(cx).text(cx).as_str() - ); - - assert_eq!( - "First console output line before thread stopped!\nSecond console output line after thread stopped!\n", - active_debug_panel_item.read(cx).console().read(cx).editor().read(cx).text(cx).as_str() - ); - }) - .unwrap(); - - let shutdown_session = project.update(cx, |project, cx| { - project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) - }) - }); - - shutdown_session.await.unwrap(); - - // assert output queue is empty - workspace - .update(cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - assert!(debug_panel.read(cx).message_queue().is_empty()); - }) - .unwrap(); -} - #[gpui::test] async fn test_handle_successful_run_in_terminal_reverse_request( executor: BackgroundExecutor, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index a0397314d36c51..3f7cca1d62caa4 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2499,8 +2499,7 @@ enum DebuggerThreadItem { Console = 0; LoadedSource = 1; Modules = 2; - Output = 3; - Variables = 4; + Variables = 3; } message DebuggerSetVariableState { From 06c11f97bbfec28f1ff264a1d8157c6fb90652a5 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 14 Jan 2025 20:04:37 +0100 Subject: [PATCH 464/650] Move send request to dap client to background thread --- crates/project/src/dap_command.rs | 24 ++++++++++++------------ crates/project/src/dap_store.rs | 14 +++++++++----- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs index 79022cdfe3eb6e..cab4c3548acf17 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/dap_command.rs @@ -11,7 +11,7 @@ use util::ResultExt; use crate::dap_store::DapStore; -pub trait DapCommand: 'static + Sized + Send + std::fmt::Debug { +pub trait DapCommand: 'static + Sized + Send + std::fmt::Debug + Clone { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; @@ -54,7 +54,7 @@ pub trait DapCommand: 'static + Sized + Send + std::fmt::Debug { ) -> Result; } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct StepCommand { pub thread_id: u64, pub granularity: Option, @@ -80,7 +80,7 @@ impl StepCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct NextCommand { pub inner: StepCommand, } @@ -144,7 +144,7 @@ impl DapCommand for NextCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct StepInCommand { pub inner: StepCommand, } @@ -216,7 +216,7 @@ impl DapCommand for StepInCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct StepOutCommand { pub inner: StepCommand, } @@ -316,7 +316,7 @@ impl DapCommand for StepOutCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct StepBackCommand { pub inner: StepCommand, } @@ -386,7 +386,7 @@ impl DapCommand for StepBackCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct ContinueCommand { pub args: ContinueArguments, } @@ -483,7 +483,7 @@ impl DapCommand for ContinueCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct PauseCommand { pub thread_id: u64, } @@ -543,7 +543,7 @@ impl DapCommand for PauseCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct DisconnectCommand { pub restart: Option, pub terminate_debuggee: Option, @@ -611,7 +611,7 @@ impl DapCommand for DisconnectCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct TerminateThreadsCommand { pub thread_ids: Option>, } @@ -675,7 +675,7 @@ impl DapCommand for TerminateThreadsCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct TerminateCommand { pub restart: Option, } @@ -735,7 +735,7 @@ impl DapCommand for TerminateCommand { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct RestartCommand { pub raw: serde_json::Value, } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 9fdcbc08442f6d..175091a5c94442 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1108,14 +1108,18 @@ impl DapStore { }; let client_id = *client_id; + let request_clone = request.clone(); - let task = cx.spawn(|this, mut cx| async move { - let args = request.to_dap(); - let response = request.response_from_dap(client.request::(args).await?); - request.handle_response(this, &client_id, response, &mut cx) + let request_task = cx.background_executor().spawn(async move { + let args = request_clone.to_dap(); + client.request::(args).await }); - cx.background_executor().spawn(task) + let request_clone = request.clone(); + cx.spawn(|this, mut cx| async move { + let response = request_clone.response_from_dap(request_task.await?); + request_clone.handle_response(this, &client_id, response, &mut cx) + }) } fn send_proto_client_request( From a9d7858f300bf449371f115bbc6263d1bac14899 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 14 Jan 2025 22:30:04 +0100 Subject: [PATCH 465/650] Add restart stack frame (#85) * Add restart stack frame * Add collab support * Add restart frame to proto capabilities * Add collab test --- crates/collab/src/db/tables/debug_clients.rs | 6 + crates/collab/src/rpc.rs | 5 +- crates/collab/src/tests/debug_panel_tests.rs | 178 +++++++++++++++++- crates/dap/src/proto_conversions.rs | 2 + crates/debugger_ui/src/debugger_panel_item.rs | 1 - crates/debugger_ui/src/stack_frame_list.rs | 80 ++++++-- crates/project/src/dap_command.rs | 60 ++++++ crates/project/src/dap_store.rs | 22 ++- crates/proto/proto/zed.proto | 9 +- crates/proto/src/proto.rs | 3 + 10 files changed, 347 insertions(+), 19 deletions(-) diff --git a/crates/collab/src/db/tables/debug_clients.rs b/crates/collab/src/db/tables/debug_clients.rs index e27febacbba831..9c6d48346ace8a 100644 --- a/crates/collab/src/db/tables/debug_clients.rs +++ b/crates/collab/src/db/tables/debug_clients.rs @@ -11,6 +11,7 @@ const SUPPORTS_SINGLE_THREAD_EXECUTION_REQUESTS_BIT: u32 = 4; const SUPPORTS_STEP_BACK_BIT: u32 = 5; const SUPPORTS_STEPPING_GRANULARITY_BIT: u32 = 6; const SUPPORTS_TERMINATE_THREADS_REQUEST_BIT: u32 = 7; +const SUPPORTS_RESTART_FRAME_REQUEST_BIT: u32 = 8; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "debug_clients")] @@ -49,6 +50,9 @@ impl Model { supports_terminate_threads_request: (self.capabilities & (1 << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT)) != 0, + supports_restart_frame_request: (self.capabilities + & (1 << SUPPORTS_RESTART_FRAME_REQUEST_BIT)) + != 0, } } @@ -69,6 +73,8 @@ impl Model { << SUPPORTS_STEPPING_GRANULARITY_BIT; capabilities_bit_mask |= (capabilities.supports_terminate_threads_request as i32) << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT; + capabilities_bit_mask |= (capabilities.supports_restart_frame_request as i32) + << SUPPORTS_RESTART_FRAME_REQUEST_BIT; self.capabilities = capabilities_bit_mask; } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 357cbb0489c568..ef8789586546af 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -434,7 +434,10 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) - .add_message_handler(broadcast_project_message_from_host::); + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler( + broadcast_project_message_from_host::, + ); Arc::new(server) } diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index c1d1425f2d6946..3c5a9e85ef7b3f 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1,7 +1,14 @@ use call::ActiveCall; -use dap::requests::{Disconnect, Initialize, Launch, StackTrace}; +use dap::{ + requests::{Disconnect, Initialize, Launch, RestartFrame, StackTrace}, + StackFrame, +}; use debugger_ui::debugger_panel::DebugPanel; use gpui::{TestAppContext, View, VisualTestContext}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use workspace::{dock::Panel, Workspace}; use super::TestServer; @@ -651,3 +658,172 @@ async fn test_debug_panel_remote_button_presses( }); }); } + +#[gpui::test] +async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + init_test(cx_a); + init_test(cx_b); + + let called_restart_frame = Arc::new(AtomicBool::new(false)); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_restart_frame: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + client + .on_request::({ + let called_restart_frame = called_restart_frame.clone(); + move |_, args| { + assert_eq!(1, args.frame_id); + + called_restart_frame.store(true, Ordering::SeqCst); + + Ok(()) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + // try to restart stack frame 1 from the guest side + workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + active_debug_panel_item.update(cx, |debug_panel_item, cx| { + debug_panel_item + .stack_frame_list() + .update(cx, |stack_frame_list, cx| { + stack_frame_list.restart_stack_frame(1, cx); + }); + }); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_restart_frame.load(std::sync::atomic::Ordering::SeqCst), + "Restart stack frame was not called" + ); + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index 2532060e0cc822..15bf89638d0da9 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -344,6 +344,7 @@ pub fn capabilities_from_proto(payload: &SetDebugClientCapabilities) -> Capabili supports_step_back: Some(payload.supports_step_back), supports_stepping_granularity: Some(payload.supports_stepping_granularity), supports_terminate_threads_request: Some(payload.supports_terminate_threads_request), + supports_restart_frame: Some(payload.supports_restart_frame_request), ..Default::default() } } @@ -374,6 +375,7 @@ pub fn capabilities_to_proto( supports_terminate_threads_request: capabilities .supports_terminate_threads_request .unwrap_or_default(), + supports_restart_frame_request: capabilities.supports_restart_frame.unwrap_or_default(), } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 437a3de8654d86..9a82798521adeb 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -232,7 +232,6 @@ impl DebugPanelItem { }); self.active_thread_item = ThreadItem::from_proto(state.active_thread_item()); - // self.update_thread_state_status(ThreadStatus::Stopped, cx); // This is a band aid fix for thread status not being sent correctly all the time if let Some(stack_frame_list) = state.stack_frame_list.as_ref() { self.stack_frame_list.update(cx, |this, cx| { diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 52e81ab9f822cf..8e53b73fc4b5a0 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -239,6 +239,14 @@ impl StackFrameList { .ok()? } + pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { + self.dap_store.update(cx, |store, cx| { + store + .restart_stack_frame(&self.client_id, stack_frame_id, cx) + .detach_and_log_err(cx); + }); + } + fn render_entry(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { let stack_frame = &self.stack_frames[ix]; @@ -251,8 +259,16 @@ impl StackFrameList { stack_frame.line, ); - v_flex() + let supports_frame_restart = self + .dap_store + .read(cx) + .capabilities_by_id(&self.client_id) + .supports_restart_frame + .unwrap_or_default(); + + h_flex() .rounded_md() + .justify_between() .w_full() .group("") .id(("stack-frame", stack_frame.id)) @@ -271,20 +287,58 @@ impl StackFrameList { .detach_and_log_err(cx); } })) - .hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer()) + .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer()) .child( - h_flex() - .gap_0p5() - .text_ui_sm(cx) - .child(stack_frame.name.clone()) - .child(formatted_path), - ) - .child( - h_flex() - .text_ui_xs(cx) - .text_color(cx.theme().colors().text_muted) - .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), + v_flex() + .child( + h_flex() + .gap_0p5() + .text_ui_sm(cx) + .truncate() + .child(stack_frame.name.clone()) + .child(formatted_path), + ) + .child( + h_flex() + .text_ui_xs(cx) + .truncate() + .text_color(cx.theme().colors().text_muted) + .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), + ), ) + .when(supports_frame_restart, |this| { + this.child( + h_flex() + .id(("restart-stack-frame", stack_frame.id)) + .visible_on_hover("") + .absolute() + .right_2() + .overflow_hidden() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().element_selected) + .bg(cx.theme().colors().element_background) + .hover(|style| { + style + .bg(cx.theme().colors().ghost_element_hover) + .cursor_pointer() + }) + .child( + IconButton::new( + ("restart-stack-frame", stack_frame.id), + IconName::DebugRestart, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener({ + let stack_frame_id = stack_frame.id; + move |this, _, cx| { + this.restart_stack_frame(stack_frame_id, cx); + } + })) + .tooltip(move |cx| Tooltip::text("Restart Stack Frame", cx)), + ), + ) + }) .into_any() } } diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs index cab4c3548acf17..386c2753f59ef4 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/dap_command.rs @@ -798,3 +798,63 @@ impl DapCommand for RestartCommand { Ok(()) } } + +#[derive(Debug, Clone)] +pub(crate) struct RestartStackFrameCommand { + pub stack_frame_id: u64, +} + +impl DapCommand for RestartStackFrameCommand { + type Response = ::Response; + type DapRequest = dap::requests::RestartFrame; + type ProtoRequest = proto::DapRestartStackFrameRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + stack_frame_id: request.stack_frame_id, + } + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapRestartStackFrameRequest { + proto::DapRestartStackFrameRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + stack_frame_id: self.stack_frame_id, + } + } + + fn response_to_proto( + _debug_client_id: &DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} + } + + fn to_dap(&self) -> ::Arguments { + dap::RestartFrameArguments { + frame_id: self.stack_frame_id, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } + + fn response_from_proto( + self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 175091a5c94442..9711f75a957cbb 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1,8 +1,8 @@ use crate::{ dap_command::{ ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, - StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, - TerminateThreadsCommand, + RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, + TerminateCommand, TerminateThreadsCommand, }, project_settings::ProjectSettings, ProjectEnvironment, ProjectItem as _, ProjectPath, @@ -155,6 +155,7 @@ impl DapStore { client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_shutdown_session); } @@ -870,6 +871,23 @@ impl DapStore { }) } + pub fn restart_stack_frame( + &mut self, + client_id: &DebugAdapterClientId, + stack_frame_id: u64, + cx: &mut ModelContext, + ) -> Task> { + if !self + .capabilities_by_id(client_id) + .supports_restart_frame + .unwrap_or_default() + { + return Task::ready(Ok(())); + } + + self.request_dap(client_id, RestartStackFrameCommand { stack_frame_id }, cx) + } + pub fn scopes( &mut self, client_id: &DebugAdapterClientId, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 3f7cca1d62caa4..8511b75f82e899 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -328,7 +328,8 @@ message Envelope { DapTerminateRequest dap_terminate_request = 307; DapRestartRequest dap_restart_request = 308; DapShutdownSession dap_shutdown_session = 309; - UpdateThreadStatus update_thread_status = 310; // current max + UpdateThreadStatus update_thread_status = 310; + DapRestartStackFrameRequest dap_restart_stack_frame_request = 311; // current max } reserved 87 to 88; @@ -2469,6 +2470,7 @@ message SetDebugClientCapabilities { bool supports_step_back = 9; bool supports_stepping_granularity = 10; bool supports_terminate_threads_request = 11; + bool supports_restart_frame_request = 12; } message Breakpoint { @@ -2649,6 +2651,11 @@ message DapRestartRequest { bytes raw_args = 3; } +message DapRestartStackFrameRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 stack_frame_id = 3; +} message DapShutdownSession { uint64 project_id = 1; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 5b55bdbb0ec885..f5985916823564 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -185,6 +185,7 @@ messages!( (DapNextRequest, Background), (DapPauseRequest, Background), (DapRestartRequest, Background), + (DapRestartStackFrameRequest, Background), (DapShutdownSession, Background), (DapStepBackRequest, Background), (DapStepInRequest, Background), @@ -531,6 +532,7 @@ request_messages!( (DapTerminateThreadsRequest, Ack), (DapTerminateRequest, Ack), (DapRestartRequest, Ack), + (DapRestartStackFrameRequest, Ack), (DapShutdownSession, Ack), ); @@ -635,6 +637,7 @@ entity_messages!( DapTerminateThreadsRequest, DapTerminateRequest, DapRestartRequest, + DapRestartStackFrameRequest, DapShutdownSession, UpdateThreadStatus, ); From 41b702807dd2829401aa57ca9cceff72260df345 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 15 Jan 2025 14:13:01 +0100 Subject: [PATCH 466/650] Fix only allow autocompletion for variables that are from the current(first) stackframe So this changes the behavior for providing variables for autocompletion inside the debug console if the adapter does not support autocompletion. Before this change you would get variables based on the selected stack frame. But this is not correct, as you cannot use variables that are not in scope anymore. So changing it to only provide variables for the current(first) stack frame we should provide variables that could always be used for autocompletion and for expressions. --- crates/debugger_ui/src/console.rs | 4 +++- crates/debugger_ui/src/stack_frame_list.rs | 7 +++++++ crates/debugger_ui/src/tests/variable_list.rs | 2 +- crates/debugger_ui/src/variable_list.rs | 15 +++++++++++++-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 456a94e51e0005..85cd80abf7c346 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -406,7 +406,9 @@ impl ConsoleQueryBarCompletionProvider { let mut variables = HashMap::new(); let mut string_matches = Vec::new(); - for variable in console.variable_list.update(cx, |v, cx| v.variables(cx)) { + for variable in console.variable_list.update(cx, |variable_list, cx| { + variable_list.completion_variables(cx) + }) { if let Some(evaluate_name) = &variable.variable.evaluate_name { variables.insert(evaluate_name.clone(), variable.variable.value.clone()); string_matches.push(StringMatchCandidate { diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 8e53b73fc4b5a0..75d6babf4595a0 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -109,6 +109,13 @@ impl StackFrameList { &self.stack_frames } + pub fn first_stack_frame_id(&self) -> u64 { + self.stack_frames + .first() + .map(|stack_frame| stack_frame.id) + .unwrap_or(0) + } + pub fn current_stack_frame_id(&self) -> u64 { self.current_stack_frame_id } diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 6c75bd76eae20d..25a866d6072ef6 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -216,7 +216,7 @@ async fn test_basic_fetch_initial_scope_and_variables( depth: 1, }, ], - variable_list.variables(cx) + variable_list.variables_by_stack_frame_id(1) ); variable_list.assert_visual_entries( diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 4f69894262a0e2..7746004e851d22 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -591,8 +591,19 @@ impl VariableList { self.variables.get(&(stack_frame_id, scope_id)) } - pub fn variables(&self, cx: &mut ViewContext) -> Vec { - let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + #[cfg(any(test, feature = "test-support"))] + pub fn variables_by_stack_frame_id( + &self, + stack_frame_id: StackFrameId, + ) -> Vec { + self.variables + .range((stack_frame_id, u64::MIN)..(stack_frame_id, u64::MAX)) + .flat_map(|(_, containers)| containers.variables.iter().cloned()) + .collect() + } + + pub fn completion_variables(&self, cx: &mut ViewContext) -> Vec { + let stack_frame_id = self.stack_frame_list.read(cx).first_stack_frame_id(); self.variables .range((stack_frame_id, u64::MIN)..(stack_frame_id, u64::MAX)) From 86946506bff68494ee679a9a29fc189b25205b8c Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:40:22 -0500 Subject: [PATCH 467/650] Send synced breakpoints to active DAP servers in collab (#89) * Send changed breakpoints to DAP servers on sync breakpoints handle Co-authored-by: Remco Smits * Add more sync breakpoints test Co-authored-by: Remco Smits --------- Co-authored-by: Remco Smits --- crates/collab/src/tests/debug_panel_tests.rs | 275 ++++++++++++++++++- crates/collab/src/tests/editor_tests.rs | 1 + crates/dap/src/session.rs | 4 + crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/project/src/dap_store.rs | 23 +- crates/project/src/project.rs | 60 ++-- 6 files changed, 321 insertions(+), 44 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 3c5a9e85ef7b3f..1b35d217bf4cf3 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1,13 +1,19 @@ use call::ActiveCall; use dap::{ - requests::{Disconnect, Initialize, Launch, RestartFrame, StackTrace}, - StackFrame, + requests::{Disconnect, Initialize, Launch, RestartFrame, SetBreakpoints, StackTrace}, + SourceBreakpoint, StackFrame, }; use debugger_ui::debugger_panel::DebugPanel; +use editor::Editor; use gpui::{TestAppContext, View, VisualTestContext}; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, +use project::ProjectPath; +use serde_json::json; +use std::{ + path::Path, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, }; use workspace::{dock::Panel, Workspace}; @@ -827,3 +833,262 @@ async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppC shutdown_client.await.unwrap(); } + +#[gpui::test] +async fn test_updated_breakpoints_send_to_dap( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "test.txt": "one\ntwo\nthree\nfour\nfive", + }), + ) + .await; + + init_test(cx_a); + init_test(cx_b); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_path = ProjectPath { + worktree_id, + path: Arc::from(Path::new(&"test.txt")), + }; + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_restart_frame: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!( + vec![SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + }], + args.breakpoints.unwrap() + ); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + // Client B opens an editor. + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor_b.update(cx_b, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + // Client A opens an editor. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert!(args.breakpoints.unwrap().is_empty()); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + // remove the breakpoint that client B added + editor_a.update(cx_a, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called" + ); + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!( + vec![ + SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + }, + SourceBreakpoint { + line: 2, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + } + ], + args.breakpoints.unwrap() + ); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + // Add our own breakpoint now + editor_a.update(cx_a, |editor, cx| { + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor.move_up(&editor::actions::MoveUp, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called" + ); + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index e39be89576c3f2..e1cdcb968b48c4 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2466,6 +2466,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte .unwrap() .downcast::() .unwrap(); + cx_a.run_until_parked(); cx_b.run_until_parked(); diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs index 735adb5bfd3d61..447350bb88477c 100644 --- a/crates/dap/src/session.rs +++ b/crates/dap/src/session.rs @@ -97,4 +97,8 @@ impl DebugSession { pub fn clients(&self) -> impl Iterator> + '_ { self.clients.values().cloned() } + + pub fn client_ids(&self) -> impl Iterator + '_ { + self.clients.keys().cloned() + } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9378a4970ec306..fee94a9326da4e 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -194,7 +194,7 @@ impl DebugPanel { cx.notify(); } project::Event::SetDebugClient(set_debug_client) => { - let _res = this.handle_set_debug_panel_item(set_debug_client, cx); + this.handle_set_debug_panel_item(set_debug_client, cx); } _ => {} } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 9711f75a957cbb..65539492969f6d 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -75,7 +75,7 @@ pub enum DapStoreEvent { message: Message, }, Notification(String), - BreakpointsChanged, + BreakpointsChanged(ProjectPath), ActiveDebugLineChanged, SetDebugPanelItem(SetDebuggerPanelItem), UpdateDebugAdapter(UpdateDebugAdapter), @@ -278,7 +278,7 @@ impl DapStore { pub fn client_by_id( &self, client_id: &DebugAdapterClientId, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Option<(Model, Arc)> { let session = self.session_by_client_id(client_id)?; let client = session.read(cx).client_by_id(client_id)?; @@ -1687,10 +1687,10 @@ impl DapStore { if breakpoints.is_empty() { store.breakpoints.remove(&project_path); } else { - store.breakpoints.insert(project_path, breakpoints); + store.breakpoints.insert(project_path.clone(), breakpoints); } - cx.emit(DapStoreEvent::BreakpointsChanged); + cx.emit(DapStoreEvent::BreakpointsChanged(project_path)); cx.notify(); }) @@ -1797,11 +1797,9 @@ impl DapStore { &mut self, project_path: &ProjectPath, mut breakpoint: Breakpoint, - buffer_path: PathBuf, - buffer_snapshot: BufferSnapshot, edit_action: BreakpointEditAction, cx: &mut ModelContext, - ) -> Task> { + ) { let upstream_client = self.upstream_client(); let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); @@ -1840,9 +1838,8 @@ impl DapStore { self.breakpoints.remove(project_path); } + cx.emit(DapStoreEvent::BreakpointsChanged(project_path.clone())); cx.notify(); - - self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) } pub fn send_breakpoints( @@ -1851,7 +1848,7 @@ impl DapStore { absolute_file_path: Arc, mut breakpoints: Vec, ignore: bool, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); @@ -1889,9 +1886,9 @@ impl DapStore { pub fn send_changed_breakpoints( &self, project_path: &ProjectPath, - buffer_path: PathBuf, + absolute_path: PathBuf, buffer_snapshot: BufferSnapshot, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let Some(local_store) = self.as_local() else { return Task::ready(Err(anyhow!("cannot start session on remote side"))); @@ -1913,7 +1910,7 @@ impl DapStore { for client in session.clients().collect::>() { tasks.push(self.send_breakpoints( &client.id(), - Arc::from(buffer_path.clone()), + Arc::from(absolute_path.clone()), source_breakpoints.clone(), ignore_breakpoints, cx, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 187df244d9d196..a27b8dfc5fc584 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1434,29 +1434,11 @@ impl Project { return; }; - let Some((project_path, buffer_path)) = maybe!({ - let project_path = buffer.read(cx).project_path(cx)?; - let worktree = self.worktree_for_id(project_path.clone().worktree_id, cx)?; - Some(( - project_path.clone(), - worktree.read(cx).absolutize(&project_path.path).ok()?, - )) - }) else { - return; - }; - - self.dap_store.update(cx, |store, cx| { - store - .toggle_breakpoint_for_buffer( - &project_path, - breakpoint, - buffer_path, - buffer.read(cx).snapshot(), - edit_action, - cx, - ) - .detach_and_log_err(cx); - }); + if let Some(project_path) = buffer.read(cx).project_path(cx) { + self.dap_store.update(cx, |store, cx| { + store.toggle_breakpoint_for_buffer(&project_path, breakpoint, edit_action, cx) + }); + } } #[cfg(any(test, feature = "test-support"))] @@ -2536,8 +2518,36 @@ impl Project { message: message.clone(), }); } - DapStoreEvent::BreakpointsChanged => { - cx.notify(); + DapStoreEvent::BreakpointsChanged(project_path) => { + cx.notify(); // so the UI updates + + let buffer_id = self + .buffer_store + .read(cx) + .buffer_id_for_project_path(&project_path); + + let Some(buffer_id) = buffer_id else { + return; + }; + + let Some(buffer) = self.buffer_for_id(*buffer_id, cx) else { + return; + }; + + let Some(absolute_path) = self.absolute_path(project_path, cx) else { + return; + }; + + self.dap_store.update(cx, |store, cx| { + store + .send_changed_breakpoints( + project_path, + absolute_path, + buffer.read(cx).snapshot(), + cx, + ) + .detach_and_log_err(cx); + }); } DapStoreEvent::ActiveDebugLineChanged => { cx.emit(Event::ActiveDebugLineChanged); From d03e4149ea3d862a127426e16eecb092e97c9211 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 16 Jan 2025 21:32:23 +0100 Subject: [PATCH 468/650] Fix CustomEvent type field are not public --- Cargo.lock | 2 +- crates/dap/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 763e9d0d157054..1661ddd85f4db3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3521,7 +3521,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=0f4da80b6f4713d268922556425b669627373b3e#0f4da80b6f4713d268922556425b669627373b3e" +source = "git+https://github.com/zed-industries/dap-types?rev=a6376f02cf9c284c706bc919c21e7ce68ef1fb47#a6376f02cf9c284c706bc919c21e7ce68ef1fb47" dependencies = [ "schemars", "serde", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 0a4dd1b349a98e..9f1161c9589adc 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -25,7 +25,7 @@ async-tar.workspace = true async-trait.workspace = true client.workspace = true collections.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "0f4da80b6f4713d268922556425b669627373b3e" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "a6376f02cf9c284c706bc919c21e7ce68ef1fb47" } fs.workspace = true futures.workspace = true gpui.workspace = true From bc49c184817ec5c9e4be5b6c06f558ddbd67da0e Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 16 Jan 2025 23:37:12 -0500 Subject: [PATCH 469/650] Fix Collab Module List Bug (#91) This PR fixes a module list bug where remote clients wouldn't have any modules in their module list when they hit their first breakpoint. We now send module list changes whenever there is a module list update and don't send update messages in the SetDebuggerPanelItem proto message. This is because the module list usually doesn't change each time a user steps over, so sending the module list was wasting bandwidth. * Add collab module list test * Get module list to send during all changes & stop redundant update * Update module list test for remote clients joining mid session --- crates/collab/src/tests/debug_panel_tests.rs | 269 ++++++++++++++++++ crates/debugger_ui/src/debugger_panel_item.rs | 10 +- crates/debugger_ui/src/module_list.rs | 51 +++- crates/proto/proto/zed.proto | 2 +- 4 files changed, 314 insertions(+), 18 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 1b35d217bf4cf3..8e5c625d3631e1 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1092,3 +1092,272 @@ async fn test_updated_breakpoints_send_to_dap( shutdown_client.await.unwrap(); } + +#[gpui::test] +async fn test_module_list( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + init_test(cx_a); + init_test(cx_b); + init_test(cx_c); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + + let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + let called_initialize = Arc::new(AtomicBool::new(false)); + + client + .on_request::({ + let called_initialize = called_initialize.clone(); + move |_, _| { + called_initialize.store(true, Ordering::SeqCst); + Ok(dap::Capabilities { + supports_restart_frame: Some(true), + supports_modules_request: Some(true), + ..Default::default() + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + let called_modules = Arc::new(AtomicBool::new(false)); + let modules = vec![ + dap::Module { + id: dap::ModuleId::Number(1), + name: "First Module".into(), + address_range: None, + date_time_stamp: None, + path: None, + symbol_file_path: None, + symbol_status: None, + version: None, + is_optimized: None, + is_user_code: None, + }, + dap::Module { + id: dap::ModuleId::Number(2), + name: "Second Module".into(), + address_range: None, + date_time_stamp: None, + path: None, + symbol_file_path: None, + symbol_status: None, + version: None, + is_optimized: None, + is_user_code: None, + }, + ]; + + client + .on_request::({ + let called_modules = called_modules.clone(); + let modules = modules.clone(); + move |_, _| unsafe { + static mut REQUEST_COUNT: i32 = 1; + assert_eq!( + 1, REQUEST_COUNT, + "This request should only be called once from the host" + ); + REQUEST_COUNT += 1; + called_modules.store(true, Ordering::SeqCst); + + Ok(dap::ModulesResponse { + modules: modules.clone(), + total_modules: Some(2u64), + }) + } + }) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_initialize.load(std::sync::atomic::Ordering::SeqCst), + "Request Initialize must be called" + ); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_modules.load(std::sync::atomic::Ordering::SeqCst), + "Request Modules must be called" + ); + + workspace_a.update(cx_a, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + debug_panel_item.update(cx, |item, cx| { + assert_eq!( + true, + item.capabilities(cx).supports_modules_request.unwrap(), + "Local supports modules request should be true" + ); + + let local_module_list = item.module_list().read(cx).modules(); + + assert_eq!( + 2usize, + local_module_list.len(), + "Local module list should have two items in it" + ); + assert_eq!( + &modules.clone(), + local_module_list, + "Local module list should match module list from response" + ); + }) + }); + + workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + debug_panel_item.update(cx, |item, cx| { + assert_eq!( + true, + item.capabilities(cx).supports_modules_request.unwrap(), + "Remote capabilities supports modules request should be true" + ); + let remote_module_list = item.module_list().read(cx).modules(); + + assert_eq!( + 2usize, + remote_module_list.len(), + "Remote module list should have two items in it" + ); + assert_eq!( + &modules.clone(), + remote_module_list, + "Remote module list should match module list from response" + ); + }) + }); + + let project_c = client_c.join_remote_project(project_id, cx_c).await; + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + add_debugger_panel(&workspace_c, cx_c).await; + cx_c.run_until_parked(); + + workspace_c.update(cx_c, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + debug_panel_item.update(cx, |item, cx| { + assert_eq!( + true, + item.capabilities(cx).supports_modules_request.unwrap(), + "Remote (mid session join) capabilities supports modules request should be true" + ); + let remote_module_list = item.module_list().read(cx).modules(); + + assert_eq!( + 2usize, + remote_module_list.len(), + "Remote (mid session join) module list should have two items in it" + ); + assert_eq!( + &modules.clone(), + remote_module_list, + "Remote (mid session join) module list should match module list from response" + ); + }) + }); + + client.on_request::(move |_, _| Ok(())).await; + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 9a82798521adeb..ff959d8dd282fa 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -129,6 +129,8 @@ impl DebugPanelItem { ) }); + cx.observe(&module_list, |_, _, cx| cx.notify()).detach(); + let _subscriptions = vec![ cx.subscribe(&debug_panel, { move |this: &mut Self, _, event: &DebugPanelEvent, cx| { @@ -198,7 +200,6 @@ impl DebugPanelItem { pub(crate) fn to_proto(&self, project_id: u64, cx: &AppContext) -> SetDebuggerPanelItem { let thread_state = Some(self.thread_state.read(cx).to_proto()); - let module_list = Some(self.module_list.read(cx).to_proto()); let variable_list = Some(self.variable_list.read(cx).to_proto()); let stack_frame_list = Some(self.stack_frame_list.read(cx).to_proto()); @@ -208,7 +209,7 @@ impl DebugPanelItem { client_id: self.client_id.to_proto(), thread_id: self.thread_id, console: None, - module_list, + module_list: None, active_thread_item: self.active_thread_item.to_proto().into(), thread_state, variable_list, @@ -487,6 +488,11 @@ impl DebugPanelItem { &self.console } + #[cfg(any(test, feature = "test-support"))] + pub fn module_list(&self) -> &View { + &self.module_list + } + #[cfg(any(test, feature = "test-support"))] pub fn variable_list(&self) -> &View { &self.variable_list diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 7c48da056f2c41..67b09d54e02b80 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -44,8 +44,9 @@ impl ModuleList { modules: Vec::default(), }; - this.fetch_modules(cx).detach_and_log_err(cx); - + if this.dap_store.read(cx).as_local().is_some() { + this.fetch_modules(cx).detach_and_log_err(cx); + } this } @@ -90,6 +91,15 @@ impl ModuleList { self.list.reset(self.modules.len()); cx.notify(); + + let task = cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |this, cx| { + this.propagate_updates(cx); + }) + .log_err(); + }); + + cx.background_executor().spawn(task).detach(); } fn fetch_modules(&self, cx: &mut ViewContext) -> Task> { @@ -105,23 +115,27 @@ impl ModuleList { this.list.reset(this.modules.len()); cx.notify(); - if let Some((client, id)) = this.dap_store.read(cx).downstream_client() { - let request = UpdateDebugAdapter { - session_id: this.session_id.to_proto(), - client_id: this.client_id.to_proto(), - project_id: *id, - thread_id: None, - variant: Some(rpc::proto::update_debug_adapter::Variant::Modules( - this.to_proto(), - )), - }; - - client.send(request).log_err(); - } + this.propagate_updates(cx); }) }) } + fn propagate_updates(&self, cx: &ViewContext) { + if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { + let request = UpdateDebugAdapter { + session_id: self.session_id.to_proto(), + client_id: self.client_id.to_proto(), + project_id: *id, + thread_id: None, + variant: Some(rpc::proto::update_debug_adapter::Variant::Modules( + self.to_proto(), + )), + }; + + client.send(request).log_err(); + } + } + fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { let module = &self.modules[ix]; @@ -156,3 +170,10 @@ impl Render for ModuleList { .child(list(self.list.clone()).size_full()) } } + +#[cfg(any(test, feature = "test-support"))] +impl ModuleList { + pub fn modules(&self) -> &Vec { + &self.modules + } +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 8511b75f82e899..57da1e70464e51 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2743,7 +2743,7 @@ message SetDebuggerPanelItem { uint64 thread_id = 4; string session_name = 5; DebuggerConsole console = 6; - DebuggerModuleList module_list = 7; + optional DebuggerModuleList module_list = 7; // We only send this when it's supported and once per client DebuggerThreadItem active_thread_item = 8; DebuggerThreadState thread_state = 9; DebuggerVariableList variable_list = 10; From 568f127f43053fb354a0d781b10771232acf17b9 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 16 Jan 2025 23:40:27 -0500 Subject: [PATCH 470/650] Fix name error in module list func --- crates/debugger_ui/src/debugger_panel_item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index ff959d8dd282fa..66b66dd068e9a6 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -356,8 +356,8 @@ impl DebugPanelItem { return; } - self.module_list.update(cx, |variable_list, cx| { - variable_list.on_module_event(event, cx); + self.module_list.update(cx, |module_list, cx| { + module_list.on_module_event(event, cx); }); } From a193efd65ae794ba901da7871adf553c660d5681 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 19 Jan 2025 12:58:45 +0100 Subject: [PATCH 471/650] Re-send breakpoints when source file has been changed (#92) * Resend breakpoints when the source/edit has been changed/saved * Check for dapstore first before checking project path * Add test to validate we re-send variables when editor is saved * Add new line after edit * Also send breakpoints changed when source on disk changed * Fix the test * Don't send breakpoints changed for saved event We send the breakpoints twice, because we already send them when the FileHandleChanged event was received and that is received after the Saved event itself. And the Saved event does not guarantee that the source is already changed on disk. Which it has to be so we can send the new breakpoints. * Assert in more places that the source modified is false --- crates/collab/src/tests/debug_panel_tests.rs | 3 + .../debugger_ui/src/tests/debugger_panel.rs | 203 +++++++++++++++++- crates/editor/src/editor.rs | 21 +- crates/project/src/dap_store.rs | 20 +- crates/project/src/project.rs | 8 +- 5 files changed, 243 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 8e5c625d3631e1..f47cf9b546d1af 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -944,6 +944,7 @@ async fn test_updated_breakpoints_send_to_dap( }], args.breakpoints.unwrap() ); + assert!(!args.source_modified.unwrap()); called_set_breakpoints.store(true, Ordering::SeqCst); @@ -1007,6 +1008,7 @@ async fn test_updated_breakpoints_send_to_dap( move |_, args| { assert_eq!("/a/test.txt", args.source.path.unwrap()); assert!(args.breakpoints.unwrap().is_empty()); + assert!(!args.source_modified.unwrap()); called_set_breakpoints.store(true, Ordering::SeqCst); @@ -1059,6 +1061,7 @@ async fn test_updated_breakpoints_send_to_dap( ], args.breakpoints.unwrap() ); + assert!(!args.source_modified.unwrap()); called_set_breakpoints.store(true, Ordering::SeqCst); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 32fa8168b8d5b6..ea925bc7ba9af6 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -2,13 +2,17 @@ use crate::*; use dap::{ client::DebugAdapterClientId, requests::{ - Continue, Disconnect, Initialize, Launch, Next, RunInTerminal, StackTrace, StartDebugging, - StepBack, StepIn, StepOut, + Continue, Disconnect, Initialize, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace, + StartDebugging, StepBack, StepIn, StepOut, }, - ErrorResponse, RunInTerminalRequestArguments, StartDebuggingRequestArguments, + ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, }; use debugger_panel::ThreadStatus; +use editor::{ + actions::{self}, + Editor, EditorMode, MultiBuffer, +}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{FakeFs, Project}; use serde_json::json; @@ -18,7 +22,7 @@ use std::sync::{ }; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use tests::{active_debug_panel_item, init_test, init_test_workspace}; -use workspace::dock::Panel; +use workspace::{dock::Panel, Item}; #[gpui::test] async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) { @@ -956,3 +960,194 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_send_breakpoints_when_editor_has_been_saved( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + fs.insert_tree( + "/a", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + + let (editor, cx) = cx.add_window_view(|cx| { + Editor::new( + EditorMode::Full, + MultiBuffer::build_from_buffer(buffer, cx), + Some(project.clone()), + true, + cx, + ) + }); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/main.rs", args.source.path.unwrap()); + assert_eq!( + vec![SourceBreakpoint { + line: 2, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + }], + args.breakpoints.unwrap() + ); + assert!(!args.source_modified.unwrap()); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + editor.update(cx, |editor, cx| { + editor.move_down(&actions::MoveDown, cx); + editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); + }); + + cx.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called" + ); + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/main.rs", args.source.path.unwrap()); + assert_eq!( + vec![SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + }], + args.breakpoints.unwrap() + ); + assert!(args.source_modified.unwrap()); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + editor.update(cx, |editor, cx| { + editor.move_up(&actions::MoveUp, cx); + editor.insert("new text\n", cx); + }); + + editor + .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .await + .unwrap(); + + cx.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called after editor is saved" + ); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 720b34ae699a10..4888e4f4dbdeda 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -106,7 +106,10 @@ use language::{ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; use mouse_context_menu::MouseContextMenu; -use project::{dap_store::BreakpointEditAction, ProjectPath}; +use project::{ + dap_store::{BreakpointEditAction, DapStoreEvent}, + ProjectPath, +}; pub use proposed_changes_editor::{ ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, }; @@ -12918,9 +12921,21 @@ impl Editor { } multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), - multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => { - cx.emit(EditorEvent::TitleChanged) + multi_buffer::Event::FileHandleChanged => { + cx.emit(EditorEvent::TitleChanged); + + if let Some(dap_store) = &self.dap_store { + if let Some(project_path) = self.project_path(cx) { + dap_store.update(cx, |_, cx| { + cx.emit(DapStoreEvent::BreakpointsChanged { + project_path, + source_changed: true, + }); + }); + } + } } + multi_buffer::Event::Reloaded => cx.emit(EditorEvent::TitleChanged), // multi_buffer::Event::DiffBaseChanged => { // self.scrollbar_marker_state.dirty = true; // cx.emit(EditorEvent::DiffBaseChanged); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 65539492969f6d..7c7bc91d1514cd 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -75,7 +75,10 @@ pub enum DapStoreEvent { message: Message, }, Notification(String), - BreakpointsChanged(ProjectPath), + BreakpointsChanged { + project_path: ProjectPath, + source_changed: bool, + }, ActiveDebugLineChanged, SetDebugPanelItem(SetDebuggerPanelItem), UpdateDebugAdapter(UpdateDebugAdapter), @@ -1690,7 +1693,10 @@ impl DapStore { store.breakpoints.insert(project_path.clone(), breakpoints); } - cx.emit(DapStoreEvent::BreakpointsChanged(project_path)); + cx.emit(DapStoreEvent::BreakpointsChanged { + project_path, + source_changed: false, + }); cx.notify(); }) @@ -1838,7 +1844,10 @@ impl DapStore { self.breakpoints.remove(project_path); } - cx.emit(DapStoreEvent::BreakpointsChanged(project_path.clone())); + cx.emit(DapStoreEvent::BreakpointsChanged { + project_path: project_path.clone(), + source_changed: false, + }); cx.notify(); } @@ -1848,6 +1857,7 @@ impl DapStore { absolute_file_path: Arc, mut breakpoints: Vec, ignore: bool, + source_changed: bool, cx: &ModelContext, ) -> Task> { let Some((_, client)) = self.client_by_id(client_id, cx) else { @@ -1874,7 +1884,7 @@ impl DapStore { checksums: None, }, breakpoints: Some(if ignore { Vec::default() } else { breakpoints }), - source_modified: Some(false), + source_modified: Some(source_changed), lines: None, }) .await?; @@ -1888,6 +1898,7 @@ impl DapStore { project_path: &ProjectPath, absolute_path: PathBuf, buffer_snapshot: BufferSnapshot, + source_changed: bool, cx: &ModelContext, ) -> Task> { let Some(local_store) = self.as_local() else { @@ -1913,6 +1924,7 @@ impl DapStore { Arc::from(absolute_path.clone()), source_breakpoints.clone(), ignore_breakpoints, + source_changed, cx, )); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a27b8dfc5fc584..c4d6d443087805 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1271,6 +1271,7 @@ impl Project { abs_path, source_breakpoints, store.ignore_breakpoints(session_id, cx), + false, cx, ) })); @@ -1403,6 +1404,7 @@ impl Project { .map(|breakpoint| breakpoint.to_source_breakpoint(buffer)) .collect::>(), store.ignore_breakpoints(session_id, cx), + false, cx, ), ); @@ -2518,7 +2520,10 @@ impl Project { message: message.clone(), }); } - DapStoreEvent::BreakpointsChanged(project_path) => { + DapStoreEvent::BreakpointsChanged { + project_path, + source_changed, + } => { cx.notify(); // so the UI updates let buffer_id = self @@ -2544,6 +2549,7 @@ impl Project { project_path, absolute_path, buffer.read(cx).snapshot(), + *source_changed, cx, ) .detach_and_log_err(cx); From f34fc7fbe8e59b92000d6c0c085257e65a84b871 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 19 Jan 2025 13:08:41 +0100 Subject: [PATCH 472/650] Use arc for dap command request instead of cloning the command itself --- crates/project/src/dap_command.rs | 26 +++++++++++++------------- crates/project/src/dap_store.rs | 10 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs index 386c2753f59ef4..b2f907d5de38d2 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/dap_command.rs @@ -11,7 +11,7 @@ use util::ResultExt; use crate::dap_store::DapStore; -pub trait DapCommand: 'static + Sized + Send + std::fmt::Debug + Clone { +pub trait DapCommand: 'static + Sized + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; @@ -54,7 +54,7 @@ pub trait DapCommand: 'static + Sized + Send + std::fmt::Debug + Clone { ) -> Result; } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct StepCommand { pub thread_id: u64, pub granularity: Option, @@ -80,7 +80,7 @@ impl StepCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct NextCommand { pub inner: StepCommand, } @@ -144,7 +144,7 @@ impl DapCommand for NextCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct StepInCommand { pub inner: StepCommand, } @@ -216,7 +216,7 @@ impl DapCommand for StepInCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct StepOutCommand { pub inner: StepCommand, } @@ -316,7 +316,7 @@ impl DapCommand for StepOutCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct StepBackCommand { pub inner: StepCommand, } @@ -386,7 +386,7 @@ impl DapCommand for StepBackCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct ContinueCommand { pub args: ContinueArguments, } @@ -483,7 +483,7 @@ impl DapCommand for ContinueCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct PauseCommand { pub thread_id: u64, } @@ -543,7 +543,7 @@ impl DapCommand for PauseCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct DisconnectCommand { pub restart: Option, pub terminate_debuggee: Option, @@ -611,7 +611,7 @@ impl DapCommand for DisconnectCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct TerminateThreadsCommand { pub thread_ids: Option>, } @@ -675,7 +675,7 @@ impl DapCommand for TerminateThreadsCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct TerminateCommand { pub restart: Option, } @@ -735,7 +735,7 @@ impl DapCommand for TerminateCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct RestartCommand { pub raw: serde_json::Value, } @@ -799,7 +799,7 @@ impl DapCommand for RestartCommand { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub(crate) struct RestartStackFrameCommand { pub stack_frame_id: u64, } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 7c7bc91d1514cd..2d9a28184c47bd 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1112,7 +1112,7 @@ impl DapStore { ) -> Task> where ::Response: 'static, - ::Arguments: 'static, + ::Arguments: 'static + Send, { if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { return self.send_proto_client_request::( @@ -1129,17 +1129,17 @@ impl DapStore { }; let client_id = *client_id; - let request_clone = request.clone(); + let request = Arc::new(request); + let request_clone = request.clone(); let request_task = cx.background_executor().spawn(async move { let args = request_clone.to_dap(); client.request::(args).await }); - let request_clone = request.clone(); cx.spawn(|this, mut cx| async move { - let response = request_clone.response_from_dap(request_task.await?); - request_clone.handle_response(this, &client_id, response, &mut cx) + let response = request.response_from_dap(request_task.await?); + request.handle_response(this, &client_id, response, &mut cx) }) } From 868f55c0740662ebd907c9c7963f50b186bb9b47 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 19 Jan 2025 14:33:41 +0100 Subject: [PATCH 473/650] Fix failing test --- crates/debugger_ui/src/tests/console.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 8e9cf6912e5a01..be0b9c0f78869c 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -157,7 +157,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp .update(cx, |this, cx| this.active_debug_panel_item(cx)) .unwrap(); - assert!(debug_panel.read(cx).message_queue().is_empty()); + assert!(!debug_panel.read(cx).message_queue().is_empty()); assert_eq!( "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", From 5ecfef0aa0a237d6010aa8e73294dd0a792a52bb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 19 Jan 2025 14:36:02 +0100 Subject: [PATCH 474/650] Fix failing test because of race condition By sorting the breakpoints based on the line number, the order does not matter. --- crates/collab/src/tests/debug_panel_tests.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index f47cf9b546d1af..04f4a38d3eae2c 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1040,10 +1040,12 @@ async fn test_updated_breakpoints_send_to_dap( let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { assert_eq!("/a/test.txt", args.source.path.unwrap()); + let mut breakpoints = args.breakpoints.unwrap(); + breakpoints.sort_by_key(|b| b.line); assert_eq!( vec![ SourceBreakpoint { - line: 3, + line: 2, column: None, condition: None, hit_condition: None, @@ -1051,7 +1053,7 @@ async fn test_updated_breakpoints_send_to_dap( mode: None }, SourceBreakpoint { - line: 2, + line: 3, column: None, condition: None, hit_condition: None, @@ -1059,7 +1061,7 @@ async fn test_updated_breakpoints_send_to_dap( mode: None } ], - args.breakpoints.unwrap() + breakpoints ); assert!(!args.source_modified.unwrap()); From 4de9921fa8960249bbaa361e162eb6906d1ecdae Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 19 Jan 2025 16:07:18 +0100 Subject: [PATCH 475/650] Add support for adapter specific clipboard value formatting --- crates/collab/src/db/tables/debug_clients.rs | 5 +++ crates/dap/src/proto_conversions.rs | 2 ++ crates/debugger_ui/src/console.rs | 1 + crates/debugger_ui/src/variable_list.rs | 37 ++++++++++++++++++-- crates/project/src/dap_store.rs | 3 +- crates/proto/proto/zed.proto | 1 + 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/db/tables/debug_clients.rs b/crates/collab/src/db/tables/debug_clients.rs index 9c6d48346ace8a..02758acaa0c4fa 100644 --- a/crates/collab/src/db/tables/debug_clients.rs +++ b/crates/collab/src/db/tables/debug_clients.rs @@ -12,6 +12,7 @@ const SUPPORTS_STEP_BACK_BIT: u32 = 5; const SUPPORTS_STEPPING_GRANULARITY_BIT: u32 = 6; const SUPPORTS_TERMINATE_THREADS_REQUEST_BIT: u32 = 7; const SUPPORTS_RESTART_FRAME_REQUEST_BIT: u32 = 8; +const SUPPORTS_CLIPBOARD_CONTEXT_BIT: u32 = 9; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "debug_clients")] @@ -53,6 +54,8 @@ impl Model { supports_restart_frame_request: (self.capabilities & (1 << SUPPORTS_RESTART_FRAME_REQUEST_BIT)) != 0, + supports_clipboard_context: (self.capabilities & (1 << SUPPORTS_CLIPBOARD_CONTEXT_BIT)) + != 0, } } @@ -75,6 +78,8 @@ impl Model { << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT; capabilities_bit_mask |= (capabilities.supports_restart_frame_request as i32) << SUPPORTS_RESTART_FRAME_REQUEST_BIT; + capabilities_bit_mask |= + (capabilities.supports_clipboard_context as i32) << SUPPORTS_CLIPBOARD_CONTEXT_BIT; self.capabilities = capabilities_bit_mask; } diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index 15bf89638d0da9..b543c01f6c2595 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -345,6 +345,7 @@ pub fn capabilities_from_proto(payload: &SetDebugClientCapabilities) -> Capabili supports_stepping_granularity: Some(payload.supports_stepping_granularity), supports_terminate_threads_request: Some(payload.supports_terminate_threads_request), supports_restart_frame: Some(payload.supports_restart_frame_request), + supports_clipboard_context: Some(payload.supports_clipboard_context), ..Default::default() } } @@ -376,6 +377,7 @@ pub fn capabilities_to_proto( .supports_terminate_threads_request .unwrap_or_default(), supports_restart_frame_request: capabilities.supports_restart_frame.unwrap_or_default(), + supports_clipboard_context: capabilities.supports_clipboard_context.unwrap_or_default(), } } diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 85cd80abf7c346..d51644cde16015 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -230,6 +230,7 @@ impl Console { self.stack_frame_list.read(cx).current_stack_frame_id(), expression, dap::EvaluateArgumentsContext::Variables, + None, cx, ) }); diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 7746004e851d22..316d8341aa29b5 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1051,8 +1051,41 @@ impl VariableList { None, cx.handler_for(&this, { let variable_value = variable.value.clone(); - move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone())) + let variable_name = variable.name.clone(); + let evaluate_name = variable.evaluate_name.clone(); + let source = scope.source.clone(); + move |this, cx| { + this.dap_store.update(cx, |dap_store, cx| { + if dap_store + .capabilities_by_id(&this.client_id) + .supports_clipboard_context + .unwrap_or_default() + { + let task = dap_store.evaluate( + &this.client_id, + this.stack_frame_list.read(cx).current_stack_frame_id(), + evaluate_name.clone().unwrap_or(variable_name.clone()), + dap::EvaluateArgumentsContext::Clipboard, + source.clone(), + cx, + ); + + cx.spawn(|_, cx| async move { + let response = task.await?; + + cx.update(|cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + response.result, + )) + }) + }) + .detach_and_log_err(cx); + } else { + cx.write_to_clipboard(ClipboardItem::new_string( + variable_value.clone(), + )) + } + }); } }), ) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 2d9a28184c47bd..83913b76339350 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1296,6 +1296,7 @@ impl DapStore { stack_frame_id: u64, expression: String, context: EvaluateArgumentsContext, + source: Option, cx: &mut ModelContext, ) -> Task> { let Some((_, client)) = self.client_by_id(client_id, cx) else { @@ -1311,7 +1312,7 @@ impl DapStore { format: None, line: None, column: None, - source: None, + source, }) .await }) diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 390e37c97157a3..d9541d88393cf6 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2501,6 +2501,7 @@ message SetDebugClientCapabilities { bool supports_stepping_granularity = 10; bool supports_terminate_threads_request = 11; bool supports_restart_frame_request = 12; + bool supports_clipboard_context = 13; } message Breakpoint { From 1a5feff9edcccbe3b7901f010c092d1edd08a1f7 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:58:46 -0500 Subject: [PATCH 476/650] Implment Variable List Toggling Function in Collab (#88) * Impl VariablesCommand for dap requests * Have remote variable list generate entries & other fields locally * Finish first version of collab variable list test (It fails) * Get variable list collab test to pass * Update variable test again (it failing) * Improve tests more WIP * Remove a test to make merge easier * Finalize collab test * Only send variables one time to collab server instead of each rebuild * Finish variable list fetch! --- .../collab/src/db/tables/debug_panel_items.rs | 13 +- crates/collab/src/rpc.rs | 1 + crates/collab/src/tests/debug_panel_tests.rs | 496 +++++++++++++++++- crates/dap/src/session.rs | 4 +- crates/debugger_ui/src/debugger_panel_item.rs | 3 + crates/debugger_ui/src/variable_list.rs | 219 ++++---- crates/project/src/dap_command.rs | 127 +++++ crates/project/src/dap_store.rs | 38 +- crates/proto/proto/zed.proto | 66 ++- crates/proto/src/proto.rs | 4 + 10 files changed, 805 insertions(+), 166 deletions(-) diff --git a/crates/collab/src/db/tables/debug_panel_items.rs b/crates/collab/src/db/tables/debug_panel_items.rs index d4d91f7d29f805..fa5a124242a4b4 100644 --- a/crates/collab/src/db/tables/debug_panel_items.rs +++ b/crates/collab/src/db/tables/debug_panel_items.rs @@ -1,5 +1,5 @@ use crate::db::ProjectId; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use prost::Message; use rpc::{proto, proto::SetDebuggerPanelItem}; use sea_orm::entity::prelude::*; @@ -114,6 +114,17 @@ impl Model { let encoded = variable_list.encode_to_vec(); self.variable_list = encoded; } + proto::update_debug_adapter::Variant::AddToVariableList(added_variables) => { + let mut variable_list = proto::DebuggerVariableList::decode( + &self.variable_list[..], + ) + .with_context(|| { + "Failed to decode DebuggerVariableList during AddToVariableList variant update" + })?; + + variable_list.added_variables.push(added_variables.clone()); + self.variable_list = variable_list.encode_to_vec(); + } proto::update_debug_adapter::Variant::Modules(module_list) => { let encoded = module_list.encode_to_vec(); self.module_list = encoded; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 305c1e676ce819..cf7083a21c1a99 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -435,6 +435,7 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) + .add_request_handler(forward_mutating_project_request::) .add_message_handler( broadcast_project_message_from_host::, ); diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 04f4a38d3eae2c..245afc0eea24ef 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1,19 +1,19 @@ use call::ActiveCall; +use dap::requests::{Disconnect, Initialize, Launch, Scopes, StackTrace, Variables}; use dap::{ - requests::{Disconnect, Initialize, Launch, RestartFrame, SetBreakpoints, StackTrace}, + requests::{RestartFrame, SetBreakpoints}, SourceBreakpoint, StackFrame, }; -use debugger_ui::debugger_panel::DebugPanel; +use dap::{Scope, Variable}; +use debugger_ui::{debugger_panel::DebugPanel, variable_list::VariableContainer}; use editor::Editor; use gpui::{TestAppContext, View, VisualTestContext}; use project::ProjectPath; use serde_json::json; +use std::sync::Arc; use std::{ path::Path, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::atomic::{AtomicBool, Ordering}, }; use workspace::{dock::Panel, Workspace}; @@ -1366,3 +1366,487 @@ async fn test_module_list( shutdown_client.await.unwrap(); } + +#[gpui::test] +async fn test_variable_list( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + init_test(cx_a); + init_test(cx_b); + init_test(cx_c); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + + let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![dap::StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }]; + + let scopes = vec![Scope { + name: "Scope 1".into(), + presentation_hint: None, + variables_reference: 1, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }]; + + let variable_1 = Variable { + name: "variable 1".into(), + value: "1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }; + + let variable_2 = Variable { + name: "variable 2".into(), + value: "2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 3, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }; + + let variable_3 = Variable { + name: "variable 3".into(), + value: "hello world".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 4, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }; + + let variable_4 = Variable { + name: "variable 4".into(), + value: "hello world this is the final variable".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }; + + client + .on_request::({ + let stack_frames = std::sync::Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + client + .on_request::({ + let scopes = Arc::new(scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*scopes).clone(), + }) + } + }) + .await; + + let first_variable_request = vec![variable_1.clone(), variable_2.clone()]; + + client + .on_request::({ + move |_, args| { + assert_eq!(1, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: first_variable_request.clone(), + }) + } + }) + .await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + cx_c.run_until_parked(); + + let local_debug_item = workspace_a.update(cx_a, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + let remote_debug_item = workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + let first_visual_entries = vec!["v Scope 1", " > variable 1", " > variable 2"]; + let first_variable_containers = vec![ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variable_1.clone(), + depth: 1, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variable_2.clone(), + depth: 1, + }, + ]; + + local_debug_item + .update(cx_a, |this, _| this.variable_list().clone()) + .update(cx_a, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!(&first_variable_containers, &variable_list.variables()); + + variable_list.assert_visual_entries(first_visual_entries.clone(), cx); + }); + + client + .on_request::({ + let variables = Arc::new(vec![variable_3.clone()]); + move |_, args| { + assert_eq!(2, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*variables).clone(), + }) + } + }) + .await; + + remote_debug_item + .update(cx_b, |this, _| this.variable_list().clone()) + .update(cx_b, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!(&first_variable_containers, &variable_list.variables()); + + variable_list.assert_visual_entries(first_visual_entries.clone(), cx); + + variable_list.toggle_variable_in_test( + scopes[0].variables_reference, + &variable_1, + 1, + cx, + ); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + cx_c.run_until_parked(); + + let second_req_variable_list = vec![ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variable_1.clone(), + depth: 1, + }, + VariableContainer { + container_reference: variable_1.variables_reference, + variable: variable_3.clone(), + depth: 2, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variable_2.clone(), + depth: 1, + }, + ]; + + remote_debug_item + .update(cx_b, |this, _| this.variable_list().clone()) + .update(cx_b, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(3, variable_list.variables().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!(&second_req_variable_list, &variable_list.variables()); + + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable 1", + " > variable 3", + " > variable 2", + ], + cx, + ); + }); + + client + .on_request::({ + let variables = Arc::new(vec![variable_4.clone()]); + move |_, args| { + assert_eq!(3, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*variables).clone(), + }) + } + }) + .await; + + local_debug_item + .update(cx_a, |this, _| this.variable_list().clone()) + .update(cx_a, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(3, variable_list.variables().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!(&second_req_variable_list, &variable_list.variables()); + + variable_list.assert_visual_entries(first_visual_entries.clone(), cx); + + variable_list.toggle_variable_in_test( + scopes[0].variables_reference, + &variable_2.clone(), + 1, + cx, + ); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + cx_c.run_until_parked(); + + let final_variable_containers: Vec = vec![ + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variable_1.clone(), + depth: 1, + }, + VariableContainer { + container_reference: variable_1.variables_reference, + variable: variable_3.clone(), + depth: 2, + }, + VariableContainer { + container_reference: scopes[0].variables_reference, + variable: variable_2.clone(), + depth: 1, + }, + VariableContainer { + container_reference: variable_2.variables_reference, + variable: variable_4.clone(), + depth: 2, + }, + ]; + + remote_debug_item + .update(cx_b, |this, _| this.variable_list().clone()) + .update(cx_b, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(4, variable_list.variables().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!(&final_variable_containers, &variable_list.variables()); + + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " v variable 1", + " > variable 3", + " > variable 2", + ], + cx, + ); + }); + + local_debug_item + .update(cx_a, |this, _| this.variable_list().clone()) + .update(cx_a, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(4, variable_list.variables().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!(&final_variable_containers, &variable_list.variables()); + + variable_list.assert_visual_entries( + vec![ + "v Scope 1", + " > variable 1", + " v variable 2", + " > variable 4", + ], + cx, + ); + }); + + let project_c = client_c.join_remote_project(project_id, cx_c).await; + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + add_debugger_panel(&workspace_c, cx_c).await; + cx_c.run_until_parked(); + + let last_join_remote_item = workspace_c.update(cx_c, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + last_join_remote_item + .update(cx_c, |this, _| this.variable_list().clone()) + .update(cx_c, |variable_list, cx| { + assert_eq!(1, variable_list.scopes().len()); + assert_eq!(4, variable_list.variables().len()); + assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); + assert_eq!(final_variable_containers, variable_list.variables()); + + variable_list.assert_visual_entries(first_visual_entries, cx); + }); + + client.on_request::(move |_, _| Ok(())).await; + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs index 447350bb88477c..77e07a76a526e0 100644 --- a/crates/dap/src/session.rs +++ b/crates/dap/src/session.rs @@ -10,8 +10,8 @@ use crate::client::{DebugAdapterClient, DebugAdapterClientId}; pub struct DebugSessionId(pub usize); impl DebugSessionId { - pub fn from_proto(client_id: u64) -> Self { - Self(client_id as usize) + pub fn from_proto(session_id: u64) -> Self { + Self(session_id as usize) } pub fn to_proto(&self) -> u64 { diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 66b66dd068e9a6..35ee5957f41737 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -457,6 +457,9 @@ impl DebugPanelItem { proto::update_debug_adapter::Variant::VariableList(variable_list) => self .variable_list .update(cx, |this, cx| this.set_from_proto(variable_list, cx)), + proto::update_debug_adapter::Variant::AddToVariableList(variables_to_add) => self + .variable_list + .update(cx, |this, _| this.add_variables(variables_to_add.clone())), proto::update_debug_adapter::Variant::Modules(module_list) => { self.module_list.update(cx, |this, cx| { this.set_from_proto(module_list, cx); diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 316d8341aa29b5..855870d260367d 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -12,10 +12,9 @@ use gpui::{ }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; use project::dap_store::DapStore; -use proto::debugger_variable_list_entry::Entry; use rpc::proto::{ self, DebuggerScopeVariableIndex, DebuggerVariableContainer, UpdateDebugAdapter, - VariableListEntries, VariableListScopes, VariableListVariables, + VariableListScopes, VariableListVariables, }; use std::{ collections::{BTreeMap, HashMap, HashSet}, @@ -68,7 +67,7 @@ pub struct SetVariableState { } impl SetVariableState { - fn from_proto(payload: proto::DebuggerSetVariableState) -> Option { + fn _from_proto(payload: proto::DebuggerSetVariableState) -> Option { let scope = payload.scope.map(|scope| { let proto_hint = scope .presentation_hint @@ -115,7 +114,7 @@ impl SetVariableState { }) } - fn to_proto(&self) -> proto::DebuggerSetVariableState { + fn _to_proto(&self) -> proto::DebuggerSetVariableState { proto::DebuggerSetVariableState { name: self.name.clone(), scope: Some(self.scope.to_proto()), @@ -140,7 +139,7 @@ pub enum OpenEntry { } impl OpenEntry { - pub(crate) fn from_proto(open_entry: &proto::VariableListOpenEntry) -> Option { + pub(crate) fn _from_proto(open_entry: &proto::VariableListOpenEntry) -> Option { match open_entry.entry.as_ref()? { proto::variable_list_open_entry::Entry::Scope(state) => Some(Self::Scope { name: state.name.clone(), @@ -153,7 +152,7 @@ impl OpenEntry { } } - pub(crate) fn to_proto(&self) -> proto::VariableListOpenEntry { + pub(crate) fn _to_proto(&self) -> proto::VariableListOpenEntry { let entry = match self { OpenEntry::Scope { name } => { proto::variable_list_open_entry::Entry::Scope(proto::DebuggerOpenEntryScope { @@ -193,52 +192,6 @@ pub enum VariableListEntry { }, } -impl VariableListEntry { - pub(crate) fn to_proto(&self) -> proto::DebuggerVariableListEntry { - let entry = match &self { - VariableListEntry::Scope(scope) => Entry::Scope(scope.to_proto()), - VariableListEntry::Variable { - depth, - scope, - variable, - has_children, - container_reference, - } => Entry::Variable(proto::VariableListEntryVariable { - depth: *depth as u64, - scope: Some(scope.to_proto()), - variable: Some(variable.to_proto()), - has_children: *has_children, - container_reference: *container_reference, - }), - VariableListEntry::SetVariableEditor { depth, state } => { - Entry::SetVariableEditor(proto::VariableListEntrySetState { - depth: *depth as u64, - state: Some(state.to_proto()), - }) - } - }; - - proto::DebuggerVariableListEntry { entry: Some(entry) } - } - - pub(crate) fn from_proto(entry: proto::DebuggerVariableListEntry) -> Option { - match entry.entry? { - Entry::Scope(scope) => Some(Self::Scope(Scope::from_proto(scope))), - Entry::Variable(var) => Some(Self::Variable { - depth: var.depth as usize, - scope: Arc::new(Scope::from_proto(var.scope?)), - variable: Arc::new(Variable::from_proto(var.variable?)), - has_children: var.has_children, - container_reference: var.container_reference, - }), - Entry::SetVariableEditor(set_state) => Some(Self::SetVariableEditor { - depth: set_state.depth as usize, - state: SetVariableState::from_proto(set_state.state?)?, - }), - } - } -} - #[derive(Debug)] pub struct ScopeVariableIndex { fetched_ids: HashSet, @@ -327,7 +280,10 @@ impl ScopeVariableIndex { /// All the variables should have the same depth and the same container reference pub fn add_variables(&mut self, container_reference: u64, variables: Vec) { - self.fetched_ids.insert(container_reference); + // We want to avoid adding the same variables dued to collab clients sending add variables updates + if !self.fetched_ids.insert(container_reference) { + return; + } let mut new_variables = SumTree::new(&()); let mut cursor = self.variables.cursor::(&()); @@ -445,12 +401,6 @@ impl VariableList { } pub(crate) fn to_proto(&self) -> proto::DebuggerVariableList { - let open_entries = self.open_entries.iter().map(OpenEntry::to_proto).collect(); - let set_variable_state = self - .set_variable_state - .as_ref() - .map(SetVariableState::to_proto); - let variables = self .variables .iter() @@ -463,19 +413,6 @@ impl VariableList { ) .collect(); - let entries = self - .entries - .iter() - .map(|(key, entries)| VariableListEntries { - stack_frame_id: *key, - entries: entries - .clone() - .iter() - .map(|entry| entry.to_proto()) - .collect(), - }) - .collect(); - let scopes = self .scopes .iter() @@ -486,11 +423,9 @@ impl VariableList { .collect(); proto::DebuggerVariableList { - open_entries, scopes, - set_variable_state, - entries, variables, + added_variables: vec![], } } @@ -510,33 +445,6 @@ impl VariableList { }) .collect(); - self.open_entries = state - .open_entries - .iter() - .filter_map(OpenEntry::from_proto) - .collect(); - - self.set_variable_state = state - .set_variable_state - .clone() - .and_then(SetVariableState::from_proto); - - self.entries = state - .entries - .iter() - .map(|entry| { - ( - entry.stack_frame_id, - entry - .entries - .clone() - .into_iter() - .filter_map(VariableListEntry::from_proto) - .collect(), - ) - }) - .collect(); - self.scopes = state .scopes .iter() @@ -553,10 +461,44 @@ impl VariableList { }) .collect(); + for variables in state.added_variables.iter() { + self.add_variables(variables.clone()); + } + self.build_entries(true, true, cx); cx.notify(); } + pub(crate) fn add_variables(&mut self, variables_to_add: proto::AddToVariableList) { + let variables: Vec = Vec::from_proto(variables_to_add.variables); + let variable_id = variables_to_add.variable_id; + let stack_frame_id = variables_to_add.stack_frame_id; + let scope_id = variables_to_add.scope_id; + let key = (stack_frame_id, scope_id); + + if let Some(depth) = self.variables.get(&key).and_then(|containers| { + containers + .variables + .iter() + .find(|container| container.variable.variables_reference == variable_id) + .map(|container| container.depth + 1usize) + }) { + if let Some(index) = self.variables.get_mut(&key) { + index.add_variables( + variable_id, + variables + .into_iter() + .map(|var| VariableContainer { + container_reference: variable_id, + variable: var, + depth, + }) + .collect(), + ); + } + } + } + fn handle_stack_frame_list_events( &mut self, _: View, @@ -578,6 +520,14 @@ impl VariableList { &self.scopes } + #[cfg(any(test, feature = "test-support"))] + pub fn variables(&self) -> Vec { + self.variables + .iter() + .flat_map(|((_, _), scope_index)| scope_index.variables()) + .collect() + } + #[cfg(any(test, feature = "test-support"))] pub fn entries(&self) -> &HashMap> { &self.entries @@ -673,7 +623,16 @@ impl VariableList { } let fetch_variables_task = self.dap_store.update(cx, |store, cx| { - store.variables(&self.client_id, variable.variables_reference, cx) + let thread_id = self.stack_frame_list.read(cx).thread_id(); + store.variables( + &self.client_id, + thread_id, + stack_frame_id, + scope_id, + self.session_id, + variable.variables_reference, + cx, + ) }); let container_reference = variable.variables_reference; @@ -864,20 +823,6 @@ impl VariableList { } cx.notify(); - - if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { - let request = UpdateDebugAdapter { - client_id: self.client_id.to_proto(), - session_id: self.session_id.to_proto(), - thread_id: Some(self.stack_frame_list.read(cx).thread_id()), - project_id: *project_id, - variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( - self.to_proto(), - )), - }; - - client.send(request).log_err(); - } } fn fetch_nested_variables( @@ -887,8 +832,21 @@ impl VariableList { open_entries: &Vec, cx: &mut ViewContext, ) -> Task>> { + let stack_frame_list = self.stack_frame_list.read(cx); + let thread_id = stack_frame_list.thread_id(); + let stack_frame_id = stack_frame_list.current_stack_frame_id(); + let scope_id = container_reference; + let variables_task = self.dap_store.update(cx, |store, cx| { - store.variables(&self.client_id, container_reference, cx) + store.variables( + &self.client_id, + thread_id, + stack_frame_id, + scope_id, + self.session_id, + container_reference, + cx, + ) }); cx.spawn({ @@ -1013,6 +971,20 @@ impl VariableList { this.entries.clear(); this.build_entries(true, true, cx); + if let Some((client, project_id)) = this.dap_store.read(cx).downstream_client() { + let request = UpdateDebugAdapter { + client_id: this.client_id.to_proto(), + session_id: this.session_id.to_proto(), + thread_id: Some(this.stack_frame_list.read(cx).thread_id()), + project_id: *project_id, + variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( + this.to_proto(), + )), + }; + + client.send(request).log_err(); + }; + this.fetch_variables_task.take(); }) })); @@ -1350,6 +1322,17 @@ impl VariableList { .into_any_element() } + #[cfg(any(test, feature = "test-support"))] + pub fn toggle_variable_in_test( + &mut self, + scope_id: u64, + variable: &Variable, + depth: usize, + cx: &mut ViewContext, + ) { + self.toggle_variable(scope_id, variable, depth, cx); + } + #[track_caller] #[cfg(any(test, feature = "test-support"))] pub fn assert_visual_entries(&self, expected: Vec<&str>, cx: &ViewContext) { diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs index b2f907d5de38d2..a7c354b22fd3f5 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/dap_command.rs @@ -3,7 +3,9 @@ use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, requests::{Continue, Next}, + session::DebugSessionId, ContinueArguments, NextArguments, StepInArguments, StepOutArguments, SteppingGranularity, + ValueFormat, Variable, VariablesArgumentsFilter, }; use gpui::{AsyncAppContext, WeakModel}; use rpc::proto; @@ -800,6 +802,131 @@ impl DapCommand for RestartCommand { } #[derive(Debug)] +pub struct VariablesCommand { + pub stack_frame_id: u64, + pub scope_id: u64, + pub thread_id: u64, + pub variables_reference: u64, + pub session_id: DebugSessionId, + pub filter: Option, + pub start: Option, + pub count: Option, + pub format: Option, +} + +impl DapCommand for VariablesCommand { + type Response = Vec; + type DapRequest = dap::requests::Variables; + type ProtoRequest = proto::VariablesRequest; + + fn handle_response( + &self, + dap_store: WeakModel, + client_id: &DebugAdapterClientId, + response: Result, + cx: &mut AsyncAppContext, + ) -> Result { + let variables = response?; + + dap_store.update(cx, |this, cx| { + if let Some((downstream_clients, project_id)) = this.downstream_client() { + let update = proto::UpdateDebugAdapter { + project_id: *project_id, + session_id: self.session_id.to_proto(), + client_id: client_id.to_proto(), + thread_id: Some(self.thread_id), + variant: Some(proto::update_debug_adapter::Variant::AddToVariableList( + proto::AddToVariableList { + scope_id: self.scope_id, + stack_frame_id: self.stack_frame_id, + variable_id: self.variables_reference, + variables: variables.to_proto(), + }, + )), + }; + + downstream_clients.send(update.clone()).log_err(); + cx.emit(crate::dap_store::DapStoreEvent::UpdateDebugAdapter(update)); + } + })?; + + Ok(variables) + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn to_dap(&self) -> ::Arguments { + dap::VariablesArguments { + variables_reference: self.variables_reference, + filter: self.filter, + start: self.start, + count: self.count, + format: self.format.clone(), + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.variables) + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest { + proto::VariablesRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.thread_id, + session_id: self.session_id.to_proto(), + stack_frame_id: self.stack_frame_id, + scope_id: self.scope_id, + variables_reference: self.variables_reference, + filter: None, + start: self.start, + count: self.count, + format: None, + } + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + thread_id: request.thread_id, + session_id: DebugSessionId::from_proto(request.session_id), + stack_frame_id: request.stack_frame_id, + scope_id: request.scope_id, + variables_reference: request.variables_reference, + filter: None, + start: request.start, + count: request.count, + format: None, + } + } + + fn response_to_proto( + debug_client_id: &DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapVariables { + client_id: debug_client_id.to_proto(), + variables: message.to_proto(), + } + } + + fn response_from_proto( + self, + message: ::Response, + ) -> Result { + Ok(Vec::from_proto(message.variables)) + } +} + +#[derive(Debug, Clone)] pub(crate) struct RestartStackFrameCommand { pub stack_frame_id: u64, } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 83913b76339350..ebd1fa17ab8d7a 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -2,7 +2,7 @@ use crate::{ dap_command::{ ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, - TerminateCommand, TerminateThreadsCommand, + TerminateCommand, TerminateThreadsCommand, VariablesCommand, }, project_settings::ProjectSettings, ProjectEnvironment, ProjectItem as _, ProjectPath, @@ -17,7 +17,7 @@ use dap::{ requests::{ Attach, Completions, ConfigurationDone, Disconnect, Evaluate, Initialize, Launch, LoadedSources, Modules, Request as _, RunInTerminal, Scopes, SetBreakpoints, SetExpression, - SetVariable, StackTrace, StartDebugging, Terminate, Variables, + SetVariable, StackTrace, StartDebugging, Terminate, }, AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, @@ -26,7 +26,7 @@ use dap::{ ModulesArguments, Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, SetVariableArguments, Source, SourceBreakpoint, StackFrame, StackTraceArguments, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, SteppingGranularity, - TerminateArguments, Variable, VariablesArguments, + TerminateArguments, Variable, }; use dap::{ session::{DebugSession, DebugSessionId}, @@ -158,6 +158,7 @@ impl DapStore { client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); + client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_shutdown_session); } @@ -1265,29 +1266,30 @@ impl DapStore { self.request_dap(client_id, command, cx) } - + #[allow(clippy::too_many_arguments)] pub fn variables( &self, client_id: &DebugAdapterClientId, + thread_id: u64, + stack_frame_id: u64, + scope_id: u64, + session_id: DebugSessionId, variables_reference: u64, cx: &mut ModelContext, ) -> Task>> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + let command = VariablesCommand { + stack_frame_id, + scope_id, + session_id, + thread_id, + variables_reference, + filter: None, + start: None, + count: None, + format: None, }; - cx.background_executor().spawn(async move { - Ok(client - .request::(VariablesArguments { - variables_reference, - filter: None, - start: None, - count: None, - format: None, - }) - .await? - .variables) - }) + self.request_dap(&client_id, command, cx) } pub fn evaluate( diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index d9541d88393cf6..52426ac69e882d 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -329,7 +329,9 @@ message Envelope { DapRestartRequest dap_restart_request = 308; DapShutdownSession dap_shutdown_session = 309; UpdateThreadStatus update_thread_status = 310; - DapRestartStackFrameRequest dap_restart_stack_frame_request = 311; // current max + VariablesRequest variables_request = 311; + DapVariables dap_variables = 312; + DapRestartStackFrameRequest dap_restart_stack_frame_request = 313; // current max } reserved 87 to 88; @@ -2561,14 +2563,6 @@ message DebuggerOpenEntryVariable { uint64 depth = 3; } -message DebuggerVariableListEntry { - oneof entry { - DapScope scope = 1; - VariableListEntrySetState set_variable_editor = 2; - VariableListEntryVariable variable = 3; - } -} - message VariableListEntrySetState { uint64 depth = 1; DebuggerSetVariableState state = 2; @@ -2617,11 +2611,6 @@ message VariableListScopes { repeated DapScope scopes = 2; } -message VariableListEntries { - uint64 stack_frame_id = 1; - repeated DebuggerVariableListEntry entries = 2; -} - message VariableListVariables { uint64 stack_frame_id = 1; uint64 scope_id = 2; @@ -2629,12 +2618,39 @@ message VariableListVariables { } message DebuggerVariableList { - repeated VariableListOpenEntry open_entries = 1; - repeated VariableListScopes scopes = 2; - // Editor set_variable_editor = 3; - DebuggerSetVariableState set_variable_state = 4; - repeated VariableListEntries entries = 5; - repeated VariableListVariables variables = 6; + repeated VariableListScopes scopes = 1; + repeated VariableListVariables variables = 2; + repeated AddToVariableList added_variables = 3; +} + +enum VariablesArgumentsFilter { + Indexed = 0; + Named = 1; +} + +message ValueFormat { + optional bool hex = 1; +} + +message VariablesRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + uint64 session_id = 4; + uint64 stack_frame_id = 5; + uint64 scope_id = 6; + uint64 variables_reference = 7; + optional VariablesArgumentsFilter filter = 8; + optional uint64 start = 9; + optional uint64 count = 10; + optional ValueFormat format = 11; +} + +message AddToVariableList { + uint64 variable_id = 1; + uint64 stack_frame_id = 2; + uint64 scope_id = 3; + repeated DapVariable variables = 4; } message DebuggerStackFrameList { @@ -2791,10 +2807,18 @@ message UpdateDebugAdapter { DebuggerThreadState thread_state = 5; DebuggerStackFrameList stack_frame_list = 6; DebuggerVariableList variable_list = 7; - DebuggerModuleList modules = 8; + AddToVariableList add_to_variable_list = 8; + DebuggerModuleList modules = 9; } } + + +message DapVariables { + uint64 client_id = 1; + repeated DapVariable variables = 2; +} + // Remote Debugging: Dap Types message DapVariable { string name = 1; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index f5985916823564..0af3cbaf4cb548 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -395,6 +395,8 @@ messages!( (UpdateWorktree, Foreground), (UpdateWorktreeSettings, Foreground), (UsersResponse, Foreground), + (VariablesRequest, Background), + (DapVariables, Background), ); request_messages!( @@ -534,6 +536,7 @@ request_messages!( (DapRestartRequest, Ack), (DapRestartStackFrameRequest, Ack), (DapShutdownSession, Ack), + (VariablesRequest, DapVariables) ); entity_messages!( @@ -640,6 +643,7 @@ entity_messages!( DapRestartStackFrameRequest, DapShutdownSession, UpdateThreadStatus, + VariablesRequest, ); entity_messages!( From 675fb2f54b1f7c9e3998ef9c47dc97a368d7850d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 20 Jan 2025 17:37:31 +0100 Subject: [PATCH 477/650] Fix only show restart frame icon for frames that allow it --- crates/debugger_ui/src/stack_frame_list.rs | 69 +++++++++++----------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 75d6babf4595a0..b6d7115c903e75 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -313,39 +313,42 @@ impl StackFrameList { .when_some(source.and_then(|s| s.path), |this, path| this.child(path)), ), ) - .when(supports_frame_restart, |this| { - this.child( - h_flex() - .id(("restart-stack-frame", stack_frame.id)) - .visible_on_hover("") - .absolute() - .right_2() - .overflow_hidden() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().element_selected) - .bg(cx.theme().colors().element_background) - .hover(|style| { - style - .bg(cx.theme().colors().ghost_element_hover) - .cursor_pointer() - }) - .child( - IconButton::new( - ("restart-stack-frame", stack_frame.id), - IconName::DebugRestart, - ) - .icon_size(IconSize::Small) - .on_click(cx.listener({ - let stack_frame_id = stack_frame.id; - move |this, _, cx| { - this.restart_stack_frame(stack_frame_id, cx); - } - })) - .tooltip(move |cx| Tooltip::text("Restart Stack Frame", cx)), - ), - ) - }) + .when( + supports_frame_restart && stack_frame.can_restart.unwrap_or(true), + |this| { + this.child( + h_flex() + .id(("restart-stack-frame", stack_frame.id)) + .visible_on_hover("") + .absolute() + .right_2() + .overflow_hidden() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().element_selected) + .bg(cx.theme().colors().element_background) + .hover(|style| { + style + .bg(cx.theme().colors().ghost_element_hover) + .cursor_pointer() + }) + .child( + IconButton::new( + ("restart-stack-frame", stack_frame.id), + IconName::DebugRestart, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener({ + let stack_frame_id = stack_frame.id; + move |this, _, cx| { + this.restart_stack_frame(stack_frame_id, cx); + } + })) + .tooltip(move |cx| Tooltip::text("Restart Stack Frame", cx)), + ), + ) + }, + ) .into_any() } } From 7b6d20b9ffde2596be15b6ffcb09c8e139d07ff1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 20 Jan 2025 20:34:28 +0100 Subject: [PATCH 478/650] Stack frame presentation hint (#94) * Update dap type to include new presentation hint for stack frames * Implement grouped/collapsed stack frames based on presentation hint * Add test Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --------- Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- Cargo.lock | 2 +- crates/dap/Cargo.toml | 2 +- crates/debugger_ui/src/stack_frame_list.rs | 121 +++++++++- .../debugger_ui/src/tests/stack_frame_list.rs | 228 +++++++++++++++++- 4 files changed, 344 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3809da5f84c32..eda06c000bf634 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3600,7 +3600,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=a6376f02cf9c284c706bc919c21e7ce68ef1fb47#a6376f02cf9c284c706bc919c21e7ce68ef1fb47" +source = "git+https://github.com/zed-industries/dap-types?rev=f44d7d8af3a8af3e0ca09933271bee17c99e15b1#f44d7d8af3a8af3e0ca09933271bee17c99e15b1" dependencies = [ "schemars", "serde", diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index 9f1161c9589adc..b4c03759718a06 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -25,7 +25,7 @@ async-tar.workspace = true async-trait.workspace = true client.workspace = true collections.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "a6376f02cf9c284c706bc919c21e7ce68ef1fb47" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "f44d7d8af3a8af3e0ca09933271bee17c99e15b1" } fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index b6d7115c903e75..15f6f9beac82be 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -30,16 +30,23 @@ pub struct StackFrameList { thread_id: u64, list: ListState, focus_handle: FocusHandle, + session_id: DebugSessionId, dap_store: Model, current_stack_frame_id: u64, stack_frames: Vec, + entries: Vec, workspace: WeakView, client_id: DebugAdapterClientId, - session_id: DebugSessionId, _subscriptions: Vec, fetch_stack_frames_task: Option>>, } +#[derive(Debug, PartialEq, Eq)] +pub enum StackFrameEntry { + Normal(StackFrame), + Collapsed(Vec), +} + impl StackFrameList { pub fn new( workspace: &WeakView, @@ -70,6 +77,7 @@ impl StackFrameList { _subscriptions, client_id: *client_id, session_id: *session_id, + entries: Default::default(), workspace: workspace.clone(), dap_store: dap_store.clone(), fetch_stack_frames_task: None, @@ -100,11 +108,16 @@ impl StackFrameList { self.client_id = DebugAdapterClientId::from_proto(stack_frame_list.client_id); self.current_stack_frame_id = stack_frame_list.current_stack_frame; self.stack_frames = Vec::from_proto(stack_frame_list.stack_frames); - self.list.reset(self.stack_frames.len()); + self.build_entries(); cx.notify(); } + #[cfg(any(test, feature = "test-support"))] + pub fn entries(&self) -> &Vec { + &self.entries + } + pub fn stack_frames(&self) -> &Vec { &self.stack_frames } @@ -138,6 +151,35 @@ impl StackFrameList { self.fetch_stack_frames(true, cx); } + fn build_entries(&mut self) { + let mut entries = Vec::new(); + let mut collapsed_entries = Vec::new(); + + for stack_frame in &self.stack_frames { + match stack_frame.presentation_hint { + Some(dap::StackFramePresentationHint::Deemphasize) => { + collapsed_entries.push(stack_frame.clone()); + } + _ => { + let collapsed_entries = std::mem::take(&mut collapsed_entries); + if !collapsed_entries.is_empty() { + entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); + } + + entries.push(StackFrameEntry::Normal(stack_frame.clone())); + } + } + } + + let collapsed_entries = std::mem::take(&mut collapsed_entries); + if !collapsed_entries.is_empty() { + entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); + } + + std::mem::swap(&mut self.entries, &mut entries); + self.list.reset(self.entries.len()); + } + fn fetch_stack_frames(&mut self, go_to_stack_frame: bool, cx: &mut ViewContext) { // If this is a remote debug session we never need to fetch stack frames ourselves // because the host will fetch and send us stack frames whenever there's a stop event @@ -155,7 +197,7 @@ impl StackFrameList { let task = this.update(&mut cx, |this, cx| { std::mem::swap(&mut this.stack_frames, &mut stack_frames); - this.list.reset(this.stack_frames.len()); + this.build_entries(); cx.emit(StackFrameListEvent::StackFramesUpdated); @@ -254,9 +296,11 @@ impl StackFrameList { }); } - fn render_entry(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let stack_frame = &self.stack_frames[ix]; - + fn render_normal_entry( + &self, + stack_frame: &StackFrame, + cx: &mut ViewContext, + ) -> AnyElement { let source = stack_frame.source.clone(); let is_selected_frame = stack_frame.id == self.current_stack_frame_id; @@ -351,6 +395,71 @@ impl StackFrameList { ) .into_any() } + + pub fn expand_collapsed_entry( + &mut self, + ix: usize, + stack_frames: &Vec, + cx: &mut ViewContext, + ) { + self.entries.splice( + ix..ix + 1, + stack_frames + .iter() + .map(|frame| StackFrameEntry::Normal(frame.clone())), + ); + self.list.reset(self.entries.len()); + cx.notify(); + } + + fn render_collapsed_entry( + &self, + ix: usize, + stack_frames: &Vec, + cx: &mut ViewContext, + ) -> AnyElement { + let first_stack_frame = &stack_frames[0]; + + h_flex() + .rounded_md() + .justify_between() + .w_full() + .group("") + .id(("stack-frame", first_stack_frame.id)) + .p_1() + .on_click(cx.listener({ + let stack_frames = stack_frames.clone(); + move |this, _, cx| { + this.expand_collapsed_entry(ix, &stack_frames, cx); + } + })) + .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer()) + .child( + v_flex() + .text_ui_sm(cx) + .truncate() + .text_color(cx.theme().colors().text_muted) + .child(format!( + "Show {} more{}", + stack_frames.len(), + first_stack_frame + .source + .as_ref() + .and_then(|source| source.origin.as_ref()) + .map_or(String::new(), |origin| format!(": {}", origin)) + )), + ) + .into_any() + } + + fn render_entry(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + match &self.entries[ix] { + StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx), + StackFrameEntry::Collapsed(stack_frames) => { + self.render_collapsed_entry(ix, stack_frames, cx) + } + } + } } impl Render for StackFrameList { diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 220cb4184eacd8..415878bb476a6f 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -1,6 +1,7 @@ use crate::{ debugger_panel::DebugPanel, - tests::{init_test, init_test_workspace}, + stack_frame_list::StackFrameEntry, + tests::{active_debug_panel_item, init_test, init_test_workspace}, }; use dap::{ requests::{Disconnect, Initialize, Launch, StackTrace}, @@ -426,3 +427,228 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + import { SOME_VALUE } './module.js'; + + console.log(SOME_VALUE); + "# + .unindent(); + + let module_file_content = r#" + export SOME_VALUE = 'some value'; + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + "module.js": module_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![ + StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + StackFrame { + id: 2, + name: "Stack Frame 2".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: Some("ignored".into()), + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), + }, + StackFrame { + id: 3, + name: "Stack Frame 3".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: Some("ignored".into()), + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize), + }, + StackFrame { + id: 4, + name: "Stack Frame 4".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + ]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + debug_panel_item + .stack_frame_list() + .update(cx, |stack_frame_list, cx| { + assert_eq!( + &vec![ + StackFrameEntry::Normal(stack_frames[0].clone()), + StackFrameEntry::Collapsed(vec![ + stack_frames[1].clone(), + stack_frames[2].clone() + ]), + StackFrameEntry::Normal(stack_frames[3].clone()), + ], + stack_frame_list.entries() + ); + + stack_frame_list.expand_collapsed_entry( + 1, + &vec![stack_frames[1].clone(), stack_frames[2].clone()], + cx, + ); + + assert_eq!( + &vec![ + StackFrameEntry::Normal(stack_frames[0].clone()), + StackFrameEntry::Normal(stack_frames[1].clone()), + StackFrameEntry::Normal(stack_frames[2].clone()), + StackFrameEntry::Normal(stack_frames[3].clone()), + ], + stack_frame_list.entries() + ); + }); + }); + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} From e1392c97171f5f8bb2a8d92997034d10670ffffe Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 20 Jan 2025 21:17:06 +0100 Subject: [PATCH 479/650] Allow sending breakpoint request for unopened buffers So before this change, if you have save breakpoints feature enabled. You had to have the buffer open for the breakpoint to be send after you launched Zed again. Because we needed to have a buffer to determine the right position based on a anchor. This changes that so we fallback the cached position (last known), that we stored in the DB. So it could be that the file has changed on disk without Zed knowning. But it's better then not sending the breakpoints at all Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/project/src/dap_store.rs | 17 ++++++++++------- crates/project/src/project.rs | 15 ++++----------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index ebd1fa17ab8d7a..17bd3f8187baed 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1900,7 +1900,7 @@ impl DapStore { &self, project_path: &ProjectPath, absolute_path: PathBuf, - buffer_snapshot: BufferSnapshot, + buffer_snapshot: Option, source_changed: bool, cx: &ModelContext, ) -> Task> { @@ -1914,7 +1914,7 @@ impl DapStore { .cloned() .unwrap_or_default() .iter() - .map(|bp| bp.source_for_snapshot(&buffer_snapshot)) + .map(|bp| bp.source_for_snapshot(buffer_snapshot.as_ref())) .collect::>(); let mut tasks = Vec::new(); @@ -2091,11 +2091,14 @@ impl Breakpoint { .unwrap_or(Point::new(self.cached_position, 0)) } - pub fn source_for_snapshot(&self, snapshot: &BufferSnapshot) -> SourceBreakpoint { - let line = self - .active_position - .map(|position| snapshot.summary_for_anchor::(&position).row) - .unwrap_or(self.cached_position) as u64; + pub fn source_for_snapshot(&self, snapshot: Option<&BufferSnapshot>) -> SourceBreakpoint { + let line = match snapshot { + Some(snapshot) => self + .active_position + .map(|position| snapshot.summary_for_anchor::(&position).row) + .unwrap_or(self.cached_position) as u64, + None => self.cached_position as u64, + }; let log_message = match &self.kind { BreakpointKind::Standard => None, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2e79e14027a141..5e721034835cbc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2544,18 +2544,11 @@ impl Project { } => { cx.notify(); // so the UI updates - let buffer_id = self + let buffer_snapshot = self .buffer_store .read(cx) - .buffer_id_for_project_path(&project_path); - - let Some(buffer_id) = buffer_id else { - return; - }; - - let Some(buffer) = self.buffer_for_id(*buffer_id, cx) else { - return; - }; + .get_by_path(&project_path, cx) + .map(|buffer| buffer.read(cx).snapshot()); let Some(absolute_path) = self.absolute_path(project_path, cx) else { return; @@ -2566,7 +2559,7 @@ impl Project { .send_changed_breakpoints( project_path, absolute_path, - buffer.read(cx).snapshot(), + buffer_snapshot, *source_changed, cx, ) From 647f411b10d05cf4f11c01210e593cec6241f3aa Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 20 Jan 2025 21:17:30 +0100 Subject: [PATCH 480/650] Add test for sending breakpoints for unopened buffers Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- .../debugger_ui/src/tests/debugger_panel.rs | 157 +++++++++++++++++- 1 file changed, 153 insertions(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index ea925bc7ba9af6..54ded0d3114905 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -14,11 +14,17 @@ use editor::{ Editor, EditorMode, MultiBuffer, }; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; -use project::{FakeFs, Project}; +use project::{ + dap_store::{Breakpoint, BreakpointEditAction, BreakpointKind}, + FakeFs, Project, +}; use serde_json::json; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, +use std::{ + path::Path, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, }; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use tests::{active_debug_panel_item, init_test, init_test_workspace}; @@ -1151,3 +1157,146 @@ async fn test_send_breakpoints_when_editor_has_been_saved( shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + fs.insert_tree( + "/a", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let worktree_id = workspace + .update(cx, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }) + .unwrap(); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/main.rs", args.source.path.unwrap()); + assert_eq!( + vec![SourceBreakpoint { + line: 2, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + }], + args.breakpoints.unwrap() + ); + assert!(!args.source_modified.unwrap()); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + // add breakpoint for file/buffer that has not been opened yet + project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.toggle_breakpoint_for_buffer( + &project::ProjectPath { + worktree_id, + path: Arc::from(Path::new(&"main.rs")), + }, + Breakpoint { + active_position: None, + cached_position: 1, + kind: BreakpointKind::Standard, + }, + BreakpointEditAction::Toggle, + cx, + ); + }); + }); + + cx.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called for unopened buffers" + ); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} From 9bca023b1ab1ccf51509e51cf899d9ce09963c2d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 20 Jan 2025 21:31:41 +0100 Subject: [PATCH 481/650] Shutdown debug session when launch/attach fails Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- crates/debugger_ui/src/debugger_panel.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index fee94a9326da4e..ec343930ef45e2 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -582,7 +582,7 @@ impl DebugPanel { task.await?; - match request_type { + let result = match request_type { DebugRequestType::Launch => { let task = this.update(&mut cx, |this, cx| { this.dap_store @@ -615,7 +615,23 @@ impl DebugPanel { })? } } + }; + + if result.is_err() { + this.update(&mut cx, |debug_panel, cx| { + debug_panel.dap_store.update(cx, |store, cx| { + cx.emit(DapStoreEvent::Notification( + "Failed to start debug session".into(), + )); + + store + .shutdown_session(&session_id, cx) + .detach_and_log_err(cx); + }); + })?; } + + result }) .detach_and_log_err(cx); } From 5a52c7e21fcdf3b5efe93025bb22beae062fb80d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 20 Jan 2025 21:32:07 +0100 Subject: [PATCH 482/650] Add test to for shutdown on launch/attach failure Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> --- .../debugger_ui/src/tests/debugger_panel.rs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 54ded0d3114905..27e027f863e97d 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1300,3 +1300,82 @@ async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, [], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + let session_id = cx.update(|cx| session.read(cx).id()); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client + .on_request::(move |_, _| { + Err(ErrorResponse { + error: Some(dap::Message { + id: 1, + format: "error".into(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + }) + }) + .await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + cx.run_until_parked(); + + project.update(cx, |project, cx| { + assert!(project + .dap_store() + .read(cx) + .session_by_id(&session_id) + .is_none()); + }); +} From 953843acddbe68f4d11a9a96612df52e4d2b9062 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 21 Jan 2025 12:38:36 +0100 Subject: [PATCH 483/650] Add stack frame source origin to tooltip --- crates/debugger_ui/src/stack_frame_list.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 15f6f9beac82be..d4ff83682501c6 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -317,6 +317,11 @@ impl StackFrameList { .supports_restart_frame .unwrap_or_default(); + let origin = stack_frame + .source + .to_owned() + .and_then(|source| source.origin); + h_flex() .rounded_md() .justify_between() @@ -325,7 +330,18 @@ impl StackFrameList { .id(("stack-frame", stack_frame.id)) .tooltip({ let formatted_path = formatted_path.clone(); - move |cx| Tooltip::text(formatted_path.clone(), cx) + move |cx| { + cx.new_view(|_| { + let mut tooltip = Tooltip::new(formatted_path.clone()); + + if let Some(origin) = &origin { + tooltip = tooltip.meta(origin); + } + + tooltip + }) + .into() + } }) .p_1() .when(is_selected_frame, |this| { From 8ceb115f3cc7953a9700499ca47b591f83ec3b8e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 21 Jan 2025 16:50:41 +0100 Subject: [PATCH 484/650] Fix typo in docs --- docs/src/debugger.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/debugger.md b/docs/src/debugger.md index 0b580341f960c8..dc08421f266caa 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -17,8 +17,8 @@ To debug a program using Zed you must first create a debug configuration within "request": "launch", // cwd: defaults to the current working directory of your project // The current working directory to start the debugger from - // accepts zed task variables e.g. $ZED_WORKPLACE_ROOT - "cwd": "$ZED_WORKPLACE_ROOT", + // accepts zed task variables e.g. $ZED_WORKTREE_ROOT + "cwd": "$ZED_WORKTREE_ROOT", // program: The program to debug // accepts zed task variables "program": "path_to_program", From a89e29554f9d80085f827d11fbcfe5249cc88573 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 21 Jan 2025 17:48:53 +0100 Subject: [PATCH 485/650] Rename `server` to `adapter` inside the dap log --- crates/debugger_tools/src/dap_log.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 8bb58529d7a4fb..c315a12384833c 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -102,10 +102,10 @@ impl LogStore { fn new(cx: &ModelContext) -> Self { let (rpc_tx, mut rpc_rx) = unbounded::<(DebugAdapterClientId, IoKind, String)>(); cx.spawn(|this, mut cx| async move { - while let Some((server_id, io_kind, message)) = rpc_rx.next().await { + while let Some((client_id, io_kind, message)) = rpc_rx.next().await { if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { - this.on_rpc_log(server_id, io_kind, &message, cx); + this.on_rpc_log(client_id, io_kind, &message, cx); })?; } @@ -118,10 +118,10 @@ impl LogStore { let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(DebugAdapterClientId, IoKind, String)>(); cx.spawn(|this, mut cx| async move { - while let Some((server_id, io_kind, message)) = adapter_log_rx.next().await { + while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await { if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { - this.on_adapter_log(server_id, io_kind, &message, cx); + this.on_adapter_log(client_id, io_kind, &message, cx); })?; } @@ -387,7 +387,7 @@ impl Render for DapLogToolbarItemView { let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") .anchor(gpui::Corner::TopLeft) .trigger(Button::new( - "debug_server_menu_header", + "debug_client_menu_header", current_client .map(|sub_item| { Cow::Owned(format!( @@ -400,7 +400,7 @@ impl Render for DapLogToolbarItemView { } )) }) - .unwrap_or_else(|| "No server selected".into()), + .unwrap_or_else(|| "No adapter selected".into()), )) .menu(move |cx| { let log_view = log_view.clone(); @@ -434,7 +434,7 @@ impl Render for DapLogToolbarItemView { .into_any_element() }, cx.handler_for(&log_view, move |view, cx| { - view.show_log_messages_for_server(sub_item.client_id, cx); + view.show_log_messages_for_adapter(sub_item.client_id, cx); }), ); } @@ -646,7 +646,7 @@ impl DapLogView { cx.focus(&self.focus_handle); } - fn show_log_messages_for_server( + fn show_log_messages_for_adapter( &mut self, client_id: DebugAdapterClientId, cx: &mut ViewContext, @@ -711,7 +711,7 @@ impl Render for DapLogView { } } -actions!(debug, [OpenDebuggerServerLogs]); +actions!(debug, [OpenDebuggerAdapterLogs]); pub fn init(cx: &mut AppContext) { let log_store = cx.new_model(|cx| LogStore::new(cx)); @@ -725,7 +725,7 @@ pub fn init(cx: &mut AppContext) { } let log_store = log_store.clone(); - workspace.register_action(move |workspace, _: &OpenDebuggerServerLogs, cx| { + workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, cx| { let project = workspace.project().read(cx); if project.is_local() { workspace.add_item_to_active_pane( From 56abc60a343b27b3c33cfbe4aaaaa3e1d0353698 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:53:33 -0500 Subject: [PATCH 486/650] Collab - Toggle Ignore Breakpoints (#93) * Create collab ignore breakpoint integration test * Add collab ignore breakpoints message handlers Still need to enable remote dap_stores the ability to store/manage ignore breakpoint state * Refactor session to have remote and local modes This was done to allow remote clients access to some session details they need such as ignore breakpoints. Co-authored-by: Remco Smits * DapStore so both local & remote modes have access to DebugSessions * Add remote sessions when creating new debug panel items * Finish implementing collab breakpoints ignore * Clippy & clean up * Clean up session information when sessions end on remote clients * Rename proto message * Add ignore breakpoints state to collab db --------- Co-authored-by: Remco Smits --- .../20221109000000_test_schema.sql | 1 + .../20250121181012_add_ignore_breakpoints.sql | 2 + crates/collab/src/db/queries/projects.rs | 39 ++ crates/collab/src/db/tables/debug_clients.rs | 1 + crates/collab/src/rpc.rs | 29 ++ crates/collab/src/tests/debug_panel_tests.rs | 492 ++++++++++++++++++ crates/dap/src/session.rs | 117 +++-- crates/debugger_tools/src/dap_log.rs | 41 +- crates/debugger_ui/src/debugger_panel.rs | 19 +- crates/debugger_ui/src/debugger_panel_item.rs | 6 + .../debugger_ui/src/tests/debugger_panel.rs | 4 +- crates/project/src/dap_store.rs | 237 +++++++-- crates/project/src/project.rs | 44 ++ crates/proto/proto/zed.proto | 25 +- crates/proto/src/proto.rs | 6 + 15 files changed, 974 insertions(+), 89 deletions(-) create mode 100644 crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 5a065878af4ab7..c36207514aed29 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -474,6 +474,7 @@ CREATE TABLE IF NOT EXISTS "debug_clients" ( project_id INTEGER NOT NULL, session_id BIGINT NOT NULL, capabilities INTEGER NOT NULL, + ignore_breakpoints BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY (id, project_id, session_id), FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE ); diff --git a/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql b/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql new file mode 100644 index 00000000000000..e1e362c5cf84ad --- /dev/null +++ b/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql @@ -0,0 +1,2 @@ + +ALTER TABLE debug_clients ADD COLUMN ignore_breakpoints BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index da0d99432be96a..b25c754d83c7a6 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -556,6 +556,40 @@ impl Database { .await } + pub async fn ignore_breakpoint_state( + &self, + connection_id: ConnectionId, + update: &proto::IgnoreBreakpointState, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + self.project_transaction(project_id, |tx| async move { + let debug_clients = debug_clients::Entity::find() + .filter( + Condition::all() + .add(debug_clients::Column::ProjectId.eq(project_id)) + .add(debug_clients::Column::SessionId.eq(update.session_id)), + ) + .all(&*tx) + .await?; + + for debug_client in debug_clients { + debug_clients::Entity::update(debug_clients::ActiveModel { + id: ActiveValue::Unchanged(debug_client.id), + project_id: ActiveValue::Unchanged(debug_client.project_id), + session_id: ActiveValue::Unchanged(debug_client.session_id), + capabilities: ActiveValue::Unchanged(debug_client.capabilities), + ignore_breakpoints: ActiveValue::Set(update.ignore), + }) + .exec(&*tx) + .await?; + } + + self.internal_project_connection_ids(project_id, connection_id, true, &tx) + .await + }) + .await + } + pub async fn update_debug_adapter( &self, connection_id: ConnectionId, @@ -647,6 +681,7 @@ impl Database { project_id: ActiveValue::Set(project_id), session_id: ActiveValue::Set(update.session_id as i64), capabilities: ActiveValue::Set(0), + ignore_breakpoints: ActiveValue::Set(false), }; new_debug_client.insert(&*tx).await?; } @@ -729,6 +764,7 @@ impl Database { project_id: ActiveValue::Set(project_id), session_id: ActiveValue::Set(update.session_id as i64), capabilities: ActiveValue::Set(0), + ignore_breakpoints: ActiveValue::Set(false), }; debug_client = Some(new_debug_client.insert(&*tx).await?); } @@ -742,6 +778,7 @@ impl Database { project_id: ActiveValue::Unchanged(debug_client.project_id), session_id: ActiveValue::Unchanged(debug_client.session_id), capabilities: ActiveValue::Set(debug_client.capabilities), + ignore_breakpoints: ActiveValue::Set(debug_client.ignore_breakpoints), }) .exec(&*tx) .await?; @@ -1086,6 +1123,7 @@ impl Database { for (session_id, clients) in debug_sessions.into_iter() { let mut debug_clients = Vec::default(); + let ignore_breakpoints = clients.iter().any(|debug| debug.ignore_breakpoints); // Temp solution until client -> session change for debug_client in clients.into_iter() { let debug_panel_items = debug_client @@ -1108,6 +1146,7 @@ impl Database { project_id, session_id: session_id as u64, clients: debug_clients, + ignore_breakpoints, }); } diff --git a/crates/collab/src/db/tables/debug_clients.rs b/crates/collab/src/db/tables/debug_clients.rs index 02758acaa0c4fa..498b9ed7359091 100644 --- a/crates/collab/src/db/tables/debug_clients.rs +++ b/crates/collab/src/db/tables/debug_clients.rs @@ -25,6 +25,7 @@ pub struct Model { pub session_id: i64, #[sea_orm(column_type = "Integer")] pub capabilities: i32, + pub ignore_breakpoints: bool, } impl Model { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index cf7083a21c1a99..fa116c8cc54938 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -438,6 +438,13 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_message_handler( broadcast_project_message_from_host::, + ) + .add_message_handler( + broadcast_project_message_from_host::, + ) + .add_message_handler(ignore_breakpoint_state) + .add_message_handler( + broadcast_project_message_from_host::, ); Arc::new(server) @@ -2155,6 +2162,28 @@ async fn shutdown_debug_client( Ok(()) } +async fn ignore_breakpoint_state( + request: proto::IgnoreBreakpointState, + session: Session, +) -> Result<()> { + let guest_connection_ids = session + .db() + .await + .ignore_breakpoint_state(session.connection_id, &request) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, request.clone()) + }, + ); + Ok(()) +} + /// Notify other participants that a debug panel item has been updated async fn update_debug_adapter(request: proto::UpdateDebugAdapter, session: Session) -> Result<()> { let guest_connection_ids = session diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 245afc0eea24ef..ff2ea023c4b660 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1850,3 +1850,495 @@ async fn test_variable_list( shutdown_client.await.unwrap(); } + +#[gpui::test] +async fn test_ignore_breakpoints( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "test.txt": "one\ntwo\nthree\nfour\nfive", + }), + ) + .await; + + init_test(cx_a); + init_test(cx_b); + init_test(cx_c); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_path = ProjectPath { + worktree_id, + path: Arc::from(Path::new(&"test.txt")), + }; + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let local_editor = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + local_editor.update(cx_a, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 2 + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 3 + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + let client_id = client.id(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_configuration_done_request: Some(true), + ..Default::default() + }) + }) + .await; + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + + let mut actual_breakpoints = args.breakpoints.unwrap(); + actual_breakpoints.sort_by_key(|b| b.line); + + let expected_breakpoints = vec![ + SourceBreakpoint { + line: 2, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None, + }, + SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None, + }, + ]; + + assert_eq!(actual_breakpoints, expected_breakpoints); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Initialized(Some( + dap::Capabilities { + supports_configuration_done_request: Some(true), + ..Default::default() + }, + ))) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called when starting debug session" + ); + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let remote_debug_item = workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + + let session_id = debug_panel.update(cx, |this, cx| { + this.dap_store() + .read(cx) + .as_remote() + .unwrap() + .session_by_client_id(&client.id()) + .unwrap() + .read(cx) + .id() + }); + + let breakpoints_ignored = active_debug_panel_item.read(cx).are_breakpoints_ignored(cx); + + assert_eq!(session_id, active_debug_panel_item.read(cx).session_id()); + assert_eq!(false, breakpoints_ignored); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + called_set_breakpoints.store(false, Ordering::SeqCst); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!(args.breakpoints, Some(vec![])); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + let local_debug_item = workspace_a.update(cx_a, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + + assert_eq!( + false, + active_debug_panel_item.read(cx).are_breakpoints_ignored(cx) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + + active_debug_panel_item + }); + + local_debug_item.update(cx_a, |item, cx| { + item.toggle_ignore_breakpoints(cx); // Set to true + assert_eq!(true, item.are_breakpoints_ignored(cx)); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called to ignore breakpoints" + ); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, _args| { + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + let remote_editor = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + called_set_breakpoints.store(false, std::sync::atomic::Ordering::SeqCst); + + remote_editor.update(cx_b, |editor, cx| { + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 1 + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request be called whenever breakpoints are toggled but with not breakpoints" + ); + + remote_debug_item.update(cx_b, |debug_panel, cx| { + let breakpoints_ignored = debug_panel.are_breakpoints_ignored(cx); + + assert_eq!(true, breakpoints_ignored); + assert_eq!(client.id(), debug_panel.client_id()); + assert_eq!(1, debug_panel.thread_id()); + }); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + + let mut actual_breakpoints = args.breakpoints.unwrap(); + actual_breakpoints.sort_by_key(|b| b.line); + + let expected_breakpoints = vec![ + SourceBreakpoint { + line: 1, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None, + }, + SourceBreakpoint { + line: 2, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None, + }, + SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None, + }, + ]; + + assert_eq!(actual_breakpoints, expected_breakpoints); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + let project_c = client_c.join_remote_project(project_id, cx_c).await; + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + add_debugger_panel(&workspace_c, cx_c).await; + + let last_join_remote_item = workspace_c.update(cx_c, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + let breakpoints_ignored = active_debug_panel_item.read(cx).are_breakpoints_ignored(cx); + + assert_eq!(true, breakpoints_ignored); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + remote_debug_item.update(cx_b, |item, cx| { + item.toggle_ignore_breakpoints(cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + cx_c.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request should be called to update breakpoints" + ); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!(args.breakpoints, Some(vec![])); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + local_debug_item.update(cx_a, |debug_panel_item, cx| { + assert_eq!( + false, + debug_panel_item.are_breakpoints_ignored(cx), + "Remote client set this to false" + ); + }); + + remote_debug_item.update(cx_b, |debug_panel_item, cx| { + assert_eq!( + false, + debug_panel_item.are_breakpoints_ignored(cx), + "Remote client set this to false" + ); + }); + + last_join_remote_item.update(cx_c, |debug_panel_item, cx| { + assert_eq!( + false, + debug_panel_item.are_breakpoints_ignored(cx), + "Remote client set this to false" + ); + }); + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + project_b.update(cx_b, |project, cx| { + project.dap_store().update(cx, |dap_store, _cx| { + let sessions = dap_store.sessions().collect::>(); + + assert_eq!( + None, + dap_store.session_by_client_id(&client_id), + "No client_id to session mapping should exist after shutdown" + ); + assert_eq!( + 0, + sessions.len(), + "No sessions should be left after shutdown" + ); + }) + }); + + project_c.update(cx_c, |project, cx| { + project.dap_store().update(cx, |dap_store, _cx| { + let sessions = dap_store.sessions().collect::>(); + + assert_eq!( + None, + dap_store.session_by_client_id(&client_id), + "No client_id to session mapping should exist after shutdown" + ); + assert_eq!( + 0, + sessions.len(), + "No sessions should be left after shutdown" + ); + }) + }); +} diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs index 77e07a76a526e0..745b0ec5244f53 100644 --- a/crates/dap/src/session.rs +++ b/crates/dap/src/session.rs @@ -19,54 +19,37 @@ impl DebugSessionId { } } -pub struct DebugSession { +pub enum DebugSession { + Local(LocalDebugSession), + Remote(RemoteDebugSession), +} + +pub struct LocalDebugSession { id: DebugSessionId, ignore_breakpoints: bool, configuration: DebugAdapterConfig, clients: HashMap>, } -impl DebugSession { - pub fn new(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { - Self { - id, - configuration, - ignore_breakpoints: false, - clients: HashMap::default(), - } - } - - pub fn id(&self) -> DebugSessionId { - self.id - } - - pub fn name(&self) -> String { - self.configuration.label.clone() - } - +impl LocalDebugSession { pub fn configuration(&self) -> &DebugAdapterConfig { &self.configuration } - pub fn ignore_breakpoints(&self) -> bool { - self.ignore_breakpoints - } - - pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut ModelContext) { - self.ignore_breakpoints = ignore; - cx.notify(); - } - pub fn update_configuration( &mut self, f: impl FnOnce(&mut DebugAdapterConfig), - cx: &mut ModelContext, + cx: &mut ModelContext, ) { f(&mut self.configuration); cx.notify(); } - pub fn add_client(&mut self, client: Arc, cx: &mut ModelContext) { + pub fn add_client( + &mut self, + client: Arc, + cx: &mut ModelContext, + ) { self.clients.insert(client.id(), client); cx.notify(); } @@ -74,7 +57,7 @@ impl DebugSession { pub fn remove_client( &mut self, client_id: &DebugAdapterClientId, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Option> { let client = self.clients.remove(client_id); cx.notify(); @@ -101,4 +84,76 @@ impl DebugSession { pub fn client_ids(&self) -> impl Iterator + '_ { self.clients.keys().cloned() } + + pub fn id(&self) -> DebugSessionId { + self.id + } +} + +pub struct RemoteDebugSession { + id: DebugSessionId, + ignore_breakpoints: bool, + label: String, +} + +impl DebugSession { + pub fn new_local(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { + Self::Local(LocalDebugSession { + id, + ignore_breakpoints: false, + configuration, + clients: HashMap::default(), + }) + } + + pub fn as_local(&self) -> Option<&LocalDebugSession> { + match self { + DebugSession::Local(local) => Some(local), + _ => None, + } + } + + pub fn as_local_mut(&mut self) -> Option<&mut LocalDebugSession> { + match self { + DebugSession::Local(local) => Some(local), + _ => None, + } + } + + pub fn new_remote(id: DebugSessionId, label: String, ignore_breakpoints: bool) -> Self { + Self::Remote(RemoteDebugSession { + id, + label: label.clone(), + ignore_breakpoints, + }) + } + + pub fn id(&self) -> DebugSessionId { + match self { + DebugSession::Local(local) => local.id, + DebugSession::Remote(remote) => remote.id, + } + } + + pub fn name(&self) -> String { + match self { + DebugSession::Local(local) => local.configuration.label.clone(), + DebugSession::Remote(remote) => remote.label.clone(), + } + } + + pub fn ignore_breakpoints(&self) -> bool { + match self { + DebugSession::Local(local) => local.ignore_breakpoints, + DebugSession::Remote(remote) => remote.ignore_breakpoints, + } + } + + pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut ModelContext) { + match self { + DebugSession::Local(local) => local.ignore_breakpoints = ignore, + DebugSession::Remote(remote) => remote.ignore_breakpoints = ignore, + } + cx.notify(); + } } diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index c315a12384833c..b111ecb1f8bb93 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -580,25 +580,28 @@ impl DapLogView { .dap_store() .read(cx) .sessions() - .map(|session| DapMenuItem { - session_id: session.read(cx).id(), - session_name: session.read(cx).name(), - clients: { - let mut clients = session - .read(cx) - .clients() - .map(|client| DapMenuSubItem { - client_id: client.id(), - client_name: client.adapter_id(), - has_adapter_logs: client.has_adapter_logs(), - selected_entry: self - .current_view - .map_or(LogKind::Adapter, |(_, kind)| kind), - }) - .collect::>(); - clients.sort_by_key(|item| item.client_id.0); - clients - }, + .filter_map(|session| { + Some(DapMenuItem { + session_id: session.read(cx).id(), + session_name: session.read(cx).name(), + clients: { + let mut clients = session + .read(cx) + .as_local()? + .clients() + .map(|client| DapMenuSubItem { + client_id: client.id(), + client_name: client.adapter_id(), + has_adapter_logs: client.has_adapter_logs(), + selected_entry: self + .current_view + .map_or(LogKind::Adapter, |(_, kind)| kind), + }) + .collect::>(); + clients.sort_by_key(|item| item.client_id.0); + clients + }, + }) }) .collect::>(); menu_items.sort_by_key(|item| item.session_id.0); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index ec343930ef45e2..9d588cd06e8138 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -292,6 +292,11 @@ impl DebugPanel { &self.message_queue } + #[cfg(any(test, feature = "test-support"))] + pub fn dap_store(&self) -> Model { + self.dap_store.clone() + } + pub fn active_debug_panel_item( &self, cx: &mut ViewContext, @@ -565,14 +570,19 @@ impl DebugPanel { client_id: &DebugAdapterClientId, cx: &mut ViewContext, ) { - let Some(session) = self.dap_store.read(cx).session_by_id(session_id) else { + let Some(session) = self + .dap_store + .read(cx) + .session_by_id(session_id) + .and_then(|session| session.read(cx).as_local()) + else { return; }; let session_id = *session_id; let client_id = *client_id; let workspace = self.workspace.clone(); - let request_type = session.read(cx).configuration().request.clone(); + let request_type = session.configuration().request.clone(); cx.spawn(|this, mut cx| async move { let task = this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { @@ -1030,6 +1040,11 @@ impl DebugPanel { ) }); + self.dap_store.update(cx, |dap_store, cx| { + dap_store.add_remote_session(session_id, None, cx); + dap_store.add_client_to_session(session_id, client_id); + }); + pane.add_item(Box::new(debug_panel_item.clone()), true, true, None, cx); debug_panel_item }); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 35ee5957f41737..a645578f3fda1f 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -506,6 +506,12 @@ impl DebugPanelItem { &self.thread_state } + #[cfg(any(test, feature = "test-support"))] + pub fn are_breakpoints_ignored(&self, cx: &AppContext) -> bool { + self.dap_store + .read_with(cx, |dap, cx| dap.ignore_breakpoints(&self.session_id, cx)) + } + pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { self.dap_store.read(cx).capabilities_by_id(&self.client_id) } diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 27e027f863e97d..411453e3189f40 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -713,7 +713,7 @@ async fn test_handle_start_debugging_reverse_request( cx.run_until_parked(); project.update(cx, |_, cx| { - assert_eq!(2, session.read(cx).clients_len()); + assert_eq!(2, session.read(cx).as_local().unwrap().clients_len()); }); assert!( send_response.load(std::sync::atomic::Ordering::SeqCst), @@ -723,6 +723,8 @@ async fn test_handle_start_debugging_reverse_request( let second_client = project.update(cx, |_, cx| { session .read(cx) + .as_local() + .unwrap() .client_by_id(&DebugAdapterClientId(1)) .unwrap() }); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 17bd3f8187baed..632d4a8c7317cc 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -36,7 +36,9 @@ use dap_adapters::build_adapter; use fs::Fs; use futures::future::Shared; use futures::FutureExt; -use gpui::{AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task}; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task, +}; use http_client::HttpClient; use language::{ proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, @@ -122,9 +124,22 @@ impl LocalDapStore { pub struct RemoteDapStore { upstream_client: Option, upstream_project_id: u64, + sessions: HashMap>, + client_by_session: HashMap, event_queue: Option>, } +impl RemoteDapStore { + pub fn session_by_client_id( + &self, + client_id: &DebugAdapterClientId, + ) -> Option> { + self.client_by_session + .get(client_id) + .and_then(|session_id| self.sessions.get(session_id).cloned()) + } +} + pub struct DapStore { mode: DapStoreMode, downstream_client: Option<(AnyProtoClient, u64)>, @@ -147,6 +162,8 @@ impl DapStore { client.add_model_message_handler(DapStore::handle_synchronize_breakpoints); client.add_model_message_handler(DapStore::handle_update_debug_adapter); client.add_model_message_handler(DapStore::handle_update_thread_status); + client.add_model_message_handler(DapStore::handle_ignore_breakpoint_state); + client.add_model_message_handler(DapStore::handle_session_has_shutdown); client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); @@ -160,7 +177,7 @@ impl DapStore { client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); - client.add_model_request_handler(DapStore::handle_shutdown_session); + client.add_model_request_handler(DapStore::handle_shutdown_session_request); } pub fn new_local( @@ -204,6 +221,8 @@ impl DapStore { mode: DapStoreMode::Remote(RemoteDapStore { upstream_client: Some(upstream_client), upstream_project_id: project_id, + sessions: Default::default(), + client_by_session: Default::default(), event_queue: Some(VecDeque::default()), }), downstream_client: None, @@ -262,21 +281,91 @@ impl DapStore { self.downstream_client.as_ref() } + pub fn add_remote_session( + &mut self, + session_id: DebugSessionId, + ignore: Option, + cx: &mut ModelContext, + ) { + match &mut self.mode { + DapStoreMode::Remote(remote) => { + remote + .sessions + .entry(session_id) + .or_insert(cx.new_model(|_| { + DebugSession::new_remote( + session_id, + "Remote-Debug".to_owned(), + ignore.unwrap_or(false), + ) + })); + } + _ => {} + } + } + + pub fn add_client_to_session( + &mut self, + session_id: DebugSessionId, + client_id: DebugAdapterClientId, + ) { + match &mut self.mode { + DapStoreMode::Local(local) => { + if local.sessions.contains_key(&session_id) { + local.client_by_session.insert(client_id, session_id); + } + } + DapStoreMode::Remote(remote) => { + if remote.sessions.contains_key(&session_id) { + remote.client_by_session.insert(client_id, session_id); + } + } + } + } + + pub fn remove_session(&mut self, session_id: DebugSessionId, cx: &mut ModelContext) { + match &mut self.mode { + DapStoreMode::Local(local) => { + if let Some(session) = local.sessions.remove(&session_id) { + for client_id in session + .read(cx) + .as_local() + .map(|local| local.client_ids()) + .expect("Local Dap can only have local sessions") + { + local.client_by_session.remove(&client_id); + } + } + } + DapStoreMode::Remote(remote) => { + remote.sessions.remove(&session_id); + remote.client_by_session.retain(|_, val| val != &session_id) + } + } + } + pub fn sessions(&self) -> impl Iterator> + '_ { - self.as_local().unwrap().sessions.values().cloned() + match &self.mode { + DapStoreMode::Local(local) => local.sessions.values().cloned(), + DapStoreMode::Remote(remote) => remote.sessions.values().cloned(), + } } pub fn session_by_id(&self, session_id: &DebugSessionId) -> Option> { - self.as_local() - .and_then(|store| store.sessions.get(session_id).cloned()) + match &self.mode { + DapStoreMode::Local(local) => local.sessions.get(session_id).cloned(), + DapStoreMode::Remote(remote) => remote.sessions.get(session_id).cloned(), + } } pub fn session_by_client_id( &self, client_id: &DebugAdapterClientId, ) -> Option> { - self.as_local() - .and_then(|store| store.session_by_client_id(client_id)) + match &self.mode { + DapStoreMode::Local(local) => local.session_by_client_id(client_id), + DapStoreMode::Remote(remote) => remote.session_by_client_id(client_id), + } } pub fn client_by_id( @@ -284,10 +373,10 @@ impl DapStore { client_id: &DebugAdapterClientId, cx: &ModelContext, ) -> Option<(Model, Arc)> { - let session = self.session_by_client_id(client_id)?; - let client = session.read(cx).client_by_id(client_id)?; + let local_session = self.session_by_client_id(client_id)?; + let client = local_session.read(cx).as_local()?.client_by_id(client_id)?; - Some((session, client)) + Some((local_session, client)) } pub fn capabilities_by_id(&self, client_id: &DebugAdapterClientId) -> Capabilities { @@ -370,7 +459,50 @@ impl DapStore { &self.breakpoints } - pub fn ignore_breakpoints(&self, session_id: &DebugSessionId, cx: &ModelContext) -> bool { + async fn handle_session_has_shutdown( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.remove_session(DebugSessionId::from_proto(envelope.payload.session_id), cx); + })?; + + Ok(()) + } + + async fn handle_ignore_breakpoint_state( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + let session_id = DebugSessionId::from_proto(envelope.payload.session_id); + + this.update(&mut cx, |this, cx| { + if let Some(session) = this.session_by_id(&session_id) { + session.update(cx, |session, cx| { + session.set_ignore_breakpoints(envelope.payload.ignore, cx) + }); + } + })?; + + Ok(()) + } + + pub fn set_ignore_breakpoints( + &mut self, + session_id: &DebugSessionId, + ignore: bool, + cx: &mut ModelContext, + ) { + if let Some(session) = self.session_by_id(session_id) { + session.update(cx, |session, cx| { + session.set_ignore_breakpoints(ignore, cx); + }); + } + } + + pub fn ignore_breakpoints(&self, session_id: &DebugSessionId, cx: &AppContext) -> bool { self.session_by_id(session_id) .map(|session| session.read(cx).ignore_breakpoints()) .unwrap_or_default() @@ -516,13 +648,15 @@ impl DapStore { let session = store.session_by_id(&session_id).unwrap(); session.update(cx, |session, cx| { - session.update_configuration( + let local_session = session.as_local_mut().unwrap(); + + local_session.update_configuration( |old_config| { *old_config = config.clone(); }, cx, ); - session.add_client(Arc::new(client), cx); + local_session.add_client(Arc::new(client), cx); }); // don't emit this event ourself in tests, so we can add request, @@ -636,7 +770,7 @@ impl DapStore { let start_client_task = self.start_client_internal(session_id, config.clone(), cx); cx.spawn(|this, mut cx| async move { - let session = cx.new_model(|_| DebugSession::new(session_id, config))?; + let session = cx.new_model(|_| DebugSession::new_local(session_id, config))?; let client = match start_client_task.await { Ok(client) => client, @@ -652,7 +786,10 @@ impl DapStore { this.update(&mut cx, |store, cx| { session.update(cx, |session, cx| { - session.add_client(client.clone(), cx); + session + .as_local_mut() + .unwrap() + .add_client(client.clone(), cx); }); let client_id = client.id(); @@ -728,7 +865,7 @@ impl DapStore { ))); }; - let config = session.read(cx).configuration(); + let config = session.read(cx).as_local().unwrap().configuration(); let mut adapter_args = client.adapter().request_args(&config); if let Some(args) = config.initialize_args.clone() { merge_json_value_into(args, &mut adapter_args); @@ -775,7 +912,7 @@ impl DapStore { // comes in we send another `attach` request with the already selected PID // If we don't do this the user has to select the process twice if the adapter sends a `startDebugging` request session.update(cx, |session, cx| { - session.update_configuration( + session.as_local_mut().unwrap().update_configuration( |config| { config.request = DebugRequestType::Attach(task::AttachConfig { process_id: Some(process_id), @@ -785,7 +922,7 @@ impl DapStore { ); }); - let config = session.read(cx).configuration(); + let config = session.read(cx).as_local().unwrap().configuration(); let mut adapter_args = client.adapter().request_args(&config); if let Some(args) = config.initialize_args.clone() { @@ -952,8 +1089,15 @@ impl DapStore { ))); }; + let Some(config) = session + .read(cx) + .as_local() + .map(|session| session.configuration()) + else { + return Task::ready(Err(anyhow!("Cannot find debug session: {:?}", session_id))); + }; + let session_id = *session_id; - let config = session.read(cx).configuration().clone(); let request_args = args.unwrap_or_else(|| StartDebuggingRequestArguments { configuration: config.initialize_args.clone().unwrap_or_default(), @@ -964,17 +1108,17 @@ impl DapStore { }); // Merge the new configuration over the existing configuration - let mut initialize_args = config.initialize_args.unwrap_or_default(); + let mut initialize_args = config.initialize_args.clone().unwrap_or_default(); merge_json_value_into(request_args.configuration, &mut initialize_args); let new_config = DebugAdapterConfig { label: config.label.clone(), kind: config.kind.clone(), - request: match request_args.request { + request: match &request_args.request { StartDebuggingRequestArgumentsRequest::Launch => DebugRequestType::Launch, StartDebuggingRequestArgumentsRequest::Attach => DebugRequestType::Attach( - if let DebugRequestType::Attach(attach_config) = config.request { - attach_config + if let DebugRequestType::Attach(attach_config) = &config.request { + attach_config.clone() } else { AttachConfig::default() }, @@ -1512,11 +1656,27 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); }; + let Some(local_session) = session.read(cx).as_local() else { + return Task::ready(Err(anyhow!( + "Cannot shutdown session on remote side: {:?}", + session_id + ))); + }; + let mut tasks = Vec::new(); - for client in session.read(cx).clients().collect::>() { + for client in local_session.clients().collect::>() { tasks.push(self.shutdown_client(&session, client, cx)); } + if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { + downstream_client + .send(proto::DebuggerSessionEnded { + project_id: *project_id, + session_id: session_id.to_proto(), + }) + .log_err(); + } + cx.background_executor().spawn(async move { futures::future::join_all(tasks).await; Ok(()) @@ -1578,11 +1738,13 @@ impl DapStore { debug_sessions: Vec, cx: &mut ModelContext, ) { - for (session_id, debug_clients) in debug_sessions - .into_iter() - .map(|session| (session.session_id, session.clients)) - { - for debug_client in debug_clients { + for session in debug_sessions.into_iter() { + let session_id = DebugSessionId::from_proto(session.session_id); + let ignore_breakpoints = Some(session.ignore_breakpoints); + + self.add_remote_session(session_id, ignore_breakpoints, cx); + + for debug_client in session.clients { if let DapStoreMode::Remote(remote) = &mut self.mode { if let Some(queue) = &mut remote.event_queue { debug_client.debug_panel_items.into_iter().for_each(|item| { @@ -1591,9 +1753,13 @@ impl DapStore { } } + let client = DebugAdapterClientId::from_proto(debug_client.client_id); + + self.add_client_to_session(session_id, client); + self.update_capabilities_for_client( - &DebugSessionId::from_proto(session_id), - &DebugAdapterClientId::from_proto(debug_client.client_id), + &session_id, + &client, &dap::proto_conversions::capabilities_from_proto( &debug_client.capabilities.unwrap_or_default(), ), @@ -1630,7 +1796,7 @@ impl DapStore { cx.notify(); } - async fn handle_shutdown_session( + async fn handle_shutdown_session_request( this: Model, envelope: TypedEnvelope, mut cx: AsyncAppContext, @@ -1918,8 +2084,11 @@ impl DapStore { .collect::>(); let mut tasks = Vec::new(); - for session in local_store.sessions.values() { - let session = session.read(cx); + for session in local_store + .sessions + .values() + .filter_map(|session| session.read(cx).as_local()) + { let ignore_breakpoints = self.ignore_breakpoints(&session.id(), cx); for client in session.clients().collect::>() { tasks.push(self.send_breakpoints( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5e721034835cbc..4b36a22cb23905 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -633,6 +633,8 @@ impl Project { client.add_model_request_handler(WorktreeStore::handle_rename_project_entry); + client.add_model_message_handler(Self::handle_toggle_ignore_breakpoints); + WorktreeStore::init(&client); BufferStore::init(&client); LspStore::init(&client); @@ -1385,6 +1387,26 @@ impl Project { result } + async fn handle_toggle_ignore_breakpoints( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |project, cx| { + // Only the host should handle this message because the host + // handles direct communication with the debugger servers. + if let Some((_, _)) = project.dap_store.read(cx).downstream_client() { + project + .toggle_ignore_breakpoints( + &DebugSessionId::from_proto(envelope.payload.session_id), + &DebugAdapterClientId::from_proto(envelope.payload.client_id), + cx, + ) + .detach_and_log_err(cx); + } + }) + } + pub fn toggle_ignore_breakpoints( &self, session_id: &DebugSessionId, @@ -1392,8 +1414,30 @@ impl Project { cx: &mut ModelContext, ) -> Task> { let tasks = self.dap_store.update(cx, |store, cx| { + if let Some((upstream_client, project_id)) = store.upstream_client() { + upstream_client + .send(proto::ToggleIgnoreBreakpoints { + session_id: session_id.to_proto(), + client_id: client_id.to_proto(), + project_id, + }) + .log_err(); + + return Vec::new(); + } + store.toggle_ignore_breakpoints(session_id, cx); + if let Some((downstream_client, project_id)) = store.downstream_client() { + downstream_client + .send(proto::IgnoreBreakpointState { + session_id: session_id.to_proto(), + project_id: *project_id, + ignore: store.ignore_breakpoints(session_id, cx), + }) + .log_err(); + } + let mut tasks = Vec::new(); for (project_path, breakpoints) in store.breakpoints() { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 52426ac69e882d..b6e6690d601b3a 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -331,7 +331,10 @@ message Envelope { UpdateThreadStatus update_thread_status = 310; VariablesRequest variables_request = 311; DapVariables dap_variables = 312; - DapRestartStackFrameRequest dap_restart_stack_frame_request = 313; // current max + DapRestartStackFrameRequest dap_restart_stack_frame_request = 313; + IgnoreBreakpointState ignore_breakpoint_state = 314; + ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 315; + DebuggerSessionEnded debugger_session_ended = 316; // current max } reserved 87 to 88; @@ -2471,10 +2474,16 @@ enum BreakpointKind { Log = 1; } +message DebuggerSessionEnded { + uint64 project_id = 1; + uint64 session_id = 2; +} + message DebuggerSession { uint64 session_id = 1; uint64 project_id = 2; - repeated DebugClient clients = 3; + bool ignore_breakpoints = 3; + repeated DebugClient clients = 4; } message DebugClient { @@ -2709,6 +2718,18 @@ message DapShutdownSession { optional uint64 session_id = 2; // Shutdown all sessions if this is None } +message ToggleIgnoreBreakpoints { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 session_id = 3; +} + +message IgnoreBreakpointState { + uint64 project_id = 1; + uint64 session_id = 2; + bool ignore = 3; +} + message DapNextRequest { uint64 project_id = 1; uint64 client_id = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 0af3cbaf4cb548..a79bcf08183941 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -397,6 +397,9 @@ messages!( (UsersResponse, Foreground), (VariablesRequest, Background), (DapVariables, Background), + (IgnoreBreakpointState, Background), + (ToggleIgnoreBreakpoints, Background), + (DebuggerSessionEnded, Background), ); request_messages!( @@ -644,6 +647,9 @@ entity_messages!( DapShutdownSession, UpdateThreadStatus, VariablesRequest, + IgnoreBreakpointState, + ToggleIgnoreBreakpoints, + DebuggerSessionEnded, ); entity_messages!( From ec547b8ca3fdd53c42cbc0f580fca9825efef0be Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 21 Jan 2025 23:43:11 +0100 Subject: [PATCH 487/650] Add debug configuration documentation (#96) * Start adding docs for configurations and adapters * Update debugger setting files with settings, attach config, & themes I also ran prettier and typos on it too." --------- Co-authored-by: Anthony Eid --- docs/src/debugger.md | 336 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 319 insertions(+), 17 deletions(-) diff --git a/docs/src/debugger.md b/docs/src/debugger.md index dc08421f266caa..b06f2f263c6686 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -1,35 +1,194 @@ # Debugger -## Debug Configuration +Zed uses the Debug Adapter Protocol (DAP) to provide debugging functionality across multiple programming languages. +DAP is a standardized protocol that defines how debuggers, editors, and IDEs communicate with each other. +It allows Zed to support various debuggers without needing to implement language-specific debugging logic. +This protocol enables features like setting breakpoints, stepping through code, inspecting variables, +and more, in a consistent manner across different programming languages and runtime environments. -To debug a program using Zed you must first create a debug configuration within your project located at `.zed/debug.json` +## Supported Debug Adapters + +Zed supports a variety of debug adapters for different programming languages: + +- JavaScript (node): Enables debugging of Node.js applications, including setting breakpoints, stepping through code, and inspecting variables in JavaScript. + +- Python (debugpy): Provides debugging capabilities for Python applications, supporting features like remote debugging, multi-threaded debugging, and Django/Flask application debugging. + +- LLDB: A powerful debugger for C, C++, Objective-C, and Swift, offering low-level debugging features and support for Apple platforms. + +- GDB: The GNU Debugger, which supports debugging for multiple programming languages including C, C++, Go, and Rust, across various platforms. + +- 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. + +- PHP (xdebug): Provides debugging and profiling capabilities for PHP applications, including remote debugging and code coverage analysis. + +- 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. + +These adapters enable Zed to provide a consistent debugging experience across multiple languages while leveraging the specific features and capabilities of each debugger. + +## How To Get Started + +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. + +### Configuration + +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. ```json [ { + // The label for the debug configuration and used to identify the debug session inside the debug panel "label": "Example Start debugger config" - // The debug adapter to use - // Zed supports javascript, python, lldb, go, and custom out of the box + // The debug adapter that Zed should use to debug the program "adapter": "custom", - // request: defaults to launch - // - launch: Zed will launch the program to be debugged - // - attach: Zed will attach to a running program to debug it + // Request: defaults to launch + // - launch: Zed will launch the program if specified or shows a debug terminal with the right configuration + // - 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) "request": "launch", - // cwd: defaults to the current working directory of your project - // The current working directory to start the debugger from - // accepts zed task variables e.g. $ZED_WORKTREE_ROOT + // cwd: defaults to the current working directory of your project ($ZED_WORKTREE_ROOT) + // this field also supports task variables e.g. $ZED_WORKTREE_ROOT "cwd": "$ZED_WORKTREE_ROOT", - // program: The program to debug - // accepts zed task variables + // program: The program that you want to debug + // this fields also support task variables e.g. $ZED_FILE + // Note: this field should only contain the path to the program you want to debug "program": "path_to_program", - // Additional initialization arguments to be sent on DAP initialization + // initialize_args: This field should contain all the adapter specific initialization arguments that are directly send to the debug adapter "initialize_args": { - + // "stopOnEntry": true // e.g. to stop on the first line of the program (These args are DAP specific) } } ] ``` +### Using Attach [WIP] + +Only javascript supports starting a debug session using attach. + +When using the attach request with a process ID the syntax is as follows: + +```json +{ + "label": "Attach to Process", + "adapter": "javascript", + "request": { + "attach": { + "process_id": "12345" + } + } +} +``` + +Without process ID the syntax is as follows: + +```json +{ + "label": "Attach to Process", + "adapter": "javascript", + "request": { + "attach": {} + } +} +``` + +#### JavaScript Configuration + +##### Debug Active File + +This configuration allows you to debug a JavaScript file in your project. + +```json +{ + "label": "JavaScript: Debug Active File", + "adapter": "javascript", + "program": "$ZED_FILE", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" +} +``` + +##### Debug Terminal + +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. + +```json +{ + "label": "JavaScript: Debug Terminal", + "adapter": "javascript", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT", + // "program": "$ZED_FILE", // optional if you pass this in, you will see the output inside the terminal itself + "initialize_args": { + "console": "integratedTerminal" + } +} +``` + +#### PHP Configuration + +##### Debug Active File + +This configuration allows you to debug a PHP file in your project. + +```json +{ + "label": "PHP: Debug Active File", + "adapter": "php", + "program": "$ZED_FILE", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" +} +``` + +#### Python Configuration + +##### Debug Active File + +This configuration allows you to debug a Python file in your project. + +```json +{ + "label": "Python: Debug Active File", + "adapter": "python", + "program": "$ZED_FILE", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" +} +``` + +#### GDB Configuration + +**NOTE:** This configuration is for Linux systems only & intel macbooks. + +##### Debug Program + +This configuration allows you to debug a program using GDB e.g. Zed itself. + +```json +{ + "label": "GDB: Debug program", + "adapter": "gdb", + "program": "$ZED_WORKTREE_ROOT/target/debug/zed", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" +} +``` + +#### LLDB Configuration + +##### Debug Program + +This configuration allows you to debug a program using LLDB e.g. Zed itself. + +```json +{ + "label": "LLDB: Debug program", + "adapter": "lldb", + "program": "$ZED_WORKTREE_ROOT/target/debug/zed", + "request": "launch", + "cwd": "$ZED_WORKTREE_ROOT" +} +``` + ## Breakpoints Zed currently supports these types of breakpoints @@ -41,8 +200,151 @@ Standard breakpoints can be toggled by left clicking on the editor gutter or usi Log breakpoints can also be edited/added through the edit log breakpoint action -## Starting a Debugger Session +## Settings + +- `stepping_granularity`: Determines the stepping granularity. +- `save_breakpoints`: Whether the breakpoints should be reused across Zed sessions. +- `button`: Whether to show the debug button in the status bar. +- `timeout`: Time in milliseconds until timeout error when connecting to a TCP debug adapter. +- `log_dap_communications`: Whether to log messages between active debug adapters and Zed +- `format_dap_log_messages`: Whether to format dap messages in when adding them to debug adapter logger + +### Stepping granularity + +- Description: The Step granularity that the debugger will use +- Default: line +- Setting: debugger.stepping_granularity + +**Options** + +1. Statement - The step should allow the program to run until the current statement has finished executing. + The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. + For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. + +```json +{ + "debugger": { + "stepping_granularity": "statement" + } +} +``` + +2. Line - The step should allow the program to run until the current source line has executed. + +```json +{ + "debugger": { + "stepping_granularity": "line" + } +} +``` + +3. Instruction - The step should allow one instruction to execute (e.g. one x86 instruction). + +```json +{ + "debugger": { + "stepping_granularity": "instruction" + } +} +``` + +### Save Breakpoints + +- Description: Whether the breakpoints should be saved across Zed sessions. +- Default: true +- Setting: debugger.save_breakpoints + +**Options** + +`boolean` values + +```json +{ + "debugger": { + "save_breakpoints": true + } +} +``` + +### Button + +- Description: Whether the button should be displayed in the debugger toolbar. +- Default: true +- Setting: debugger.show_button + +**Options** + +`boolean` values + +```json +{ + "debugger": { + "show_button": true + } +} +``` + +### Timeout + +- Description: Time in milliseconds until timeout error when connecting to a TCP debug adapter. +- Default: 2000ms +- Setting: debugger.timeout + +**Options** + +`integer` values + +```json +{ + "debugger": { + "timeout": 3000 + } +} +``` + +### Log Dap Communications + +- Description: Whether to log messages between active debug adapters and Zed. (Used for DAP development) +- Default: false +- Setting: debugger.log_dap_communications + +**Options** + +`boolean` values + +```json +{ + "debugger": { + "log_dap_communications": true + } +} +``` + +### Format Dap Log Messages + +- Description: Whether to format dap messages in when adding them to debug adapter logger. (Used for DAP development) +- Default: false +- Setting: debugger.format_dap_log_messages + +**Options** + +`boolean` values + +```json +{ + "debugger": { + "format_dap_log_messages": true + } +} +``` + +## Theme + +The Debugger supports the following theme options -A debugger session can be started by the Start Debugging action or clicking the "Choose Debugger" button in the debugger panel when there are no active sessions. + /// Color used to accent some of the debuggers elements + /// Only accent breakpoint & breakpoint related symbols right now -Zed supports having multiple sessions +**debugger.accent**: Color used to accent breakpoint & breakpoint related symbols +**editor.debugger_active_line.background**: Background color of active debug line From 1fb0c5b84e9b160c1f647bb1ecc580357eafbb39 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 24 Jan 2025 03:21:30 -0500 Subject: [PATCH 488/650] Fix bug where lldb user installed path wasn't used in some cases We checked if the user passed in an lldb-dap path after we searched for lldb-dap in PATH. I switched around the order so if there's a user_installed_path passed through it takes priority --- crates/dap_adapters/src/lldb.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index d9281a0d546367..9206375b88ba83 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -33,7 +33,9 @@ impl DebugAdapter for LldbDebugAdapter { config: &DebugAdapterConfig, user_installed_path: Option, ) -> Result { - let lldb_dap_path = if cfg!(target_os = "macos") { + let lldb_dap_path = if let Some(user_installed_path) = user_installed_path { + user_installed_path.to_string_lossy().into() + } else if cfg!(target_os = "macos") { util::command::new_smol_command("xcrun") .args(&["-f", "lldb-dap"]) .output() @@ -42,8 +44,6 @@ impl DebugAdapter for LldbDebugAdapter { .and_then(|output| String::from_utf8(output.stdout).ok()) .map(|path| path.trim().to_string()) .ok_or(anyhow!("Failed to find lldb-dap in user's path"))? - } else if let Some(user_installed_path) = user_installed_path { - user_installed_path.to_string_lossy().into() } else { delegate .which(OsStr::new("lldb-dap")) From 78865820d878f68e06c747dbe253851cb2db6202 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 29 Jan 2025 03:46:37 -0500 Subject: [PATCH 489/650] Transition to gpui3 (#103) Update debugger's branch gpui version to 3 Co-authored-by: Remco Smits djsmits12@gmail.com --- .github/ISSUE_TEMPLATE/0_feature_request.yml | 37 +- .github/ISSUE_TEMPLATE/1_bug_report.yml | 58 +- .github/ISSUE_TEMPLATE/2_crash_report.yml | 33 +- .github/ISSUE_TEMPLATE/config.yml | 21 +- .github/workflows/bump_patch_version.yml | 1 + .github/workflows/ci.yml | 14 +- .../community_close_stale_issues.yml | 8 +- .../workflows/community_delete_comments.yml | 1 + .../workflows/community_release_actions.yml | 4 +- ...ommunity_update_all_top_ranking_issues.yml | 25 - ...unity_update_weekly_top_ranking_issues.yml | 25 - .github/workflows/danger.yml | 1 + .github/workflows/deploy_cloudflare.yml | 8 +- .github/workflows/deploy_collab.yml | 1 + .github/workflows/publish_extension_cli.yml | 1 + .github/workflows/randomized_tests.yml | 1 + .mailmap | 17 +- Cargo.lock | 248 +- Cargo.toml | 14 +- assets/icons/ai_deep_seek.svg | 1 + assets/icons/file.svg | 10 +- assets/icons/folder.svg | 2 +- assets/icons/globe.svg | 13 +- assets/icons/message_bubbles.svg | 8 +- assets/keymaps/default-linux.json | 188 +- assets/keymaps/default-macos.json | 87 +- assets/keymaps/linux/emacs.json | 32 +- assets/keymaps/macos/emacs.json | 32 +- assets/keymaps/storybook.json | 22 +- assets/keymaps/vim.json | 74 +- assets/settings/default.json | 7 +- .../src/activity_indicator.rs | 124 +- crates/anthropic/src/anthropic.rs | 25 +- crates/assets/src/assets.rs | 6 +- crates/assistant/Cargo.toml | 12 +- crates/assistant/src/assistant.rs | 145 +- .../assistant/src/assistant_configuration.rs | 199 + crates/assistant/src/assistant_panel.rs | 4565 ++------------- crates/assistant/src/inline_assistant.rs | 839 +-- .../assistant/src/slash_command_settings.rs | 4 +- .../src/terminal_inline_assistant.rs | 304 +- crates/assistant2/Cargo.toml | 5 +- crates/assistant2/src/active_thread.rs | 89 +- crates/assistant2/src/assistant.rs | 31 +- .../assistant2/src/assistant_configuration.rs | 173 + .../src/assistant_model_selector.rs | 50 +- crates/assistant2/src/assistant_panel.rs | 808 ++- crates/assistant2/src/buffer_codegen.rs | 138 +- crates/assistant2/src/context.rs | 27 +- crates/assistant2/src/context_picker.rs | 196 +- .../directory_context_picker.rs | 73 +- .../context_picker/fetch_context_picker.rs | 67 +- .../src/context_picker/file_context_picker.rs | 232 +- .../context_picker/thread_context_picker.rs | 107 +- crates/assistant2/src/context_store.rs | 123 +- crates/assistant2/src/context_strip.rs | 145 +- crates/assistant2/src/inline_assistant.rs | 492 +- crates/assistant2/src/inline_prompt_editor.rs | 328 +- crates/assistant2/src/message_editor.rs | 190 +- crates/assistant2/src/terminal_codegen.rs | 24 +- .../src/terminal_inline_assistant.rs | 111 +- crates/assistant2/src/thread.rs | 60 +- crates/assistant2/src/thread_history.rs | 133 +- crates/assistant2/src/thread_store.rs | 404 +- crates/assistant2/src/ui/context_pill.rs | 30 +- crates/assistant_context_editor/Cargo.toml | 68 + crates/assistant_context_editor/LICENSE-GPL | 1 + .../src/assistant_context_editor.rs | 23 + .../src/context.rs | 245 +- .../src/context/context_tests.rs | 95 +- .../src/context_editor.rs | 3788 ++++++++++++ .../src/context_history.rs | 269 + .../src/context_store.rs | 120 +- .../src/patch.rs | 54 +- .../src/slash_command.rs | 158 +- .../src/slash_command_picker.rs | 59 +- crates/assistant_settings/Cargo.toml | 1 + .../src/assistant_settings.rs | 33 +- .../src/assistant_slash_command.rs | 84 +- .../src/extension_slash_command.rs | 14 +- .../src/slash_command_registry.rs | 6 +- .../src/slash_command_working_set.rs | 8 +- crates/assistant_slash_commands/Cargo.toml | 1 + .../src/assistant_slash_commands.rs | 8 +- .../src/auto_command.rs | 22 +- .../src/cargo_workspace_command.rs | 16 +- .../src/context_server_command.rs | 18 +- .../src/default_command.rs | 12 +- .../src/delta_command.rs | 13 +- .../src/diagnostics_command.rs | 26 +- .../src/docs_command.rs | 18 +- .../src/fetch_command.rs | 12 +- .../src/file_command.rs | 29 +- .../src/now_command.rs | 12 +- .../src/project_command.rs | 16 +- .../src/prompt_command.rs | 14 +- .../src/search_command.rs | 16 +- .../src/selection_command.rs | 17 +- .../src/streaming_example_command.rs | 12 +- .../src/symbols_command.rs | 14 +- .../src/tab_command.rs | 30 +- .../src/terminal_command.rs | 22 +- crates/assistant_tool/src/assistant_tool.rs | 9 +- crates/assistant_tool/src/tool_registry.rs | 6 +- crates/assistant_tool/src/tool_working_set.rs | 6 +- crates/assistant_tools/src/assistant_tools.rs | 4 +- crates/assistant_tools/src/now_tool.rs | 7 +- crates/audio/src/assets.rs | 6 +- crates/audio/src/audio.rs | 8 +- crates/auto_update/src/auto_update.rs | 70 +- crates/auto_update_ui/src/auto_update_ui.rs | 55 +- .../auto_update_ui/src/update_notification.rs | 22 +- crates/breadcrumbs/src/breadcrumbs.rs | 31 +- crates/call/src/call_settings.rs | 4 +- crates/call/src/cross_platform/mod.rs | 82 +- crates/call/src/cross_platform/participant.rs | 14 +- crates/call/src/cross_platform/room.rs | 222 +- crates/call/src/macos/mod.rs | 82 +- crates/call/src/macos/participant.rs | 4 +- crates/call/src/macos/room.rs | 126 +- crates/channel/src/channel.rs | 4 +- crates/channel/src/channel_buffer.rs | 54 +- crates/channel/src/channel_chat.rs | 76 +- crates/channel/src/channel_store.rs | 97 +- crates/channel/src/channel_store_tests.rs | 18 +- crates/cli/src/main.rs | 4 +- crates/client/src/client.rs | 116 +- crates/client/src/telemetry.rs | 6 +- crates/client/src/test.rs | 6 +- crates/client/src/user.rs | 105 +- crates/client/src/zed_urls.rs | 6 +- crates/collab/Cargo.toml | 1 + crates/collab/src/auth.rs | 2 +- crates/collab/src/lib.rs | 2 +- crates/collab/src/llm.rs | 36 +- crates/collab/src/main.rs | 1 + crates/collab/src/rpc.rs | 3 + crates/collab/src/seed.rs | 2 +- crates/collab/src/stripe_billing.rs | 2 +- crates/collab/src/tests.rs | 6 +- .../collab/src/tests/channel_buffer_tests.rs | 75 +- .../collab/src/tests/channel_guest_tests.rs | 4 +- .../collab/src/tests/channel_message_tests.rs | 24 +- crates/collab/src/tests/channel_tests.rs | 8 +- crates/collab/src/tests/debug_panel_tests.rs | 84 +- crates/collab/src/tests/editor_tests.rs | 355 +- crates/collab/src/tests/following_tests.rs | 408 +- crates/collab/src/tests/integration_tests.rs | 71 +- crates/collab/src/tests/notification_tests.rs | 4 +- .../random_project_collaboration_tests.rs | 22 +- .../remote_editing_collaboration_tests.rs | 10 +- crates/collab/src/tests/test_server.rs | 96 +- crates/collab/src/user_backfiller.rs | 2 +- crates/collab_ui/src/channel_view.rs | 275 +- crates/collab_ui/src/chat_panel.rs | 227 +- .../src/chat_panel/message_editor.rs | 80 +- crates/collab_ui/src/collab_panel.rs | 892 +-- .../src/collab_panel/channel_modal.rs | 185 +- .../src/collab_panel/contact_finder.rs | 51 +- crates/collab_ui/src/collab_ui.rs | 6 +- crates/collab_ui/src/notification_panel.rs | 222 +- crates/collab_ui/src/notifications.rs | 4 +- .../src/notifications/collab_notification.rs | 2 +- .../incoming_call_notification.rs | 24 +- .../project_shared_notification.rs | 34 +- .../stories/collab_notification.rs | 2 +- crates/collab_ui/src/panel_settings.rs | 8 +- crates/command_palette/src/command_palette.rs | 146 +- .../src/command_palette_hooks.rs | 28 +- crates/context_server/src/client.rs | 12 +- crates/context_server/src/context_server.rs | 4 +- .../context_server/src/context_server_tool.rs | 11 +- .../src/extension_context_server.rs | 13 +- crates/context_server/src/manager.rs | 20 +- crates/context_server/src/registry.rs | 15 +- .../src/context_server_settings.rs | 6 +- crates/copilot/src/copilot.rs | 93 +- crates/copilot/src/copilot_chat.rs | 14 +- .../src/copilot_completion_provider.rs | 201 +- crates/copilot/src/sign_in.rs | 74 +- crates/dap/src/client.rs | 16 +- crates/dap/src/debugger_settings.rs | 7 +- crates/dap/src/session.rs | 15 +- crates/dap/src/transport.rs | 34 +- crates/db/src/db.rs | 6 +- crates/debugger_tools/src/dap_log.rs | 203 +- crates/debugger_tools/src/debugger_tools.rs | 4 +- crates/debugger_ui/src/attach_modal.rs | 50 +- crates/debugger_ui/src/console.rs | 146 +- crates/debugger_ui/src/debugger_panel.rs | 209 +- crates/debugger_ui/src/debugger_panel_item.rs | 238 +- crates/debugger_ui/src/lib.rs | 193 +- crates/debugger_ui/src/loaded_source_list.rs | 53 +- crates/debugger_ui/src/module_list.rs | 43 +- crates/debugger_ui/src/stack_frame_list.rs | 127 +- crates/debugger_ui/src/tests.rs | 29 +- crates/debugger_ui/src/tests/attach_modal.rs | 10 +- crates/debugger_ui/src/tests/console.rs | 18 +- .../debugger_ui/src/tests/debugger_panel.rs | 51 +- .../debugger_ui/src/tests/stack_frame_list.rs | 16 +- crates/debugger_ui/src/tests/variable_list.rs | 9 +- crates/debugger_ui/src/variable_list.rs | 290 +- crates/deepseek/Cargo.toml | 24 + crates/deepseek/LICENSE-GPL | 1 + crates/deepseek/src/deepseek.rs | 301 + crates/diagnostics/src/diagnostics.rs | 276 +- crates/diagnostics/src/diagnostics_tests.rs | 154 +- crates/diagnostics/src/items.rs | 84 +- .../src/project_diagnostics_settings.rs | 4 +- crates/diagnostics/src/toolbar_controls.rs | 23 +- .../src/docs_preprocessor.rs | 5 +- crates/docs_preprocessor/src/main.rs | 2 +- crates/editor/Cargo.toml | 1 + crates/editor/src/actions.rs | 5 +- crates/editor/src/blame_entry_tooltip.rs | 35 +- crates/editor/src/blink_manager.rs | 16 +- crates/editor/src/clangd_ext.rs | 15 +- crates/editor/src/code_context_menus.rs | 226 +- crates/editor/src/display_map.rs | 163 +- crates/editor/src/display_map/block_map.rs | 144 +- crates/editor/src/display_map/crease_map.rs | 50 +- crates/editor/src/display_map/fold_map.rs | 91 +- crates/editor/src/display_map/inlay_map.rs | 41 +- crates/editor/src/display_map/tab_map.rs | 14 +- crates/editor/src/display_map/wrap_map.rs | 62 +- crates/editor/src/editor.rs | 4408 ++++++++------ crates/editor/src/editor_settings.rs | 9 +- crates/editor/src/editor_settings_controls.rs | 78 +- crates/editor/src/editor_tests.rs | 5147 ++++++++++------- crates/editor/src/element.rs | 2849 +++++---- crates/editor/src/git/blame.rs | 173 +- crates/editor/src/git/project_diff.rs | 238 +- .../editor/src/highlight_matching_bracket.rs | 13 +- crates/editor/src/hover_links.rs | 120 +- crates/editor/src/hover_popover.rs | 244 +- crates/editor/src/hunk_diff.rs | 289 +- crates/editor/src/indent_guides.rs | 85 +- crates/editor/src/inlay_hint_cache.rs | 263 +- crates/editor/src/inline_completion_tests.rs | 71 +- crates/editor/src/items.rs | 385 +- crates/editor/src/linked_editing_ranges.rs | 10 +- crates/editor/src/lsp_ext.rs | 6 +- crates/editor/src/mouse_context_menu.rs | 95 +- crates/editor/src/movement.rs | 47 +- crates/editor/src/proposed_changes_editor.rs | 179 +- crates/editor/src/rust_analyzer_ext.rs | 31 +- crates/editor/src/scroll.rs | 123 +- crates/editor/src/scroll/actions.rs | 68 +- crates/editor/src/scroll/autoscroll.rs | 27 +- crates/editor/src/selections_collection.rs | 66 +- crates/editor/src/signature_help.rs | 39 +- crates/editor/src/signature_help/popover.rs | 12 +- crates/editor/src/tasks.rs | 17 +- crates/editor/src/test.rs | 36 +- .../src/test/editor_lsp_test_context.rs | 39 +- crates/editor/src/test/editor_test_context.rs | 262 +- crates/evals/src/eval.rs | 20 +- crates/extension/src/extension.rs | 4 +- crates/extension/src/extension_host_proxy.rs | 14 +- crates/extension/src/extension_manifest.rs | 2 +- crates/extension_host/src/extension_host.rs | 79 +- .../extension_host/src/extension_settings.rs | 4 +- .../src/extension_store_test.rs | 14 +- crates/extension_host/src/headless_host.rs | 26 +- crates/extension_host/src/wasm_host.rs | 11 +- crates/extension_host/src/wasm_host/wit.rs | 2 +- .../src/components/extension_card.rs | 2 +- .../src/components/feature_upsell.rs | 4 +- crates/extensions_ui/src/extension_suggest.rs | 21 +- .../src/extension_version_selector.rs | 55 +- crates/extensions_ui/src/extensions_ui.rs | 237 +- crates/feature_flags/src/feature_flags.rs | 39 +- crates/feedback/src/feedback.rs | 63 +- crates/feedback/src/feedback_modal.rs | 110 +- crates/feedback/src/system_specs.rs | 6 +- crates/file_finder/src/file_finder.rs | 284 +- .../file_finder/src/file_finder_settings.rs | 2 +- crates/file_finder/src/file_finder_tests.rs | 205 +- crates/file_finder/src/new_path_prompt.rs | 79 +- crates/file_finder/src/open_path_prompt.rs | 42 +- crates/file_icons/src/file_icons.rs | 16 +- crates/fs/src/fs.rs | 30 +- crates/fs/src/fs_watcher.rs | 7 +- crates/git/Cargo.toml | 1 + crates/git/src/blame.rs | 2 +- crates/git/src/diff.rs | 225 +- crates/git/src/git.rs | 2 +- crates/git/src/hosting_provider.rs | 10 +- crates/git/src/repository.rs | 2 +- crates/git/src/status.rs | 40 +- .../src/git_hosting_providers.rs | 4 +- crates/git_ui/Cargo.toml | 3 +- crates/git_ui/src/git_panel.rs | 808 ++- crates/git_ui/src/git_panel_settings.rs | 2 +- crates/git_ui/src/git_ui.rs | 48 +- crates/git_ui/src/repository_selector.rs | 253 + crates/go_to_line/Cargo.toml | 1 + crates/go_to_line/src/cursor_position.rs | 127 +- crates/go_to_line/src/go_to_line.rs | 508 +- crates/gpui/Cargo.toml | 25 +- crates/gpui/docs/contexts.md | 6 +- crates/gpui/docs/key_dispatch.md | 8 +- crates/gpui/examples/animation.rs | 14 +- crates/gpui/examples/gif_viewer.rs | 8 +- crates/gpui/examples/gradient.rs | 18 +- crates/gpui/examples/hello_world.rs | 10 +- crates/gpui/examples/image/image.rs | 18 +- crates/gpui/examples/image_loading.rs | 38 +- crates/gpui/examples/input.rs | 169 +- crates/gpui/examples/opacity.rs | 20 +- crates/gpui/examples/ownership_post.rs | 8 +- crates/gpui/examples/painting.rs | 28 +- crates/gpui/examples/pattern.rs | 103 + crates/gpui/examples/set_menus.rs | 14 +- crates/gpui/examples/shadow.rs | 10 +- crates/gpui/examples/svg/svg.rs | 12 +- crates/gpui/examples/text_wrapper.rs | 9 +- crates/gpui/examples/uniform_list.rs | 47 +- crates/gpui/examples/window.rs | 65 +- crates/gpui/examples/window_positioning.rs | 46 +- crates/gpui/examples/window_shadow.rs | 60 +- crates/gpui/src/app.rs | 498 +- crates/gpui/src/app/async_context.rs | 197 +- crates/gpui/src/app/entity_map.rs | 294 +- crates/gpui/src/app/model_context.rs | 583 +- crates/gpui/src/app/test_context.rs | 303 +- crates/gpui/src/asset_cache.rs | 6 +- crates/gpui/src/color.rs | 11 + crates/gpui/src/element.rs | 178 +- crates/gpui/src/elements/anchored.rs | 27 +- crates/gpui/src/elements/animation.rs | 21 +- crates/gpui/src/elements/canvas.rs | 29 +- crates/gpui/src/elements/deferred.rs | 17 +- crates/gpui/src/elements/div.rs | 868 +-- crates/gpui/src/elements/img.rs | 114 +- crates/gpui/src/elements/list.rs | 123 +- crates/gpui/src/elements/surface.rs | 17 +- crates/gpui/src/elements/svg.rs | 48 +- crates/gpui/src/elements/text.rs | 183 +- crates/gpui/src/elements/uniform_list.rs | 175 +- crates/gpui/src/executor.rs | 4 +- crates/gpui/src/geometry.rs | 8 +- crates/gpui/src/global.rs | 6 +- crates/gpui/src/gpui.rs | 98 +- crates/gpui/src/input.rs | 74 +- crates/gpui/src/interactive.rs | 32 +- crates/gpui/src/key_dispatch.rs | 22 +- crates/gpui/src/keymap.rs | 3 +- crates/gpui/src/platform.rs | 73 +- crates/gpui/src/platform/app_menu.rs | 4 +- crates/gpui/src/platform/blade/shaders.wgsl | 19 +- crates/gpui/src/platform/linux/platform.rs | 6 + crates/gpui/src/platform/linux/text_system.rs | 2 +- crates/gpui/src/platform/linux/x11/window.rs | 2 +- crates/gpui/src/platform/mac/platform.rs | 5 + crates/gpui/src/platform/mac/shaders.metal | 44 +- .../gpui/src/platform/windows/dispatcher.rs | 2 +- crates/gpui/src/platform/windows/events.rs | 131 +- crates/gpui/src/platform/windows/platform.rs | 4 +- crates/gpui/src/platform/windows/util.rs | 19 +- crates/gpui/src/platform/windows/window.rs | 4 +- crates/gpui/src/prelude.rs | 2 +- crates/gpui/src/style.rs | 42 +- crates/gpui/src/taffy.rs | 16 +- crates/gpui/src/test.rs | 2 +- crates/gpui/src/text_system/line.rs | 42 +- crates/gpui/src/text_system/line_layout.rs | 8 + crates/gpui/src/view.rs | 389 +- crates/gpui/src/window.rs | 2861 +++------ crates/gpui/src/window/prompts.rs | 73 +- crates/gpui_macros/Cargo.toml | 5 +- crates/gpui_macros/src/derive_app_context.rs | 88 + crates/gpui_macros/src/derive_render.rs | 2 +- .../gpui_macros/src/derive_visual_context.rs | 71 + crates/gpui_macros/src/gpui_macros.rs | 66 + crates/gpui_macros/src/test.rs | 2 +- crates/gpui_macros/tests/derive_context.rs | 13 + crates/gpui_macros/tests/render_test.rs | 7 + .../html_to_markdown/src/html_to_markdown.rs | 2 +- crates/image_viewer/src/image_viewer.rs | 91 +- .../src/extension_indexed_docs_provider.rs | 4 +- crates/indexed_docs/src/indexed_docs.rs | 4 +- crates/indexed_docs/src/registry.rs | 6 +- crates/indexed_docs/src/store.rs | 4 +- .../src/inline_completion.rs | 85 +- crates/inline_completion_button/Cargo.toml | 2 + .../src/inline_completion_button.rs | 375 +- crates/install_cli/src/install_cli.rs | 4 +- crates/journal/src/journal.rs | 103 +- crates/language/src/buffer.rs | 641 +- crates/language/src/buffer_tests.rs | 412 +- crates/language/src/language.rs | 50 +- crates/language/src/language_registry.rs | 8 +- crates/language/src/language_settings.rs | 16 +- crates/language/src/syntax_map.rs | 2 +- .../src/syntax_map/syntax_map_tests.rs | 50 +- crates/language/src/task_context.rs | 6 +- crates/language/src/toolchain.rs | 4 +- crates/language_extension/Cargo.toml | 1 + .../src/extension_lsp_adapter.rs | 11 +- crates/language_model/Cargo.toml | 1 + crates/language_model/src/fake_provider.rs | 24 +- crates/language_model/src/language_model.rs | 51 +- .../language_model/src/model/cloud_model.rs | 1 + crates/language_model/src/registry.rs | 38 +- crates/language_model/src/request.rs | 82 +- crates/language_model/src/role.rs | 10 + .../src/language_model_selector.rs | 107 +- crates/language_models/Cargo.toml | 1 + crates/language_models/src/language_models.rs | 18 +- crates/language_models/src/logging.rs | 2 +- crates/language_models/src/provider.rs | 1 + .../language_models/src/provider/anthropic.rs | 77 +- crates/language_models/src/provider/cloud.rs | 248 +- .../src/provider/copilot_chat.rs | 44 +- .../language_models/src/provider/deepseek.rs | 559 ++ crates/language_models/src/provider/google.rs | 73 +- .../language_models/src/provider/lmstudio.rs | 58 +- crates/language_models/src/provider/ollama.rs | 61 +- .../language_models/src/provider/open_ai.rs | 81 +- crates/language_models/src/settings.rs | 27 +- .../src/active_buffer_language.rs | 26 +- .../src/language_selector.rs | 92 +- crates/language_tools/src/key_context_view.rs | 75 +- crates/language_tools/src/language_tools.rs | 4 +- crates/language_tools/src/lsp_log.rs | 670 ++- crates/language_tools/src/lsp_log_tests.rs | 7 +- crates/language_tools/src/syntax_tree_view.rs | 157 +- crates/languages/Cargo.toml | 1 + crates/languages/src/c.rs | 8 +- crates/languages/src/css.rs | 7 +- crates/languages/src/go.rs | 14 +- crates/languages/src/json.rs | 24 +- crates/languages/src/lib.rs | 6 +- crates/languages/src/python.rs | 27 +- crates/languages/src/python/highlights.scm | 72 +- crates/languages/src/rust.rs | 36 +- crates/languages/src/tailwind.rs | 8 +- crates/languages/src/typescript.rs | 37 +- crates/languages/src/vtsls.rs | 30 +- crates/languages/src/yaml.rs | 15 +- crates/livekit_client/Cargo.toml | 4 +- crates/livekit_client/examples/test_app.rs | 53 +- crates/livekit_client/src/livekit_client.rs | 58 +- .../src/remote_video_track_view.rs | 23 +- crates/livekit_client/src/test.rs | 32 +- crates/livekit_client/src/test/participant.rs | 5 + crates/livekit_client/src/test/publication.rs | 5 + crates/livekit_client/src/test/track.rs | 13 + .../examples/test_app_macos.rs | 4 +- crates/livekit_client_macos/src/prod.rs | 2 +- crates/livekit_client_macos/src/test.rs | 2 +- crates/lmstudio/src/lmstudio.rs | 2 +- crates/lsp/src/lsp.rs | 30 +- crates/markdown/examples/markdown.rs | 20 +- crates/markdown/examples/markdown_as_child.rs | 23 +- crates/markdown/src/markdown.rs | 180 +- .../markdown_preview/src/markdown_preview.rs | 11 +- .../src/markdown_preview_view.rs | 194 +- .../markdown_preview/src/markdown_renderer.rs | 75 +- crates/multi_buffer/Cargo.toml | 7 + crates/multi_buffer/src/anchor.rs | 83 +- crates/multi_buffer/src/multi_buffer.rs | 4860 +++++++++++----- crates/multi_buffer/src/multi_buffer_tests.rs | 3040 ++++++---- crates/multi_buffer/src/position.rs | 264 + .../notifications/src/notification_store.rs | 56 +- crates/ollama/src/ollama.rs | 6 +- crates/open_ai/src/open_ai.rs | 10 +- crates/outline/src/outline.rs | 143 +- crates/outline_panel/src/outline_panel.rs | 1170 ++-- .../src/outline_panel_settings.rs | 2 +- crates/paths/src/paths.rs | 6 + crates/picker/src/head.rs | 36 +- .../src/highlighted_match_with_paths.rs | 4 +- crates/picker/src/picker.rs | 341 +- crates/prettier/src/prettier.rs | 12 +- crates/project/src/buffer_store.rs | 430 +- crates/project/src/connection_manager.rs | 26 +- crates/project/src/dap_command.rs | 18 +- crates/project/src/dap_store.rs | 251 +- crates/project/src/debounced_delay.rs | 6 +- crates/project/src/environment.rs | 16 +- crates/project/src/git.rs | 329 +- crates/project/src/image_store.rs | 125 +- crates/project/src/lsp_command.rs | 384 +- crates/project/src/lsp_ext_command.rs | 70 +- crates/project/src/lsp_store.rs | 1033 ++-- crates/project/src/prettier_store.rs | 46 +- crates/project/src/project.rs | 864 ++- crates/project/src/project_settings.rs | 45 +- crates/project/src/project_tests.rs | 119 +- crates/project/src/search.rs | 22 +- crates/project/src/task_inventory.rs | 38 +- crates/project/src/task_store.rs | 71 +- crates/project/src/terminals.rs | 43 +- crates/project/src/toolchain_store.rs | 66 +- crates/project/src/worktree_store.rs | 224 +- crates/project/src/yarn.rs | 8 +- crates/project_panel/src/project_panel.rs | 2209 ++++--- .../src/project_panel_settings.rs | 2 +- crates/project_symbols/src/project_symbols.rs | 109 +- crates/prompt_library/src/prompt_library.rs | 539 +- crates/prompt_library/src/prompt_store.rs | 6 +- crates/prompt_library/src/prompts.rs | 17 +- crates/proto/proto/zed.proto | 66 +- crates/proto/src/proto.rs | 6 +- .../src/disconnected_overlay.rs | 103 +- crates/recent_projects/src/recent_projects.rs | 189 +- crates/recent_projects/src/remote_servers.rs | 418 +- crates/recent_projects/src/ssh_connections.rs | 132 +- crates/release_channel/src/lib.rs | 14 +- crates/remote/src/ssh_session.rs | 120 +- crates/remote_server/src/headless_project.rs | 100 +- .../remote_server/src/remote_editing_tests.rs | 64 +- crates/remote_server/src/unix.rs | 22 +- .../repl/src/components/kernel_list_item.rs | 2 +- crates/repl/src/components/kernel_options.rs | 38 +- crates/repl/src/jupyter_settings.rs | 6 +- crates/repl/src/kernels/mod.rs | 10 +- crates/repl/src/kernels/native_kernel.rs | 19 +- crates/repl/src/kernels/remote_kernels.rs | 17 +- crates/repl/src/notebook/cell.rs | 119 +- crates/repl/src/notebook/notebook_ui.rs | 308 +- crates/repl/src/outputs.rs | 168 +- crates/repl/src/outputs/image.rs | 10 +- crates/repl/src/outputs/markdown.rs | 18 +- crates/repl/src/outputs/plain.rs | 69 +- crates/repl/src/outputs/table.rs | 29 +- crates/repl/src/outputs/user_error.rs | 8 +- crates/repl/src/repl.rs | 6 +- crates/repl/src/repl_editor.rs | 88 +- crates/repl/src/repl_sessions_ui.rs | 177 +- crates/repl/src/repl_store.rs | 40 +- crates/repl/src/session.rs | 118 +- crates/reqwest_client/examples/client.rs | 2 +- crates/rich_text/src/rich_text.rs | 18 +- crates/rope/src/chunk.rs | 13 +- crates/rope/src/rope.rs | 163 +- crates/rope/src/unclipped.rs | 16 +- crates/rpc/src/auth.rs | 2 +- crates/rpc/src/peer.rs | 2 +- crates/rpc/src/proto_client.rs | 22 +- crates/search/src/buffer_search.rs | 1011 ++-- crates/search/src/buffer_search/registrar.rs | 41 +- crates/search/src/project_search.rs | 1319 +++-- crates/search/src/search.rs | 29 +- crates/search/src/search_bar.rs | 4 +- crates/semantic_index/examples/index.rs | 4 +- crates/semantic_index/src/embedding/cloud.rs | 2 +- crates/semantic_index/src/embedding_index.rs | 31 +- crates/semantic_index/src/project_index.rs | 40 +- .../src/project_index_debug_view.rs | 91 +- crates/semantic_index/src/semantic_index.rs | 33 +- crates/semantic_index/src/summary_index.rs | 30 +- crates/semantic_index/src/worktree_index.rs | 30 +- crates/session/src/session.rs | 6 +- .../settings/src/editable_setting_control.rs | 8 +- crates/settings/src/keymap_file.rs | 84 +- crates/settings/src/settings.rs | 4 +- crates/settings/src/settings_file.rs | 12 +- crates/settings/src/settings_store.rs | 85 +- .../src/appearance_settings_controls.rs | 70 +- crates/settings_ui/src/settings_ui.rs | 55 +- crates/snippet/src/snippet.rs | 2 +- crates/snippet_provider/Cargo.toml | 3 +- .../snippet_provider/src/extension_snippet.rs | 4 +- crates/snippet_provider/src/format.rs | 45 +- crates/snippet_provider/src/lib.rs | 37 +- crates/snippet_provider/src/registry.rs | 11 +- crates/snippets_ui/src/snippets_ui.rs | 79 +- crates/sqlez/src/bindable.rs | 2 +- crates/sqlez/src/migrations.rs | 2 +- crates/sqlez/src/thread_safe_connection.rs | 2 +- crates/sqlez/src/typed_statements.rs | 2 +- crates/story/src/story.rs | 8 +- .../src/stories/auto_height_editor.rs | 16 +- crates/storybook/src/stories/cursor.rs | 2 +- .../storybook/src/stories/default_colors.rs | 10 +- crates/storybook/src/stories/focus.rs | 44 +- crates/storybook/src/stories/indent_guides.rs | 11 +- crates/storybook/src/stories/kitchen_sink.rs | 10 +- .../storybook/src/stories/overflow_scroll.rs | 2 +- crates/storybook/src/stories/picker.rs | 36 +- crates/storybook/src/stories/scroll.rs | 12 +- crates/storybook/src/stories/text.rs | 16 +- .../storybook/src/stories/viewport_units.rs | 10 +- crates/storybook/src/stories/with_rem_size.rs | 4 +- crates/storybook/src/story_selector.rs | 68 +- crates/storybook/src/storybook.rs | 21 +- crates/sum_tree/src/sum_tree.rs | 19 +- crates/supermaven/src/supermaven.rs | 30 +- .../src/supermaven_completion_provider.rs | 29 +- crates/supermaven_api/src/supermaven_api.rs | 2 +- crates/tab_switcher/src/tab_switcher.rs | 198 +- crates/tab_switcher/src/tab_switcher_tests.rs | 82 +- crates/task/src/static_source.rs | 6 +- crates/task/src/task_template.rs | 27 +- crates/tasks_ui/Cargo.toml | 3 + crates/tasks_ui/src/modal.rs | 218 +- crates/tasks_ui/src/settings.rs | 5 +- crates/tasks_ui/src/{lib.rs => tasks_ui.rs} | 99 +- crates/terminal/src/terminal.rs | 73 +- crates/terminal/src/terminal_settings.rs | 9 +- crates/terminal_view/src/persistence.rs | 77 +- crates/terminal_view/src/terminal_element.rs | 199 +- crates/terminal_view/src/terminal_panel.rs | 517 +- .../terminal_view/src/terminal_tab_tooltip.rs | 8 +- crates/terminal_view/src/terminal_view.rs | 440 +- crates/text/Cargo.toml | 2 +- crates/text/src/patch.rs | 1 + crates/text/src/tests.rs | 19 + crates/text/src/text.rs | 38 +- crates/theme/src/default_colors.rs | 41 +- crates/theme/src/fallback_themes.rs | 31 +- crates/theme/src/font_family_cache.rs | 8 +- crates/theme/src/registry.rs | 10 +- crates/theme/src/scale.rs | 6 +- crates/theme/src/schema.rs | 80 + crates/theme/src/settings.rs | 23 +- crates/theme/src/styles/colors.rs | 55 +- crates/theme/src/theme.rs | 8 +- crates/theme_extension/src/theme_extension.rs | 4 +- crates/theme_importer/src/main.rs | 2 +- crates/theme_selector/src/theme_selector.rs | 81 +- crates/title_bar/Cargo.toml | 1 + crates/title_bar/src/application_menu.rs | 48 +- crates/title_bar/src/collab.rs | 116 +- .../title_bar/src/platforms/platform_linux.rs | 6 +- .../src/platforms/platform_windows.rs | 8 +- .../title_bar/src/stories/application_menu.rs | 10 +- crates/title_bar/src/title_bar.rs | 318 +- crates/title_bar/src/window_controls.rs | 21 +- .../src/active_toolchain.rs | 60 +- .../src/toolchain_selector.rs | 108 +- crates/ui/src/components/avatar/avatar.rs | 4 +- .../avatar/avatar_audio_status_indicator.rs | 10 +- .../avatar/avatar_availability_indicator.rs | 4 +- crates/ui/src/components/button/button.rs | 14 +- .../ui/src/components/button/button_icon.rs | 2 +- .../ui/src/components/button/button_like.rs | 86 +- .../ui/src/components/button/icon_button.rs | 10 +- .../ui/src/components/button/toggle_button.rs | 6 +- crates/ui/src/components/content_group.rs | 4 +- crates/ui/src/components/context_menu.rs | 190 +- crates/ui/src/components/disclosure.rs | 10 +- crates/ui/src/components/divider.rs | 8 +- crates/ui/src/components/dropdown_menu.rs | 22 +- crates/ui/src/components/facepile.rs | 4 +- crates/ui/src/components/icon.rs | 21 +- .../ui/src/components/icon/decorated_icon.rs | 4 +- .../ui/src/components/icon/icon_decoration.rs | 6 +- crates/ui/src/components/image.rs | 6 +- crates/ui/src/components/indent_guides.rs | 85 +- crates/ui/src/components/indicator.rs | 4 +- crates/ui/src/components/keybinding.rs | 36 +- .../src/components/label/highlighted_label.rs | 4 +- crates/ui/src/components/label/label.rs | 4 +- crates/ui/src/components/label/label_like.rs | 9 +- crates/ui/src/components/list/list.rs | 2 +- crates/ui/src/components/list/list_header.rs | 10 +- crates/ui/src/components/list/list_item.rs | 32 +- .../ui/src/components/list/list_separator.rs | 2 +- .../ui/src/components/list/list_sub_header.rs | 2 +- crates/ui/src/components/modal.rs | 20 +- crates/ui/src/components/navigable.rs | 27 +- crates/ui/src/components/numeric_stepper.rs | 16 +- crates/ui/src/components/popover.rs | 4 +- crates/ui/src/components/popover_menu.rs | 130 +- crates/ui/src/components/radio.rs | 10 +- crates/ui/src/components/right_click_menu.rs | 247 +- crates/ui/src/components/scrollbar.rs | 48 +- .../ui/src/components/settings_container.rs | 2 +- crates/ui/src/components/settings_group.rs | 2 +- crates/ui/src/components/stories/avatar.rs | 2 +- crates/ui/src/components/stories/button.rs | 2 +- .../ui/src/components/stories/context_menu.rs | 32 +- .../ui/src/components/stories/disclosure.rs | 2 +- crates/ui/src/components/stories/icon.rs | 2 +- .../ui/src/components/stories/icon_button.rs | 18 +- .../ui/src/components/stories/keybinding.rs | 2 +- crates/ui/src/components/stories/label.rs | 2 +- crates/ui/src/components/stories/list.rs | 2 +- .../ui/src/components/stories/list_header.rs | 2 +- crates/ui/src/components/stories/list_item.rs | 16 +- crates/ui/src/components/stories/tab.rs | 2 +- crates/ui/src/components/stories/tab_bar.rs | 2 +- .../src/components/stories/toggle_button.rs | 2 +- .../ui/src/components/stories/tool_strip.rs | 8 +- crates/ui/src/components/tab.rs | 6 +- crates/ui/src/components/tab_bar.rs | 2 +- crates/ui/src/components/table.rs | 6 +- crates/ui/src/components/toggle.rs | 62 +- crates/ui/src/components/tool_strip.rs | 2 +- crates/ui/src/components/tooltip.rs | 65 +- crates/ui/src/prelude.rs | 5 +- crates/ui/src/styles/appearance.rs | 6 +- crates/ui/src/styles/color.rs | 4 +- crates/ui/src/styles/elevation.rs | 8 +- crates/ui/src/styles/spacing.rs | 4 +- crates/ui/src/styles/typography.rs | 23 +- crates/ui/src/styles/units.rs | 10 +- crates/ui/src/traits/clickable.rs | 4 +- crates/ui/src/traits/component_preview.rs | 19 +- crates/ui/src/traits/styled_ext.rs | 22 +- crates/ui/src/utils.rs | 4 +- crates/ui/src/utils/with_rem_size.rs | 26 +- crates/ui_input/src/ui_input.rs | 27 +- crates/ui_macros/src/dynamic_spacing.rs | 6 +- crates/util/Cargo.toml | 2 +- crates/util/src/markdown.rs | 2 +- crates/util/src/paths.rs | 17 + crates/util/src/test.rs | 24 +- crates/util/src/util.rs | 16 +- crates/vcs_menu/src/lib.rs | 97 +- crates/vim/Cargo.toml | 1 + crates/vim/src/change_list.rs | 27 +- crates/vim/src/command.rs | 281 +- crates/vim/src/digraph.rs | 75 +- crates/vim/src/helix.rs | 68 +- crates/vim/src/indent.rs | 82 +- crates/vim/src/insert.rs | 34 +- crates/vim/src/mode_indicator.rs | 60 +- crates/vim/src/motion.rs | 363 +- crates/vim/src/normal.rs | 304 +- crates/vim/src/normal/case.rs | 80 +- crates/vim/src/normal/change.rs | 43 +- crates/vim/src/normal/delete.rs | 39 +- crates/vim/src/normal/increment.rs | 30 +- crates/vim/src/normal/mark.rs | 46 +- crates/vim/src/normal/paste.rs | 16 +- crates/vim/src/normal/repeat.rs | 112 +- crates/vim/src/normal/scroll.rs | 92 +- crates/vim/src/normal/search.rs | 181 +- crates/vim/src/normal/substitute.rs | 29 +- crates/vim/src/normal/toggle_comments.rs | 30 +- crates/vim/src/normal/yank.rs | 42 +- crates/vim/src/object.rs | 97 +- crates/vim/src/replace.rs | 42 +- crates/vim/src/rewrap.rs | 41 +- crates/vim/src/state.rs | 22 +- crates/vim/src/surrounds.rs | 53 +- crates/vim/src/test.rs | 50 +- .../src/test/neovim_backed_test_context.rs | 14 +- crates/vim/src/test/vim_test_context.rs | 50 +- crates/vim/src/vim.rs | 406 +- crates/vim/src/visual.rs | 249 +- .../vim_mode_setting/src/vim_mode_setting.rs | 6 +- crates/welcome/src/base_keymap_picker.rs | 63 +- crates/welcome/src/base_keymap_setting.rs | 2 +- crates/welcome/src/multibuffer_hint.rs | 26 +- crates/welcome/src/welcome.rs | 126 +- crates/workspace/src/dock.rs | 461 +- crates/workspace/src/item.rs | 654 ++- crates/workspace/src/modal_layer.rs | 68 +- crates/workspace/src/notifications.rs | 457 +- crates/workspace/src/pane.rs | 1317 +++-- crates/workspace/src/pane_group.rs | 170 +- crates/workspace/src/persistence/model.rs | 39 +- crates/workspace/src/searchable.rs | 208 +- .../src/shared_screen/cross_platform.rs | 41 +- crates/workspace/src/shared_screen/macos.rs | 38 +- crates/workspace/src/status_bar.rs | 79 +- crates/workspace/src/tasks.rs | 6 +- crates/workspace/src/theme_preview.rs | 131 +- crates/workspace/src/toolbar.rs | 54 +- crates/workspace/src/workspace.rs | 3040 ++++++---- crates/workspace/src/workspace_settings.rs | 11 +- crates/worktree/src/worktree.rs | 283 +- crates/worktree/src/worktree_settings.rs | 9 +- crates/worktree/src/worktree_tests.rs | 94 +- crates/zed/Cargo.toml | 4 +- crates/zed/RELEASE_CHANNEL | 2 +- crates/zed/src/main.rs | 107 +- crates/zed/src/reliability.rs | 12 +- crates/zed/src/zed.rs | 1158 ++-- crates/zed/src/zed/app_menus.rs | 11 +- .../zed/src/zed/inline_completion_registry.rs | 149 +- crates/zed/src/zed/linux_prompts.rs | 49 +- crates/zed/src/zed/open_listener.rs | 47 +- crates/zed/src/zed/quick_action_bar.rs | 82 +- .../zed/quick_action_bar/markdown_preview.rs | 17 +- .../zed/src/zed/quick_action_bar/repl_menu.rs | 78 +- crates/zed_actions/src/lib.rs | 22 +- crates/zed_predict_tos/Cargo.toml | 23 + crates/zed_predict_tos/LICENSE-GPL | 1 + crates/zed_predict_tos/src/zed_predict_tos.rs | 155 + crates/zeta/Cargo.toml | 2 + crates/zeta/src/completion_diff_element.rs | 25 +- crates/zeta/src/rate_completion_modal.rs | 177 +- crates/zeta/src/zeta.rs | 512 +- docs/src/assistant/configuration.md | 44 +- docs/src/assistant/context-servers.md | 1 + docs/src/completions.md | 2 +- docs/src/configuring-zed.md | 13 +- docs/src/development.md | 24 + docs/src/development/linux.md | 4 - docs/src/development/local-collaboration.md | 2 + docs/src/development/windows.md | 50 +- docs/src/languages.md | 2 +- docs/src/languages/elm.md | 32 +- docs/src/languages/yaml.md | 33 + docs/src/linux.md | 2 +- extensions/csharp/Cargo.toml | 2 +- extensions/csharp/extension.toml | 2 +- extensions/elixir/Cargo.toml | 2 +- extensions/elixir/extension.toml | 2 +- extensions/haskell/Cargo.toml | 2 +- extensions/haskell/extension.toml | 2 +- extensions/html/Cargo.toml | 2 +- extensions/html/extension.toml | 2 +- extensions/php/Cargo.toml | 2 +- extensions/php/extension.toml | 2 +- extensions/php/languages/php/injections.scm | 2 + extensions/prisma/extension.toml | 4 +- .../prisma/languages/prisma/highlights.scm | 19 +- extensions/purescript/Cargo.toml | 2 +- extensions/purescript/extension.toml | 2 +- extensions/scheme/extension.toml | 2 +- extensions/terraform/Cargo.toml | 2 +- extensions/terraform/extension.toml | 2 +- extensions/zig/Cargo.toml | 2 +- extensions/zig/extension.toml | 2 +- flake.lock | 18 +- nix/shell.nix | 12 +- script/label_data.json | 15 - .../update_top_ranking_issues/.python-version | 1 - script/update_top_ranking_issues/main.py | 311 - .../update_top_ranking_issues/pyproject.toml | 13 - .../pyrightconfig.json | 4 - script/update_top_ranking_issues/uv.lock | 385 -- tooling/xtask/src/tasks/package_conformity.rs | 2 +- tooling/xtask/src/workspace.rs | 2 +- 832 files changed, 58527 insertions(+), 41970 deletions(-) delete mode 100644 .github/workflows/community_update_all_top_ranking_issues.yml delete mode 100644 .github/workflows/community_update_weekly_top_ranking_issues.yml create mode 100644 assets/icons/ai_deep_seek.svg create mode 100644 crates/assistant/src/assistant_configuration.rs create mode 100644 crates/assistant2/src/assistant_configuration.rs create mode 100644 crates/assistant_context_editor/Cargo.toml create mode 120000 crates/assistant_context_editor/LICENSE-GPL create mode 100644 crates/assistant_context_editor/src/assistant_context_editor.rs rename crates/{assistant => assistant_context_editor}/src/context.rs (95%) rename crates/{assistant => assistant_context_editor}/src/context/context_tests.rs (96%) create mode 100644 crates/assistant_context_editor/src/context_editor.rs create mode 100644 crates/assistant_context_editor/src/context_history.rs rename crates/{assistant => assistant_context_editor}/src/context_store.rs (91%) rename crates/{assistant => assistant_context_editor}/src/patch.rs (95%) rename crates/{assistant => assistant_context_editor}/src/slash_command.rs (72%) rename crates/{assistant => assistant_context_editor}/src/slash_command_picker.rs (85%) create mode 100644 crates/deepseek/Cargo.toml create mode 120000 crates/deepseek/LICENSE-GPL create mode 100644 crates/deepseek/src/deepseek.rs create mode 100644 crates/git_ui/src/repository_selector.rs create mode 100644 crates/gpui/examples/pattern.rs create mode 100644 crates/gpui_macros/src/derive_app_context.rs create mode 100644 crates/gpui_macros/src/derive_visual_context.rs create mode 100644 crates/gpui_macros/tests/derive_context.rs create mode 100644 crates/gpui_macros/tests/render_test.rs create mode 100644 crates/language_models/src/provider/deepseek.rs create mode 100644 crates/multi_buffer/src/position.rs rename crates/tasks_ui/src/{lib.rs => tasks_ui.rs} (86%) create mode 100644 crates/zed_predict_tos/Cargo.toml create mode 120000 crates/zed_predict_tos/LICENSE-GPL create mode 100644 crates/zed_predict_tos/src/zed_predict_tos.rs delete mode 100644 script/label_data.json delete mode 100644 script/update_top_ranking_issues/.python-version delete mode 100644 script/update_top_ranking_issues/main.py delete mode 100644 script/update_top_ranking_issues/pyproject.toml delete mode 100644 script/update_top_ranking_issues/pyrightconfig.json delete mode 100644 script/update_top_ranking_issues/uv.lock diff --git a/.github/ISSUE_TEMPLATE/0_feature_request.yml b/.github/ISSUE_TEMPLATE/0_feature_request.yml index 900df46e7a5836..0bf3a61757cab5 100644 --- a/.github/ISSUE_TEMPLATE/0_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/0_feature_request.yml @@ -1,34 +1,15 @@ name: Feature Request description: "Tip: open this issue template from within Zed with the `request feature` command palette action" -labels: ["admin read", "triage", "enhancement"] +type: "Feature" body: - - type: checkboxes - attributes: - label: Check for existing issues - description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it. - options: - - label: Completed - required: true - type: textarea attributes: label: Describe the feature - description: A clear and concise description of what you want to happen. - validations: - required: true - - type: textarea - id: environment - attributes: - label: Zed Version and System Specs - description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount. - placeholder: | - - - validations: - required: true - - type: textarea - attributes: - label: | - If applicable, add mockups / screenshots to help present your vision of the feature - description: Drag images into the text input below - validations: - required: false + description: A one line summary, and description of what you want to happen. + value: | + Summary: + + Description: + + Screenshots: + diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yml b/.github/ISSUE_TEMPLATE/1_bug_report.yml index 3a26baa7397620..371e5bfb3b81e5 100644 --- a/.github/ISSUE_TEMPLATE/1_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1_bug_report.yml @@ -1,54 +1,34 @@ name: Bug Report description: | - Use this template for **non-crash-related** bug reports. - Tip: open this issue template from within Zed with the `file bug report` command palette action. -labels: ["admin read", "triage", "bug"] + Something is broken in Zed (exclude crashing). +type: "Bug" body: - - type: checkboxes - attributes: - label: Check for existing issues - description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it. - options: - - label: Completed - required: true - type: textarea attributes: label: Describe the bug / provide steps to reproduce it - description: A clear and concise description of what the bug is. + description: A one line summary, and detailed reproduction steps + value: | + Summary: + + + Steps to trigger the problem: + 1. + 2. + 3. + + Actual Behavior: + + Expected Behavior: + validations: required: true + - type: textarea id: environment attributes: label: Zed Version and System Specs - description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount. + description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"' placeholder: | - - - - - ``` - - ``` - - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/2_crash_report.yml b/.github/ISSUE_TEMPLATE/2_crash_report.yml index 87f27667910b1e..a7bb3d7b9aa84f 100644 --- a/.github/ISSUE_TEMPLATE/2_crash_report.yml +++ b/.github/ISSUE_TEMPLATE/2_crash_report.yml @@ -1,26 +1,33 @@ name: Crash Report -description: | - Use this template for crash reports. -labels: ["admin read", "triage", "bug", "panic / crash"] +description: Zed is Crashing or Hanging +type: "Crash" body: - - type: checkboxes - attributes: - label: Check for existing issues - description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it. - options: - - label: Completed - required: true - type: textarea attributes: label: Describe the bug / provide steps to reproduce it - description: A clear and concise description of what the bug is. + description: A one line summary, and detailed reproduction steps + value: | + Summary: + + + Steps to trigger the problem: + 1. + 2. + 3. + + Actual Behavior: + + Expected Behavior: + validations: required: true - type: textarea id: environment attributes: - label: Environment - description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture. + label: Zed Version and System Specs + description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"' + placeholder: | + Output of "zed: Copy System Specs Into Clipboard" validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4be3d54638fab9..9a51874331a4cc 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,18 +1,9 @@ # yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json blank_issues_enabled: false contact_links: - - name: Language Request - url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=language&projects=&template=1_language_request.yml&title=%3Cname_of_language%3E - about: Request a language in the extensions repository - - name: Theme Request - url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=theme&projects=&template=0_theme_request.yml&title=%3Cname_of_theme%3E+theme - about: Request a theme in the extensions repository - - name: Top-Ranking Issues - url: https://github.com/zed-industries/zed/issues/5393 - about: See an overview of the most popular Zed issues - - name: Platform Support - url: https://github.com/zed-industries/zed/issues/5391 - about: A quick note on platform support - - name: Positive Feedback - url: https://github.com/zed-industries/zed/discussions/5397 - about: A central location for kind words about Zed + - name: Zed Discussion Forum + url: https://github.com/zed-industries/zed/discussions + about: A community discussion forum + - name: "Zed Discord: #Support Channel" + url: https://zed.dev/community-links + about: Real-time discussion and user support diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml index 8bbefe64e8e7a2..02857a9151cc3e 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -14,6 +14,7 @@ concurrency: jobs: bump_patch_version: + if: github.repository_owner == 'zed-industries' runs-on: - buildjet-16vcpu-ubuntu-2204 steps: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f559588034b0f..88684a88b5a524 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,16 +7,10 @@ on: - "v[0-9]+.[0-9]+.x" tags: - "v*" - paths-ignore: - - "docs/**/*" - - ".github/workflows/community_*" pull_request: branches: - "**" - paths-ignore: - - "docs/**/*" - - ".github/workflows/community_*" concurrency: # Allow only one workflow per any non-`main` branch. @@ -333,14 +327,14 @@ jobs: mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg - name: Upload app bundle (aarch64) to workflow run if main branch or specific label - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} with: name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg - name: Upload app bundle (x86_64) to workflow run if main branch or specific label - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} with: name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg @@ -391,7 +385,7 @@ jobs: run: script/bundle-linux - name: Upload Linux bundle to workflow run if main branch or specific label - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} with: name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz @@ -439,7 +433,7 @@ jobs: run: script/bundle-linux - name: Upload Linux bundle to workflow run if main branch or specific label - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} with: name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz diff --git a/.github/workflows/community_close_stale_issues.yml b/.github/workflows/community_close_stale_issues.yml index 33be239b5c417d..bc4873f44e09d6 100644 --- a/.github/workflows/community_close_stale_issues.yml +++ b/.github/workflows/community_close_stale_issues.yml @@ -9,7 +9,7 @@ jobs: if: github.repository_owner == 'zed-industries' runs-on: ubuntu-latest steps: - - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: > @@ -19,11 +19,7 @@ jobs: Thanks for your help! close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue." - # We will increase `days-before-stale` to 365 on or after Jan 24th, - # 2024. This date marks one year since migrating issues from - # 'community' to 'zed' repository. The migration added activity to all - # issues, preventing 365 days from working until then. - days-before-stale: 180 + days-before-stale: 120 days-before-close: 7 any-of-issue-labels: "bug,panic / crash" operations-per-run: 1000 diff --git a/.github/workflows/community_delete_comments.yml b/.github/workflows/community_delete_comments.yml index e95a2d33d48ea8..0ebe1ac3acea5f 100644 --- a/.github/workflows/community_delete_comments.yml +++ b/.github/workflows/community_delete_comments.yml @@ -9,6 +9,7 @@ permissions: jobs: delete_comment: + if: github.repository_owner == 'zed-industries' runs-on: ubuntu-latest steps: - name: Check for specific strings in comment diff --git a/.github/workflows/community_release_actions.yml b/.github/workflows/community_release_actions.yml index 713c999c43b4be..e4f88fdc092934 100644 --- a/.github/workflows/community_release_actions.yml +++ b/.github/workflows/community_release_actions.yml @@ -6,6 +6,7 @@ on: jobs: discord_release: + if: github.repository_owner == 'zed-industries' runs-on: ubuntu-latest steps: - name: Get release URL @@ -28,7 +29,8 @@ jobs: maxLength: 2000 truncationSymbol: "..." - name: Discord Webhook Action - uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0 + uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 # v6.0.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} content: ${{ steps.get-content.outputs.string }} + flags: 4 # suppress embeds - https://discord.com/developers/docs/resources/message#message-object-message-flags diff --git a/.github/workflows/community_update_all_top_ranking_issues.yml b/.github/workflows/community_update_all_top_ranking_issues.yml deleted file mode 100644 index 66b1873b9594fe..00000000000000 --- a/.github/workflows/community_update_all_top_ranking_issues.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Update All Top Ranking Issues - -on: - schedule: - - cron: "0 */12 * * *" - workflow_dispatch: - -jobs: - update_top_ranking_issues: - runs-on: ubuntu-latest - if: github.repository == 'zed-industries/zed' - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Set up uv - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3 - with: - version: "latest" - enable-cache: true - cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml" - - name: Install Python 3.13 - run: uv python install 3.13 - - name: Install dependencies - run: uv sync --project script/update_top_ranking_issues -p 3.13 - - name: Run script - run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 5393 diff --git a/.github/workflows/community_update_weekly_top_ranking_issues.yml b/.github/workflows/community_update_weekly_top_ranking_issues.yml deleted file mode 100644 index e1514da7167d0c..00000000000000 --- a/.github/workflows/community_update_weekly_top_ranking_issues.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Update Weekly Top Ranking Issues - -on: - schedule: - - cron: "0 15 * * *" - workflow_dispatch: - -jobs: - update_top_ranking_issues: - runs-on: ubuntu-latest - if: github.repository == 'zed-industries/zed' - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Set up uv - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3 - with: - version: "latest" - enable-cache: true - cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml" - - name: Install Python 3.13 - run: uv python install 3.13 - - name: Install dependencies - run: uv sync --project script/update_top_ranking_issues -p 3.13 - - name: Run script - run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 6952 --query-day-interval 7 diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 897d4b47c58667..09084cbc03102e 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,6 +11,7 @@ on: jobs: danger: + if: github.repository_owner == 'zed-industries' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index 6cc4ea0a3329a7..284c10915eb57a 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -37,28 +37,28 @@ jobs: mdbook build ./docs --dest-dir=../target/deploy/docs/ - name: Deploy Docs - uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3 + uses: cloudflare/wrangler-action@7a5f8bbdfeedcde38e6777a50fe685f89259d4ca # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy target/deploy --project-name=docs - name: Deploy Install - uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3 + uses: cloudflare/wrangler-action@7a5f8bbdfeedcde38e6777a50fe685f89259d4ca # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh - name: Deploy Docs Workers - uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3 + uses: cloudflare/wrangler-action@7a5f8bbdfeedcde38e6777a50fe685f89259d4ca # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: deploy .cloudflare/docs-proxy/src/worker.js - name: Deploy Install Workers - uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3 + uses: cloudflare/wrangler-action@7a5f8bbdfeedcde38e6777a50fe685f89259d4ca # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index afc96c32bf0882..d921a08bf142ad 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -12,6 +12,7 @@ env: jobs: style: name: Check formatting and Clippy lints + if: github.repository_owner == 'zed-industries' runs-on: - self-hosted - test diff --git a/.github/workflows/publish_extension_cli.yml b/.github/workflows/publish_extension_cli.yml index cc589c85e23e33..24e2bb89437f34 100644 --- a/.github/workflows/publish_extension_cli.yml +++ b/.github/workflows/publish_extension_cli.yml @@ -12,6 +12,7 @@ env: jobs: publish: name: Publish zed-extension CLI + if: github.repository_owner == 'zed-industries' runs-on: - ubuntu-latest steps: diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index 1ecf511100844b..f421c4b6b93b1e 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -18,6 +18,7 @@ env: jobs: tests: name: Run randomized tests + if: github.repository_owner == 'zed-industries' runs-on: - buildjet-16vcpu-ubuntu-2204 steps: diff --git a/.mailmap b/.mailmap index 75bdba644afe78..77736f04453fc3 100644 --- a/.mailmap +++ b/.mailmap @@ -9,6 +9,8 @@ # Keep these entries sorted alphabetically. # In Zed: `editor: sort lines case insensitive` +Agus Zubiaga +Agus Zubiaga Alex Viscreanu Alex Viscreanu Alexander Mankuta @@ -24,6 +26,7 @@ Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Bennet Bo Fenner Boris Cherny Boris Cherny +Brian Tan Chris Hayes Christian Bergschneider Christian Bergschneider @@ -32,11 +35,16 @@ Conrad Irwin Dairon Medina Danilo Leal Danilo Leal <67129314+danilo-leal@users.noreply.github.com> +Edwin Aronsson <75266237+4teapo@users.noreply.github.com> Evren Sen Evren Sen <146845123+evrensen467@users.noreply.github.com> Evren Sen <146845123+evrsen@users.noreply.github.com> Fernando Tagawa Fernando Tagawa +Finn Evers +Finn Evers <75036051+MrSubidubi@users.noreply.github.com> +Finn Evers +Gowtham K <73059450+dovakin0007@users.noreply.github.com> Greg Morenz Greg Morenz Ihnat Aŭtuška @@ -54,11 +62,14 @@ Kirill Bulatov Kirill Bulatov Kyle Caverly Kyle Caverly +Lilith Iris +Lilith Iris <83819417+Irilith@users.noreply.github.com> LoganDark LoganDark LoganDark -Marshall Bowers -Marshall Bowers +Marshall Bowers +Marshall Bowers +Marshall Bowers Matt Fellenz Matt Fellenz Max Brunsfeld @@ -112,5 +123,7 @@ Uladzislau Kaminski Uladzislau Kaminski Vitaly Slobodin Vitaly Slobodin +Will Bradley +Will Bradley WindSoilder 张小白 <364772080@qq.com> diff --git a/Cargo.lock b/Cargo.lock index eda06c000bf634..2a6b22e4a6fca6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,14 +372,13 @@ name = "assistant" version = "0.1.0" dependencies = [ "anyhow", + "assistant_context_editor", "assistant_settings", "assistant_slash_command", "assistant_slash_commands", "assistant_tool", "async-watch", - "chrono", "client", - "clock", "collections", "command_palette_hooks", "context_server", @@ -390,7 +389,6 @@ dependencies = [ "feature_flags", "fs", "futures 0.3.31", - "fuzzy", "gpui", "indexed_docs", "indoc", @@ -403,30 +401,23 @@ dependencies = [ "lsp", "menu", "multi_buffer", - "open_ai", "parking_lot", "paths", - "picker", "pretty_assertions", "project", "prompt_library", "proto", "rand 0.8.5", - "regex", "rope", - "rpc", "schemars", "search", "semantic_index", "serde", - "serde_json", "serde_json_lenient", "settings", "similar", - "smallvec", "smol", "streaming_diff", - "strum", "telemetry", "telemetry_events", "terminal", @@ -437,7 +428,6 @@ dependencies = [ "ui", "unindent", "util", - "uuid", "workspace", "zed_actions", ] @@ -447,7 +437,9 @@ name = "assistant2" version = "0.1.0" dependencies = [ "anyhow", + "assistant_context_editor", "assistant_settings", + "assistant_slash_command", "assistant_tool", "async-watch", "chrono", @@ -464,6 +456,7 @@ dependencies = [ "futures 0.3.31", "fuzzy", "gpui", + "heed", "html_to_markdown", "http_client", "indoc", @@ -478,6 +471,7 @@ dependencies = [ "menu", "multi_buffer", "parking_lot", + "paths", "picker", "project", "prompt_library", @@ -498,19 +492,75 @@ dependencies = [ "time", "time_format", "ui", - "unindent", "util", "uuid", "workspace", "zed_actions", ] +[[package]] +name = "assistant_context_editor" +version = "0.1.0" +dependencies = [ + "anyhow", + "assistant_settings", + "assistant_slash_command", + "assistant_slash_commands", + "assistant_tool", + "chrono", + "client", + "clock", + "collections", + "context_server", + "editor", + "feature_flags", + "fs", + "futures 0.3.31", + "fuzzy", + "gpui", + "indexed_docs", + "language", + "language_model", + "language_model_selector", + "language_models", + "languages", + "log", + "multi_buffer", + "open_ai", + "parking_lot", + "paths", + "picker", + "pretty_assertions", + "project", + "prompt_library", + "rand 0.8.5", + "regex", + "rope", + "rpc", + "serde", + "serde_json", + "settings", + "smallvec", + "smol", + "strum", + "telemetry_events", + "text", + "theme", + "tree-sitter-md", + "ui", + "unindent", + "util", + "uuid", + "workspace", +] + [[package]] name = "assistant_settings" version = "0.1.0" dependencies = [ "anthropic", "anyhow", + "deepseek", "feature_flags", "fs", "gpui", @@ -587,6 +637,7 @@ dependencies = [ "ui", "util", "workspace", + "worktree", ] [[package]] @@ -2640,6 +2691,7 @@ dependencies = [ "anthropic", "anyhow", "assistant", + "assistant_context_editor", "assistant_slash_command", "assistant_tool", "async-stripe", @@ -2961,9 +3013,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "convert_case" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" dependencies = [ "unicode-segmentation", ] @@ -3748,6 +3800,18 @@ dependencies = [ "workspace", ] +[[package]] +name = "deepseek" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures 0.3.31", + "http_client", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "deflate64" version = "0.1.9" @@ -4016,7 +4080,7 @@ dependencies = [ "client", "clock", "collections", - "convert_case 0.6.0", + "convert_case 0.7.1", "ctor", "db", "emojis", @@ -4073,6 +4137,7 @@ dependencies = [ "util", "uuid", "workspace", + "zed_predict_tos", ] [[package]] @@ -4287,9 +4352,9 @@ dependencies = [ [[package]] name = "etagere" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2f1e3be19fb10f549be8c1bf013e8675b4066c445e36eb76d2ebb2f54ee495" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" dependencies = [ "euclid", "svg_fmt", @@ -5268,9 +5333,8 @@ dependencies = [ [[package]] name = "git2" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +version = "0.20.0" +source = "git+https://github.com/rust-lang/git2-rs?rev=a3b90cb3756c1bb63e2317bf9cfa57838178de5c#a3b90cb3756c1bb63e2317bf9cfa57838178de5c" dependencies = [ "bitflags 2.8.0", "libc", @@ -5309,8 +5373,8 @@ dependencies = [ "futures 0.3.31", "git", "gpui", - "language", "menu", + "picker", "project", "schemars", "serde", @@ -5322,7 +5386,6 @@ dependencies = [ "util", "windows 0.58.0", "workspace", - "worktree", ] [[package]] @@ -5529,6 +5592,7 @@ dependencies = [ name = "gpui_macros" version = "0.1.0" dependencies = [ + "gpui", "proc-macro2", "quote", "syn 1.0.109", @@ -5679,11 +5743,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -6373,9 +6437,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -6418,6 +6482,7 @@ name = "inline_completion_button" version = "0.1.0" dependencies = [ "anyhow", + "client", "copilot", "editor", "feature_flags", @@ -6437,6 +6502,7 @@ dependencies = [ "ui", "workspace", "zed_actions", + "zed_predict_tos", "zeta", ] @@ -6852,6 +6918,7 @@ dependencies = [ "async-trait", "collections", "extension", + "fs", "futures 0.3.31", "gpui", "language", @@ -6869,6 +6936,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "collections", + "deepseek", "futures 0.3.31", "google_ai", "gpui", @@ -6912,6 +6980,7 @@ dependencies = [ "client", "collections", "copilot", + "deepseek", "editor", "feature_flags", "fs", @@ -7017,6 +7086,7 @@ dependencies = [ "serde_json", "settings", "smol", + "snippet_provider", "task", "text", "theme", @@ -7092,9 +7162,8 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.17.0+1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +version = "0.18.0+1.9.0" +source = "git+https://github.com/rust-lang/git2-rs?rev=a3b90cb3756c1bb63e2317bf9cfa57838178de5c#a3b90cb3756c1bb63e2317bf9cfa57838178de5c" dependencies = [ "cc", "libc", @@ -7705,9 +7774,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3572083504c43e14aec05447f8a3d57cce0f66d7a3c1b9058572eca4d70ab9" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ "bitflags 2.8.0", "block", @@ -7817,15 +7886,21 @@ dependencies = [ "ctor", "env_logger 0.11.6", "futures 0.3.31", + "git", "gpui", + "indoc", "itertools 0.14.0", "language", "log", "parking_lot", + "pretty_assertions", + "project", "rand 0.8.5", + "rope", "serde", "settings", "smallvec", + "smol", "sum_tree", "text", "theme", @@ -9575,9 +9650,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -10082,7 +10157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes 1.9.0", - "heck 0.5.0", + "heck 0.4.1", "itertools 0.12.1", "log", "multimap 0.10.0", @@ -11644,9 +11719,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] @@ -11693,9 +11768,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "indexmap", "itoa", @@ -12118,8 +12193,9 @@ dependencies = [ "gpui", "parking_lot", "paths", + "schemars", "serde", - "serde_json", + "serde_json_lenient", "snippet", "util", ] @@ -12240,9 +12316,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" dependencies = [ "sqlx-core", "sqlx-macros", @@ -12253,32 +12329,27 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ - "atoi", "bigdecimal", - "byteorder", "bytes 1.9.0", "chrono", "crc", "crossbeam-queue", "either", "event-listener 5.3.1", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.14.5", - "hashlink 0.9.1", - "hex", + "hashbrown 0.15.2", + "hashlink 0.10.0", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", "rust_decimal", "rustls 0.23.20", @@ -12287,8 +12358,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror 2.0.6", "time", "tokio", "tokio-stream", @@ -12300,9 +12370,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" dependencies = [ "proc-macro2", "quote", @@ -12313,9 +12383,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" dependencies = [ "dotenvy", "either", @@ -12339,9 +12409,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", "base64 0.22.1", @@ -12377,7 +12447,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.6", "time", "tracing", "uuid", @@ -12386,9 +12456,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", "base64 0.22.1", @@ -12401,7 +12471,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -12421,7 +12490,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.6", "time", "tracing", "uuid", @@ -12430,9 +12499,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ "atoi", "chrono", @@ -13441,6 +13510,7 @@ dependencies = [ "client", "collections", "feature_flags", + "git_ui", "gpui", "http_client", "notifications", @@ -13465,9 +13535,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes 1.9.0", @@ -13494,9 +13564,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -14158,7 +14228,7 @@ dependencies = [ name = "ui_macros" version = "0.1.0" dependencies = [ - "convert_case 0.6.0", + "convert_case 0.7.1", "proc-macro2", "quote", "syn 1.0.109", @@ -14460,6 +14530,7 @@ dependencies = [ "command_palette_hooks", "editor", "futures 0.3.31", + "git_ui", "gpui", "indoc", "itertools 0.14.0", @@ -15333,7 +15404,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -16334,7 +16405,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.171.0" +version = "0.172.0" dependencies = [ "activity_indicator", "anyhow", @@ -16342,6 +16413,7 @@ dependencies = [ "assets", "assistant", "assistant2", + "assistant_context_editor", "assistant_settings", "assistant_tools", "async-watch", @@ -16458,6 +16530,7 @@ dependencies = [ "winresource", "workspace", "zed_actions", + "zed_predict_tos", "zeta", ] @@ -16472,7 +16545,7 @@ dependencies = [ [[package]] name = "zed_csharp" -version = "0.1.0" +version = "0.1.1" dependencies = [ "zed_extension_api 0.1.0", ] @@ -16486,7 +16559,7 @@ dependencies = [ [[package]] name = "zed_elixir" -version = "0.1.2" +version = "0.1.4" dependencies = [ "zed_extension_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -16545,14 +16618,14 @@ dependencies = [ [[package]] name = "zed_haskell" -version = "0.1.2" +version = "0.1.3" dependencies = [ "zed_extension_api 0.1.0", ] [[package]] name = "zed_html" -version = "0.1.4" +version = "0.1.5" dependencies = [ "zed_extension_api 0.1.0", ] @@ -16566,11 +16639,22 @@ dependencies = [ [[package]] name = "zed_php" -version = "0.2.3" +version = "0.2.4" dependencies = [ "zed_extension_api 0.1.0", ] +[[package]] +name = "zed_predict_tos" +version = "0.1.0" +dependencies = [ + "client", + "gpui", + "menu", + "ui", + "workspace", +] + [[package]] name = "zed_prisma" version = "0.0.4" @@ -16587,7 +16671,7 @@ dependencies = [ [[package]] name = "zed_purescript" -version = "0.0.1" +version = "0.1.0" dependencies = [ "zed_extension_api 0.1.0", ] @@ -16609,7 +16693,7 @@ dependencies = [ [[package]] name = "zed_terraform" -version = "0.1.1" +version = "0.1.2" dependencies = [ "zed_extension_api 0.1.0", ] @@ -16637,7 +16721,7 @@ dependencies = [ [[package]] name = "zed_zig" -version = "0.3.2" +version = "0.3.3" dependencies = [ "zed_extension_api 0.1.0", ] @@ -16766,9 +16850,11 @@ dependencies = [ "client", "clock", "collections", + "command_palette_hooks", "ctor", "editor", "env_logger 0.11.6", + "feature_flags", "futures 0.3.31", "gpui", "http_client", diff --git a/Cargo.toml b/Cargo.toml index e0813c1b7cb041..35812c432d9a22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,12 @@ resolver = "2" members = [ "crates/activity_indicator", + "crates/zed_predict_tos", "crates/anthropic", "crates/assets", "crates/assistant", "crates/assistant2", + "crates/assistant_context_editor", "crates/assistant_settings", "crates/assistant_slash_command", "crates/assistant_slash_commands", @@ -33,6 +35,7 @@ members = [ "crates/debugger_ui", "crates/db", "crates/diagnostics", + "crates/deepseek", "crates/docs_preprocessor", "crates/editor", "crates/evals", @@ -202,10 +205,12 @@ edition = "2021" activity_indicator = { path = "crates/activity_indicator" } ai = { path = "crates/ai" } +zed_predict_tos = { path = "crates/zed_predict_tos" } anthropic = { path = "crates/anthropic" } assets = { path = "crates/assets" } assistant = { path = "crates/assistant" } assistant2 = { path = "crates/assistant2" } +assistant_context_editor = { path = "crates/assistant_context_editor" } assistant_settings = { path = "crates/assistant_settings" } assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_slash_commands = { path = "crates/assistant_slash_commands" } @@ -233,6 +238,7 @@ dap_adapters = { path = "crates/dap_adapters" } db = { path = "crates/db" } debugger_ui = { path = "crates/debugger_ui" } debugger_tools = { path = "crates/debugger_tools" } +deepseek = { path = "crates/deepseek" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } extension = { path = "crates/extension" } @@ -388,7 +394,7 @@ chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.4", features = ["derive"] } cocoa = "0.26" cocoa-foundation = "0.2.0" -convert_case = "0.6.0" +convert_case = "0.7.0" core-foundation = "0.9.3" core-foundation-sys = "0.8.6" ctor = "0.2.6" @@ -404,7 +410,8 @@ fork = "0.2.0" futures = "0.3" futures-batch = "0.6.1" futures-lite = "1.13" -git2 = { version = "0.19", default-features = false } +# TODO: get back to regular versions when https://github.com/rust-lang/git2-rs/pull/1120 is released +git2 = { git = "https://github.com/rust-lang/git2-rs", rev = "a3b90cb3756c1bb63e2317bf9cfa57838178de5c", default-features = false } globset = "0.4" handlebars = "4.3" heed = { version = "0.21.0", features = ["read-txn-no-tls"] } @@ -493,6 +500,7 @@ strum = { version = "0.26.0", features = ["derive"] } subtle = "2.5.0" sys-locale = "0.3.1" sysinfo = "0.31.0" +take-until = "0.2.0" tempfile = "3.9.0" thiserror = "1.0.29" tiktoken-rs = "0.6.0" @@ -548,7 +556,7 @@ wasmtime-wasi = "24" which = "6.0.0" wit-component = "0.201" zstd = "0.11" -metal = "0.30" +metal = "0.31" [workspace.dependencies.async-stripe] git = "https://github.com/zed-industries/async-stripe" diff --git a/assets/icons/ai_deep_seek.svg b/assets/icons/ai_deep_seek.svg new file mode 100644 index 00000000000000..cf480c834c9f01 --- /dev/null +++ b/assets/icons/ai_deep_seek.svg @@ -0,0 +1 @@ +DeepSeek diff --git a/assets/icons/file.svg b/assets/icons/file.svg index 5f256b42e1fde3..5b1b8927564f67 100644 --- a/assets/icons/file.svg +++ b/assets/icons/file.svg @@ -1,8 +1,4 @@ - - + + + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg index a76dc63d1a6639..1a40805a702c02 100644 --- a/assets/icons/folder.svg +++ b/assets/icons/folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/globe.svg b/assets/icons/globe.svg index 2082a43984a0cc..545b83aa718123 100644 --- a/assets/icons/globe.svg +++ b/assets/icons/globe.svg @@ -1 +1,12 @@ - + + + + + + + + + + + + diff --git a/assets/icons/message_bubbles.svg b/assets/icons/message_bubbles.svg index f4ff1851a30861..03a6c7760cdce8 100644 --- a/assets/icons/message_bubbles.svg +++ b/assets/icons/message_bubbles.svg @@ -1,4 +1,6 @@ - - - + + + + + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b3bf895c5d1c32..2fc6c3c344dfe7 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -2,26 +2,27 @@ // Standard Linux bindings { "bindings": { - "shift-tab": "menu::SelectPrev", "home": "menu::SelectFirst", - "pageup": "menu::SelectFirst", "shift-pageup": "menu::SelectFirst", - "ctrl-p": "menu::SelectPrev", - "tab": "menu::SelectNext", + "pageup": "menu::SelectFirst", "end": "menu::SelectLast", + "shift-pagedown": "menu::SelectLast", "pagedown": "menu::SelectLast", - "shift-pagedown": "menu::SelectFirst", "ctrl-n": "menu::SelectNext", + "tab": "menu::SelectNext", + "ctrl-p": "menu::SelectPrev", + "shift-tab": "menu::SelectPrev", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", - "escape": "menu::Cancel", "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", + "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", "alt-enter": ["picker::ConfirmInput", { "secondary": false }], "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], "ctrl-shift-w": "workspace::CloseWindow", "shift-escape": "workspace::ToggleZoom", + "open": "workspace::Open", "ctrl-o": "workspace::Open", "ctrl-=": "zed::IncreaseBufferFontSize", "ctrl-+": "zed::IncreaseBufferFontSize", @@ -29,7 +30,9 @@ "ctrl-0": "zed::ResetBufferFontSize", "ctrl-,": "zed::OpenSettings", "ctrl-q": "zed::Quit", - "f11": "zed::ToggleFullScreen" + "f11": "zed::ToggleFullScreen", + "ctrl-alt-z": "zeta::RateCompletions", + "ctrl-shift-i": "inline_completion::ToggleMenu" } }, { @@ -50,8 +53,8 @@ "context": "Editor", "bindings": { "escape": "editor::Cancel", - "backspace": "editor::Backspace", "shift-backspace": "editor::Backspace", + "backspace": "editor::Backspace", "delete": "editor::Delete", "tab": "editor::Tab", "shift-tab": "editor::TabPrev", @@ -61,11 +64,19 @@ "ctrl-k q": "editor::Rewrap", "ctrl-backspace": "editor::DeleteToPreviousWordStart", "ctrl-delete": "editor::DeleteToNextWordEnd", + "cut": "editor::Cut", "shift-delete": "editor::Cut", + "ctrl-x": "editor::Cut", + "copy": "editor::Copy", "ctrl-insert": "editor::Copy", + "ctrl-c": "editor::Copy", + "paste": "editor::Paste", "shift-insert": "editor::Paste", - "ctrl-y": "editor::Redo", + "ctrl-v": "editor::Paste", + "undo": "editor::Undo", "ctrl-z": "editor::Undo", + "redo": "editor::Redo", + "ctrl-y": "editor::Redo", "ctrl-shift-z": "editor::Redo", "up": "editor::MoveUp", "ctrl-up": "editor::LineUp", @@ -97,41 +108,34 @@ "ctrl-l": "editor::SelectLine", "ctrl-shift-i": "editor::Format", // "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true }], - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }], // "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }], + "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }], // "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], // "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], + "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], // "alt-v": ["editor::MovePageUp", { "center_cursor": true }], "ctrl-alt-space": "editor::ShowCharacterPalette", "ctrl-;": "editor::ToggleLineNumbers", "ctrl-k ctrl-r": "editor::RevertSelectedHunks", - "ctrl-'": "editor::ToggleHunkDiff", + "ctrl-'": "editor::ToggleSelectedDiffHunks", "ctrl-\"": "editor::ExpandAllHunkDiffs", "ctrl-i": "editor::ShowSignatureHelp", "alt-g b": "editor::ToggleGitBlame", "menu": "editor::OpenContextMenu", - "shift-f10": "editor::OpenContextMenu" - } - }, - { - // Separate block with same context so these display in context menus - "context": "Editor", - "bindings": { - "ctrl-x": "editor::Cut", - "ctrl-c": "editor::Copy", - "ctrl-v": "editor::Paste" + "shift-f10": "editor::OpenContextMenu", + "alt-enter": "editor::OpenSelectionsInMultibuffer" } }, { "context": "Editor && mode == full", "bindings": { - "enter": "editor::Newline", "shift-enter": "editor::Newline", + "enter": "editor::Newline", "ctrl-enter": "editor::NewlineAbove", "ctrl-shift-enter": "editor::NewlineBelow", "ctrl-k ctrl-z": "editor::ToggleSoftWrap", "ctrl-k z": "editor::ToggleSoftWrap", + "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", "ctrl-h": ["buffer_search::Deploy", { "replace_enabled": true }], // "cmd-e": ["buffer_search::Deploy", { "focus": false }], @@ -165,6 +169,7 @@ { "context": "Markdown", "bindings": { + "copy": "markdown::Copy", "ctrl-c": "markdown::Copy" } }, @@ -178,12 +183,14 @@ "ctrl-alt-/": "assistant::ToggleModelSelector", "ctrl-k h": "assistant::DeployHistory", "ctrl-k l": "assistant::DeployPromptLibrary", + "new": "assistant::NewContext", "ctrl-n": "assistant::NewContext" } }, { "context": "PromptLibrary", "bindings": { + "new": "prompt_library::NewPrompt", "ctrl-n": "prompt_library::NewPrompt", "ctrl-shift-s": "prompt_library::ToggleDefaultPrompt" } @@ -197,6 +204,7 @@ "shift-enter": "search::SelectPrevMatch", "alt-enter": "search::SelectAllMatches", "ctrl-f": "search::FocusSearch", + "find": "search::FocusSearch", "ctrl-h": "search::ToggleReplace", "ctrl-l": "search::ToggleSelection" } @@ -219,6 +227,7 @@ "context": "ProjectSearchBar", "bindings": { "escape": "project_search::ToggleFocus", + "shift-find": "search::FocusSearch", "ctrl-shift-f": "search::FocusSearch", "ctrl-shift-h": "search::ToggleReplace", "alt-ctrl-g": "search::ToggleRegex", @@ -251,32 +260,48 @@ { "context": "Pane", "bindings": { + "alt-1": ["pane::ActivateItem", 0], + "alt-2": ["pane::ActivateItem", 1], + "alt-3": ["pane::ActivateItem", 2], + "alt-4": ["pane::ActivateItem", 3], + "alt-5": ["pane::ActivateItem", 4], + "alt-6": ["pane::ActivateItem", 5], + "alt-7": ["pane::ActivateItem", 6], + "alt-8": ["pane::ActivateItem", 7], + "alt-9": ["pane::ActivateItem", 8], + "alt-0": "pane::ActivateLastItem", "ctrl-pageup": "pane::ActivatePrevItem", "ctrl-pagedown": "pane::ActivateNextItem", "ctrl-shift-pageup": "pane::SwapItemLeft", "ctrl-shift-pagedown": "pane::SwapItemRight", - "back": "pane::GoBack", - "forward": "pane::GoForward", - "ctrl-w": "pane::CloseActiveItem", "ctrl-f4": "pane::CloseActiveItem", + "ctrl-w": "pane::CloseActiveItem", "alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }], "alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes", "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }], "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }], - "ctrl-shift-f": "pane::DeploySearch", + "back": "pane::GoBack", + "ctrl-alt--": "pane::GoBack", + "ctrl-alt-_": "pane::GoForward", + "forward": "pane::GoForward", "ctrl-alt-g": "search::SelectNextMatch", + "f3": "search::SelectNextMatch", "ctrl-alt-shift-g": "search::SelectPrevMatch", + "shift-f3": "search::SelectPrevMatch", + "ctrl-shift-f": "project_search::ToggleFocus", + "shift-find": "project_search::ToggleFocus", "ctrl-alt-shift-h": "search::ToggleReplace", "ctrl-alt-shift-l": "search::ToggleSelection", "alt-enter": "search::SelectAllMatches", "alt-c": "search::ToggleCaseSensitive", "alt-w": "search::ToggleWholeWord", - "alt-r": "search::ToggleRegex", "alt-ctrl-f": "project_search::ToggleFilters", + "alt-find": "project_search::ToggleFilters", "ctrl-alt-shift-r": "search::ToggleRegex", "ctrl-alt-shift-x": "search::ToggleRegex", + "alt-r": "search::ToggleRegex", "ctrl-k shift-enter": "pane::TogglePinTab" } }, @@ -351,39 +376,25 @@ "ctrl-g": "go_to_line::Toggle" } }, - { - "context": "Pane", - "bindings": { - "alt-1": ["pane::ActivateItem", 0], - "alt-2": ["pane::ActivateItem", 1], - "alt-3": ["pane::ActivateItem", 2], - "alt-4": ["pane::ActivateItem", 3], - "alt-5": ["pane::ActivateItem", 4], - "alt-6": ["pane::ActivateItem", 5], - "alt-7": ["pane::ActivateItem", 6], - "alt-8": ["pane::ActivateItem", 7], - "alt-9": ["pane::ActivateItem", 8], - "alt-0": "pane::ActivateLastItem", - "ctrl-alt--": "pane::GoBack", - "ctrl-alt-_": "pane::GoForward", - "f3": "search::SelectNextMatch", - "shift-f3": "search::SelectPrevMatch", - "ctrl-shift-f": "project_search::ToggleFocus" - } - }, { "context": "Workspace", "bindings": { // Change the default action on `menu::Confirm` by setting the parameter // "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }], + "alt-open": "projects::OpenRecent", "alt-ctrl-o": "projects::OpenRecent", + "alt-shift-open": "projects::OpenRemote", "alt-ctrl-shift-o": "projects::OpenRemote", "alt-ctrl-shift-b": "branches::OpenRecent", "ctrl-~": "workspace::NewTerminal", + "save": "workspace::Save", "ctrl-s": "workspace::Save", "ctrl-k s": "workspace::SaveWithoutFormat", + "shift-save": "workspace::SaveAs", "ctrl-shift-s": "workspace::SaveAs", + "new": "workspace::NewFile", "ctrl-n": "workspace::NewFile", + "shift-new": "workspace::NewWindow", "ctrl-shift-n": "workspace::NewWindow", "ctrl-`": "terminal_panel::ToggleFocus", "alt-1": ["workspace::ActivatePane", 0], @@ -399,6 +410,7 @@ "ctrl-b": "workspace::ToggleLeftDock", "ctrl-j": "workspace::ToggleBottomDock", "ctrl-alt-y": "workspace::CloseAllDocks", + "shift-find": "pane::DeploySearch", "ctrl-shift-f": "pane::DeploySearch", "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], "ctrl-shift-t": "pane::ReopenClosedItem", @@ -409,12 +421,14 @@ "ctrl-tab": "tab_switcher::Toggle", "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], "ctrl-e": "file_finder::Toggle", - "ctrl-shift-p": "command_palette::Toggle", "f1": "command_palette::Toggle", + "ctrl-shift-p": "command_palette::Toggle", "ctrl-shift-m": "diagnostics::Deploy", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-b": "outline_panel::ToggleFocus", + "ctrl-shift-g": "git_panel::ToggleFocus", "ctrl-?": "assistant::ToggleFocus", + "alt-save": "workspace::SaveAll", "ctrl-alt-s": "workspace::SaveAll", "ctrl-k m": "language_selector::Toggle", "escape": "workspace::Unfollow", @@ -447,7 +461,6 @@ { "context": "Editor", "bindings": { - "ctrl-shift-k": "editor::DeleteLine", "ctrl-shift-d": "editor::DuplicateLineDown", "ctrl-shift-j": "editor::JoinLines", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", @@ -505,10 +518,10 @@ { "context": "Editor && (showing_code_actions || showing_completions)", "bindings": { - "up": "editor::ContextMenuPrev", "ctrl-p": "editor::ContextMenuPrev", - "down": "editor::ContextMenuNext", + "up": "editor::ContextMenuPrev", "ctrl-n": "editor::ContextMenuNext", + "down": "editor::ContextMenuNext", "pageup": "editor::ContextMenuFirst", "pagedown": "editor::ContextMenuLast" } @@ -532,7 +545,7 @@ "bindings": { "alt-enter": "editor::OpenExcerpts", "shift-enter": "editor::ExpandExcerpts", - "ctrl-k enter": "editor::OpenExcerptsSplit", + "ctrl-alt-enter": "editor::OpenExcerptsSplit", "ctrl-shift-e": "pane::RevealInProjectPanel", "ctrl-f8": "editor::GoToHunk", "ctrl-shift-f8": "editor::GoToPrevHunk", @@ -559,6 +572,7 @@ "ctrl-enter": "assistant::Assist", "ctrl-shift-enter": "assistant::Edit", "ctrl-s": "workspace::Save", + "save": "workspace::Save", "ctrl->": "assistant::QuoteSelection", "ctrl-<": "assistant::InsertIntoEditor", "shift-enter": "assistant::Split", @@ -571,6 +585,7 @@ "context": "AssistantPanel2", "bindings": { "ctrl-n": "assistant2::NewThread", + "new": "assistant2::NewThread", "ctrl-shift-h": "assistant2::OpenHistory", "ctrl-alt-/": "assistant2::ToggleModelSelector", "ctrl-shift-a": "assistant2::ToggleContextPicker", @@ -623,14 +638,16 @@ "escape": "menu::Cancel", "left": "outline_panel::CollapseSelectedEntry", "right": "outline_panel::ExpandSelectedEntry", + "alt-copy": "outline_panel::CopyPath", "ctrl-alt-c": "outline_panel::CopyPath", + "alt-shift-copy": "outline_panel::CopyRelativePath", "alt-ctrl-shift-c": "outline_panel::CopyRelativePath", "alt-ctrl-r": "outline_panel::RevealInFileManager", "space": "outline_panel::Open", "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrev", "alt-enter": "editor::OpenExcerpts", - "ctrl-k enter": "editor::OpenExcerptsSplit" + "ctrl-alt-enter": "editor::OpenExcerptsSplit" } }, { @@ -638,40 +655,70 @@ "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", + "new": "project_panel::NewFile", "ctrl-n": "project_panel::NewFile", + "alt-new": "project_panel::NewDirectory", "alt-ctrl-n": "project_panel::NewDirectory", + "cut": "project_panel::Cut", + "ctrl-x": "project_panel::Cut", + "copy": "project_panel::Copy", "ctrl-insert": "project_panel::Copy", + "ctrl-c": "project_panel::Copy", + "paste": "project_panel::Paste", "shift-insert": "project_panel::Paste", + "ctrl-v": "project_panel::Paste", + "alt-copy": "project_panel::CopyPath", "ctrl-alt-c": "project_panel::CopyPath", + "alt-shift-copy": "project_panel::CopyRelativePath", "alt-ctrl-shift-c": "project_panel::CopyRelativePath", "enter": "project_panel::Rename", + "f2": "project_panel::Rename", "backspace": ["project_panel::Trash", { "skip_prompt": false }], + "delete": ["project_panel::Trash", { "skip_prompt": false }], "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-ctrl-r": "project_panel::RevealInFileManager", "ctrl-shift-enter": "project_panel::OpenWithSystem", "ctrl-shift-f": "project_panel::NewSearchInDirectory", + "shift-find": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrev", "escape": "menu::Cancel" } }, { - // Separate block with same context so these display in context menus - "context": "ProjectPanel", + "context": "ProjectPanel && not_editing", "bindings": { - "f2": "project_panel::Rename", - "ctrl-c": "project_panel::Copy", - "ctrl-x": "project_panel::Cut", - "ctrl-v": "project_panel::Paste", - "delete": ["project_panel::Trash", { "skip_prompt": false }] + "space": "project_panel::Open" } }, { - "context": "ProjectPanel && not_editing", + "context": "GitPanel && !CommitEditor", + "use_key_equivalents": true, "bindings": { - "space": "project_panel::Open" + "escape": "git_panel::Close" + } + }, + { + "context": "GitPanel && ChangesList", + "use_key_equivalents": true, + "bindings": { + "up": "menu::SelectPrev", + "down": "menu::SelectNext", + "enter": "menu::Confirm", + "space": "git::ToggleStaged", + "ctrl-space": "git::StageAll", + "ctrl-shift-space": "git::UnstageAll" + } + }, + { + "context": "GitPanel && CommitEditor > Editor", + "use_key_equivalents": true, + "bindings": { + "escape": "git_panel::FocusChanges", + "ctrl-enter": "git::CommitChanges", + "ctrl-shift-enter": "git::CommitAllChanges" } }, { @@ -734,9 +781,9 @@ { "context": "TabSwitcher", "bindings": { + "ctrl-shift-tab": "menu::SelectPrev", "ctrl-up": "menu::SelectPrev", "ctrl-down": "menu::SelectNext", - "ctrl-shift-tab": "menu::SelectPrev", "ctrl-backspace": "tab_switcher::CloseSelectedItem" } }, @@ -744,12 +791,17 @@ "context": "Terminal", "bindings": { "ctrl-alt-space": "terminal::ShowCharacterPalette", + "copy": "terminal::Copy", "ctrl-insert": "terminal::Copy", + "ctrl-shift-c": "terminal::Copy", + "paste": "terminal::Paste", "shift-insert": "terminal::Paste", + "ctrl-shift-v": "terminal::Paste", "ctrl-enter": "assistant::InlineAssist", // Overrides for conflicting keybindings "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"], "ctrl-shift-a": "editor::SelectAll", + "find": "buffer_search::Deploy", "ctrl-shift-f": "buffer_search::Deploy", "ctrl-shift-l": "terminal::Clear", "ctrl-shift-w": "pane::CloseActiveItem", @@ -769,13 +821,5 @@ "shift-end": "terminal::ScrollToBottom", "ctrl-shift-space": "terminal::ToggleViMode" } - }, - { - // Separate block with same context so these display in context menus - "context": "Terminal", - "bindings": { - "ctrl-shift-c": "terminal::Copy", - "ctrl-shift-v": "terminal::Paste" - } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 71886f5bb5e973..1aa1915c12896f 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -3,27 +3,27 @@ { "use_key_equivalents": true, "bindings": { - "up": "menu::SelectPrev", - "shift-tab": "menu::SelectPrev", "home": "menu::SelectFirst", - "pageup": "menu::SelectFirst", "shift-pageup": "menu::SelectFirst", - "ctrl-p": "menu::SelectPrev", - "down": "menu::SelectNext", - "tab": "menu::SelectNext", + "pageup": "menu::SelectFirst", + "cmd-up": "menu::SelectFirst", "end": "menu::SelectLast", + "shift-pagedown": "menu::SelectLast", "pagedown": "menu::SelectLast", - "shift-pagedown": "menu::SelectFirst", - "ctrl-n": "menu::SelectNext", - "cmd-up": "menu::SelectFirst", "cmd-down": "menu::SelectLast", + "tab": "menu::SelectNext", + "ctrl-n": "menu::SelectNext", + "down": "menu::SelectNext", + "shift-tab": "menu::SelectPrev", + "ctrl-p": "menu::SelectPrev", + "up": "menu::SelectPrev", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", "cmd-enter": "menu::SecondaryConfirm", - "escape": "menu::Cancel", - "cmd-escape": "menu::Cancel", "ctrl-escape": "menu::Cancel", + "cmd-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", + "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", "cmd-shift-w": "workspace::CloseWindow", "shift-escape": "workspace::ToggleZoom", @@ -38,7 +38,9 @@ "alt-cmd-h": "zed::HideOthers", "cmd-m": "zed::Minimize", "fn-f": "zed::ToggleFullScreen", - "ctrl-cmd-f": "zed::ToggleFullScreen" + "ctrl-cmd-f": "zed::ToggleFullScreen", + "ctrl-shift-z": "zeta::RateCompletions", + "ctrl-shift-i": "inline_completion::ToggleMenu" } }, { @@ -46,18 +48,18 @@ "use_key_equivalents": true, "bindings": { "escape": "editor::Cancel", - "backspace": "editor::Backspace", "shift-backspace": "editor::Backspace", "ctrl-h": "editor::Backspace", - "delete": "editor::Delete", + "backspace": "editor::Backspace", "ctrl-d": "editor::Delete", + "delete": "editor::Delete", "tab": "editor::Tab", "shift-tab": "editor::TabPrev", "ctrl-t": "editor::Transpose", "ctrl-k": "editor::KillRingCut", "ctrl-y": "editor::KillRingYank", - "cmd-k q": "editor::Rewrap", "cmd-k cmd-q": "editor::Rewrap", + "cmd-k q": "editor::Rewrap", "cmd-backspace": "editor::DeleteToBeginningOfLine", "cmd-delete": "editor::DeleteToEndOfLine", "alt-backspace": "editor::DeleteToPreviousWordStart", @@ -68,34 +70,33 @@ "cmd-v": "editor::Paste", "cmd-z": "editor::Undo", "cmd-shift-z": "editor::Redo", - "ctrl-shift-z": "zeta::RateCompletions", "up": "editor::MoveUp", "ctrl-up": "editor::MoveToStartOfParagraph", "pageup": "editor::MovePageUp", "shift-pageup": "editor::SelectPageUp", "cmd-pageup": "editor::PageUp", "ctrl-pageup": "editor::LineUp", - "home": "editor::MoveToBeginningOfLine", "down": "editor::MoveDown", "ctrl-down": "editor::MoveToEndOfParagraph", "pagedown": "editor::MovePageDown", "shift-pagedown": "editor::SelectPageDown", "cmd-pagedown": "editor::PageDown", "ctrl-pagedown": "editor::LineDown", - "end": "editor::MoveToEndOfLine", - "left": "editor::MoveLeft", - "right": "editor::MoveRight", "ctrl-p": "editor::MoveUp", "ctrl-n": "editor::MoveDown", "ctrl-b": "editor::MoveLeft", + "left": "editor::MoveLeft", "ctrl-f": "editor::MoveRight", + "right": "editor::MoveRight", "ctrl-l": "editor::ScrollCursorCenter", "alt-left": "editor::MoveToPreviousWordStart", "alt-right": "editor::MoveToNextWordEnd", "cmd-left": "editor::MoveToBeginningOfLine", "ctrl-a": "editor::MoveToBeginningOfLine", + "home": "editor::MoveToBeginningOfLine", "cmd-right": "editor::MoveToEndOfLine", "ctrl-e": "editor::MoveToEndOfLine", + "end": "editor::MoveToEndOfLine", "cmd-up": "editor::MoveToBeginning", "cmd-down": "editor::MoveToEnd", "shift-up": "editor::SelectUp", @@ -126,20 +127,21 @@ "ctrl-cmd-space": "editor::ShowCharacterPalette", "cmd-;": "editor::ToggleLineNumbers", "cmd-alt-z": "editor::RevertSelectedHunks", - "cmd-'": "editor::ToggleHunkDiff", + "cmd-'": "editor::ToggleSelectedDiffHunks", "cmd-\"": "editor::ExpandAllHunkDiffs", "cmd-alt-g b": "editor::ToggleGitBlame", "cmd-i": "editor::ShowSignatureHelp", "ctrl-f12": "editor::GoToDeclaration", - "alt-ctrl-f12": "editor::GoToDeclarationSplit" + "alt-ctrl-f12": "editor::GoToDeclarationSplit", + "alt-enter": "editor::OpenSelectionsInMultibuffer" } }, { "context": "Editor && mode == full", "use_key_equivalents": true, "bindings": { - "enter": "editor::Newline", "shift-enter": "editor::Newline", + "enter": "editor::Newline", "cmd-enter": "editor::NewlineBelow", "cmd-shift-enter": "editor::NewlineAbove", "cmd-k z": "editor::ToggleSoftWrap", @@ -158,7 +160,7 @@ "bindings": { "alt-tab": "editor::NextInlineCompletion", "alt-shift-tab": "editor::PreviousInlineCompletion", - "ctrl-right": "editor::AcceptPartialInlineCompletion" + "ctrl-cmd-right": "editor::AcceptPartialInlineCompletion" } }, { @@ -226,6 +228,7 @@ "use_key_equivalents": true, "bindings": { "cmd-n": "assistant2::NewThread", + "cmd-alt-p": "assistant2::NewPromptEditor", "cmd-shift-h": "assistant2::OpenHistory", "cmd-alt-/": "assistant2::ToggleModelSelector", "cmd-shift-a": "assistant2::ToggleContextPicker", @@ -340,10 +343,10 @@ "context": "Pane", "use_key_equivalents": true, "bindings": { - "cmd-{": "pane::ActivatePrevItem", - "cmd-}": "pane::ActivateNextItem", "alt-cmd-left": "pane::ActivatePrevItem", + "cmd-{": "pane::ActivatePrevItem", "alt-cmd-right": "pane::ActivateNextItem", + "cmd-}": "pane::ActivateNextItem", "ctrl-shift-pageup": "pane::SwapItemLeft", "ctrl-shift-pagedown": "pane::SwapItemRight", "cmd-w": "pane::CloseActiveItem", @@ -373,10 +376,10 @@ "bindings": { "cmd-[": "editor::Outdent", "cmd-]": "editor::Indent", - "cmd-alt-up": "editor::AddSelectionAbove", // Insert cursor above - "cmd-ctrl-p": "editor::AddSelectionAbove", - "cmd-alt-down": "editor::AddSelectionBelow", // Insert cursor below - "cmd-ctrl-n": "editor::AddSelectionBelow", + "cmd-ctrl-p": "editor::AddSelectionAbove", // Insert cursor above + "cmd-alt-up": "editor::AddSelectionAbove", + "cmd-ctrl-n": "editor::AddSelectionBelow", // Insert cursor below + "cmd-alt-down": "editor::AddSelectionBelow", "cmd-shift-k": "editor::DeleteLine", "alt-up": "editor::MoveLineUp", "alt-down": "editor::MoveLineDown", @@ -403,8 +406,8 @@ "shift-f12": "editor::GoToImplementation", "alt-cmd-f12": "editor::GoToTypeDefinitionSplit", "alt-shift-f12": "editor::FindAllReferences", - "ctrl-m": "editor::MoveToEnclosingBracket", "cmd-|": "editor::MoveToEnclosingBracket", + "ctrl-m": "editor::MoveToEnclosingBracket", "alt-cmd-[": "editor::Fold", "alt-cmd-]": "editor::UnfoldLines", "cmd-k cmd-l": "editor::ToggleFold", @@ -501,6 +504,7 @@ "cmd-shift-m": "diagnostics::Deploy", "cmd-shift-e": "project_panel::ToggleFocus", "cmd-shift-b": "outline_panel::ToggleFocus", + "ctrl-shift-g": "git_panel::ToggleFocus", "cmd-?": "assistant::ToggleFocus", "cmd-alt-s": "workspace::SaveAll", "cmd-k m": "language_selector::Toggle", @@ -618,7 +622,7 @@ "bindings": { "alt-enter": "editor::OpenExcerpts", "shift-enter": "editor::ExpandExcerpts", - "cmd-k enter": "editor::OpenExcerptsSplit", + "cmd-alt-enter": "editor::OpenExcerptsSplit", "cmd-shift-e": "pane::RevealInProjectPanel", "cmd-f8": "editor::GoToHunk", "cmd-shift-f8": "editor::GoToPrevHunk", @@ -665,7 +669,7 @@ "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrev", "alt-enter": "editor::OpenExcerpts", - "cmd-k enter": "editor::OpenExcerptsSplit" + "cmd-alt-enter": "editor::OpenExcerptsSplit" } }, { @@ -814,9 +818,9 @@ "context": "TabSwitcher", "use_key_equivalents": true, "bindings": { + "ctrl-shift-tab": "menu::SelectPrev", "ctrl-up": "menu::SelectPrev", "ctrl-down": "menu::SelectNext", - "ctrl-shift-tab": "menu::SelectPrev", "ctrl-backspace": "tab_switcher::CloseSelectedItem" } }, @@ -847,16 +851,16 @@ "escape": ["terminal::SendKeystroke", "escape"], "enter": ["terminal::SendKeystroke", "enter"], "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"], - "cmd-up": "terminal::ScrollPageUp", - "cmd-down": "terminal::ScrollPageDown", "shift-pageup": "terminal::ScrollPageUp", + "cmd-up": "terminal::ScrollPageUp", "shift-pagedown": "terminal::ScrollPageDown", + "cmd-down": "terminal::ScrollPageDown", "shift-up": "terminal::ScrollLineUp", "shift-down": "terminal::ScrollLineDown", - "cmd-home": "terminal::ScrollToTop", - "cmd-end": "terminal::ScrollToBottom", "shift-home": "terminal::ScrollToTop", + "cmd-home": "terminal::ScrollToTop", "shift-end": "terminal::ScrollToBottom", + "cmd-end": "terminal::ScrollToBottom", "ctrl-shift-space": "terminal::ToggleViMode", "ctrl-k up": "pane::SplitUp", "ctrl-k down": "pane::SplitDown", @@ -883,5 +887,12 @@ "cmd-shift-enter": "zeta::ThumbsUpActiveCompletion", "cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion" } + }, + { + "context": "ZedPredictTos", + "use_key_equivalents": true, + "bindings": { + "escape": "menu::Cancel" + } } ] diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 816e20ad787a66..2c1128d8d66922 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -15,7 +15,9 @@ "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer "alt-g g": "go_to_line::Toggle", // goto-line "alt-g alt-g": "go_to_line::Toggle", // goto-line - //"ctrl-space": "editor::SetMark", + "ctrl-space": "editor::SetMark", // set-mark + "ctrl-@": "editor::SetMark", // set-mark + "ctrl-x ctrl-x": "editor::SwapSelectionEnds", // exchange-point-and-mark "ctrl-f": "editor::MoveRight", // forward-char "ctrl-b": "editor::MoveLeft", // backward-char "ctrl-n": "editor::MoveDown", // next-line @@ -24,6 +26,8 @@ "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line "ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line + "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line + "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line "alt-f": "editor::MoveToNextSubwordEnd", // forward-word "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word "alt-u": "editor::ConvertToUpperCase", // upcase-word @@ -55,6 +59,32 @@ "alt-^": "editor::JoinLines" // join-line } }, + { + "context": "Editor && selection_mode", // region selection + "bindings": { + "right": "editor::SelectRight", + "left": "editor::SelectLeft", + "down": "editor::SelectDown", + "up": "editor::SelectUp", + "alt-left": "editor::SelectToPreviousWordStart", + "alt-right": "editor::SelectToNextWordEnd", + "pagedown": "editor::SelectPageDown", + "pageup": "editor::SelectPageUp", + "ctrl-f": "editor::SelectRight", + "ctrl-b": "editor::SelectLeft", + "ctrl-n": "editor::SelectDown", + "ctrl-p": "editor::SelectUp", + "home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], + "end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], + "ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], + "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], + "alt-f": "editor::SelectToNextWordEnd", + "alt-b": "editor::SelectToPreviousSubwordStart", + "alt-<": "editor::SelectToBeginning", + "alt->": "editor::SelectToEnd", + "ctrl-g": "editor::Cancel" + } + }, { "context": "Workspace", "bindings": { diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 816e20ad787a66..2c1128d8d66922 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -15,7 +15,9 @@ "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer "alt-g g": "go_to_line::Toggle", // goto-line "alt-g alt-g": "go_to_line::Toggle", // goto-line - //"ctrl-space": "editor::SetMark", + "ctrl-space": "editor::SetMark", // set-mark + "ctrl-@": "editor::SetMark", // set-mark + "ctrl-x ctrl-x": "editor::SwapSelectionEnds", // exchange-point-and-mark "ctrl-f": "editor::MoveRight", // forward-char "ctrl-b": "editor::MoveLeft", // backward-char "ctrl-n": "editor::MoveDown", // next-line @@ -24,6 +26,8 @@ "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line "ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line + "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line + "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line "alt-f": "editor::MoveToNextSubwordEnd", // forward-word "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word "alt-u": "editor::ConvertToUpperCase", // upcase-word @@ -55,6 +59,32 @@ "alt-^": "editor::JoinLines" // join-line } }, + { + "context": "Editor && selection_mode", // region selection + "bindings": { + "right": "editor::SelectRight", + "left": "editor::SelectLeft", + "down": "editor::SelectDown", + "up": "editor::SelectUp", + "alt-left": "editor::SelectToPreviousWordStart", + "alt-right": "editor::SelectToNextWordEnd", + "pagedown": "editor::SelectPageDown", + "pageup": "editor::SelectPageUp", + "ctrl-f": "editor::SelectRight", + "ctrl-b": "editor::SelectLeft", + "ctrl-n": "editor::SelectDown", + "ctrl-p": "editor::SelectUp", + "home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], + "end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], + "ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], + "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], + "alt-f": "editor::SelectToNextWordEnd", + "alt-b": "editor::SelectToPreviousSubwordStart", + "alt-<": "editor::SelectToBeginning", + "alt->": "editor::SelectToEnd", + "ctrl-g": "editor::Cancel" + } + }, { "context": "Workspace", "bindings": { diff --git a/assets/keymaps/storybook.json b/assets/keymaps/storybook.json index 5e375821e0a07a..a59e084a8833a8 100644 --- a/assets/keymaps/storybook.json +++ b/assets/keymaps/storybook.json @@ -2,21 +2,27 @@ // Standard macOS bindings { "bindings": { - "up": "menu::SelectPrev", - "pageup": "menu::SelectFirst", + "home": "menu::SelectFirst", "shift-pageup": "menu::SelectFirst", - "ctrl-p": "menu::SelectPrev", - "down": "menu::SelectNext", - "pagedown": "menu::SelectLast", - "shift-pagedown": "menu::SelectFirst", - "ctrl-n": "menu::SelectNext", + "pageup": "menu::SelectFirst", "cmd-up": "menu::SelectFirst", + "end": "menu::SelectLast", + "shift-pagedown": "menu::SelectLast", + "pagedown": "menu::SelectLast", "cmd-down": "menu::SelectLast", + "tab": "menu::SelectNext", + "ctrl-n": "menu::SelectNext", + "down": "menu::SelectNext", + "shift-tab": "menu::SelectPrev", + "ctrl-p": "menu::SelectPrev", + "up": "menu::SelectPrev", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", "cmd-enter": "menu::SecondaryConfirm", - "escape": "menu::Cancel", + "ctrl-escape": "menu::Cancel", + "cmd-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", + "escape": "menu::Cancel", "cmd-q": "storybook::Quit", "backspace": "editor::Backspace", "delete": "editor::Delete", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1d4cd7f0c0d168..c45a71ff7e7fdf 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -4,25 +4,25 @@ "bindings": { "i": ["vim::PushOperator", { "Object": { "around": false } }], "a": ["vim::PushOperator", { "Object": { "around": true } }], - "h": "vim::Left", "left": "vim::Left", + "h": "vim::Left", "backspace": "vim::Backspace", - "j": "vim::Down", "down": "vim::Down", "ctrl-j": "vim::Down", - "enter": "vim::NextLineStart", + "j": "vim::Down", "ctrl-m": "vim::NextLineStart", "+": "vim::NextLineStart", + "enter": "vim::NextLineStart", "-": "vim::PreviousLineStart", - "tab": "vim::Tab", "shift-tab": "vim::Tab", - "k": "vim::Up", + "tab": "vim::Tab", "up": "vim::Up", - "l": "vim::Right", + "k": "vim::Up", "right": "vim::Right", + "l": "vim::Right", "space": "vim::Space", - "$": "vim::EndOfLine", "end": "vim::EndOfLine", + "$": "vim::EndOfLine", "^": "vim::FirstNonWhitespace", "_": "vim::StartOfLineDownward", "g _": "vim::EndOfLineDownward", @@ -86,6 +86,7 @@ "ctrl-[": ["vim::SwitchMode", "Normal"], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", + "ctrl-g": "vim::ShowLocation", "ctrl-v": "vim::ToggleVisualBlock", "ctrl-q": "vim::ToggleVisualBlock", "shift-k": "editor::Hover", @@ -188,8 +189,8 @@ { "context": "vim_mode == normal", "bindings": { - "escape": "editor::Cancel", "ctrl-[": "editor::Cancel", + "escape": "editor::Cancel", ":": "command_palette::Toggle", ".": "vim::Repeat", "c": ["vim::PushOperator", "Change"], @@ -226,8 +227,8 @@ "g shift-u": ["vim::PushOperator", "Uppercase"], "g ~": ["vim::PushOperator", "OppositeCase"], "\"": ["vim::PushOperator", "Register"], - "g q": ["vim::PushOperator", "Rewrap"], "g w": ["vim::PushOperator", "Rewrap"], + "g q": ["vim::PushOperator", "Rewrap"], "ctrl-pagedown": "pane::ActivateNextItem", "ctrl-pageup": "pane::ActivatePrevItem", "insert": "vim::InsertBefore", @@ -254,8 +255,8 @@ ":": "vim::VisualCommand", "u": "vim::ConvertToLowerCase", "shift-u": "vim::ConvertToUpperCase", - "o": "vim::OtherEnd", "shift-o": "vim::OtherEnd", + "o": "vim::OtherEnd", "d": "vim::VisualDelete", "x": "vim::VisualDelete", "shift-d": "vim::VisualDeleteLine", @@ -264,10 +265,10 @@ "shift-y": "vim::VisualYankLine", "p": "vim::Paste", "shift-p": ["vim::Paste", { "preserveClipboard": true }], + "c": "vim::Substitute", "s": "vim::Substitute", - "shift-s": "vim::SubstituteLine", "shift-r": "vim::SubstituteLine", - "c": "vim::Substitute", + "shift-s": "vim::SubstituteLine", "~": "vim::ChangeCase", "*": ["vim::MoveToNext", { "partialWord": true }], "#": ["vim::MoveToPrev", { "partialWord": true }], @@ -283,8 +284,8 @@ "g shift-j": "vim::JoinLinesNoWhitespace", "r": ["vim::PushOperator", "Replace"], "ctrl-c": ["vim::SwitchMode", "Normal"], - "escape": ["vim::SwitchMode", "Normal"], "ctrl-[": ["vim::SwitchMode", "Normal"], + "escape": ["vim::SwitchMode", "Normal"], ">": "vim::Indent", "<": "vim::Outdent", "=": "vim::AutoIndent", @@ -302,9 +303,9 @@ { "context": "vim_mode == insert", "bindings": { - "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore", "ctrl-[": "vim::NormalBefore", + "escape": "vim::NormalBefore", "ctrl-x": null, "ctrl-x ctrl-o": "editor::ShowCompletions", "ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific @@ -352,9 +353,9 @@ { "context": "vim_mode == replace", "bindings": { - "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore", "ctrl-[": "vim::NormalBefore", + "escape": "vim::NormalBefore", "ctrl-k": ["vim::PushOperator", { "Digraph": {} }], "ctrl-v": ["vim::PushOperator", { "Literal": {} }], "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. @@ -371,9 +372,9 @@ "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", - "escape": "vim::ClearOperators", "ctrl-c": "vim::ClearOperators", "ctrl-[": "vim::ClearOperators", + "escape": "vim::ClearOperators", "ctrl-k": ["vim::PushOperator", { "Digraph": {} }], "ctrl-v": ["vim::PushOperator", { "Literal": {} }], "ctrl-q": ["vim::PushOperator", { "Literal": {} }] @@ -382,9 +383,9 @@ { "context": "vim_mode == operator", "bindings": { - "escape": "vim::ClearOperators", "ctrl-c": "vim::ClearOperators", "ctrl-[": "vim::ClearOperators", + "escape": "vim::ClearOperators", "g c": "vim::Comment" } }, @@ -435,7 +436,7 @@ "bindings": { "d": "vim::CurrentLine", "s": ["vim::PushOperator", "DeleteSurrounds"], - "o": "editor::ToggleHunkDiff", // "d o" + "o": "editor::ToggleSelectedDiffHunks", // "d o" "p": "editor::RevertSelectedHunks" // "d p" } }, @@ -571,14 +572,14 @@ "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], @@ -603,19 +604,19 @@ "ctrl-w ctrl-p": "workspace::ActivatePreviousPane", "ctrl-w shift-w": "workspace::ActivatePreviousPane", "ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane", - "ctrl-w v": "pane::SplitVertical", "ctrl-w ctrl-v": "pane::SplitVertical", - "ctrl-w s": "pane::SplitHorizontal", + "ctrl-w v": "pane::SplitVertical", "ctrl-w shift-s": "pane::SplitHorizontal", "ctrl-w ctrl-s": "pane::SplitHorizontal", - "ctrl-w c": "pane::CloseAllItems", + "ctrl-w s": "pane::SplitHorizontal", "ctrl-w ctrl-c": "pane::CloseAllItems", - "ctrl-w q": "pane::CloseAllItems", + "ctrl-w c": "pane::CloseAllItems", "ctrl-w ctrl-q": "pane::CloseAllItems", - "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w q": "pane::CloseAllItems", "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w n": "workspace::NewFileSplitHorizontal", - "ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal" + "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal", + "ctrl-w n": "workspace::NewFileSplitHorizontal" } }, { @@ -667,5 +668,20 @@ "shift-g": "menu::SelectLast", "g g": "menu::SelectFirst" } + }, + { + "context": "GitPanel && ChangesList", + "use_key_equivalents": true, + "bindings": { + "k": "menu::SelectPrev", + "j": "menu::SelectNext", + "g g": "menu::SelectFirst", + "shift-g": "menu::SelectLast", + "g f": "menu::Confirm", + "i": "git_panel::FocusEditor", + "x": "git::ToggleStaged", + "shift-x": "git::StageAll", + "shift-u": "git::UnstageAll" + } } ] diff --git a/assets/settings/default.json b/assets/settings/default.json index 7f4d77be7afef8..e2dd25945ecc51 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -592,7 +592,9 @@ // Whether or not to show the tab bar in the editor "show": true, // Whether or not to show the navigation history buttons. - "show_nav_history_buttons": true + "show_nav_history_buttons": true, + /// Whether or not to show the tab bar buttons. + "show_tab_bar_buttons": true }, // Settings related to the editor's tabs "tabs": { @@ -1166,6 +1168,9 @@ }, "lmstudio": { "api_url": "http://localhost:1234/api/v0" + }, + "deepseek": { + "api_url": "https://api.deepseek.com" } }, // Zed's Prettier integration settings. diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index ec7d1e64690183..21ee6dc2ed7aa0 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -3,9 +3,9 @@ use editor::Editor; use extension_host::ExtensionStore; use futures::StreamExt; use gpui::{ - actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter, - InteractiveElement as _, Model, ParentElement as _, Render, SharedString, - StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _, + actions, percentage, Animation, AnimationExt as _, App, Context, CursorStyle, Entity, + EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString, + StatefulInteractiveElement, Styled, Transformation, Window, }; use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId}; use lsp::LanguageServerName; @@ -27,8 +27,8 @@ pub enum Event { pub struct ActivityIndicator { statuses: Vec, - project: Model, - auto_updater: Option>, + project: Entity, + auto_updater: Option>, context_menu_handle: PopoverMenuHandle, } @@ -46,22 +46,24 @@ struct PendingWork<'a> { struct Content { icon: Option, message: String, - on_click: Option)>>, + on_click: + Option)>>, } impl ActivityIndicator { pub fn new( workspace: &mut Workspace, languages: Arc, - cx: &mut ViewContext, - ) -> View { + window: &mut Window, + cx: &mut Context, + ) -> Entity { let project = workspace.project().clone(); let auto_updater = AutoUpdater::get(cx); - let this = cx.new_view(|cx: &mut ViewContext| { + let this = cx.new(|cx| { let mut status_events = languages.language_server_binary_statuses(); cx.spawn(|this, mut cx| async move { while let Some((name, status)) = status_events.next().await { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this: &mut ActivityIndicator, cx| { this.statuses.retain(|s| s.name != name); this.statuses.push(ServerStatus { name, status }); cx.notify(); @@ -98,7 +100,7 @@ impl ActivityIndicator { } }); - cx.subscribe(&this, move |_, _, event, cx| match event { + cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event { Event::ShowError { server_name: lsp_name, error, @@ -107,7 +109,7 @@ impl ActivityIndicator { let project = project.clone(); let error = error.clone(); let lsp_name = lsp_name.clone(); - cx.spawn(|workspace, mut cx| async move { + cx.spawn_in(window, |workspace, mut cx| async move { let buffer = create_buffer.await?; buffer.update(&mut cx, |buffer, cx| { buffer.edit( @@ -120,13 +122,14 @@ impl ActivityIndicator { ); buffer.set_capability(language::Capability::ReadOnly, cx); })?; - workspace.update(&mut cx, |workspace, cx| { + workspace.update_in(&mut cx, |workspace, window, cx| { workspace.add_item_to_active_pane( - Box::new(cx.new_view(|cx| { - Editor::for_buffer(buffer, Some(project.clone()), cx) + Box::new(cx.new(|cx| { + Editor::for_buffer(buffer, Some(project.clone()), window, cx) })), None, true, + window, cx, ); })?; @@ -140,7 +143,7 @@ impl ActivityIndicator { this } - fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext) { + fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context) { self.statuses.retain(|status| { if let LanguageServerBinaryStatus::Failed { error } = &status.status { cx.emit(Event::ShowError { @@ -156,7 +159,12 @@ impl ActivityIndicator { cx.notify(); } - fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { + fn dismiss_error_message( + &mut self, + _: &DismissErrorMessage, + _: &mut Window, + cx: &mut Context, + ) { if let Some(updater) = &self.auto_updater { updater.update(cx, |updater, cx| { updater.dismiss_error(cx); @@ -167,7 +175,7 @@ impl ActivityIndicator { fn pending_language_server_work<'a>( &self, - cx: &'a AppContext, + cx: &'a App, ) -> impl Iterator> { self.project .read(cx) @@ -195,12 +203,12 @@ impl ActivityIndicator { fn pending_environment_errors<'a>( &'a self, - cx: &'a AppContext, + cx: &'a App, ) -> impl Iterator { self.project.read(cx).shell_environment_errors(cx) } - fn content_to_render(&mut self, cx: &mut ViewContext) -> Option { + fn content_to_render(&mut self, cx: &mut Context) -> Option { // Show if any direnv calls failed if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() { return Some(Content { @@ -210,11 +218,11 @@ impl ActivityIndicator { .into_any_element(), ), message: error.0.clone(), - on_click: Some(Arc::new(move |this, cx| { + on_click: Some(Arc::new(move |this, window, cx| { this.project.update(cx, |project, cx| { project.remove_environment_error(cx, worktree_id); }); - cx.dispatch_action(Box::new(workspace::OpenLog)); + window.dispatch_action(Box::new(workspace::OpenLog), cx); })), }); } @@ -297,10 +305,10 @@ impl ActivityIndicator { } ) ), - on_click: Some(Arc::new(move |this, cx| { + on_click: Some(Arc::new(move |this, window, cx| { this.statuses .retain(|status| !downloading.contains(&status.name)); - this.dismiss_error_message(&DismissErrorMessage, cx) + this.dismiss_error_message(&DismissErrorMessage, window, cx) })), }); } @@ -325,10 +333,10 @@ impl ActivityIndicator { } ), ), - on_click: Some(Arc::new(move |this, cx| { + on_click: Some(Arc::new(move |this, window, cx| { this.statuses .retain(|status| !checking_for_update.contains(&status.name)); - this.dismiss_error_message(&DismissErrorMessage, cx) + this.dismiss_error_message(&DismissErrorMessage, window, cx) })), }); } @@ -353,8 +361,8 @@ impl ActivityIndicator { acc }), ), - on_click: Some(Arc::new(|this, cx| { - this.show_error_message(&Default::default(), cx) + on_click: Some(Arc::new(|this, window, cx| { + this.show_error_message(&Default::default(), window, cx) })), }); } @@ -368,11 +376,11 @@ impl ActivityIndicator { .into_any_element(), ), message: format!("Formatting failed: {}. Click to see logs.", failure), - on_click: Some(Arc::new(|indicator, cx| { + on_click: Some(Arc::new(|indicator, window, cx| { indicator.project.update(cx, |project, cx| { project.reset_last_formatting_failure(cx); }); - cx.dispatch_action(Box::new(workspace::OpenLog)); + window.dispatch_action(Box::new(workspace::OpenLog), cx); })), }); } @@ -387,8 +395,8 @@ impl ActivityIndicator { .into_any_element(), ), message: "Checking for Zed updates…".to_string(), - on_click: Some(Arc::new(|this, cx| { - this.dismiss_error_message(&DismissErrorMessage, cx) + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&DismissErrorMessage, window, cx) })), }), AutoUpdateStatus::Downloading => Some(Content { @@ -398,8 +406,8 @@ impl ActivityIndicator { .into_any_element(), ), message: "Downloading Zed update…".to_string(), - on_click: Some(Arc::new(|this, cx| { - this.dismiss_error_message(&DismissErrorMessage, cx) + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&DismissErrorMessage, window, cx) })), }), AutoUpdateStatus::Installing => Some(Content { @@ -409,8 +417,8 @@ impl ActivityIndicator { .into_any_element(), ), message: "Installing Zed update…".to_string(), - on_click: Some(Arc::new(|this, cx| { - this.dismiss_error_message(&DismissErrorMessage, cx) + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&DismissErrorMessage, window, cx) })), }), AutoUpdateStatus::Updated { binary_path } => Some(Content { @@ -420,7 +428,7 @@ impl ActivityIndicator { let reload = workspace::Reload { binary_path: Some(binary_path.clone()), }; - move |_, cx| workspace::reload(&reload, cx) + move |_, _, cx| workspace::reload(&reload, cx) })), }), AutoUpdateStatus::Errored => Some(Content { @@ -430,8 +438,8 @@ impl ActivityIndicator { .into_any_element(), ), message: "Auto update failed".to_string(), - on_click: Some(Arc::new(|this, cx| { - this.dismiss_error_message(&DismissErrorMessage, cx) + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&DismissErrorMessage, window, cx) })), }), AutoUpdateStatus::Idle => None, @@ -449,8 +457,8 @@ impl ActivityIndicator { .into_any_element(), ), message: format!("Updating {extension_id} extension…"), - on_click: Some(Arc::new(|this, cx| { - this.dismiss_error_message(&DismissErrorMessage, cx) + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&DismissErrorMessage, window, cx) })), }); } @@ -459,8 +467,12 @@ impl ActivityIndicator { None } - fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext) { - self.context_menu_handle.toggle(cx); + fn toggle_language_server_work_context_menu( + &mut self, + window: &mut Window, + cx: &mut Context, + ) { + self.context_menu_handle.toggle(window, cx); } } @@ -469,7 +481,7 @@ impl EventEmitter for ActivityIndicator {} const MAX_MESSAGE_LEN: usize = 50; impl Render for ActivityIndicator { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let result = h_flex() .id("activity-indicator") .on_action(cx.listener(Self::show_error_message)) @@ -477,7 +489,7 @@ impl Render for ActivityIndicator { let Some(content) = self.content_to_render(cx) else { return result; }; - let this = cx.view().downgrade(); + let this = cx.entity().downgrade(); let truncate_content = content.message.len() > MAX_MESSAGE_LEN; result.gap_2().child( PopoverMenu::new("activity-indicator-popover") @@ -497,24 +509,24 @@ impl Render for ActivityIndicator { )) .size(LabelSize::Small), ) - .tooltip(move |cx| Tooltip::text(&content.message, cx)) + .tooltip(Tooltip::text(content.message)) } else { button.child(Label::new(content.message).size(LabelSize::Small)) } }) .when_some(content.on_click, |this, handler| { - this.on_click(cx.listener(move |this, _, cx| { - handler(this, cx); + this.on_click(cx.listener(move |this, _, window, cx| { + handler(this, window, cx); })) .cursor(CursorStyle::PointingHand) }), ), ) .anchor(gpui::Corner::BottomLeft) - .menu(move |cx| { + .menu(move |window, cx| { let strong_this = this.upgrade()?; let mut has_work = false; - let menu = ContextMenu::build(cx, |mut menu, cx| { + let menu = ContextMenu::build(window, cx, |mut menu, _, cx| { for work in strong_this.read(cx).pending_language_server_work(cx) { has_work = true; let this = this.clone(); @@ -530,7 +542,7 @@ impl Render for ActivityIndicator { let token = work.progress_token.to_string(); let title = SharedString::from(title); menu = menu.custom_entry( - move |_| { + move |_, _| { h_flex() .w_full() .justify_between() @@ -538,7 +550,7 @@ impl Render for ActivityIndicator { .child(Icon::new(IconName::XCircle)) .into_any_element() }, - move |cx| { + move |_, cx| { this.update(cx, |this, cx| { this.project.update(cx, |project, cx| { project.cancel_language_server_work( @@ -571,5 +583,11 @@ impl Render for ActivityIndicator { } impl StatusItemView for ActivityIndicator { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} + fn set_active_pane_item( + &mut self, + _: Option<&dyn ItemHandle>, + _window: &mut Window, + _: &mut Context, + ) { + } } diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index 03f60d5a86bea9..1dd447a1c98e49 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -2,7 +2,7 @@ mod supported_countries; use std::{pin::Pin, str::FromStr}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use chrono::{DateTime, Utc}; use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use http_client::http::{HeaderMap, HeaderValue}; @@ -77,8 +77,8 @@ impl Model { Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest", Model::Claude3_5Haiku => "claude-3-5-haiku-latest", Model::Claude3Opus => "claude-3-opus-latest", - Model::Claude3Sonnet => "claude-3-sonnet-latest", - Model::Claude3Haiku => "claude-3-haiku-latest", + Model::Claude3Sonnet => "claude-3-sonnet-20240229", + Model::Claude3Haiku => "claude-3-haiku-20240307", Self::Custom { name, .. } => name, } } @@ -148,8 +148,13 @@ impl Model { } } + pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"]; + pub fn beta_headers(&self) -> String { - let mut headers = vec!["prompt-caching-2024-07-31".to_string()]; + let mut headers = Self::DEFAULT_BETA_HEADERS + .into_iter() + .map(|header| header.to_string()) + .collect::>(); if let Self::Custom { extra_beta_headers, .. @@ -186,12 +191,14 @@ pub async fn complete( request: Request, ) -> Result { let uri = format!("{api_url}/v1/messages"); - let model = Model::from_id(&request.model)?; + let beta_headers = Model::from_id(&request.model) + .map(|model| model.beta_headers()) + .unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(",")); let request_builder = HttpRequest::builder() .method(Method::POST) .uri(uri) .header("Anthropic-Version", "2023-06-01") - .header("Anthropic-Beta", model.beta_headers()) + .header("Anthropic-Beta", beta_headers) .header("X-Api-Key", api_key) .header("Content-Type", "application/json"); @@ -302,12 +309,14 @@ pub async fn stream_completion_with_rate_limit_info( stream: true, }; let uri = format!("{api_url}/v1/messages"); - let model = Model::from_id(&request.base.model)?; + let beta_headers = Model::from_id(&request.base.model) + .map(|model| model.beta_headers()) + .unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(",")); let request_builder = HttpRequest::builder() .method(Method::POST) .uri(uri) .header("Anthropic-Version", "2023-06-01") - .header("Anthropic-Beta", model.beta_headers()) + .header("Anthropic-Beta", beta_headers) .header("X-Api-Key", api_key) .header("Content-Type", "application/json"); let serialized_request = diff --git a/crates/assets/src/assets.rs b/crates/assets/src/assets.rs index ee990085f6de17..8f8831269bed0d 100644 --- a/crates/assets/src/assets.rs +++ b/crates/assets/src/assets.rs @@ -1,7 +1,7 @@ // This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build. use anyhow::anyhow; -use gpui::{AppContext, AssetSource, Result, SharedString}; +use gpui::{App, AssetSource, Result, SharedString}; use rust_embed::RustEmbed; #[derive(RustEmbed)] @@ -39,7 +39,7 @@ impl AssetSource for Assets { impl Assets { /// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory. - pub fn load_fonts(&self, cx: &AppContext) -> gpui::Result<()> { + pub fn load_fonts(&self, cx: &App) -> gpui::Result<()> { let font_paths = self.list("fonts")?; let mut embedded_fonts = Vec::new(); for font_path in font_paths { @@ -55,7 +55,7 @@ impl Assets { cx.text_system().add_fonts(embedded_fonts) } - pub fn load_test_fonts(&self, cx: &AppContext) { + pub fn load_test_fonts(&self, cx: &App) { cx.text_system() .add_fonts(vec![self .load("fonts/plex-mono/ZedPlexMono-Regular.ttf") diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index f076135ef5dc07..2ad0e7e0820a10 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -22,14 +22,13 @@ test-support = [ [dependencies] anyhow.workspace = true +assistant_context_editor.workspace = true assistant_settings.workspace = true assistant_slash_command.workspace = true assistant_slash_commands.workspace = true assistant_tool.workspace = true async-watch.workspace = true -chrono.workspace = true client.workspace = true -clock.workspace = true collections.workspace = true command_palette_hooks.workspace = true context_server.workspace = true @@ -38,7 +37,6 @@ editor.workspace = true feature_flags.workspace = true fs.workspace = true futures.workspace = true -fuzzy.workspace = true gpui.workspace = true indexed_docs.workspace = true indoc.workspace = true @@ -50,27 +48,20 @@ log.workspace = true lsp.workspace = true menu.workspace = true multi_buffer.workspace = true -open_ai = { workspace = true, features = ["schemars"] } parking_lot.workspace = true paths.workspace = true -picker.workspace = true project.workspace = true prompt_library.workspace = true proto.workspace = true -regex.workspace = true rope.workspace = true -rpc.workspace = true schemars.workspace = true search.workspace = true semantic_index.workspace = true serde.workspace = true -serde_json.workspace = true settings.workspace = true similar.workspace = true -smallvec.workspace = true smol.workspace = true streaming_diff.workspace = true -strum.workspace = true telemetry.workspace = true telemetry_events.workspace = true terminal.workspace = true @@ -79,7 +70,6 @@ text.workspace = true theme.workspace = true ui.workspace = true util.workspace = true -uuid.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 19774180cb03b0..7817c958b395c8 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -1,84 +1,46 @@ #![cfg_attr(target_os = "windows", allow(unused, dead_code))] +mod assistant_configuration; pub mod assistant_panel; -mod context; -pub mod context_store; mod inline_assistant; -mod patch; -mod slash_command; -pub(crate) mod slash_command_picker; pub mod slash_command_settings; mod terminal_inline_assistant; -use std::path::PathBuf; use std::sync::Arc; use assistant_settings::AssistantSettings; use assistant_slash_command::SlashCommandRegistry; use assistant_slash_commands::{ProjectSlashCommandFeatureFlag, SearchSlashCommandFeatureFlag}; -use client::{proto, Client}; +use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt; use fs::Fs; -use gpui::impl_internal_actions; -use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal}; +use gpui::{actions, App, Global, UpdateGlobal}; use language_model::{ LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage, }; -use prompt_library::{PromptBuilder, PromptLoadingParams}; +use prompt_library::PromptBuilder; use semantic_index::{CloudEmbeddingProvider, SemanticDb}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use settings::{Settings, SettingsStore}; -use util::ResultExt; pub use crate::assistant_panel::{AssistantPanel, AssistantPanelEvent}; -pub use crate::context::*; -pub use crate::context_store::*; pub(crate) use crate::inline_assistant::*; -pub use crate::patch::*; use crate::slash_command_settings::SlashCommandSettings; actions!( assistant, [ - Assist, - Edit, - Split, - CopyCode, - CycleMessageRole, - QuoteSelection, - InsertIntoEditor, - ToggleFocus, InsertActivePrompt, DeployHistory, - DeployPromptLibrary, - ConfirmCommand, NewContext, - ToggleModelSelector, CycleNextInlineAssist, CyclePreviousInlineAssist ] ); -#[derive(PartialEq, Clone)] -pub enum InsertDraggedFiles { - ProjectPaths(Vec), - ExternalFiles(Vec), -} - -impl_internal_actions!(assistant, [InsertDraggedFiles]); - const DEFAULT_CONTEXT_LINES: usize = 50; -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct MessageId(clock::Lamport); - -impl MessageId { - pub fn as_u64(self) -> u64 { - self.0.as_u64() - } -} - #[derive(Deserialize, Debug)] pub struct LanguageModelUsage { pub prompt_tokens: u32, @@ -93,55 +55,6 @@ pub struct LanguageModelChoiceDelta { pub finish_reason: Option, } -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum MessageStatus { - Pending, - Done, - Error(SharedString), - Canceled, -} - -impl MessageStatus { - pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus { - match status.variant { - Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending, - Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done, - Some(proto::context_message_status::Variant::Error(error)) => { - MessageStatus::Error(error.message.into()) - } - Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled, - None => MessageStatus::Pending, - } - } - - pub fn to_proto(&self) -> proto::ContextMessageStatus { - match self { - MessageStatus::Pending => proto::ContextMessageStatus { - variant: Some(proto::context_message_status::Variant::Pending( - proto::context_message_status::Pending {}, - )), - }, - MessageStatus::Done => proto::ContextMessageStatus { - variant: Some(proto::context_message_status::Variant::Done( - proto::context_message_status::Done {}, - )), - }, - MessageStatus::Error(message) => proto::ContextMessageStatus { - variant: Some(proto::context_message_status::Variant::Error( - proto::context_message_status::Error { - message: message.to_string(), - }, - )), - }, - MessageStatus::Canceled => proto::ContextMessageStatus { - variant: Some(proto::context_message_status::Variant::Canceled( - proto::context_message_status::Canceled {}, - )), - }, - } - } -} - /// The state pertaining to the Assistant. #[derive(Default)] struct Assistant { @@ -154,7 +67,7 @@ impl Global for Assistant {} impl Assistant { const NAMESPACE: &'static str = "assistant"; - fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) { + fn set_enabled(&mut self, enabled: bool, cx: &mut App) { if self.enabled == enabled { return; } @@ -178,9 +91,9 @@ impl Assistant { pub fn init( fs: Arc, client: Arc, - stdout_is_a_pty: bool, - cx: &mut AppContext, -) -> Arc { + prompt_builder: Arc, + cx: &mut App, +) { cx.set_global(Assistant::default()); AssistantSettings::register(cx); SlashCommandSettings::register(cx); @@ -212,7 +125,7 @@ pub fn init( }) .detach(); - context_store::init(&client.clone().into()); + assistant_context_editor::init(client.clone(), cx); prompt_library::init(cx); init_language_model_settings(cx); assistant_slash_command::init(cx); @@ -220,16 +133,6 @@ pub fn init( assistant_panel::init(cx); context_server::init(cx); - let prompt_builder = PromptBuilder::new(Some(PromptLoadingParams { - fs: fs.clone(), - repo_path: stdout_is_a_pty - .then(|| std::env::current_dir().log_err()) - .flatten(), - cx, - })) - .log_err() - .map(Arc::new) - .unwrap_or_else(|| Arc::new(PromptBuilder::new(None).unwrap())); register_slash_commands(Some(prompt_builder.clone()), cx); inline_assistant::init( fs.clone(), @@ -260,11 +163,9 @@ pub fn init( }); }) .detach(); - - prompt_builder } -fn init_language_model_settings(cx: &mut AppContext) { +fn init_language_model_settings(cx: &mut App) { update_active_language_model_from_settings(cx); cx.observe_global::(update_active_language_model_from_settings) @@ -283,7 +184,7 @@ fn init_language_model_settings(cx: &mut AppContext) { .detach(); } -fn update_active_language_model_from_settings(cx: &mut AppContext) { +fn update_active_language_model_from_settings(cx: &mut App) { let settings = AssistantSettings::get_global(cx); let provider_name = LanguageModelProviderId::from(settings.default_model.provider.clone()); let model_id = LanguageModelId::from(settings.default_model.model.clone()); @@ -303,7 +204,7 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) { }); } -fn register_slash_commands(prompt_builder: Option>, cx: &mut AppContext) { +fn register_slash_commands(prompt_builder: Option>, cx: &mut App) { let slash_command_registry = SlashCommandRegistry::global(cx); slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true); @@ -377,7 +278,7 @@ fn register_slash_commands(prompt_builder: Option>, cx: &mut .detach(); } -fn update_slash_commands_from_settings(cx: &mut AppContext) { +fn update_slash_commands_from_settings(cx: &mut App) { let slash_command_registry = SlashCommandRegistry::global(cx); let settings = SlashCommandSettings::get_global(cx); @@ -396,24 +297,6 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) { } } -pub fn humanize_token_count(count: usize) -> String { - match count { - 0..=999 => count.to_string(), - 1000..=9999 => { - let thousands = count / 1000; - let hundreds = (count % 1000 + 50) / 100; - if hundreds == 0 { - format!("{}k", thousands) - } else if hundreds == 10 { - format!("{}k", thousands + 1) - } else { - format!("{}.{}k", thousands, hundreds) - } - } - _ => format!("{}k", (count + 500) / 1000), - } -} - #[cfg(test)] #[ctor::ctor] fn init_logger() { diff --git a/crates/assistant/src/assistant_configuration.rs b/crates/assistant/src/assistant_configuration.rs new file mode 100644 index 00000000000000..fdff8977529cac --- /dev/null +++ b/crates/assistant/src/assistant_configuration.rs @@ -0,0 +1,199 @@ +use std::sync::Arc; + +use collections::HashMap; +use gpui::{canvas, AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription}; +use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; +use ui::{prelude::*, ElevationIndex}; +use workspace::Item; + +pub struct ConfigurationView { + focus_handle: FocusHandle, + configuration_views: HashMap, + _registry_subscription: Subscription, +} + +impl ConfigurationView { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + + let registry_subscription = cx.subscribe_in( + &LanguageModelRegistry::global(cx), + window, + |this, _, event: &language_model::Event, window, cx| match event { + language_model::Event::AddedProvider(provider_id) => { + let provider = LanguageModelRegistry::read_global(cx).provider(provider_id); + if let Some(provider) = provider { + this.add_configuration_view(&provider, window, cx); + } + } + language_model::Event::RemovedProvider(provider_id) => { + this.remove_configuration_view(provider_id); + } + _ => {} + }, + ); + + let mut this = Self { + focus_handle, + configuration_views: HashMap::default(), + _registry_subscription: registry_subscription, + }; + this.build_configuration_views(window, cx); + this + } + + fn build_configuration_views(&mut self, window: &mut Window, cx: &mut Context) { + let providers = LanguageModelRegistry::read_global(cx).providers(); + for provider in providers { + self.add_configuration_view(&provider, window, cx); + } + } + + fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) { + self.configuration_views.remove(provider_id); + } + + fn add_configuration_view( + &mut self, + provider: &Arc, + window: &mut Window, + cx: &mut Context, + ) { + let configuration_view = provider.configuration_view(window, cx); + self.configuration_views + .insert(provider.id(), configuration_view); + } + + fn render_provider_view( + &mut self, + provider: &Arc, + cx: &mut Context, + ) -> Div { + let provider_id = provider.id().0.clone(); + let provider_name = provider.name().0.clone(); + let configuration_view = self.configuration_views.get(&provider.id()).cloned(); + + let open_new_context = cx.listener({ + let provider = provider.clone(); + move |_, _, _window, cx| { + cx.emit(ConfigurationViewEvent::NewProviderContextEditor( + provider.clone(), + )) + } + }); + + v_flex() + .gap_2() + .child( + h_flex() + .justify_between() + .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small)) + .when(provider.is_authenticated(cx), move |this| { + this.child( + h_flex().justify_end().child( + Button::new( + SharedString::from(format!("new-context-{provider_id}")), + "Open New Chat", + ) + .icon_position(IconPosition::Start) + .icon(IconName::Plus) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .on_click(open_new_context), + ), + ) + }), + ) + .child( + div() + .p(DynamicSpacing::Base08.rems(cx)) + .bg(cx.theme().colors().surface_background) + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .when(configuration_view.is_none(), |this| { + this.child(div().child(Label::new(format!( + "No configuration view for {}", + provider_name + )))) + }) + .when_some(configuration_view, |this, configuration_view| { + this.child(configuration_view) + }), + ) + } +} + +impl Render for ConfigurationView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let providers = LanguageModelRegistry::read_global(cx).providers(); + let provider_views = providers + .into_iter() + .map(|provider| self.render_provider_view(&provider, cx)) + .collect::>(); + + let mut element = v_flex() + .id("assistant-configuration-view") + .track_focus(&self.focus_handle(cx)) + .bg(cx.theme().colors().editor_background) + .size_full() + .overflow_y_scroll() + .child( + v_flex() + .p(DynamicSpacing::Base16.rems(cx)) + .border_b_1() + .border_color(cx.theme().colors().border) + .gap_1() + .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium)) + .child( + Label::new( + "At least one LLM provider must be configured to use the Assistant.", + ) + .color(Color::Muted), + ), + ) + .child( + v_flex() + .p(DynamicSpacing::Base16.rems(cx)) + .mt_1() + .gap_6() + .flex_1() + .children(provider_views), + ) + .into_any(); + + // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround + // because we couldn't the element to take up the size of the parent. + canvas( + move |bounds, window, cx| { + element.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx); + element + }, + |_, mut element, window, cx| { + element.paint(window, cx); + }, + ) + .flex_1() + .w_full() + } +} + +pub enum ConfigurationViewEvent { + NewProviderContextEditor(Arc), +} + +impl EventEmitter for ConfigurationView {} + +impl Focusable for ConfigurationView { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for ConfigurationView { + type Event = ConfigurationViewEvent; + + fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { + Some("Configuration".into()) + } +} diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 421275ea1f12bc..10b3affca402cb 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,108 +1,49 @@ +use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent}; use crate::{ - humanize_token_count, slash_command::SlashCommandCompletionProvider, slash_command_picker, - terminal_inline_assistant::TerminalInlineAssistant, Assist, AssistantPatch, - AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context, ContextEvent, ContextId, - ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory, - DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles, InsertIntoEditor, - InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, - MessageStatus, NewContext, ParsedSlashCommand, PendingSlashCommandStatus, QuoteSelection, - RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus, - ToggleModelSelector, + terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewContext, }; -use anyhow::Result; -use assistant_settings::{AssistantDockPosition, AssistantSettings}; -use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet}; -use assistant_slash_commands::{ - selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, - FileSlashCommand, +use anyhow::{anyhow, Result}; +use assistant_context_editor::{ + make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor, + ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId, + ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider, + ToggleModelSelector, DEFAULT_TAB_TITLE, }; +use assistant_settings::{AssistantDockPosition, AssistantSettings}; +use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; -use client::{proto, zed_urls, Client, Status}; -use collections::{hash_map, BTreeSet, HashMap, HashSet}; -use editor::{ - actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt}, - display_map::{ - BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, - CustomBlockId, FoldId, RenderBlock, ToDisplayPoint, - }, - scroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt, - ToOffset as _, ToPoint, -}; -use editor::{display_map::CreaseId, FoldPlaceholder}; +use client::{proto, Client, Status}; +use editor::{Editor, EditorEvent}; use fs::Fs; -use futures::FutureExt; use gpui::{ - canvas, div, img, percentage, point, prelude::*, pulsating_between, size, Action, Animation, - AnimationExt, AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, - ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle, - FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, - Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, - Task, Transformation, UpdateGlobal, View, WeakModel, WeakView, -}; -use indexed_docs::IndexedDocsStore; -use language::{ - language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset, + prelude::*, Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, + Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled, + Subscription, Task, UpdateGlobal, WeakEntity, }; -use language_model::{LanguageModelImage, LanguageModelToolUse}; -use language_model::{ - LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role, - ZED_CLOUD_PROVIDER_ID, -}; -use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; -use multi_buffer::MultiBufferRow; -use picker::{Picker, PickerDelegate}; -use project::lsp_store::LocalLspAdapterDelegate; -use project::{Project, Worktree}; +use language::LanguageRegistry; +use language_model::{LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; +use language_model_selector::LanguageModelSelector; +use project::Project; use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary}; -use rope::Point; use search::{buffer_search::DivRegistrar, BufferSearchBar}; -use serde::{Deserialize, Serialize}; use settings::{update_settings_file, Settings}; use smol::stream::StreamExt; -use std::{ - any::TypeId, - borrow::Cow, - cmp, - ops::{ControlFlow, Range}, - path::PathBuf, - sync::Arc, - time::Duration, -}; +use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; -use text::SelectionGoal; -use ui::{ - prelude::*, - utils::{format_distance_from_now, DateTimeType}, - Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, - ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, -}; +use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip}; use util::{maybe, ResultExt}; +use workspace::DraggedTab; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - item::{self, FollowableItem, Item, ItemHandle}, - notifications::NotificationId, - pane::{self, SaveIntent}, - searchable::{SearchEvent, SearchableItem}, - DraggedSelection, Pane, Save, ShowConfiguration, Toast, ToggleZoom, ToolbarItemEvent, - ToolbarItemLocation, ToolbarItemView, Workspace, + pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace, }; -use workspace::{searchable::SearchableItemHandle, DraggedTab}; -use zed_actions::InlineAssist; +use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus}; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { workspace::FollowableViewRegistry::register::(cx); - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { + cx.observe_new( + |workspace: &mut Workspace, _window, _cx: &mut Context| { workspace - .register_action(|workspace, _: &ToggleFocus, cx| { - let settings = AssistantSettings::get_global(cx); - if !settings.enabled { - return; - } - - workspace.toggle_panel_focus::(cx); - }) .register_action(ContextEditor::quote_selection) .register_action(ContextEditor::insert_selection) .register_action(ContextEditor::copy_code) @@ -114,8 +55,8 @@ pub fn init(cx: &mut AppContext) { ) .detach(); - cx.observe_new_views( - |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext| { + cx.observe_new( + |terminal_panel: &mut TerminalPanel, _, cx: &mut Context| { let settings = AssistantSettings::get_global(cx); terminal_panel.set_assistant_enabled(settings.enabled, cx); }, @@ -128,189 +69,35 @@ pub enum AssistantPanelEvent { } pub struct AssistantPanel { - pane: View, - workspace: WeakView, + pane: Entity, + workspace: WeakEntity, width: Option, height: Option, - project: Model, - context_store: Model, + project: Entity, + context_store: Entity, languages: Arc, fs: Arc, subscriptions: Vec, model_selector_menu_handle: PopoverMenuHandle, - model_summary_editor: View, + model_summary_editor: Entity, authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>, configuration_subscription: Option, client_status: Option, watch_client_status: Option>, - show_zed_ai_notice: bool, -} - -#[derive(Clone)] -enum ContextMetadata { - Remote(RemoteContextMetadata), - Saved(SavedContextMetadata), -} - -struct SavedContextPickerDelegate { - store: Model, - project: Model, - matches: Vec, - selected_index: usize, -} - -enum SavedContextPickerEvent { - Confirmed(ContextMetadata), + pub(crate) show_zed_ai_notice: bool, } enum InlineAssistTarget { - Editor(View, bool), - Terminal(View), -} - -impl EventEmitter for Picker {} - -impl SavedContextPickerDelegate { - fn new(project: Model, store: Model) -> Self { - Self { - project, - store, - matches: Vec::new(), - selected_index: 0, - } - } -} - -impl PickerDelegate for SavedContextPickerDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { - self.selected_index = ix; - } - - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { - "Search...".into() - } - - fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { - let search = self.store.read(cx).search(query, cx); - cx.spawn(|this, mut cx| async move { - let matches = search.await; - this.update(&mut cx, |this, cx| { - let host_contexts = this.delegate.store.read(cx).host_contexts(); - this.delegate.matches = host_contexts - .iter() - .cloned() - .map(ContextMetadata::Remote) - .chain(matches.into_iter().map(ContextMetadata::Saved)) - .collect(); - this.delegate.selected_index = 0; - cx.notify(); - }) - .ok(); - }) - } - - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { - if let Some(metadata) = self.matches.get(self.selected_index) { - cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone())); - } - } - - fn dismissed(&mut self, _cx: &mut ViewContext>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - cx: &mut ViewContext>, - ) -> Option { - let context = self.matches.get(ix)?; - let item = match context { - ContextMetadata::Remote(context) => { - let host_user = self.project.read(cx).host().and_then(|collaborator| { - self.project - .read(cx) - .user_store() - .read(cx) - .get_cached_user(collaborator.user_id) - }); - div() - .flex() - .w_full() - .justify_between() - .gap_2() - .child( - h_flex().flex_1().overflow_x_hidden().child( - Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into())) - .size(LabelSize::Small), - ), - ) - .child( - h_flex() - .gap_2() - .children(if let Some(host_user) = host_user { - vec![ - Avatar::new(host_user.avatar_uri.clone()).into_any_element(), - Label::new(format!("Shared by @{}", host_user.github_login)) - .color(Color::Muted) - .size(LabelSize::Small) - .into_any_element(), - ] - } else { - vec![Label::new("Shared by host") - .color(Color::Muted) - .size(LabelSize::Small) - .into_any_element()] - }), - ) - } - ContextMetadata::Saved(context) => div() - .flex() - .w_full() - .justify_between() - .gap_2() - .child( - h_flex() - .flex_1() - .child(Label::new(context.title.clone()).size(LabelSize::Small)) - .overflow_x_hidden(), - ) - .child( - Label::new(format_distance_from_now( - DateTimeType::Local(context.mtime), - false, - true, - true, - )) - .color(Color::Muted) - .size(LabelSize::Small), - ), - }; - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(item), - ) - } + Editor(Entity, bool), + Terminal(Entity), } impl AssistantPanel { pub fn load( - workspace: WeakView, + workspace: WeakEntity, prompt_builder: Arc, cx: AsyncWindowContext, - ) -> Task>> { + ) -> Task>> { cx.spawn(|mut cx| async move { let slash_commands = Arc::new(SlashCommandWorkingSet::default()); let tools = Arc::new(ToolWorkingSet::default()); @@ -321,41 +108,44 @@ impl AssistantPanel { })? .await?; - workspace.update(&mut cx, |workspace, cx| { + workspace.update_in(&mut cx, |workspace, window, cx| { // TODO: deserialize state. - cx.new_view(|cx| Self::new(workspace, context_store, cx)) + cx.new(|cx| Self::new(workspace, context_store, window, cx)) }) }) } fn new( workspace: &Workspace, - context_store: Model, - cx: &mut ViewContext, + context_store: Entity, + window: &mut Window, + cx: &mut Context, ) -> Self { let model_selector_menu_handle = PopoverMenuHandle::default(); - let model_summary_editor = cx.new_view(Editor::single_line); - let context_editor_toolbar = cx.new_view(|cx| { + let model_summary_editor = cx.new(|cx| Editor::single_line(window, cx)); + let context_editor_toolbar = cx.new(|cx| { ContextEditorToolbarItem::new( workspace, model_selector_menu_handle.clone(), model_summary_editor.clone(), + window, cx, ) }); - let pane = cx.new_view(|cx| { + let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.weak_handle(), workspace.project().clone(), Default::default(), None, Some(NewContext.boxed_clone()), + window, cx, ); let project = workspace.project().clone(); - pane.set_custom_drop_handle(cx, move |_, dropped_item, cx| { + pane.set_custom_drop_handle(cx, move |_, dropped_item, window, cx| { let action = maybe!({ if project.read(cx).is_local() { if let Some(paths) = dropped_item.downcast_ref::() { @@ -365,7 +155,7 @@ impl AssistantPanel { let project_paths = if let Some(tab) = dropped_item.downcast_ref::() { - if &tab.pane == cx.view() { + if tab.pane == cx.entity() { return None; } let item = tab.pane.read(cx).item_for_index(tab.ix); @@ -405,7 +195,7 @@ impl AssistantPanel { }); if let Some(action) = action { - cx.dispatch_action(action.boxed_clone()); + window.dispatch_action(action.boxed_clone(), cx); } ControlFlow::Break(()) @@ -413,25 +203,26 @@ impl AssistantPanel { pane.set_can_navigate(true, cx); pane.display_nav_history_buttons(None); - pane.set_should_display_tab_bar(|_| true); - pane.set_render_tab_bar_buttons(cx, move |pane, cx| { + pane.set_should_display_tab_bar(|_, _| true); + pane.set_render_tab_bar_buttons(cx, move |pane, _window, cx| { let focus_handle = pane.focus_handle(cx); let left_children = IconButton::new("history", IconName::HistoryRerun) .icon_size(IconSize::Small) .on_click(cx.listener({ let focus_handle = focus_handle.clone(); - move |_, _, cx| { - focus_handle.focus(cx); - cx.dispatch_action(DeployHistory.boxed_clone()) + move |_, _, window, cx| { + focus_handle.focus(window); + window.dispatch_action(DeployHistory.boxed_clone(), cx) } })) .tooltip({ let focus_handle = focus_handle.clone(); - move |cx| { + move |window, cx| { Tooltip::for_action_in( "Open History", &DeployHistory, &focus_handle, + window, cx, ) } @@ -440,19 +231,23 @@ impl AssistantPanel { pane.active_item() .map_or(false, |item| item.downcast::().is_some()), ); - let _pane = cx.view().clone(); + let _pane = cx.entity().clone(); let right_children = h_flex() .gap(DynamicSpacing::Base02.rems(cx)) .child( IconButton::new("new-chat", IconName::Plus) .icon_size(IconSize::Small) - .on_click( - cx.listener(|_, _, cx| { - cx.dispatch_action(NewContext.boxed_clone()) - }), - ) - .tooltip(move |cx| { - Tooltip::for_action_in("New Chat", &NewContext, &focus_handle, cx) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(NewContext.boxed_clone(), cx) + })) + .tooltip(move |window, cx| { + Tooltip::for_action_in( + "New Chat", + &NewContext, + &focus_handle, + window, + cx, + ) }), ) .child( @@ -460,16 +255,16 @@ impl AssistantPanel { .trigger( IconButton::new("menu", IconName::EllipsisVertical) .icon_size(IconSize::Small) - .tooltip(|cx| Tooltip::text("Toggle Assistant Menu", cx)), + .tooltip(Tooltip::text("Toggle Assistant Menu")), ) - .menu(move |cx| { + .menu(move |window, cx| { let zoom_label = if _pane.read(cx).is_zoomed() { "Zoom Out" } else { "Zoom In" }; let focus_handle = _pane.focus_handle(cx); - Some(ContextMenu::build(cx, move |menu, _| { + Some(ContextMenu::build(window, cx, move |menu, _, _| { menu.context(focus_handle.clone()) .action("New Chat", Box::new(NewContext)) .action("History", Box::new(DeployHistory)) @@ -485,37 +280,38 @@ impl AssistantPanel { (Some(left_children.into_any_element()), right_children) }); pane.toolbar().update(cx, |toolbar, cx| { - toolbar.add_item(context_editor_toolbar.clone(), cx); - toolbar.add_item(cx.new_view(BufferSearchBar::new), cx) + toolbar.add_item(context_editor_toolbar.clone(), window, cx); + toolbar.add_item(cx.new(|cx| BufferSearchBar::new(window, cx)), window, cx) }); pane }); let subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe(&pane, Self::handle_pane_event), + cx.subscribe_in(&pane, window, Self::handle_pane_event), cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event), cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event), - cx.subscribe(&context_store, Self::handle_context_store_event), - cx.subscribe( + cx.subscribe_in(&context_store, window, Self::handle_context_store_event), + cx.subscribe_in( &LanguageModelRegistry::global(cx), - |this, _, event: &language_model::Event, cx| match event { + window, + |this, _, event: &language_model::Event, window, cx| match event { language_model::Event::ActiveModelChanged => { - this.completion_provider_changed(cx); + this.completion_provider_changed(window, cx); } language_model::Event::ProviderStateChanged => { - this.ensure_authenticated(cx); + this.ensure_authenticated(window, cx); cx.notify() } language_model::Event::AddedProvider(_) | language_model::Event::RemovedProvider(_) => { - this.ensure_authenticated(cx); + this.ensure_authenticated(window, cx); } }, ), ]; - let watch_client_status = Self::watch_client_status(workspace.client().clone(), cx); + let watch_client_status = Self::watch_client_status(workspace.client().clone(), window, cx); let mut this = Self { pane, @@ -535,14 +331,32 @@ impl AssistantPanel { watch_client_status: Some(watch_client_status), show_zed_ai_notice: false, }; - this.new_context(cx); + this.new_context(window, cx); this } - fn watch_client_status(client: Arc, cx: &mut ViewContext) -> Task<()> { + pub fn toggle_focus( + workspace: &mut Workspace, + _: &ToggleFocus, + window: &mut Window, + cx: &mut Context, + ) { + let settings = AssistantSettings::get_global(cx); + if !settings.enabled { + return; + } + + workspace.toggle_panel_focus::(window, cx); + } + + fn watch_client_status( + client: Arc, + window: &mut Window, + cx: &mut Context, + ) -> Task<()> { let mut status_rx = client.status(); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { while let Some(status) = status_rx.next().await { this.update(&mut cx, |this, cx| { if this.client_status.is_none() @@ -563,9 +377,10 @@ impl AssistantPanel { fn handle_pane_event( &mut self, - pane: View, + pane: &Entity, event: &pane::Event, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let update_model_summary = match event { pane::Event::Remove { .. } => { @@ -584,7 +399,7 @@ impl AssistantPanel { pane::Event::AddItem { item } => { self.workspace .update(cx, |workspace, cx| { - item.added_to_pane(workspace, self.pane.clone(), cx) + item.added_to_pane(workspace, self.pane.clone(), window, cx) }) .ok(); true @@ -594,7 +409,7 @@ impl AssistantPanel { if *local { self.workspace .update(cx, |workspace, cx| { - workspace.unfollow_in_pane(&pane, cx); + workspace.unfollow_in_pane(&pane, window, cx); }) .ok(); } @@ -622,22 +437,22 @@ impl AssistantPanel { if update_model_summary { if let Some(editor) = self.active_context_editor(cx) { - self.show_updated_summary(&editor, cx) + self.show_updated_summary(&editor, window, cx) } } } fn handle_summary_editor_event( &mut self, - model_summary_editor: View, + model_summary_editor: Entity, event: &EditorEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { if matches!(event, EditorEvent::Edited { .. }) { if let Some(context_editor) = self.active_context_editor(cx) { let new_summary = model_summary_editor.read(cx).text(cx); context_editor.update(cx, |context_editor, cx| { - context_editor.context.update(cx, |context, cx| { + context_editor.context().update(cx, |context, cx| { if context.summary().is_none() && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty()) { @@ -650,11 +465,7 @@ impl AssistantPanel { } } - fn update_zed_ai_notice_visibility( - &mut self, - client_status: Status, - cx: &mut ViewContext, - ) { + fn update_zed_ai_notice_visibility(&mut self, client_status: Status, cx: &mut Context) { let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is @@ -668,13 +479,13 @@ impl AssistantPanel { fn handle_toolbar_event( &mut self, - _: View, + _: Entity, _: &ContextEditorToolbarItemEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { if let Some(context_editor) = self.active_context_editor(cx) { context_editor.update(cx, |context_editor, cx| { - context_editor.context.update(cx, |context, cx| { + context_editor.context().update(cx, |context, cx| { context.summarize(true, cx); }) }) @@ -683,9 +494,10 @@ impl AssistantPanel { fn handle_context_store_event( &mut self, - _context_store: Model, + _context_store: &Entity, event: &ContextStoreEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let ContextStoreEvent::ContextCreated(context_id) = event; let Some(context) = self @@ -700,29 +512,28 @@ impl AssistantPanel { .log_err() .flatten(); - let assistant_panel = cx.view().downgrade(); - let editor = cx.new_view(|cx| { + let editor = cx.new(|cx| { let mut editor = ContextEditor::for_context( context, self.fs.clone(), self.workspace.clone(), self.project.clone(), lsp_adapter_delegate, - assistant_panel, + window, cx, ); - editor.insert_default_prompt(cx); + editor.insert_default_prompt(window, cx); editor }); - self.show_context(editor.clone(), cx); + self.show_context(editor.clone(), window, cx); } - fn completion_provider_changed(&mut self, cx: &mut ViewContext) { + fn completion_provider_changed(&mut self, window: &mut Window, cx: &mut Context) { if let Some(editor) = self.active_context_editor(cx) { editor.update(cx, |active_context, cx| { active_context - .context + .context() .update(cx, |context, cx| context.completion_provider_changed(cx)) }) } @@ -742,7 +553,7 @@ impl AssistantPanel { }) { self.authenticate_provider_task = None; - self.ensure_authenticated(cx); + self.ensure_authenticated(window, cx); } if let Some(status) = self.client_status { @@ -750,7 +561,7 @@ impl AssistantPanel { } } - fn ensure_authenticated(&mut self, cx: &mut ViewContext) { + fn ensure_authenticated(&mut self, window: &mut Window, cx: &mut Context) { if self.is_authenticated(cx) { return; } @@ -764,7 +575,7 @@ impl AssistantPanel { if self.authenticate_provider_task.is_none() { self.authenticate_provider_task = Some(( provider.id(), - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { if let Some(future) = load_credentials { let _ = future.await; } @@ -780,7 +591,8 @@ impl AssistantPanel { pub fn inline_assist( workspace: &mut Workspace, action: &InlineAssist, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let settings = AssistantSettings::get_global(cx); if !settings.enabled { @@ -792,7 +604,7 @@ impl AssistantPanel { }; let Some(inline_assist_target) = - Self::resolve_inline_assist_target(workspace, &assistant_panel, cx) + Self::resolve_inline_assist_target(workspace, &assistant_panel, window, cx) else { return; }; @@ -805,9 +617,10 @@ impl AssistantPanel { InlineAssistant::update_global(cx, |assistant, cx| { assistant.assist( &active_editor, - Some(cx.view().downgrade()), + Some(cx.entity().downgrade()), include_context.then_some(&assistant_panel), initial_prompt, + window, cx, ) }) @@ -816,9 +629,10 @@ impl AssistantPanel { TerminalInlineAssistant::update_global(cx, |assistant, cx| { assistant.assist( &active_terminal, - Some(cx.view().downgrade()), + Some(cx.entity().downgrade()), Some(&assistant_panel), initial_prompt, + window, cx, ) }) @@ -826,7 +640,7 @@ impl AssistantPanel { } } else { let assistant_panel = assistant_panel.downgrade(); - cx.spawn(|workspace, mut cx| async move { + cx.spawn_in(window, |workspace, mut cx| async move { let Some(task) = assistant_panel.update(&mut cx, |assistant, cx| assistant.authenticate(cx))? else { @@ -841,15 +655,17 @@ impl AssistantPanel { .ok(); if let Some(answer) = answer { if answer == 0 { - cx.update(|cx| cx.dispatch_action(Box::new(ShowConfiguration))) - .ok(); + cx.update(|window, cx| { + window.dispatch_action(Box::new(ShowConfiguration), cx) + }) + .ok(); } } return Ok(()); }; task.await?; if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? { - cx.update(|cx| match inline_assist_target { + cx.update(|window, cx| match inline_assist_target { InlineAssistTarget::Editor(active_editor, include_context) => { let assistant_panel = if include_context { assistant_panel.upgrade() @@ -862,6 +678,7 @@ impl AssistantPanel { Some(workspace), assistant_panel.as_ref(), initial_prompt, + window, cx, ) }) @@ -873,14 +690,15 @@ impl AssistantPanel { Some(workspace), assistant_panel.upgrade().as_ref(), initial_prompt, + window, cx, ) }) } })? } else { - workspace.update(&mut cx, |workspace, cx| { - workspace.focus_panel::(cx) + workspace.update_in(&mut cx, |workspace, window, cx| { + workspace.focus_panel::(window, cx) })?; } @@ -892,14 +710,15 @@ impl AssistantPanel { fn resolve_inline_assist_target( workspace: &mut Workspace, - assistant_panel: &View, - cx: &mut WindowContext, + assistant_panel: &Entity, + window: &mut Window, + cx: &mut App, ) -> Option { if let Some(terminal_panel) = workspace.panel::(cx) { if terminal_panel .read(cx) .focus_handle(cx) - .contains_focused(cx) + .contains_focused(window, cx) { if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { pane.read(cx) @@ -915,8 +734,8 @@ impl AssistantPanel { .read(cx) .active_context_editor(cx) .and_then(|editor| { - let editor = &editor.read(cx).editor; - if editor.read(cx).is_focused(cx) { + let editor = &editor.read(cx).editor().clone(); + if editor.read(cx).is_focused(window) { Some(editor.clone()) } else { None @@ -943,33 +762,38 @@ impl AssistantPanel { pub fn create_new_context( workspace: &mut Workspace, _: &NewContext, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if let Some(panel) = workspace.panel::(cx) { let did_create_context = panel .update(cx, |panel, cx| { - panel.new_context(cx)?; + panel.new_context(window, cx)?; Some(()) }) .is_some(); if did_create_context { - ContextEditor::quote_selection(workspace, &Default::default(), cx); + ContextEditor::quote_selection(workspace, &Default::default(), window, cx); } } } - fn new_context(&mut self, cx: &mut ViewContext) -> Option> { + pub fn new_context( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option> { let project = self.project.read(cx); if project.is_via_collab() { let task = self .context_store .update(cx, |store, cx| store.create_remote_context(cx)); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { let context = task.await?; - this.update(&mut cx, |this, cx| { + this.update_in(&mut cx, |this, window, cx| { let workspace = this.workspace.clone(); let project = this.project.clone(); let lsp_adapter_delegate = @@ -977,21 +801,20 @@ impl AssistantPanel { let fs = this.fs.clone(); let project = this.project.clone(); - let weak_assistant_panel = cx.view().downgrade(); - let editor = cx.new_view(|cx| { + let editor = cx.new(|cx| { ContextEditor::for_context( context, fs, workspace, project, lsp_adapter_delegate, - weak_assistant_panel, + window, cx, ) }); - this.show_context(editor, cx); + this.show_context(editor, window, cx); anyhow::Ok(()) })??; @@ -1007,27 +830,26 @@ impl AssistantPanel { .log_err() .flatten(); - let assistant_panel = cx.view().downgrade(); - let editor = cx.new_view(|cx| { + let editor = cx.new(|cx| { let mut editor = ContextEditor::for_context( context, self.fs.clone(), self.workspace.clone(), self.project.clone(), lsp_adapter_delegate, - assistant_panel, + window, cx, ); - editor.insert_default_prompt(cx); + editor.insert_default_prompt(window, cx); editor }); - self.show_context(editor.clone(), cx); + self.show_context(editor.clone(), window, cx); let workspace = self.workspace.clone(); - cx.spawn(move |_, mut cx| async move { + cx.spawn_in(window, move |_, mut cx| async move { workspace - .update(&mut cx, |workspace, cx| { - workspace.focus_panel::(cx); + .update_in(&mut cx, |workspace, window, cx| { + workspace.focus_panel::(window, cx); }) .ok(); }) @@ -1036,19 +858,34 @@ impl AssistantPanel { } } - fn show_context(&mut self, context_editor: View, cx: &mut ViewContext) { - let focus = self.focus_handle(cx).contains_focused(cx); + fn show_context( + &mut self, + context_editor: Entity, + window: &mut Window, + cx: &mut Context, + ) { + let focus = self.focus_handle(cx).contains_focused(window, cx); let prev_len = self.pane.read(cx).items_len(); self.pane.update(cx, |pane, cx| { - pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx) + pane.add_item( + Box::new(context_editor.clone()), + focus, + focus, + None, + window, + cx, + ) }); if prev_len != self.pane.read(cx).items_len() { - self.subscriptions - .push(cx.subscribe(&context_editor, Self::handle_context_editor_event)); + self.subscriptions.push(cx.subscribe_in( + &context_editor, + window, + Self::handle_context_editor_event, + )); } - self.show_updated_summary(&context_editor, cx); + self.show_updated_summary(&context_editor, window, cx); cx.emit(AssistantPanelEvent::ContextEdited); cx.notify(); @@ -1056,14 +893,15 @@ impl AssistantPanel { fn show_updated_summary( &self, - context_editor: &View, - cx: &mut ViewContext, + context_editor: &Entity, + window: &mut Window, + cx: &mut Context, ) { context_editor.update(cx, |context_editor, cx| { let new_summary = context_editor.title(cx).to_string(); self.model_summary_editor.update(cx, |summary_editor, cx| { if summary_editor.text(cx) != new_summary { - summary_editor.set_text(new_summary, cx); + summary_editor.set_text(new_summary, window, cx); } }); }); @@ -1071,13 +909,14 @@ impl AssistantPanel { fn handle_context_editor_event( &mut self, - context_editor: View, + context_editor: &Entity, event: &EditorEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { EditorEvent::TitleChanged => { - self.show_updated_summary(&context_editor, cx); + self.show_updated_summary(&context_editor, window, cx); cx.notify() } EditorEvent::Edited { .. } => { @@ -1102,22 +941,23 @@ impl AssistantPanel { fn show_configuration( workspace: &mut Workspace, _: &ShowConfiguration, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let Some(panel) = workspace.panel::(cx) else { return; }; - if !panel.focus_handle(cx).contains_focused(cx) { - workspace.toggle_panel_focus::(cx); + if !panel.focus_handle(cx).contains_focused(window, cx) { + workspace.toggle_panel_focus::(window, cx); } panel.update(cx, |this, cx| { - this.show_configuration_tab(cx); + this.show_configuration_tab(window, cx); }) } - fn show_configuration_tab(&mut self, cx: &mut ViewContext) { + fn show_configuration_tab(&mut self, window: &mut Window, cx: &mut Context) { let configuration_item_ix = self .pane .read(cx) @@ -1126,13 +966,14 @@ impl AssistantPanel { if let Some(configuration_item_ix) = configuration_item_ix { self.pane.update(cx, |pane, cx| { - pane.activate_item(configuration_item_ix, true, true, cx); + pane.activate_item(configuration_item_ix, true, true, window, cx); }); } else { - let configuration = cx.new_view(ConfigurationView::new); - self.configuration_subscription = Some(cx.subscribe( + let configuration = cx.new(|cx| ConfigurationView::new(window, cx)); + self.configuration_subscription = Some(cx.subscribe_in( &configuration, - |this, _, event: &ConfigurationViewEvent, cx| match event { + window, + |this, _, event: &ConfigurationViewEvent, window, cx| match event { ConfigurationViewEvent::NewProviderContextEditor(provider) => { if LanguageModelRegistry::read_global(cx) .active_provider() @@ -1147,17 +988,17 @@ impl AssistantPanel { } } - this.new_context(cx); + this.new_context(window, cx); } }, )); self.pane.update(cx, |pane, cx| { - pane.add_item(Box::new(configuration), true, true, None, cx); + pane.add_item(Box::new(configuration), true, true, None, window, cx); }); } } - fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext) { + fn deploy_history(&mut self, _: &DeployHistory, window: &mut Window, cx: &mut Context) { let history_item_ix = self .pane .read(cx) @@ -1166,25 +1007,30 @@ impl AssistantPanel { if let Some(history_item_ix) = history_item_ix { self.pane.update(cx, |pane, cx| { - pane.activate_item(history_item_ix, true, true, cx); + pane.activate_item(history_item_ix, true, true, window, cx); }); } else { - let assistant_panel = cx.view().downgrade(); - let history = cx.new_view(|cx| { + let history = cx.new(|cx| { ContextHistory::new( self.project.clone(), self.context_store.clone(), - assistant_panel, + self.workspace.clone(), + window, cx, ) }); self.pane.update(cx, |pane, cx| { - pane.add_item(Box::new(history), true, true, None, cx); + pane.add_item(Box::new(history), true, true, None, window, cx); }); } } - fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext) { + fn deploy_prompt_library( + &mut self, + _: &DeployPromptLibrary, + _window: &mut Window, + cx: &mut Context, + ) { open_prompt_library( self.languages.clone(), Box::new(PromptLibraryInlineAssist), @@ -1200,33 +1046,41 @@ impl AssistantPanel { .detach_and_log_err(cx); } - fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext) { - self.model_selector_menu_handle.toggle(cx); + fn toggle_model_selector( + &mut self, + _: &ToggleModelSelector, + window: &mut Window, + cx: &mut Context, + ) { + self.model_selector_menu_handle.toggle(window, cx); } - fn active_context_editor(&self, cx: &AppContext) -> Option> { + pub(crate) fn active_context_editor(&self, cx: &App) -> Option> { self.pane .read(cx) .active_item()? .downcast::() } - pub fn active_context(&self, cx: &AppContext) -> Option> { - Some(self.active_context_editor(cx)?.read(cx).context.clone()) + pub fn active_context(&self, cx: &App) -> Option> { + Some(self.active_context_editor(cx)?.read(cx).context().clone()) } - fn open_saved_context( + pub fn open_saved_context( &mut self, path: PathBuf, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { let existing_context = self.pane.read(cx).items().find_map(|item| { item.downcast::() - .filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path)) + .filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path)) }); if let Some(existing_context) = existing_context { - return cx.spawn(|this, mut cx| async move { - this.update(&mut cx, |this, cx| this.show_context(existing_context, cx)) + return cx.spawn_in(window, |this, mut cx| async move { + this.update_in(&mut cx, |this, window, cx| { + this.show_context(existing_context, window, cx) + }) }); } @@ -1239,41 +1093,41 @@ impl AssistantPanel { let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten(); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { let context = context.await?; - let assistant_panel = this.clone(); - this.update(&mut cx, |this, cx| { - let editor = cx.new_view(|cx| { + this.update_in(&mut cx, |this, window, cx| { + let editor = cx.new(|cx| { ContextEditor::for_context( context, fs, workspace, project, lsp_adapter_delegate, - assistant_panel, + window, cx, ) }); - this.show_context(editor, cx); + this.show_context(editor, window, cx); anyhow::Ok(()) })??; Ok(()) }) } - fn open_remote_context( + pub fn open_remote_context( &mut self, id: ContextId, - cx: &mut ViewContext, - ) -> Task>> { + window: &mut Window, + cx: &mut Context, + ) -> Task>> { let existing_context = self.pane.read(cx).items().find_map(|item| { item.downcast::() - .filter(|editor| *editor.read(cx).context.read(cx).id() == id) + .filter(|editor| *editor.read(cx).context().read(cx).id() == id) }); if let Some(existing_context) = existing_context { - return cx.spawn(|this, mut cx| async move { - this.update(&mut cx, |this, cx| { - this.show_context(existing_context.clone(), cx) + return cx.spawn_in(window, |this, mut cx| async move { + this.update_in(&mut cx, |this, window, cx| { + this.show_context(existing_context.clone(), window, cx) })?; Ok(existing_context) }); @@ -1288,34 +1142,33 @@ impl AssistantPanel { .log_err() .flatten(); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { let context = context.await?; - let assistant_panel = this.clone(); - this.update(&mut cx, |this, cx| { - let editor = cx.new_view(|cx| { + this.update_in(&mut cx, |this, window, cx| { + let editor = cx.new(|cx| { ContextEditor::for_context( context, fs, workspace, this.project.clone(), lsp_adapter_delegate, - assistant_panel, + window, cx, ) }); - this.show_context(editor.clone(), cx); + this.show_context(editor.clone(), window, cx); anyhow::Ok(editor) })? }) } - fn is_authenticated(&mut self, cx: &mut ViewContext) -> bool { + fn is_authenticated(&mut self, cx: &mut Context) -> bool { LanguageModelRegistry::read_global(cx) .active_provider() .map_or(false, |provider| provider.is_authenticated(cx)) } - fn authenticate(&mut self, cx: &mut ViewContext) -> Option>> { + fn authenticate(&mut self, cx: &mut Context) -> Option>> { LanguageModelRegistry::read_global(cx) .active_provider() .map_or(None, |provider| Some(provider.authenticate(cx))) @@ -1324,7 +1177,8 @@ impl AssistantPanel { fn restart_context_servers( workspace: &mut Workspace, _action: &context_server::Restart, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) { let Some(assistant_panel) = workspace.panel::(cx) else { return; @@ -1341,9 +1195,9 @@ impl AssistantPanel { } impl Render for AssistantPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let mut registrar = DivRegistrar::new( - |panel, cx| { + |panel, _, cx| { panel .pane .read(cx) @@ -1359,12 +1213,12 @@ impl Render for AssistantPanel { v_flex() .key_context("AssistantPanel") .size_full() - .on_action(cx.listener(|this, _: &NewContext, cx| { - this.new_context(cx); + .on_action(cx.listener(|this, _: &NewContext, window, cx| { + this.new_context(window, cx); + })) + .on_action(cx.listener(|this, _: &ShowConfiguration, window, cx| { + this.show_configuration_tab(window, cx) })) - .on_action( - cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)), - ) .on_action(cx.listener(AssistantPanel::deploy_history)) .on_action(cx.listener(AssistantPanel::deploy_prompt_library)) .on_action(cx.listener(AssistantPanel::toggle_model_selector)) @@ -1378,7 +1232,7 @@ impl Panel for AssistantPanel { "AssistantPanel" } - fn position(&self, cx: &WindowContext) -> DockPosition { + fn position(&self, _: &Window, cx: &App) -> DockPosition { match AssistantSettings::get_global(cx).dock { AssistantDockPosition::Left => DockPosition::Left, AssistantDockPosition::Bottom => DockPosition::Bottom, @@ -1390,7 +1244,7 @@ impl Panel for AssistantPanel { true } - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { settings::update_settings_file::( self.fs.clone(), cx, @@ -1405,9 +1259,9 @@ impl Panel for AssistantPanel { ); } - fn size(&self, cx: &WindowContext) -> Pixels { + fn size(&self, window: &Window, cx: &App) -> Pixels { let settings = AssistantSettings::get_global(cx); - match self.position(cx) { + match self.position(window, cx) { DockPosition::Left | DockPosition::Right => { self.width.unwrap_or(settings.default_width) } @@ -1415,33 +1269,33 @@ impl Panel for AssistantPanel { } } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - match self.position(cx) { + fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { + match self.position(window, cx) { DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Bottom => self.height = size, } cx.notify(); } - fn is_zoomed(&self, cx: &WindowContext) -> bool { + fn is_zoomed(&self, _: &Window, cx: &App) -> bool { self.pane.read(cx).is_zoomed() } - fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context) { self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); } - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { if active { if self.pane.read(cx).items_len() == 0 { - self.new_context(cx); + self.new_context(window, cx); } - self.ensure_authenticated(cx); + self.ensure_authenticated(window, cx); } } - fn pane(&self) -> Option> { + fn pane(&self) -> Option> { Some(self.pane.clone()) } @@ -1449,7 +1303,7 @@ impl Panel for AssistantPanel { Some(proto::PanelId::AssistantPanel) } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, _: &Window, cx: &App) -> Option { let settings = AssistantSettings::get_global(cx); if !settings.enabled || !settings.button { return None; @@ -1458,7 +1312,7 @@ impl Panel for AssistantPanel { Some(IconName::ZedAssistant) } - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> { Some("Assistant Panel") } @@ -1474,8 +1328,8 @@ impl Panel for AssistantPanel { impl EventEmitter for AssistantPanel {} impl EventEmitter for AssistantPanel {} -impl FocusableView for AssistantPanel { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for AssistantPanel { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.pane.focus_handle(cx) } } @@ -1485,3189 +1339,100 @@ struct PromptLibraryInlineAssist; impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist { fn assist( &self, - prompt_editor: &View, + prompt_editor: &Entity, initial_prompt: Option, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { InlineAssistant::update_global(cx, |assistant, cx| { - assistant.assist(&prompt_editor, None, None, initial_prompt, cx) + assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx) }) } fn focus_assistant_panel( &self, workspace: &mut Workspace, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> bool { - workspace.focus_panel::(cx).is_some() + workspace + .focus_panel::(window, cx) + .is_some() } } -pub enum ContextEditorEvent { - Edited, - TabContentChanged, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -struct ScrollPosition { - offset_before_cursor: gpui::Point, - cursor: Anchor, -} - -struct PatchViewState { - crease_id: CreaseId, - editor: Option, - update_task: Option>, -} - -struct PatchEditorState { - editor: WeakView, - opened_patch: AssistantPatch, -} - -type MessageHeader = MessageMetadata; +pub struct ConcreteAssistantPanelDelegate; -#[derive(Clone)] -enum AssistError { - FileRequired, - PaymentRequired, - MaxMonthlySpendReached, - Message(SharedString), -} - -pub struct ContextEditor { - context: Model, - fs: Arc, - slash_commands: Arc, - tools: Arc, - workspace: WeakView, - project: Model, - lsp_adapter_delegate: Option>, - editor: View, - blocks: HashMap, - image_blocks: HashSet, - scroll_position: Option, - remote_id: Option, - pending_slash_command_creases: HashMap, CreaseId>, - invoked_slash_command_creases: HashMap, - pending_tool_use_creases: HashMap, CreaseId>, - _subscriptions: Vec, - patches: HashMap, PatchViewState>, - active_patch: Option>, - assistant_panel: WeakView, - last_error: Option, - show_accept_terms: bool, - pub(crate) slash_menu_handle: - PopoverMenuHandle>, - // dragged_file_worktrees is used to keep references to worktrees that were added - // when the user drag/dropped an external file onto the context editor. Since - // the worktree is not part of the project panel, it would be dropped as soon as - // the file is opened. In order to keep the worktree alive for the duration of the - // context editor, we keep a reference here. - dragged_file_worktrees: Vec>, -} - -const DEFAULT_TAB_TITLE: &str = "New Chat"; -const MAX_TAB_TITLE_LEN: usize = 16; - -impl ContextEditor { - fn for_context( - context: Model, - fs: Arc, - workspace: WeakView, - project: Model, - lsp_adapter_delegate: Option>, - assistant_panel: WeakView, - cx: &mut ViewContext, - ) -> Self { - let completion_provider = SlashCommandCompletionProvider::new( - context.read(cx).slash_commands.clone(), - Some(cx.view().downgrade()), - Some(workspace.clone()), - ); - - let editor = cx.new_view(|cx| { - let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx); - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor.set_show_line_numbers(false, cx); - editor.set_show_scrollbars(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_show_code_actions(false, cx); - editor.set_show_runnables(false, cx); - editor.set_show_wrap_guides(false, cx); - editor.set_show_indent_guides(false, cx); - editor.set_completion_provider(Some(Box::new(completion_provider))); - editor.set_collaboration_hub(Box::new(project.clone())); - editor - }); - - let _subscriptions = vec![ - cx.observe(&context, |_, _, cx| cx.notify()), - cx.subscribe(&context, Self::handle_context_event), - cx.subscribe(&editor, Self::handle_editor_event), - cx.subscribe(&editor, Self::handle_editor_search_event), - ]; - - let sections = context.read(cx).slash_command_output_sections().to_vec(); - let patch_ranges = context.read(cx).patch_ranges().collect::>(); - let slash_commands = context.read(cx).slash_commands.clone(); - let tools = context.read(cx).tools.clone(); - let mut this = Self { - context, - slash_commands, - tools, - editor, - lsp_adapter_delegate, - blocks: Default::default(), - image_blocks: Default::default(), - scroll_position: None, - remote_id: None, - fs, - workspace, - project, - pending_slash_command_creases: HashMap::default(), - invoked_slash_command_creases: HashMap::default(), - pending_tool_use_creases: HashMap::default(), - _subscriptions, - patches: HashMap::default(), - active_patch: None, - assistant_panel, - last_error: None, - show_accept_terms: false, - slash_menu_handle: Default::default(), - dragged_file_worktrees: Vec::new(), - }; - this.update_message_headers(cx); - this.update_image_blocks(cx); - this.insert_slash_command_output_sections(sections, false, cx); - this.patches_updated(&Vec::new(), &patch_ranges, cx); - this - } - - fn insert_default_prompt(&mut self, cx: &mut ViewContext) { - let command_name = DefaultSlashCommand.name(); - self.editor.update(cx, |editor, cx| { - editor.insert(&format!("/{command_name}\n\n"), cx) - }); - let command = self.context.update(cx, |context, cx| { - context.reparse(cx); - context.parsed_slash_commands()[0].clone() - }); - self.run_command( - command.source_range, - &command.name, - &command.arguments, - false, - self.workspace.clone(), - cx, - ); - } - - fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { - self.send_to_model(RequestType::Chat, cx); - } - - fn edit(&mut self, _: &Edit, cx: &mut ViewContext) { - self.send_to_model(RequestType::SuggestEdits, cx); +impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { + fn active_context_editor( + &self, + workspace: &mut Workspace, + _window: &mut Window, + cx: &mut Context, + ) -> Option> { + let panel = workspace.panel::(cx)?; + panel.read(cx).active_context_editor(cx) } - fn focus_active_patch(&mut self, cx: &mut ViewContext) -> bool { - if let Some((_range, patch)) = self.active_patch() { - if let Some(editor) = patch - .editor - .as_ref() - .and_then(|state| state.editor.upgrade()) - { - cx.focus_view(&editor); - return true; - } - } + fn open_saved_context( + &self, + workspace: &mut Workspace, + path: PathBuf, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let Some(panel) = workspace.panel::(cx) else { + return Task::ready(Err(anyhow!("no Assistant panel found"))); + }; - false + panel.update(cx, |panel, cx| panel.open_saved_context(path, window, cx)) } - fn send_to_model(&mut self, request_type: RequestType, cx: &mut ViewContext) { - let provider = LanguageModelRegistry::read_global(cx).active_provider(); - if provider - .as_ref() - .map_or(false, |provider| provider.must_accept_terms(cx)) - { - self.show_accept_terms = true; - cx.notify(); - return; - } - - if self.focus_active_patch(cx) { - return; - } - - self.last_error = None; - - if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) { - self.last_error = Some(AssistError::FileRequired); - cx.notify(); - } else if let Some(user_message) = self - .context - .update(cx, |context, cx| context.assist(request_type, cx)) - { - let new_selection = { - let cursor = user_message - .start - .to_offset(self.context.read(cx).buffer().read(cx)); - cursor..cursor - }; - self.editor.update(cx, |editor, cx| { - editor.change_selections( - Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), - cx, - |selections| selections.select_ranges([new_selection]), - ); - }); - // Avoid scrolling to the new cursor position so the assistant's output is stable. - cx.defer(|this, _| this.scroll_position = None); - } + fn open_remote_context( + &self, + workspace: &mut Workspace, + context_id: ContextId, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let Some(panel) = workspace.panel::(cx) else { + return Task::ready(Err(anyhow!("no Assistant panel found"))); + }; - cx.notify(); + panel.update(cx, |panel, cx| { + panel.open_remote_context(context_id, window, cx) + }) } - fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { - self.last_error = None; - - if self - .context - .update(cx, |context, cx| context.cancel_last_assist(cx)) - { + fn quote_selection( + &self, + workspace: &mut Workspace, + creases: Vec<(String, String)>, + window: &mut Window, + cx: &mut Context, + ) { + let Some(panel) = workspace.panel::(cx) else { return; - } - - cx.propagate(); - } - - fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext) { - let cursors = self.cursors(cx); - self.context.update(cx, |context, cx| { - let messages = context - .messages_for_offsets(cursors, cx) - .into_iter() - .map(|message| message.id) - .collect(); - context.cycle_message_roles(messages, cx) - }); - } - - fn cursors(&self, cx: &mut WindowContext) -> Vec { - let selections = self - .editor - .update(cx, |editor, cx| editor.selections.all::(cx)); - selections - .into_iter() - .map(|selection| selection.head()) - .collect() - } - - pub fn insert_command(&mut self, name: &str, cx: &mut ViewContext) { - if let Some(command) = self.slash_commands.command(name, cx) { - self.editor.update(cx, |editor, cx| { - editor.transact(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()); - let snapshot = editor.buffer().read(cx).snapshot(cx); - let newest_cursor = editor.selections.newest::(cx).head(); - if newest_cursor.column > 0 - || snapshot - .chars_at(newest_cursor) - .next() - .map_or(false, |ch| ch != '\n') - { - editor.move_to_end_of_line( - &MoveToEndOfLine { - stop_at_soft_wraps: false, - }, - cx, - ); - editor.newline(&Newline, cx); - } - - editor.insert(&format!("/{name}"), cx); - if command.accepts_arguments() { - editor.insert(" ", cx); - editor.show_completions(&ShowCompletions::default(), cx); - } - }); - }); - if !command.requires_argument() { - self.confirm_command(&ConfirmCommand, cx); - } - } - } + }; - pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext) { - if self.editor.read(cx).has_active_completions_menu() { - return; + if !panel.focus_handle(cx).contains_focused(window, cx) { + workspace.toggle_panel_focus::(window, cx); } - let selections = self.editor.read(cx).selections.disjoint_anchors(); - let mut commands_by_range = HashMap::default(); - let workspace = self.workspace.clone(); - self.context.update(cx, |context, cx| { - context.reparse(cx); - for selection in selections.iter() { - if let Some(command) = - context.pending_command_for_position(selection.head().text_anchor, cx) + panel.update(cx, |_, cx| { + // Wait to create a new context until the workspace is no longer + // being updated. + cx.defer_in(window, move |panel, window, cx| { + if let Some(context) = panel + .active_context_editor(cx) + .or_else(|| panel.new_context(window, cx)) { - commands_by_range - .entry(command.source_range.clone()) - .or_insert_with(|| command.clone()); - } - } - }); - - if commands_by_range.is_empty() { - cx.propagate(); - } else { - for command in commands_by_range.into_values() { - self.run_command( - command.source_range, - &command.name, - &command.arguments, - true, - workspace.clone(), - cx, - ); - } - cx.stop_propagation(); - } - } - - #[allow(clippy::too_many_arguments)] - pub fn run_command( - &mut self, - command_range: Range, - name: &str, - arguments: &[String], - ensure_trailing_newline: bool, - workspace: WeakView, - cx: &mut ViewContext, - ) { - if let Some(command) = self.slash_commands.command(name, cx) { - let context = self.context.read(cx); - let sections = context - .slash_command_output_sections() - .into_iter() - .filter(|section| section.is_valid(context.buffer().read(cx))) - .cloned() - .collect::>(); - let snapshot = context.buffer().read(cx).snapshot(); - let output = command.run( - arguments, - §ions, - snapshot, - workspace, - self.lsp_adapter_delegate.clone(), - cx, - ); - self.context.update(cx, |context, cx| { - context.insert_command_output( - command_range, - name, - output, - ensure_trailing_newline, - cx, - ) + context.update(cx, |context, cx| context.quote_creases(creases, window, cx)); + }; }); - } + }); } - - fn handle_context_event( - &mut self, - _: Model, - event: &ContextEvent, - cx: &mut ViewContext, - ) { - let context_editor = cx.view().downgrade(); - - match event { - ContextEvent::MessagesEdited => { - self.update_message_headers(cx); - self.update_image_blocks(cx); - self.context.update(cx, |context, cx| { - context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); - }); - } - ContextEvent::SummaryChanged => { - cx.emit(EditorEvent::TitleChanged); - self.context.update(cx, |context, cx| { - context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); - }); - } - ContextEvent::StreamedCompletion => { - self.editor.update(cx, |editor, cx| { - if let Some(scroll_position) = self.scroll_position { - let snapshot = editor.snapshot(cx); - let cursor_point = scroll_position.cursor.to_display_point(&snapshot); - let scroll_top = - cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y; - editor.set_scroll_position( - point(scroll_position.offset_before_cursor.x, scroll_top), - cx, - ); - } - - let new_tool_uses = self - .context - .read(cx) - .pending_tool_uses() - .into_iter() - .filter(|tool_use| { - !self - .pending_tool_use_creases - .contains_key(&tool_use.source_range) - }) - .cloned() - .collect::>(); - - let buffer = editor.buffer().read(cx).snapshot(cx); - let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap(); - let excerpt_id = *excerpt_id; - - let mut buffer_rows_to_fold = BTreeSet::new(); - - let creases = new_tool_uses - .iter() - .map(|tool_use| { - let placeholder = FoldPlaceholder { - render: render_fold_icon_button( - cx.view().downgrade(), - IconName::PocketKnife, - tool_use.name.clone().into(), - ), - ..Default::default() - }; - let render_trailer = - move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any(); - - let start = buffer - .anchor_in_excerpt(excerpt_id, tool_use.source_range.start) - .unwrap(); - let end = buffer - .anchor_in_excerpt(excerpt_id, tool_use.source_range.end) - .unwrap(); - - let buffer_row = MultiBufferRow(start.to_point(&buffer).row); - buffer_rows_to_fold.insert(buffer_row); - - self.context.update(cx, |context, cx| { - context.insert_content( - Content::ToolUse { - range: tool_use.source_range.clone(), - tool_use: LanguageModelToolUse { - id: tool_use.id.clone(), - name: tool_use.name.clone(), - input: tool_use.input.clone(), - }, - }, - cx, - ); - }); - - Crease::inline( - start..end, - placeholder, - fold_toggle("tool-use"), - render_trailer, - ) - }) - .collect::>(); - - let crease_ids = editor.insert_creases(creases, cx); - - for buffer_row in buffer_rows_to_fold.into_iter().rev() { - editor.fold_at(&FoldAt { buffer_row }, cx); - } - - self.pending_tool_use_creases.extend( - new_tool_uses - .iter() - .map(|tool_use| tool_use.source_range.clone()) - .zip(crease_ids), - ); - }); - } - ContextEvent::PatchesUpdated { removed, updated } => { - self.patches_updated(removed, updated, cx); - } - ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => { - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let (&excerpt_id, _, _) = buffer.as_singleton().unwrap(); - - editor.remove_creases( - removed - .iter() - .filter_map(|range| self.pending_slash_command_creases.remove(range)), - cx, - ); - - let crease_ids = editor.insert_creases( - updated.iter().map(|command| { - let workspace = self.workspace.clone(); - let confirm_command = Arc::new({ - let context_editor = context_editor.clone(); - let command = command.clone(); - move |cx: &mut WindowContext| { - context_editor - .update(cx, |context_editor, cx| { - context_editor.run_command( - command.source_range.clone(), - &command.name, - &command.arguments, - false, - workspace.clone(), - cx, - ); - }) - .ok(); - } - }); - let placeholder = FoldPlaceholder { - render: Arc::new(move |_, _, _| Empty.into_any()), - ..Default::default() - }; - let render_toggle = { - let confirm_command = confirm_command.clone(); - let command = command.clone(); - move |row, _, _, _cx: &mut WindowContext| { - render_pending_slash_command_gutter_decoration( - row, - &command.status, - confirm_command.clone(), - ) - } - }; - let render_trailer = { - let command = command.clone(); - move |row, _unfold, cx: &mut WindowContext| { - // TODO: In the future we should investigate how we can expose - // this as a hook on the `SlashCommand` trait so that we don't - // need to special-case it here. - if command.name == DocsSlashCommand::NAME { - return render_docs_slash_command_trailer( - row, - command.clone(), - cx, - ); - } - - Empty.into_any() - } - }; - - let start = buffer - .anchor_in_excerpt(excerpt_id, command.source_range.start) - .unwrap(); - let end = buffer - .anchor_in_excerpt(excerpt_id, command.source_range.end) - .unwrap(); - Crease::inline(start..end, placeholder, render_toggle, render_trailer) - }), - cx, - ); - - self.pending_slash_command_creases.extend( - updated - .iter() - .map(|command| command.source_range.clone()) - .zip(crease_ids), - ); - }) - } - ContextEvent::InvokedSlashCommandChanged { command_id } => { - self.update_invoked_slash_command(*command_id, cx); - } - ContextEvent::SlashCommandOutputSectionAdded { section } => { - self.insert_slash_command_output_sections([section.clone()], false, cx); - } - ContextEvent::UsePendingTools => { - let pending_tool_uses = self - .context - .read(cx) - .pending_tool_uses() - .into_iter() - .filter(|tool_use| tool_use.status.is_idle()) - .cloned() - .collect::>(); - - for tool_use in pending_tool_uses { - if let Some(tool) = self.tools.tool(&tool_use.name, cx) { - let task = tool.run(tool_use.input, self.workspace.clone(), cx); - - self.context.update(cx, |context, cx| { - context.insert_tool_output(tool_use.id.clone(), task, cx); - }); - } - } - } - ContextEvent::ToolFinished { - tool_use_id, - output_range, - } => { - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap(); - let excerpt_id = *excerpt_id; - - let placeholder = FoldPlaceholder { - render: render_fold_icon_button( - cx.view().downgrade(), - IconName::PocketKnife, - format!("Tool Result: {tool_use_id}").into(), - ), - ..Default::default() - }; - let render_trailer = - move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any(); - - let start = buffer - .anchor_in_excerpt(excerpt_id, output_range.start) - .unwrap(); - let end = buffer - .anchor_in_excerpt(excerpt_id, output_range.end) - .unwrap(); - - let buffer_row = MultiBufferRow(start.to_point(&buffer).row); - - let crease = Crease::inline( - start..end, - placeholder, - fold_toggle("tool-use"), - render_trailer, - ); - - editor.insert_creases([crease], cx); - editor.fold_at(&FoldAt { buffer_row }, cx); - }); - } - ContextEvent::Operation(_) => {} - ContextEvent::ShowAssistError(error_message) => { - self.last_error = Some(AssistError::Message(error_message.clone())); - } - ContextEvent::ShowPaymentRequiredError => { - self.last_error = Some(AssistError::PaymentRequired); - } - ContextEvent::ShowMaxMonthlySpendReachedError => { - self.last_error = Some(AssistError::MaxMonthlySpendReached); - } - } - } - - fn update_invoked_slash_command( - &mut self, - command_id: InvokedSlashCommandId, - cx: &mut ViewContext, - ) { - if let Some(invoked_slash_command) = - self.context.read(cx).invoked_slash_command(&command_id) - { - if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - let run_commands_in_ranges = invoked_slash_command - .run_commands_in_ranges - .iter() - .cloned() - .collect::>(); - for range in run_commands_in_ranges { - let commands = self.context.update(cx, |context, cx| { - context.reparse(cx); - context - .pending_commands_for_range(range.clone(), cx) - .to_vec() - }); - - for command in commands { - self.run_command( - command.source_range, - &command.name, - &command.arguments, - false, - self.workspace.clone(), - cx, - ); - } - } - } - } - - self.editor.update(cx, |editor, cx| { - if let Some(invoked_slash_command) = - self.context.read(cx).invoked_slash_command(&command_id) - { - if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - let buffer = editor.buffer().read(cx).snapshot(cx); - let (&excerpt_id, _buffer_id, _buffer_snapshot) = - buffer.as_singleton().unwrap(); - - let start = buffer - .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start) - .unwrap(); - let end = buffer - .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end) - .unwrap(); - editor.remove_folds_with_type( - &[start..end], - TypeId::of::(), - false, - cx, - ); - - editor.remove_creases( - HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), - cx, - ); - } else if let hash_map::Entry::Vacant(entry) = - self.invoked_slash_command_creases.entry(command_id) - { - let buffer = editor.buffer().read(cx).snapshot(cx); - let (&excerpt_id, _buffer_id, _buffer_snapshot) = - buffer.as_singleton().unwrap(); - let context = self.context.downgrade(); - let crease_start = buffer - .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start) - .unwrap(); - let crease_end = buffer - .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end) - .unwrap(); - let crease = Crease::inline( - crease_start..crease_end, - invoked_slash_command_fold_placeholder(command_id, context), - fold_toggle("invoked-slash-command"), - |_row, _folded, _cx| Empty.into_any(), - ); - let crease_ids = editor.insert_creases([crease.clone()], cx); - editor.fold_creases(vec![crease], false, cx); - entry.insert(crease_ids[0]); - } else { - cx.notify() - } - } else { - editor.remove_creases( - HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), - cx, - ); - cx.notify(); - }; - }); - } - - fn patches_updated( - &mut self, - removed: &Vec>, - updated: &Vec>, - cx: &mut ViewContext, - ) { - let this = cx.view().downgrade(); - let mut editors_to_close = Vec::new(); - - self.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let multibuffer = &snapshot.buffer_snapshot; - let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap(); - - let mut removed_crease_ids = Vec::new(); - let mut ranges_to_unfold: Vec> = Vec::new(); - for range in removed { - if let Some(state) = self.patches.remove(range) { - let patch_start = multibuffer - .anchor_in_excerpt(excerpt_id, range.start) - .unwrap(); - let patch_end = multibuffer - .anchor_in_excerpt(excerpt_id, range.end) - .unwrap(); - - editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade())); - ranges_to_unfold.push(patch_start..patch_end); - removed_crease_ids.push(state.crease_id); - } - } - editor.unfold_ranges(&ranges_to_unfold, true, false, cx); - editor.remove_creases(removed_crease_ids, cx); - - for range in updated { - let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else { - continue; - }; - - let path_count = patch.path_count(); - let patch_start = multibuffer - .anchor_in_excerpt(excerpt_id, patch.range.start) - .unwrap(); - let patch_end = multibuffer - .anchor_in_excerpt(excerpt_id, patch.range.end) - .unwrap(); - let render_block: RenderBlock = Arc::new({ - let this = this.clone(); - let patch_range = range.clone(); - move |cx: &mut BlockContext<'_, '_>| { - let max_width = cx.max_width; - let gutter_width = cx.gutter_dimensions.full_width(); - let block_id = cx.block_id; - let selected = cx.selected; - this.update(&mut **cx, |this, cx| { - this.render_patch_block( - patch_range.clone(), - max_width, - gutter_width, - block_id, - selected, - cx, - ) - }) - .ok() - .flatten() - .unwrap_or_else(|| Empty.into_any()) - } - }); - - let height = path_count as u32 + 1; - let crease = Crease::block( - patch_start..patch_end, - height, - BlockStyle::Flex, - render_block.clone(), - ); - - let should_refold; - if let Some(state) = self.patches.get_mut(&range) { - if let Some(editor_state) = &state.editor { - if editor_state.opened_patch != patch { - state.update_task = Some({ - let this = this.clone(); - cx.spawn(|_, cx| async move { - Self::update_patch_editor(this.clone(), patch, cx) - .await - .log_err(); - }) - }); - } - } - - should_refold = - snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot)); - } else { - let crease_id = editor.insert_creases([crease.clone()], cx)[0]; - self.patches.insert( - range.clone(), - PatchViewState { - crease_id, - editor: None, - update_task: None, - }, - ); - - should_refold = true; - } - - if should_refold { - editor.unfold_ranges(&[patch_start..patch_end], true, false, cx); - editor.fold_creases(vec![crease], false, cx); - } - } - }); - - for editor in editors_to_close { - self.close_patch_editor(editor, cx); - } - - self.update_active_patch(cx); - } - - fn insert_slash_command_output_sections( - &mut self, - sections: impl IntoIterator>, - expand_result: bool, - cx: &mut ViewContext, - ) { - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let excerpt_id = *buffer.as_singleton().unwrap().0; - let mut buffer_rows_to_fold = BTreeSet::new(); - let mut creases = Vec::new(); - for section in sections { - let start = buffer - .anchor_in_excerpt(excerpt_id, section.range.start) - .unwrap(); - let end = buffer - .anchor_in_excerpt(excerpt_id, section.range.end) - .unwrap(); - let buffer_row = MultiBufferRow(start.to_point(&buffer).row); - buffer_rows_to_fold.insert(buffer_row); - creases.push( - Crease::inline( - start..end, - FoldPlaceholder { - render: render_fold_icon_button( - cx.view().downgrade(), - section.icon, - section.label.clone(), - ), - merge_adjacent: false, - ..Default::default() - }, - render_slash_command_output_toggle, - |_, _, _| Empty.into_any_element(), - ) - .with_metadata(CreaseMetadata { - icon: section.icon, - label: section.label, - }), - ); - } - - editor.insert_creases(creases, cx); - - if expand_result { - buffer_rows_to_fold.clear(); - } - for buffer_row in buffer_rows_to_fold.into_iter().rev() { - editor.fold_at(&FoldAt { buffer_row }, cx); - } - }); - } - - fn handle_editor_event( - &mut self, - _: View, - event: &EditorEvent, - cx: &mut ViewContext, - ) { - match event { - EditorEvent::ScrollPositionChanged { autoscroll, .. } => { - let cursor_scroll_position = self.cursor_scroll_position(cx); - if *autoscroll { - self.scroll_position = cursor_scroll_position; - } else if self.scroll_position != cursor_scroll_position { - self.scroll_position = None; - } - } - EditorEvent::SelectionsChanged { .. } => { - self.scroll_position = self.cursor_scroll_position(cx); - self.update_active_patch(cx); - } - _ => {} - } - cx.emit(event.clone()); - } - - fn active_patch(&self) -> Option<(Range, &PatchViewState)> { - let patch = self.active_patch.as_ref()?; - Some((patch.clone(), self.patches.get(&patch)?)) - } - - fn update_active_patch(&mut self, cx: &mut ViewContext) { - let newest_cursor = self.editor.update(cx, |editor, cx| { - editor.selections.newest::(cx).head() - }); - let context = self.context.read(cx); - - let new_patch = context.patch_containing(newest_cursor, cx).cloned(); - - if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() { - return; - } - - if let Some(old_patch_range) = self.active_patch.take() { - if let Some(patch_state) = self.patches.get_mut(&old_patch_range) { - if let Some(state) = patch_state.editor.take() { - if let Some(editor) = state.editor.upgrade() { - self.close_patch_editor(editor, cx); - } - } - } - } - - if let Some(new_patch) = new_patch { - self.active_patch = Some(new_patch.range.clone()); - - if let Some(patch_state) = self.patches.get_mut(&new_patch.range) { - let mut editor = None; - if let Some(state) = &patch_state.editor { - if let Some(opened_editor) = state.editor.upgrade() { - editor = Some(opened_editor); - } - } - - if let Some(editor) = editor { - self.workspace - .update(cx, |workspace, cx| { - workspace.activate_item(&editor, true, false, cx); - }) - .ok(); - } else { - patch_state.update_task = Some(cx.spawn(move |this, cx| async move { - Self::open_patch_editor(this, new_patch, cx).await.log_err(); - })); - } - } - } - } - - fn close_patch_editor( - &mut self, - editor: View, - cx: &mut ViewContext, - ) { - self.workspace - .update(cx, |workspace, cx| { - if let Some(pane) = workspace.pane_for(&editor) { - pane.update(cx, |pane, cx| { - let item_id = editor.entity_id(); - if !editor.read(cx).focus_handle(cx).is_focused(cx) { - pane.close_item_by_id(item_id, SaveIntent::Skip, cx) - .detach_and_log_err(cx); - } - }); - } - }) - .ok(); - } - - async fn open_patch_editor( - this: WeakView, - patch: AssistantPatch, - mut cx: AsyncWindowContext, - ) -> Result<()> { - let project = this.update(&mut cx, |this, _| this.project.clone())?; - let resolved_patch = patch.resolve(project.clone(), &mut cx).await; - - let editor = cx.new_view(|cx| { - let editor = ProposedChangesEditor::new( - patch.title.clone(), - resolved_patch - .edit_groups - .iter() - .map(|(buffer, groups)| ProposedChangeLocation { - buffer: buffer.clone(), - ranges: groups - .iter() - .map(|group| group.context_range.clone()) - .collect(), - }) - .collect(), - Some(project.clone()), - cx, - ); - resolved_patch.apply(&editor, cx); - editor - })?; - - this.update(&mut cx, |this, cx| { - if let Some(patch_state) = this.patches.get_mut(&patch.range) { - patch_state.editor = Some(PatchEditorState { - editor: editor.downgrade(), - opened_patch: patch, - }); - patch_state.update_task.take(); - } - - this.workspace - .update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx) - }) - .log_err(); - })?; - - Ok(()) - } - - async fn update_patch_editor( - this: WeakView, - patch: AssistantPatch, - mut cx: AsyncWindowContext, - ) -> Result<()> { - let project = this.update(&mut cx, |this, _| this.project.clone())?; - let resolved_patch = patch.resolve(project.clone(), &mut cx).await; - this.update(&mut cx, |this, cx| { - let patch_state = this.patches.get_mut(&patch.range)?; - - let locations = resolved_patch - .edit_groups - .iter() - .map(|(buffer, groups)| ProposedChangeLocation { - buffer: buffer.clone(), - ranges: groups - .iter() - .map(|group| group.context_range.clone()) - .collect(), - }) - .collect(); - - if let Some(state) = &mut patch_state.editor { - if let Some(editor) = state.editor.upgrade() { - editor.update(cx, |editor, cx| { - editor.set_title(patch.title.clone(), cx); - editor.reset_locations(locations, cx); - resolved_patch.apply(editor, cx); - }); - - state.opened_patch = patch; - } else { - patch_state.editor.take(); - } - } - patch_state.update_task.take(); - - Some(()) - })?; - Ok(()) - } - - fn handle_editor_search_event( - &mut self, - _: View, - event: &SearchEvent, - cx: &mut ViewContext, - ) { - cx.emit(event.clone()); - } - - fn cursor_scroll_position(&self, cx: &mut ViewContext) -> Option { - self.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let cursor = editor.selections.newest_anchor().head(); - let cursor_row = cursor - .to_display_point(&snapshot.display_snapshot) - .row() - .as_f32(); - let scroll_position = editor - .scroll_manager - .anchor() - .scroll_position(&snapshot.display_snapshot); - - let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.); - if (scroll_position.y..scroll_bottom).contains(&cursor_row) { - Some(ScrollPosition { - cursor, - offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), - }) - } else { - None - } - }) - } - - fn esc_kbd(cx: &WindowContext) -> Div { - let colors = cx.theme().colors().clone(); - - h_flex() - .items_center() - .gap_1() - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .text_size(TextSize::XSmall.rems(cx)) - .text_color(colors.text_muted) - .child("Press") - .child( - h_flex() - .rounded_md() - .px_1() - .mr_0p5() - .border_1() - .border_color(theme::color_alpha(colors.border_variant, 0.6)) - .bg(theme::color_alpha(colors.element_background, 0.6)) - .child("esc"), - ) - .child("to cancel") - } - - fn update_message_headers(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - - let excerpt_id = *buffer.as_singleton().unwrap().0; - let mut old_blocks = std::mem::take(&mut self.blocks); - let mut blocks_to_remove: HashMap<_, _> = old_blocks - .iter() - .map(|(message_id, (_, block_id))| (*message_id, *block_id)) - .collect(); - let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default(); - - let render_block = |message: MessageMetadata| -> RenderBlock { - Arc::new({ - let context = self.context.clone(); - - move |cx| { - let message_id = MessageId(message.timestamp); - let llm_loading = message.role == Role::Assistant - && message.status == MessageStatus::Pending; - - let (label, spinner, note) = match message.role { - Role::User => ( - Label::new("You").color(Color::Default).into_any_element(), - None, - None, - ), - Role::Assistant => { - let base_label = Label::new("Assistant").color(Color::Info); - let mut spinner = None; - let mut note = None; - let animated_label = if llm_loading { - base_label - .with_animation( - "pulsating-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.alpha(delta), - ) - .into_any_element() - } else { - base_label.into_any_element() - }; - if llm_loading { - spinner = Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::XSmall) - .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate( - percentage(delta), - )) - }, - ) - .into_any_element(), - ); - note = Some(Self::esc_kbd(cx).into_any_element()); - } - (animated_label, spinner, note) - } - Role::System => ( - Label::new("System") - .color(Color::Warning) - .into_any_element(), - None, - None, - ), - }; - - let sender = h_flex() - .items_center() - .gap_2p5() - .child( - ButtonLike::new("role") - .style(ButtonStyle::Filled) - .child( - h_flex() - .items_center() - .gap_1p5() - .child(label) - .children(spinner), - ) - .tooltip(|cx| { - Tooltip::with_meta( - "Toggle message role", - None, - "Available roles: You (User), Assistant, System", - cx, - ) - }) - .on_click({ - let context = context.clone(); - move |_, cx| { - context.update(cx, |context, cx| { - context.cycle_message_roles( - HashSet::from_iter(Some(message_id)), - cx, - ) - }) - } - }), - ) - .children(note); - - h_flex() - .id(("message_header", message_id.as_u64())) - .pl(cx.gutter_dimensions.full_width()) - .h_11() - .w_full() - .relative() - .gap_1p5() - .child(sender) - .children(match &message.cache { - Some(cache) if cache.is_final_anchor => match cache.status { - CacheStatus::Cached => Some( - div() - .id("cached") - .child( - Icon::new(IconName::DatabaseZap) - .size(IconSize::XSmall) - .color(Color::Hint), - ) - .tooltip(|cx| { - Tooltip::with_meta( - "Context Cached", - None, - "Large messages cached to optimize performance", - cx, - ) - }) - .into_any_element(), - ), - CacheStatus::Pending => Some( - div() - .child( - Icon::new(IconName::Ellipsis) - .size(IconSize::XSmall) - .color(Color::Hint), - ) - .into_any_element(), - ), - }, - _ => None, - }) - .children(match &message.status { - MessageStatus::Error(error) => Some( - Button::new("show-error", "Error") - .color(Color::Error) - .selected_label_color(Color::Error) - .selected_icon_color(Color::Error) - .icon(IconName::XCircle) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .tooltip(move |cx| Tooltip::text("View Details", cx)) - .on_click({ - let context = context.clone(); - let error = error.clone(); - move |_, cx| { - context.update(cx, |_, cx| { - cx.emit(ContextEvent::ShowAssistError( - error.clone(), - )); - }); - } - }) - .into_any_element(), - ), - MessageStatus::Canceled => Some( - h_flex() - .gap_1() - .items_center() - .child( - Icon::new(IconName::XCircle) - .color(Color::Disabled) - .size(IconSize::XSmall), - ) - .child( - Label::new("Canceled") - .size(LabelSize::Small) - .color(Color::Disabled), - ) - .into_any_element(), - ), - _ => None, - }) - .into_any_element() - } - }) - }; - let create_block_properties = |message: &Message| BlockProperties { - height: 2, - style: BlockStyle::Sticky, - placement: BlockPlacement::Above( - buffer - .anchor_in_excerpt(excerpt_id, message.anchor_range.start) - .unwrap(), - ), - priority: usize::MAX, - render: render_block(MessageMetadata::from(message)), - }; - let mut new_blocks = vec![]; - let mut block_index_to_message = vec![]; - for message in self.context.read(cx).messages(cx) { - if let Some(_) = blocks_to_remove.remove(&message.id) { - // This is an old message that we might modify. - let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else { - debug_assert!( - false, - "old_blocks should contain a message_id we've just removed." - ); - continue; - }; - // Should we modify it? - let message_meta = MessageMetadata::from(&message); - if meta != &message_meta { - blocks_to_replace.insert(*block_id, render_block(message_meta.clone())); - *meta = message_meta; - } - } else { - // This is a new message. - new_blocks.push(create_block_properties(&message)); - block_index_to_message.push((message.id, MessageMetadata::from(&message))); - } - } - editor.replace_blocks(blocks_to_replace, None, cx); - editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx); - - let ids = editor.insert_blocks(new_blocks, None, cx); - old_blocks.extend(ids.into_iter().zip(block_index_to_message).map( - |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)), - )); - self.blocks = old_blocks; - }); - } - - /// Returns either the selected text, or the content of the Markdown code - /// block surrounding the cursor. - fn get_selection_or_code_block( - context_editor_view: &View, - cx: &mut ViewContext, - ) -> Option<(String, bool)> { - const CODE_FENCE_DELIMITER: &'static str = "```"; - - let context_editor = context_editor_view.read(cx).editor.clone(); - context_editor.update(cx, |context_editor, cx| { - if context_editor.selections.newest::(cx).is_empty() { - let snapshot = context_editor.buffer().read(cx).snapshot(cx); - let (_, _, snapshot) = snapshot.as_singleton()?; - - let head = context_editor.selections.newest::(cx).head(); - let offset = snapshot.point_to_offset(head); - - let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?; - let mut text = snapshot - .text_for_range(surrounding_code_block_range) - .collect::(); - - // If there is no newline trailing the closing three-backticks, then - // tree-sitter-md extends the range of the content node to include - // the backticks. - if text.ends_with(CODE_FENCE_DELIMITER) { - text.drain((text.len() - CODE_FENCE_DELIMITER.len())..); - } - - (!text.is_empty()).then_some((text, true)) - } else { - let anchor = context_editor.selections.newest_anchor(); - let text = context_editor - .buffer() - .read(cx) - .read(cx) - .text_for_range(anchor.range()) - .collect::(); - - (!text.is_empty()).then_some((text, false)) - } - }) - } - - fn insert_selection( - workspace: &mut Workspace, - _: &InsertIntoEditor, - cx: &mut ViewContext, - ) { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else { - return; - }; - let Some(active_editor_view) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - - if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) { - active_editor_view.update(cx, |editor, cx| { - editor.insert(&text, cx); - editor.focus(cx); - }) - } - } - - fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext) { - let result = maybe!({ - let panel = workspace.panel::(cx)?; - let context_editor_view = panel.read(cx).active_context_editor(cx)?; - Self::get_selection_or_code_block(&context_editor_view, cx) - }); - let Some((text, is_code_block)) = result else { - return; - }; - - cx.write_to_clipboard(ClipboardItem::new_string(text)); - - struct CopyToClipboardToast; - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - format!( - "{} copied to clipboard.", - if is_code_block { - "Code block" - } else { - "Selection" - } - ), - ) - .autohide(), - cx, - ); - } - - fn insert_dragged_files( - workspace: &mut Workspace, - action: &InsertDraggedFiles, - cx: &mut ViewContext, - ) { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else { - return; - }; - - let project = workspace.project().clone(); - - let paths = match action { - InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])), - InsertDraggedFiles::ExternalFiles(paths) => { - let tasks = paths - .clone() - .into_iter() - .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx)) - .collect::>(); - - cx.spawn(move |_, cx| async move { - let mut paths = vec![]; - let mut worktrees = vec![]; - - let opened_paths = futures::future::join_all(tasks).await; - for (worktree, project_path) in opened_paths.into_iter().flatten() { - let Ok(worktree_root_name) = - worktree.read_with(&cx, |worktree, _| worktree.root_name().to_string()) - else { - continue; - }; - - let mut full_path = PathBuf::from(worktree_root_name.clone()); - full_path.push(&project_path.path); - paths.push(full_path); - worktrees.push(worktree); - } - - (paths, worktrees) - }) - } - }; - - cx.spawn(|_, mut cx| async move { - let (paths, dragged_file_worktrees) = paths.await; - let cmd_name = FileSlashCommand.name(); - - context_editor_view - .update(&mut cx, |context_editor, cx| { - let file_argument = paths - .into_iter() - .map(|path| path.to_string_lossy().to_string()) - .collect::>() - .join(" "); - - context_editor.editor.update(cx, |editor, cx| { - editor.insert("\n", cx); - editor.insert(&format!("/{} {}", cmd_name, file_argument), cx); - }); - - context_editor.confirm_command(&ConfirmCommand, cx); - - context_editor - .dragged_file_worktrees - .extend(dragged_file_worktrees); - }) - .log_err(); - }) - .detach(); - } - - fn quote_selection( - workspace: &mut Workspace, - _: &QuoteSelection, - cx: &mut ViewContext, - ) { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - - let Some(creases) = selections_creases(workspace, cx) else { - return; - }; - - if creases.is_empty() { - return; - } - // Activate the panel - if !panel.focus_handle(cx).contains_focused(cx) { - workspace.toggle_panel_focus::(cx); - } - - panel.update(cx, |_, cx| { - // Wait to create a new context until the workspace is no longer - // being updated. - cx.defer(move |panel, cx| { - if let Some(context) = panel - .active_context_editor(cx) - .or_else(|| panel.new_context(cx)) - { - context.update(cx, |context, cx| { - context.editor.update(cx, |editor, cx| { - editor.insert("\n", cx); - for (text, crease_title) in creases { - let point = editor.selections.newest::(cx).head(); - let start_row = MultiBufferRow(point.row); - - editor.insert(&text, cx); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - let anchor_before = snapshot.anchor_after(point); - let anchor_after = editor - .selections - .newest_anchor() - .head() - .bias_left(&snapshot); - - editor.insert("\n", cx); - - let fold_placeholder = quote_selection_fold_placeholder( - crease_title, - cx.view().downgrade(), - ); - let crease = Crease::inline( - anchor_before..anchor_after, - fold_placeholder, - render_quote_selection_output_toggle, - |_, _, _| Empty.into_any(), - ); - editor.insert_creases(vec![crease], cx); - editor.fold_at( - &FoldAt { - buffer_row: start_row, - }, - cx, - ); - } - }) - }); - }; - }); - }); - } - - fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext) { - if self.editor.read(cx).selections.count() == 1 { - let (copied_text, metadata, _) = self.get_clipboard_contents(cx); - cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( - copied_text, - metadata, - )); - cx.stop_propagation(); - return; - } - - cx.propagate(); - } - - fn cut(&mut self, _: &editor::actions::Cut, cx: &mut ViewContext) { - if self.editor.read(cx).selections.count() == 1 { - let (copied_text, metadata, selections) = self.get_clipboard_contents(cx); - - self.editor.update(cx, |editor, cx| { - editor.transact(cx, |this, cx| { - this.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select(selections); - }); - this.insert("", cx); - cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( - copied_text, - metadata, - )); - }); - }); - - cx.stop_propagation(); - return; - } - - cx.propagate(); - } - - fn get_clipboard_contents( - &mut self, - cx: &mut ViewContext, - ) -> (String, CopyMetadata, Vec>) { - let (snapshot, selection, creases) = self.editor.update(cx, |editor, cx| { - let mut selection = editor.selections.newest::(cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); - - let is_entire_line = selection.is_empty() || editor.selections.line_mode; - if is_entire_line { - selection.start = Point::new(selection.start.row, 0); - selection.end = - cmp::min(snapshot.max_point(), Point::new(selection.start.row + 1, 0)); - selection.goal = SelectionGoal::None; - } - - let selection_start = snapshot.point_to_offset(selection.start); - - ( - snapshot.clone(), - selection.clone(), - editor.display_map.update(cx, |display_map, cx| { - display_map - .snapshot(cx) - .crease_snapshot - .creases_in_range( - MultiBufferRow(selection.start.row) - ..MultiBufferRow(selection.end.row + 1), - &snapshot, - ) - .filter_map(|crease| { - if let Crease::Inline { - range, metadata, .. - } = &crease - { - let metadata = metadata.as_ref()?; - let start = range - .start - .to_offset(&snapshot) - .saturating_sub(selection_start); - let end = range - .end - .to_offset(&snapshot) - .saturating_sub(selection_start); - - let range_relative_to_selection = start..end; - if !range_relative_to_selection.is_empty() { - return Some(SelectedCreaseMetadata { - range_relative_to_selection, - crease: metadata.clone(), - }); - } - } - None - }) - .collect::>() - }), - ) - }); - - let selection = selection.map(|point| snapshot.point_to_offset(point)); - let context = self.context.read(cx); - - let mut text = String::new(); - for message in context.messages(cx) { - if message.offset_range.start >= selection.range().end { - break; - } else if message.offset_range.end >= selection.range().start { - let range = cmp::max(message.offset_range.start, selection.range().start) - ..cmp::min(message.offset_range.end, selection.range().end); - if !range.is_empty() { - for chunk in context.buffer().read(cx).text_for_range(range) { - text.push_str(chunk); - } - if message.offset_range.end < selection.range().end { - text.push('\n'); - } - } - } - } - - (text, CopyMetadata { creases }, vec![selection]) - } - - fn paste(&mut self, action: &editor::actions::Paste, cx: &mut ViewContext) { - cx.stop_propagation(); - - let images = if let Some(item) = cx.read_from_clipboard() { - item.into_entries() - .filter_map(|entry| { - if let ClipboardEntry::Image(image) = entry { - Some(image) - } else { - None - } - }) - .collect() - } else { - Vec::new() - }; - - let metadata = if let Some(item) = cx.read_from_clipboard() { - item.entries().first().and_then(|entry| { - if let ClipboardEntry::String(text) = entry { - text.metadata_json::() - } else { - None - } - }) - } else { - None - }; - - if images.is_empty() { - self.editor.update(cx, |editor, cx| { - let paste_position = editor.selections.newest::(cx).head(); - editor.paste(action, cx); - - if let Some(metadata) = metadata { - let buffer = editor.buffer().read(cx).snapshot(cx); - - let mut buffer_rows_to_fold = BTreeSet::new(); - let weak_editor = cx.view().downgrade(); - editor.insert_creases( - metadata.creases.into_iter().map(|metadata| { - let start = buffer.anchor_after( - paste_position + metadata.range_relative_to_selection.start, - ); - let end = buffer.anchor_before( - paste_position + metadata.range_relative_to_selection.end, - ); - - let buffer_row = MultiBufferRow(start.to_point(&buffer).row); - buffer_rows_to_fold.insert(buffer_row); - Crease::inline( - start..end, - FoldPlaceholder { - render: render_fold_icon_button( - weak_editor.clone(), - metadata.crease.icon, - metadata.crease.label.clone(), - ), - ..Default::default() - }, - render_slash_command_output_toggle, - |_, _, _| Empty.into_any(), - ) - .with_metadata(metadata.crease.clone()) - }), - cx, - ); - for buffer_row in buffer_rows_to_fold.into_iter().rev() { - editor.fold_at(&FoldAt { buffer_row }, cx); - } - } - }); - } else { - let mut image_positions = Vec::new(); - self.editor.update(cx, |editor, cx| { - editor.transact(cx, |editor, cx| { - let edits = editor - .selections - .all::(cx) - .into_iter() - .map(|selection| (selection.start..selection.end, "\n")); - editor.edit(edits, cx); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all::(cx) { - image_positions.push(snapshot.anchor_before(selection.end)); - } - }); - }); - - self.context.update(cx, |context, cx| { - for image in images { - let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err() - else { - continue; - }; - let image_id = image.id(); - let image_task = LanguageModelImage::from_image(image, cx).shared(); - - for image_position in image_positions.iter() { - context.insert_content( - Content::Image { - anchor: image_position.text_anchor, - image_id, - image: image_task.clone(), - render_image: render_image.clone(), - }, - cx, - ); - } - } - }); - } - } - - fn update_image_blocks(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let excerpt_id = *buffer.as_singleton().unwrap().0; - let old_blocks = std::mem::take(&mut self.image_blocks); - let new_blocks = self - .context - .read(cx) - .contents(cx) - .filter_map(|content| { - if let Content::Image { - anchor, - render_image, - .. - } = content - { - Some((anchor, render_image)) - } else { - None - } - }) - .filter_map(|(anchor, render_image)| { - const MAX_HEIGHT_IN_LINES: u32 = 8; - let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap(); - let image = render_image.clone(); - anchor.is_valid(&buffer).then(|| BlockProperties { - placement: BlockPlacement::Above(anchor), - height: MAX_HEIGHT_IN_LINES, - style: BlockStyle::Sticky, - render: Arc::new(move |cx| { - let image_size = size_for_image( - &image, - size( - cx.max_width - cx.gutter_dimensions.full_width(), - MAX_HEIGHT_IN_LINES as f32 * cx.line_height, - ), - ); - h_flex() - .pl(cx.gutter_dimensions.full_width()) - .child( - img(image.clone()) - .object_fit(gpui::ObjectFit::ScaleDown) - .w(image_size.width) - .h(image_size.height), - ) - .into_any_element() - }), - priority: 0, - }) - }) - .collect::>(); - - editor.remove_blocks(old_blocks, None, cx); - let ids = editor.insert_blocks(new_blocks, None, cx); - self.image_blocks = HashSet::from_iter(ids); - }); - } - - fn split(&mut self, _: &Split, cx: &mut ViewContext) { - self.context.update(cx, |context, cx| { - let selections = self.editor.read(cx).selections.disjoint_anchors(); - for selection in selections.as_ref() { - let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); - let range = selection - .map(|endpoint| endpoint.to_offset(&buffer)) - .range(); - context.split_message(range, cx); - } - }); - } - - fn save(&mut self, _: &Save, cx: &mut ViewContext) { - self.context.update(cx, |context, cx| { - context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx) - }); - } - - fn title(&self, cx: &AppContext) -> Cow { - self.context - .read(cx) - .summary() - .map(|summary| summary.text.clone()) - .map(Cow::Owned) - .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE)) - } - - fn render_patch_block( - &mut self, - range: Range, - max_width: Pixels, - gutter_width: Pixels, - id: BlockId, - selected: bool, - cx: &mut ViewContext, - ) -> Option { - let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx)); - let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap(); - let excerpt_id = *excerpt_id; - let anchor = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id, range.start) - .unwrap(); - - let theme = cx.theme().clone(); - let patch = self.context.read(cx).patch_for_range(&range, cx)?; - let paths = patch - .paths() - .map(|p| SharedString::from(p.to_string())) - .collect::>(); - - Some( - v_flex() - .id(id) - .bg(theme.colors().editor_background) - .ml(gutter_width) - .pb_1() - .w(max_width - gutter_width) - .rounded_md() - .border_1() - .border_color(theme.colors().border_variant) - .overflow_hidden() - .hover(|style| style.border_color(theme.colors().text_accent)) - .when(selected, |this| { - this.border_color(theme.colors().text_accent) - }) - .cursor(CursorStyle::PointingHand) - .on_click(cx.listener(move |this, _, cx| { - this.editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| { - selections.select_ranges(vec![anchor..anchor]); - }); - }); - this.focus_active_patch(cx); - })) - .child( - div() - .px_2() - .py_1() - .overflow_hidden() - .text_ellipsis() - .border_b_1() - .border_color(theme.colors().border_variant) - .bg(theme.colors().element_background) - .child( - Label::new(patch.title.clone()) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .children(paths.into_iter().map(|path| { - h_flex() - .px_2() - .pt_1() - .gap_1p5() - .child(Icon::new(IconName::File).size(IconSize::Small)) - .child(Label::new(path).size(LabelSize::Small)) - })) - .when(patch.status == AssistantPatchStatus::Pending, |div| { - div.child( - h_flex() - .pt_1() - .px_2() - .gap_1() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::XSmall) - .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ), - ) - .child( - Label::new("Generating…") - .color(Color::Muted) - .size(LabelSize::Small) - .with_animation( - "pulsating-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.alpha(delta), - ), - ), - ) - }) - .into_any(), - ) - } - - fn render_notice(&self, cx: &mut ViewContext) -> Option { - use feature_flags::FeatureFlagAppExt; - let nudge = self.assistant_panel.upgrade().map(|assistant_panel| { - assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::() - }); - - if nudge.map_or(false, |value| value) { - Some( - h_flex() - .p_3() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .justify_between() - .child( - h_flex() - .gap_3() - .child(Icon::new(IconName::ZedAssistant).color(Color::Accent)) - .child(Label::new("Zed AI is here! Get started by signing in →")), - ) - .child( - Button::new("sign-in", "Sign in") - .size(ButtonSize::Compact) - .style(ButtonStyle::Filled) - .on_click(cx.listener(|this, _event, cx| { - let client = this - .workspace - .update(cx, |workspace, _| workspace.client().clone()) - .log_err(); - - if let Some(client) = client { - cx.spawn(|this, mut cx| async move { - client.authenticate_and_connect(true, &mut cx).await?; - this.update(&mut cx, |_, cx| cx.notify()) - }) - .detach_and_log_err(cx) - } - })), - ) - .into_any_element(), - ) - } else if let Some(configuration_error) = configuration_error(cx) { - let label = match configuration_error { - ConfigurationError::NoProvider => "No LLM provider selected.", - ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.", - }; - Some( - h_flex() - .px_3() - .py_2() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .justify_between() - .child( - h_flex() - .gap_3() - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .child(Label::new(label)), - ) - .child( - Button::new("open-configuration", "Configure Providers") - .size(ButtonSize::Compact) - .icon(Some(IconName::SlidersVertical)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .style(ButtonStyle::Filled) - .on_click({ - let focus_handle = self.focus_handle(cx).clone(); - move |_event, cx| { - focus_handle.dispatch_action(&ShowConfiguration, cx); - } - }), - ) - .into_any_element(), - ) - } else { - None - } - } - - fn render_send_button(&self, cx: &mut ViewContext) -> impl IntoElement { - let focus_handle = self.focus_handle(cx).clone(); - - let (style, tooltip) = match token_state(&self.context, cx) { - Some(TokenState::NoTokensLeft { .. }) => ( - ButtonStyle::Tinted(TintColor::Error), - Some(Tooltip::text("Token limit reached", cx)), - ), - Some(TokenState::HasMoreTokens { - over_warn_threshold, - .. - }) => { - let (style, tooltip) = if over_warn_threshold { - ( - ButtonStyle::Tinted(TintColor::Warning), - Some(Tooltip::text("Token limit is close to exhaustion", cx)), - ) - } else { - (ButtonStyle::Filled, None) - }; - (style, tooltip) - } - None => (ButtonStyle::Filled, None), - }; - - let provider = LanguageModelRegistry::read_global(cx).active_provider(); - - let has_configuration_error = configuration_error(cx).is_some(); - let needs_to_accept_terms = self.show_accept_terms - && provider - .as_ref() - .map_or(false, |provider| provider.must_accept_terms(cx)); - let disabled = has_configuration_error || needs_to_accept_terms; - - ButtonLike::new("send_button") - .disabled(disabled) - .style(style) - .when_some(tooltip, |button, tooltip| { - button.tooltip(move |_| tooltip.clone()) - }) - .layer(ElevationIndex::ModalSurface) - .child(Label::new( - if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) { - "Chat" - } else { - "Send" - }, - )) - .children( - KeyBinding::for_action_in(&Assist, &focus_handle, cx) - .map(|binding| binding.into_any_element()), - ) - .on_click(move |_event, cx| { - focus_handle.dispatch_action(&Assist, cx); - }) - } - - fn render_edit_button(&self, cx: &mut ViewContext) -> impl IntoElement { - let focus_handle = self.focus_handle(cx).clone(); - - let (style, tooltip) = match token_state(&self.context, cx) { - Some(TokenState::NoTokensLeft { .. }) => ( - ButtonStyle::Tinted(TintColor::Error), - Some(Tooltip::text("Token limit reached", cx)), - ), - Some(TokenState::HasMoreTokens { - over_warn_threshold, - .. - }) => { - let (style, tooltip) = if over_warn_threshold { - ( - ButtonStyle::Tinted(TintColor::Warning), - Some(Tooltip::text("Token limit is close to exhaustion", cx)), - ) - } else { - (ButtonStyle::Filled, None) - }; - (style, tooltip) - } - None => (ButtonStyle::Filled, None), - }; - - let provider = LanguageModelRegistry::read_global(cx).active_provider(); - - let has_configuration_error = configuration_error(cx).is_some(); - let needs_to_accept_terms = self.show_accept_terms - && provider - .as_ref() - .map_or(false, |provider| provider.must_accept_terms(cx)); - let disabled = has_configuration_error || needs_to_accept_terms; - - ButtonLike::new("edit_button") - .disabled(disabled) - .style(style) - .when_some(tooltip, |button, tooltip| { - button.tooltip(move |_| tooltip.clone()) - }) - .layer(ElevationIndex::ModalSurface) - .child(Label::new("Suggest Edits")) - .children( - KeyBinding::for_action_in(&Edit, &focus_handle, cx) - .map(|binding| binding.into_any_element()), - ) - .on_click(move |_event, cx| { - focus_handle.dispatch_action(&Edit, cx); - }) - } - - fn render_inject_context_menu(&self, cx: &mut ViewContext) -> impl IntoElement { - slash_command_picker::SlashCommandSelector::new( - self.slash_commands.clone(), - cx.view().downgrade(), - Button::new("trigger", "Add Context") - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)), - ) - } - - fn render_last_error(&self, cx: &mut ViewContext) -> Option { - let last_error = self.last_error.as_ref()?; - - Some( - div() - .absolute() - .right_3() - .bottom_12() - .max_w_96() - .py_2() - .px_3() - .elevation_2(cx) - .occlude() - .child(match last_error { - AssistError::FileRequired => self.render_file_required_error(cx), - AssistError::PaymentRequired => self.render_payment_required_error(cx), - AssistError::MaxMonthlySpendReached => { - self.render_max_monthly_spend_reached_error(cx) - } - AssistError::Message(error_message) => { - self.render_assist_error(error_message, cx) - } - }) - .into_any(), - ) - } - - fn render_file_required_error(&self, cx: &mut ViewContext) -> AnyElement { - v_flex() - .gap_0p5() - .child( - h_flex() - .gap_1p5() - .items_center() - .child(Icon::new(IconName::Warning).color(Color::Warning)) - .child( - Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM), - ), - ) - .child( - div() - .id("error-message") - .max_h_24() - .overflow_y_scroll() - .child(Label::new( - "To include files, type /file or /tab in your prompt.", - )), - ) - .child( - h_flex() - .justify_end() - .mt_1() - .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( - |this, _, cx| { - this.last_error = None; - cx.notify(); - }, - ))), - ) - .into_any() - } - - fn render_payment_required_error(&self, cx: &mut ViewContext) -> AnyElement { - const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used."; - - v_flex() - .gap_0p5() - .child( - h_flex() - .gap_1p5() - .items_center() - .child(Icon::new(IconName::XCircle).color(Color::Error)) - .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)), - ) - .child( - div() - .id("error-message") - .max_h_24() - .overflow_y_scroll() - .child(Label::new(ERROR_MESSAGE)), - ) - .child( - h_flex() - .justify_end() - .mt_1() - .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( - |this, _, cx| { - this.last_error = None; - cx.open_url(&zed_urls::account_url(cx)); - cx.notify(); - }, - ))) - .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( - |this, _, cx| { - this.last_error = None; - cx.notify(); - }, - ))), - ) - .into_any() - } - - fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext) -> AnyElement { - const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs."; - - v_flex() - .gap_0p5() - .child( - h_flex() - .gap_1p5() - .items_center() - .child(Icon::new(IconName::XCircle).color(Color::Error)) - .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)), - ) - .child( - div() - .id("error-message") - .max_h_24() - .overflow_y_scroll() - .child(Label::new(ERROR_MESSAGE)), - ) - .child( - h_flex() - .justify_end() - .mt_1() - .child( - Button::new("subscribe", "Update Monthly Spend Limit").on_click( - cx.listener(|this, _, cx| { - this.last_error = None; - cx.open_url(&zed_urls::account_url(cx)); - cx.notify(); - }), - ), - ) - .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( - |this, _, cx| { - this.last_error = None; - cx.notify(); - }, - ))), - ) - .into_any() - } - - fn render_assist_error( - &self, - error_message: &SharedString, - cx: &mut ViewContext, - ) -> AnyElement { - v_flex() - .gap_0p5() - .child( - h_flex() - .gap_1p5() - .items_center() - .child(Icon::new(IconName::XCircle).color(Color::Error)) - .child( - Label::new("Error interacting with language model") - .weight(FontWeight::MEDIUM), - ), - ) - .child( - div() - .id("error-message") - .max_h_32() - .overflow_y_scroll() - .child(Label::new(error_message.clone())), - ) - .child( - h_flex() - .justify_end() - .mt_1() - .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( - |this, _, cx| { - this.last_error = None; - cx.notify(); - }, - ))), - ) - .into_any() - } -} - -/// Returns the contents of the *outermost* fenced code block that contains the given offset. -fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option> { - const CODE_BLOCK_NODE: &'static str = "fenced_code_block"; - const CODE_BLOCK_CONTENT: &'static str = "code_fence_content"; - - let layer = snapshot.syntax_layers().next()?; - - let root_node = layer.node(); - let mut cursor = root_node.walk(); - - // Go to the first child for the given offset - while cursor.goto_first_child_for_byte(offset).is_some() { - // If we're at the end of the node, go to the next one. - // Example: if you have a fenced-code-block, and you're on the start of the line - // right after the closing ```, you want to skip the fenced-code-block and - // go to the next sibling. - if cursor.node().end_byte() == offset { - cursor.goto_next_sibling(); - } - - if cursor.node().start_byte() > offset { - break; - } - - // We found the fenced code block. - if cursor.node().kind() == CODE_BLOCK_NODE { - // Now we need to find the child node that contains the code. - cursor.goto_first_child(); - loop { - if cursor.node().kind() == CODE_BLOCK_CONTENT { - return Some(cursor.node().byte_range()); - } - if !cursor.goto_next_sibling() { - break; - } - } - } - } - - None -} - -fn render_fold_icon_button( - editor: WeakView, - icon: IconName, - label: SharedString, -) -> Arc, &mut WindowContext) -> AnyElement> { - Arc::new(move |fold_id, fold_range, _cx| { - let editor = editor.clone(); - ButtonLike::new(fold_id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(icon)) - .child(Label::new(label.clone()).single_line()) - .on_click(move |_, cx| { - editor - .update(cx, |editor, cx| { - let buffer_start = fold_range - .start - .to_point(&editor.buffer().read(cx).read(cx)); - let buffer_row = MultiBufferRow(buffer_start.row); - editor.unfold_at(&UnfoldAt { buffer_row }, cx); - }) - .ok(); - }) - .into_any_element() - }) -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct CopyMetadata { - creases: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct SelectedCreaseMetadata { - range_relative_to_selection: Range, - crease: CreaseMetadata, -} - -impl EventEmitter for ContextEditor {} -impl EventEmitter for ContextEditor {} - -impl Render for ContextEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let provider = LanguageModelRegistry::read_global(cx).active_provider(); - let accept_terms = if self.show_accept_terms { - provider - .as_ref() - .and_then(|provider| provider.render_accept_terms(cx)) - } else { - None - }; - - v_flex() - .key_context("ContextEditor") - .capture_action(cx.listener(ContextEditor::cancel)) - .capture_action(cx.listener(ContextEditor::save)) - .capture_action(cx.listener(ContextEditor::copy)) - .capture_action(cx.listener(ContextEditor::cut)) - .capture_action(cx.listener(ContextEditor::paste)) - .capture_action(cx.listener(ContextEditor::cycle_message_role)) - .capture_action(cx.listener(ContextEditor::confirm_command)) - .on_action(cx.listener(ContextEditor::edit)) - .on_action(cx.listener(ContextEditor::assist)) - .on_action(cx.listener(ContextEditor::split)) - .size_full() - .children(self.render_notice(cx)) - .child( - div() - .flex_grow() - .bg(cx.theme().colors().editor_background) - .child(self.editor.clone()), - ) - .when_some(accept_terms, |this, element| { - this.child( - div() - .absolute() - .right_3() - .bottom_12() - .max_w_96() - .py_2() - .px_3() - .elevation_2(cx) - .bg(cx.theme().colors().surface_background) - .occlude() - .child(element), - ) - }) - .children(self.render_last_error(cx)) - .child( - h_flex().w_full().relative().child( - h_flex() - .p_2() - .w_full() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .child(h_flex().gap_1().child(self.render_inject_context_menu(cx))) - .child( - h_flex() - .w_full() - .justify_end() - .when( - AssistantSettings::get_global(cx).are_live_diffs_enabled(cx), - |buttons| { - buttons - .items_center() - .gap_1p5() - .child(self.render_edit_button(cx)) - .child( - Label::new("or") - .size(LabelSize::Small) - .color(Color::Muted), - ) - }, - ) - .child(self.render_send_button(cx)), - ), - ), - ) - } -} - -impl FocusableView for ContextEditor { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.editor.focus_handle(cx) - } -} - -impl Item for ContextEditor { - type Event = editor::EditorEvent; - - fn tab_content_text(&self, cx: &WindowContext) -> Option { - Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) { - match event { - EditorEvent::Edited { .. } => { - f(item::ItemEvent::Edit); - } - EditorEvent::TitleChanged => { - f(item::ItemEvent::UpdateTab); - } - _ => {} - } - } - - fn tab_tooltip_text(&self, cx: &AppContext) -> Option { - Some(self.title(cx).to_string().into()) - } - - fn as_searchable(&self, handle: &View) -> Option> { - Some(Box::new(handle.clone())) - } - - fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - Item::set_nav_history(editor, nav_history, cx) - }) - } - - fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { - self.editor - .update(cx, |editor, cx| Item::navigate(editor, data, cx)) - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, Item::deactivated) - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a View, - _: &'a AppContext, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.to_any()) - } else if type_id == TypeId::of::() { - Some(self.editor.to_any()) - } else { - None - } - } - - fn include_in_nav_history() -> bool { - false - } -} - -impl SearchableItem for ContextEditor { - type Match = ::Match; - - fn clear_matches(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - editor.clear_matches(cx); - }); - } - - fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| editor.update_matches(matches, cx)); - } - - fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - self.editor - .update(cx, |editor, cx| editor.query_suggestion(cx)) - } - - fn activate_match( - &mut self, - index: usize, - matches: &[Self::Match], - cx: &mut ViewContext, - ) { - self.editor.update(cx, |editor, cx| { - editor.activate_match(index, matches, cx); - }); - } - - fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| editor.select_matches(matches, cx)); - } - - fn replace( - &mut self, - identifier: &Self::Match, - query: &project::search::SearchQuery, - cx: &mut ViewContext, - ) { - self.editor - .update(cx, |editor, cx| editor.replace(identifier, query, cx)); - } - - fn find_matches( - &mut self, - query: Arc, - cx: &mut ViewContext, - ) -> Task> { - self.editor - .update(cx, |editor, cx| editor.find_matches(query, cx)) - } - - fn active_match_index( - &mut self, - matches: &[Self::Match], - cx: &mut ViewContext, - ) -> Option { - self.editor - .update(cx, |editor, cx| editor.active_match_index(matches, cx)) - } -} - -impl FollowableItem for ContextEditor { - fn remote_id(&self) -> Option { - self.remote_id - } - - fn to_state_proto(&self, cx: &WindowContext) -> Option { - let context = self.context.read(cx); - Some(proto::view::Variant::ContextEditor( - proto::view::ContextEditor { - context_id: context.id().to_proto(), - editor: if let Some(proto::view::Variant::Editor(proto)) = - self.editor.read(cx).to_state_proto(cx) - { - Some(proto) - } else { - None - }, - }, - )) - } - - fn from_state_proto( - workspace: View, - id: workspace::ViewId, - state: &mut Option, - cx: &mut WindowContext, - ) -> Option>>> { - let proto::view::Variant::ContextEditor(_) = state.as_ref()? else { - return None; - }; - let Some(proto::view::Variant::ContextEditor(state)) = state.take() else { - unreachable!() - }; - - let context_id = ContextId::from_proto(state.context_id); - let editor_state = state.editor?; - - let (project, panel) = workspace.update(cx, |workspace, cx| { - Some(( - workspace.project().clone(), - workspace.panel::(cx)?, - )) - })?; - - let context_editor = - panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx)); - - Some(cx.spawn(|mut cx| async move { - let context_editor = context_editor.await?; - context_editor - .update(&mut cx, |context_editor, cx| { - context_editor.remote_id = Some(id); - context_editor.editor.update(cx, |editor, cx| { - editor.apply_update_proto( - &project, - proto::update_view::Variant::Editor(proto::update_view::Editor { - selections: editor_state.selections, - pending_selection: editor_state.pending_selection, - scroll_top_anchor: editor_state.scroll_top_anchor, - scroll_x: editor_state.scroll_y, - scroll_y: editor_state.scroll_y, - ..Default::default() - }), - cx, - ) - }) - })? - .await?; - Ok(context_editor) - })) - } - - fn to_follow_event(event: &Self::Event) -> Option { - Editor::to_follow_event(event) - } - - fn add_event_to_update_proto( - &self, - event: &Self::Event, - update: &mut Option, - cx: &WindowContext, - ) -> bool { - self.editor - .read(cx) - .add_event_to_update_proto(event, update, cx) - } - - fn apply_update_proto( - &mut self, - project: &Model, - message: proto::update_view::Variant, - cx: &mut ViewContext, - ) -> Task> { - self.editor.update(cx, |editor, cx| { - editor.apply_update_proto(project, message, cx) - }) - } - - fn is_project_item(&self, _cx: &WindowContext) -> bool { - true - } - - fn set_leader_peer_id( - &mut self, - leader_peer_id: Option, - cx: &mut ViewContext, - ) { - self.editor.update(cx, |editor, cx| { - editor.set_leader_peer_id(leader_peer_id, cx) - }) - } - - fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option { - if existing.context.read(cx).id() == self.context.read(cx).id() { - Some(item::Dedup::KeepExisting) - } else { - None - } - } -} - -pub struct ContextEditorToolbarItem { - active_context_editor: Option>, - model_summary_editor: View, - language_model_selector: View, - language_model_selector_menu_handle: PopoverMenuHandle, -} - -impl ContextEditorToolbarItem { - pub fn new( - workspace: &Workspace, - model_selector_menu_handle: PopoverMenuHandle, - model_summary_editor: View, - cx: &mut ViewContext, - ) -> Self { - Self { - active_context_editor: None, - model_summary_editor, - language_model_selector: cx.new_view(|cx| { - let fs = workspace.app_state().fs.clone(); - LanguageModelSelector::new( - move |model, cx| { - update_settings_file::( - fs.clone(), - cx, - move |settings, _| settings.set_model(model.clone()), - ); - }, - cx, - ) - }), - language_model_selector_menu_handle: model_selector_menu_handle, - } - } - - fn render_remaining_tokens(&self, cx: &mut ViewContext) -> Option { - let context = &self - .active_context_editor - .as_ref()? - .upgrade()? - .read(cx) - .context; - let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? { - TokenState::NoTokensLeft { - max_token_count, - token_count, - } => (Color::Error, token_count, max_token_count), - TokenState::HasMoreTokens { - max_token_count, - token_count, - over_warn_threshold, - } => { - let color = if over_warn_threshold { - Color::Warning - } else { - Color::Muted - }; - (color, token_count, max_token_count) - } - }; - Some( - h_flex() - .gap_0p5() - .child( - Label::new(humanize_token_count(token_count)) - .size(LabelSize::Small) - .color(token_count_color), - ) - .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) - .child( - Label::new(humanize_token_count(max_token_count)) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - } -} - -impl Render for ContextEditorToolbarItem { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let left_side = h_flex() - .group("chat-title-group") - .gap_1() - .items_center() - .flex_grow() - .child( - div() - .w_full() - .when(self.active_context_editor.is_some(), |left_side| { - left_side.child(self.model_summary_editor.clone()) - }), - ) - .child( - div().visible_on_hover("chat-title-group").child( - IconButton::new("regenerate-context", IconName::RefreshTitle) - .shape(ui::IconButtonShape::Square) - .tooltip(|cx| Tooltip::text("Regenerate Title", cx)) - .on_click(cx.listener(move |_, _, cx| { - cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary) - })), - ), - ); - let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); - let active_model = LanguageModelRegistry::read_global(cx).active_model(); - let right_side = h_flex() - .gap_2() - // TODO display this in a nicer way, once we have a design for it. - // .children({ - // let project = self - // .workspace - // .upgrade() - // .map(|workspace| workspace.read(cx).project().downgrade()); - // - // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| { - // project.and_then(|project| db.remaining_summaries(&project, cx)) - // }); - // scan_items_remaining - // .map(|remaining_items| format!("Files to scan: {}", remaining_items)) - // }) - .child( - LanguageModelSelectorPopoverMenu::new( - self.language_model_selector.clone(), - ButtonLike::new("active-model") - .style(ButtonStyle::Subtle) - .child( - h_flex() - .w_full() - .gap_0p5() - .child( - div() - .overflow_x_hidden() - .flex_grow() - .whitespace_nowrap() - .child(match (active_provider, active_model) { - (Some(provider), Some(model)) => h_flex() - .gap_1() - .child( - Icon::new( - model - .icon() - .unwrap_or_else(|| provider.icon()), - ) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child( - Label::new(model.name().0) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element(), - _ => Label::new("No model selected") - .size(LabelSize::Small) - .color(Color::Muted) - .into_any_element(), - }), - ) - .child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ) - .tooltip(move |cx| { - Tooltip::for_action("Change Model", &ToggleModelSelector, cx) - }), - ) - .with_handle(self.language_model_selector_menu_handle.clone()), - ) - .children(self.render_remaining_tokens(cx)); - - h_flex() - .px_0p5() - .size_full() - .gap_2() - .justify_between() - .child(left_side) - .child(right_side) - } -} - -impl ToolbarItemView for ContextEditorToolbarItem { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> ToolbarItemLocation { - self.active_context_editor = active_pane_item - .and_then(|item| item.act_as::(cx)) - .map(|editor| editor.downgrade()); - cx.notify(); - if self.active_context_editor.is_none() { - ToolbarItemLocation::Hidden - } else { - ToolbarItemLocation::PrimaryRight - } - } - - fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext) { - cx.notify(); - } -} - -impl EventEmitter for ContextEditorToolbarItem {} - -enum ContextEditorToolbarItemEvent { - RegenerateSummary, -} -impl EventEmitter for ContextEditorToolbarItem {} - -pub struct ContextHistory { - picker: View>, - _subscriptions: Vec, - assistant_panel: WeakView, -} - -impl ContextHistory { - fn new( - project: Model, - context_store: Model, - assistant_panel: WeakView, - cx: &mut ViewContext, - ) -> Self { - let picker = cx.new_view(|cx| { - Picker::uniform_list( - SavedContextPickerDelegate::new(project, context_store.clone()), - cx, - ) - .modal(false) - .max_height(None) - }); - - let _subscriptions = vec![ - cx.observe(&context_store, |this, _, cx| { - this.picker.update(cx, |picker, cx| picker.refresh(cx)); - }), - cx.subscribe(&picker, Self::handle_picker_event), - ]; - - Self { - picker, - _subscriptions, - assistant_panel, - } - } - - fn handle_picker_event( - &mut self, - _: View>, - event: &SavedContextPickerEvent, - cx: &mut ViewContext, - ) { - let SavedContextPickerEvent::Confirmed(context) = event; - self.assistant_panel - .update(cx, |assistant_panel, cx| match context { - ContextMetadata::Remote(metadata) => { - assistant_panel - .open_remote_context(metadata.id.clone(), cx) - .detach_and_log_err(cx); - } - ContextMetadata::Saved(metadata) => { - assistant_panel - .open_saved_context(metadata.path.clone(), cx) - .detach_and_log_err(cx); - } - }) - .ok(); - } -} +} #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum WorkflowAssistStatus { @@ -4676,619 +1441,3 @@ pub enum WorkflowAssistStatus { Done, Idle, } - -impl Render for ContextHistory { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { - div().size_full().child(self.picker.clone()) - } -} - -impl FocusableView for ContextHistory { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter<()> for ContextHistory {} - -impl Item for ContextHistory { - type Event = (); - - fn tab_content_text(&self, _cx: &WindowContext) -> Option { - Some("History".into()) - } -} - -pub struct ConfigurationView { - focus_handle: FocusHandle, - configuration_views: HashMap, - _registry_subscription: Subscription, -} - -impl ConfigurationView { - fn new(cx: &mut ViewContext) -> Self { - let focus_handle = cx.focus_handle(); - - let registry_subscription = cx.subscribe( - &LanguageModelRegistry::global(cx), - |this, _, event: &language_model::Event, cx| match event { - language_model::Event::AddedProvider(provider_id) => { - let provider = LanguageModelRegistry::read_global(cx).provider(provider_id); - if let Some(provider) = provider { - this.add_configuration_view(&provider, cx); - } - } - language_model::Event::RemovedProvider(provider_id) => { - this.remove_configuration_view(provider_id); - } - _ => {} - }, - ); - - let mut this = Self { - focus_handle, - configuration_views: HashMap::default(), - _registry_subscription: registry_subscription, - }; - this.build_configuration_views(cx); - this - } - - fn build_configuration_views(&mut self, cx: &mut ViewContext) { - let providers = LanguageModelRegistry::read_global(cx).providers(); - for provider in providers { - self.add_configuration_view(&provider, cx); - } - } - - fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) { - self.configuration_views.remove(provider_id); - } - - fn add_configuration_view( - &mut self, - provider: &Arc, - cx: &mut ViewContext, - ) { - let configuration_view = provider.configuration_view(cx); - self.configuration_views - .insert(provider.id(), configuration_view); - } - - fn render_provider_view( - &mut self, - provider: &Arc, - cx: &mut ViewContext, - ) -> Div { - let provider_id = provider.id().0.clone(); - let provider_name = provider.name().0.clone(); - let configuration_view = self.configuration_views.get(&provider.id()).cloned(); - - let open_new_context = cx.listener({ - let provider = provider.clone(); - move |_, _, cx| { - cx.emit(ConfigurationViewEvent::NewProviderContextEditor( - provider.clone(), - )) - } - }); - - v_flex() - .gap_2() - .child( - h_flex() - .justify_between() - .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small)) - .when(provider.is_authenticated(cx), move |this| { - this.child( - h_flex().justify_end().child( - Button::new( - SharedString::from(format!("new-context-{provider_id}")), - "Open New Chat", - ) - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .on_click(open_new_context), - ), - ) - }), - ) - .child( - div() - .p(DynamicSpacing::Base08.rems(cx)) - .bg(cx.theme().colors().surface_background) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() - .when(configuration_view.is_none(), |this| { - this.child(div().child(Label::new(format!( - "No configuration view for {}", - provider_name - )))) - }) - .when_some(configuration_view, |this, configuration_view| { - this.child(configuration_view) - }), - ) - } -} - -impl Render for ConfigurationView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let providers = LanguageModelRegistry::read_global(cx).providers(); - let provider_views = providers - .into_iter() - .map(|provider| self.render_provider_view(&provider, cx)) - .collect::>(); - - let mut element = v_flex() - .id("assistant-configuration-view") - .track_focus(&self.focus_handle(cx)) - .bg(cx.theme().colors().editor_background) - .size_full() - .overflow_y_scroll() - .child( - v_flex() - .p(DynamicSpacing::Base16.rems(cx)) - .border_b_1() - .border_color(cx.theme().colors().border) - .gap_1() - .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium)) - .child( - Label::new( - "At least one LLM provider must be configured to use the Assistant.", - ) - .color(Color::Muted), - ), - ) - .child( - v_flex() - .p(DynamicSpacing::Base16.rems(cx)) - .mt_1() - .gap_6() - .flex_1() - .children(provider_views), - ) - .into_any(); - - // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround - // because we couldn't the element to take up the size of the parent. - canvas( - move |bounds, cx| { - element.prepaint_as_root(bounds.origin, bounds.size.into(), cx); - element - }, - |_, mut element, cx| { - element.paint(cx); - }, - ) - .flex_1() - .w_full() - } -} - -pub enum ConfigurationViewEvent { - NewProviderContextEditor(Arc), -} - -impl EventEmitter for ConfigurationView {} - -impl FocusableView for ConfigurationView { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for ConfigurationView { - type Event = ConfigurationViewEvent; - - fn tab_content_text(&self, _cx: &WindowContext) -> Option { - Some("Configuration".into()) - } -} - -type ToggleFold = Arc; - -fn render_slash_command_output_toggle( - row: MultiBufferRow, - is_folded: bool, - fold: ToggleFold, - _cx: &mut WindowContext, -) -> AnyElement { - Disclosure::new( - ("slash-command-output-fold-indicator", row.0 as u64), - !is_folded, - ) - .toggle_state(is_folded) - .on_click(move |_e, cx| fold(!is_folded, cx)) - .into_any_element() -} - -fn fold_toggle( - name: &'static str, -) -> impl Fn( - MultiBufferRow, - bool, - Arc, - &mut WindowContext, -) -> AnyElement { - move |row, is_folded, fold, _cx| { - Disclosure::new((name, row.0 as u64), !is_folded) - .toggle_state(is_folded) - .on_click(move |_e, cx| fold(!is_folded, cx)) - .into_any_element() - } -} - -fn quote_selection_fold_placeholder(title: String, editor: WeakView) -> FoldPlaceholder { - FoldPlaceholder { - render: Arc::new({ - move |fold_id, fold_range, _cx| { - let editor = editor.clone(); - ButtonLike::new(fold_id) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ElevatedSurface) - .child(Icon::new(IconName::TextSnippet)) - .child(Label::new(title.clone()).single_line()) - .on_click(move |_, cx| { - editor - .update(cx, |editor, cx| { - let buffer_start = fold_range - .start - .to_point(&editor.buffer().read(cx).read(cx)); - let buffer_row = MultiBufferRow(buffer_start.row); - editor.unfold_at(&UnfoldAt { buffer_row }, cx); - }) - .ok(); - }) - .into_any_element() - } - }), - merge_adjacent: false, - ..Default::default() - } -} - -fn render_quote_selection_output_toggle( - row: MultiBufferRow, - is_folded: bool, - fold: ToggleFold, - _cx: &mut WindowContext, -) -> AnyElement { - Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded) - .toggle_state(is_folded) - .on_click(move |_e, cx| fold(!is_folded, cx)) - .into_any_element() -} - -fn render_pending_slash_command_gutter_decoration( - row: MultiBufferRow, - status: &PendingSlashCommandStatus, - confirm_command: Arc, -) -> AnyElement { - let mut icon = IconButton::new( - ("slash-command-gutter-decoration", row.0), - ui::IconName::TriangleRight, - ) - .on_click(move |_e, cx| confirm_command(cx)) - .icon_size(ui::IconSize::Small) - .size(ui::ButtonSize::None); - - match status { - PendingSlashCommandStatus::Idle => { - icon = icon.icon_color(Color::Muted); - } - PendingSlashCommandStatus::Running { .. } => { - icon = icon.toggle_state(true); - } - PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error), - } - - icon.into_any_element() -} - -fn render_docs_slash_command_trailer( - row: MultiBufferRow, - command: ParsedSlashCommand, - cx: &mut WindowContext, -) -> AnyElement { - if command.arguments.is_empty() { - return Empty.into_any(); - } - let args = DocsSlashCommandArgs::parse(&command.arguments); - - let Some(store) = args - .provider() - .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok()) - else { - return Empty.into_any(); - }; - - let Some(package) = args.package() else { - return Empty.into_any(); - }; - - let mut children = Vec::new(); - - if store.is_indexing(&package) { - children.push( - div() - .id(("crates-being-indexed", row.0)) - .child(Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - )) - .tooltip({ - let package = package.clone(); - move |cx| Tooltip::text(format!("Indexing {package}…"), cx) - }) - .into_any_element(), - ); - } - - if let Some(latest_error) = store.latest_error_for_package(&package) { - children.push( - div() - .id(("latest-error", row.0)) - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .tooltip(move |cx| Tooltip::text(format!("Failed to index: {latest_error}"), cx)) - .into_any_element(), - ) - } - - let is_indexing = store.is_indexing(&package); - let latest_error = store.latest_error_for_package(&package); - - if !is_indexing && latest_error.is_none() { - return Empty.into_any(); - } - - h_flex().gap_2().children(children).into_any_element() -} - -fn make_lsp_adapter_delegate( - project: &Model, - cx: &mut AppContext, -) -> Result>> { - project.update(cx, |project, cx| { - // TODO: Find the right worktree. - let Some(worktree) = project.worktrees(cx).next() else { - return Ok(None::>); - }; - let http_client = project.client().http_client().clone(); - project.lsp_store().update(cx, |_, cx| { - Ok(Some(LocalLspAdapterDelegate::new( - project.languages().clone(), - project.environment(), - cx.weak_model(), - &worktree, - http_client, - project.fs().clone(), - cx, - ) as Arc)) - }) - }) -} - -enum PendingSlashCommand {} - -fn invoked_slash_command_fold_placeholder( - command_id: InvokedSlashCommandId, - context: WeakModel, -) -> FoldPlaceholder { - FoldPlaceholder { - constrain_width: false, - merge_adjacent: false, - render: Arc::new(move |fold_id, _, cx| { - let Some(context) = context.upgrade() else { - return Empty.into_any(); - }; - - let Some(command) = context.read(cx).invoked_slash_command(&command_id) else { - return Empty.into_any(); - }; - - h_flex() - .id(fold_id) - .px_1() - .ml_6() - .gap_2() - .bg(cx.theme().colors().surface_background) - .rounded_md() - .child(Label::new(format!("/{}", command.name.clone()))) - .map(|parent| match &command.status { - InvokedSlashCommandStatus::Running(_) => { - parent.child(Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - )) - } - InvokedSlashCommandStatus::Error(message) => parent.child( - Label::new(format!("error: {message}")) - .single_line() - .color(Color::Error), - ), - InvokedSlashCommandStatus::Finished => parent, - }) - .into_any_element() - }), - type_tag: Some(TypeId::of::()), - } -} - -enum TokenState { - NoTokensLeft { - max_token_count: usize, - token_count: usize, - }, - HasMoreTokens { - max_token_count: usize, - token_count: usize, - over_warn_threshold: bool, - }, -} - -fn token_state(context: &Model, cx: &AppContext) -> Option { - const WARNING_TOKEN_THRESHOLD: f32 = 0.8; - - let model = LanguageModelRegistry::read_global(cx).active_model()?; - let token_count = context.read(cx).token_count()?; - let max_token_count = model.max_token_count(); - - let remaining_tokens = max_token_count as isize - token_count as isize; - let token_state = if remaining_tokens <= 0 { - TokenState::NoTokensLeft { - max_token_count, - token_count, - } - } else { - let over_warn_threshold = - token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD; - TokenState::HasMoreTokens { - max_token_count, - token_count, - over_warn_threshold, - } - }; - Some(token_state) -} - -fn size_for_image(data: &RenderImage, max_size: Size) -> Size { - let image_size = data - .size(0) - .map(|dimension| Pixels::from(u32::from(dimension))); - let image_ratio = image_size.width / image_size.height; - let bounds_ratio = max_size.width / max_size.height; - - if image_size.width > max_size.width || image_size.height > max_size.height { - if bounds_ratio > image_ratio { - size( - image_size.width * (max_size.height / image_size.height), - max_size.height, - ) - } else { - size( - max_size.width, - image_size.height * (max_size.width / image_size.width), - ) - } - } else { - size(image_size.width, image_size.height) - } -} - -enum ConfigurationError { - NoProvider, - ProviderNotAuthenticated, -} - -fn configuration_error(cx: &AppContext) -> Option { - let provider = LanguageModelRegistry::read_global(cx).active_provider(); - let is_authenticated = provider - .as_ref() - .map_or(false, |provider| provider.is_authenticated(cx)); - - if provider.is_some() && is_authenticated { - return None; - } - - if provider.is_none() { - return Some(ConfigurationError::NoProvider); - } - - if !is_authenticated { - return Some(ConfigurationError::ProviderNotAuthenticated); - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{AppContext, Context}; - use language::Buffer; - use unindent::Unindent; - - #[gpui::test] - fn test_find_code_blocks(cx: &mut AppContext) { - let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into()); - - let buffer = cx.new_model(|cx| { - let text = r#" - line 0 - line 1 - ```rust - fn main() {} - ``` - line 5 - line 6 - line 7 - ```go - func main() {} - ``` - line 11 - ``` - this is plain text code block - ``` - - ```go - func another() {} - ``` - line 19 - "# - .unindent(); - let mut buffer = Buffer::local(text, cx); - buffer.set_language(Some(markdown.clone()), cx); - buffer - }); - let snapshot = buffer.read(cx).snapshot(); - - let code_blocks = vec![ - Point::new(3, 0)..Point::new(4, 0), - Point::new(9, 0)..Point::new(10, 0), - Point::new(13, 0)..Point::new(14, 0), - Point::new(17, 0)..Point::new(18, 0), - ] - .into_iter() - .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end)) - .collect::>(); - - let expected_results = vec![ - (0, None), - (1, None), - (2, Some(code_blocks[0].clone())), - (3, Some(code_blocks[0].clone())), - (4, Some(code_blocks[0].clone())), - (5, None), - (6, None), - (7, None), - (8, Some(code_blocks[1].clone())), - (9, Some(code_blocks[1].clone())), - (10, Some(code_blocks[1].clone())), - (11, None), - (12, Some(code_blocks[2].clone())), - (13, Some(code_blocks[2].clone())), - (14, Some(code_blocks[2].clone())), - (15, None), - (16, Some(code_blocks[3].clone())), - (17, Some(code_blocks[3].clone())), - (18, Some(code_blocks[3].clone())), - (19, None), - ]; - - for (row, expected) in expected_results { - let offset = snapshot.point_to_offset(Point::new(row, 0)); - let range = find_surrounding_code_block(&snapshot, offset); - assert_eq!(range, expected, "unexpected result on row {:?}", row); - } - } -} diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 74d20a5ed65bef..53f142c029fb08 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1,8 +1,8 @@ use crate::{ - humanize_token_count, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, - CyclePreviousInlineAssist, RequestType, + AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, CyclePreviousInlineAssist, }; use anyhow::{anyhow, Context as _, Result}; +use assistant_context_editor::{humanize_token_count, RequestType}; use assistant_settings::AssistantSettings; use client::{telemetry::Telemetry, ErrorExt}; use collections::{hash_map, HashMap, HashSet, VecDeque}; @@ -26,9 +26,9 @@ use futures::{ join, SinkExt, Stream, StreamExt, }; use gpui::{ - anchored, deferred, point, AnyElement, AppContext, ClickEvent, CursorStyle, EventEmitter, - FocusHandle, FocusableView, FontWeight, Global, HighlightStyle, Model, ModelContext, - Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView, WindowContext, + anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity, + EventEmitter, FocusHandle, Focusable, FontWeight, Global, HighlightStyle, Subscription, Task, + TextStyle, UpdateGlobal, WeakEntity, Window, }; use language::{Buffer, IndentKind, Point, Selection, TransactionId}; use language_model::{ @@ -70,17 +70,20 @@ pub fn init( fs: Arc, prompt_builder: Arc, telemetry: Arc, - cx: &mut AppContext, + cx: &mut App, ) { cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry)); - cx.observe_new_views(|_, cx| { - let workspace = cx.view().clone(); + cx.observe_new(|_, window, cx| { + let Some(window) = window else { + return; + }; + let workspace = cx.entity().clone(); InlineAssistant::update_global(cx, |inline_assistant, cx| { - inline_assistant.register_workspace(&workspace, cx) + inline_assistant.register_workspace(&workspace, window, cx) }); - cx.observe_flag::({ - |is_assistant2_enabled, _view, cx| { + cx.observe_flag::(window, { + |is_assistant2_enabled, _workspace, _window, cx| { InlineAssistant::update_global(cx, |inline_assistant, _cx| { inline_assistant.is_assistant2_enabled = is_assistant2_enabled; }); @@ -97,9 +100,9 @@ pub struct InlineAssistant { next_assist_id: InlineAssistId, next_assist_group_id: InlineAssistGroupId, assists: HashMap, - assists_by_editor: HashMap, EditorInlineAssists>, + assists_by_editor: HashMap, EditorInlineAssists>, assist_groups: HashMap, - confirmed_assists: HashMap>, + confirmed_assists: HashMap>, prompt_history: VecDeque, prompt_builder: Arc, telemetry: Arc, @@ -130,13 +133,19 @@ impl InlineAssistant { } } - pub fn register_workspace(&mut self, workspace: &View, cx: &mut WindowContext) { - cx.subscribe(workspace, |workspace, event, cx| { - Self::update_global(cx, |this, cx| { - this.handle_workspace_event(workspace, event, cx) - }); - }) - .detach(); + pub fn register_workspace( + &mut self, + workspace: &Entity, + window: &mut Window, + cx: &mut App, + ) { + window + .subscribe(workspace, cx, |workspace, event, window, cx| { + Self::update_global(cx, |this, cx| { + this.handle_workspace_event(workspace, event, window, cx) + }); + }) + .detach(); let workspace = workspace.downgrade(); cx.observe_global::(move |cx| { @@ -156,9 +165,10 @@ impl InlineAssistant { fn handle_workspace_event( &mut self, - workspace: View, + workspace: Entity, event: &workspace::Event, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { match event { workspace::Event::UserSavedItem { item, .. } => { @@ -168,14 +178,14 @@ impl InlineAssistant { for assist_id in editor_assists.assist_ids.clone() { let assist = &self.assists[&assist_id]; if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) { - self.finish_assist(assist_id, false, cx) + self.finish_assist(assist_id, false, window, cx) } } } } } workspace::Event::ItemAdded { item } => { - self.register_workspace_item(&workspace, item.as_ref(), cx); + self.register_workspace_item(&workspace, item.as_ref(), window, cx); } _ => (), } @@ -183,23 +193,28 @@ impl InlineAssistant { fn register_workspace_item( &mut self, - workspace: &View, + workspace: &Entity, item: &dyn ItemHandle, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let is_assistant2_enabled = self.is_assistant2_enabled; if let Some(editor) = item.act_as::(cx) { editor.update(cx, |editor, cx| { if is_assistant2_enabled { - editor - .remove_code_action_provider(ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), cx); + editor.remove_code_action_provider( + ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), + window, + cx, + ); } else { editor.add_code_action_provider( Rc::new(AssistantCodeActionProvider { - editor: cx.view().downgrade(), + editor: cx.entity().downgrade(), workspace: workspace.downgrade(), }), + window, cx, ); } @@ -209,11 +224,12 @@ impl InlineAssistant { pub fn assist( &mut self, - editor: &View, - workspace: Option>, - assistant_panel: Option<&View>, + editor: &Entity, + workspace: Option>, + assistant_panel: Option<&Entity>, initial_prompt: Option, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let (snapshot, initial_selections) = editor.update(cx, |editor, cx| { ( @@ -250,22 +266,19 @@ impl InlineAssistant { let newest_selection = newest_selection.unwrap(); let mut codegen_ranges = Vec::new(); - for (excerpt_id, buffer, buffer_range) in - snapshot.excerpts_in_ranges(selections.iter().map(|selection| { + for (buffer, buffer_range, excerpt_id) in + snapshot.ranges_to_buffer_ranges(selections.iter().map(|selection| { snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end) })) { - let start = Anchor { - buffer_id: Some(buffer.remote_id()), - excerpt_id, - text_anchor: buffer.anchor_before(buffer_range.start), - }; - let end = Anchor { - buffer_id: Some(buffer.remote_id()), + let start = buffer.anchor_before(buffer_range.start); + let end = buffer.anchor_after(buffer_range.end); + + codegen_ranges.push(Anchor::range_in_buffer( excerpt_id, - text_anchor: buffer.anchor_after(buffer_range.end), - }; - codegen_ranges.push(start..end); + buffer.remote_id(), + start..end, + )); if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { self.telemetry.report_assistant_event(AssistantEvent { @@ -283,15 +296,14 @@ impl InlineAssistant { } let assist_group_id = self.next_assist_group_id.post_inc(); - let prompt_buffer = - cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)); - let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx)); + let prompt_buffer = cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)); + let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx)); let mut assists = Vec::new(); let mut assist_to_focus = None; for range in codegen_ranges { let assist_id = self.next_assist_id.post_inc(); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { Codegen::new( editor.read(cx).buffer().clone(), range.clone(), @@ -303,7 +315,7 @@ impl InlineAssistant { }); let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { PromptEditor::new( assist_id, gutter_dimensions.clone(), @@ -314,6 +326,7 @@ impl InlineAssistant { assistant_panel, workspace.clone(), self.fs.clone(), + window, cx, ) }); @@ -344,7 +357,7 @@ impl InlineAssistant { let editor_assists = self .assists_by_editor .entry(editor.downgrade()) - .or_insert_with(|| EditorInlineAssists::new(&editor, cx)); + .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx)); let mut assist_group = InlineAssistGroup::new(); for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists { self.assists.insert( @@ -360,6 +373,7 @@ impl InlineAssistant { range, prompt_editor.read(cx).codegen.clone(), workspace.clone(), + window, cx, ), ); @@ -369,25 +383,26 @@ impl InlineAssistant { self.assist_groups.insert(assist_group_id, assist_group); if let Some(assist_id) = assist_to_focus { - self.focus_assist(assist_id, cx); + self.focus_assist(assist_id, window, cx); } } #[allow(clippy::too_many_arguments)] pub fn suggest_assist( &mut self, - editor: &View, + editor: &Entity, mut range: Range, initial_prompt: String, initial_transaction_id: Option, focus: bool, - workspace: Option>, - assistant_panel: Option<&View>, - cx: &mut WindowContext, + workspace: Option>, + assistant_panel: Option<&Entity>, + window: &mut Window, + cx: &mut App, ) -> InlineAssistId { let assist_group_id = self.next_assist_group_id.post_inc(); - let prompt_buffer = cx.new_model(|cx| Buffer::local(&initial_prompt, cx)); - let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx)); + let prompt_buffer = cx.new(|cx| Buffer::local(&initial_prompt, cx)); + let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx)); let assist_id = self.next_assist_id.post_inc(); @@ -398,7 +413,7 @@ impl InlineAssistant { range.end = range.end.bias_right(&snapshot); } - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { Codegen::new( editor.read(cx).buffer().clone(), range.clone(), @@ -410,7 +425,7 @@ impl InlineAssistant { }); let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { PromptEditor::new( assist_id, gutter_dimensions.clone(), @@ -421,6 +436,7 @@ impl InlineAssistant { assistant_panel, workspace.clone(), self.fs.clone(), + window, cx, ) }); @@ -431,7 +447,7 @@ impl InlineAssistant { let editor_assists = self .assists_by_editor .entry(editor.downgrade()) - .or_insert_with(|| EditorInlineAssists::new(&editor, cx)); + .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx)); let mut assist_group = InlineAssistGroup::new(); self.assists.insert( @@ -447,6 +463,7 @@ impl InlineAssistant { range, prompt_editor.read(cx).codegen.clone(), workspace.clone(), + window, cx, ), ); @@ -455,7 +472,7 @@ impl InlineAssistant { self.assist_groups.insert(assist_group_id, assist_group); if focus { - self.focus_assist(assist_id, cx); + self.focus_assist(assist_id, window, cx); } assist_id @@ -463,10 +480,10 @@ impl InlineAssistant { fn insert_assist_blocks( &self, - editor: &View, + editor: &Entity, range: &Range, - prompt_editor: &View, - cx: &mut WindowContext, + prompt_editor: &Entity, + cx: &mut App, ) -> [CustomBlockId; 2] { let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| { prompt_editor @@ -503,7 +520,7 @@ impl InlineAssistant { }) } - fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut App) { let assist = &self.assists[&assist_id]; let Some(decorations) = assist.decorations.as_ref() else { return; @@ -544,11 +561,7 @@ impl InlineAssistant { .ok(); } - fn handle_prompt_editor_focus_out( - &mut self, - assist_id: InlineAssistId, - cx: &mut WindowContext, - ) { + fn handle_prompt_editor_focus_out(&mut self, assist_id: InlineAssistId, cx: &mut App) { let assist = &self.assists[&assist_id]; let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap(); if assist_group.active_assist_id == Some(assist_id) { @@ -567,31 +580,32 @@ impl InlineAssistant { fn handle_prompt_editor_event( &mut self, - prompt_editor: View, + prompt_editor: Entity, event: &PromptEditorEvent, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let assist_id = prompt_editor.read(cx).id; match event { PromptEditorEvent::StartRequested => { - self.start_assist(assist_id, cx); + self.start_assist(assist_id, window, cx); } PromptEditorEvent::StopRequested => { self.stop_assist(assist_id, cx); } PromptEditorEvent::ConfirmRequested => { - self.finish_assist(assist_id, false, cx); + self.finish_assist(assist_id, false, window, cx); } PromptEditorEvent::CancelRequested => { - self.finish_assist(assist_id, true, cx); + self.finish_assist(assist_id, true, window, cx); } PromptEditorEvent::DismissRequested => { - self.dismiss_assist(assist_id, cx); + self.dismiss_assist(assist_id, window, cx); } } } - fn handle_editor_newline(&mut self, editor: View, cx: &mut WindowContext) { + fn handle_editor_newline(&mut self, editor: Entity, window: &mut Window, cx: &mut App) { let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else { return; }; @@ -609,9 +623,9 @@ impl InlineAssistant { if assist_range.contains(&selection.start) && assist_range.contains(&selection.end) { if matches!(assist.codegen.read(cx).status(cx), CodegenStatus::Pending) { - self.dismiss_assist(*assist_id, cx); + self.dismiss_assist(*assist_id, window, cx); } else { - self.finish_assist(*assist_id, false, cx); + self.finish_assist(*assist_id, false, window, cx); } return; @@ -622,7 +636,7 @@ impl InlineAssistant { cx.propagate(); } - fn handle_editor_cancel(&mut self, editor: View, cx: &mut WindowContext) { + fn handle_editor_cancel(&mut self, editor: Entity, window: &mut Window, cx: &mut App) { let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else { return; }; @@ -642,7 +656,7 @@ impl InlineAssistant { if assist_range.contains(&selection.start) && assist_range.contains(&selection.end) { - self.focus_assist(*assist_id, cx); + self.focus_assist(*assist_id, window, cx); return; } else { let distance_from_selection = assist_range @@ -669,22 +683,27 @@ impl InlineAssistant { } if let Some((&assist_id, _)) = closest_assist_fallback { - self.focus_assist(assist_id, cx); + self.focus_assist(assist_id, window, cx); } } cx.propagate(); } - fn handle_editor_release(&mut self, editor: WeakView, cx: &mut WindowContext) { + fn handle_editor_release( + &mut self, + editor: WeakEntity, + window: &mut Window, + cx: &mut App, + ) { if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor) { for assist_id in editor_assists.assist_ids.clone() { - self.finish_assist(assist_id, true, cx); + self.finish_assist(assist_id, true, window, cx); } } } - fn handle_editor_change(&mut self, editor: View, cx: &mut WindowContext) { + fn handle_editor_change(&mut self, editor: Entity, window: &mut Window, cx: &mut App) { let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else { return; }; @@ -704,16 +723,17 @@ impl InlineAssistant { .0 as f32 - scroll_lock.distance_from_top; if target_scroll_top != scroll_position.y { - editor.set_scroll_position(point(scroll_position.x, target_scroll_top), cx); + editor.set_scroll_position(point(scroll_position.x, target_scroll_top), window, cx); } }); } fn handle_editor_event( &mut self, - editor: View, + editor: Entity, event: &EditorEvent, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) else { return; @@ -737,7 +757,7 @@ impl InlineAssistant { .iter() .any(|range| range.overlaps(&assist_range)) { - self.finish_assist(assist_id, false, cx); + self.finish_assist(assist_id, false, window, cx); } } } @@ -765,7 +785,11 @@ impl InlineAssistant { for assist_id in editor_assists.assist_ids.clone() { let assist = &self.assists[&assist_id]; if let Some(decorations) = assist.decorations.as_ref() { - if decorations.prompt_editor.focus_handle(cx).is_focused(cx) { + if decorations + .prompt_editor + .focus_handle(cx) + .is_focused(window) + { return; } } @@ -777,18 +801,24 @@ impl InlineAssistant { } } - pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) { + pub fn finish_assist( + &mut self, + assist_id: InlineAssistId, + undo: bool, + window: &mut Window, + cx: &mut App, + ) { if let Some(assist) = self.assists.get(&assist_id) { let assist_group_id = assist.group_id; if self.assist_groups[&assist_group_id].linked { - for assist_id in self.unlink_assist_group(assist_group_id, cx) { - self.finish_assist(assist_id, undo, cx); + for assist_id in self.unlink_assist_group(assist_group_id, window, cx) { + self.finish_assist(assist_id, undo, window, cx); } return; } } - self.dismiss_assist(assist_id, cx); + self.dismiss_assist(assist_id, window, cx); if let Some(assist) = self.assists.remove(&assist_id) { if let hash_map::Entry::Occupied(mut entry) = self.assist_groups.entry(assist.group_id) @@ -823,7 +853,7 @@ impl InlineAssistant { let ranges = multibuffer_snapshot.range_to_buffer_ranges(assist.range.clone()); ranges .first() - .and_then(|(excerpt, _)| excerpt.buffer().language()) + .and_then(|(buffer, _, _)| buffer.language()) .map(|language| language.name()) }); report_assistant_event( @@ -857,7 +887,12 @@ impl InlineAssistant { } } - fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool { + fn dismiss_assist( + &mut self, + assist_id: InlineAssistId, + window: &mut Window, + cx: &mut App, + ) -> bool { let Some(assist) = self.assists.get_mut(&assist_id) else { return false; }; @@ -878,9 +913,9 @@ impl InlineAssistant { if decorations .prompt_editor .focus_handle(cx) - .contains_focused(cx) + .contains_focused(window, cx) { - self.focus_next_assist(assist_id, cx); + self.focus_next_assist(assist_id, window, cx); } if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) { @@ -897,7 +932,7 @@ impl InlineAssistant { true } - fn focus_next_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + fn focus_next_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) { let Some(assist) = self.assists.get(&assist_id) else { return; }; @@ -917,15 +952,18 @@ impl InlineAssistant { for assist_id in assist_ids { let assist = &self.assists[assist_id]; if assist.decorations.is_some() { - self.focus_assist(*assist_id, cx); + self.focus_assist(*assist_id, window, cx); return; } } - assist.editor.update(cx, |editor, cx| editor.focus(cx)).ok(); + assist + .editor + .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx))) + .ok(); } - fn focus_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + fn focus_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) { let Some(assist) = self.assists.get(&assist_id) else { return; }; @@ -933,16 +971,21 @@ impl InlineAssistant { if let Some(decorations) = assist.decorations.as_ref() { decorations.prompt_editor.update(cx, |prompt_editor, cx| { prompt_editor.editor.update(cx, |editor, cx| { - editor.focus(cx); - editor.select_all(&SelectAll, cx); + window.focus(&editor.focus_handle(cx)); + editor.select_all(&SelectAll, window, cx); }) }); } - self.scroll_to_assist(assist_id, cx); + self.scroll_to_assist(assist_id, window, cx); } - pub fn scroll_to_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + pub fn scroll_to_assist( + &mut self, + assist_id: InlineAssistId, + window: &mut Window, + cx: &mut App, + ) { let Some(assist) = self.assists.get(&assist_id) else { return; }; @@ -952,7 +995,7 @@ impl InlineAssistant { let position = assist.range.start; editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| { + editor.change_selections(None, window, cx, |selections| { selections.select_anchor_ranges([position..position]) }); @@ -968,7 +1011,7 @@ impl InlineAssistant { .unwrap() .0 as f32; } else { - let snapshot = editor.snapshot(cx); + let snapshot = editor.snapshot(window, cx); let start_row = assist .range .start @@ -985,13 +1028,16 @@ impl InlineAssistant { let scroll_bottom = scroll_top + height_in_lines; if scroll_target_top < scroll_top { - editor.set_scroll_position(point(0., scroll_target_top), cx); + editor.set_scroll_position(point(0., scroll_target_top), window, cx); } else if scroll_target_bottom > scroll_bottom { if (scroll_target_bottom - scroll_target_top) <= height_in_lines { - editor - .set_scroll_position(point(0., scroll_target_bottom - height_in_lines), cx); + editor.set_scroll_position( + point(0., scroll_target_bottom - height_in_lines), + window, + cx, + ); } else { - editor.set_scroll_position(point(0., scroll_target_top), cx); + editor.set_scroll_position(point(0., scroll_target_top), window, cx); } } }); @@ -1000,7 +1046,8 @@ impl InlineAssistant { fn unlink_assist_group( &mut self, assist_group_id: InlineAssistGroupId, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Vec { let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap(); assist_group.linked = false; @@ -1009,13 +1056,13 @@ impl InlineAssistant { if let Some(editor_decorations) = assist.decorations.as_ref() { editor_decorations .prompt_editor - .update(cx, |prompt_editor, cx| prompt_editor.unlink(cx)); + .update(cx, |prompt_editor, cx| prompt_editor.unlink(window, cx)); } } assist_group.assist_ids.clone() } - pub fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + pub fn start_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -1024,8 +1071,8 @@ impl InlineAssistant { let assist_group_id = assist.group_id; if self.assist_groups[&assist_group_id].linked { - for assist_id in self.unlink_assist_group(assist_group_id, cx) { - self.start_assist(assist_id, cx); + for assist_id in self.unlink_assist_group(assist_group_id, window, cx) { + self.start_assist(assist_id, window, cx); } return; } @@ -1050,7 +1097,7 @@ impl InlineAssistant { .log_err(); } - pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut App) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -1060,7 +1107,7 @@ impl InlineAssistant { assist.codegen.update(cx, |codegen, cx| codegen.stop(cx)); } - fn update_editor_highlights(&self, editor: &View, cx: &mut WindowContext) { + fn update_editor_highlights(&self, editor: &Entity, cx: &mut App) { let mut gutter_pending_ranges = Vec::new(); let mut gutter_transformed_ranges = Vec::new(); let mut foreground_ranges = Vec::new(); @@ -1153,9 +1200,10 @@ impl InlineAssistant { fn update_editor_blocks( &mut self, - editor: &View, + editor: &Entity, assist_id: InlineAssistId, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let Some(assist) = self.assists.get_mut(&assist_id) else { return; @@ -1185,10 +1233,9 @@ impl InlineAssistant { )) .unwrap(); - let deleted_lines_editor = cx.new_view(|cx| { - let multi_buffer = cx.new_model(|_| { - MultiBuffer::without_headers(language::Capability::ReadOnly) - }); + let deleted_lines_editor = cx.new(|cx| { + let multi_buffer = + cx.new(|_| MultiBuffer::without_headers(language::Capability::ReadOnly)); multi_buffer.update(cx, |multi_buffer, cx| { multi_buffer.push_excerpts( old_buffer.clone(), @@ -1201,14 +1248,14 @@ impl InlineAssistant { }); enum DeletedLines {} - let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx); + let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_gutter(false, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); editor.set_show_scrollbars(false, cx); editor.set_read_only(true); - editor.set_show_inline_completions(Some(false), cx); + editor.set_show_inline_completions(Some(false), window, cx); editor.highlight_rows::( Anchor::min()..Anchor::max(), cx.theme().status().deleted_background, @@ -1229,7 +1276,7 @@ impl InlineAssistant { .block_mouse_down() .bg(cx.theme().status().deleted_background) .size_full() - .h(height as f32 * cx.line_height()) + .h(height as f32 * cx.window.line_height()) .pl(cx.gutter_dimensions.full_width()) .child(deleted_lines_editor.clone()) .into_any_element() @@ -1260,14 +1307,13 @@ struct InlineAssistScrollLock { } impl EditorInlineAssists { - #[allow(clippy::too_many_arguments)] - fn new(editor: &View, cx: &mut WindowContext) -> Self { + fn new(editor: &Entity, window: &mut Window, cx: &mut App) -> Self { let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(()); Self { assist_ids: Vec::new(), scroll_lock: None, highlight_updates: highlight_updates_tx, - _update_highlights: cx.spawn(|mut cx| { + _update_highlights: cx.spawn(|cx| { let editor = editor.downgrade(); async move { while let Ok(()) = highlight_updates_rx.changed().await { @@ -1280,47 +1326,43 @@ impl EditorInlineAssists { } }), _subscriptions: vec![ - cx.observe_release(editor, { + cx.observe_release_in(editor, window, { let editor = editor.downgrade(); - |_, cx| { + |_, window, cx| { InlineAssistant::update_global(cx, |this, cx| { - this.handle_editor_release(editor, cx); + this.handle_editor_release(editor, window, cx); }) } }), - cx.observe(editor, move |editor, cx| { + window.observe(editor, cx, move |editor, window, cx| { InlineAssistant::update_global(cx, |this, cx| { - this.handle_editor_change(editor, cx) + this.handle_editor_change(editor, window, cx) }) }), - cx.subscribe(editor, move |editor, event, cx| { + window.subscribe(editor, cx, move |editor, event, window, cx| { InlineAssistant::update_global(cx, |this, cx| { - this.handle_editor_event(editor, event, cx) + this.handle_editor_event(editor, event, window, cx) }) }), editor.update(cx, |editor, cx| { - let editor_handle = cx.view().downgrade(); - editor.register_action( - move |_: &editor::actions::Newline, cx: &mut WindowContext| { - InlineAssistant::update_global(cx, |this, cx| { - if let Some(editor) = editor_handle.upgrade() { - this.handle_editor_newline(editor, cx) - } - }) - }, - ) + let editor_handle = cx.entity().downgrade(); + editor.register_action(move |_: &editor::actions::Newline, window, cx| { + InlineAssistant::update_global(cx, |this, cx| { + if let Some(editor) = editor_handle.upgrade() { + this.handle_editor_newline(editor, window, cx) + } + }) + }) }), editor.update(cx, |editor, cx| { - let editor_handle = cx.view().downgrade(); - editor.register_action( - move |_: &editor::actions::Cancel, cx: &mut WindowContext| { - InlineAssistant::update_global(cx, |this, cx| { - if let Some(editor) = editor_handle.upgrade() { - this.handle_editor_cancel(editor, cx) - } - }) - }, - ) + let editor_handle = cx.entity().downgrade(); + editor.register_action(move |_: &editor::actions::Cancel, window, cx| { + InlineAssistant::update_global(cx, |this, cx| { + if let Some(editor) = editor_handle.upgrade() { + this.handle_editor_cancel(editor, window, cx) + } + }) + }) }), ], } @@ -1343,7 +1385,7 @@ impl InlineAssistGroup { } } -fn build_assist_editor_renderer(editor: &View) -> RenderBlock { +fn build_assist_editor_renderer(editor: &Entity) -> RenderBlock { let editor = editor.clone(); Arc::new(move |cx: &mut BlockContext| { *editor.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions; @@ -1383,20 +1425,20 @@ enum PromptEditorEvent { struct PromptEditor { id: InlineAssistId, - editor: View, - language_model_selector: View, + editor: Entity, + language_model_selector: Entity, edited_since_done: bool, gutter_dimensions: Arc>, prompt_history: VecDeque, prompt_history_ix: Option, pending_prompt: String, - codegen: Model, + codegen: Entity, _codegen_subscription: Subscription, editor_subscriptions: Vec, pending_token_count: Task>, token_counts: Option, _token_count_subscriptions: Vec, - workspace: Option>, + workspace: Option>, show_rate_limit_notice: bool, } @@ -1409,7 +1451,7 @@ pub struct TokenCounts { impl EventEmitter for PromptEditor {} impl Render for PromptEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let gutter_dimensions = *self.gutter_dimensions.lock(); let codegen = self.codegen.read(cx); @@ -1425,17 +1467,21 @@ impl Render for PromptEditor { IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx)) + .tooltip(|window, cx| { + Tooltip::for_action("Cancel Assist", &menu::Cancel, window, cx) + }) .on_click( - cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), ) .into_any_element(), IconButton::new("start", IconName::SparkleAlt) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::for_action("Transform", &menu::Confirm, cx)) + .tooltip(|window, cx| { + Tooltip::for_action("Transform", &menu::Confirm, window, cx) + }) .on_click( - cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)), + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)), ) .into_any_element(), ] @@ -1445,23 +1491,26 @@ impl Render for PromptEditor { IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::text("Cancel Assist", cx)) + .tooltip(Tooltip::text("Cancel Assist")) .on_click( - cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), ) .into_any_element(), IconButton::new("stop", IconName::Stop) .icon_color(Color::Error) .shape(IconButtonShape::Square) - .tooltip(|cx| { + .tooltip(|window, cx| { Tooltip::with_meta( "Interrupt Transformation", Some(&menu::Cancel), "Changes won't be discarded", + window, cx, ) }) - .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested))) + .on_click( + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)), + ) .into_any_element(), ] } @@ -1479,23 +1528,26 @@ impl Render for PromptEditor { IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx)) + .tooltip(|window, cx| { + Tooltip::for_action("Cancel Assist", &menu::Cancel, window, cx) + }) .on_click( - cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), ) .into_any_element(), IconButton::new("restart", IconName::RotateCw) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| { + .tooltip(|window, cx| { Tooltip::with_meta( "Regenerate Transformation", Some(restart_key), "Current change will be discarded", + window, cx, ) }) - .on_click(cx.listener(|_, _, cx| { + .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::StartRequested); })) .into_any_element(), @@ -1503,8 +1555,10 @@ impl Render for PromptEditor { IconButton::new("confirm", IconName::Check) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::for_action("Confirm Assist", &menu::Confirm, cx)) - .on_click(cx.listener(|_, _, cx| { + .tooltip(|window, cx| { + Tooltip::for_action("Confirm Assist", &menu::Confirm, window, cx) + }) + .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::ConfirmRequested); })) .into_any_element() @@ -1523,7 +1577,7 @@ impl Render for PromptEditor { .border_y_1() .border_color(cx.theme().status().info_border) .size_full() - .py(cx.line_height() / 2.5) + .py(window.line_height() / 2.5) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::restart)) @@ -1542,7 +1596,7 @@ impl Render for PromptEditor { .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(Color::Muted) - .tooltip(move |cx| { + .tooltip(move |window, cx| { Tooltip::with_meta( format!( "Using {}", @@ -1553,6 +1607,7 @@ impl Render for PromptEditor { ), None, "Change Model", + window, cx, ) }), @@ -1589,7 +1644,7 @@ impl Render for PromptEditor { el.child( div() .id("error") - .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) + .tooltip(Tooltip::text(error_message)) .child( Icon::new(IconName::XCircle) .size(IconSize::Small) @@ -1610,8 +1665,8 @@ impl Render for PromptEditor { } } -impl FocusableView for PromptEditor { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for PromptEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) } } @@ -1624,15 +1679,16 @@ impl PromptEditor { id: InlineAssistId, gutter_dimensions: Arc>, prompt_history: VecDeque, - prompt_buffer: Model, - codegen: Model, - parent_editor: &View, - assistant_panel: Option<&View>, - workspace: Option>, + prompt_buffer: Entity, + codegen: Entity, + parent_editor: &Entity, + assistant_panel: Option<&Entity>, + workspace: Option>, fs: Arc, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { let mut editor = Editor::new( EditorMode::AutoHeight { max_lines: Self::MAX_LINES as usize, @@ -1640,6 +1696,7 @@ impl PromptEditor { prompt_buffer, None, false, + window, cx, ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); @@ -1647,22 +1704,28 @@ impl PromptEditor { // always show the cursor (even when it isn't focused) because // typing in one will make what you typed appear in all of them. editor.set_show_cursor_when_unfocused(true, cx); - editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx), cx), cx); + editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx), window), cx); editor }); let mut token_count_subscriptions = Vec::new(); - token_count_subscriptions - .push(cx.subscribe(parent_editor, Self::handle_parent_editor_event)); + token_count_subscriptions.push(cx.subscribe_in( + parent_editor, + window, + Self::handle_parent_editor_event, + )); if let Some(assistant_panel) = assistant_panel { - token_count_subscriptions - .push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event)); + token_count_subscriptions.push(cx.subscribe_in( + assistant_panel, + window, + Self::handle_assistant_panel_event, + )); } let mut this = Self { id, editor: prompt_editor, - language_model_selector: cx.new_view(|cx| { + language_model_selector: cx.new(|cx| { let fs = fs.clone(); LanguageModelSelector::new( move |model, cx| { @@ -1672,6 +1735,7 @@ impl PromptEditor { move |settings, _| settings.set_model(model.clone()), ); }, + window, cx, ) }), @@ -1690,45 +1754,48 @@ impl PromptEditor { show_rate_limit_notice: false, }; this.count_tokens(cx); - this.subscribe_to_editor(cx); + this.subscribe_to_editor(window, cx); this } - fn subscribe_to_editor(&mut self, cx: &mut ViewContext) { + fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context) { self.editor_subscriptions.clear(); - self.editor_subscriptions - .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events)); + self.editor_subscriptions.push(cx.subscribe_in( + &self.editor, + window, + Self::handle_prompt_editor_events, + )); } fn set_show_cursor_when_unfocused( &mut self, show_cursor_when_unfocused: bool, - cx: &mut ViewContext, + cx: &mut Context, ) { self.editor.update(cx, |editor, cx| { editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx) }); } - fn unlink(&mut self, cx: &mut ViewContext) { + fn unlink(&mut self, window: &mut Window, cx: &mut Context) { let prompt = self.prompt(cx); - let focus = self.editor.focus_handle(cx).contains_focused(cx); - self.editor = cx.new_view(|cx| { - let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx); + let focus = self.editor.focus_handle(cx).contains_focused(window, cx); + self.editor = cx.new(|cx| { + let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor.set_placeholder_text(Self::placeholder_text(self.codegen.read(cx), cx), cx); + editor.set_placeholder_text(Self::placeholder_text(self.codegen.read(cx), window), cx); editor.set_placeholder_text("Add a prompt…", cx); - editor.set_text(prompt, cx); + editor.set_text(prompt, window, cx); if focus { - editor.focus(cx); + window.focus(&editor.focus_handle(cx)); } editor }); - self.subscribe_to_editor(cx); + self.subscribe_to_editor(window, cx); } - fn placeholder_text(codegen: &Codegen, cx: &WindowContext) -> String { - let context_keybinding = text_for_action(&crate::ToggleFocus, cx) + fn placeholder_text(codegen: &Codegen, window: &Window) -> String { + let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window) .map(|keybinding| format!(" • {keybinding} for context")) .unwrap_or_default(); @@ -1741,23 +1808,29 @@ impl PromptEditor { format!("{action}…{context_keybinding} • ↓↑ for history") } - fn prompt(&self, cx: &AppContext) -> String { + fn prompt(&self, cx: &App) -> String { self.editor.read(cx).text(cx) } - fn toggle_rate_limit_notice(&mut self, _: &ClickEvent, cx: &mut ViewContext) { + fn toggle_rate_limit_notice( + &mut self, + _: &ClickEvent, + window: &mut Window, + cx: &mut Context, + ) { self.show_rate_limit_notice = !self.show_rate_limit_notice; if self.show_rate_limit_notice { - cx.focus_view(&self.editor); + window.focus(&self.editor.focus_handle(cx)); } cx.notify(); } fn handle_parent_editor_event( &mut self, - _: View, + _: &Entity, event: &EditorEvent, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) { if let EditorEvent::BufferEdited { .. } = event { self.count_tokens(cx); @@ -1766,15 +1839,16 @@ impl PromptEditor { fn handle_assistant_panel_event( &mut self, - _: View, + _: &Entity, event: &AssistantPanelEvent, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) { let AssistantPanelEvent::ContextEdited { .. } = event; self.count_tokens(cx); } - fn count_tokens(&mut self, cx: &mut ViewContext) { + fn count_tokens(&mut self, cx: &mut Context) { let assist_id = self.id; self.pending_token_count = cx.spawn(|this, mut cx| async move { cx.background_executor().timer(Duration::from_secs(1)).await; @@ -1797,25 +1871,24 @@ impl PromptEditor { fn handle_prompt_editor_events( &mut self, - _: View, + _: &Entity, event: &EditorEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { EditorEvent::Edited { .. } => { - if let Some(workspace) = cx.window_handle().downcast::() { - workspace - .update(cx, |workspace, cx| { - let is_via_ssh = workspace - .project() - .update(cx, |project, _| project.is_via_ssh()); - - workspace - .client() - .telemetry() - .log_edit_event("inline assist", is_via_ssh); - }) - .log_err(); + if let Some(workspace) = window.root::().flatten() { + workspace.update(cx, |workspace, cx| { + let is_via_ssh = workspace + .project() + .update(cx, |project, _| project.is_via_ssh()); + + workspace + .client() + .telemetry() + .log_edit_event("inline assist", is_via_ssh); + }); } let prompt = self.editor.read(cx).text(cx); if self @@ -1842,7 +1915,7 @@ impl PromptEditor { } } - fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { + fn handle_codegen_changed(&mut self, _: Entity, cx: &mut Context) { match self.codegen.read(cx).status(cx) { CodegenStatus::Idle => { self.editor @@ -1873,11 +1946,16 @@ impl PromptEditor { } } - fn restart(&mut self, _: &menu::Restart, cx: &mut ViewContext) { + fn restart(&mut self, _: &menu::Restart, _window: &mut Window, cx: &mut Context) { cx.emit(PromptEditorEvent::StartRequested); } - fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { + fn cancel( + &mut self, + _: &editor::actions::Cancel, + _window: &mut Window, + cx: &mut Context, + ) { match self.codegen.read(cx).status(cx) { CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => { cx.emit(PromptEditorEvent::CancelRequested); @@ -1888,7 +1966,7 @@ impl PromptEditor { } } - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { match self.codegen.read(cx).status(cx) { CodegenStatus::Idle => { cx.emit(PromptEditorEvent::StartRequested); @@ -1909,57 +1987,62 @@ impl PromptEditor { } } - fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { if let Some(ix) = self.prompt_history_ix { if ix > 0 { self.prompt_history_ix = Some(ix - 1); let prompt = self.prompt_history[ix - 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_beginning(&Default::default(), cx); + editor.set_text(prompt, window, cx); + editor.move_to_beginning(&Default::default(), window, cx); }); } } else if !self.prompt_history.is_empty() { self.prompt_history_ix = Some(self.prompt_history.len() - 1); let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_beginning(&Default::default(), cx); + editor.set_text(prompt, window, cx); + editor.move_to_beginning(&Default::default(), window, cx); }); } } - fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { if let Some(ix) = self.prompt_history_ix { if ix < self.prompt_history.len() - 1 { self.prompt_history_ix = Some(ix + 1); let prompt = self.prompt_history[ix + 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_end(&Default::default(), cx) + editor.set_text(prompt, window, cx); + editor.move_to_end(&Default::default(), window, cx) }); } else { self.prompt_history_ix = None; let prompt = self.pending_prompt.as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_end(&Default::default(), cx) + editor.set_text(prompt, window, cx); + editor.move_to_end(&Default::default(), window, cx) }); } } } - fn cycle_prev(&mut self, _: &CyclePreviousInlineAssist, cx: &mut ViewContext) { + fn cycle_prev( + &mut self, + _: &CyclePreviousInlineAssist, + _: &mut Window, + cx: &mut Context, + ) { self.codegen .update(cx, |codegen, cx| codegen.cycle_prev(cx)); } - fn cycle_next(&mut self, _: &CycleNextInlineAssist, cx: &mut ViewContext) { + fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context) { self.codegen .update(cx, |codegen, cx| codegen.cycle_next(cx)); } - fn render_cycle_controls(&self, cx: &ViewContext) -> AnyElement { + fn render_cycle_controls(&self, cx: &Context) -> AnyElement { let codegen = self.codegen.read(cx); let disabled = matches!(codegen.status(cx), CodegenStatus::Idle); @@ -2000,13 +2083,13 @@ impl PromptEditor { .shape(IconButtonShape::Square) .tooltip({ let focus_handle = self.editor.focus_handle(cx); - move |cx| { - cx.new_view(|cx| { + move |window, cx| { + cx.new(|_| { let mut tooltip = Tooltip::new("Previous Alternative").key_binding( KeyBinding::for_action_in( &CyclePreviousInlineAssist, &focus_handle, - cx, + window, ), ); if !disabled && current_index != 0 { @@ -2017,7 +2100,7 @@ impl PromptEditor { .into() } }) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _, cx| { this.codegen .update(cx, |codegen, cx| codegen.cycle_prev(cx)) })), @@ -2042,13 +2125,13 @@ impl PromptEditor { .shape(IconButtonShape::Square) .tooltip({ let focus_handle = self.editor.focus_handle(cx); - move |cx| { - cx.new_view(|cx| { + move |window, cx| { + cx.new(|_| { let mut tooltip = Tooltip::new("Next Alternative").key_binding( KeyBinding::for_action_in( &CycleNextInlineAssist, &focus_handle, - cx, + window, ), ); if !disabled && current_index != total_models - 1 { @@ -2059,7 +2142,7 @@ impl PromptEditor { .into() } }) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _, cx| { this.codegen .update(cx, |codegen, cx| codegen.cycle_next(cx)) })), @@ -2067,7 +2150,7 @@ impl PromptEditor { .into_any_element() } - fn render_token_count(&self, cx: &mut ViewContext) -> Option { + fn render_token_count(&self, cx: &mut Context) -> Option { let model = LanguageModelRegistry::read_global(cx).active_model()?; let token_counts = self.token_counts?; let max_token_count = model.max_token_count(); @@ -2097,7 +2180,7 @@ impl PromptEditor { ); if let Some(workspace) = self.workspace.clone() { token_count = token_count - .tooltip(move |cx| { + .tooltip(move |window, cx| { Tooltip::with_meta( format!( "Tokens Used ({} from the Assistant Panel)", @@ -2105,29 +2188,30 @@ impl PromptEditor { ), None, "Click to open the Assistant Panel", + window, cx, ) }) .cursor_pointer() - .on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation()) - .on_click(move |_, cx| { + .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(move |_, window, cx| { cx.stop_propagation(); workspace .update(cx, |workspace, cx| { - workspace.focus_panel::(cx) + workspace.focus_panel::(window, cx) }) .ok(); }); } else { token_count = token_count .cursor_default() - .tooltip(|cx| Tooltip::text("Tokens used", cx)); + .tooltip(Tooltip::text("Tokens used")); } Some(token_count) } - fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + fn render_prompt_editor(&self, cx: &mut Context) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if self.editor.read(cx).read_only(cx) { @@ -2153,7 +2237,7 @@ impl PromptEditor { ) } - fn render_rate_limit_notice(&self, cx: &mut ViewContext) -> impl IntoElement { + fn render_rate_limit_notice(&self, cx: &mut Context) -> impl IntoElement { Popover::new().child( v_flex() .occlude() @@ -2177,7 +2261,7 @@ impl PromptEditor { } else { ui::ToggleState::Unselected }, - |selection, cx| { + |selection, _, cx| { let is_dismissed = match selection { ui::ToggleState::Unselected => false, ui::ToggleState::Indeterminate => return, @@ -2196,10 +2280,11 @@ impl PromptEditor { .on_click(cx.listener(Self::toggle_rate_limit_notice)), ) .child(Button::new("more-info", "More Info").on_click( - |_event, cx| { - cx.dispatch_action(Box::new( - zed_actions::OpenAccountSettings, - )) + |_event, window, cx| { + window.dispatch_action( + Box::new(zed_actions::OpenAccountSettings), + cx, + ) }, )), ), @@ -2217,7 +2302,7 @@ fn dismissed_rate_limit_notice() -> bool { .map_or(false, |s| s.is_some()) } -fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut AppContext) { +fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) { db::write_and_log(cx, move || async move { if is_dismissed { db::kvp::KEY_VALUE_STORE @@ -2234,11 +2319,11 @@ fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut AppContext) { struct InlineAssist { group_id: InlineAssistGroupId, range: Range, - editor: WeakView, + editor: WeakEntity, decorations: Option, - codegen: Model, + codegen: Entity, _subscriptions: Vec, - workspace: Option>, + workspace: Option>, include_context: bool, } @@ -2248,14 +2333,15 @@ impl InlineAssist { assist_id: InlineAssistId, group_id: InlineAssistGroupId, include_context: bool, - editor: &View, - prompt_editor: &View, + editor: &Entity, + prompt_editor: &Entity, prompt_block_id: CustomBlockId, end_block_id: CustomBlockId, range: Range, - codegen: Model, - workspace: Option>, - cx: &mut WindowContext, + codegen: Entity, + workspace: Option>, + window: &mut Window, + cx: &mut App, ) -> Self { let prompt_editor_focus_handle = prompt_editor.focus_handle(cx); InlineAssist { @@ -2272,24 +2358,28 @@ impl InlineAssist { codegen: codegen.clone(), workspace: workspace.clone(), _subscriptions: vec![ - cx.on_focus_in(&prompt_editor_focus_handle, move |cx| { + window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| { InlineAssistant::update_global(cx, |this, cx| { this.handle_prompt_editor_focus_in(assist_id, cx) }) }), - cx.on_focus_out(&prompt_editor_focus_handle, move |_, cx| { + window.on_focus_out(&prompt_editor_focus_handle, cx, move |_, _, cx| { InlineAssistant::update_global(cx, |this, cx| { this.handle_prompt_editor_focus_out(assist_id, cx) }) }), - cx.subscribe(prompt_editor, |prompt_editor, event, cx| { - InlineAssistant::update_global(cx, |this, cx| { - this.handle_prompt_editor_event(prompt_editor, event, cx) - }) - }), - cx.observe(&codegen, { + window.subscribe( + prompt_editor, + cx, + move |prompt_editor, event, window, cx| { + InlineAssistant::update_global(cx, |this, cx| { + this.handle_prompt_editor_event(prompt_editor, event, window, cx) + }) + }, + ), + window.observe(&codegen, cx, { let editor = editor.downgrade(); - move |_, cx| { + move |_, window, cx| { if let Some(editor) = editor.upgrade() { InlineAssistant::update_global(cx, |this, cx| { if let Some(editor_assists) = @@ -2298,14 +2388,14 @@ impl InlineAssist { editor_assists.highlight_updates.send(()).ok(); } - this.update_editor_blocks(&editor, assist_id, cx); + this.update_editor_blocks(&editor, assist_id, window, cx); }) } } }), - cx.subscribe(&codegen, move |codegen, event, cx| { + window.subscribe(&codegen, cx, move |codegen, event, window, cx| { InlineAssistant::update_global(cx, |this, cx| match event { - CodegenEvent::Undone => this.finish_assist(assist_id, false, cx), + CodegenEvent::Undone => this.finish_assist(assist_id, false, window, cx), CodegenEvent::Finished => { let assist = if let Some(assist) = this.assists.get(&assist_id) { assist @@ -2336,7 +2426,7 @@ impl InlineAssist { } if assist.decorations.is_none() { - this.finish_assist(assist_id, false, cx); + this.finish_assist(assist_id, false, window, cx); } } }) @@ -2345,12 +2435,12 @@ impl InlineAssist { } } - fn user_prompt(&self, cx: &AppContext) -> Option { + fn user_prompt(&self, cx: &App) -> Option { let decorations = self.decorations.as_ref()?; Some(decorations.prompt_editor.read(cx).prompt(cx)) } - fn assistant_panel_context(&self, cx: &WindowContext) -> Option { + fn assistant_panel_context(&self, cx: &mut App) -> Option { if self.include_context { let workspace = self.workspace.as_ref()?; let workspace = workspace.upgrade()?.read(cx); @@ -2367,7 +2457,7 @@ impl InlineAssist { } } - pub fn count_tokens(&self, cx: &WindowContext) -> BoxFuture<'static, Result> { + pub fn count_tokens(&self, cx: &mut App) -> BoxFuture<'static, Result> { let Some(user_prompt) = self.user_prompt(cx) else { return future::ready(Err(anyhow!("no user prompt"))).boxed(); }; @@ -2380,7 +2470,7 @@ impl InlineAssist { struct InlineAssistDecorations { prompt_block_id: CustomBlockId, - prompt_editor: View, + prompt_editor: Entity, removed_line_block_ids: HashSet, end_block_id: CustomBlockId, } @@ -2392,11 +2482,11 @@ pub enum CodegenEvent { } pub struct Codegen { - alternatives: Vec>, + alternatives: Vec>, active_alternative: usize, seen_alternatives: HashSet, subscriptions: Vec, - buffer: Model, + buffer: Entity, range: Range, initial_transaction_id: Option, telemetry: Arc, @@ -2406,14 +2496,14 @@ pub struct Codegen { impl Codegen { pub fn new( - buffer: Model, + buffer: Entity, range: Range, initial_transaction_id: Option, telemetry: Arc, builder: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -2439,7 +2529,7 @@ impl Codegen { this } - fn subscribe_to_alternative(&mut self, cx: &mut ModelContext) { + fn subscribe_to_alternative(&mut self, cx: &mut Context) { let codegen = self.active_alternative().clone(); self.subscriptions.clear(); self.subscriptions @@ -2448,22 +2538,22 @@ impl Codegen { .push(cx.subscribe(&codegen, |_, _, event, cx| cx.emit(*event))); } - fn active_alternative(&self) -> &Model { + fn active_alternative(&self) -> &Entity { &self.alternatives[self.active_alternative] } - fn status<'a>(&self, cx: &'a AppContext) -> &'a CodegenStatus { + fn status<'a>(&self, cx: &'a App) -> &'a CodegenStatus { &self.active_alternative().read(cx).status } - fn alternative_count(&self, cx: &AppContext) -> usize { + fn alternative_count(&self, cx: &App) -> usize { LanguageModelRegistry::read_global(cx) .inline_alternative_models() .len() + 1 } - pub fn cycle_prev(&mut self, cx: &mut ModelContext) { + pub fn cycle_prev(&mut self, cx: &mut Context) { let next_active_ix = if self.active_alternative == 0 { self.alternatives.len() - 1 } else { @@ -2472,12 +2562,12 @@ impl Codegen { self.activate(next_active_ix, cx); } - pub fn cycle_next(&mut self, cx: &mut ModelContext) { + pub fn cycle_next(&mut self, cx: &mut Context) { let next_active_ix = (self.active_alternative + 1) % self.alternatives.len(); self.activate(next_active_ix, cx); } - fn activate(&mut self, index: usize, cx: &mut ModelContext) { + fn activate(&mut self, index: usize, cx: &mut Context) { self.active_alternative() .update(cx, |codegen, cx| codegen.set_active(false, cx)); self.seen_alternatives.insert(index); @@ -2492,7 +2582,7 @@ impl Codegen { &mut self, user_prompt: String, assistant_panel_context: Option, - cx: &mut ModelContext, + cx: &mut Context, ) -> Result<()> { let alternative_models = LanguageModelRegistry::read_global(cx) .inline_alternative_models() @@ -2504,7 +2594,7 @@ impl Codegen { self.alternatives.truncate(1); for _ in 0..alternative_models.len() { - self.alternatives.push(cx.new_model(|cx| { + self.alternatives.push(cx.new(|cx| { CodegenAlternative::new( self.buffer.clone(), self.range.clone(), @@ -2537,13 +2627,13 @@ impl Codegen { Ok(()) } - pub fn stop(&mut self, cx: &mut ModelContext) { + pub fn stop(&mut self, cx: &mut Context) { for codegen in &self.alternatives { codegen.update(cx, |codegen, cx| codegen.stop(cx)); } } - pub fn undo(&mut self, cx: &mut ModelContext) { + pub fn undo(&mut self, cx: &mut Context) { self.active_alternative() .update(cx, |codegen, cx| codegen.undo(cx)); @@ -2559,34 +2649,34 @@ impl Codegen { &self, user_prompt: String, assistant_panel_context: Option, - cx: &AppContext, + cx: &App, ) -> BoxFuture<'static, Result> { self.active_alternative() .read(cx) .count_tokens(user_prompt, assistant_panel_context, cx) } - pub fn buffer(&self, cx: &AppContext) -> Model { + pub fn buffer(&self, cx: &App) -> Entity { self.active_alternative().read(cx).buffer.clone() } - pub fn old_buffer(&self, cx: &AppContext) -> Model { + pub fn old_buffer(&self, cx: &App) -> Entity { self.active_alternative().read(cx).old_buffer.clone() } - pub fn snapshot(&self, cx: &AppContext) -> MultiBufferSnapshot { + pub fn snapshot(&self, cx: &App) -> MultiBufferSnapshot { self.active_alternative().read(cx).snapshot.clone() } - pub fn edit_position(&self, cx: &AppContext) -> Option { + pub fn edit_position(&self, cx: &App) -> Option { self.active_alternative().read(cx).edit_position } - fn diff<'a>(&self, cx: &'a AppContext) -> &'a Diff { + fn diff<'a>(&self, cx: &'a App) -> &'a Diff { &self.active_alternative().read(cx).diff } - pub fn last_equal_ranges<'a>(&self, cx: &'a AppContext) -> &'a [Range] { + pub fn last_equal_ranges<'a>(&self, cx: &'a App) -> &'a [Range] { self.active_alternative().read(cx).last_equal_ranges() } } @@ -2594,8 +2684,8 @@ impl Codegen { impl EventEmitter for Codegen {} pub struct CodegenAlternative { - buffer: Model, - old_buffer: Model, + buffer: Entity, + old_buffer: Entity, snapshot: MultiBufferSnapshot, edit_position: Option, range: Range, @@ -2639,26 +2729,26 @@ impl EventEmitter for CodegenAlternative {} impl CodegenAlternative { pub fn new( - multi_buffer: Model, + multi_buffer: Entity, range: Range, active: bool, telemetry: Option>, builder: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { let snapshot = multi_buffer.read(cx).snapshot(cx); - let (old_excerpt, _) = snapshot + let (buffer, _, _) = snapshot .range_to_buffer_ranges(range.clone()) .pop() .unwrap(); - let old_buffer = cx.new_model(|cx| { - let text = old_excerpt.buffer().as_rope().clone(); - let line_ending = old_excerpt.buffer().line_ending(); - let language = old_excerpt.buffer().language().cloned(); + let old_buffer = cx.new(|cx| { + let text = buffer.as_rope().clone(); + let line_ending = buffer.line_ending(); + let language = buffer.language().cloned(); let language_registry = multi_buffer .read(cx) - .buffer(old_excerpt.buffer_id()) + .buffer(buffer.remote_id()) .unwrap() .read(cx) .language_registry(); @@ -2695,7 +2785,7 @@ impl CodegenAlternative { } } - fn set_active(&mut self, active: bool, cx: &mut ModelContext) { + fn set_active(&mut self, active: bool, cx: &mut Context) { if active != self.active { self.active = active; @@ -2719,9 +2809,9 @@ impl CodegenAlternative { fn handle_buffer_event( &mut self, - _buffer: Model, + _buffer: Entity, event: &multi_buffer::Event, - cx: &mut ModelContext, + cx: &mut Context, ) { if let multi_buffer::Event::TransactionUndone { transaction_id } = event { if self.transformation_transaction_id == Some(*transaction_id) { @@ -2740,7 +2830,7 @@ impl CodegenAlternative { &self, user_prompt: String, assistant_panel_context: Option, - cx: &AppContext, + cx: &App, ) -> BoxFuture<'static, Result> { if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx); @@ -2771,7 +2861,7 @@ impl CodegenAlternative { user_prompt: String, assistant_panel_context: Option, model: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) -> Result<()> { if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() { self.buffer.update(cx, |buffer, cx| { @@ -2802,7 +2892,7 @@ impl CodegenAlternative { &self, user_prompt: String, assistant_panel_context: Option, - cx: &AppContext, + cx: &App, ) -> Result { let buffer = self.buffer.read(cx).snapshot(cx); let language = buffer.language_at(self.range.start); @@ -2861,7 +2951,7 @@ impl CodegenAlternative { model_provider_id: String, model_api_key: Option, stream: impl 'static + Future>, - cx: &mut ModelContext, + cx: &mut Context, ) { let start_time = Instant::now(); let snapshot = self.snapshot.clone(); @@ -2898,7 +2988,7 @@ impl CodegenAlternative { let ranges = snapshot.range_to_buffer_ranges(self.range.clone()); ranges .first() - .and_then(|(excerpt, _)| excerpt.buffer().language()) + .and_then(|(buffer, _, _)| buffer.language()) .map(|language| language.name()) }; @@ -3119,7 +3209,7 @@ impl CodegenAlternative { cx.notify(); } - pub fn stop(&mut self, cx: &mut ModelContext) { + pub fn stop(&mut self, cx: &mut Context) { self.last_equal_ranges.clear(); if self.diff.is_empty() { self.status = CodegenStatus::Idle; @@ -3131,7 +3221,7 @@ impl CodegenAlternative { cx.notify(); } - pub fn undo(&mut self, cx: &mut ModelContext) { + pub fn undo(&mut self, cx: &mut Context) { self.buffer.update(cx, |buffer, cx| { if let Some(transaction_id) = self.transformation_transaction_id.take() { buffer.undo_transaction(transaction_id, cx); @@ -3143,7 +3233,7 @@ impl CodegenAlternative { fn apply_edits( &mut self, edits: impl IntoIterator, String)>, - cx: &mut ModelContext, + cx: &mut Context, ) { let transaction = self.buffer.update(cx, |buffer, cx| { // Avoid grouping assistant edits with user edits. @@ -3170,7 +3260,7 @@ impl CodegenAlternative { fn reapply_line_based_diff( &mut self, line_operations: impl IntoIterator, - cx: &mut ModelContext, + cx: &mut Context, ) { let old_snapshot = self.snapshot.clone(); let old_range = self.range.to_point(&old_snapshot); @@ -3226,7 +3316,7 @@ impl CodegenAlternative { } } - fn reapply_batch_diff(&mut self, cx: &mut ModelContext) -> Task<()> { + fn reapply_batch_diff(&mut self, cx: &mut Context) -> Task<()> { let old_snapshot = self.snapshot.clone(); let old_range = self.range.to_point(&old_snapshot); let new_snapshot = self.buffer.read(cx).snapshot(cx); @@ -3445,8 +3535,8 @@ where } struct AssistantCodeActionProvider { - editor: WeakView, - workspace: WeakView, + editor: WeakEntity, + workspace: WeakEntity, } const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant"; @@ -3458,9 +3548,10 @@ impl CodeActionProvider for AssistantCodeActionProvider { fn code_actions( &self, - buffer: &Model, + buffer: &Entity, range: Range, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task>> { if !AssistantSettings::get_global(cx).enabled { return Task::ready(Ok(Vec::new())); @@ -3509,15 +3600,16 @@ impl CodeActionProvider for AssistantCodeActionProvider { fn apply_code_action( &self, - buffer: Model, + buffer: Entity, action: CodeAction, excerpt_id: ExcerptId, _push_to_history: bool, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task> { let editor = self.editor.clone(); let workspace = self.workspace.clone(); - cx.spawn(|mut cx| async move { + window.spawn(cx, |mut cx| async move { let editor = editor.upgrade().context("editor was released")?; let range = editor .update(&mut cx, |editor, cx| { @@ -3561,7 +3653,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { .context("assistant panel was released") })??; - cx.update_global(|assistant: &mut InlineAssistant, cx| { + cx.update_global(|assistant: &mut InlineAssistant, window, cx| { let assist_id = assistant.suggest_assist( &editor, range, @@ -3570,9 +3662,10 @@ impl CodeActionProvider for AssistantCodeActionProvider { true, Some(workspace), Some(&assistant_panel), + window, cx, ); - assistant.start_assist(assist_id, cx); + assistant.start_assist(assist_id, window, cx); })?; Ok(ProjectTransaction::default()) @@ -3610,7 +3703,7 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { mod tests { use super::*; use futures::stream::{self}; - use gpui::{Context, TestAppContext}; + use gpui::TestAppContext; use indoc::indoc; use language::{ language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher, @@ -3641,15 +3734,14 @@ mod tests { } } "}; - let buffer = - cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -3705,15 +3797,14 @@ mod tests { le } "}; - let buffer = - cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -3772,15 +3863,14 @@ mod tests { " \n", "}\n" // ); - let buffer = - cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -3839,14 +3929,14 @@ mod tests { \t} } "}; - let buffer = cx.new_model(|cx| Buffer::local(text, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -3893,15 +3983,14 @@ mod tests { let x = 0; } "}; - let buffer = - cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -3987,7 +4076,7 @@ mod tests { } fn simulate_response_stream( - codegen: Model, + codegen: Entity, cx: &mut TestAppContext, ) -> mpsc::UnboundedSender { let (chunks_tx, chunks_rx) = mpsc::unbounded(); diff --git a/crates/assistant/src/slash_command_settings.rs b/crates/assistant/src/slash_command_settings.rs index 5918769d711c3f..25d575ed534b37 100644 --- a/crates/assistant/src/slash_command_settings.rs +++ b/crates/assistant/src/slash_command_settings.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use gpui::AppContext; +use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -36,7 +36,7 @@ impl Settings for SlashCommandSettings { type FileContent = Self; - fn load(sources: SettingsSources, _cx: &mut AppContext) -> Result { + fn load(sources: SettingsSources, _cx: &mut App) -> Result { SettingsSources::::json_merge_with( [sources.default] .into_iter() diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index b55a731c47d4fa..4547ea8e679e44 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -1,7 +1,6 @@ -use crate::{ - humanize_token_count, AssistantPanel, AssistantPanelEvent, RequestType, DEFAULT_CONTEXT_LINES, -}; +use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES}; use anyhow::{Context as _, Result}; +use assistant_context_editor::{humanize_token_count, RequestType}; use assistant_settings::AssistantSettings; use client::telemetry::Telemetry; use collections::{HashMap, VecDeque}; @@ -12,8 +11,8 @@ use editor::{ use fs::Fs; use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ - AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext, - Subscription, Task, TextStyle, UpdateGlobal, View, WeakView, + App, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, Task, + TextStyle, UpdateGlobal, WeakEntity, }; use language::Buffer; use language_model::{ @@ -40,7 +39,7 @@ pub fn init( fs: Arc, prompt_builder: Arc, telemetry: Arc, - cx: &mut AppContext, + cx: &mut App, ) { cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry)); } @@ -87,20 +86,20 @@ impl TerminalInlineAssistant { pub fn assist( &mut self, - terminal_view: &View, - workspace: Option>, - assistant_panel: Option<&View>, + terminal_view: &Entity, + workspace: Option>, + assistant_panel: Option<&Entity>, initial_prompt: Option, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let terminal = terminal_view.read(cx).terminal().clone(); let assist_id = self.next_assist_id.post_inc(); - let prompt_buffer = - cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)); - let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx)); - let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone())); + let prompt_buffer = cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)); + let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx)); + let codegen = cx.new(|_| Codegen::new(terminal, self.telemetry.clone())); - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { PromptEditor::new( assist_id, self.prompt_history.clone(), @@ -109,6 +108,7 @@ impl TerminalInlineAssistant { assistant_panel, workspace.clone(), self.fs.clone(), + window, cx, ) }); @@ -118,7 +118,7 @@ impl TerminalInlineAssistant { render: Box::new(move |_| prompt_editor_render.clone().into_any_element()), }; terminal_view.update(cx, |terminal_view, cx| { - terminal_view.set_block_below_cursor(block, cx); + terminal_view.set_block_below_cursor(block, window, cx); }); let terminal_assistant = TerminalInlineAssist::new( @@ -127,21 +127,27 @@ impl TerminalInlineAssistant { assistant_panel.is_some(), prompt_editor, workspace.clone(), + window, cx, ); self.assists.insert(assist_id, terminal_assistant); - self.focus_assist(assist_id, cx); + self.focus_assist(assist_id, window, cx); } - fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { + fn focus_assist( + &mut self, + assist_id: TerminalInlineAssistId, + window: &mut Window, + cx: &mut App, + ) { let assist = &self.assists[&assist_id]; if let Some(prompt_editor) = assist.prompt_editor.as_ref() { prompt_editor.update(cx, |this, cx| { this.editor.update(cx, |editor, cx| { - editor.focus(cx); - editor.select_all(&SelectAll, cx); + window.focus(&editor.focus_handle(cx)); + editor.select_all(&SelectAll, window, cx); }); }); } @@ -149,9 +155,10 @@ impl TerminalInlineAssistant { fn handle_prompt_editor_event( &mut self, - prompt_editor: View, + prompt_editor: Entity, event: &PromptEditorEvent, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let assist_id = prompt_editor.read(cx).id; match event { @@ -162,21 +169,21 @@ impl TerminalInlineAssistant { self.stop_assist(assist_id, cx); } PromptEditorEvent::ConfirmRequested { execute } => { - self.finish_assist(assist_id, false, *execute, cx); + self.finish_assist(assist_id, false, *execute, window, cx); } PromptEditorEvent::CancelRequested => { - self.finish_assist(assist_id, true, false, cx); + self.finish_assist(assist_id, true, false, window, cx); } PromptEditorEvent::DismissRequested => { - self.dismiss_assist(assist_id, cx); + self.dismiss_assist(assist_id, window, cx); } PromptEditorEvent::Resized { height_in_lines } => { - self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx); + self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx); } } } - fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { + fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -214,7 +221,7 @@ impl TerminalInlineAssistant { codegen.update(cx, |codegen, cx| codegen.start(request, cx)); } - fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { + fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -227,7 +234,7 @@ impl TerminalInlineAssistant { fn request_for_inline_assist( &self, assist_id: TerminalInlineAssistId, - cx: &mut WindowContext, + cx: &mut App, ) -> Result { let assist = self.assists.get(&assist_id).context("invalid assist")?; @@ -235,7 +242,7 @@ impl TerminalInlineAssistant { let (latest_output, working_directory) = assist .terminal .update(cx, |terminal, cx| { - let terminal = terminal.model().read(cx); + let terminal = terminal.entity().read(cx); let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES); let working_directory = terminal .working_directory() @@ -297,16 +304,17 @@ impl TerminalInlineAssistant { assist_id: TerminalInlineAssistId, undo: bool, execute: bool, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { - self.dismiss_assist(assist_id, cx); + self.dismiss_assist(assist_id, window, cx); if let Some(assist) = self.assists.remove(&assist_id) { assist .terminal .update(cx, |this, cx| { this.clear_block_below_cursor(cx); - this.focus_handle(cx).focus(cx); + this.focus_handle(cx).focus(window); }) .log_err(); @@ -349,7 +357,8 @@ impl TerminalInlineAssistant { fn dismiss_assist( &mut self, assist_id: TerminalInlineAssistId, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> bool { let Some(assist) = self.assists.get_mut(&assist_id) else { return false; @@ -362,7 +371,7 @@ impl TerminalInlineAssistant { .terminal .update(cx, |this, cx| { this.clear_block_below_cursor(cx); - this.focus_handle(cx).focus(cx); + this.focus_handle(cx).focus(window); }) .is_ok() } @@ -371,7 +380,8 @@ impl TerminalInlineAssistant { &mut self, assist_id: TerminalInlineAssistId, height: u8, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { if let Some(assist) = self.assists.get_mut(&assist_id) { if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() { @@ -383,7 +393,7 @@ impl TerminalInlineAssistant { height, render: Box::new(move |_| prompt_editor.clone().into_any_element()), }; - terminal.set_block_below_cursor(block, cx); + terminal.set_block_below_cursor(block, window, cx); }) .log_err(); } @@ -392,10 +402,10 @@ impl TerminalInlineAssistant { } struct TerminalInlineAssist { - terminal: WeakView, - prompt_editor: Option>, - codegen: Model, - workspace: Option>, + terminal: WeakEntity, + prompt_editor: Option>, + codegen: Entity, + workspace: Option>, include_context: bool, _subscriptions: Vec, } @@ -403,11 +413,12 @@ struct TerminalInlineAssist { impl TerminalInlineAssist { pub fn new( assist_id: TerminalInlineAssistId, - terminal: &View, + terminal: &Entity, include_context: bool, - prompt_editor: View, - workspace: Option>, - cx: &mut WindowContext, + prompt_editor: Entity, + workspace: Option>, + window: &mut Window, + cx: &mut App, ) -> Self { let codegen = prompt_editor.read(cx).codegen.clone(); Self { @@ -417,12 +428,12 @@ impl TerminalInlineAssist { workspace: workspace.clone(), include_context, _subscriptions: vec![ - cx.subscribe(&prompt_editor, |prompt_editor, event, cx| { + window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| { TerminalInlineAssistant::update_global(cx, |this, cx| { - this.handle_prompt_editor_event(prompt_editor, event, cx) + this.handle_prompt_editor_event(prompt_editor, event, window, cx) }) }), - cx.subscribe(&codegen, move |codegen, event, cx| { + window.subscribe(&codegen, cx, move |codegen, event, window, cx| { TerminalInlineAssistant::update_global(cx, |this, cx| match event { CodegenEvent::Finished => { let assist = if let Some(assist) = this.assists.get(&assist_id) { @@ -455,7 +466,7 @@ impl TerminalInlineAssist { } if assist.prompt_editor.is_none() { - this.finish_assist(assist_id, false, false, cx); + this.finish_assist(assist_id, false, false, window, cx); } } }) @@ -477,25 +488,25 @@ enum PromptEditorEvent { struct PromptEditor { id: TerminalInlineAssistId, height_in_lines: u8, - editor: View, - language_model_selector: View, + editor: Entity, + language_model_selector: Entity, edited_since_done: bool, prompt_history: VecDeque, prompt_history_ix: Option, pending_prompt: String, - codegen: Model, + codegen: Entity, _codegen_subscription: Subscription, editor_subscriptions: Vec, pending_token_count: Task>, token_count: Option, _token_count_subscriptions: Vec, - workspace: Option>, + workspace: Option>, } impl EventEmitter for PromptEditor {} impl Render for PromptEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let status = &self.codegen.read(cx).status; let buttons = match status { CodegenStatus::Idle => { @@ -503,16 +514,20 @@ impl Render for PromptEditor { IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx)) + .tooltip(|window, cx| { + Tooltip::for_action("Cancel Assist", &menu::Cancel, window, cx) + }) .on_click( - cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), ), IconButton::new("start", IconName::SparkleAlt) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx)) + .tooltip(|window, cx| { + Tooltip::for_action("Generate", &menu::Confirm, window, cx) + }) .on_click( - cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)), + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)), ), ] } @@ -521,23 +536,24 @@ impl Render for PromptEditor { IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::text("Cancel Assist", cx)) + .tooltip(Tooltip::text("Cancel Assist")) .on_click( - cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), ), IconButton::new("stop", IconName::Stop) .icon_color(Color::Error) .shape(IconButtonShape::Square) - .tooltip(|cx| { + .tooltip(|window, cx| { Tooltip::with_meta( "Interrupt Generation", Some(&menu::Cancel), "Changes won't be discarded", + window, cx, ) }) .on_click( - cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)), + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)), ), ] } @@ -545,8 +561,12 @@ impl Render for PromptEditor { let cancel = IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx)) - .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested))); + .tooltip(|window, cx| { + Tooltip::for_action("Cancel Assist", &menu::Cancel, window, cx) + }) + .on_click( + cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), + ); let has_error = matches!(status, CodegenStatus::Error(_)); if has_error || self.edited_since_done { @@ -555,15 +575,16 @@ impl Render for PromptEditor { IconButton::new("restart", IconName::RotateCw) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(|cx| { + .tooltip(|window, cx| { Tooltip::with_meta( "Restart Generation", Some(&menu::Confirm), "Changes will be discarded", + window, cx, ) }) - .on_click(cx.listener(|_, _, cx| { + .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::StartRequested); })), ] @@ -573,23 +594,29 @@ impl Render for PromptEditor { IconButton::new("accept", IconName::Check) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(|cx| { - Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx) + .tooltip(|window, cx| { + Tooltip::for_action( + "Accept Generated Command", + &menu::Confirm, + window, + cx, + ) }) - .on_click(cx.listener(|_, _, cx| { + .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::ConfirmRequested { execute: false }); })), IconButton::new("confirm", IconName::Play) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(|cx| { + .tooltip(|window, cx| { Tooltip::for_action( "Execute Generated Command", &menu::SecondaryConfirm, + window, cx, ) }) - .on_click(cx.listener(|_, _, cx| { + .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::ConfirmRequested { execute: true }); })), ] @@ -620,7 +647,7 @@ impl Render for PromptEditor { .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(Color::Muted) - .tooltip(move |cx| { + .tooltip(move |window, cx| { Tooltip::with_meta( format!( "Using {}", @@ -631,6 +658,7 @@ impl Render for PromptEditor { ), None, "Change Model", + window, cx, ) }), @@ -641,7 +669,7 @@ impl Render for PromptEditor { Some( div() .id("error") - .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) + .tooltip(Tooltip::text(error_message)) .child( Icon::new(IconName::XCircle) .size(IconSize::Small) @@ -664,8 +692,8 @@ impl Render for PromptEditor { } } -impl FocusableView for PromptEditor { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for PromptEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) } } @@ -677,14 +705,15 @@ impl PromptEditor { fn new( id: TerminalInlineAssistId, prompt_history: VecDeque, - prompt_buffer: Model, - codegen: Model, - assistant_panel: Option<&View>, - workspace: Option>, + prompt_buffer: Entity, + codegen: Entity, + assistant_panel: Option<&Entity>, + workspace: Option>, fs: Arc, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { let mut editor = Editor::new( EditorMode::AutoHeight { max_lines: Self::MAX_LINES as usize, @@ -692,24 +721,28 @@ impl PromptEditor { prompt_buffer, None, false, + window, cx, ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor.set_placeholder_text(Self::placeholder_text(cx), cx); + editor.set_placeholder_text(Self::placeholder_text(window), cx); editor }); let mut token_count_subscriptions = Vec::new(); if let Some(assistant_panel) = assistant_panel { - token_count_subscriptions - .push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event)); + token_count_subscriptions.push(cx.subscribe_in( + assistant_panel, + window, + Self::handle_assistant_panel_event, + )); } let mut this = Self { id, height_in_lines: 1, editor: prompt_editor, - language_model_selector: cx.new_view(|cx| { + language_model_selector: cx.new(|cx| { let fs = fs.clone(); LanguageModelSelector::new( move |model, cx| { @@ -719,6 +752,7 @@ impl PromptEditor { move |settings, _| settings.set_model(model.clone()), ); }, + window, cx, ) }), @@ -726,7 +760,7 @@ impl PromptEditor { prompt_history, prompt_history_ix: None, pending_prompt: String::new(), - _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed), + _codegen_subscription: cx.observe_in(&codegen, window, Self::handle_codegen_changed), editor_subscriptions: Vec::new(), codegen, pending_token_count: Task::ready(Ok(())), @@ -740,15 +774,15 @@ impl PromptEditor { this } - fn placeholder_text(cx: &WindowContext) -> String { - let context_keybinding = text_for_action(&crate::ToggleFocus, cx) + fn placeholder_text(window: &Window) -> String { + let context_keybinding = text_for_action(&zed_actions::assistant::ToggleFocus, window) .map(|keybinding| format!(" • {keybinding} for context")) .unwrap_or_default(); format!("Generate…{context_keybinding} • ↓↑ for history") } - fn subscribe_to_editor(&mut self, cx: &mut ViewContext) { + fn subscribe_to_editor(&mut self, cx: &mut Context) { self.editor_subscriptions.clear(); self.editor_subscriptions .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed)); @@ -756,11 +790,11 @@ impl PromptEditor { .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events)); } - fn prompt(&self, cx: &AppContext) -> String { + fn prompt(&self, cx: &App) -> String { self.editor.read(cx).text(cx) } - fn count_lines(&mut self, cx: &mut ViewContext) { + fn count_lines(&mut self, cx: &mut Context) { let height_in_lines = cmp::max( 2, // Make the editor at least two lines tall, to account for padding and buttons. cmp::min( @@ -778,15 +812,16 @@ impl PromptEditor { fn handle_assistant_panel_event( &mut self, - _: View, + _: &Entity, event: &AssistantPanelEvent, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) { let AssistantPanelEvent::ContextEdited { .. } = event; self.count_tokens(cx); } - fn count_tokens(&mut self, cx: &mut ViewContext) { + fn count_tokens(&mut self, cx: &mut Context) { let assist_id = self.id; let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { return; @@ -806,15 +841,15 @@ impl PromptEditor { }) } - fn handle_prompt_editor_changed(&mut self, _: View, cx: &mut ViewContext) { + fn handle_prompt_editor_changed(&mut self, _: Entity, cx: &mut Context) { self.count_lines(cx); } fn handle_prompt_editor_events( &mut self, - _: View, + _: Entity, event: &EditorEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { match event { EditorEvent::Edited { .. } => { @@ -837,7 +872,12 @@ impl PromptEditor { } } - fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { + fn handle_codegen_changed( + &mut self, + _: Entity, + _: &mut Window, + cx: &mut Context, + ) { match &self.codegen.read(cx).status { CodegenStatus::Idle => { self.editor @@ -855,7 +895,7 @@ impl PromptEditor { } } - fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { + fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context) { match &self.codegen.read(cx).status { CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => { cx.emit(PromptEditorEvent::CancelRequested); @@ -866,7 +906,7 @@ impl PromptEditor { } } - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context) { match &self.codegen.read(cx).status { CodegenStatus::Idle => { if !self.editor.read(cx).text(cx).trim().is_empty() { @@ -889,53 +929,58 @@ impl PromptEditor { } } - fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { + fn secondary_confirm( + &mut self, + _: &menu::SecondaryConfirm, + _: &mut Window, + cx: &mut Context, + ) { if matches!(self.codegen.read(cx).status, CodegenStatus::Done) { cx.emit(PromptEditorEvent::ConfirmRequested { execute: true }); } } - fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { if let Some(ix) = self.prompt_history_ix { if ix > 0 { self.prompt_history_ix = Some(ix - 1); let prompt = self.prompt_history[ix - 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_beginning(&Default::default(), cx); + editor.set_text(prompt, window, cx); + editor.move_to_beginning(&Default::default(), window, cx); }); } } else if !self.prompt_history.is_empty() { self.prompt_history_ix = Some(self.prompt_history.len() - 1); let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_beginning(&Default::default(), cx); + editor.set_text(prompt, window, cx); + editor.move_to_beginning(&Default::default(), window, cx); }); } } - fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { if let Some(ix) = self.prompt_history_ix { if ix < self.prompt_history.len() - 1 { self.prompt_history_ix = Some(ix + 1); let prompt = self.prompt_history[ix + 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_end(&Default::default(), cx) + editor.set_text(prompt, window, cx); + editor.move_to_end(&Default::default(), window, cx) }); } else { self.prompt_history_ix = None; let prompt = self.pending_prompt.as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_end(&Default::default(), cx) + editor.set_text(prompt, window, cx); + editor.move_to_end(&Default::default(), window, cx) }); } } } - fn render_token_count(&self, cx: &mut ViewContext) -> Option { + fn render_token_count(&self, cx: &mut Context) -> Option { let model = LanguageModelRegistry::read_global(cx).active_model()?; let token_count = self.token_count?; let max_token_count = model.max_token_count(); @@ -965,34 +1010,35 @@ impl PromptEditor { ); if let Some(workspace) = self.workspace.clone() { token_count = token_count - .tooltip(|cx| { + .tooltip(|window, cx| { Tooltip::with_meta( "Tokens Used by Inline Assistant", None, "Click to Open Assistant Panel", + window, cx, ) }) .cursor_pointer() - .on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation()) - .on_click(move |_, cx| { + .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(move |_, window, cx| { cx.stop_propagation(); workspace .update(cx, |workspace, cx| { - workspace.focus_panel::(cx) + workspace.focus_panel::(window, cx) }) .ok(); }); } else { token_count = token_count .cursor_default() - .tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx)); + .tooltip(Tooltip::text("Tokens Used by Inline Assistant")); } Some(token_count) } - fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + fn render_prompt_editor(&self, cx: &mut Context) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if self.editor.read(cx).read_only(cx) { @@ -1030,27 +1076,27 @@ const CLEAR_INPUT: &str = "\x15"; const CARRIAGE_RETURN: &str = "\x0d"; struct TerminalTransaction { - terminal: Model, + terminal: Entity, } impl TerminalTransaction { - pub fn start(terminal: Model) -> Self { + pub fn start(terminal: Entity) -> Self { Self { terminal } } - pub fn push(&mut self, hunk: String, cx: &mut AppContext) { + pub fn push(&mut self, hunk: String, cx: &mut App) { // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal let input = Self::sanitize_input(hunk); self.terminal .update(cx, |terminal, _| terminal.input(input)); } - pub fn undo(&self, cx: &mut AppContext) { + pub fn undo(&self, cx: &mut App) { self.terminal .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string())); } - pub fn complete(&self, cx: &mut AppContext) { + pub fn complete(&self, cx: &mut App) { self.terminal.update(cx, |terminal, _| { terminal.input(CARRIAGE_RETURN.to_string()) }); @@ -1064,14 +1110,14 @@ impl TerminalTransaction { pub struct Codegen { status: CodegenStatus, telemetry: Option>, - terminal: Model, + terminal: Entity, generation: Task<()>, message_id: Option, transaction: Option, } impl Codegen { - pub fn new(terminal: Model, telemetry: Option>) -> Self { + pub fn new(terminal: Entity, telemetry: Option>) -> Self { Self { terminal, telemetry, @@ -1082,7 +1128,7 @@ impl Codegen { } } - pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext) { + pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context) { let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { return; }; @@ -1182,20 +1228,20 @@ impl Codegen { cx.notify(); } - pub fn stop(&mut self, cx: &mut ModelContext) { + pub fn stop(&mut self, cx: &mut Context) { self.status = CodegenStatus::Done; self.generation = Task::ready(()); cx.emit(CodegenEvent::Finished); cx.notify(); } - pub fn complete(&mut self, cx: &mut ModelContext) { + pub fn complete(&mut self, cx: &mut Context) { if let Some(transaction) = self.transaction.take() { transaction.complete(cx); } } - pub fn undo(&mut self, cx: &mut ModelContext) { + pub fn undo(&mut self, cx: &mut Context) { if let Some(transaction) = self.transaction.take() { transaction.undo(cx); } diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index e28526bcea050c..f53595cd0c067f 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -20,7 +20,9 @@ test-support = [ [dependencies] anyhow.workspace = true +assistant_context_editor.workspace = true assistant_settings.workspace = true +assistant_slash_command.workspace = true assistant_tool.workspace = true async-watch.workspace = true chrono.workspace = true @@ -37,6 +39,7 @@ fs.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true +heed.workspace = true html_to_markdown.workspace = true http_client.workspace = true itertools.workspace = true @@ -50,6 +53,7 @@ markdown.workspace = true menu.workspace = true multi_buffer.workspace = true parking_lot.workspace = true +paths.workspace = true picker.workspace = true project.workspace = true prompt_library.workspace = true @@ -69,7 +73,6 @@ theme.workspace = true time.workspace = true time_format.workspace = true ui.workspace = true -unindent.workspace = true util.workspace = true uuid.workspace = true workspace.workspace = true diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs index 1bfd607866dfe8..f479f755a1e770 100644 --- a/crates/assistant2/src/active_thread.rs +++ b/crates/assistant2/src/active_thread.rs @@ -3,9 +3,9 @@ use std::sync::Arc; use assistant_tool::ToolWorkingSet; use collections::HashMap; use gpui::{ - list, AbsoluteLength, AnyElement, AppContext, DefiniteLength, EdgesRefinement, Empty, Length, - ListAlignment, ListOffset, ListState, Model, StyleRefinement, Subscription, - TextStyleRefinement, UnderlineStyle, View, WeakView, + list, AbsoluteLength, AnyElement, App, DefiniteLength, EdgesRefinement, Empty, Entity, Length, + ListAlignment, ListOffset, ListState, StyleRefinement, Subscription, TextStyleRefinement, + UnderlineStyle, WeakEntity, }; use language::LanguageRegistry; use language_model::Role; @@ -16,43 +16,48 @@ use ui::prelude::*; use workspace::Workspace; use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent}; +use crate::thread_store::ThreadStore; use crate::ui::ContextPill; pub struct ActiveThread { - workspace: WeakView, + workspace: WeakEntity, language_registry: Arc, tools: Arc, - pub(crate) thread: Model, + thread_store: Entity, + thread: Entity, messages: Vec, list_state: ListState, - rendered_messages_by_id: HashMap>, + rendered_messages_by_id: HashMap>, last_error: Option, _subscriptions: Vec, } impl ActiveThread { pub fn new( - thread: Model, - workspace: WeakView, + thread: Entity, + thread_store: Entity, + workspace: WeakEntity, language_registry: Arc, tools: Arc, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { let subscriptions = vec![ cx.observe(&thread, |_, _, cx| cx.notify()), - cx.subscribe(&thread, Self::handle_thread_event), + cx.subscribe_in(&thread, window, Self::handle_thread_event), ]; let mut this = Self { workspace, language_registry, tools, + thread_store, thread: thread.clone(), messages: Vec::new(), rendered_messages_by_id: HashMap::default(), list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), { - let this = cx.view().downgrade(); - move |ix, cx: &mut WindowContext| { + let this = cx.entity().downgrade(); + move |ix, _: &mut Window, cx: &mut App| { this.update(cx, |this, cx| this.render_message(ix, cx)) .unwrap() } @@ -62,25 +67,29 @@ impl ActiveThread { }; for message in thread.read(cx).messages().cloned().collect::>() { - this.push_message(&message.id, message.text.clone(), cx); + this.push_message(&message.id, message.text.clone(), window, cx); } this } + pub fn thread(&self) -> &Entity { + &self.thread + } + pub fn is_empty(&self) -> bool { self.messages.is_empty() } - pub fn summary(&self, cx: &AppContext) -> Option { + pub fn summary(&self, cx: &App) -> Option { self.thread.read(cx).summary() } - pub fn summary_or_default(&self, cx: &AppContext) -> SharedString { + pub fn summary_or_default(&self, cx: &App) -> SharedString { self.thread.read(cx).summary_or_default() } - pub fn cancel_last_completion(&mut self, cx: &mut AppContext) -> bool { + pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool { self.last_error.take(); self.thread .update(cx, |thread, _cx| thread.cancel_last_completion()) @@ -94,7 +103,13 @@ impl ActiveThread { self.last_error.take(); } - fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext) { + fn push_message( + &mut self, + id: &MessageId, + text: String, + window: &mut Window, + cx: &mut Context, + ) { let old_len = self.messages.len(); self.messages.push(*id); self.list_state.splice(old_len..old_len, 1); @@ -103,7 +118,7 @@ impl ActiveThread { let colors = cx.theme().colors(); let ui_font_size = TextSize::Default.rems(cx); let buffer_font_size = TextSize::Small.rems(cx); - let mut text_style = cx.text_style(); + let mut text_style = window.text_style(); text_style.refine(&TextStyleRefinement { font_family: Some(theme_settings.ui_font.family.clone()), @@ -162,12 +177,13 @@ impl ActiveThread { ..Default::default() }; - let markdown = cx.new_view(|cx| { + let markdown = cx.new(|cx| { Markdown::new( text, markdown_style, Some(self.language_registry.clone()), None, + window, cx, ) }); @@ -180,20 +196,26 @@ impl ActiveThread { fn handle_thread_event( &mut self, - _: Model, + _: &Entity, event: &ThreadEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { ThreadEvent::ShowError(error) => { self.last_error = Some(error.clone()); } - ThreadEvent::StreamedCompletion => {} - ThreadEvent::SummaryChanged => {} + ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => { + self.thread_store + .update(cx, |thread_store, cx| { + thread_store.save_thread(&self.thread, cx) + }) + .detach_and_log_err(cx); + } ThreadEvent::StreamedAssistantText(message_id, text) => { if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { markdown.update(cx, |markdown, cx| { - markdown.append(text, cx); + markdown.append(text, window, cx); }); } } @@ -204,9 +226,15 @@ impl ActiveThread { .message(*message_id) .map(|message| message.text.clone()) { - self.push_message(message_id, message_text, cx); + self.push_message(message_id, message_text, window, cx); } + self.thread_store + .update(cx, |thread_store, cx| { + thread_store.save_thread(&self.thread, cx) + }) + .detach_and_log_err(cx); + cx.notify(); } ThreadEvent::UsePendingTools => { @@ -221,7 +249,7 @@ impl ActiveThread { for tool_use in pending_tool_uses { if let Some(tool) = self.tools.tool(&tool_use.name, cx) { - let task = tool.run(tool_use.input, self.workspace.clone(), cx); + let task = tool.run(tool_use.input, self.workspace.clone(), window, cx); self.thread.update(cx, |thread, cx| { thread.insert_tool_output( @@ -238,7 +266,7 @@ impl ActiveThread { } } - fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + fn render_message(&self, ix: usize, cx: &mut Context) -> AnyElement { let message_id = self.messages[ix]; let Some(message) = self.thread.read(cx).message(message_id) else { return Empty.into_any(); @@ -259,7 +287,7 @@ impl ActiveThread { h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children( context .into_iter() - .map(|context| ContextPill::new_added(context, false, false, None)), + .map(|context| ContextPill::added(context, false, false, None)), ), ) } else { @@ -270,7 +298,7 @@ impl ActiveThread { let styled_message = match message.role { Role::User => v_flex() .id(("message-container", ix)) - .py_1() + .pt_2p5() .px_2p5() .child( v_flex() @@ -319,10 +347,9 @@ impl ActiveThread { } impl Render for ActiveThread { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { v_flex() .size_full() - .pt_1p5() .child(list(self.list_state.clone()).flex_grow()) } } diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index bf35d35765eb04..0d1303ea90d51f 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,4 +1,5 @@ mod active_thread; +mod assistant_configuration; mod assistant_model_selector; mod assistant_panel; mod buffer_codegen; @@ -23,23 +24,24 @@ use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; use fs::Fs; -use gpui::{actions, AppContext}; -use prompt_library::{PromptBuilder, PromptLoadingParams}; +use gpui::{actions, App}; +use prompt_library::PromptBuilder; use settings::Settings as _; -use util::ResultExt; -pub use crate::assistant_panel::AssistantPanel; +pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}; pub use crate::inline_assistant::InlineAssistant; actions!( assistant2, [ - ToggleFocus, NewThread, + NewPromptEditor, ToggleContextPicker, ToggleModelSelector, RemoveAllContext, OpenHistory, + OpenPromptEditorHistory, + OpenConfiguration, RemoveSelectedThread, Chat, ChatMode, @@ -57,20 +59,15 @@ actions!( const NAMESPACE: &str = "assistant2"; /// Initializes the `assistant2` crate. -pub fn init(fs: Arc, client: Arc, stdout_is_a_pty: bool, cx: &mut AppContext) { +pub fn init( + fs: Arc, + client: Arc, + prompt_builder: Arc, + cx: &mut App, +) { AssistantSettings::register(cx); assistant_panel::init(cx); - let prompt_builder = PromptBuilder::new(Some(PromptLoadingParams { - fs: fs.clone(), - repo_path: stdout_is_a_pty - .then(|| std::env::current_dir().log_err()) - .flatten(), - cx, - })) - .log_err() - .map(Arc::new) - .unwrap_or_else(|| Arc::new(PromptBuilder::new(None).unwrap())); inline_assistant::init( fs.clone(), prompt_builder.clone(), @@ -87,7 +84,7 @@ pub fn init(fs: Arc, client: Arc, stdout_is_a_pty: bool, cx: &mu feature_gate_assistant2_actions(cx); } -fn feature_gate_assistant2_actions(cx: &mut AppContext) { +fn feature_gate_assistant2_actions(cx: &mut App) { CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_namespace(NAMESPACE); }); diff --git a/crates/assistant2/src/assistant_configuration.rs b/crates/assistant2/src/assistant_configuration.rs new file mode 100644 index 00000000000000..efecfe729ae48a --- /dev/null +++ b/crates/assistant2/src/assistant_configuration.rs @@ -0,0 +1,173 @@ +use std::sync::Arc; + +use collections::HashMap; +use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription}; +use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; +use ui::{prelude::*, ElevationIndex}; +use zed_actions::assistant::DeployPromptLibrary; + +pub struct AssistantConfiguration { + focus_handle: FocusHandle, + configuration_views_by_provider: HashMap, + _registry_subscription: Subscription, +} + +impl AssistantConfiguration { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + + let registry_subscription = cx.subscribe_in( + &LanguageModelRegistry::global(cx), + window, + |this, _, event: &language_model::Event, window, cx| match event { + language_model::Event::AddedProvider(provider_id) => { + let provider = LanguageModelRegistry::read_global(cx).provider(provider_id); + if let Some(provider) = provider { + this.add_provider_configuration_view(&provider, window, cx); + } + } + language_model::Event::RemovedProvider(provider_id) => { + this.remove_provider_configuration_view(provider_id); + } + _ => {} + }, + ); + + let mut this = Self { + focus_handle, + configuration_views_by_provider: HashMap::default(), + _registry_subscription: registry_subscription, + }; + this.build_provider_configuration_views(window, cx); + this + } + + fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context) { + let providers = LanguageModelRegistry::read_global(cx).providers(); + for provider in providers { + self.add_provider_configuration_view(&provider, window, cx); + } + } + + fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) { + self.configuration_views_by_provider.remove(provider_id); + } + + fn add_provider_configuration_view( + &mut self, + provider: &Arc, + window: &mut Window, + cx: &mut Context, + ) { + let configuration_view = provider.configuration_view(window, cx); + self.configuration_views_by_provider + .insert(provider.id(), configuration_view); + } +} + +impl Focusable for AssistantConfiguration { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +pub enum AssistantConfigurationEvent { + NewThread(Arc), +} + +impl EventEmitter for AssistantConfiguration {} + +impl AssistantConfiguration { + fn render_provider_configuration( + &mut self, + provider: &Arc, + cx: &mut Context, + ) -> impl IntoElement { + let provider_id = provider.id().0.clone(); + let provider_name = provider.name().0.clone(); + let configuration_view = self + .configuration_views_by_provider + .get(&provider.id()) + .cloned(); + + v_flex() + .gap_2() + .child( + h_flex() + .justify_between() + .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small)) + .when(provider.is_authenticated(cx), |parent| { + parent.child( + h_flex().justify_end().child( + Button::new( + SharedString::from(format!("new-thread-{provider_id}")), + "Open New Thread", + ) + .icon_position(IconPosition::Start) + .icon(IconName::Plus) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .on_click(cx.listener({ + let provider = provider.clone(); + move |_this, _event, _window, cx| { + cx.emit(AssistantConfigurationEvent::NewThread( + provider.clone(), + )) + } + })), + ), + ) + }), + ) + .child( + div() + .p(DynamicSpacing::Base08.rems(cx)) + .bg(cx.theme().colors().surface_background) + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .map(|parent| match configuration_view { + Some(configuration_view) => parent.child(configuration_view), + None => parent.child(div().child(Label::new(format!( + "No configuration view for {provider_name}", + )))), + }), + ) + } +} + +impl Render for AssistantConfiguration { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let providers = LanguageModelRegistry::read_global(cx).providers(); + + v_flex() + .id("assistant-configuration") + .track_focus(&self.focus_handle(cx)) + .bg(cx.theme().colors().editor_background) + .size_full() + .overflow_y_scroll() + .child( + h_flex().p(DynamicSpacing::Base16.rems(cx)).child( + Button::new("open-prompt-library", "Open Prompt Library") + .style(ButtonStyle::Filled) + .full_width() + .icon(IconName::Book) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .on_click(|_event, _window, cx| cx.dispatch_action(&DeployPromptLibrary)), + ), + ) + .child( + v_flex() + .p(DynamicSpacing::Base16.rems(cx)) + .mt_1() + .gap_6() + .flex_1() + .children( + providers + .into_iter() + .map(|provider| self.render_provider_configuration(&provider, cx)), + ), + ) + } +} diff --git a/crates/assistant2/src/assistant_model_selector.rs b/crates/assistant2/src/assistant_model_selector.rs index c31b6bcba38cc0..cca0454bf17ad7 100644 --- a/crates/assistant2/src/assistant_model_selector.rs +++ b/crates/assistant2/src/assistant_model_selector.rs @@ -1,6 +1,6 @@ use assistant_settings::AssistantSettings; use fs::Fs; -use gpui::{FocusHandle, View}; +use gpui::{Entity, FocusHandle, SharedString}; use language_model::LanguageModelRegistry; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use settings::update_settings_file; @@ -10,7 +10,7 @@ use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip}; use crate::ToggleModelSelector; pub struct AssistantModelSelector { - selector: View, + selector: Entity, menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, } @@ -20,10 +20,11 @@ impl AssistantModelSelector { fs: Arc, menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Self { Self { - selector: cx.new_view(|cx| { + selector: cx.new(|cx| { let fs = fs.clone(); LanguageModelSelector::new( move |model, cx| { @@ -33,6 +34,7 @@ impl AssistantModelSelector { move |settings, _cx| settings.set_model(model.clone()), ); }, + window, cx, ) }), @@ -43,9 +45,13 @@ impl AssistantModelSelector { } impl Render for AssistantModelSelector { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let active_model = LanguageModelRegistry::read_global(cx).active_model(); let focus_handle = self.focus_handle.clone(); + let model_name = match active_model { + Some(model) => model.name().0, + _ => SharedString::from("No model selected"), + }; LanguageModelSelectorPopoverMenu::new( self.selector.clone(), @@ -55,23 +61,13 @@ impl Render for AssistantModelSelector { h_flex() .gap_0p5() .child( - div() - .overflow_x_hidden() - .flex_grow() - .whitespace_nowrap() - .child(match active_model { - Some(model) => h_flex() - .child( - Label::new(model.name().0) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element(), - _ => Label::new("No model selected") - .size(LabelSize::Small) - .color(Color::Muted) - .into_any_element(), - }), + div().max_w_32().child( + Label::new(model_name) + .size(LabelSize::Small) + .color(Color::Muted) + .text_ellipsis() + .into_any_element(), + ), ) .child( Icon::new(IconName::ChevronDown) @@ -79,8 +75,14 @@ impl Render for AssistantModelSelector { .size(IconSize::XSmall), ), ) - .tooltip(move |cx| { - Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) + .tooltip(move |window, cx| { + Tooltip::for_action_in( + "Change Model", + &ToggleModelSelector, + &focus_handle, + window, + cx, + ) }), ) .with_handle(self.menu_handle.clone()) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index e2f14264d3c2da..926fd4cdd6a0cf 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1,46 +1,77 @@ +use std::path::PathBuf; use std::sync::Arc; -use anyhow::Result; +use anyhow::{anyhow, Result}; +use assistant_context_editor::{ + make_lsp_adapter_delegate, AssistantPanelDelegate, ConfigurationError, ContextEditor, + ContextHistory, SlashCommandCompletionProvider, +}; use assistant_settings::{AssistantDockPosition, AssistantSettings}; +use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; + use client::zed_urls; +use editor::Editor; use fs::Fs; use gpui::{ - prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, - FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView, - WindowContext, + prelude::*, px, svg, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter, + FocusHandle, Focusable, FontWeight, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, }; use language::LanguageRegistry; -use settings::Settings; +use language_model::{LanguageModelProviderTosView, LanguageModelRegistry}; +use project::Project; +use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary}; +use settings::{update_settings_file, Settings}; use time::UtcOffset; -use ui::{prelude::*, KeyBinding, Tab, Tooltip}; +use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip}; +use util::ResultExt as _; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; +use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus}; use crate::active_thread::ActiveThread; +use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent}; use crate::message_editor::MessageEditor; use crate::thread::{Thread, ThreadError, ThreadId}; use crate::thread_history::{PastThread, ThreadHistory}; use crate::thread_store::ThreadStore; -use crate::{NewThread, OpenHistory, ToggleFocus}; +use crate::{ + InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory, + OpenPromptEditorHistory, +}; -pub fn init(cx: &mut AppContext) { - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { +pub fn init(cx: &mut App) { + cx.observe_new( + |workspace: &mut Workspace, _window, _cx: &mut Context| { workspace - .register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); + .register_action(|workspace, _: &NewThread, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| panel.new_thread(window, cx)); + workspace.focus_panel::(window, cx); + } }) - .register_action(|workspace, _: &NewThread, cx| { + .register_action(|workspace, _: &OpenHistory, window, cx| { if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| panel.new_thread(cx)); - workspace.focus_panel::(cx); + workspace.focus_panel::(window, cx); + panel.update(cx, |panel, cx| panel.open_history(window, cx)); } }) - .register_action(|workspace, _: &OpenHistory, cx| { + .register_action(|workspace, _: &NewPromptEditor, window, cx| { if let Some(panel) = workspace.panel::(cx) { - workspace.focus_panel::(cx); - panel.update(cx, |panel, cx| panel.open_history(cx)); + workspace.focus_panel::(window, cx); + panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); + } + }) + .register_action(|workspace, _: &OpenPromptEditorHistory, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + workspace.focus_panel::(window, cx); + panel.update(cx, |panel, cx| panel.open_prompt_editor_history(window, cx)); + } + }) + .register_action(|workspace, _: &OpenConfiguration, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + workspace.focus_panel::(window, cx); + panel.update(cx, |panel, cx| panel.open_configuration(window, cx)); } }); }, @@ -50,62 +81,95 @@ pub fn init(cx: &mut AppContext) { enum ActiveView { Thread, + PromptEditor, History, + PromptEditorHistory, + Configuration, } pub struct AssistantPanel { - workspace: WeakView, + workspace: WeakEntity, + project: Entity, fs: Arc, language_registry: Arc, - thread_store: Model, - thread: View, - message_editor: View, + thread_store: Entity, + thread: Entity, + message_editor: Entity, + context_store: Entity, + context_editor: Option>, + context_history: Option>, + configuration: Option>, + configuration_subscription: Option, tools: Arc, local_timezone: UtcOffset, active_view: ActiveView, - history: View, + history: Entity, + new_item_context_menu_handle: PopoverMenuHandle, + open_history_context_menu_handle: PopoverMenuHandle, width: Option, height: Option, } impl AssistantPanel { pub fn load( - workspace: WeakView, + workspace: WeakEntity, + prompt_builder: Arc, cx: AsyncWindowContext, - ) -> Task>> { + ) -> Task>> { cx.spawn(|mut cx| async move { let tools = Arc::new(ToolWorkingSet::default()); - let thread_store = workspace + log::info!("[assistant2-debug] initializing ThreadStore"); + let thread_store = workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + ThreadStore::new(project, tools.clone(), cx) + })??; + log::info!("[assistant2-debug] finished initializing ThreadStore"); + + let slash_commands = Arc::new(SlashCommandWorkingSet::default()); + log::info!("[assistant2-debug] initializing ContextStore"); + let context_store = workspace .update(&mut cx, |workspace, cx| { let project = workspace.project().clone(); - ThreadStore::new(project, tools.clone(), cx) + assistant_context_editor::ContextStore::new( + project, + prompt_builder.clone(), + slash_commands, + tools.clone(), + cx, + ) })? .await?; + log::info!("[assistant2-debug] finished initializing ContextStore"); - workspace.update(&mut cx, |workspace, cx| { - cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx)) + workspace.update_in(&mut cx, |workspace, window, cx| { + cx.new(|cx| Self::new(workspace, thread_store, context_store, tools, window, cx)) }) }) } fn new( workspace: &Workspace, - thread_store: Model, + thread_store: Entity, + context_store: Entity, tools: Arc, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { + log::info!("[assistant2-debug] AssistantPanel::new"); let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); let fs = workspace.app_state().fs.clone(); - let language_registry = workspace.project().read(cx).languages().clone(); + let project = workspace.project().clone(); + let language_registry = project.read(cx).languages().clone(); let workspace = workspace.weak_handle(); - let weak_self = cx.view().downgrade(); + let weak_self = cx.entity().downgrade(); - let message_editor = cx.new_view(|cx| { + let message_editor = cx.new(|cx| { MessageEditor::new( fs.clone(), workspace.clone(), thread_store.downgrade(), thread.clone(), + window, cx, ) }); @@ -113,121 +177,336 @@ impl AssistantPanel { Self { active_view: ActiveView::Thread, workspace: workspace.clone(), + project, fs: fs.clone(), language_registry: language_registry.clone(), thread_store: thread_store.clone(), - thread: cx.new_view(|cx| { + thread: cx.new(|cx| { ActiveThread::new( thread.clone(), + thread_store.clone(), workspace, language_registry, tools.clone(), + window, cx, ) }), message_editor, + context_store, + context_editor: None, + context_history: None, + configuration: None, + configuration_subscription: None, tools, local_timezone: UtcOffset::from_whole_seconds( chrono::Local::now().offset().local_minus_utc(), ) .unwrap(), - history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)), + history: cx.new(|cx| ThreadHistory::new(weak_self, thread_store, cx)), + new_item_context_menu_handle: PopoverMenuHandle::default(), + open_history_context_menu_handle: PopoverMenuHandle::default(), width: None, height: None, } } + pub fn toggle_focus( + workspace: &mut Workspace, + _: &ToggleFocus, + window: &mut Window, + cx: &mut Context, + ) { + let settings = AssistantSettings::get_global(cx); + if !settings.enabled { + return; + } + + workspace.toggle_panel_focus::(window, cx); + } + pub(crate) fn local_timezone(&self) -> UtcOffset { self.local_timezone } - pub(crate) fn thread_store(&self) -> &Model { + pub(crate) fn thread_store(&self) -> &Entity { &self.thread_store } - fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { + fn cancel( + &mut self, + _: &editor::actions::Cancel, + _window: &mut Window, + cx: &mut Context, + ) { self.thread .update(cx, |thread, cx| thread.cancel_last_completion(cx)); } - fn new_thread(&mut self, cx: &mut ViewContext) { + fn new_thread(&mut self, window: &mut Window, cx: &mut Context) { let thread = self .thread_store .update(cx, |this, cx| this.create_thread(cx)); self.active_view = ActiveView::Thread; - self.thread = cx.new_view(|cx| { + self.thread = cx.new(|cx| { ActiveThread::new( thread.clone(), + self.thread_store.clone(), self.workspace.clone(), self.language_registry.clone(), self.tools.clone(), + window, cx, ) }); - self.message_editor = cx.new_view(|cx| { + self.message_editor = cx.new(|cx| { MessageEditor::new( self.fs.clone(), self.workspace.clone(), self.thread_store.downgrade(), thread, + window, cx, ) }); - self.message_editor.focus_handle(cx).focus(cx); + self.message_editor.focus_handle(cx).focus(window); } - fn open_history(&mut self, cx: &mut ViewContext) { - self.active_view = ActiveView::History; - self.history.focus_handle(cx).focus(cx); - cx.notify(); - } + fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { + self.active_view = ActiveView::PromptEditor; - pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { - let Some(thread) = self - .thread_store - .update(cx, |this, cx| this.open_thread(thread_id, cx)) - else { - return; - }; + let context = self + .context_store + .update(cx, |context_store, cx| context_store.create(cx)); + let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx) + .log_err() + .flatten(); - self.active_view = ActiveView::Thread; - self.thread = cx.new_view(|cx| { - ActiveThread::new( - thread.clone(), + self.context_editor = Some(cx.new(|cx| { + let mut editor = ContextEditor::for_context( + context, + self.fs.clone(), self.workspace.clone(), - self.language_registry.clone(), - self.tools.clone(), + self.project.clone(), + lsp_adapter_delegate, + window, cx, - ) - }); - self.message_editor = cx.new_view(|cx| { - MessageEditor::new( - self.fs.clone(), + ); + editor.insert_default_prompt(window, cx); + editor + })); + + if let Some(context_editor) = self.context_editor.as_ref() { + context_editor.focus_handle(cx).focus(window); + } + } + + fn deploy_prompt_library( + &mut self, + _: &DeployPromptLibrary, + _window: &mut Window, + cx: &mut Context, + ) { + open_prompt_library( + self.language_registry.clone(), + Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())), + Arc::new(|| { + Box::new(SlashCommandCompletionProvider::new( + Arc::new(SlashCommandWorkingSet::default()), + None, + None, + )) + }), + cx, + ) + .detach_and_log_err(cx); + } + + fn open_history(&mut self, window: &mut Window, cx: &mut Context) { + self.active_view = ActiveView::History; + self.history.focus_handle(cx).focus(window); + cx.notify(); + } + + fn open_prompt_editor_history(&mut self, window: &mut Window, cx: &mut Context) { + self.active_view = ActiveView::PromptEditorHistory; + self.context_history = Some(cx.new(|cx| { + ContextHistory::new( + self.project.clone(), + self.context_store.clone(), self.workspace.clone(), - self.thread_store.downgrade(), - thread, + window, cx, ) - }); - self.message_editor.focus_handle(cx).focus(cx); + })); + + if let Some(context_history) = self.context_history.as_ref() { + context_history.focus_handle(cx).focus(window); + } + + cx.notify(); + } + + fn open_saved_prompt_editor( + &mut self, + path: PathBuf, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let context = self + .context_store + .update(cx, |store, cx| store.open_local_context(path.clone(), cx)); + let fs = self.fs.clone(); + let project = self.project.clone(); + let workspace = self.workspace.clone(); + + let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten(); + + cx.spawn_in(window, |this, mut cx| async move { + let context = context.await?; + this.update_in(&mut cx, |this, window, cx| { + let editor = cx.new(|cx| { + ContextEditor::for_context( + context, + fs, + workspace, + project, + lsp_adapter_delegate, + window, + cx, + ) + }); + this.active_view = ActiveView::PromptEditor; + this.context_editor = Some(editor); + + anyhow::Ok(()) + })??; + Ok(()) + }) + } + + pub(crate) fn open_thread( + &mut self, + thread_id: &ThreadId, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let open_thread_task = self + .thread_store + .update(cx, |this, cx| this.open_thread(thread_id, cx)); + + cx.spawn_in(window, |this, mut cx| async move { + let thread = open_thread_task.await?; + this.update_in(&mut cx, |this, window, cx| { + this.active_view = ActiveView::Thread; + this.thread = cx.new(|cx| { + ActiveThread::new( + thread.clone(), + this.thread_store.clone(), + this.workspace.clone(), + this.language_registry.clone(), + this.tools.clone(), + window, + cx, + ) + }); + this.message_editor = cx.new(|cx| { + MessageEditor::new( + this.fs.clone(), + this.workspace.clone(), + this.thread_store.downgrade(), + thread, + window, + cx, + ) + }); + this.message_editor.focus_handle(cx).focus(window); + }) + }) + } + + pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context) { + self.active_view = ActiveView::Configuration; + self.configuration = Some(cx.new(|cx| AssistantConfiguration::new(window, cx))); + + if let Some(configuration) = self.configuration.as_ref() { + self.configuration_subscription = Some(cx.subscribe_in( + configuration, + window, + Self::handle_assistant_configuration_event, + )); + + configuration.focus_handle(cx).focus(window); + } + } + + fn handle_assistant_configuration_event( + &mut self, + _model: &Entity, + event: &AssistantConfigurationEvent, + window: &mut Window, + cx: &mut Context, + ) { + match event { + AssistantConfigurationEvent::NewThread(provider) => { + if LanguageModelRegistry::read_global(cx) + .active_provider() + .map_or(true, |active_provider| { + active_provider.id() != provider.id() + }) + { + if let Some(model) = provider.provided_models(cx).first().cloned() { + update_settings_file::( + self.fs.clone(), + cx, + move |settings, _| settings.set_model(model), + ); + } + } + + self.new_thread(window, cx); + } + } } - pub(crate) fn active_thread(&self, cx: &AppContext) -> Model { - self.thread.read(cx).thread.clone() + pub(crate) fn active_thread(&self, cx: &App) -> Entity { + self.thread.read(cx).thread().clone() } - pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { + pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut Context) { self.thread_store - .update(cx, |this, cx| this.delete_thread(thread_id, cx)); + .update(cx, |this, cx| this.delete_thread(thread_id, cx)) + .detach_and_log_err(cx); } } -impl FocusableView for AssistantPanel { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for AssistantPanel { + fn focus_handle(&self, cx: &App) -> FocusHandle { match self.active_view { ActiveView::Thread => self.message_editor.focus_handle(cx), ActiveView::History => self.history.focus_handle(cx), + ActiveView::PromptEditor => { + if let Some(context_editor) = self.context_editor.as_ref() { + context_editor.focus_handle(cx) + } else { + cx.focus_handle() + } + } + ActiveView::PromptEditorHistory => { + if let Some(context_history) = self.context_history.as_ref() { + context_history.focus_handle(cx) + } else { + cx.focus_handle() + } + } + ActiveView::Configuration => { + if let Some(configuration) = self.configuration.as_ref() { + configuration.focus_handle(cx) + } else { + cx.focus_handle() + } + } } } } @@ -239,15 +518,19 @@ impl Panel for AssistantPanel { "AssistantPanel2" } - fn position(&self, _cx: &WindowContext) -> DockPosition { - DockPosition::Right + fn position(&self, _window: &Window, cx: &App) -> DockPosition { + match AssistantSettings::get_global(cx).dock { + AssistantDockPosition::Left => DockPosition::Left, + AssistantDockPosition::Bottom => DockPosition::Bottom, + AssistantDockPosition::Right => DockPosition::Right, + } } fn position_is_valid(&self, _: DockPosition) -> bool { true } - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { settings::update_settings_file::( self.fs.clone(), cx, @@ -262,9 +545,9 @@ impl Panel for AssistantPanel { ); } - fn size(&self, cx: &WindowContext) -> Pixels { + fn size(&self, window: &Window, cx: &App) -> Pixels { let settings = AssistantSettings::get_global(cx); - match self.position(cx) { + match self.position(window, cx) { DockPosition::Left | DockPosition::Right => { self.width.unwrap_or(settings.default_width) } @@ -272,30 +555,30 @@ impl Panel for AssistantPanel { } } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - match self.position(cx) { + fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { + match self.position(window, cx) { DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Bottom => self.height = size, } cx.notify(); } - fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} + fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context) {} fn remote_id() -> Option { Some(proto::PanelId::AssistantPanel) } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, _window: &Window, cx: &App) -> Option { let settings = AssistantSettings::get_global(cx); if !settings.enabled || !settings.button { return None; } - Some(IconName::ZedAssistant2) + Some(IconName::ZedAssistant) } - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { Some("Assistant Panel") } @@ -309,17 +592,36 @@ impl Panel for AssistantPanel { } impl AssistantPanel { - fn render_toolbar(&self, cx: &mut ViewContext) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - + fn render_toolbar(&self, cx: &mut Context) -> impl IntoElement { let thread = self.thread.read(cx); - let title = if thread.is_empty() { - thread.summary_or_default(cx) - } else { - thread - .summary(cx) - .unwrap_or_else(|| SharedString::from("Loading Summary…")) + let title = match self.active_view { + ActiveView::Thread => { + if thread.is_empty() { + thread.summary_or_default(cx) + } else { + thread + .summary(cx) + .unwrap_or_else(|| SharedString::from("Loading Summary…")) + } + } + ActiveView::PromptEditor => self + .context_editor + .as_ref() + .map(|context_editor| { + SharedString::from(context_editor.read(cx).title(cx).to_string()) + }) + .unwrap_or_else(|| SharedString::from("Loading Summary…")), + ActiveView::History | ActiveView::PromptEditorHistory => "History".into(), + ActiveView::Configuration => "Configuration".into(), + }; + + let sub_title = match self.active_view { + ActiveView::Thread => None, + ActiveView::PromptEditor => None, + ActiveView::History => Some("Thread"), + ActiveView::PromptEditorHistory => Some("Prompt Editor"), + ActiveView::Configuration => None, }; h_flex() @@ -332,7 +634,24 @@ impl AssistantPanel { .bg(cx.theme().colors().tab_bar_background) .border_b_1() .border_color(cx.theme().colors().border) - .child(h_flex().child(Label::new(title))) + .child( + h_flex() + .child(Label::new(title)) + .when(sub_title.is_some(), |this| { + this.child( + h_flex() + .pl_1p5() + .gap_1p5() + .child( + Label::new("/") + .size(LabelSize::Small) + .color(Color::Disabled) + .alpha(0.5), + ) + .child(Label::new(sub_title.unwrap())), + ) + }), + ) .child( h_flex() .h_full() @@ -341,67 +660,102 @@ impl AssistantPanel { .border_color(cx.theme().colors().border) .gap(DynamicSpacing::Base02.rems(cx)) .child( - IconButton::new("new-thread", IconName::Plus) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |cx| { - Tooltip::for_action_in( - "New Thread", - &NewThread, - &focus_handle, - cx, - ) - } - }) - .on_click(move |_event, cx| { - cx.dispatch_action(NewThread.boxed_clone()); + PopoverMenu::new("assistant-toolbar-new-popover-menu") + .trigger( + IconButton::new("new", IconName::Plus) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip(Tooltip::text("New…")), + ) + .anchor(Corner::TopRight) + .with_handle(self.new_item_context_menu_handle.clone()) + .menu(move |window, cx| { + Some(ContextMenu::build(window, cx, |menu, _window, _cx| { + menu.action("New Thread", NewThread.boxed_clone()) + .action("New Prompt Editor", NewPromptEditor.boxed_clone()) + })) }), ) .child( - IconButton::new("open-history", IconName::HistoryRerun) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |cx| { - Tooltip::for_action_in( - "Open History", - &OpenHistory, - &focus_handle, - cx, - ) - } - }) - .on_click(move |_event, cx| { - cx.dispatch_action(OpenHistory.boxed_clone()); + PopoverMenu::new("assistant-toolbar-history-popover-menu") + .trigger( + IconButton::new("open-history", IconName::HistoryRerun) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip(Tooltip::text("History…")), + ) + .anchor(Corner::TopRight) + .with_handle(self.open_history_context_menu_handle.clone()) + .menu(move |window, cx| { + Some(ContextMenu::build(window, cx, |menu, _window, _cx| { + menu.action("Thread History", OpenHistory.boxed_clone()) + .action( + "Prompt Editor History", + OpenPromptEditorHistory.boxed_clone(), + ) + })) }), ) .child( IconButton::new("configure-assistant", IconName::Settings) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Configure Assistant", cx)) - .on_click(move |_event, _cx| { - println!("Configure Assistant"); + .tooltip(Tooltip::text("Configure Assistant")) + .on_click(move |_event, window, cx| { + window.dispatch_action(OpenConfiguration.boxed_clone(), cx); }), ), ) } - fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext) -> AnyElement { + fn render_active_thread_or_empty_state( + &self, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { if self.thread.read(cx).is_empty() { - return self.render_thread_empty_state(cx).into_any_element(); + return self + .render_thread_empty_state(window, cx) + .into_any_element(); } - self.thread.clone().into_any() + self.thread.clone().into_any_element() } - fn render_thread_empty_state(&self, cx: &mut ViewContext) -> impl IntoElement { + fn configuration_error(&self, cx: &App) -> Option { + let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else { + return Some(ConfigurationError::NoProvider); + }; + + if !provider.is_authenticated(cx) { + return Some(ConfigurationError::ProviderNotAuthenticated); + } + + if provider.must_accept_terms(cx) { + return Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)); + } + + None + } + + fn render_thread_empty_state( + &self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { let recent_threads = self .thread_store - .update(cx, |this, cx| this.recent_threads(3, cx)); + .update(cx, |this, _cx| this.recent_threads(3)); + + let create_welcome_heading = || { + h_flex() + .w_full() + .justify_center() + .child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small)) + }; + + let configuration_error = self.configuration_error(cx); + let no_error = configuration_error.is_none(); v_flex() .gap_2() @@ -416,6 +770,62 @@ impl AssistantPanel { .mb_4(), ), ) + .map(|parent| { + match configuration_error { + Some(ConfigurationError::ProviderNotAuthenticated) | Some(ConfigurationError::NoProvider) => { + parent.child( + v_flex() + .gap_0p5() + .child(create_welcome_heading()) + .child( + h_flex().mb_2().w_full().justify_center().child( + Label::new( + "To start using the assistant, configure at least one LLM provider.", + ) + .color(Color::Muted), + ), + ) + .child( + h_flex().w_full().justify_center().child( + Button::new("open-configuration", "Configure a Provider") + .size(ButtonSize::Compact) + .icon(Some(IconName::Sliders)) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .on_click(cx.listener(|this, _, window, cx| { + this.open_configuration(window, cx); + })), + ), + ), + ) + } + Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { + parent.child( + v_flex() + .gap_0p5() + .child(create_welcome_heading()) + .children(provider.render_accept_terms( + LanguageModelProviderTosView::ThreadEmptyState, + cx, + )), + ) + } + None => parent, + } + }) + .when( + recent_threads.is_empty() && no_error, + |parent| { + parent.child( + v_flex().gap_0p5().child(create_welcome_heading()).child( + h_flex().w_full().justify_center().child( + Label::new("Start typing to chat with your codebase") + .color(Color::Muted), + ), + ), + ) + }, + ) .when(!recent_threads.is_empty(), |parent| { parent .child( @@ -428,7 +838,7 @@ impl AssistantPanel { .child(v_flex().mx_auto().w_4_5().gap_2().children( recent_threads.into_iter().map(|thread| { // TODO: keyboard navigation - PastThread::new(thread, cx.view().downgrade(), false) + PastThread::new(thread, cx.entity().downgrade(), false) }), )) .child( @@ -439,17 +849,17 @@ impl AssistantPanel { .key_binding(KeyBinding::for_action_in( &OpenHistory, &self.focus_handle(cx), - cx, + window, )) - .on_click(move |_event, cx| { - cx.dispatch_action(OpenHistory.boxed_clone()); + .on_click(move |_event, window, cx| { + window.dispatch_action(OpenHistory.boxed_clone(), cx); }), ), ) }) } - fn render_last_error(&self, cx: &mut ViewContext) -> Option { + fn render_last_error(&self, cx: &mut Context) -> Option { let last_error = self.thread.read(cx).last_error()?; Some( @@ -475,7 +885,7 @@ impl AssistantPanel { ) } - fn render_payment_required_error(&self, cx: &mut ViewContext) -> AnyElement { + fn render_payment_required_error(&self, cx: &mut Context) -> AnyElement { const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used."; v_flex() @@ -499,7 +909,7 @@ impl AssistantPanel { .justify_end() .mt_1() .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( - |this, _, cx| { + |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); @@ -509,7 +919,7 @@ impl AssistantPanel { }, ))) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( - |this, _, cx| { + |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); @@ -521,7 +931,7 @@ impl AssistantPanel { .into_any() } - fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext) -> AnyElement { + fn render_max_monthly_spend_reached_error(&self, cx: &mut Context) -> AnyElement { const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs."; v_flex() @@ -546,7 +956,7 @@ impl AssistantPanel { .mt_1() .child( Button::new("subscribe", "Update Monthly Spend Limit").on_click( - cx.listener(|this, _, cx| { + cx.listener(|this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); @@ -557,7 +967,7 @@ impl AssistantPanel { ), ) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( - |this, _, cx| { + |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); @@ -572,7 +982,7 @@ impl AssistantPanel { fn render_error_message( &self, error_message: &SharedString, - cx: &mut ViewContext, + cx: &mut Context, ) -> AnyElement { v_flex() .gap_0p5() @@ -598,7 +1008,7 @@ impl AssistantPanel { .justify_end() .mt_1() .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( - |this, _, cx| { + |this, _, _, cx| { this.thread.update(cx, |this, _cx| { this.clear_last_error(); }); @@ -612,22 +1022,23 @@ impl AssistantPanel { } impl Render for AssistantPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .key_context("AssistantPanel2") .justify_between() .size_full() .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(|this, _: &NewThread, cx| { - this.new_thread(cx); + .on_action(cx.listener(|this, _: &NewThread, window, cx| { + this.new_thread(window, cx); })) - .on_action(cx.listener(|this, _: &OpenHistory, cx| { - this.open_history(cx); + .on_action(cx.listener(|this, _: &OpenHistory, window, cx| { + this.open_history(window, cx); })) + .on_action(cx.listener(Self::deploy_prompt_library)) .child(self.render_toolbar(cx)) .map(|parent| match self.active_view { ActiveView::Thread => parent - .child(self.render_active_thread_or_empty_state(cx)) + .child(self.render_active_thread_or_empty_state(window, cx)) .child( h_flex() .border_t_1() @@ -636,6 +1047,93 @@ impl Render for AssistantPanel { ) .children(self.render_last_error(cx)), ActiveView::History => parent.child(self.history.clone()), + ActiveView::PromptEditor => parent.children(self.context_editor.clone()), + ActiveView::PromptEditorHistory => parent.children(self.context_history.clone()), + ActiveView::Configuration => parent.children(self.configuration.clone()), }) } } + +struct PromptLibraryInlineAssist { + workspace: WeakEntity, +} + +impl PromptLibraryInlineAssist { + pub fn new(workspace: WeakEntity) -> Self { + Self { workspace } + } +} + +impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist { + fn assist( + &self, + prompt_editor: &Entity, + _initial_prompt: Option, + window: &mut Window, + cx: &mut Context, + ) { + InlineAssistant::update_global(cx, |assistant, cx| { + assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx) + }) + } + + fn focus_assistant_panel( + &self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) -> bool { + workspace + .focus_panel::(window, cx) + .is_some() + } +} + +pub struct ConcreteAssistantPanelDelegate; + +impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate { + fn active_context_editor( + &self, + workspace: &mut Workspace, + _window: &mut Window, + cx: &mut Context, + ) -> Option> { + let panel = workspace.panel::(cx)?; + panel.update(cx, |panel, _cx| panel.context_editor.clone()) + } + + fn open_saved_context( + &self, + workspace: &mut Workspace, + path: std::path::PathBuf, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let Some(panel) = workspace.panel::(cx) else { + return Task::ready(Err(anyhow!("Assistant panel not found"))); + }; + + panel.update(cx, |panel, cx| { + panel.open_saved_prompt_editor(path, window, cx) + }) + } + + fn open_remote_context( + &self, + _workspace: &mut Workspace, + _context_id: assistant_context_editor::ContextId, + _window: &mut Window, + _cx: &mut Context, + ) -> Task>> { + Task::ready(Err(anyhow!("opening remote context not implemented"))) + } + + fn quote_selection( + &self, + _workspace: &mut Workspace, + _creases: Vec<(String, String)>, + _window: &mut Window, + _cx: &mut Context, + ) { + } +} diff --git a/crates/assistant2/src/buffer_codegen.rs b/crates/assistant2/src/buffer_codegen.rs index f5700e46c53fd8..6c3e67f8de8697 100644 --- a/crates/assistant2/src/buffer_codegen.rs +++ b/crates/assistant2/src/buffer_codegen.rs @@ -6,7 +6,7 @@ use client::telemetry::Telemetry; use collections::HashSet; use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint}; use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, StreamExt}; -use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription, Task}; +use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task}; use language::{Buffer, IndentKind, Point, TransactionId}; use language_model::{ LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, @@ -32,14 +32,14 @@ use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff}; use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; pub struct BufferCodegen { - alternatives: Vec>, + alternatives: Vec>, pub active_alternative: usize, seen_alternatives: HashSet, subscriptions: Vec, - buffer: Model, + buffer: Entity, range: Range, initial_transaction_id: Option, - context_store: Model, + context_store: Entity, telemetry: Arc, builder: Arc, pub is_insertion: bool, @@ -47,15 +47,15 @@ pub struct BufferCodegen { impl BufferCodegen { pub fn new( - buffer: Model, + buffer: Entity, range: Range, initial_transaction_id: Option, - context_store: Model, + context_store: Entity, telemetry: Arc, builder: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -83,7 +83,7 @@ impl BufferCodegen { this } - fn subscribe_to_alternative(&mut self, cx: &mut ModelContext) { + fn subscribe_to_alternative(&mut self, cx: &mut Context) { let codegen = self.active_alternative().clone(); self.subscriptions.clear(); self.subscriptions @@ -92,22 +92,22 @@ impl BufferCodegen { .push(cx.subscribe(&codegen, |_, _, event, cx| cx.emit(*event))); } - pub fn active_alternative(&self) -> &Model { + pub fn active_alternative(&self) -> &Entity { &self.alternatives[self.active_alternative] } - pub fn status<'a>(&self, cx: &'a AppContext) -> &'a CodegenStatus { + pub fn status<'a>(&self, cx: &'a App) -> &'a CodegenStatus { &self.active_alternative().read(cx).status } - pub fn alternative_count(&self, cx: &AppContext) -> usize { + pub fn alternative_count(&self, cx: &App) -> usize { LanguageModelRegistry::read_global(cx) .inline_alternative_models() .len() + 1 } - pub fn cycle_prev(&mut self, cx: &mut ModelContext) { + pub fn cycle_prev(&mut self, cx: &mut Context) { let next_active_ix = if self.active_alternative == 0 { self.alternatives.len() - 1 } else { @@ -116,12 +116,12 @@ impl BufferCodegen { self.activate(next_active_ix, cx); } - pub fn cycle_next(&mut self, cx: &mut ModelContext) { + pub fn cycle_next(&mut self, cx: &mut Context) { let next_active_ix = (self.active_alternative + 1) % self.alternatives.len(); self.activate(next_active_ix, cx); } - fn activate(&mut self, index: usize, cx: &mut ModelContext) { + fn activate(&mut self, index: usize, cx: &mut Context) { self.active_alternative() .update(cx, |codegen, cx| codegen.set_active(false, cx)); self.seen_alternatives.insert(index); @@ -132,7 +132,7 @@ impl BufferCodegen { cx.notify(); } - pub fn start(&mut self, user_prompt: String, cx: &mut ModelContext) -> Result<()> { + pub fn start(&mut self, user_prompt: String, cx: &mut Context) -> Result<()> { let alternative_models = LanguageModelRegistry::read_global(cx) .inline_alternative_models() .to_vec(); @@ -143,7 +143,7 @@ impl BufferCodegen { self.alternatives.truncate(1); for _ in 0..alternative_models.len() { - self.alternatives.push(cx.new_model(|cx| { + self.alternatives.push(cx.new(|cx| { CodegenAlternative::new( self.buffer.clone(), self.range.clone(), @@ -172,13 +172,13 @@ impl BufferCodegen { Ok(()) } - pub fn stop(&mut self, cx: &mut ModelContext) { + pub fn stop(&mut self, cx: &mut Context) { for codegen in &self.alternatives { codegen.update(cx, |codegen, cx| codegen.stop(cx)); } } - pub fn undo(&mut self, cx: &mut ModelContext) { + pub fn undo(&mut self, cx: &mut Context) { self.active_alternative() .update(cx, |codegen, cx| codegen.undo(cx)); @@ -190,27 +190,27 @@ impl BufferCodegen { }); } - pub fn buffer(&self, cx: &AppContext) -> Model { + pub fn buffer(&self, cx: &App) -> Entity { self.active_alternative().read(cx).buffer.clone() } - pub fn old_buffer(&self, cx: &AppContext) -> Model { + pub fn old_buffer(&self, cx: &App) -> Entity { self.active_alternative().read(cx).old_buffer.clone() } - pub fn snapshot(&self, cx: &AppContext) -> MultiBufferSnapshot { + pub fn snapshot(&self, cx: &App) -> MultiBufferSnapshot { self.active_alternative().read(cx).snapshot.clone() } - pub fn edit_position(&self, cx: &AppContext) -> Option { + pub fn edit_position(&self, cx: &App) -> Option { self.active_alternative().read(cx).edit_position } - pub fn diff<'a>(&self, cx: &'a AppContext) -> &'a Diff { + pub fn diff<'a>(&self, cx: &'a App) -> &'a Diff { &self.active_alternative().read(cx).diff } - pub fn last_equal_ranges<'a>(&self, cx: &'a AppContext) -> &'a [Range] { + pub fn last_equal_ranges<'a>(&self, cx: &'a App) -> &'a [Range] { self.active_alternative().read(cx).last_equal_ranges() } } @@ -218,8 +218,8 @@ impl BufferCodegen { impl EventEmitter for BufferCodegen {} pub struct CodegenAlternative { - buffer: Model, - old_buffer: Model, + buffer: Entity, + old_buffer: Entity, snapshot: MultiBufferSnapshot, edit_position: Option, range: Range, @@ -228,7 +228,7 @@ pub struct CodegenAlternative { status: CodegenStatus, generation: Task<()>, diff: Diff, - context_store: Option>, + context_store: Option>, telemetry: Option>, _subscription: gpui::Subscription, builder: Arc, @@ -245,27 +245,27 @@ impl EventEmitter for CodegenAlternative {} impl CodegenAlternative { pub fn new( - buffer: Model, + buffer: Entity, range: Range, active: bool, - context_store: Option>, + context_store: Option>, telemetry: Option>, builder: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { let snapshot = buffer.read(cx).snapshot(cx); - let (old_excerpt, _) = snapshot + let (old_buffer, _, _) = snapshot .range_to_buffer_ranges(range.clone()) .pop() .unwrap(); - let old_buffer = cx.new_model(|cx| { - let text = old_excerpt.buffer().as_rope().clone(); - let line_ending = old_excerpt.buffer().line_ending(); - let language = old_excerpt.buffer().language().cloned(); + let old_buffer = cx.new(|cx| { + let text = old_buffer.as_rope().clone(); + let line_ending = old_buffer.line_ending(); + let language = old_buffer.language().cloned(); let language_registry = buffer .read(cx) - .buffer(old_excerpt.buffer_id()) + .buffer(old_buffer.remote_id()) .unwrap() .read(cx) .language_registry(); @@ -303,7 +303,7 @@ impl CodegenAlternative { } } - pub fn set_active(&mut self, active: bool, cx: &mut ModelContext) { + pub fn set_active(&mut self, active: bool, cx: &mut Context) { if active != self.active { self.active = active; @@ -327,9 +327,9 @@ impl CodegenAlternative { fn handle_buffer_event( &mut self, - _buffer: Model, + _buffer: Entity, event: &multi_buffer::Event, - cx: &mut ModelContext, + cx: &mut Context, ) { if let multi_buffer::Event::TransactionUndone { transaction_id } = event { if self.transformation_transaction_id == Some(*transaction_id) { @@ -348,7 +348,7 @@ impl CodegenAlternative { &mut self, user_prompt: String, model: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) -> Result<()> { if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() { self.buffer.update(cx, |buffer, cx| { @@ -375,11 +375,7 @@ impl CodegenAlternative { Ok(()) } - fn build_request( - &self, - user_prompt: String, - cx: &mut AppContext, - ) -> Result { + fn build_request(&self, user_prompt: String, cx: &mut App) -> Result { let buffer = self.buffer.read(cx).snapshot(cx); let language = buffer.language_at(self.range.start); let language_name = if let Some(language) = language.as_ref() { @@ -438,7 +434,7 @@ impl CodegenAlternative { model_provider_id: String, model_api_key: Option, stream: impl 'static + Future>, - cx: &mut ModelContext, + cx: &mut Context, ) { let start_time = Instant::now(); let snapshot = self.snapshot.clone(); @@ -475,7 +471,7 @@ impl CodegenAlternative { let ranges = snapshot.range_to_buffer_ranges(self.range.clone()); ranges .first() - .and_then(|(excerpt, _)| excerpt.buffer().language()) + .and_then(|(buffer, _, _)| buffer.language()) .map(|language| language.name()) }; @@ -696,7 +692,7 @@ impl CodegenAlternative { cx.notify(); } - pub fn stop(&mut self, cx: &mut ModelContext) { + pub fn stop(&mut self, cx: &mut Context) { self.last_equal_ranges.clear(); if self.diff.is_empty() { self.status = CodegenStatus::Idle; @@ -708,7 +704,7 @@ impl CodegenAlternative { cx.notify(); } - pub fn undo(&mut self, cx: &mut ModelContext) { + pub fn undo(&mut self, cx: &mut Context) { self.buffer.update(cx, |buffer, cx| { if let Some(transaction_id) = self.transformation_transaction_id.take() { buffer.undo_transaction(transaction_id, cx); @@ -720,7 +716,7 @@ impl CodegenAlternative { fn apply_edits( &mut self, edits: impl IntoIterator, String)>, - cx: &mut ModelContext, + cx: &mut Context, ) { let transaction = self.buffer.update(cx, |buffer, cx| { // Avoid grouping assistant edits with user edits. @@ -747,7 +743,7 @@ impl CodegenAlternative { fn reapply_line_based_diff( &mut self, line_operations: impl IntoIterator, - cx: &mut ModelContext, + cx: &mut Context, ) { let old_snapshot = self.snapshot.clone(); let old_range = self.range.to_point(&old_snapshot); @@ -803,7 +799,7 @@ impl CodegenAlternative { } } - fn reapply_batch_diff(&mut self, cx: &mut ModelContext) -> Task<()> { + fn reapply_batch_diff(&mut self, cx: &mut Context) -> Task<()> { let old_snapshot = self.snapshot.clone(); let old_range = self.range.to_point(&old_snapshot); let new_snapshot = self.buffer.read(cx).snapshot(cx); @@ -1081,15 +1077,14 @@ mod tests { } } "}; - let buffer = - cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -1146,15 +1141,14 @@ mod tests { le } "}; - let buffer = - cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -1214,15 +1208,14 @@ mod tests { " \n", "}\n" // ); - let buffer = - cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -1282,14 +1275,14 @@ mod tests { \t} } "}; - let buffer = cx.new_model(|cx| Buffer::local(text, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -1337,15 +1330,14 @@ mod tests { let x = 0; } "}; - let buffer = - cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14)) }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { CodegenAlternative::new( buffer.clone(), range.clone(), @@ -1432,7 +1424,7 @@ mod tests { } fn simulate_response_stream( - codegen: Model, + codegen: Entity, cx: &mut TestAppContext, ) -> mpsc::UnboundedSender { let (chunks_tx, chunks_rx) = mpsc::unbounded(); diff --git a/crates/assistant2/src/context.rs b/crates/assistant2/src/context.rs index e4957b75913afe..091cac0ddd5254 100644 --- a/crates/assistant2/src/context.rs +++ b/crates/assistant2/src/context.rs @@ -2,7 +2,7 @@ use std::path::Path; use std::rc::Rc; use file_icons::FileIcons; -use gpui::{AppContext, Model, SharedString}; +use gpui::{App, Entity, SharedString}; use language::Buffer; use language_model::{LanguageModelRequestMessage, MessageContent}; use serde::{Deserialize, Serialize}; @@ -43,15 +43,6 @@ pub enum ContextKind { } impl ContextKind { - pub fn all() -> &'static [ContextKind] { - &[ - ContextKind::File, - ContextKind::Directory, - ContextKind::FetchedUrl, - ContextKind::Thread, - ] - } - pub fn label(&self) -> &'static str { match self { ContextKind::File => "File", @@ -72,14 +63,14 @@ impl ContextKind { } #[derive(Debug)] -pub enum Context { +pub enum AssistantContext { File(FileContext), Directory(DirectoryContext), FetchedUrl(FetchedUrlContext), Thread(ThreadContext), } -impl Context { +impl AssistantContext { pub fn id(&self) -> ContextId { match self { Self::File(file) => file.id, @@ -116,7 +107,7 @@ pub struct FetchedUrlContext { #[derive(Debug)] pub struct ThreadContext { pub id: ContextId, - pub thread: Model, + pub thread: Entity, pub text: SharedString, } @@ -126,13 +117,13 @@ pub struct ThreadContext { #[derive(Debug, Clone)] pub struct ContextBuffer { pub id: BufferId, - pub buffer: Model, + pub buffer: Entity, pub version: clock::Global, pub text: SharedString, } -impl Context { - pub fn snapshot(&self, cx: &AppContext) -> Option { +impl AssistantContext { + pub fn snapshot(&self, cx: &App) -> Option { match &self { Self::File(file_context) => file_context.snapshot(cx), Self::Directory(directory_context) => Some(directory_context.snapshot()), @@ -143,7 +134,7 @@ impl Context { } impl FileContext { - pub fn snapshot(&self, cx: &AppContext) -> Option { + pub fn snapshot(&self, cx: &App) -> Option { let buffer = self.context_buffer.buffer.read(cx); let path = buffer_path_log_err(buffer)?; let full_path: SharedString = path.to_string_lossy().into_owned().into(); @@ -230,7 +221,7 @@ impl FetchedUrlContext { } impl ThreadContext { - pub fn snapshot(&self, cx: &AppContext) -> ContextSnapshot { + pub fn snapshot(&self, cx: &App) -> ContextSnapshot { let thread = self.thread.read(cx); ContextSnapshot { id: self.id, diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs index e7adc61eea1feb..89bc09ea6d941a 100644 --- a/crates/assistant2/src/context_picker.rs +++ b/crates/assistant2/src/context_picker.rs @@ -6,11 +6,10 @@ mod thread_context_picker; use std::path::PathBuf; use std::sync::Arc; +use anyhow::{anyhow, Result}; use editor::Editor; use file_context_picker::render_file_context_entry; -use gpui::{ - AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakModel, WeakView, -}; +use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity}; use project::ProjectPath; use thread_context_picker::{render_thread_context_entry, ThreadContextEntry}; use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem}; @@ -33,57 +32,55 @@ pub enum ConfirmBehavior { #[derive(Debug, Clone)] enum ContextPickerMode { - Default(View), - File(View), - Directory(View), - Fetch(View), - Thread(View), + Default(Entity), + File(Entity), + Directory(Entity), + Fetch(Entity), + Thread(Entity), } pub(super) struct ContextPicker { mode: ContextPickerMode, - workspace: WeakView, - context_store: WeakModel, - thread_store: Option>, + workspace: WeakEntity, + editor: WeakEntity, + context_store: WeakEntity, + thread_store: Option>, confirm_behavior: ConfirmBehavior, } impl ContextPicker { pub fn new( - workspace: WeakView, - thread_store: Option>, - context_store: WeakModel, + workspace: WeakEntity, + thread_store: Option>, + context_store: WeakEntity, + editor: WeakEntity, confirm_behavior: ConfirmBehavior, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { ContextPicker { - mode: ContextPickerMode::Default(ContextMenu::build(cx, |menu, _cx| menu)), + mode: ContextPickerMode::Default(ContextMenu::build( + window, + cx, + |menu, _window, _cx| menu, + )), workspace, context_store, thread_store, + editor, confirm_behavior, } } - pub fn init(&mut self, cx: &mut ViewContext) { - self.mode = ContextPickerMode::Default(self.build_menu(cx)); + pub fn init(&mut self, window: &mut Window, cx: &mut Context) { + self.mode = ContextPickerMode::Default(self.build_menu(window, cx)); cx.notify(); } - fn build_menu(&mut self, cx: &mut ViewContext) -> View { - let context_picker = cx.view().clone(); - - let menu = ContextMenu::build(cx, move |menu, cx| { - let kind_entry = |kind: &'static ContextKind| { - let context_picker = context_picker.clone(); - - ContextMenuEntry::new(kind.label()) - .icon(kind.icon()) - .handler(move |cx| { - context_picker.update(cx, |this, cx| this.select_kind(*kind, cx)) - }) - }; + fn build_menu(&mut self, window: &mut Window, cx: &mut Context) -> Entity { + let context_picker = cx.entity().clone(); + let menu = ContextMenu::build(window, cx, move |menu, _window, cx| { let recent = self.recent_entries(cx); let has_recent = !recent.is_empty(); let recent_entries = recent @@ -91,11 +88,41 @@ impl ContextPicker { .enumerate() .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry)); + let mut context_kinds = vec![ + ContextKind::File, + ContextKind::Directory, + ContextKind::FetchedUrl, + ]; + if self.allow_threads() { + context_kinds.push(ContextKind::Thread); + } + let menu = menu - .when(has_recent, |menu| menu.label("Recent")) + .when(has_recent, |menu| { + menu.custom_row(|_, _| { + div() + .mb_1() + .child( + Label::new("Recent") + .color(Color::Muted) + .size(LabelSize::Small), + ) + .into_any_element() + }) + }) .extend(recent_entries) .when(has_recent, |menu| menu.separator()) - .extend(ContextKind::all().into_iter().map(kind_entry)); + .extend(context_kinds.into_iter().map(|kind| { + let context_picker = context_picker.clone(); + + ContextMenuEntry::new(kind.label()) + .icon(kind.icon()) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .handler(move |window, cx| { + context_picker.update(cx, |this, cx| this.select_kind(kind, window, cx)) + }) + })); match self.confirm_behavior { ConfirmBehavior::KeepOpen => menu.keep_open_on_confirm(), @@ -111,51 +138,61 @@ impl ContextPicker { menu } - fn select_kind(&mut self, kind: ContextKind, cx: &mut ViewContext) { - let context_picker = cx.view().downgrade(); + /// Whether threads are allowed as context. + pub fn allow_threads(&self) -> bool { + self.thread_store.is_some() + } + + fn select_kind(&mut self, kind: ContextKind, window: &mut Window, cx: &mut Context) { + let context_picker = cx.entity().downgrade(); match kind { ContextKind::File => { - self.mode = ContextPickerMode::File(cx.new_view(|cx| { + self.mode = ContextPickerMode::File(cx.new(|cx| { FileContextPicker::new( context_picker.clone(), self.workspace.clone(), + self.editor.clone(), self.context_store.clone(), self.confirm_behavior, + window, cx, ) })); } ContextKind::Directory => { - self.mode = ContextPickerMode::Directory(cx.new_view(|cx| { + self.mode = ContextPickerMode::Directory(cx.new(|cx| { DirectoryContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), self.confirm_behavior, + window, cx, ) })); } ContextKind::FetchedUrl => { - self.mode = ContextPickerMode::Fetch(cx.new_view(|cx| { + self.mode = ContextPickerMode::Fetch(cx.new(|cx| { FetchContextPicker::new( context_picker.clone(), self.workspace.clone(), self.context_store.clone(), self.confirm_behavior, + window, cx, ) })); } ContextKind::Thread => { if let Some(thread_store) = self.thread_store.as_ref() { - self.mode = ContextPickerMode::Thread(cx.new_view(|cx| { + self.mode = ContextPickerMode::Thread(cx.new(|cx| { ThreadContextPicker::new( thread_store.clone(), context_picker.clone(), self.context_store.clone(), self.confirm_behavior, + window, cx, ) })); @@ -164,12 +201,12 @@ impl ContextPicker { } cx.notify(); - cx.focus_self(); + cx.focus_self(window); } fn recent_menu_item( &self, - context_picker: View, + context_picker: Entity, ix: usize, entry: RecentEntry, ) -> ContextMenuItem { @@ -182,7 +219,7 @@ impl ContextPicker { let path = project_path.path.clone(); ContextMenuItem::custom_entry( - move |cx| { + move |_window, cx| { render_file_context_entry( ElementId::NamedInteger("ctx-recent".into(), ix), &path, @@ -192,9 +229,9 @@ impl ContextPicker { ) .into_any() }, - move |cx| { + move |window, cx| { context_picker.update(cx, |this, cx| { - this.add_recent_file(project_path.clone(), cx); + this.add_recent_file(project_path.clone(), window, cx); }) }, ) @@ -204,13 +241,14 @@ impl ContextPicker { let view_thread = thread.clone(); ContextMenuItem::custom_entry( - move |cx| { + move |_window, cx| { render_thread_context_entry(&view_thread, context_store.clone(), cx) .into_any() }, - move |cx| { + move |_window, cx| { context_picker.update(cx, |this, cx| { - this.add_recent_thread(thread.clone(), cx); + this.add_recent_thread(thread.clone(), cx) + .detach_and_log_err(cx); }) }, ) @@ -218,7 +256,12 @@ impl ContextPicker { } } - fn add_recent_file(&self, project_path: ProjectPath, cx: &mut ViewContext) { + fn add_recent_file( + &self, + project_path: ProjectPath, + window: &mut Window, + cx: &mut Context, + ) { let Some(context_store) = self.context_store.upgrade() else { return; }; @@ -227,34 +270,43 @@ impl ContextPicker { context_store.add_file_from_path(project_path.clone(), cx) }); - cx.spawn(|_, mut cx| async move { task.await.notify_async_err(&mut cx) }) - .detach(); + cx.spawn_in(window, |_, mut cx| async move { + task.await.notify_async_err(&mut cx) + }) + .detach(); cx.notify(); } - fn add_recent_thread(&self, thread: ThreadContextEntry, cx: &mut ViewContext) { + fn add_recent_thread( + &self, + thread: ThreadContextEntry, + cx: &mut Context, + ) -> Task> { let Some(context_store) = self.context_store.upgrade() else { - return; + return Task::ready(Err(anyhow!("context store not available"))); }; - let Some(thread) = self + let Some(thread_store) = self .thread_store - .clone() - .and_then(|this| this.upgrade()) - .and_then(|this| this.update(cx, |this, cx| this.open_thread(&thread.id, cx))) + .as_ref() + .and_then(|thread_store| thread_store.upgrade()) else { - return; + return Task::ready(Err(anyhow!("thread store not available"))); }; - context_store.update(cx, |context_store, cx| { - context_store.add_thread(thread, cx); - }); + let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&thread.id, cx)); + cx.spawn(|this, mut cx| async move { + let thread = open_thread_task.await?; + context_store.update(&mut cx, |context_store, cx| { + context_store.add_thread(thread, cx); + })?; - cx.notify(); + this.update(&mut cx, |_this, cx| cx.notify()) + }) } - fn recent_entries(&self, cx: &mut WindowContext) -> Vec { + fn recent_entries(&self, cx: &mut App) -> Vec { let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else { return vec![]; }; @@ -305,19 +357,17 @@ impl ContextPicker { return recent; }; - thread_store.update(cx, |thread_store, cx| { + thread_store.update(cx, |thread_store, _cx| { recent.extend( thread_store - .threads(cx) + .threads() .into_iter() - .filter(|thread| !current_threads.contains(thread.read(cx).id())) + .filter(|thread| !current_threads.contains(&thread.id)) .take(2) .map(|thread| { - let thread = thread.read(cx); - RecentEntry::Thread(ThreadContextEntry { - id: thread.id().clone(), - summary: thread.summary_or_default(), + id: thread.id, + summary: thread.summary, }) }), ) @@ -326,7 +376,7 @@ impl ContextPicker { recent } - fn active_singleton_buffer_path(workspace: &Workspace, cx: &AppContext) -> Option { + fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option { let active_item = workspace.active_item(cx)?; let editor = active_item.to_any().downcast::().ok()?.read(cx); @@ -339,8 +389,8 @@ impl ContextPicker { impl EventEmitter for ContextPicker {} -impl FocusableView for ContextPicker { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for ContextPicker { + fn focus_handle(&self, cx: &App) -> FocusHandle { match &self.mode { ContextPickerMode::Default(menu) => menu.focus_handle(cx), ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx), @@ -352,7 +402,7 @@ impl FocusableView for ContextPicker { } impl Render for ContextPicker { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { v_flex() .w(px(400.)) .min_w(px(400.)) diff --git a/crates/assistant2/src/context_picker/directory_context_picker.rs b/crates/assistant2/src/context_picker/directory_context_picker.rs index bdd191037c6ef3..0b5b5452283747 100644 --- a/crates/assistant2/src/context_picker/directory_context_picker.rs +++ b/crates/assistant2/src/context_picker/directory_context_picker.rs @@ -3,7 +3,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use fuzzy::PathMatch; -use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView}; +use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, ProjectPath, WorktreeId}; use ui::{prelude::*, ListItem}; @@ -14,16 +14,17 @@ use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_store::ContextStore; pub struct DirectoryContextPicker { - picker: View>, + picker: Entity>, } impl DirectoryContextPicker { pub fn new( - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { let delegate = DirectoryContextPickerDelegate::new( context_picker, @@ -31,28 +32,28 @@ impl DirectoryContextPicker { context_store, confirm_behavior, ); - let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); Self { picker } } } -impl FocusableView for DirectoryContextPicker { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for DirectoryContextPicker { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl Render for DirectoryContextPicker { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { self.picker.clone() } } pub struct DirectoryContextPickerDelegate { - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, matches: Vec, selected_index: usize, @@ -60,9 +61,9 @@ pub struct DirectoryContextPickerDelegate { impl DirectoryContextPickerDelegate { pub fn new( - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, ) -> Self { Self { @@ -79,8 +80,8 @@ impl DirectoryContextPickerDelegate { &mut self, query: String, cancellation_flag: Arc, - workspace: &View, - cx: &mut ViewContext>, + workspace: &Entity, + cx: &mut Context>, ) -> Task> { if query.is_empty() { let workspace = workspace.read(cx); @@ -146,15 +147,25 @@ impl PickerDelegate for DirectoryContextPickerDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { self.selected_index = ix; } - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Search folders…".into() } - fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { let Some(workspace) = self.workspace.upgrade() else { return Task::ready(()); }; @@ -173,7 +184,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate { }) } - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(mat) = self.matches.get(self.selected_index) else { return; }; @@ -194,19 +205,19 @@ impl PickerDelegate for DirectoryContextPickerDelegate { }; let confirm_behavior = self.confirm_behavior; - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { match task.await.notify_async_err(&mut cx) { None => anyhow::Ok(()), - Some(()) => this.update(&mut cx, |this, cx| match confirm_behavior { + Some(()) => this.update_in(&mut cx, |this, window, cx| match confirm_behavior { ConfirmBehavior::KeepOpen => {} - ConfirmBehavior::Close => this.delegate.dismissed(cx), + ConfirmBehavior::Close => this.delegate.dismissed(window, cx), }), } }) .detach_and_log_err(cx); } - fn dismissed(&mut self, cx: &mut ViewContext>) { + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { self.context_picker .update(cx, |_, cx| { cx.emit(DismissEvent); @@ -218,7 +229,8 @@ impl PickerDelegate for DirectoryContextPickerDelegate { &self, ix: usize, selected: bool, - cx: &mut ViewContext>, + _window: &mut Window, + cx: &mut Context>, ) -> Option { let path_match = &self.matches[ix]; let directory_name = path_match.path.to_string_lossy().to_string(); @@ -234,7 +246,12 @@ impl PickerDelegate for DirectoryContextPickerDelegate { ListItem::new(ix) .inset(true) .toggle_state(selected) - .child(h_flex().gap_2().child(Label::new(directory_name))) + .start_slot( + Icon::new(IconName::Folder) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child(Label::new(directory_name)) .when(added, |el| { el.end_slot( h_flex() diff --git a/crates/assistant2/src/context_picker/fetch_context_picker.rs b/crates/assistant2/src/context_picker/fetch_context_picker.rs index 7bb089e82b9208..c16beeea7a009d 100644 --- a/crates/assistant2/src/context_picker/fetch_context_picker.rs +++ b/crates/assistant2/src/context_picker/fetch_context_picker.rs @@ -4,27 +4,28 @@ use std::sync::Arc; use anyhow::{bail, Context as _, Result}; use futures::AsyncReadExt as _; -use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView}; +use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler}; use http_client::{AsyncBody, HttpClientWithUrl}; use picker::{Picker, PickerDelegate}; -use ui::{prelude::*, ListItem, ViewContext}; +use ui::{prelude::*, Context, ListItem, Window}; use workspace::Workspace; use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_store::ContextStore; pub struct FetchContextPicker { - picker: View>, + picker: Entity>, } impl FetchContextPicker { pub fn new( - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { let delegate = FetchContextPickerDelegate::new( context_picker, @@ -32,20 +33,20 @@ impl FetchContextPicker { context_store, confirm_behavior, ); - let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); Self { picker } } } -impl FocusableView for FetchContextPicker { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for FetchContextPicker { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl Render for FetchContextPicker { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { self.picker.clone() } } @@ -58,18 +59,18 @@ enum ContentType { } pub struct FetchContextPickerDelegate { - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, url: String, } impl FetchContextPickerDelegate { pub fn new( - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, ) -> Self { FetchContextPickerDelegate { @@ -166,7 +167,7 @@ impl PickerDelegate for FetchContextPickerDelegate { } } - fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString { + fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString { "Enter the URL that you would like to fetch".into() } @@ -174,19 +175,30 @@ impl PickerDelegate for FetchContextPickerDelegate { 0 } - fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext>) {} + fn set_selected_index( + &mut self, + _ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + } - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Enter a URL…".into() } - fn update_matches(&mut self, query: String, _cx: &mut ViewContext>) -> Task<()> { + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + _cx: &mut Context>, + ) -> Task<()> { self.url = query; Task::ready(()) } - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(workspace) = self.workspace.upgrade() else { return; }; @@ -194,13 +206,13 @@ impl PickerDelegate for FetchContextPickerDelegate { let http_client = workspace.read(cx).client().http_client().clone(); let url = self.url.clone(); let confirm_behavior = self.confirm_behavior; - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { let text = cx .background_executor() .spawn(Self::build_message(http_client, url.clone())) .await?; - this.update(&mut cx, |this, cx| { + this.update_in(&mut cx, |this, window, cx| { this.delegate .context_store .update(cx, |context_store, _cx| { @@ -209,7 +221,7 @@ impl PickerDelegate for FetchContextPickerDelegate { match confirm_behavior { ConfirmBehavior::KeepOpen => {} - ConfirmBehavior::Close => this.delegate.dismissed(cx), + ConfirmBehavior::Close => this.delegate.dismissed(window, cx), } anyhow::Ok(()) @@ -220,7 +232,7 @@ impl PickerDelegate for FetchContextPickerDelegate { .detach_and_log_err(cx); } - fn dismissed(&mut self, cx: &mut ViewContext>) { + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { self.context_picker .update(cx, |_, cx| { cx.emit(DismissEvent); @@ -232,7 +244,8 @@ impl PickerDelegate for FetchContextPickerDelegate { &self, ix: usize, selected: bool, - cx: &mut ViewContext>, + _window: &mut Window, + cx: &mut Context>, ) -> Option { let added = self.context_store.upgrade().map_or(false, |context_store| { context_store.read(cx).includes_url(&self.url).is_some() diff --git a/crates/assistant2/src/context_picker/file_context_picker.rs b/crates/assistant2/src/context_picker/file_context_picker.rs index 29cab4936e01ef..757249dbea59c1 100644 --- a/crates/assistant2/src/context_picker/file_context_picker.rs +++ b/crates/assistant2/src/context_picker/file_context_picker.rs @@ -1,15 +1,25 @@ +use std::collections::BTreeSet; +use std::ops::Range; use std::path::Path; use std::sync::atomic::AtomicBool; use std::sync::Arc; +use editor::actions::FoldAt; +use editor::display_map::{Crease, FoldId}; +use editor::scroll::Autoscroll; +use editor::{Anchor, Editor, FoldPlaceholder, ToPoint}; use file_icons::FileIcons; use fuzzy::PathMatch; use gpui::{ - AppContext, DismissEvent, FocusHandle, FocusableView, Stateful, Task, View, WeakModel, WeakView, + AnyElement, App, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful, Task, + WeakEntity, }; +use multi_buffer::{MultiBufferPoint, MultiBufferRow}; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, ProjectPath, WorktreeId}; -use ui::{prelude::*, ListItem, Tooltip}; +use rope::Point; +use text::SelectionGoal; +use ui::{prelude::*, ButtonLike, Disclosure, ElevationIndex, ListItem, Tooltip}; use util::ResultExt as _; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -17,45 +27,49 @@ use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_store::{ContextStore, FileInclusion}; pub struct FileContextPicker { - picker: View>, + picker: Entity>, } impl FileContextPicker { pub fn new( - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + editor: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { let delegate = FileContextPickerDelegate::new( context_picker, workspace, + editor, context_store, confirm_behavior, ); - let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); Self { picker } } } -impl FocusableView for FileContextPicker { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for FileContextPicker { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl Render for FileContextPicker { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { self.picker.clone() } } pub struct FileContextPickerDelegate { - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + editor: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, matches: Vec, selected_index: usize, @@ -63,14 +77,16 @@ pub struct FileContextPickerDelegate { impl FileContextPickerDelegate { pub fn new( - context_picker: WeakView, - workspace: WeakView, - context_store: WeakModel, + context_picker: WeakEntity, + workspace: WeakEntity, + editor: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, ) -> Self { Self { context_picker, workspace, + editor, context_store, confirm_behavior, matches: Vec::new(), @@ -82,8 +98,9 @@ impl FileContextPickerDelegate { &mut self, query: String, cancellation_flag: Arc, - workspace: &View, - cx: &mut ViewContext>, + workspace: &Entity, + + cx: &mut Context>, ) -> Task> { if query.is_empty() { let workspace = workspace.read(cx); @@ -107,7 +124,7 @@ impl FileContextPickerDelegate { let file_matches = project.worktrees(cx).flat_map(|worktree| { let worktree = worktree.read(cx); let path_prefix: Arc = worktree.root_name().into(); - worktree.files(true, 0).map(move |entry| PathMatch { + worktree.files(false, 0).map(move |entry| PathMatch { score: 0., positions: Vec::new(), worktree_id: worktree.id().to_usize(), @@ -165,22 +182,32 @@ impl PickerDelegate for FileContextPickerDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { self.selected_index = ix; } - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Search files…".into() } - fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { let Some(workspace) = self.workspace.upgrade() else { return Task::ready(()); }; let search_task = self.search(query, Arc::::default(), &workspace, cx); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { // TODO: This should be probably be run in the background. let paths = search_task.await; @@ -191,16 +218,108 @@ impl PickerDelegate for FileContextPickerDelegate { }) } - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(mat) = self.matches.get(self.selected_index) else { return; }; + let Some(file_name) = mat + .path + .file_name() + .map(|os_str| os_str.to_string_lossy().into_owned()) + else { + return; + }; + + let full_path = mat.path.display().to_string(); + let project_path = ProjectPath { worktree_id: WorktreeId::from_usize(mat.worktree_id), path: mat.path.clone(), }; + let Some(editor) = self.editor.upgrade() else { + return; + }; + + editor.update(cx, |editor, cx| { + editor.transact(window, cx, |editor, window, cx| { + // Move empty selections left by 1 column to select the `@`s, so they get overwritten when we insert. + { + let mut selections = editor.selections.all::(cx); + + for selection in selections.iter_mut() { + if selection.is_empty() { + let old_head = selection.head(); + let new_head = MultiBufferPoint::new( + old_head.row, + old_head.column.saturating_sub(1), + ); + selection.set_head(new_head, SelectionGoal::None); + } + } + + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + } + + let start_anchors = { + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor + .selections + .all::(cx) + .into_iter() + .map(|selection| snapshot.anchor_before(selection.start)) + .collect::>() + }; + + editor.insert(&full_path, window, cx); + + let end_anchors = { + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor + .selections + .all::(cx) + .into_iter() + .map(|selection| snapshot.anchor_after(selection.end)) + .collect::>() + }; + + editor.insert("\n", window, cx); // Needed to end the fold + + let placeholder = FoldPlaceholder { + render: render_fold_icon_button(IconName::File, file_name.into()), + ..Default::default() + }; + + let render_trailer = + move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any(); + + let buffer = editor.buffer().read(cx).snapshot(cx); + let mut rows_to_fold = BTreeSet::new(); + let crease_iter = start_anchors + .into_iter() + .zip(end_anchors) + .map(|(start, end)| { + rows_to_fold.insert(MultiBufferRow(start.to_point(&buffer).row)); + + Crease::inline( + start..end, + placeholder.clone(), + fold_toggle("tool-use"), + render_trailer, + ) + }); + + editor.insert_creases(crease_iter, cx); + + for buffer_row in rows_to_fold { + editor.fold_at(&FoldAt { buffer_row }, window, cx); + } + }); + }); + let Some(task) = self .context_store .update(cx, |context_store, cx| { @@ -212,19 +331,19 @@ impl PickerDelegate for FileContextPickerDelegate { }; let confirm_behavior = self.confirm_behavior; - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { match task.await.notify_async_err(&mut cx) { None => anyhow::Ok(()), - Some(()) => this.update(&mut cx, |this, cx| match confirm_behavior { + Some(()) => this.update_in(&mut cx, |this, window, cx| match confirm_behavior { ConfirmBehavior::KeepOpen => {} - ConfirmBehavior::Close => this.delegate.dismissed(cx), + ConfirmBehavior::Close => this.delegate.dismissed(window, cx), }), } }) .detach_and_log_err(cx); } - fn dismissed(&mut self, cx: &mut ViewContext>) { + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { self.context_picker .update(cx, |_, cx| { cx.emit(DismissEvent); @@ -236,7 +355,8 @@ impl PickerDelegate for FileContextPickerDelegate { &self, ix: usize, selected: bool, - cx: &mut ViewContext>, + _window: &mut Window, + cx: &mut Context>, ) -> Option { let path_match = &self.matches[ix]; @@ -259,8 +379,8 @@ pub fn render_file_context_entry( id: ElementId, path: &Path, path_prefix: &Arc, - context_store: WeakModel, - cx: &WindowContext, + context_store: WeakEntity, + cx: &App, ) -> Stateful
{ let (file_name, directory) = if path == Path::new("") { (SharedString::from(path_prefix.clone()), None) @@ -292,12 +412,12 @@ pub fn render_file_context_entry( h_flex() .id(id) - .gap_1() + .gap_1p5() .w_full() - .child(file_icon.size(IconSize::Small)) + .child(file_icon.size(IconSize::Small).color(Color::Muted)) .child( h_flex() - .gap_2() + .gap_1() .child(Label::new(file_name)) .children(directory.map(|directory| { Label::new(directory) @@ -305,11 +425,12 @@ pub fn render_file_context_entry( .color(Color::Muted) })), ) - .child(div().w_full()) .when_some(added, |el, added| match added { FileInclusion::Direct(_) => el.child( h_flex() - .gap_1() + .w_full() + .justify_end() + .gap_0p5() .child( Icon::new(IconName::Check) .size(IconSize::Small) @@ -322,7 +443,9 @@ pub fn render_file_context_entry( el.child( h_flex() - .gap_1() + .w_full() + .justify_end() + .gap_0p5() .child( Icon::new(IconName::Check) .size(IconSize::Small) @@ -330,7 +453,38 @@ pub fn render_file_context_entry( ) .child(Label::new("Included").size(LabelSize::Small)), ) - .tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx)) + .tooltip(Tooltip::text(format!("in {dir_name}"))) } }) } + +fn render_fold_icon_button( + icon: IconName, + label: SharedString, +) -> Arc, &mut Window, &mut App) -> AnyElement> { + Arc::new(move |fold_id, _fold_range, _window, _cx| { + ButtonLike::new(fold_id) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ElevatedSurface) + .child(Icon::new(icon)) + .child(Label::new(label.clone()).single_line()) + .into_any_element() + }) +} + +fn fold_toggle( + name: &'static str, +) -> impl Fn( + MultiBufferRow, + bool, + Arc, + &mut Window, + &mut App, +) -> AnyElement { + move |row, is_folded, fold, _window, _cx| { + Disclosure::new((name, row.0 as u64), !is_folded) + .toggle_state(is_folded) + .on_click(move |_e, window, cx| fold(!is_folded, window, cx)) + .into_any_element() + } +} diff --git a/crates/assistant2/src/context_picker/thread_context_picker.rs b/crates/assistant2/src/context_picker/thread_context_picker.rs index bea32d3d043ee8..e0dd0cef088c93 100644 --- a/crates/assistant2/src/context_picker/thread_context_picker.rs +++ b/crates/assistant2/src/context_picker/thread_context_picker.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use fuzzy::StringMatchCandidate; -use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView}; +use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; use picker::{Picker, PickerDelegate}; use ui::{prelude::*, ListItem}; @@ -11,16 +11,17 @@ use crate::thread::ThreadId; use crate::thread_store::ThreadStore; pub struct ThreadContextPicker { - picker: View>, + picker: Entity>, } impl ThreadContextPicker { pub fn new( - thread_store: WeakModel, - context_picker: WeakView, - context_store: WeakModel, + thread_store: WeakEntity, + context_picker: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { let delegate = ThreadContextPickerDelegate::new( thread_store, @@ -28,20 +29,20 @@ impl ThreadContextPicker { context_store, confirm_behavior, ); - let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); ThreadContextPicker { picker } } } -impl FocusableView for ThreadContextPicker { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for ThreadContextPicker { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl Render for ThreadContextPicker { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { self.picker.clone() } } @@ -53,9 +54,9 @@ pub struct ThreadContextEntry { } pub struct ThreadContextPickerDelegate { - thread_store: WeakModel, - context_picker: WeakView, - context_store: WeakModel, + thread_store: WeakEntity, + context_picker: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, matches: Vec, selected_index: usize, @@ -63,9 +64,9 @@ pub struct ThreadContextPickerDelegate { impl ThreadContextPickerDelegate { pub fn new( - thread_store: WeakModel, - context_picker: WeakView, - context_store: WeakModel, + thread_store: WeakEntity, + context_picker: WeakEntity, + context_store: WeakEntity, confirm_behavior: ConfirmBehavior, ) -> Self { ThreadContextPickerDelegate { @@ -90,22 +91,31 @@ impl PickerDelegate for ThreadContextPickerDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { self.selected_index = ix; } - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Search threads…".into() } - fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { - let Ok(threads) = self.thread_store.update(cx, |this, cx| { - this.threads(cx) + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + let Ok(threads) = self.thread_store.update(cx, |this, _cx| { + this.threads() .into_iter() - .map(|thread| { - let id = thread.read(cx).id().clone(); - let summary = thread.read(cx).summary_or_default(); - ThreadContextEntry { id, summary } + .map(|thread| ThreadContextEntry { + id: thread.id, + summary: thread.summary, }) .collect::>() }) else { @@ -139,7 +149,7 @@ impl PickerDelegate for ThreadContextPickerDelegate { } }); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { let matches = search_task.await; this.update(&mut cx, |this, cx| { this.delegate.matches = matches; @@ -150,7 +160,7 @@ impl PickerDelegate for ThreadContextPickerDelegate { }) } - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(entry) = self.matches.get(self.selected_index) else { return; }; @@ -159,22 +169,26 @@ impl PickerDelegate for ThreadContextPickerDelegate { return; }; - let Some(thread) = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx)) - else { - return; - }; + let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx)); - self.context_store - .update(cx, |context_store, cx| context_store.add_thread(thread, cx)) - .ok(); + cx.spawn_in(window, |this, mut cx| async move { + let thread = open_thread_task.await?; + this.update_in(&mut cx, |this, window, cx| { + this.delegate + .context_store + .update(cx, |context_store, cx| context_store.add_thread(thread, cx)) + .ok(); - match self.confirm_behavior { - ConfirmBehavior::KeepOpen => {} - ConfirmBehavior::Close => self.dismissed(cx), - } + match this.delegate.confirm_behavior { + ConfirmBehavior::KeepOpen => {} + ConfirmBehavior::Close => this.delegate.dismissed(window, cx), + } + }) + }) + .detach_and_log_err(cx); } - fn dismissed(&mut self, cx: &mut ViewContext>) { + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { self.context_picker .update(cx, |_, cx| { cx.emit(DismissEvent); @@ -186,7 +200,8 @@ impl PickerDelegate for ThreadContextPickerDelegate { &self, ix: usize, selected: bool, - cx: &mut ViewContext>, + _window: &mut Window, + cx: &mut Context>, ) -> Option { let thread = &self.matches[ix]; @@ -198,17 +213,21 @@ impl PickerDelegate for ThreadContextPickerDelegate { pub fn render_thread_context_entry( thread: &ThreadContextEntry, - context_store: WeakModel, - cx: &mut WindowContext, + context_store: WeakEntity, + cx: &mut App, ) -> Div { let added = context_store.upgrade().map_or(false, |ctx_store| { ctx_store.read(cx).includes_thread(&thread.id).is_some() }); h_flex() - .gap_1() + .gap_1p5() .w_full() - .child(Icon::new(IconName::MessageCircle).size(IconSize::Small)) + .child( + Icon::new(IconName::MessageCircle) + .size(IconSize::XSmall) + .color(Color::Muted), + ) .child(Label::new(thread.summary.clone())) .child(div().w_full()) .when(added, |el| { diff --git a/crates/assistant2/src/context_store.rs b/crates/assistant2/src/context_store.rs index f6b2d5c38ff363..9fd4ebd23f3df0 100644 --- a/crates/assistant2/src/context_store.rs +++ b/crates/assistant2/src/context_store.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use collections::{BTreeMap, HashMap, HashSet}; use futures::{self, future, Future, FutureExt}; -use gpui::{AppContext, AsyncAppContext, Model, ModelContext, SharedString, Task, WeakView}; +use gpui::{App, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use language::Buffer; use project::{ProjectPath, Worktree}; use rope::Rope; @@ -12,15 +12,15 @@ use text::BufferId; use workspace::Workspace; use crate::context::{ - Context, ContextBuffer, ContextId, ContextSnapshot, DirectoryContext, FetchedUrlContext, - FileContext, ThreadContext, + AssistantContext, ContextBuffer, ContextId, ContextSnapshot, DirectoryContext, + FetchedUrlContext, FileContext, ThreadContext, }; use crate::context_strip::SuggestedContext; use crate::thread::{Thread, ThreadId}; pub struct ContextStore { - workspace: WeakView, - context: Vec, + workspace: WeakEntity, + context: Vec, // TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId. next_context_id: ContextId, files: BTreeMap, @@ -30,7 +30,7 @@ pub struct ContextStore { } impl ContextStore { - pub fn new(workspace: WeakView) -> Self { + pub fn new(workspace: WeakEntity) -> Self { Self { workspace, context: Vec::new(), @@ -42,16 +42,13 @@ impl ContextStore { } } - pub fn snapshot<'a>( - &'a self, - cx: &'a AppContext, - ) -> impl Iterator + 'a { + pub fn snapshot<'a>(&'a self, cx: &'a App) -> impl Iterator + 'a { self.context() .iter() .flat_map(|context| context.snapshot(cx)) } - pub fn context(&self) -> &Vec { + pub fn context(&self) -> &Vec { &self.context } @@ -66,7 +63,7 @@ impl ContextStore { pub fn add_file_from_path( &mut self, project_path: ProjectPath, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let workspace = self.workspace.clone(); @@ -122,8 +119,8 @@ impl ContextStore { pub fn add_file_from_buffer( &mut self, - buffer_model: Model, - cx: &mut ModelContext, + buffer_model: Entity, + cx: &mut Context, ) -> Task> { cx.spawn(|this, mut cx| async move { let (buffer_info, text_task) = this.update(&mut cx, |_, cx| { @@ -153,13 +150,13 @@ impl ContextStore { let id = self.next_context_id.post_inc(); self.files.insert(context_buffer.id, id); self.context - .push(Context::File(FileContext { id, context_buffer })); + .push(AssistantContext::File(FileContext { id, context_buffer })); } pub fn add_directory( &mut self, project_path: ProjectPath, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let workspace = self.workspace.clone(); let Some(project) = workspace @@ -244,14 +241,15 @@ impl ContextStore { let id = self.next_context_id.post_inc(); self.directories.insert(path.to_path_buf(), id); - self.context.push(Context::Directory(DirectoryContext::new( - id, - path, - context_buffers, - ))); + self.context + .push(AssistantContext::Directory(DirectoryContext::new( + id, + path, + context_buffers, + ))); } - pub fn add_thread(&mut self, thread: Model, cx: &mut ModelContext) { + pub fn add_thread(&mut self, thread: Entity, cx: &mut Context) { if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) { self.remove_context(context_id); } else { @@ -259,13 +257,13 @@ impl ContextStore { } } - fn insert_thread(&mut self, thread: Model, cx: &AppContext) { + fn insert_thread(&mut self, thread: Entity, cx: &App) { let id = self.next_context_id.post_inc(); let text = thread.read(cx).text().into(); self.threads.insert(thread.read(cx).id().clone(), id); self.context - .push(Context::Thread(ThreadContext { id, thread, text })); + .push(AssistantContext::Thread(ThreadContext { id, thread, text })); } pub fn add_fetched_url(&mut self, url: String, text: impl Into) { @@ -278,17 +276,18 @@ impl ContextStore { let id = self.next_context_id.post_inc(); self.fetched_urls.insert(url.clone(), id); - self.context.push(Context::FetchedUrl(FetchedUrlContext { - id, - url: url.into(), - text: text.into(), - })); + self.context + .push(AssistantContext::FetchedUrl(FetchedUrlContext { + id, + url: url.into(), + text: text.into(), + })); } pub fn accept_suggested_context( &mut self, suggested: &SuggestedContext, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { match suggested { SuggestedContext::File { @@ -315,16 +314,16 @@ impl ContextStore { }; match self.context.remove(ix) { - Context::File(_) => { + AssistantContext::File(_) => { self.files.retain(|_, context_id| *context_id != id); } - Context::Directory(_) => { + AssistantContext::Directory(_) => { self.directories.retain(|_, context_id| *context_id != id); } - Context::FetchedUrl(_) => { + AssistantContext::FetchedUrl(_) => { self.fetched_urls.retain(|_, context_id| *context_id != id); } - Context::Thread(_) => { + AssistantContext::Thread(_) => { self.threads.retain(|_, context_id| *context_id != id); } } @@ -343,10 +342,10 @@ impl ContextStore { /// Returns whether this file path is already included directly in the context, or if it will be /// included in the context via a directory. - pub fn will_include_file_path(&self, path: &Path, cx: &AppContext) -> Option { + pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option { if !self.files.is_empty() { let found_file_context = self.context.iter().find(|context| match &context { - Context::File(file_context) => { + AssistantContext::File(file_context) => { let buffer = file_context.context_buffer.buffer.read(cx); if let Some(file_path) = buffer_path_log_err(buffer) { *file_path == *path @@ -393,7 +392,7 @@ impl ContextStore { } /// Replaces the context that matches the ID of the new context, if any match. - fn replace_context(&mut self, new_context: Context) { + fn replace_context(&mut self, new_context: AssistantContext) { let id = new_context.id(); for context in self.context.iter_mut() { if context.id() == id { @@ -403,15 +402,17 @@ impl ContextStore { } } - pub fn file_paths(&self, cx: &AppContext) -> HashSet { + pub fn file_paths(&self, cx: &App) -> HashSet { self.context .iter() .filter_map(|context| match context { - Context::File(file) => { + AssistantContext::File(file) => { let buffer = file.context_buffer.buffer.read(cx); buffer_path_log_err(buffer).map(|p| p.to_path_buf()) } - Context::Directory(_) | Context::FetchedUrl(_) | Context::Thread(_) => None, + AssistantContext::Directory(_) + | AssistantContext::FetchedUrl(_) + | AssistantContext::Thread(_) => None, }) .collect() } @@ -428,7 +429,7 @@ pub enum FileInclusion { // ContextBuffer without text. struct BufferInfo { - buffer_model: Model, + buffer_model: Entity, id: BufferId, version: clock::Global, } @@ -444,9 +445,9 @@ fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer { fn collect_buffer_info_and_text( path: Arc, - buffer_model: Model, + buffer_model: Entity, buffer: &Buffer, - cx: AsyncAppContext, + cx: AsyncApp, ) -> (BufferInfo, Task) { let buffer_info = BufferInfo { id: buffer.remote_id(), @@ -525,32 +526,32 @@ fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec> { } pub fn refresh_context_store_text( - context_store: Model, - cx: &AppContext, + context_store: Entity, + cx: &App, ) -> impl Future { let mut tasks = Vec::new(); for context in &context_store.read(cx).context { match context { - Context::File(file_context) => { + AssistantContext::File(file_context) => { let context_store = context_store.clone(); if let Some(task) = refresh_file_text(context_store, file_context, cx) { tasks.push(task); } } - Context::Directory(directory_context) => { + AssistantContext::Directory(directory_context) => { let context_store = context_store.clone(); if let Some(task) = refresh_directory_text(context_store, directory_context, cx) { tasks.push(task); } } - Context::Thread(thread_context) => { + AssistantContext::Thread(thread_context) => { let context_store = context_store.clone(); tasks.push(refresh_thread_text(context_store, thread_context, cx)); } // Intentionally omit refreshing fetched URLs as it doesn't seem all that useful, // and doing the caching properly could be tricky (unless it's already handled by // the HttpClient?). - Context::FetchedUrl(_) => {} + AssistantContext::FetchedUrl(_) => {} } } @@ -558,9 +559,9 @@ pub fn refresh_context_store_text( } fn refresh_file_text( - context_store: Model, + context_store: Entity, file_context: &FileContext, - cx: &AppContext, + cx: &App, ) -> Option> { let id = file_context.id; let task = refresh_context_buffer(&file_context.context_buffer, cx); @@ -570,7 +571,7 @@ fn refresh_file_text( context_store .update(&mut cx, |context_store, _| { let new_file_context = FileContext { id, context_buffer }; - context_store.replace_context(Context::File(new_file_context)); + context_store.replace_context(AssistantContext::File(new_file_context)); }) .ok(); })) @@ -580,9 +581,9 @@ fn refresh_file_text( } fn refresh_directory_text( - context_store: Model, + context_store: Entity, directory_context: &DirectoryContext, - cx: &AppContext, + cx: &App, ) -> Option> { let mut stale = false; let futures = directory_context @@ -611,16 +612,16 @@ fn refresh_directory_text( context_store .update(&mut cx, |context_store, _| { let new_directory_context = DirectoryContext::new(id, &path, context_buffers); - context_store.replace_context(Context::Directory(new_directory_context)); + context_store.replace_context(AssistantContext::Directory(new_directory_context)); }) .ok(); })) } fn refresh_thread_text( - context_store: Model, + context_store: Entity, thread_context: &ThreadContext, - cx: &AppContext, + cx: &App, ) -> Task<()> { let id = thread_context.id; let thread = thread_context.thread.clone(); @@ -628,7 +629,11 @@ fn refresh_thread_text( context_store .update(&mut cx, |context_store, cx| { let text = thread.read(cx).text().into(); - context_store.replace_context(Context::Thread(ThreadContext { id, thread, text })); + context_store.replace_context(AssistantContext::Thread(ThreadContext { + id, + thread, + text, + })); }) .ok(); }) @@ -636,7 +641,7 @@ fn refresh_thread_text( fn refresh_context_buffer( context_buffer: &ContextBuffer, - cx: &AppContext, + cx: &App, ) -> Option> { let buffer = context_buffer.buffer.read(cx); let path = buffer_path_log_err(buffer)?; diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs index e10be7d5ebadcc..97e9b633d34dc2 100644 --- a/crates/assistant2/src/context_strip.rs +++ b/crates/assistant2/src/context_strip.rs @@ -4,8 +4,8 @@ use collections::HashSet; use editor::Editor; use file_icons::FileIcons; use gpui::{ - AppContext, Bounds, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, - Subscription, View, WeakModel, WeakView, + App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, + WeakEntity, }; use itertools::Itertools; use language::Buffer; @@ -24,32 +24,37 @@ use crate::{ }; pub struct ContextStrip { - context_store: Model, - pub context_picker: View, + context_store: Entity, + pub context_picker: Entity, context_picker_menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, suggest_context_kind: SuggestContextKind, - workspace: WeakView, + workspace: WeakEntity, _subscriptions: Vec, focused_index: Option, children_bounds: Option>>, } impl ContextStrip { + #[allow(clippy::too_many_arguments)] pub fn new( - context_store: Model, - workspace: WeakView, - thread_store: Option>, + context_store: Entity, + workspace: WeakEntity, + editor: WeakEntity, + thread_store: Option>, context_picker_menu_handle: PopoverMenuHandle, suggest_context_kind: SuggestContextKind, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { - let context_picker = cx.new_view(|cx| { + let context_picker = cx.new(|cx| { ContextPicker::new( workspace.clone(), thread_store.clone(), context_store.downgrade(), + editor.clone(), ConfirmBehavior::KeepOpen, + window, cx, ) }); @@ -57,9 +62,9 @@ impl ContextStrip { let focus_handle = cx.focus_handle(); let subscriptions = vec![ - cx.subscribe(&context_picker, Self::handle_context_picker_event), - cx.on_focus(&focus_handle, Self::handle_focus), - cx.on_blur(&focus_handle, Self::handle_blur), + cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event), + cx.on_focus(&focus_handle, window, Self::handle_focus), + cx.on_blur(&focus_handle, window, Self::handle_blur), ]; Self { @@ -75,14 +80,14 @@ impl ContextStrip { } } - fn suggested_context(&self, cx: &ViewContext) -> Option { + fn suggested_context(&self, cx: &Context) -> Option { match self.suggest_context_kind { SuggestContextKind::File => self.suggested_file(cx), SuggestContextKind::Thread => self.suggested_thread(cx), } } - fn suggested_file(&self, cx: &ViewContext) -> Option { + fn suggested_file(&self, cx: &Context) -> Option { let workspace = self.workspace.upgrade()?; let active_item = workspace.read(cx).active_item(cx)?; @@ -115,7 +120,11 @@ impl ContextStrip { }) } - fn suggested_thread(&self, cx: &ViewContext) -> Option { + fn suggested_thread(&self, cx: &Context) -> Option { + if !self.context_picker.read(cx).allow_threads() { + return None; + } + let workspace = self.workspace.upgrade()?; let active_thread = workspace .read(cx) @@ -143,24 +152,25 @@ impl ContextStrip { fn handle_context_picker_event( &mut self, - _picker: View, + _picker: &Entity, _event: &DismissEvent, - cx: &mut ViewContext, + _window: &mut Window, + cx: &mut Context, ) { cx.emit(ContextStripEvent::PickerDismissed); } - fn handle_focus(&mut self, cx: &mut ViewContext) { + fn handle_focus(&mut self, _window: &mut Window, cx: &mut Context) { self.focused_index = self.last_pill_index(); cx.notify(); } - fn handle_blur(&mut self, cx: &mut ViewContext) { + fn handle_blur(&mut self, _window: &mut Window, cx: &mut Context) { self.focused_index = None; cx.notify(); } - fn focus_left(&mut self, _: &FocusLeft, cx: &mut ViewContext) { + fn focus_left(&mut self, _: &FocusLeft, _window: &mut Window, cx: &mut Context) { self.focused_index = match self.focused_index { Some(index) if index > 0 => Some(index - 1), _ => self.last_pill_index(), @@ -169,7 +179,7 @@ impl ContextStrip { cx.notify(); } - fn focus_right(&mut self, _: &FocusRight, cx: &mut ViewContext) { + fn focus_right(&mut self, _: &FocusRight, _window: &mut Window, cx: &mut Context) { let Some(last_index) = self.last_pill_index() else { return; }; @@ -182,7 +192,7 @@ impl ContextStrip { cx.notify(); } - fn focus_up(&mut self, _: &FocusUp, cx: &mut ViewContext) { + fn focus_up(&mut self, _: &FocusUp, _window: &mut Window, cx: &mut Context) { let Some(focused_index) = self.focused_index else { return; }; @@ -200,7 +210,7 @@ impl ContextStrip { cx.notify(); } - fn focus_down(&mut self, _: &FocusDown, cx: &mut ViewContext) { + fn focus_down(&mut self, _: &FocusDown, _window: &mut Window, cx: &mut Context) { let Some(focused_index) = self.focused_index else { return; }; @@ -270,7 +280,12 @@ impl ContextStrip { best.map(|(index, _, _)| index) } - fn remove_focused_context(&mut self, _: &RemoveFocusedContext, cx: &mut ViewContext) { + fn remove_focused_context( + &mut self, + _: &RemoveFocusedContext, + _window: &mut Window, + cx: &mut Context, + ) { if let Some(index) = self.focused_index { let mut is_empty = false; @@ -296,22 +311,32 @@ impl ContextStrip { self.focused_index == Some(context.len()) } - fn accept_suggested_context(&mut self, _: &AcceptSuggestedContext, cx: &mut ViewContext) { + fn accept_suggested_context( + &mut self, + _: &AcceptSuggestedContext, + window: &mut Window, + cx: &mut Context, + ) { if let Some(suggested) = self.suggested_context(cx) { let context_store = self.context_store.read(cx); if self.is_suggested_focused(context_store.context()) { - self.add_suggested_context(&suggested, cx); + self.add_suggested_context(&suggested, window, cx); } } } - fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut ViewContext) { + fn add_suggested_context( + &mut self, + suggested: &SuggestedContext, + window: &mut Window, + cx: &mut Context, + ) { let task = self.context_store.update(cx, |context_store, cx| { context_store.accept_suggested_context(&suggested, cx) }); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { match task.await.notify_async_err(&mut cx) { None => {} Some(()) => { @@ -328,14 +353,14 @@ impl ContextStrip { } } -impl FocusableView for ContextStrip { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { +impl Focusable for ContextStrip { + fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for ContextStrip { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let context_store = self.context_store.read(cx); let context = context_store .context() @@ -368,19 +393,20 @@ impl Render for ContextStrip { .on_action(cx.listener(Self::remove_focused_context)) .on_action(cx.listener(Self::accept_suggested_context)) .on_children_prepainted({ - let view = cx.view().downgrade(); - move |children_bounds, cx| { - view.update(cx, |this, _| { - this.children_bounds = Some(children_bounds); - }) - .ok(); + let model = cx.entity().downgrade(); + move |children_bounds, _window, cx| { + model + .update(cx, |this, _| { + this.children_bounds = Some(children_bounds); + }) + .ok(); } }) .child( PopoverMenu::new("context-picker") - .menu(move |cx| { + .menu(move |window, cx| { context_picker.update(cx, |this, cx| { - this.init(cx); + this.init(window, cx); }); Some(context_picker.clone()) @@ -391,12 +417,12 @@ impl Render for ContextStrip { .style(ui::ButtonStyle::Filled) .tooltip({ let focus_handle = focus_handle.clone(); - - move |cx| { + move |window, cx| { Tooltip::for_action_in( "Add Context", &ToggleContextPicker, &focus_handle, + window, cx, ) } @@ -423,21 +449,25 @@ impl Render for ContextStrip { ) .opacity(0.5) .children( - KeyBinding::for_action_in(&ToggleContextPicker, &focus_handle, cx) - .map(|binding| binding.into_any_element()), + KeyBinding::for_action_in( + &ToggleContextPicker, + &focus_handle, + window, + ) + .map(|binding| binding.into_any_element()), ), ) } }) .children(context.iter().enumerate().map(|(i, context)| { - ContextPill::new_added( + ContextPill::added( context.clone(), dupe_names.contains(&context.name), self.focused_index == Some(i), Some({ let id = context.id; let context_store = self.context_store.clone(); - Rc::new(cx.listener(move |_this, _event, cx| { + Rc::new(cx.listener(move |_this, _event, _window, cx| { context_store.update(cx, |this, _cx| { this.remove_context(id); }); @@ -445,22 +475,24 @@ impl Render for ContextStrip { })) }), ) - .on_click(Rc::new(cx.listener(move |this, _, cx| { + .on_click(Rc::new(cx.listener(move |this, _, _window, cx| { this.focused_index = Some(i); cx.notify(); }))) })) .when_some(suggested_context, |el, suggested| { el.child( - ContextPill::new_suggested( + ContextPill::suggested( suggested.name().clone(), suggested.icon_path(), suggested.kind(), self.is_suggested_focused(&context), ) - .on_click(Rc::new(cx.listener(move |this, _event, cx| { - this.add_suggested_context(&suggested, cx); - }))), + .on_click(Rc::new(cx.listener( + move |this, _event, window, cx| { + this.add_suggested_context(&suggested, window, cx); + }, + ))), ) }) .when(!context.is_empty(), { @@ -470,19 +502,20 @@ impl Render for ContextStrip { .icon_size(IconSize::Small) .tooltip({ let focus_handle = focus_handle.clone(); - move |cx| { + move |window, cx| { Tooltip::for_action_in( "Remove All Context", &RemoveAllContext, &focus_handle, + window, cx, ) } }) .on_click(cx.listener({ let focus_handle = focus_handle.clone(); - move |_this, _event, cx| { - focus_handle.dispatch_action(&RemoveAllContext, cx); + move |_this, _event, window, cx| { + focus_handle.dispatch_action(&RemoveAllContext, window, cx); } })), ) @@ -510,11 +543,11 @@ pub enum SuggestedContext { File { name: SharedString, icon_path: Option, - buffer: WeakModel, + buffer: WeakEntity, }, Thread { name: SharedString, - thread: WeakModel, + thread: WeakEntity, }, } diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index 69dd5987a38d3a..a59534148161f4 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -20,8 +20,8 @@ use editor::{ use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _}; use fs::Fs; use gpui::{ - point, AppContext, FocusableView, Global, HighlightStyle, Model, Subscription, Task, - UpdateGlobal, View, ViewContext, WeakModel, WeakView, WindowContext, + point, App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, + UpdateGlobal, WeakEntity, Window, }; use language::{Buffer, Point, Selection, TransactionId}; use language_model::LanguageModelRegistry; @@ -51,17 +51,20 @@ pub fn init( fs: Arc, prompt_builder: Arc, telemetry: Arc, - cx: &mut AppContext, + cx: &mut App, ) { cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry)); - cx.observe_new_views(|_workspace: &mut Workspace, cx| { - let workspace = cx.view().clone(); + cx.observe_new(|_workspace: &mut Workspace, window, cx| { + let Some(window) = window else { + return; + }; + let workspace = cx.entity().clone(); InlineAssistant::update_global(cx, |inline_assistant, cx| { - inline_assistant.register_workspace(&workspace, cx) + inline_assistant.register_workspace(&workspace, window, cx) }); - cx.observe_flag::({ - |is_assistant2_enabled, _view, cx| { + cx.observe_flag::(window, { + |is_assistant2_enabled, _workspace, _window, cx| { InlineAssistant::update_global(cx, |inline_assistant, _cx| { inline_assistant.is_assistant2_enabled = is_assistant2_enabled; }); @@ -75,17 +78,17 @@ pub fn init( const PROMPT_HISTORY_MAX_LEN: usize = 20; enum InlineAssistTarget { - Editor(View), - Terminal(View), + Editor(Entity), + Terminal(Entity), } pub struct InlineAssistant { next_assist_id: InlineAssistId, next_assist_group_id: InlineAssistGroupId, assists: HashMap, - assists_by_editor: HashMap, EditorInlineAssists>, + assists_by_editor: HashMap, EditorInlineAssists>, assist_groups: HashMap, - confirmed_assists: HashMap>, + confirmed_assists: HashMap>, prompt_history: VecDeque, prompt_builder: Arc, telemetry: Arc, @@ -116,13 +119,19 @@ impl InlineAssistant { } } - pub fn register_workspace(&mut self, workspace: &View, cx: &mut WindowContext) { - cx.subscribe(workspace, |workspace, event, cx| { - Self::update_global(cx, |this, cx| { - this.handle_workspace_event(workspace, event, cx) - }); - }) - .detach(); + pub fn register_workspace( + &mut self, + workspace: &Entity, + window: &mut Window, + cx: &mut App, + ) { + window + .subscribe(workspace, cx, |workspace, event, window, cx| { + Self::update_global(cx, |this, cx| { + this.handle_workspace_event(workspace, event, window, cx) + }); + }) + .detach(); let workspace = workspace.downgrade(); cx.observe_global::(move |cx| { @@ -142,9 +151,10 @@ impl InlineAssistant { fn handle_workspace_event( &mut self, - workspace: View, + workspace: Entity, event: &workspace::Event, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { match event { workspace::Event::UserSavedItem { item, .. } => { @@ -154,14 +164,14 @@ impl InlineAssistant { for assist_id in editor_assists.assist_ids.clone() { let assist = &self.assists[&assist_id]; if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) { - self.finish_assist(assist_id, false, cx) + self.finish_assist(assist_id, false, window, cx) } } } } } workspace::Event::ItemAdded { item } => { - self.register_workspace_item(&workspace, item.as_ref(), cx); + self.register_workspace_item(&workspace, item.as_ref(), window, cx); } _ => (), } @@ -169,9 +179,10 @@ impl InlineAssistant { fn register_workspace_item( &mut self, - workspace: &View, + workspace: &Entity, item: &dyn ItemHandle, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let is_assistant2_enabled = self.is_assistant2_enabled; @@ -185,18 +196,22 @@ impl InlineAssistant { editor.add_code_action_provider( Rc::new(AssistantCodeActionProvider { - editor: cx.view().downgrade(), + editor: cx.entity().downgrade(), workspace: workspace.downgrade(), thread_store, }), + window, cx, ); // Remove the Assistant1 code action provider, as it still might be registered. - editor.remove_code_action_provider("assistant".into(), cx); + editor.remove_code_action_provider("assistant".into(), window, cx); } else { - editor - .remove_code_action_provider(ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), cx); + editor.remove_code_action_provider( + ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), + window, + cx, + ); } }); } @@ -204,15 +219,17 @@ impl InlineAssistant { pub fn inline_assist( workspace: &mut Workspace, - _action: &zed_actions::InlineAssist, - cx: &mut ViewContext, + _action: &zed_actions::assistant::InlineAssist, + window: &mut Window, + cx: &mut Context, ) { let settings = AssistantSettings::get_global(cx); if !settings.enabled { return; } - let Some(inline_assist_target) = Self::resolve_inline_assist_target(workspace, cx) else { + let Some(inline_assist_target) = Self::resolve_inline_assist_target(workspace, window, cx) + else { return; }; @@ -226,24 +243,37 @@ impl InlineAssistant { .panel::(cx) .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade()); - let handle_assist = |cx: &mut ViewContext| match inline_assist_target { - InlineAssistTarget::Editor(active_editor) => { - InlineAssistant::update_global(cx, |assistant, cx| { - assistant.assist(&active_editor, cx.view().downgrade(), thread_store, cx) - }) - } - InlineAssistTarget::Terminal(active_terminal) => { - TerminalInlineAssistant::update_global(cx, |assistant, cx| { - assistant.assist(&active_terminal, cx.view().downgrade(), thread_store, cx) - }) - } - }; + let handle_assist = + |window: &mut Window, cx: &mut Context| match inline_assist_target { + InlineAssistTarget::Editor(active_editor) => { + InlineAssistant::update_global(cx, |assistant, cx| { + assistant.assist( + &active_editor, + cx.entity().downgrade(), + thread_store, + window, + cx, + ) + }) + } + InlineAssistTarget::Terminal(active_terminal) => { + TerminalInlineAssistant::update_global(cx, |assistant, cx| { + assistant.assist( + &active_terminal, + cx.entity().downgrade(), + thread_store, + window, + cx, + ) + }) + } + }; if is_authenticated() { - handle_assist(cx); + handle_assist(window, cx); } else { - cx.spawn(|_workspace, mut cx| async move { - let Some(task) = cx.update(|cx| { + cx.spawn_in(window, |_workspace, mut cx| async move { + let Some(task) = cx.update(|_, cx| { LanguageModelRegistry::read_global(cx) .active_provider() .map_or(None, |provider| Some(provider.authenticate(cx))) @@ -260,8 +290,10 @@ impl InlineAssistant { .ok(); if let Some(answer) = answer { if answer == 0 { - cx.update(|cx| cx.dispatch_action(Box::new(ShowConfiguration))) - .ok(); + cx.update(|window, cx| { + window.dispatch_action(Box::new(ShowConfiguration), cx) + }) + .ok(); } } return Ok(()); @@ -273,17 +305,18 @@ impl InlineAssistant { .detach_and_log_err(cx); if is_authenticated() { - handle_assist(cx); + handle_assist(window, cx); } } } pub fn assist( &mut self, - editor: &View, - workspace: WeakView, - thread_store: Option>, - cx: &mut WindowContext, + editor: &Entity, + workspace: WeakEntity, + thread_store: Option>, + window: &mut Window, + cx: &mut App, ) { let (snapshot, initial_selections) = editor.update(cx, |editor, cx| { ( @@ -320,22 +353,18 @@ impl InlineAssistant { let newest_selection = newest_selection.unwrap(); let mut codegen_ranges = Vec::new(); - for (excerpt_id, buffer, buffer_range) in - snapshot.excerpts_in_ranges(selections.iter().map(|selection| { + for (buffer, buffer_range, excerpt_id) in + snapshot.ranges_to_buffer_ranges(selections.iter().map(|selection| { snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end) })) { - let start = Anchor { - buffer_id: Some(buffer.remote_id()), + let anchor_range = Anchor::range_in_buffer( excerpt_id, - text_anchor: buffer.anchor_before(buffer_range.start), - }; - let end = Anchor { - buffer_id: Some(buffer.remote_id()), - excerpt_id, - text_anchor: buffer.anchor_after(buffer_range.end), - }; - codegen_ranges.push(start..end); + buffer.remote_id(), + buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end), + ); + + codegen_ranges.push(anchor_range); if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { self.telemetry.report_assistant_event(AssistantEvent { @@ -353,16 +382,15 @@ impl InlineAssistant { } let assist_group_id = self.next_assist_group_id.post_inc(); - let prompt_buffer = cx.new_model(|cx| { - MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx) - }); + let prompt_buffer = + cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx)); let mut assists = Vec::new(); let mut assist_to_focus = None; for range in codegen_ranges { let assist_id = self.next_assist_id.post_inc(); - let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone())); - let codegen = cx.new_model(|cx| { + let context_store = cx.new(|_cx| ContextStore::new(workspace.clone())); + let codegen = cx.new(|cx| { BufferCodegen::new( editor.read(cx).buffer().clone(), range.clone(), @@ -375,7 +403,7 @@ impl InlineAssistant { }); let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { PromptEditor::new_buffer( assist_id, gutter_dimensions.clone(), @@ -386,6 +414,7 @@ impl InlineAssistant { context_store, workspace.clone(), thread_store.clone(), + window, cx, ) }); @@ -416,7 +445,7 @@ impl InlineAssistant { let editor_assists = self .assists_by_editor .entry(editor.downgrade()) - .or_insert_with(|| EditorInlineAssists::new(&editor, cx)); + .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx)); let mut assist_group = InlineAssistGroup::new(); for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists { let codegen = prompt_editor.read(cx).codegen().clone(); @@ -433,6 +462,7 @@ impl InlineAssistant { range, codegen, workspace.clone(), + window, cx, ), ); @@ -442,25 +472,26 @@ impl InlineAssistant { self.assist_groups.insert(assist_group_id, assist_group); if let Some(assist_id) = assist_to_focus { - self.focus_assist(assist_id, cx); + self.focus_assist(assist_id, window, cx); } } #[allow(clippy::too_many_arguments)] pub fn suggest_assist( &mut self, - editor: &View, + editor: &Entity, mut range: Range, initial_prompt: String, initial_transaction_id: Option, focus: bool, - workspace: WeakView, - thread_store: Option>, - cx: &mut WindowContext, + workspace: WeakEntity, + thread_store: Option>, + window: &mut Window, + cx: &mut App, ) -> InlineAssistId { let assist_group_id = self.next_assist_group_id.post_inc(); - let prompt_buffer = cx.new_model(|cx| Buffer::local(&initial_prompt, cx)); - let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx)); + let prompt_buffer = cx.new(|cx| Buffer::local(&initial_prompt, cx)); + let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx)); let assist_id = self.next_assist_id.post_inc(); @@ -471,9 +502,9 @@ impl InlineAssistant { range.end = range.end.bias_right(&snapshot); } - let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone())); + let context_store = cx.new(|_cx| ContextStore::new(workspace.clone())); - let codegen = cx.new_model(|cx| { + let codegen = cx.new(|cx| { BufferCodegen::new( editor.read(cx).buffer().clone(), range.clone(), @@ -486,7 +517,7 @@ impl InlineAssistant { }); let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { PromptEditor::new_buffer( assist_id, gutter_dimensions.clone(), @@ -497,6 +528,7 @@ impl InlineAssistant { context_store, workspace.clone(), thread_store, + window, cx, ) }); @@ -507,7 +539,7 @@ impl InlineAssistant { let editor_assists = self .assists_by_editor .entry(editor.downgrade()) - .or_insert_with(|| EditorInlineAssists::new(&editor, cx)); + .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx)); let mut assist_group = InlineAssistGroup::new(); self.assists.insert( @@ -522,6 +554,7 @@ impl InlineAssistant { range, codegen.clone(), workspace.clone(), + window, cx, ), ); @@ -530,7 +563,7 @@ impl InlineAssistant { self.assist_groups.insert(assist_group_id, assist_group); if focus { - self.focus_assist(assist_id, cx); + self.focus_assist(assist_id, window, cx); } assist_id @@ -538,10 +571,10 @@ impl InlineAssistant { fn insert_assist_blocks( &self, - editor: &View, + editor: &Entity, range: &Range, - prompt_editor: &View>, - cx: &mut WindowContext, + prompt_editor: &Entity>, + cx: &mut App, ) -> [CustomBlockId; 2] { let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| { prompt_editor @@ -578,7 +611,7 @@ impl InlineAssistant { }) } - fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut App) { let assist = &self.assists[&assist_id]; let Some(decorations) = assist.decorations.as_ref() else { return; @@ -619,11 +652,7 @@ impl InlineAssistant { .ok(); } - fn handle_prompt_editor_focus_out( - &mut self, - assist_id: InlineAssistId, - cx: &mut WindowContext, - ) { + fn handle_prompt_editor_focus_out(&mut self, assist_id: InlineAssistId, cx: &mut App) { let assist = &self.assists[&assist_id]; let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap(); if assist_group.active_assist_id == Some(assist_id) { @@ -642,26 +671,27 @@ impl InlineAssistant { fn handle_prompt_editor_event( &mut self, - prompt_editor: View>, + prompt_editor: Entity>, event: &PromptEditorEvent, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let assist_id = prompt_editor.read(cx).id(); match event { PromptEditorEvent::StartRequested => { - self.start_assist(assist_id, cx); + self.start_assist(assist_id, window, cx); } PromptEditorEvent::StopRequested => { self.stop_assist(assist_id, cx); } PromptEditorEvent::ConfirmRequested { execute: _ } => { - self.finish_assist(assist_id, false, cx); + self.finish_assist(assist_id, false, window, cx); } PromptEditorEvent::CancelRequested => { - self.finish_assist(assist_id, true, cx); + self.finish_assist(assist_id, true, window, cx); } PromptEditorEvent::DismissRequested => { - self.dismiss_assist(assist_id, cx); + self.dismiss_assist(assist_id, window, cx); } PromptEditorEvent::Resized { .. } => { // This only matters for the terminal inline assistant @@ -669,7 +699,7 @@ impl InlineAssistant { } } - fn handle_editor_newline(&mut self, editor: View, cx: &mut WindowContext) { + fn handle_editor_newline(&mut self, editor: Entity, window: &mut Window, cx: &mut App) { let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else { return; }; @@ -687,9 +717,9 @@ impl InlineAssistant { if assist_range.contains(&selection.start) && assist_range.contains(&selection.end) { if matches!(assist.codegen.read(cx).status(cx), CodegenStatus::Pending) { - self.dismiss_assist(*assist_id, cx); + self.dismiss_assist(*assist_id, window, cx); } else { - self.finish_assist(*assist_id, false, cx); + self.finish_assist(*assist_id, false, window, cx); } return; @@ -700,7 +730,7 @@ impl InlineAssistant { cx.propagate(); } - fn handle_editor_cancel(&mut self, editor: View, cx: &mut WindowContext) { + fn handle_editor_cancel(&mut self, editor: Entity, window: &mut Window, cx: &mut App) { let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else { return; }; @@ -720,7 +750,7 @@ impl InlineAssistant { if assist_range.contains(&selection.start) && assist_range.contains(&selection.end) { - self.focus_assist(*assist_id, cx); + self.focus_assist(*assist_id, window, cx); return; } else { let distance_from_selection = assist_range @@ -747,22 +777,27 @@ impl InlineAssistant { } if let Some((&assist_id, _)) = closest_assist_fallback { - self.focus_assist(assist_id, cx); + self.focus_assist(assist_id, window, cx); } } cx.propagate(); } - fn handle_editor_release(&mut self, editor: WeakView, cx: &mut WindowContext) { + fn handle_editor_release( + &mut self, + editor: WeakEntity, + window: &mut Window, + cx: &mut App, + ) { if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor) { for assist_id in editor_assists.assist_ids.clone() { - self.finish_assist(assist_id, true, cx); + self.finish_assist(assist_id, true, window, cx); } } } - fn handle_editor_change(&mut self, editor: View, cx: &mut WindowContext) { + fn handle_editor_change(&mut self, editor: Entity, window: &mut Window, cx: &mut App) { let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else { return; }; @@ -782,16 +817,17 @@ impl InlineAssistant { .0 as f32 - scroll_lock.distance_from_top; if target_scroll_top != scroll_position.y { - editor.set_scroll_position(point(scroll_position.x, target_scroll_top), cx); + editor.set_scroll_position(point(scroll_position.x, target_scroll_top), window, cx); } }); } fn handle_editor_event( &mut self, - editor: View, + editor: Entity, event: &EditorEvent, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) else { return; @@ -815,7 +851,7 @@ impl InlineAssistant { .iter() .any(|range| range.overlaps(&assist_range)) { - self.finish_assist(assist_id, false, cx); + self.finish_assist(assist_id, false, window, cx); } } } @@ -843,7 +879,11 @@ impl InlineAssistant { for assist_id in editor_assists.assist_ids.clone() { let assist = &self.assists[&assist_id]; if let Some(decorations) = assist.decorations.as_ref() { - if decorations.prompt_editor.focus_handle(cx).is_focused(cx) { + if decorations + .prompt_editor + .focus_handle(cx) + .is_focused(window) + { return; } } @@ -855,18 +895,24 @@ impl InlineAssistant { } } - pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) { + pub fn finish_assist( + &mut self, + assist_id: InlineAssistId, + undo: bool, + window: &mut Window, + cx: &mut App, + ) { if let Some(assist) = self.assists.get(&assist_id) { let assist_group_id = assist.group_id; if self.assist_groups[&assist_group_id].linked { - for assist_id in self.unlink_assist_group(assist_group_id, cx) { - self.finish_assist(assist_id, undo, cx); + for assist_id in self.unlink_assist_group(assist_group_id, window, cx) { + self.finish_assist(assist_id, undo, window, cx); } return; } } - self.dismiss_assist(assist_id, cx); + self.dismiss_assist(assist_id, window, cx); if let Some(assist) = self.assists.remove(&assist_id) { if let hash_map::Entry::Occupied(mut entry) = self.assist_groups.entry(assist.group_id) @@ -901,7 +947,7 @@ impl InlineAssistant { let ranges = snapshot.range_to_buffer_ranges(assist.range.clone()); ranges .first() - .and_then(|(excerpt, _)| excerpt.buffer().language()) + .and_then(|(buffer, _, _)| buffer.language()) .map(|language| language.name()) }); report_assistant_event( @@ -935,7 +981,12 @@ impl InlineAssistant { } } - fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool { + fn dismiss_assist( + &mut self, + assist_id: InlineAssistId, + window: &mut Window, + cx: &mut App, + ) -> bool { let Some(assist) = self.assists.get_mut(&assist_id) else { return false; }; @@ -956,9 +1007,9 @@ impl InlineAssistant { if decorations .prompt_editor .focus_handle(cx) - .contains_focused(cx) + .contains_focused(window, cx) { - self.focus_next_assist(assist_id, cx); + self.focus_next_assist(assist_id, window, cx); } if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) { @@ -975,7 +1026,7 @@ impl InlineAssistant { true } - fn focus_next_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + fn focus_next_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) { let Some(assist) = self.assists.get(&assist_id) else { return; }; @@ -995,15 +1046,18 @@ impl InlineAssistant { for assist_id in assist_ids { let assist = &self.assists[assist_id]; if assist.decorations.is_some() { - self.focus_assist(*assist_id, cx); + self.focus_assist(*assist_id, window, cx); return; } } - assist.editor.update(cx, |editor, cx| editor.focus(cx)).ok(); + assist + .editor + .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx))) + .ok(); } - fn focus_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + fn focus_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) { let Some(assist) = self.assists.get(&assist_id) else { return; }; @@ -1011,16 +1065,21 @@ impl InlineAssistant { if let Some(decorations) = assist.decorations.as_ref() { decorations.prompt_editor.update(cx, |prompt_editor, cx| { prompt_editor.editor.update(cx, |editor, cx| { - editor.focus(cx); - editor.select_all(&SelectAll, cx); + window.focus(&editor.focus_handle(cx)); + editor.select_all(&SelectAll, window, cx); }) }); } - self.scroll_to_assist(assist_id, cx); + self.scroll_to_assist(assist_id, window, cx); } - pub fn scroll_to_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + pub fn scroll_to_assist( + &mut self, + assist_id: InlineAssistId, + window: &mut Window, + cx: &mut App, + ) { let Some(assist) = self.assists.get(&assist_id) else { return; }; @@ -1030,7 +1089,7 @@ impl InlineAssistant { let position = assist.range.start; editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| { + editor.change_selections(None, window, cx, |selections| { selections.select_anchor_ranges([position..position]) }); @@ -1046,7 +1105,7 @@ impl InlineAssistant { .unwrap() .0 as f32; } else { - let snapshot = editor.snapshot(cx); + let snapshot = editor.snapshot(window, cx); let start_row = assist .range .start @@ -1063,13 +1122,16 @@ impl InlineAssistant { let scroll_bottom = scroll_top + height_in_lines; if scroll_target_top < scroll_top { - editor.set_scroll_position(point(0., scroll_target_top), cx); + editor.set_scroll_position(point(0., scroll_target_top), window, cx); } else if scroll_target_bottom > scroll_bottom { if (scroll_target_bottom - scroll_target_top) <= height_in_lines { - editor - .set_scroll_position(point(0., scroll_target_bottom - height_in_lines), cx); + editor.set_scroll_position( + point(0., scroll_target_bottom - height_in_lines), + window, + cx, + ); } else { - editor.set_scroll_position(point(0., scroll_target_top), cx); + editor.set_scroll_position(point(0., scroll_target_top), window, cx); } } }); @@ -1078,7 +1140,8 @@ impl InlineAssistant { fn unlink_assist_group( &mut self, assist_group_id: InlineAssistGroupId, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Vec { let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap(); assist_group.linked = false; @@ -1087,13 +1150,13 @@ impl InlineAssistant { if let Some(editor_decorations) = assist.decorations.as_ref() { editor_decorations .prompt_editor - .update(cx, |prompt_editor, cx| prompt_editor.unlink(cx)); + .update(cx, |prompt_editor, cx| prompt_editor.unlink(window, cx)); } } assist_group.assist_ids.clone() } - pub fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + pub fn start_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -1102,8 +1165,8 @@ impl InlineAssistant { let assist_group_id = assist.group_id; if self.assist_groups[&assist_group_id].linked { - for assist_id in self.unlink_assist_group(assist_group_id, cx) { - self.start_assist(assist_id, cx); + for assist_id in self.unlink_assist_group(assist_group_id, window, cx) { + self.start_assist(assist_id, window, cx); } return; } @@ -1124,7 +1187,7 @@ impl InlineAssistant { .log_err(); } - pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) { + pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut App) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -1134,7 +1197,7 @@ impl InlineAssistant { assist.codegen.update(cx, |codegen, cx| codegen.stop(cx)); } - fn update_editor_highlights(&self, editor: &View, cx: &mut WindowContext) { + fn update_editor_highlights(&self, editor: &Entity, cx: &mut App) { let mut gutter_pending_ranges = Vec::new(); let mut gutter_transformed_ranges = Vec::new(); let mut foreground_ranges = Vec::new(); @@ -1227,9 +1290,10 @@ impl InlineAssistant { fn update_editor_blocks( &mut self, - editor: &View, + editor: &Entity, assist_id: InlineAssistId, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let Some(assist) = self.assists.get_mut(&assist_id) else { return; @@ -1259,10 +1323,9 @@ impl InlineAssistant { )) .unwrap(); - let deleted_lines_editor = cx.new_view(|cx| { - let multi_buffer = cx.new_model(|_| { - MultiBuffer::without_headers(language::Capability::ReadOnly) - }); + let deleted_lines_editor = cx.new(|cx| { + let multi_buffer = + cx.new(|_| MultiBuffer::without_headers(language::Capability::ReadOnly)); multi_buffer.update(cx, |multi_buffer, cx| { multi_buffer.push_excerpts( old_buffer.clone(), @@ -1275,14 +1338,14 @@ impl InlineAssistant { }); enum DeletedLines {} - let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx); + let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_gutter(false, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); editor.set_show_scrollbars(false, cx); editor.set_read_only(true); - editor.set_show_inline_completions(Some(false), cx); + editor.set_show_inline_completions(Some(false), window, cx); editor.highlight_rows::( Anchor::min()..Anchor::max(), cx.theme().status().deleted_background, @@ -1303,7 +1366,7 @@ impl InlineAssistant { .block_mouse_down() .bg(cx.theme().status().deleted_background) .size_full() - .h(height as f32 * cx.line_height()) + .h(height as f32 * cx.window.line_height()) .pl(cx.gutter_dimensions.full_width()) .child(deleted_lines_editor.clone()) .into_any_element() @@ -1321,13 +1384,14 @@ impl InlineAssistant { fn resolve_inline_assist_target( workspace: &mut Workspace, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Option { if let Some(terminal_panel) = workspace.panel::(cx) { if terminal_panel .read(cx) .focus_handle(cx) - .contains_focused(cx) + .contains_focused(window, cx) { if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { pane.read(cx) @@ -1370,13 +1434,13 @@ struct InlineAssistScrollLock { impl EditorInlineAssists { #[allow(clippy::too_many_arguments)] - fn new(editor: &View, cx: &mut WindowContext) -> Self { + fn new(editor: &Entity, window: &mut Window, cx: &mut App) -> Self { let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(()); Self { assist_ids: Vec::new(), scroll_lock: None, highlight_updates: highlight_updates_tx, - _update_highlights: cx.spawn(|mut cx| { + _update_highlights: cx.spawn(|cx| { let editor = editor.downgrade(); async move { while let Ok(()) = highlight_updates_rx.changed().await { @@ -1389,47 +1453,43 @@ impl EditorInlineAssists { } }), _subscriptions: vec![ - cx.observe_release(editor, { + cx.observe_release_in(editor, window, { let editor = editor.downgrade(); - |_, cx| { + |_, window, cx| { InlineAssistant::update_global(cx, |this, cx| { - this.handle_editor_release(editor, cx); + this.handle_editor_release(editor, window, cx); }) } }), - cx.observe(editor, move |editor, cx| { + window.observe(editor, cx, move |editor, window, cx| { InlineAssistant::update_global(cx, |this, cx| { - this.handle_editor_change(editor, cx) + this.handle_editor_change(editor, window, cx) }) }), - cx.subscribe(editor, move |editor, event, cx| { + window.subscribe(editor, cx, move |editor, event, window, cx| { InlineAssistant::update_global(cx, |this, cx| { - this.handle_editor_event(editor, event, cx) + this.handle_editor_event(editor, event, window, cx) }) }), editor.update(cx, |editor, cx| { - let editor_handle = cx.view().downgrade(); - editor.register_action( - move |_: &editor::actions::Newline, cx: &mut WindowContext| { - InlineAssistant::update_global(cx, |this, cx| { - if let Some(editor) = editor_handle.upgrade() { - this.handle_editor_newline(editor, cx) - } - }) - }, - ) + let editor_handle = cx.entity().downgrade(); + editor.register_action(move |_: &editor::actions::Newline, window, cx| { + InlineAssistant::update_global(cx, |this, cx| { + if let Some(editor) = editor_handle.upgrade() { + this.handle_editor_newline(editor, window, cx) + } + }) + }) }), editor.update(cx, |editor, cx| { - let editor_handle = cx.view().downgrade(); - editor.register_action( - move |_: &editor::actions::Cancel, cx: &mut WindowContext| { - InlineAssistant::update_global(cx, |this, cx| { - if let Some(editor) = editor_handle.upgrade() { - this.handle_editor_cancel(editor, cx) - } - }) - }, - ) + let editor_handle = cx.entity().downgrade(); + editor.register_action(move |_: &editor::actions::Cancel, window, cx| { + InlineAssistant::update_global(cx, |this, cx| { + if let Some(editor) = editor_handle.upgrade() { + this.handle_editor_cancel(editor, window, cx) + } + }) + }) }), ], } @@ -1452,7 +1512,7 @@ impl InlineAssistGroup { } } -fn build_assist_editor_renderer(editor: &View>) -> RenderBlock { +fn build_assist_editor_renderer(editor: &Entity>) -> RenderBlock { let editor = editor.clone(); Arc::new(move |cx: &mut BlockContext| { @@ -1477,11 +1537,11 @@ impl InlineAssistGroupId { pub struct InlineAssist { group_id: InlineAssistGroupId, range: Range, - editor: WeakView, + editor: WeakEntity, decorations: Option, - codegen: Model, + codegen: Entity, _subscriptions: Vec, - workspace: WeakView, + workspace: WeakEntity, } impl InlineAssist { @@ -1489,14 +1549,15 @@ impl InlineAssist { fn new( assist_id: InlineAssistId, group_id: InlineAssistGroupId, - editor: &View, - prompt_editor: &View>, + editor: &Entity, + prompt_editor: &Entity>, prompt_block_id: CustomBlockId, end_block_id: CustomBlockId, range: Range, - codegen: Model, - workspace: WeakView, - cx: &mut WindowContext, + codegen: Entity, + workspace: WeakEntity, + window: &mut Window, + cx: &mut App, ) -> Self { let prompt_editor_focus_handle = prompt_editor.focus_handle(cx); InlineAssist { @@ -1512,24 +1573,24 @@ impl InlineAssist { codegen: codegen.clone(), workspace: workspace.clone(), _subscriptions: vec![ - cx.on_focus_in(&prompt_editor_focus_handle, move |cx| { + window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| { InlineAssistant::update_global(cx, |this, cx| { this.handle_prompt_editor_focus_in(assist_id, cx) }) }), - cx.on_focus_out(&prompt_editor_focus_handle, move |_, cx| { + window.on_focus_out(&prompt_editor_focus_handle, cx, move |_, _, cx| { InlineAssistant::update_global(cx, |this, cx| { this.handle_prompt_editor_focus_out(assist_id, cx) }) }), - cx.subscribe(prompt_editor, |prompt_editor, event, cx| { + window.subscribe(prompt_editor, cx, |prompt_editor, event, window, cx| { InlineAssistant::update_global(cx, |this, cx| { - this.handle_prompt_editor_event(prompt_editor, event, cx) + this.handle_prompt_editor_event(prompt_editor, event, window, cx) }) }), - cx.observe(&codegen, { + window.observe(&codegen, cx, { let editor = editor.downgrade(); - move |_, cx| { + move |_, window, cx| { if let Some(editor) = editor.upgrade() { InlineAssistant::update_global(cx, |this, cx| { if let Some(editor_assists) = @@ -1538,14 +1599,14 @@ impl InlineAssist { editor_assists.highlight_updates.send(()).ok(); } - this.update_editor_blocks(&editor, assist_id, cx); + this.update_editor_blocks(&editor, assist_id, window, cx); }) } } }), - cx.subscribe(&codegen, move |codegen, event, cx| { + window.subscribe(&codegen, cx, move |codegen, event, window, cx| { InlineAssistant::update_global(cx, |this, cx| match event { - CodegenEvent::Undone => this.finish_assist(assist_id, false, cx), + CodegenEvent::Undone => this.finish_assist(assist_id, false, window, cx), CodegenEvent::Finished => { let assist = if let Some(assist) = this.assists.get(&assist_id) { assist @@ -1572,7 +1633,7 @@ impl InlineAssist { } if assist.decorations.is_none() { - this.finish_assist(assist_id, false, cx); + this.finish_assist(assist_id, false, window, cx); } } }) @@ -1581,7 +1642,7 @@ impl InlineAssist { } } - fn user_prompt(&self, cx: &AppContext) -> Option { + fn user_prompt(&self, cx: &App) -> Option { let decorations = self.decorations.as_ref()?; Some(decorations.prompt_editor.read(cx).prompt(cx)) } @@ -1589,15 +1650,15 @@ impl InlineAssist { struct InlineAssistDecorations { prompt_block_id: CustomBlockId, - prompt_editor: View>, + prompt_editor: Entity>, removed_line_block_ids: HashSet, end_block_id: CustomBlockId, } struct AssistantCodeActionProvider { - editor: WeakView, - workspace: WeakView, - thread_store: Option>, + editor: WeakEntity, + workspace: WeakEntity, + thread_store: Option>, } const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2"; @@ -1609,9 +1670,10 @@ impl CodeActionProvider for AssistantCodeActionProvider { fn code_actions( &self, - buffer: &Model, + buffer: &Entity, range: Range, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task>> { if !AssistantSettings::get_global(cx).enabled { return Task::ready(Ok(Vec::new())); @@ -1660,16 +1722,17 @@ impl CodeActionProvider for AssistantCodeActionProvider { fn apply_code_action( &self, - buffer: Model, + buffer: Entity, action: CodeAction, excerpt_id: ExcerptId, _push_to_history: bool, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task> { let editor = self.editor.clone(); let workspace = self.workspace.clone(); let thread_store = self.thread_store.clone(); - cx.spawn(|mut cx| async move { + window.spawn(cx, |mut cx| async move { let editor = editor.upgrade().context("editor was released")?; let range = editor .update(&mut cx, |editor, cx| { @@ -1708,7 +1771,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { })? .context("invalid range")?; - cx.update_global(|assistant: &mut InlineAssistant, cx| { + cx.update_global(|assistant: &mut InlineAssistant, window, cx| { let assist_id = assistant.suggest_assist( &editor, range, @@ -1717,9 +1780,10 @@ impl CodeActionProvider for AssistantCodeActionProvider { true, workspace, thread_store, + window, cx, ); - assistant.start_assist(assist_id, cx); + assistant.start_assist(assist_id, window, cx); })?; Ok(ProjectTransaction::default()) diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs index 86f980d3db61c5..a8c4c3d0f9f996 100644 --- a/crates/assistant2/src/inline_prompt_editor.rs +++ b/crates/assistant2/src/inline_prompt_editor.rs @@ -16,9 +16,8 @@ use editor::{ use feature_flags::{FeatureFlagAppExt as _, ZedPro}; use fs::Fs; use gpui::{ - anchored, deferred, point, AnyElement, AppContext, ClickEvent, CursorStyle, EventEmitter, - FocusHandle, FocusableView, FontWeight, Model, Subscription, TextStyle, View, ViewContext, - WeakModel, WeakView, WindowContext, + anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity, + EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, }; use language_model::{LanguageModel, LanguageModelRegistry}; use language_model_selector::LanguageModelSelector; @@ -35,12 +34,12 @@ use util::ResultExt; use workspace::Workspace; pub struct PromptEditor { - pub editor: View, + pub editor: Entity, mode: PromptEditorMode, - context_store: Model, - context_strip: View, + context_store: Entity, + context_strip: Entity, context_picker_menu_handle: PopoverMenuHandle, - model_selector: View, + model_selector: Entity, model_selector_menu_handle: PopoverMenuHandle, edited_since_done: bool, prompt_history: VecDeque, @@ -56,7 +55,7 @@ pub struct PromptEditor { impl EventEmitter for PromptEditor {} impl Render for PromptEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; let mut buttons = Vec::new(); @@ -87,7 +86,7 @@ impl Render for PromptEditor { PromptEditorMode::Terminal { .. } => Pixels::from(8.0), }; - buttons.extend(self.render_buttons(cx)); + buttons.extend(self.render_buttons(window, cx)); v_flex() .key_context("PromptEditor") @@ -163,9 +162,7 @@ impl Render for PromptEditor { el.child( div() .id("error") - .tooltip(move |cx| { - Tooltip::text(error_message.clone(), cx) - }) + .tooltip(Tooltip::text(error_message)) .child( Icon::new(IconName::XCircle) .size(IconSize::Small) @@ -179,7 +176,7 @@ impl Render for PromptEditor { h_flex() .w_full() .justify_between() - .child(div().flex_1().child(self.render_editor(cx))) + .child(div().flex_1().child(self.render_editor(window, cx))) .child( WithRemSize::new(ui_font_size) .flex() @@ -209,8 +206,8 @@ impl Render for PromptEditor { } } -impl FocusableView for PromptEditor { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for PromptEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) } } @@ -218,47 +215,50 @@ impl FocusableView for PromptEditor { impl PromptEditor { const MAX_LINES: u8 = 8; - fn codegen_status<'a>(&'a self, cx: &'a AppContext) -> &'a CodegenStatus { + fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus { match &self.mode { PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx), PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status, } } - fn subscribe_to_editor(&mut self, cx: &mut ViewContext) { + fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context) { self.editor_subscriptions.clear(); - self.editor_subscriptions - .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events)); + self.editor_subscriptions.push(cx.subscribe_in( + &self.editor, + window, + Self::handle_prompt_editor_events, + )); } pub fn set_show_cursor_when_unfocused( &mut self, show_cursor_when_unfocused: bool, - cx: &mut ViewContext, + cx: &mut Context, ) { self.editor.update(cx, |editor, cx| { editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx) }); } - pub fn unlink(&mut self, cx: &mut ViewContext) { + pub fn unlink(&mut self, window: &mut Window, cx: &mut Context) { let prompt = self.prompt(cx); - let focus = self.editor.focus_handle(cx).contains_focused(cx); - self.editor = cx.new_view(|cx| { - let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx); + let focus = self.editor.focus_handle(cx).contains_focused(window, cx); + self.editor = cx.new(|cx| { + let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor.set_placeholder_text(Self::placeholder_text(&self.mode, cx), cx); + editor.set_placeholder_text(Self::placeholder_text(&self.mode, window, cx), cx); editor.set_placeholder_text("Add a prompt…", cx); - editor.set_text(prompt, cx); + editor.set_text(prompt, window, cx); if focus { - editor.focus(cx); + window.focus(&editor.focus_handle(cx)); } editor }); - self.subscribe_to_editor(cx); + self.subscribe_to_editor(window, cx); } - pub fn placeholder_text(mode: &PromptEditorMode, cx: &WindowContext) -> String { + pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String { let action = match mode { PromptEditorMode::Buffer { codegen, .. } => { if codegen.read(cx).is_insertion { @@ -270,46 +270,51 @@ impl PromptEditor { PromptEditorMode::Terminal { .. } => "Generate", }; - let assistant_panel_keybinding = ui::text_for_action(&crate::ToggleFocus, cx) - .map(|keybinding| format!("{keybinding} to chat ― ")) - .unwrap_or_default(); + let assistant_panel_keybinding = + ui::text_for_action(&zed_actions::assistant::ToggleFocus, window) + .map(|keybinding| format!("{keybinding} to chat ― ")) + .unwrap_or_default(); format!("{action}… ({assistant_panel_keybinding}↓↑ for history)") } - pub fn prompt(&self, cx: &AppContext) -> String { + pub fn prompt(&self, cx: &App) -> String { self.editor.read(cx).text(cx) } - fn toggle_rate_limit_notice(&mut self, _: &ClickEvent, cx: &mut ViewContext) { + fn toggle_rate_limit_notice( + &mut self, + _: &ClickEvent, + window: &mut Window, + cx: &mut Context, + ) { self.show_rate_limit_notice = !self.show_rate_limit_notice; if self.show_rate_limit_notice { - cx.focus_view(&self.editor); + window.focus(&self.editor.focus_handle(cx)); } cx.notify(); } fn handle_prompt_editor_events( &mut self, - _: View, + _: &Entity, event: &EditorEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { EditorEvent::Edited { .. } => { - if let Some(workspace) = cx.window_handle().downcast::() { - workspace - .update(cx, |workspace, cx| { - let is_via_ssh = workspace - .project() - .update(cx, |project, _| project.is_via_ssh()); - - workspace - .client() - .telemetry() - .log_edit_event("inline assist", is_via_ssh); - }) - .log_err(); + if let Some(workspace) = window.root::().flatten() { + workspace.update(cx, |workspace, cx| { + let is_via_ssh = workspace + .project() + .update(cx, |project, _| project.is_via_ssh()); + + workspace + .client() + .telemetry() + .log_edit_event("inline assist", is_via_ssh); + }); } let prompt = self.editor.read(cx).text(cx); if self @@ -333,20 +338,40 @@ impl PromptEditor { } } - fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext) { - self.context_picker_menu_handle.toggle(cx); + fn toggle_context_picker( + &mut self, + _: &ToggleContextPicker, + window: &mut Window, + cx: &mut Context, + ) { + self.context_picker_menu_handle.toggle(window, cx); } - fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext) { - self.model_selector_menu_handle.toggle(cx); + fn toggle_model_selector( + &mut self, + _: &ToggleModelSelector, + window: &mut Window, + cx: &mut Context, + ) { + self.model_selector_menu_handle.toggle(window, cx); } - pub fn remove_all_context(&mut self, _: &RemoveAllContext, cx: &mut ViewContext) { + pub fn remove_all_context( + &mut self, + _: &RemoveAllContext, + _window: &mut Window, + cx: &mut Context, + ) { self.context_store.update(cx, |store, _cx| store.clear()); cx.notify(); } - fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { + fn cancel( + &mut self, + _: &editor::actions::Cancel, + _window: &mut Window, + cx: &mut Context, + ) { match self.codegen_status(cx) { CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => { cx.emit(PromptEditorEvent::CancelRequested); @@ -357,7 +382,7 @@ impl PromptEditor { } } - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { match self.codegen_status(cx) { CodegenStatus::Idle => { cx.emit(PromptEditorEvent::StartRequested); @@ -378,49 +403,49 @@ impl PromptEditor { } } - fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { if let Some(ix) = self.prompt_history_ix { if ix > 0 { self.prompt_history_ix = Some(ix - 1); let prompt = self.prompt_history[ix - 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_beginning(&Default::default(), cx); + editor.set_text(prompt, window, cx); + editor.move_to_beginning(&Default::default(), window, cx); }); } } else if !self.prompt_history.is_empty() { self.prompt_history_ix = Some(self.prompt_history.len() - 1); let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_beginning(&Default::default(), cx); + editor.set_text(prompt, window, cx); + editor.move_to_beginning(&Default::default(), window, cx); }); } } - fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { if let Some(ix) = self.prompt_history_ix { if ix < self.prompt_history.len() - 1 { self.prompt_history_ix = Some(ix + 1); let prompt = self.prompt_history[ix + 1].as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_end(&Default::default(), cx) + editor.set_text(prompt, window, cx); + editor.move_to_end(&Default::default(), window, cx) }); } else { self.prompt_history_ix = None; let prompt = self.pending_prompt.as_str(); self.editor.update(cx, |editor, cx| { - editor.set_text(prompt, cx); - editor.move_to_end(&Default::default(), cx) + editor.set_text(prompt, window, cx); + editor.move_to_end(&Default::default(), window, cx) }); } } else { - cx.focus_view(&self.context_strip); + self.context_strip.focus_handle(cx).focus(window); } } - fn render_buttons(&self, cx: &mut ViewContext) -> Vec { + fn render_buttons(&self, _window: &mut Window, cx: &mut Context) -> Vec { let mode = match &self.mode { PromptEditorMode::Buffer { codegen, .. } => { let codegen = codegen.read(cx); @@ -442,21 +467,22 @@ impl PromptEditor { .icon(IconName::Return) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) - .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested))) + .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested))) .into_any_element()] } CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop) .icon_color(Color::Error) .shape(IconButtonShape::Square) - .tooltip(move |cx| { + .tooltip(move |window, cx| { Tooltip::with_meta( mode.tooltip_interrupt(), Some(&menu::Cancel), "Changes won't be discarded", + window, cx, ) }) - .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested))) + .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested))) .into_any_element()], CodegenStatus::Done | CodegenStatus::Error(_) => { let has_error = matches!(codegen_status, CodegenStatus::Error(_)); @@ -464,15 +490,16 @@ impl PromptEditor { vec![IconButton::new("restart", IconName::RotateCw) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(move |cx| { + .tooltip(move |window, cx| { Tooltip::with_meta( mode.tooltip_restart(), Some(&menu::Confirm), "Changes will be discarded", + window, cx, ) }) - .on_click(cx.listener(|_, _, cx| { + .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::StartRequested); })) .into_any_element()] @@ -480,10 +507,10 @@ impl PromptEditor { let accept = IconButton::new("accept", IconName::Check) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(move |cx| { - Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx) + .tooltip(move |window, cx| { + Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, window, cx) }) - .on_click(cx.listener(|_, _, cx| { + .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::ConfirmRequested { execute: false }); })) .into_any_element(); @@ -494,14 +521,15 @@ impl PromptEditor { IconButton::new("confirm", IconName::Play) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(|cx| { + .tooltip(|window, cx| { Tooltip::for_action( "Execute Generated Command", &menu::SecondaryConfirm, + window, cx, ) }) - .on_click(cx.listener(|_, _, cx| { + .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::ConfirmRequested { execute: true }); })) .into_any_element(), @@ -513,7 +541,12 @@ impl PromptEditor { } } - fn cycle_prev(&mut self, _: &CyclePreviousInlineAssist, cx: &mut ViewContext) { + fn cycle_prev( + &mut self, + _: &CyclePreviousInlineAssist, + _: &mut Window, + cx: &mut Context, + ) { match &self.mode { PromptEditorMode::Buffer { codegen, .. } => { codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx)); @@ -524,7 +557,7 @@ impl PromptEditor { } } - fn cycle_next(&mut self, _: &CycleNextInlineAssist, cx: &mut ViewContext) { + fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context) { match &self.mode { PromptEditorMode::Buffer { codegen, .. } => { codegen.update(cx, |codegen, cx| codegen.cycle_next(cx)); @@ -535,16 +568,16 @@ impl PromptEditor { } } - fn render_close_button(&self, cx: &ViewContext) -> AnyElement { + fn render_close_button(&self, cx: &mut Context) -> AnyElement { IconButton::new("cancel", IconName::Close) .icon_color(Color::Muted) .shape(IconButtonShape::Square) - .tooltip(|cx| Tooltip::text("Close Assistant", cx)) - .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested))) + .tooltip(Tooltip::text("Close Assistant")) + .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested))) .into_any_element() } - fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &ViewContext) -> AnyElement { + fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context) -> AnyElement { let disabled = matches!(codegen.status(cx), CodegenStatus::Idle); let model_registry = LanguageModelRegistry::read_global(cx); @@ -584,13 +617,13 @@ impl PromptEditor { .shape(IconButtonShape::Square) .tooltip({ let focus_handle = self.editor.focus_handle(cx); - move |cx| { - cx.new_view(|cx| { + move |window, cx| { + cx.new(|_| { let mut tooltip = Tooltip::new("Previous Alternative").key_binding( KeyBinding::for_action_in( &CyclePreviousInlineAssist, &focus_handle, - cx, + window, ), ); if !disabled && current_index != 0 { @@ -601,8 +634,8 @@ impl PromptEditor { .into() } }) - .on_click(cx.listener(|this, _, cx| { - this.cycle_prev(&CyclePreviousInlineAssist, cx); + .on_click(cx.listener(|this, _, window, cx| { + this.cycle_prev(&CyclePreviousInlineAssist, window, cx); })), ) .child( @@ -625,13 +658,13 @@ impl PromptEditor { .shape(IconButtonShape::Square) .tooltip({ let focus_handle = self.editor.focus_handle(cx); - move |cx| { - cx.new_view(|cx| { + move |window, cx| { + cx.new(|_| { let mut tooltip = Tooltip::new("Next Alternative").key_binding( KeyBinding::for_action_in( &CycleNextInlineAssist, &focus_handle, - cx, + window, ), ); if !disabled && current_index != total_models - 1 { @@ -642,14 +675,14 @@ impl PromptEditor { .into() } }) - .on_click( - cx.listener(|this, _, cx| this.cycle_next(&CycleNextInlineAssist, cx)), - ), + .on_click(cx.listener(|this, _, window, cx| { + this.cycle_next(&CycleNextInlineAssist, window, cx) + })), ) .into_any_element() } - fn render_rate_limit_notice(&self, cx: &mut ViewContext) -> impl IntoElement { + fn render_rate_limit_notice(&self, cx: &mut Context) -> impl IntoElement { Popover::new().child( v_flex() .occlude() @@ -673,7 +706,7 @@ impl PromptEditor { } else { ui::ToggleState::Unselected }, - |selection, cx| { + |selection, _, cx| { let is_dismissed = match selection { ui::ToggleState::Unselected => false, ui::ToggleState::Indeterminate => return, @@ -692,10 +725,11 @@ impl PromptEditor { .on_click(cx.listener(Self::toggle_rate_limit_notice)), ) .child(Button::new("more-info", "More Info").on_click( - |_event, cx| { - cx.dispatch_action(Box::new( - zed_actions::OpenAccountSettings, - )) + |_event, window, cx| { + window.dispatch_action( + Box::new(zed_actions::OpenAccountSettings), + cx, + ) }, )), ), @@ -703,9 +737,9 @@ impl PromptEditor { ) } - fn render_editor(&mut self, cx: &mut ViewContext) -> AnyElement { + fn render_editor(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { let font_size = TextSize::Default.rems(cx); - let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; + let line_height = font_size.to_pixels(window.rem_size()) * 1.3; div() .key_context("InlineAssistEditor") @@ -739,17 +773,15 @@ impl PromptEditor { fn handle_context_strip_event( &mut self, - _context_strip: View, + _context_strip: &Entity, event: &ContextStripEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { ContextStripEvent::PickerDismissed | ContextStripEvent::BlurredEmpty - | ContextStripEvent::BlurredUp => { - let editor_focus_handle = self.editor.focus_handle(cx); - cx.focus(&editor_focus_handle); - } + | ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window), ContextStripEvent::BlurredDown => {} } } @@ -758,12 +790,12 @@ impl PromptEditor { pub enum PromptEditorMode { Buffer { id: InlineAssistId, - codegen: Model, + codegen: Entity, gutter_dimensions: Arc>, }, Terminal { id: TerminalInlineAssistId, - codegen: Model, + codegen: Entity, height_in_lines: u8, }, } @@ -794,13 +826,14 @@ impl PromptEditor { id: InlineAssistId, gutter_dimensions: Arc>, prompt_history: VecDeque, - prompt_buffer: Model, - codegen: Model, + prompt_buffer: Entity, + codegen: Entity, fs: Arc, - context_store: Model, - workspace: WeakView, - thread_store: Option>, - cx: &mut ViewContext>, + context_store: Entity, + workspace: WeakEntity, + thread_store: Option>, + window: &mut Window, + cx: &mut Context>, ) -> PromptEditor { let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed); let mode = PromptEditorMode::Buffer { @@ -809,7 +842,7 @@ impl PromptEditor { gutter_dimensions, }; - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { let mut editor = Editor::new( EditorMode::AutoHeight { max_lines: Self::MAX_LINES as usize, @@ -817,6 +850,7 @@ impl PromptEditor { prompt_buffer, None, false, + window, cx, ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); @@ -824,36 +858,39 @@ impl PromptEditor { // always show the cursor (even when it isn't focused) because // typing in one will make what you typed appear in all of them. editor.set_show_cursor_when_unfocused(true, cx); - editor.set_placeholder_text(Self::placeholder_text(&mode, cx), cx); + editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx); editor }); let context_picker_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default(); - let context_strip = cx.new_view(|cx| { + let context_strip = cx.new(|cx| { ContextStrip::new( context_store.clone(), workspace.clone(), + prompt_editor.downgrade(), thread_store.clone(), context_picker_menu_handle.clone(), SuggestContextKind::Thread, + window, cx, ) }); let context_strip_subscription = - cx.subscribe(&context_strip, Self::handle_context_strip_event); + cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event); let mut this: PromptEditor = PromptEditor { editor: prompt_editor.clone(), context_store, context_strip, context_picker_menu_handle, - model_selector: cx.new_view(|cx| { + model_selector: cx.new(|cx| { AssistantModelSelector::new( fs, model_selector_menu_handle.clone(), prompt_editor.focus_handle(cx), + window, cx, ) }), @@ -870,14 +907,14 @@ impl PromptEditor { _phantom: Default::default(), }; - this.subscribe_to_editor(cx); + this.subscribe_to_editor(window, cx); this } fn handle_codegen_changed( &mut self, - _: Model, - cx: &mut ViewContext>, + _: Entity, + cx: &mut Context>, ) { match self.codegen_status(cx) { CodegenStatus::Idle => { @@ -916,7 +953,7 @@ impl PromptEditor { } } - pub fn codegen(&self) -> &Model { + pub fn codegen(&self) -> &Entity { match &self.mode { PromptEditorMode::Buffer { codegen, .. } => codegen, PromptEditorMode::Terminal { .. } => unreachable!(), @@ -949,13 +986,14 @@ impl PromptEditor { pub fn new_terminal( id: TerminalInlineAssistId, prompt_history: VecDeque, - prompt_buffer: Model, - codegen: Model, + prompt_buffer: Entity, + codegen: Entity, fs: Arc, - context_store: Model, - workspace: WeakView, - thread_store: Option>, - cx: &mut ViewContext, + context_store: Entity, + workspace: WeakEntity, + thread_store: Option>, + window: &mut Window, + cx: &mut Context, ) -> Self { let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed); let mode = PromptEditorMode::Terminal { @@ -964,7 +1002,7 @@ impl PromptEditor { height_in_lines: 1, }; - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { let mut editor = Editor::new( EditorMode::AutoHeight { max_lines: Self::MAX_LINES as usize, @@ -972,39 +1010,43 @@ impl PromptEditor { prompt_buffer, None, false, + window, cx, ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor.set_placeholder_text(Self::placeholder_text(&mode, cx), cx); + editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx); editor }); let context_picker_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default(); - let context_strip = cx.new_view(|cx| { + let context_strip = cx.new(|cx| { ContextStrip::new( context_store.clone(), workspace.clone(), + prompt_editor.downgrade(), thread_store.clone(), context_picker_menu_handle.clone(), SuggestContextKind::Thread, + window, cx, ) }); let context_strip_subscription = - cx.subscribe(&context_strip, Self::handle_context_strip_event); + cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event); let mut this = Self { editor: prompt_editor.clone(), context_store, context_strip, context_picker_menu_handle, - model_selector: cx.new_view(|cx| { + model_selector: cx.new(|cx| { AssistantModelSelector::new( fs, model_selector_menu_handle.clone(), prompt_editor.focus_handle(cx), + window, cx, ) }), @@ -1021,11 +1063,11 @@ impl PromptEditor { _phantom: Default::default(), }; this.count_lines(cx); - this.subscribe_to_editor(cx); + this.subscribe_to_editor(window, cx); this } - fn count_lines(&mut self, cx: &mut ViewContext) { + fn count_lines(&mut self, cx: &mut Context) { let height_in_lines = cmp::max( 2, // Make the editor at least two lines tall, to account for padding and buttons. cmp::min( @@ -1049,7 +1091,7 @@ impl PromptEditor { } } - fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { + fn handle_codegen_changed(&mut self, _: Entity, cx: &mut Context) { match &self.codegen().read(cx).status { CodegenStatus::Idle => { self.editor @@ -1067,7 +1109,7 @@ impl PromptEditor { } } - pub fn codegen(&self) -> &Model { + pub fn codegen(&self) -> &Entity { match &self.mode { PromptEditorMode::Buffer { .. } => unreachable!(), PromptEditorMode::Terminal { codegen, .. } => codegen, @@ -1091,7 +1133,7 @@ fn dismissed_rate_limit_notice() -> bool { .map_or(false, |s| s.is_some()) } -fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut AppContext) { +fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) { db::write_and_log(cx, move || async move { if is_dismissed { db::kvp::KEY_VALUE_STORE diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 7a597849b933cb..70d32f3dda53a9 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -4,8 +4,8 @@ use editor::actions::MoveUp; use editor::{Editor, EditorElement, EditorEvent, EditorStyle}; use fs::Fs; use gpui::{ - pulsating_between, Animation, AnimationExt, AppContext, DismissEvent, FocusableView, Model, - Subscription, TextStyle, View, WeakModel, WeakView, + pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, + TextStyle, WeakEntity, }; use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use language_model_selector::LanguageModelSelector; @@ -13,7 +13,9 @@ use rope::Point; use settings::Settings; use std::time::Duration; use theme::ThemeSettings; -use ui::{prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor}; +use ui::{ + prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor, Tooltip, +}; use workspace::Workspace; use crate::assistant_model_selector::AssistantModelSelector; @@ -25,14 +27,14 @@ use crate::thread_store::ThreadStore; use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker, ToggleModelSelector}; pub struct MessageEditor { - thread: Model, - editor: View, - context_store: Model, - context_strip: View, + thread: Entity, + editor: Entity, + context_store: Entity, + context_strip: Entity, context_picker_menu_handle: PopoverMenuHandle, - inline_context_picker: View, + inline_context_picker: Entity, inline_context_picker_menu_handle: PopoverMenuHandle, - model_selector: View, + model_selector: Entity, model_selector_menu_handle: PopoverMenuHandle, use_tools: bool, _subscriptions: Vec, @@ -41,52 +43,58 @@ pub struct MessageEditor { impl MessageEditor { pub fn new( fs: Arc, - workspace: WeakView, - thread_store: WeakModel, - thread: Model, - cx: &mut ViewContext, + workspace: WeakEntity, + thread_store: WeakEntity, + thread: Entity, + window: &mut Window, + cx: &mut Context, ) -> Self { - let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone())); + let context_store = cx.new(|_cx| ContextStore::new(workspace.clone())); let context_picker_menu_handle = PopoverMenuHandle::default(); let inline_context_picker_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default(); - let editor = cx.new_view(|cx| { - let mut editor = Editor::auto_height(10, cx); - editor.set_placeholder_text("Ask anything…", cx); + let editor = cx.new(|cx| { + let mut editor = Editor::auto_height(10, window, cx); + editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx); editor.set_show_indent_guides(false, cx); editor }); - let inline_context_picker = cx.new_view(|cx| { + let inline_context_picker = cx.new(|cx| { ContextPicker::new( workspace.clone(), Some(thread_store.clone()), context_store.downgrade(), + editor.downgrade(), ConfirmBehavior::Close, + window, cx, ) }); - let context_strip = cx.new_view(|cx| { + let context_strip = cx.new(|cx| { ContextStrip::new( context_store.clone(), workspace.clone(), + editor.downgrade(), Some(thread_store.clone()), context_picker_menu_handle.clone(), SuggestContextKind::File, + window, cx, ) }); let subscriptions = vec![ - cx.subscribe(&editor, Self::handle_editor_event), - cx.subscribe( + cx.subscribe_in(&editor, window, Self::handle_editor_event), + cx.subscribe_in( &inline_context_picker, + window, Self::handle_inline_context_picker_event, ), - cx.subscribe(&context_strip, Self::handle_context_strip_event), + cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event), ]; Self { @@ -97,11 +105,12 @@ impl MessageEditor { context_picker_menu_handle, inline_context_picker, inline_context_picker_menu_handle, - model_selector: cx.new_view(|cx| { + model_selector: cx.new(|cx| { AssistantModelSelector::new( fs, model_selector_menu_handle.clone(), editor.focus_handle(cx), + window, cx, ) }), @@ -111,29 +120,59 @@ impl MessageEditor { } } - fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext) { - self.model_selector_menu_handle.toggle(cx) + fn toggle_model_selector( + &mut self, + _: &ToggleModelSelector, + window: &mut Window, + cx: &mut Context, + ) { + self.model_selector_menu_handle.toggle(window, cx) } - fn toggle_chat_mode(&mut self, _: &ChatMode, cx: &mut ViewContext) { + fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context) { self.use_tools = !self.use_tools; cx.notify(); } - fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext) { - self.context_picker_menu_handle.toggle(cx); + fn toggle_context_picker( + &mut self, + _: &ToggleContextPicker, + window: &mut Window, + cx: &mut Context, + ) { + self.context_picker_menu_handle.toggle(window, cx); } - pub fn remove_all_context(&mut self, _: &RemoveAllContext, cx: &mut ViewContext) { + pub fn remove_all_context( + &mut self, + _: &RemoveAllContext, + _window: &mut Window, + cx: &mut Context, + ) { self.context_store.update(cx, |store, _cx| store.clear()); cx.notify(); } - fn chat(&mut self, _: &Chat, cx: &mut ViewContext) { - self.send_to_model(RequestKind::Chat, cx); + fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context) { + self.send_to_model(RequestKind::Chat, window, cx); + } + + fn is_editor_empty(&self, cx: &App) -> bool { + self.editor.read(cx).text(cx).is_empty() + } + + fn is_model_selected(&self, cx: &App) -> bool { + LanguageModelRegistry::read_global(cx) + .active_model() + .is_some() } - fn send_to_model(&mut self, request_kind: RequestKind, cx: &mut ViewContext) { + fn send_to_model( + &mut self, + request_kind: RequestKind, + window: &mut Window, + cx: &mut Context, + ) { let provider = LanguageModelRegistry::read_global(cx).active_provider(); if provider .as_ref() @@ -150,7 +189,7 @@ impl MessageEditor { let user_message = self.editor.update(cx, |editor, cx| { let text = editor.text(cx); - editor.clear(cx); + editor.clear(window, cx); text }); @@ -189,9 +228,10 @@ impl MessageEditor { fn handle_editor_event( &mut self, - editor: View, + editor: &Entity, event: &EditorEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { EditorEvent::SelectionsChanged { .. } => { @@ -202,7 +242,7 @@ impl MessageEditor { let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1); let char_behind_cursor = snapshot.chars_at(behind_cursor).next(); if char_behind_cursor == Some('@') { - self.inline_context_picker_menu_handle.show(cx); + self.inline_context_picker_menu_handle.show(window, cx); } } }); @@ -213,55 +253,66 @@ impl MessageEditor { fn handle_inline_context_picker_event( &mut self, - _inline_context_picker: View, + _inline_context_picker: &Entity, _event: &DismissEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let editor_focus_handle = self.editor.focus_handle(cx); - cx.focus(&editor_focus_handle); + window.focus(&editor_focus_handle); } fn handle_context_strip_event( &mut self, - _context_strip: View, + _context_strip: &Entity, event: &ContextStripEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { ContextStripEvent::PickerDismissed | ContextStripEvent::BlurredEmpty | ContextStripEvent::BlurredDown => { let editor_focus_handle = self.editor.focus_handle(cx); - cx.focus(&editor_focus_handle); + window.focus(&editor_focus_handle); } ContextStripEvent::BlurredUp => {} } } - fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { - if self.context_picker_menu_handle.is_deployed() { + fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { + if self.context_picker_menu_handle.is_deployed() + || self.inline_context_picker_menu_handle.is_deployed() + { cx.propagate(); } else { - cx.focus_view(&self.context_strip); + self.context_strip.focus_handle(cx).focus(window); } } } -impl FocusableView for MessageEditor { - fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { +impl Focusable for MessageEditor { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.editor.focus_handle(cx) } } impl Render for MessageEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let font_size = TextSize::Default.rems(cx); - let line_height = font_size.to_pixels(cx.rem_size()) * 1.5; + let line_height = font_size.to_pixels(window.rem_size()) * 1.5; let focus_handle = self.editor.focus_handle(cx); let inline_context_picker = self.inline_context_picker.clone(); let bg_color = cx.theme().colors().editor_background; let is_streaming_completion = self.thread.read(cx).is_streaming(); let button_width = px(64.); + let is_model_selected = self.is_model_selected(cx); + let is_editor_empty = self.is_editor_empty(cx); + let submit_label_color = if is_editor_empty { + Color::Muted + } else { + Color::Default + }; v_flex() .key_context("MessageEditor") @@ -303,9 +354,9 @@ impl Render for MessageEditor { }) .child( PopoverMenu::new("inline-context-picker") - .menu(move |cx| { + .menu(move |window, cx| { inline_context_picker.update(cx, |this, cx| { - this.init(cx); + this.init(window, cx); }); Some(inline_context_picker.clone()) @@ -328,7 +379,7 @@ impl Render for MessageEditor { .child( Switch::new("use-tools", self.use_tools.into()) .label("Tools") - .on_click(cx.listener(|this, selection, _cx| { + .on_click(cx.listener(|this, selection, _window, _cx| { this.use_tools = match selection { ToggleState::Selected => true, ToggleState::Unselected @@ -338,7 +389,7 @@ impl Render for MessageEditor { .key_binding(KeyBinding::for_action_in( &ChatMode, &focus_handle, - cx, + window, )), ) .child(h_flex().gap_1().child(self.model_selector.clone()).child( @@ -367,35 +418,52 @@ impl Render for MessageEditor { KeyBinding::for_action_in( &editor::actions::Cancel, &focus_handle, - cx, + window, ) .map(|binding| binding.into_any_element()), ), ) - .on_click(move |_event, cx| { - focus_handle - .dispatch_action(&editor::actions::Cancel, cx); + .on_click(move |_event, window, cx| { + focus_handle.dispatch_action( + &editor::actions::Cancel, + window, + cx, + ); }) } else { ButtonLike::new("submit-message") .width(button_width.into()) .style(ButtonStyle::Filled) + .disabled(is_editor_empty || !is_model_selected) .child( h_flex() .w_full() .justify_between() - .child(Label::new("Submit").size(LabelSize::Small)) + .child( + Label::new("Submit") + .size(LabelSize::Small) + .color(submit_label_color), + ) .children( KeyBinding::for_action_in( &Chat, &focus_handle, - cx, + window, ) .map(|binding| binding.into_any_element()), ), ) - .on_click(move |_event, cx| { - focus_handle.dispatch_action(&Chat, cx); + .on_click(move |_event, window, cx| { + focus_handle.dispatch_action(&Chat, window, cx); + }) + .when(is_editor_empty, |button| { + button + .tooltip(Tooltip::text("Type a message to submit")) + }) + .when(!is_model_selected, |button| { + button.tooltip(Tooltip::text( + "Select a model to continue", + )) }) }, )), diff --git a/crates/assistant2/src/terminal_codegen.rs b/crates/assistant2/src/terminal_codegen.rs index 97cb18e4400bbb..0bd0bfb0411c79 100644 --- a/crates/assistant2/src/terminal_codegen.rs +++ b/crates/assistant2/src/terminal_codegen.rs @@ -1,7 +1,7 @@ use crate::inline_prompt_editor::CodegenStatus; use client::telemetry::Telemetry; use futures::{channel::mpsc, SinkExt, StreamExt}; -use gpui::{AppContext, EventEmitter, Model, ModelContext, Task}; +use gpui::{App, Context, Entity, EventEmitter, Task}; use language_model::{LanguageModelRegistry, LanguageModelRequest}; use language_models::report_assistant_event; use std::{sync::Arc, time::Instant}; @@ -11,7 +11,7 @@ use terminal::Terminal; pub struct TerminalCodegen { pub status: CodegenStatus, pub telemetry: Option>, - terminal: Model, + terminal: Entity, generation: Task<()>, pub message_id: Option, transaction: Option, @@ -20,7 +20,7 @@ pub struct TerminalCodegen { impl EventEmitter for TerminalCodegen {} impl TerminalCodegen { - pub fn new(terminal: Model, telemetry: Option>) -> Self { + pub fn new(terminal: Entity, telemetry: Option>) -> Self { Self { terminal, telemetry, @@ -31,7 +31,7 @@ impl TerminalCodegen { } } - pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext) { + pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context) { let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { return; }; @@ -131,20 +131,20 @@ impl TerminalCodegen { cx.notify(); } - pub fn stop(&mut self, cx: &mut ModelContext) { + pub fn stop(&mut self, cx: &mut Context) { self.status = CodegenStatus::Done; self.generation = Task::ready(()); cx.emit(CodegenEvent::Finished); cx.notify(); } - pub fn complete(&mut self, cx: &mut ModelContext) { + pub fn complete(&mut self, cx: &mut Context) { if let Some(transaction) = self.transaction.take() { transaction.complete(cx); } } - pub fn undo(&mut self, cx: &mut ModelContext) { + pub fn undo(&mut self, cx: &mut Context) { if let Some(transaction) = self.transaction.take() { transaction.undo(cx); } @@ -160,27 +160,27 @@ pub const CLEAR_INPUT: &str = "\x15"; const CARRIAGE_RETURN: &str = "\x0d"; struct TerminalTransaction { - terminal: Model, + terminal: Entity, } impl TerminalTransaction { - pub fn start(terminal: Model) -> Self { + pub fn start(terminal: Entity) -> Self { Self { terminal } } - pub fn push(&mut self, hunk: String, cx: &mut AppContext) { + pub fn push(&mut self, hunk: String, cx: &mut App) { // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal let input = Self::sanitize_input(hunk); self.terminal .update(cx, |terminal, _| terminal.input(input)); } - pub fn undo(&self, cx: &mut AppContext) { + pub fn undo(&self, cx: &mut App) { self.terminal .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string())); } - pub fn complete(&self, cx: &mut AppContext) { + pub fn complete(&self, cx: &mut App) { self.terminal.update(cx, |terminal, _| { terminal.input(CARRIAGE_RETURN.to_string()) }); diff --git a/crates/assistant2/src/terminal_inline_assistant.rs b/crates/assistant2/src/terminal_inline_assistant.rs index 65be6a6e983ca4..9abe2cbadb3338 100644 --- a/crates/assistant2/src/terminal_inline_assistant.rs +++ b/crates/assistant2/src/terminal_inline_assistant.rs @@ -10,10 +10,7 @@ use client::telemetry::Telemetry; use collections::{HashMap, VecDeque}; use editor::{actions::SelectAll, MultiBuffer}; use fs::Fs; -use gpui::{ - AppContext, Context, FocusableView, Global, Model, Subscription, UpdateGlobal, View, WeakModel, - WeakView, -}; +use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity}; use language::Buffer; use language_model::{ LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, @@ -31,7 +28,7 @@ pub fn init( fs: Arc, prompt_builder: Arc, telemetry: Arc, - cx: &mut AppContext, + cx: &mut App, ) { cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry)); } @@ -68,20 +65,20 @@ impl TerminalInlineAssistant { pub fn assist( &mut self, - terminal_view: &View, - workspace: WeakView, - thread_store: Option>, - cx: &mut WindowContext, + terminal_view: &Entity, + workspace: WeakEntity, + thread_store: Option>, + window: &mut Window, + cx: &mut App, ) { let terminal = terminal_view.read(cx).terminal().clone(); let assist_id = self.next_assist_id.post_inc(); - let prompt_buffer = cx.new_model(|cx| { - MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx) - }); - let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone())); - let codegen = cx.new_model(|_| TerminalCodegen::new(terminal, self.telemetry.clone())); + let prompt_buffer = + cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx)); + let context_store = cx.new(|_cx| ContextStore::new(workspace.clone())); + let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone())); - let prompt_editor = cx.new_view(|cx| { + let prompt_editor = cx.new(|cx| { PromptEditor::new_terminal( assist_id, self.prompt_history.clone(), @@ -91,6 +88,7 @@ impl TerminalInlineAssistant { context_store.clone(), workspace.clone(), thread_store.clone(), + window, cx, ) }); @@ -100,7 +98,7 @@ impl TerminalInlineAssistant { render: Box::new(move |_| prompt_editor_render.clone().into_any_element()), }; terminal_view.update(cx, |terminal_view, cx| { - terminal_view.set_block_below_cursor(block, cx); + terminal_view.set_block_below_cursor(block, window, cx); }); let terminal_assistant = TerminalInlineAssist::new( @@ -109,21 +107,27 @@ impl TerminalInlineAssistant { prompt_editor, workspace.clone(), context_store, + window, cx, ); self.assists.insert(assist_id, terminal_assistant); - self.focus_assist(assist_id, cx); + self.focus_assist(assist_id, window, cx); } - fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { + fn focus_assist( + &mut self, + assist_id: TerminalInlineAssistId, + window: &mut Window, + cx: &mut App, + ) { let assist = &self.assists[&assist_id]; if let Some(prompt_editor) = assist.prompt_editor.as_ref() { prompt_editor.update(cx, |this, cx| { this.editor.update(cx, |editor, cx| { - editor.focus(cx); - editor.select_all(&SelectAll, cx); + window.focus(&editor.focus_handle(cx)); + editor.select_all(&SelectAll, window, cx); }); }); } @@ -131,9 +135,10 @@ impl TerminalInlineAssistant { fn handle_prompt_editor_event( &mut self, - prompt_editor: View>, + prompt_editor: Entity>, event: &PromptEditorEvent, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { let assist_id = prompt_editor.read(cx).id(); match event { @@ -144,21 +149,21 @@ impl TerminalInlineAssistant { self.stop_assist(assist_id, cx); } PromptEditorEvent::ConfirmRequested { execute } => { - self.finish_assist(assist_id, false, *execute, cx); + self.finish_assist(assist_id, false, *execute, window, cx); } PromptEditorEvent::CancelRequested => { - self.finish_assist(assist_id, true, false, cx); + self.finish_assist(assist_id, true, false, window, cx); } PromptEditorEvent::DismissRequested => { - self.dismiss_assist(assist_id, cx); + self.dismiss_assist(assist_id, window, cx); } PromptEditorEvent::Resized { height_in_lines } => { - self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx); + self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx); } } } - fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { + fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -196,7 +201,7 @@ impl TerminalInlineAssistant { codegen.update(cx, |codegen, cx| codegen.start(request, cx)); } - fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) { + fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) { let assist = if let Some(assist) = self.assists.get_mut(&assist_id) { assist } else { @@ -209,7 +214,7 @@ impl TerminalInlineAssistant { fn request_for_inline_assist( &self, assist_id: TerminalInlineAssistId, - cx: &mut WindowContext, + cx: &mut App, ) -> Result { let assist = self.assists.get(&assist_id).context("invalid assist")?; @@ -217,7 +222,7 @@ impl TerminalInlineAssistant { let (latest_output, working_directory) = assist .terminal .update(cx, |terminal, cx| { - let terminal = terminal.model().read(cx); + let terminal = terminal.entity().read(cx); let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES); let working_directory = terminal .working_directory() @@ -265,16 +270,17 @@ impl TerminalInlineAssistant { assist_id: TerminalInlineAssistId, undo: bool, execute: bool, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { - self.dismiss_assist(assist_id, cx); + self.dismiss_assist(assist_id, window, cx); if let Some(assist) = self.assists.remove(&assist_id) { assist .terminal .update(cx, |this, cx| { this.clear_block_below_cursor(cx); - this.focus_handle(cx).focus(cx); + this.focus_handle(cx).focus(window); }) .log_err(); @@ -317,7 +323,8 @@ impl TerminalInlineAssistant { fn dismiss_assist( &mut self, assist_id: TerminalInlineAssistId, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> bool { let Some(assist) = self.assists.get_mut(&assist_id) else { return false; @@ -330,7 +337,7 @@ impl TerminalInlineAssistant { .terminal .update(cx, |this, cx| { this.clear_block_below_cursor(cx); - this.focus_handle(cx).focus(cx); + this.focus_handle(cx).focus(window); }) .is_ok() } @@ -339,7 +346,8 @@ impl TerminalInlineAssistant { &mut self, assist_id: TerminalInlineAssistId, height: u8, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) { if let Some(assist) = self.assists.get_mut(&assist_id) { if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() { @@ -351,7 +359,7 @@ impl TerminalInlineAssistant { height, render: Box::new(move |_| prompt_editor.clone().into_any_element()), }; - terminal.set_block_below_cursor(block, cx); + terminal.set_block_below_cursor(block, window, cx); }) .log_err(); } @@ -360,22 +368,23 @@ impl TerminalInlineAssistant { } struct TerminalInlineAssist { - terminal: WeakView, - prompt_editor: Option>>, - codegen: Model, - workspace: WeakView, - context_store: Model, + terminal: WeakEntity, + prompt_editor: Option>>, + codegen: Entity, + workspace: WeakEntity, + context_store: Entity, _subscriptions: Vec, } impl TerminalInlineAssist { pub fn new( assist_id: TerminalInlineAssistId, - terminal: &View, - prompt_editor: View>, - workspace: WeakView, - context_store: Model, - cx: &mut WindowContext, + terminal: &Entity, + prompt_editor: Entity>, + workspace: WeakEntity, + context_store: Entity, + window: &mut Window, + cx: &mut App, ) -> Self { let codegen = prompt_editor.read(cx).codegen().clone(); Self { @@ -385,12 +394,12 @@ impl TerminalInlineAssist { workspace: workspace.clone(), context_store, _subscriptions: vec![ - cx.subscribe(&prompt_editor, |prompt_editor, event, cx| { + window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| { TerminalInlineAssistant::update_global(cx, |this, cx| { - this.handle_prompt_editor_event(prompt_editor, event, cx) + this.handle_prompt_editor_event(prompt_editor, event, window, cx) }) }), - cx.subscribe(&codegen, move |codegen, event, cx| { + window.subscribe(&codegen, cx, move |codegen, event, window, cx| { TerminalInlineAssistant::update_global(cx, |this, cx| match event { CodegenEvent::Finished => { let assist = if let Some(assist) = this.assists.get(&assist_id) { @@ -419,7 +428,7 @@ impl TerminalInlineAssist { } if assist.prompt_editor.is_none() { - this.finish_assist(assist_id, false, false, cx); + this.finish_assist(assist_id, false, false, window, cx); } } }) diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index e7c4036a55c291..9ccb0664807bf5 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -6,7 +6,7 @@ use chrono::{DateTime, Utc}; use collections::{BTreeMap, HashMap, HashSet}; use futures::future::Shared; use futures::{FutureExt as _, StreamExt as _}; -use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task}; +use gpui::{App, Context, EventEmitter, SharedString, Task}; use language_model::{ LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, @@ -18,6 +18,7 @@ use util::{post_inc, TryFutureExt as _}; use uuid::Uuid; use crate::context::{attach_context_to_message, ContextId, ContextSnapshot}; +use crate::thread_store::SavedThread; #[derive(Debug, Clone, Copy)] pub enum RequestKind { @@ -75,7 +76,7 @@ pub struct Thread { } impl Thread { - pub fn new(tools: Arc, _cx: &mut ModelContext) -> Self { + pub fn new(tools: Arc, _cx: &mut Context) -> Self { Self { id: ThreadId::new(), updated_at: Utc::now(), @@ -94,6 +95,40 @@ impl Thread { } } + pub fn from_saved( + id: ThreadId, + saved: SavedThread, + tools: Arc, + _cx: &mut Context, + ) -> Self { + let next_message_id = MessageId(saved.messages.len()); + + Self { + id, + updated_at: saved.updated_at, + summary: Some(saved.summary), + pending_summary: Task::ready(None), + messages: saved + .messages + .into_iter() + .map(|message| Message { + id: message.id, + role: message.role, + text: message.text, + }) + .collect(), + next_message_id, + context: BTreeMap::default(), + context_by_message: HashMap::default(), + completion_count: 0, + pending_completions: Vec::new(), + tools, + tool_uses_by_message: HashMap::default(), + tool_results_by_message: HashMap::default(), + pending_tool_uses_by_id: HashMap::default(), + } + } + pub fn id(&self) -> &ThreadId { &self.id } @@ -119,7 +154,7 @@ impl Thread { self.summary.clone().unwrap_or(DEFAULT) } - pub fn set_summary(&mut self, summary: impl Into, cx: &mut ModelContext) { + pub fn set_summary(&mut self, summary: impl Into, cx: &mut Context) { self.summary = Some(summary.into()); cx.emit(ThreadEvent::SummaryChanged); } @@ -159,7 +194,7 @@ impl Thread { &mut self, text: impl Into, context: Vec, - cx: &mut ModelContext, + cx: &mut Context, ) { let message_id = self.insert_message(Role::User, text, cx); let context_ids = context.iter().map(|context| context.id).collect::>(); @@ -172,7 +207,7 @@ impl Thread { &mut self, role: Role, text: impl Into, - cx: &mut ModelContext, + cx: &mut Context, ) -> MessageId { let id = self.next_message_id.post_inc(); self.messages.push(Message { @@ -209,7 +244,7 @@ impl Thread { pub fn to_completion_request( &self, _request_kind: RequestKind, - _cx: &AppContext, + _cx: &App, ) -> LanguageModelRequest { let mut request = LanguageModelRequest { messages: vec![], @@ -279,7 +314,7 @@ impl Thread { &mut self, request: LanguageModelRequest, model: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) { let pending_completion_id = post_inc(&mut self.completion_count); @@ -308,6 +343,13 @@ impl Thread { last_message.id, chunk, )); + } else { + // If we won't have an Assistant message yet, assume this chunk marks the beginning + // of a new Assistant response. + // + // Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it + // will result in duplicating the text of the chunk in the rendered Markdown. + thread.insert_message(Role::Assistant, chunk, cx); } } } @@ -397,7 +439,7 @@ impl Thread { }); } - pub fn summarize(&mut self, cx: &mut ModelContext) { + pub fn summarize(&mut self, cx: &mut Context) { let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else { return; }; @@ -455,7 +497,7 @@ impl Thread { assistant_message_id: MessageId, tool_use_id: LanguageModelToolUseId, output: Task>, - cx: &mut ModelContext, + cx: &mut Context, ) { let insert_output_task = cx.spawn(|thread, mut cx| { let tool_use_id = tool_use_id.clone(); diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs index 18619fd0514b79..db86e753bb8aa0 100644 --- a/crates/assistant2/src/thread_history.rs +++ b/crates/assistant2/src/thread_history.rs @@ -1,27 +1,27 @@ use gpui::{ - uniform_list, AppContext, FocusHandle, FocusableView, Model, ScrollStrategy, - UniformListScrollHandle, WeakView, + uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle, + WeakEntity, }; use time::{OffsetDateTime, UtcOffset}; use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip}; -use crate::thread::Thread; -use crate::thread_store::ThreadStore; +use crate::thread_store::{SavedThreadMetadata, ThreadStore}; use crate::{AssistantPanel, RemoveSelectedThread}; pub struct ThreadHistory { focus_handle: FocusHandle, - assistant_panel: WeakView, - thread_store: Model, + assistant_panel: WeakEntity, + thread_store: Entity, scroll_handle: UniformListScrollHandle, selected_index: usize, } impl ThreadHistory { pub(crate) fn new( - assistant_panel: WeakView, - thread_store: Model, - cx: &mut ViewContext, + assistant_panel: WeakEntity, + thread_store: Entity, + + cx: &mut Context, ) -> Self { Self { focus_handle: cx.focus_handle(), @@ -32,74 +32,83 @@ impl ThreadHistory { } } - pub fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext) { - let count = self.thread_store.read(cx).non_empty_len(cx); - + pub fn select_prev( + &mut self, + _: &menu::SelectPrev, + window: &mut Window, + cx: &mut Context, + ) { + let count = self.thread_store.read(cx).thread_count(); if count > 0 { if self.selected_index == 0 { - self.set_selected_index(count - 1, cx); + self.set_selected_index(count - 1, window, cx); } else { - self.set_selected_index(self.selected_index - 1, cx); + self.set_selected_index(self.selected_index - 1, window, cx); } } } - pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { - let count = self.thread_store.read(cx).non_empty_len(cx); - + pub fn select_next( + &mut self, + _: &menu::SelectNext, + window: &mut Window, + cx: &mut Context, + ) { + let count = self.thread_store.read(cx).thread_count(); if count > 0 { if self.selected_index == count - 1 { - self.set_selected_index(0, cx); + self.set_selected_index(0, window, cx); } else { - self.set_selected_index(self.selected_index + 1, cx); + self.set_selected_index(self.selected_index + 1, window, cx); } } } - fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext) { - let count = self.thread_store.read(cx).non_empty_len(cx); + fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context) { + let count = self.thread_store.read(cx).thread_count(); if count > 0 { - self.set_selected_index(0, cx); + self.set_selected_index(0, window, cx); } } - fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext) { - let count = self.thread_store.read(cx).non_empty_len(cx); + fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context) { + let count = self.thread_store.read(cx).thread_count(); if count > 0 { - self.set_selected_index(count - 1, cx); + self.set_selected_index(count - 1, window, cx); } } - fn set_selected_index(&mut self, index: usize, cx: &mut ViewContext) { + fn set_selected_index(&mut self, index: usize, _window: &mut Window, cx: &mut Context) { self.selected_index = index; self.scroll_handle .scroll_to_item(index, ScrollStrategy::Top); cx.notify(); } - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - let threads = self.thread_store.update(cx, |this, cx| this.threads(cx)); + fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { + let threads = self.thread_store.update(cx, |this, _cx| this.threads()); if let Some(thread) = threads.get(self.selected_index) { self.assistant_panel - .update(cx, move |this, cx| { - let thread_id = thread.read(cx).id().clone(); - this.open_thread(&thread_id, cx) - }) + .update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)) .ok(); cx.notify(); } } - fn remove_selected_thread(&mut self, _: &RemoveSelectedThread, cx: &mut ViewContext) { - let threads = self.thread_store.update(cx, |this, cx| this.threads(cx)); + fn remove_selected_thread( + &mut self, + _: &RemoveSelectedThread, + _window: &mut Window, + cx: &mut Context, + ) { + let threads = self.thread_store.update(cx, |this, _cx| this.threads()); if let Some(thread) = threads.get(self.selected_index) { self.assistant_panel .update(cx, |this, cx| { - let thread_id = thread.read(cx).id().clone(); - this.delete_thread(&thread_id, cx); + this.delete_thread(&thread.id, cx); }) .ok(); @@ -108,15 +117,15 @@ impl ThreadHistory { } } -impl FocusableView for ThreadHistory { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { +impl Focusable for ThreadHistory { + fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for ThreadHistory { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let threads = self.thread_store.update(cx, |this, cx| this.threads(cx)); + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let threads = self.thread_store.update(cx, |this, _cx| this.threads()); let selected_index = self.selected_index; v_flex() @@ -145,10 +154,10 @@ impl Render for ThreadHistory { } else { history.child( uniform_list( - cx.view().clone(), + cx.entity().clone(), "thread-history", threads.len(), - move |history, range, _cx| { + move |history, range, _window, _cx| { threads[range] .iter() .enumerate() @@ -172,15 +181,15 @@ impl Render for ThreadHistory { #[derive(IntoElement)] pub struct PastThread { - thread: Model, - assistant_panel: WeakView, + thread: SavedThreadMetadata, + assistant_panel: WeakEntity, selected: bool, } impl PastThread { pub fn new( - thread: Model, - assistant_panel: WeakView, + thread: SavedThreadMetadata, + assistant_panel: WeakEntity, selected: bool, ) -> Self { Self { @@ -192,15 +201,11 @@ impl PastThread { } impl RenderOnce for PastThread { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let (id, summary) = { - let thread = self.thread.read(cx); - (thread.id().clone(), thread.summary_or_default()) - }; + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let summary = self.thread.summary; let thread_timestamp = time_format::format_localized_timestamp( - OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp()) - .unwrap(), + OffsetDateTime::from_unix_timestamp(self.thread.updated_at.timestamp()).unwrap(), OffsetDateTime::now_utc(), self.assistant_panel .update(cx, |this, _cx| this.local_timezone()) @@ -208,7 +213,7 @@ impl RenderOnce for PastThread { time_format::TimestampFormat::EnhancedAbsolute, ); - ListItem::new(("past-thread", self.thread.entity_id())) + ListItem::new(SharedString::from(self.thread.id.to_string())) .outlined() .toggle_state(self.selected) .start_slot( @@ -220,21 +225,21 @@ impl RenderOnce for PastThread { .child(Label::new(summary).size(LabelSize::Small).text_ellipsis()) .end_slot( h_flex() - .gap_2() + .gap_1p5() .child( Label::new(thread_timestamp) - .color(Color::Disabled) - .size(LabelSize::Small), + .color(Color::Muted) + .size(LabelSize::XSmall), ) .child( IconButton::new("delete", IconName::TrashAlt) .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip(|cx| Tooltip::text("Delete Thread", cx)) + .icon_size(IconSize::XSmall) + .tooltip(Tooltip::text("Delete Thread")) .on_click({ let assistant_panel = self.assistant_panel.clone(); - let id = id.clone(); - move |_event, cx| { + let id = self.thread.id.clone(); + move |_event, _window, cx| { assistant_panel .update(cx, |this, cx| { this.delete_thread(&id, cx); @@ -246,11 +251,11 @@ impl RenderOnce for PastThread { ) .on_click({ let assistant_panel = self.assistant_panel.clone(); - let id = id.clone(); - move |_event, cx| { + let id = self.thread.id.clone(); + move |_event, window, cx| { assistant_panel .update(cx, |this, cx| { - this.open_thread(&id, cx); + this.open_thread(&id, window, cx).detach_and_log_err(cx); }) .ok(); } diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index e07e447f79e2b0..3e7ee82da91713 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -1,98 +1,171 @@ +use std::path::PathBuf; use std::sync::Arc; -use anyhow::Result; +use anyhow::{anyhow, Result}; use assistant_tool::{ToolId, ToolWorkingSet}; +use chrono::{DateTime, Utc}; use collections::HashMap; use context_server::manager::ContextServerManager; use context_server::{ContextServerFactoryRegistry, ContextServerTool}; -use gpui::{prelude::*, AppContext, Model, ModelContext, Task}; +use futures::future::{self, BoxFuture, Shared}; +use futures::FutureExt as _; +use gpui::{prelude::*, App, BackgroundExecutor, Context, Entity, SharedString, Task}; +use heed::types::SerdeBincode; +use heed::Database; +use language_model::Role; use project::Project; -use unindent::Unindent; +use serde::{Deserialize, Serialize}; use util::ResultExt as _; -use crate::thread::{Thread, ThreadId}; +use crate::thread::{MessageId, Thread, ThreadId}; pub struct ThreadStore { #[allow(unused)] - project: Model, + project: Entity, tools: Arc, - context_server_manager: Model, + context_server_manager: Entity, context_server_tool_ids: HashMap, Vec>, - threads: Vec>, + threads: Vec, + database_future: Shared, Arc>>>, } impl ThreadStore { pub fn new( - project: Model, + project: Entity, tools: Arc, - cx: &mut AppContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let this = cx.new_model(|cx: &mut ModelContext| { - let context_server_factory_registry = - ContextServerFactoryRegistry::default_global(cx); - let context_server_manager = cx.new_model(|cx| { - ContextServerManager::new(context_server_factory_registry, project.clone(), cx) - }); - - let mut this = Self { - project, - tools, - context_server_manager, - context_server_tool_ids: HashMap::default(), - threads: Vec::new(), - }; - this.mock_recent_threads(cx); - this.register_context_server_handlers(cx); + cx: &mut App, + ) -> Result> { + let this = cx.new(|cx| { + let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx); + let context_server_manager = cx.new(|cx| { + ContextServerManager::new(context_server_factory_registry, project.clone(), cx) + }); + + let executor = cx.background_executor().clone(); + let database_future = executor + .spawn({ + let executor = executor.clone(); + let database_path = paths::support_dir().join("threads/threads-db.0.mdb"); + async move { ThreadsDatabase::new(database_path, executor) } + }) + .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new))) + .boxed() + .shared(); + + let this = Self { + project, + tools, + context_server_manager, + context_server_tool_ids: HashMap::default(), + threads: Vec::new(), + database_future, + }; + this.register_context_server_handlers(cx); + this.reload(cx).detach_and_log_err(cx); + + this + }); + + Ok(this) + } - this - })?; + /// Returns the number of threads. + pub fn thread_count(&self) -> usize { + self.threads.len() + } - Ok(this) - }) + pub fn threads(&self) -> Vec { + let mut threads = self.threads.iter().cloned().collect::>(); + threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at)); + threads } - /// Returns the number of non-empty threads. - pub fn non_empty_len(&self, cx: &AppContext) -> usize { - self.threads - .iter() - .filter(|thread| !thread.read(cx).is_empty()) - .count() + pub fn recent_threads(&self, limit: usize) -> Vec { + self.threads().into_iter().take(limit).collect() } - pub fn threads(&self, cx: &ModelContext) -> Vec> { - let mut threads = self - .threads - .iter() - .filter(|thread| !thread.read(cx).is_empty()) - .cloned() - .collect::>(); - threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.read(cx).updated_at())); - threads + pub fn create_thread(&mut self, cx: &mut Context) -> Entity { + cx.new(|cx| Thread::new(self.tools.clone(), cx)) } - pub fn recent_threads(&self, limit: usize, cx: &ModelContext) -> Vec> { - self.threads(cx).into_iter().take(limit).collect() + pub fn open_thread( + &self, + id: &ThreadId, + cx: &mut Context, + ) -> Task>> { + let id = id.clone(); + let database_future = self.database_future.clone(); + cx.spawn(|this, mut cx| async move { + let database = database_future.await.map_err(|err| anyhow!(err))?; + let thread = database + .try_find_thread(id.clone()) + .await? + .ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?; + + this.update(&mut cx, |this, cx| { + cx.new(|cx| Thread::from_saved(id.clone(), thread, this.tools.clone(), cx)) + }) + }) } - pub fn create_thread(&mut self, cx: &mut ModelContext) -> Model { - let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx)); - self.threads.push(thread.clone()); - thread + pub fn save_thread(&self, thread: &Entity, cx: &mut Context) -> Task> { + let (metadata, thread) = thread.update(cx, |thread, _cx| { + let id = thread.id().clone(); + let thread = SavedThread { + summary: thread.summary_or_default(), + updated_at: thread.updated_at(), + messages: thread + .messages() + .map(|message| SavedMessage { + id: message.id, + role: message.role, + text: message.text.clone(), + }) + .collect(), + }; + + (id, thread) + }); + + let database_future = self.database_future.clone(); + cx.spawn(|this, mut cx| async move { + let database = database_future.await.map_err(|err| anyhow!(err))?; + database.save_thread(metadata, thread).await?; + + this.update(&mut cx, |this, cx| this.reload(cx))?.await + }) } - pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext) -> Option> { - self.threads - .iter() - .find(|thread| thread.read(cx).id() == id) - .cloned() + pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut Context) -> Task> { + let id = id.clone(); + let database_future = self.database_future.clone(); + cx.spawn(|this, mut cx| async move { + let database = database_future.await.map_err(|err| anyhow!(err))?; + database.delete_thread(id.clone()).await?; + + this.update(&mut cx, |this, _cx| { + this.threads.retain(|thread| thread.id != id) + }) + }) } - pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut ModelContext) { - self.threads.retain(|thread| thread.read(cx).id() != id); + fn reload(&self, cx: &mut Context) -> Task> { + let database_future = self.database_future.clone(); + cx.spawn(|this, mut cx| async move { + let threads = database_future + .await + .map_err(|err| anyhow!(err))? + .list_threads() + .await?; + + this.update(&mut cx, |this, cx| { + this.threads = threads; + cx.notify(); + }) + }) } - fn register_context_server_handlers(&self, cx: &mut ModelContext) { + fn register_context_server_handlers(&self, cx: &mut Context) { cx.subscribe( &self.context_server_manager.clone(), Self::handle_context_server_event, @@ -102,9 +175,9 @@ impl ThreadStore { fn handle_context_server_event( &mut self, - context_server_manager: Model, + context_server_manager: Entity, event: &context_server::manager::Event, - cx: &mut ModelContext, + cx: &mut Context, ) { let tool_working_set = self.tools.clone(); match event { @@ -159,133 +232,108 @@ impl ThreadStore { } } -impl ThreadStore { - /// Creates some mocked recent threads for testing purposes. - fn mock_recent_threads(&mut self, cx: &mut ModelContext) { - use language_model::Role; - - self.threads.push(cx.new_model(|cx| { - let mut thread = Thread::new(self.tools.clone(), cx); - thread.set_summary("Introduction to quantum computing", cx); - thread.insert_user_message("Hello! Can you help me understand quantum computing?", Vec::new(), cx); - thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx); - thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", Vec::new(), cx); - thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx); - thread - })); - - self.threads.push(cx.new_model(|cx| { - let mut thread = Thread::new(self.tools.clone(), cx); - thread.set_summary("Rust web development and async programming", cx); - thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", Vec::new(), cx); - thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework: - - ```rust - use actix_web::{web, App, HttpResponse, HttpServer, Responder}; - - async fn hello() -> impl Responder { - HttpResponse::Ok().body(\"Hello, World!\") - } - - #[actix_web::main] - async fn main() -> std::io::Result<()> { - HttpServer::new(|| { - App::new() - .route(\"/\", web::get().to(hello)) - }) - .bind(\"127.0.0.1:8080\")? - .run() - .await - } - ``` - - This code creates a basic web server that responds with 'Hello, World!' when you access the root URL. Here's a breakdown of what's happening: - - 1. We import necessary items from the `actix-web` crate. - 2. We define an async `hello` function that returns a simple HTTP response. - 3. In the `main` function, we set up the server to listen on `127.0.0.1:8080`. - 4. We configure the app to respond to GET requests on the root path with our `hello` function. - - To run this, you'd need to add `actix-web` to your `Cargo.toml` dependencies: - - ```toml - [dependencies] - actix-web = \"4.0\" - ``` - - Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx); - thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", Vec::new(), cx); - thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview: - - 1. **Syntax**: Async functions are declared using the `async` keyword: - - ```rust - async fn my_async_function() -> Result<(), Error> { - // Asynchronous code here - } - ``` - - 2. **Futures**: Async functions return a `Future`. A `Future` represents a value that may not be available yet but will be at some point. - - 3. **Await**: Inside an async function, you can use the `.await` syntax to wait for other async operations to complete: - - ```rust - async fn fetch_data() -> Result { - let response = make_http_request().await?; - let data = process_response(response).await?; - Ok(data) - } - ``` - - 4. **Non-blocking**: Async functions allow the runtime to work on other tasks while waiting for I/O or other operations to complete, making efficient use of system resources. - - 5. **Runtime**: To execute async code, you need a runtime like `tokio` or `async-std`. Actix-web, which we used in the previous example, includes its own runtime. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SavedThreadMetadata { + pub id: ThreadId, + pub summary: SharedString, + pub updated_at: DateTime, +} - 6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling. +#[derive(Serialize, Deserialize)] +pub struct SavedThread { + pub summary: SharedString, + pub updated_at: DateTime, + pub messages: Vec, +} - Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx); - thread - })); +#[derive(Serialize, Deserialize)] +pub struct SavedMessage { + pub id: MessageId, + pub role: Role, + pub text: String, +} - self.threads.push(cx.new_model(|cx| { - let mut thread = Thread::new(self.tools.clone(), cx); - thread.set_summary("Rust code with long lines", cx); - thread.insert_user_message("Could you write me some Rust code with long lines?", Vec::new(), cx); - thread.insert_message(Role::Assistant, r#"Here's some Rust code with some intentionally long lines: - ```rust - use std::collections::{HashMap, HashSet}; - use std::sync::{Arc, Mutex}; - use std::thread; +struct ThreadsDatabase { + executor: BackgroundExecutor, + env: heed::Env, + threads: Database, SerdeBincode>, +} - fn main() { - let very_long_vector = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]; +impl ThreadsDatabase { + pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result { + std::fs::create_dir_all(&path)?; + + const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024; + let env = unsafe { + heed::EnvOpenOptions::new() + .map_size(ONE_GB_IN_BYTES) + .max_dbs(1) + .open(path)? + }; + + let mut txn = env.write_txn()?; + let threads = env.create_database(&mut txn, Some("threads"))?; + txn.commit()?; + + Ok(Self { + executor, + env, + threads, + }) + } - let complicated_hashmap: HashMap> = [("key1".to_string(), vec![(1, 1.1, "value1".to_string()), (2, 2.2, "value2".to_string())]), ("key2".to_string(), vec![(3, 3.3, "value3".to_string()), (4, 4.4, "value4".to_string())])].iter().cloned().collect(); + pub fn list_threads(&self) -> Task>> { + let env = self.env.clone(); + let threads = self.threads; + + self.executor.spawn(async move { + let txn = env.read_txn()?; + let mut iter = threads.iter(&txn)?; + let mut threads = Vec::new(); + while let Some((key, value)) = iter.next().transpose()? { + threads.push(SavedThreadMetadata { + id: key, + summary: value.summary, + updated_at: value.updated_at, + }); + } - let nested_structure = Arc::new(Mutex::new(HashMap::new())); + Ok(threads) + }) + } - let long_closure = |x: i32, y: i32, z: i32| -> i32 { let result = x * y + z; println!("The result of the long closure calculation is: {}", result); result }; + pub fn try_find_thread(&self, id: ThreadId) -> Task>> { + let env = self.env.clone(); + let threads = self.threads; - let thread_handles: Vec<_> = (0..10).map(|i| { - let nested_structure_clone = Arc::clone(&nested_structure); - thread::spawn(move || { - let mut lock = nested_structure_clone.lock().unwrap(); - lock.entry(format!("thread_{}", i)).or_insert_with(|| HashSet::new()).insert(i * i); - }) - }).collect(); + self.executor.spawn(async move { + let txn = env.read_txn()?; + let thread = threads.get(&txn, &id)?; + Ok(thread) + }) + } - for handle in thread_handles { - handle.join().unwrap(); - } + pub fn save_thread(&self, id: ThreadId, thread: SavedThread) -> Task> { + let env = self.env.clone(); + let threads = self.threads; - println!("The final state of the nested structure is: {:?}", nested_structure.lock().unwrap()); + self.executor.spawn(async move { + let mut txn = env.write_txn()?; + threads.put(&mut txn, &id, &thread)?; + txn.commit()?; + Ok(()) + }) + } - let complex_expression = very_long_vector.iter().filter(|&&x| x % 2 == 0).map(|&x| x * x).fold(0, |acc, x| acc + x) + long_closure(5, 10, 15); + pub fn delete_thread(&self, id: ThreadId) -> Task> { + let env = self.env.clone(); + let threads = self.threads; - println!("The result of the complex expression is: {}", complex_expression); - } - ```"#.unindent(), cx); - thread - })); + self.executor.spawn(async move { + let mut txn = env.write_txn()?; + threads.delete(&mut txn, &id)?; + txn.commit()?; + Ok(()) + }) } } diff --git a/crates/assistant2/src/ui/context_pill.rs b/crates/assistant2/src/ui/context_pill.rs index 7e82ed7b69455b..48886388bfc191 100644 --- a/crates/assistant2/src/ui/context_pill.rs +++ b/crates/assistant2/src/ui/context_pill.rs @@ -11,24 +11,24 @@ pub enum ContextPill { context: ContextSnapshot, dupe_name: bool, focused: bool, - on_click: Option>, - on_remove: Option>, + on_click: Option>, + on_remove: Option>, }, Suggested { name: SharedString, icon_path: Option, kind: ContextKind, focused: bool, - on_click: Option>, + on_click: Option>, }, } impl ContextPill { - pub fn new_added( + pub fn added( context: ContextSnapshot, dupe_name: bool, focused: bool, - on_remove: Option>, + on_remove: Option>, ) -> Self { Self::Added { context, @@ -39,7 +39,7 @@ impl ContextPill { } } - pub fn new_suggested( + pub fn suggested( name: SharedString, icon_path: Option, kind: ContextKind, @@ -54,7 +54,7 @@ impl ContextPill { } } - pub fn on_click(mut self, listener: Rc) -> Self { + pub fn on_click(mut self, listener: Rc) -> Self { match &mut self { ContextPill::Added { on_click, .. } => { *on_click = Some(listener); @@ -95,7 +95,7 @@ impl ContextPill { } impl RenderOnce for ContextPill { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let color = cx.theme().colors(); let base_pill = h_flex() @@ -139,7 +139,7 @@ impl RenderOnce for ContextPill { } }) .when_some(context.tooltip.clone(), |element, tooltip| { - element.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) + element.tooltip(Tooltip::text(tooltip.clone())) }), ) .when_some(on_remove.as_ref(), |element, on_remove| { @@ -147,16 +147,16 @@ impl RenderOnce for ContextPill { IconButton::new(("remove", context.id.0), IconName::Close) .shape(IconButtonShape::Square) .icon_size(IconSize::XSmall) - .tooltip(|cx| Tooltip::text("Remove Context", cx)) + .tooltip(Tooltip::text("Remove Context")) .on_click({ let on_remove = on_remove.clone(); - move |event, cx| on_remove(event, cx) + move |event, window, cx| on_remove(event, window, cx) }), ) }) .when_some(on_click.as_ref(), |element, on_click| { let on_click = on_click.clone(); - element.on_click(move |event, cx| on_click(event, cx)) + element.on_click(move |event, window, cx| on_click(event, window, cx)) }), ContextPill::Suggested { name, @@ -195,10 +195,12 @@ impl RenderOnce for ContextPill { .size(IconSize::XSmall) .into_any_element(), ) - .tooltip(|cx| Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)) + .tooltip(|window, cx| { + Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx) + }) .when_some(on_click.as_ref(), |element, on_click| { let on_click = on_click.clone(); - element.on_click(move |event, cx| on_click(event, cx)) + element.on_click(move |event, window, cx| on_click(event, window, cx)) }), } } diff --git a/crates/assistant_context_editor/Cargo.toml b/crates/assistant_context_editor/Cargo.toml new file mode 100644 index 00000000000000..8ad036893bc887 --- /dev/null +++ b/crates/assistant_context_editor/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "assistant_context_editor" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/assistant_context_editor.rs" + +[dependencies] +anyhow.workspace = true +assistant_settings.workspace = true +assistant_slash_command.workspace = true +assistant_slash_commands.workspace = true +assistant_tool.workspace = true +chrono.workspace = true +client.workspace = true +clock.workspace = true +collections.workspace = true +context_server.workspace = true +editor.workspace = true +feature_flags.workspace = true +fs.workspace = true +futures.workspace = true +fuzzy.workspace = true +gpui.workspace = true +indexed_docs.workspace = true +language.workspace = true +language_model.workspace = true +language_model_selector.workspace = true +language_models.workspace = true +log.workspace = true +multi_buffer.workspace = true +open_ai.workspace = true +parking_lot.workspace = true +paths.workspace = true +picker.workspace = true +project.workspace = true +prompt_library.workspace = true +regex.workspace = true +rope.workspace = true +rpc.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +smallvec.workspace = true +smol.workspace = true +strum.workspace = true +telemetry_events.workspace = true +text.workspace = true +theme.workspace = true +ui.workspace = true +util.workspace = true +uuid.workspace = true +workspace.workspace = true + +[dev-dependencies] +language_model = { workspace = true, features = ["test-support"] } +languages = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true +rand.workspace = true +tree-sitter-md.workspace = true +unindent.workspace = true +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant_context_editor/LICENSE-GPL b/crates/assistant_context_editor/LICENSE-GPL new file mode 120000 index 00000000000000..89e542f750cd38 --- /dev/null +++ b/crates/assistant_context_editor/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant_context_editor/src/assistant_context_editor.rs b/crates/assistant_context_editor/src/assistant_context_editor.rs new file mode 100644 index 00000000000000..b399059beb92c7 --- /dev/null +++ b/crates/assistant_context_editor/src/assistant_context_editor.rs @@ -0,0 +1,23 @@ +mod context; +mod context_editor; +mod context_history; +mod context_store; +mod patch; +mod slash_command; +mod slash_command_picker; + +use std::sync::Arc; + +use client::Client; +use gpui::App; + +pub use crate::context::*; +pub use crate::context_editor::*; +pub use crate::context_history::*; +pub use crate::context_store::*; +pub use crate::patch::*; +pub use crate::slash_command::*; + +pub fn init(client: Arc, _cx: &mut App) { + context_store::init(&client.into()); +} diff --git a/crates/assistant/src/context.rs b/crates/assistant_context_editor/src/context.rs similarity index 95% rename from crates/assistant/src/context.rs rename to crates/assistant_context_editor/src/context.rs index 038bc0177de18b..ba3f3527c25865 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant_context_editor/src/context.rs @@ -1,14 +1,11 @@ #[cfg(test)] mod context_tests; -use crate::{ - slash_command::SlashCommandLine, AssistantEdit, AssistantPatch, AssistantPatchStatus, - MessageId, MessageStatus, -}; +use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus}; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ - SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, - SlashCommandWorkingSet, + SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection, + SlashCommandResult, SlashCommandWorkingSet, }; use assistant_slash_commands::FileCommandMetadata; use assistant_tool::ToolWorkingSet; @@ -19,11 +16,9 @@ use feature_flags::{FeatureFlagAppExt, ToolUseFeatureFlag}; use fs::{Fs, RemoveOptions}; use futures::{future::Shared, FutureExt, StreamExt}; use gpui::{ - AppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage, SharedString, - Subscription, Task, + App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription, + Task, }; -use prompt_library::PromptBuilder; - use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset}; use language_model::{ LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent, @@ -38,6 +33,7 @@ use language_models::{ use open_ai::Model as OpenAiModel; use paths::contexts_dir; use project::Project; +use prompt_library::PromptBuilder; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{ @@ -52,9 +48,9 @@ use std::{ }; use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use text::{BufferSnapshot, ToPoint}; +use ui::IconName; use util::{post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; -use workspace::ui::IconName; #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct ContextId(String); @@ -73,6 +69,64 @@ impl ContextId { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct MessageId(pub clock::Lamport); + +impl MessageId { + pub fn as_u64(self) -> u64 { + self.0.as_u64() + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum MessageStatus { + Pending, + Done, + Error(SharedString), + Canceled, +} + +impl MessageStatus { + pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus { + match status.variant { + Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending, + Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done, + Some(proto::context_message_status::Variant::Error(error)) => { + MessageStatus::Error(error.message.into()) + } + Some(proto::context_message_status::Variant::Canceled(_)) => MessageStatus::Canceled, + None => MessageStatus::Pending, + } + } + + pub fn to_proto(&self) -> proto::ContextMessageStatus { + match self { + MessageStatus::Pending => proto::ContextMessageStatus { + variant: Some(proto::context_message_status::Variant::Pending( + proto::context_message_status::Pending {}, + )), + }, + MessageStatus::Done => proto::ContextMessageStatus { + variant: Some(proto::context_message_status::Variant::Done( + proto::context_message_status::Done {}, + )), + }, + MessageStatus::Error(message) => proto::ContextMessageStatus { + variant: Some(proto::context_message_status::Variant::Error( + proto::context_message_status::Error { + message: message.to_string(), + }, + )), + }, + MessageStatus::Canceled => proto::ContextMessageStatus { + variant: Some(proto::context_message_status::Variant::Canceled( + proto::context_message_status::Canceled {}, + )), + }, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RequestType { /// Request a normal chat response from the model. @@ -423,7 +477,7 @@ pub struct MessageCacheMetadata { pub struct MessageMetadata { pub role: Role, pub status: MessageStatus, - pub(crate) timestamp: clock::Lamport, + pub timestamp: clock::Lamport, #[serde(skip)] pub cache: Option, } @@ -534,18 +588,18 @@ pub enum XmlTagKind { Operation, } -pub struct Context { +pub struct AssistantContext { id: ContextId, timestamp: clock::Lamport, version: clock::Global, pending_ops: Vec, operations: Vec, - buffer: Model, + buffer: Entity, parsed_slash_commands: Vec, invoked_slash_commands: HashMap, edits_since_last_parse: language::Subscription, - pub(crate) slash_commands: Arc, - pub(crate) tools: Arc, + slash_commands: Arc, + tools: Arc, slash_command_output_sections: Vec>, pending_tool_uses_by_id: HashMap, message_anchors: Vec, @@ -565,7 +619,7 @@ pub struct Context { language_registry: Arc, patches: Vec, xml_tags: Vec, - project: Option>, + project: Option>, prompt_builder: Arc, } @@ -591,17 +645,17 @@ impl ContextAnnotation for XmlTag { } } -impl EventEmitter for Context {} +impl EventEmitter for AssistantContext {} -impl Context { +impl AssistantContext { pub fn local( language_registry: Arc, - project: Option>, + project: Option>, telemetry: Option>, prompt_builder: Arc, slash_commands: Arc, tools: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { Self::new( ContextId::new(), @@ -626,11 +680,11 @@ impl Context { prompt_builder: Arc, slash_commands: Arc, tools: Arc, - project: Option>, + project: Option>, telemetry: Option>, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { - let buffer = cx.new_model(|_cx| { + let buffer = cx.new(|_cx| { let buffer = Buffer::remote( language::BufferId::new(1).unwrap(), replica_id, @@ -701,7 +755,7 @@ impl Context { this } - pub(crate) fn serialize(&self, cx: &AppContext) -> SavedContext { + pub(crate) fn serialize(&self, cx: &App) -> SavedContext { let buffer = self.buffer.read(cx); SavedContext { id: Some(self.id.clone()), @@ -749,9 +803,9 @@ impl Context { prompt_builder: Arc, slash_commands: Arc, tools: Arc, - project: Option>, + project: Option>, telemetry: Option>, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { let id = saved_context.id.clone().unwrap_or_else(ContextId::new); let mut this = Self::new( @@ -783,18 +837,22 @@ impl Context { self.timestamp.replica_id } - pub fn version(&self, cx: &AppContext) -> ContextVersion { + pub fn version(&self, cx: &App) -> ContextVersion { ContextVersion { context: self.version.clone(), buffer: self.buffer.read(cx).version(), } } - pub fn set_capability( - &mut self, - capability: language::Capability, - cx: &mut ModelContext, - ) { + pub fn slash_commands(&self) -> &Arc { + &self.slash_commands + } + + pub fn tools(&self) -> &Arc { + &self.tools + } + + pub fn set_capability(&mut self, capability: language::Capability, cx: &mut Context) { self.buffer .update(cx, |buffer, cx| buffer.set_capability(capability, cx)); } @@ -808,7 +866,7 @@ impl Context { pub fn serialize_ops( &self, since: &ContextVersion, - cx: &AppContext, + cx: &App, ) -> Task> { let buffer_ops = self .buffer @@ -843,7 +901,7 @@ impl Context { pub fn apply_ops( &mut self, ops: impl IntoIterator, - cx: &mut ModelContext, + cx: &mut Context, ) { let mut buffer_ops = Vec::new(); for op in ops { @@ -857,7 +915,7 @@ impl Context { self.flush_ops(cx); } - fn flush_ops(&mut self, cx: &mut ModelContext) { + fn flush_ops(&mut self, cx: &mut Context) { let mut changed_messages = HashSet::default(); let mut summary_changed = false; @@ -976,7 +1034,7 @@ impl Context { } } - fn can_apply_op(&self, op: &ContextOperation, cx: &AppContext) -> bool { + fn can_apply_op(&self, op: &ContextOperation, cx: &App) -> bool { if !self.version.observed_all(op.version()) { return false; } @@ -1007,7 +1065,7 @@ impl Context { fn has_received_operations_for_anchor_range( &self, range: Range, - cx: &AppContext, + cx: &App, ) -> bool { let version = &self.buffer.read(cx).version; let observed_start = range.start == language::Anchor::MIN @@ -1019,12 +1077,12 @@ impl Context { observed_start && observed_end } - fn push_op(&mut self, op: ContextOperation, cx: &mut ModelContext) { + fn push_op(&mut self, op: ContextOperation, cx: &mut Context) { self.operations.push(op.clone()); cx.emit(ContextEvent::Operation(op)); } - pub fn buffer(&self) -> &Model { + pub fn buffer(&self) -> &Entity { &self.buffer } @@ -1032,7 +1090,7 @@ impl Context { self.language_registry.clone() } - pub fn project(&self) -> Option> { + pub fn project(&self) -> Option> { self.project.clone() } @@ -1048,11 +1106,7 @@ impl Context { self.summary.as_ref() } - pub(crate) fn patch_containing( - &self, - position: Point, - cx: &AppContext, - ) -> Option<&AssistantPatch> { + pub fn patch_containing(&self, position: Point, cx: &App) -> Option<&AssistantPatch> { let buffer = self.buffer.read(cx); let index = self.patches.binary_search_by(|patch| { let patch_range = patch.range.to_point(&buffer); @@ -1075,10 +1129,10 @@ impl Context { self.patches.iter().map(|patch| patch.range.clone()) } - pub(crate) fn patch_for_range( + pub fn patch_for_range( &self, range: &Range, - cx: &AppContext, + cx: &App, ) -> Option<&AssistantPatch> { let buffer = self.buffer.read(cx); let index = self.patch_index_for_range(range, buffer).ok()?; @@ -1109,7 +1163,7 @@ impl Context { &self.slash_command_output_sections } - pub fn contains_files(&self, cx: &AppContext) -> bool { + pub fn contains_files(&self, cx: &App) -> bool { let buffer = self.buffer.read(cx); self.slash_command_output_sections.iter().any(|section| { section.is_valid(buffer) @@ -1131,7 +1185,7 @@ impl Context { self.pending_tool_uses_by_id.get(id) } - fn set_language(&mut self, cx: &mut ModelContext) { + fn set_language(&mut self, cx: &mut Context) { let markdown = self.language_registry.language_for_name("Markdown"); cx.spawn(|this, mut cx| async move { let markdown = markdown.await?; @@ -1145,9 +1199,9 @@ impl Context { fn handle_buffer_event( &mut self, - _: Model, + _: Entity, event: &language::BufferEvent, - cx: &mut ModelContext, + cx: &mut Context, ) { match event { language::BufferEvent::Operation { @@ -1165,11 +1219,11 @@ impl Context { } } - pub(crate) fn token_count(&self) -> Option { + pub fn token_count(&self) -> Option { self.token_count } - pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { + pub(crate) fn count_remaining_tokens(&mut self, cx: &mut Context) { // Assume it will be a Chat request, even though that takes fewer tokens (and risks going over the limit), // because otherwise you see in the UI that your empty message has a bunch of tokens already used. let request = self.to_completion_request(RequestType::Chat, cx); @@ -1197,7 +1251,7 @@ impl Context { &mut self, cache_configuration: &Option, speculative: bool, - cx: &mut ModelContext, + cx: &mut Context, ) -> bool { let cache_configuration = cache_configuration @@ -1299,7 +1353,7 @@ impl Context { new_anchor_needs_caching } - fn start_cache_warming(&mut self, model: &Arc, cx: &mut ModelContext) { + fn start_cache_warming(&mut self, model: &Arc, cx: &mut Context) { let cache_configuration = model.cache_configuration(); if !self.mark_cache_anchors(&cache_configuration, true, cx) { @@ -1349,7 +1403,7 @@ impl Context { }); } - pub fn update_cache_status_for_completion(&mut self, cx: &mut ModelContext) { + pub fn update_cache_status_for_completion(&mut self, cx: &mut Context) { let cached_message_ids: Vec = self .messages_metadata .iter() @@ -1374,7 +1428,7 @@ impl Context { cx.notify(); } - pub fn reparse(&mut self, cx: &mut ModelContext) { + pub fn reparse(&mut self, cx: &mut Context) { let buffer = self.buffer.read(cx).text_snapshot(); let mut row_ranges = self .edits_since_last_parse @@ -1447,7 +1501,7 @@ impl Context { buffer: &BufferSnapshot, updated: &mut Vec, removed: &mut Vec>, - cx: &AppContext, + cx: &App, ) { let old_range = self.pending_command_indices_for_range(range.clone(), cx); @@ -1501,7 +1555,7 @@ impl Context { fn invalidate_pending_slash_commands( &mut self, buffer: &BufferSnapshot, - cx: &mut ModelContext, + cx: &mut Context, ) { let mut invalidated_command_ids = Vec::new(); for (&command_id, command) in self.invoked_slash_commands.iter_mut() { @@ -1535,7 +1589,7 @@ impl Context { buffer: &BufferSnapshot, updated: &mut Vec>, removed: &mut Vec>, - cx: &mut ModelContext, + cx: &mut Context, ) { // Rebuild the XML tags in the edited range. let intersecting_tags_range = @@ -1578,7 +1632,7 @@ impl Context { &self, buffer: &BufferSnapshot, range: Range, - cx: &AppContext, + cx: &App, ) -> Vec { let mut messages = self.messages(cx).peekable(); @@ -1635,7 +1689,7 @@ impl Context { tags_start_ix: usize, buffer_end: text::Anchor, buffer: &BufferSnapshot, - cx: &AppContext, + cx: &App, ) -> Vec { let mut new_patches = Vec::new(); let mut pending_patch = None; @@ -1793,7 +1847,7 @@ impl Context { pub fn pending_command_for_position( &mut self, position: language::Anchor, - cx: &mut ModelContext, + cx: &mut Context, ) -> Option<&mut ParsedSlashCommand> { let buffer = self.buffer.read(cx); match self @@ -1817,7 +1871,7 @@ impl Context { pub fn pending_commands_for_range( &self, range: Range, - cx: &AppContext, + cx: &App, ) -> &[ParsedSlashCommand] { let range = self.pending_command_indices_for_range(range, cx); &self.parsed_slash_commands[range] @@ -1826,7 +1880,7 @@ impl Context { fn pending_command_indices_for_range( &self, range: Range, - cx: &AppContext, + cx: &App, ) -> Range { self.indices_intersecting_buffer_range(&self.parsed_slash_commands, range, cx) } @@ -1835,7 +1889,7 @@ impl Context { &self, all_annotations: &[T], range: Range, - cx: &AppContext, + cx: &App, ) -> Range { let buffer = self.buffer.read(cx); let start_ix = match all_annotations @@ -1858,7 +1912,7 @@ impl Context { name: &str, output: Task, ensure_trailing_newline: bool, - cx: &mut ModelContext, + cx: &mut Context, ) { let version = self.version.clone(); let command_id = InvokedSlashCommandId(self.next_timestamp()); @@ -2126,7 +2180,7 @@ impl Context { fn insert_slash_command_output_section( &mut self, section: SlashCommandOutputSection, - cx: &mut ModelContext, + cx: &mut Context, ) { let buffer = self.buffer.read(cx); let insertion_ix = match self @@ -2156,7 +2210,7 @@ impl Context { &mut self, tool_use_id: LanguageModelToolUseId, output: Task>, - cx: &mut ModelContext, + cx: &mut Context, ) { let insert_output_task = cx.spawn(|this, mut cx| { let tool_use_id = tool_use_id.clone(); @@ -2214,11 +2268,11 @@ impl Context { } } - pub fn completion_provider_changed(&mut self, cx: &mut ModelContext) { + pub fn completion_provider_changed(&mut self, cx: &mut Context) { self.count_remaining_tokens(cx); } - fn get_last_valid_message_id(&self, cx: &ModelContext) -> Option { + fn get_last_valid_message_id(&self, cx: &Context) -> Option { self.message_anchors.iter().rev().find_map(|message| { message .start @@ -2230,7 +2284,7 @@ impl Context { pub fn assist( &mut self, request_type: RequestType, - cx: &mut ModelContext, + cx: &mut Context, ) -> Option { let model_registry = LanguageModelRegistry::read_global(cx); let provider = model_registry.active_provider()?; @@ -2246,7 +2300,10 @@ impl Context { let mut request = self.to_completion_request(request_type, cx); - if cx.has_flag::() { + // Don't attach tools for now; we'll be removing tool use from + // Assistant1 shortly. + #[allow(clippy::overly_complex_bool_expr)] + if false && cx.has_flag::() { request.tools = self .tools .tools(cx) @@ -2458,7 +2515,7 @@ impl Context { pub fn to_completion_request( &self, request_type: RequestType, - cx: &AppContext, + cx: &App, ) -> LanguageModelRequest { let buffer = self.buffer.read(cx); @@ -2570,7 +2627,7 @@ impl Context { completion_request } - pub fn cancel_last_assist(&mut self, cx: &mut ModelContext) -> bool { + pub fn cancel_last_assist(&mut self, cx: &mut Context) -> bool { if let Some(pending_completion) = self.pending_completions.pop() { self.update_metadata(pending_completion.assistant_message_id, cx, |metadata| { if metadata.status == MessageStatus::Pending { @@ -2583,7 +2640,7 @@ impl Context { } } - pub fn cycle_message_roles(&mut self, ids: HashSet, cx: &mut ModelContext) { + pub fn cycle_message_roles(&mut self, ids: HashSet, cx: &mut Context) { for id in &ids { if let Some(metadata) = self.messages_metadata.get(id) { let role = metadata.role.cycle(); @@ -2594,7 +2651,7 @@ impl Context { self.message_roles_updated(ids, cx); } - fn message_roles_updated(&mut self, ids: HashSet, cx: &mut ModelContext) { + fn message_roles_updated(&mut self, ids: HashSet, cx: &mut Context) { let mut ranges = Vec::new(); for message in self.messages(cx) { if ids.contains(&message.id) { @@ -2617,7 +2674,7 @@ impl Context { pub fn update_metadata( &mut self, id: MessageId, - cx: &mut ModelContext, + cx: &mut Context, f: impl FnOnce(&mut MessageMetadata), ) { let version = self.version.clone(); @@ -2641,7 +2698,7 @@ impl Context { message_id: MessageId, role: Role, status: MessageStatus, - cx: &mut ModelContext, + cx: &mut Context, ) -> Option { if let Some(prev_message_ix) = self .message_anchors @@ -2675,7 +2732,7 @@ impl Context { offset: usize, role: Role, status: MessageStatus, - cx: &mut ModelContext, + cx: &mut Context, ) -> MessageAnchor { let start = self.buffer.update(cx, |buffer, cx| { buffer.edit([(offset..offset, "\n")], None, cx); @@ -2705,7 +2762,7 @@ impl Context { anchor } - pub fn insert_content(&mut self, content: Content, cx: &mut ModelContext) { + pub fn insert_content(&mut self, content: Content, cx: &mut Context) { let buffer = self.buffer.read(cx); let insertion_ix = match self .contents @@ -2721,7 +2778,7 @@ impl Context { cx.emit(ContextEvent::MessagesEdited); } - pub fn contents<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator { + pub fn contents<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator { let buffer = self.buffer.read(cx); self.contents .iter() @@ -2735,7 +2792,7 @@ impl Context { pub fn split_message( &mut self, range: Range, - cx: &mut ModelContext, + cx: &mut Context, ) -> (Option, Option) { let start_message = self.message_for_offset(range.start, cx); let end_message = self.message_for_offset(range.end, cx); @@ -2861,7 +2918,7 @@ impl Context { &mut self, new_anchor: MessageAnchor, new_metadata: MessageMetadata, - cx: &mut ModelContext, + cx: &mut Context, ) { cx.emit(ContextEvent::MessagesEdited); @@ -2879,7 +2936,7 @@ impl Context { self.message_anchors.insert(insertion_ix, new_anchor); } - pub(super) fn summarize(&mut self, replace_old: bool, cx: &mut ModelContext) { + pub fn summarize(&mut self, replace_old: bool, cx: &mut Context) { let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else { return; }; @@ -2957,14 +3014,14 @@ impl Context { } } - fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option { + fn message_for_offset(&self, offset: usize, cx: &App) -> Option { self.messages_for_offsets([offset], cx).pop() } pub fn messages_for_offsets( &self, offsets: impl IntoIterator, - cx: &AppContext, + cx: &App, ) -> Vec { let mut result = Vec::new(); @@ -2997,14 +3054,14 @@ impl Context { fn messages_from_anchors<'a>( &'a self, message_anchors: impl Iterator + 'a, - cx: &'a AppContext, + cx: &'a App, ) -> impl 'a + Iterator { let buffer = self.buffer.read(cx); Self::messages_from_iters(buffer, &self.messages_metadata, message_anchors.enumerate()) } - pub fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator { + pub fn messages<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator { self.messages_from_anchors(self.message_anchors.iter(), cx) } @@ -3052,7 +3109,7 @@ impl Context { &mut self, debounce: Option, fs: Arc, - cx: &mut ModelContext, + cx: &mut Context, ) { if self.replica_id() != ReplicaId::default() { // Prevent saving a remote context for now. @@ -3118,7 +3175,7 @@ impl Context { }); } - pub(crate) fn custom_summary(&mut self, custom_summary: String, cx: &mut ModelContext) { + pub fn custom_summary(&mut self, custom_summary: String, cx: &mut Context) { let timestamp = self.next_timestamp(); let summary = self.summary.get_or_insert(ContextSummary::default()); summary.timestamp = timestamp; @@ -3278,8 +3335,8 @@ impl SavedContext { fn into_ops( self, - buffer: &Model, - cx: &mut ModelContext, + buffer: &Entity, + cx: &mut Context, ) -> Vec { let mut operations = Vec::new(); let mut version = clock::Global::new(); diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant_context_editor/src/context/context_tests.rs similarity index 96% rename from crates/assistant/src/context/context_tests.rs rename to crates/assistant_context_editor/src/context/context_tests.rs index 6876b29913b035..5f433239b24b67 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant_context_editor/src/context/context_tests.rs @@ -1,7 +1,6 @@ -use super::{AssistantEdit, MessageCacheMetadata}; use crate::{ - assistant_panel, AssistantEditKind, CacheStatus, Context, ContextEvent, ContextId, - ContextOperation, InvokedSlashCommandId, MessageId, MessageStatus, + AssistantContext, AssistantEdit, AssistantEditKind, CacheStatus, ContextEvent, ContextId, + ContextOperation, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus, }; use anyhow::Result; use assistant_slash_command::{ @@ -16,7 +15,7 @@ use futures::{ channel::mpsc, stream::{self, StreamExt}, }; -use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView}; +use gpui::{prelude::*, App, Entity, SharedString, Task, TestAppContext, WeakEntity}; use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate}; use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role}; use parking_lot::Mutex; @@ -35,7 +34,7 @@ use std::{ sync::{atomic::AtomicBool, Arc}, }; use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset}; -use ui::{IconName, WindowContext}; +use ui::{IconName, Window}; use unindent::Unindent; use util::{ test::{generate_marked_text, marked_text_ranges}, @@ -44,15 +43,14 @@ use util::{ use workspace::Workspace; #[gpui::test] -fn test_inserting_and_removing_messages(cx: &mut AppContext) { +fn test_inserting_and_removing_messages(cx: &mut App) { let settings_store = SettingsStore::test(cx); LanguageModelRegistry::test(cx); cx.set_global(settings_store); - assistant_panel::init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new_model(|cx| { - Context::local( + let context = cx.new(|cx| { + AssistantContext::local( registry, None, None, @@ -185,16 +183,15 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) { } #[gpui::test] -fn test_message_splitting(cx: &mut AppContext) { +fn test_message_splitting(cx: &mut App) { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); LanguageModelRegistry::test(cx); - assistant_panel::init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new_model(|cx| { - Context::local( + let context = cx.new(|cx| { + AssistantContext::local( registry.clone(), None, None, @@ -290,15 +287,14 @@ fn test_message_splitting(cx: &mut AppContext) { } #[gpui::test] -fn test_messages_for_offsets(cx: &mut AppContext) { +fn test_messages_for_offsets(cx: &mut App) { let settings_store = SettingsStore::test(cx); LanguageModelRegistry::test(cx); cx.set_global(settings_store); - assistant_panel::init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new_model(|cx| { - Context::local( + let context = cx.new(|cx| { + AssistantContext::local( registry, None, None, @@ -371,9 +367,9 @@ fn test_messages_for_offsets(cx: &mut AppContext) { ); fn message_ids_for_offsets( - context: &Model, + context: &Entity, offsets: &[usize], - cx: &AppContext, + cx: &App, ) -> Vec { context .read(cx) @@ -390,7 +386,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) { cx.set_global(settings_store); cx.update(LanguageModelRegistry::test); cx.update(Project::init_settings); - cx.update(assistant_panel::init); let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( @@ -412,8 +407,8 @@ async fn test_slash_commands(cx: &mut TestAppContext) { let registry = Arc::new(LanguageRegistry::test(cx.executor())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new_model(|cx| { - Context::local( + let context = cx.new(|cx| { + AssistantContext::local( registry.clone(), None, None, @@ -613,7 +608,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) { #[track_caller] fn assert_text_and_context_ranges( - buffer: &Model, + buffer: &Entity, ranges: &RefCell, expected_marked_text: &str, cx: &mut TestAppContext, @@ -698,13 +693,12 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { let project = Project::test(fs, [Path::new("/root")], cx).await; cx.update(LanguageModelRegistry::test); - cx.update(assistant_panel::init); let registry = Arc::new(LanguageRegistry::test(cx.executor())); // Create a new context let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new_model(|cx| { - Context::local( + let context = cx.new(|cx| { + AssistantContext::local( registry.clone(), Some(project), None, @@ -968,8 +962,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { // Ensure steps are re-parsed when deserializing. let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx)); - let deserialized_context = cx.new_model(|cx| { - Context::deserialize( + let deserialized_context = cx.new(|cx| { + AssistantContext::deserialize( serialized_context, Default::default(), registry.clone(), @@ -1012,7 +1006,11 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { cx, ); - fn edit(context: &Model, new_text_marked_with_edits: &str, cx: &mut TestAppContext) { + fn edit( + context: &Entity, + new_text_marked_with_edits: &str, + cx: &mut TestAppContext, + ) { context.update(cx, |context, cx| { context.buffer.update(cx, |buffer, cx| { buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx); @@ -1023,7 +1021,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { #[track_caller] fn expect_patches( - context: &Model, + context: &Entity, expected_marked_text: &str, expected_suggestions: &[&[AssistantEdit]], cx: &mut TestAppContext, @@ -1081,11 +1079,10 @@ async fn test_serialization(cx: &mut TestAppContext) { let settings_store = cx.update(SettingsStore::test); cx.set_global(settings_store); cx.update(LanguageModelRegistry::test); - cx.update(assistant_panel::init); let registry = Arc::new(LanguageRegistry::test(cx.executor())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new_model(|cx| { - Context::local( + let context = cx.new(|cx| { + AssistantContext::local( registry.clone(), None, None, @@ -1128,8 +1125,8 @@ async fn test_serialization(cx: &mut TestAppContext) { ); let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx)); - let deserialized_context = cx.new_model(|cx| { - Context::deserialize( + let deserialized_context = cx.new(|cx| { + AssistantContext::deserialize( serialized_context, Default::default(), registry.clone(), @@ -1173,7 +1170,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std cx.set_global(settings_store); cx.update(LanguageModelRegistry::test); - cx.update(assistant_panel::init); let slash_commands = cx.update(SlashCommandRegistry::default_global); slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false); slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false); @@ -1187,8 +1183,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std let context_id = ContextId::new(); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); for i in 0..num_peers { - let context = cx.new_model(|cx| { - Context::new( + let context = cx.new(|cx| { + AssistantContext::new( context_id.clone(), i as ReplicaId, language::Capability::ReadWrite, @@ -1442,15 +1438,14 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std } #[gpui::test] -fn test_mark_cache_anchors(cx: &mut AppContext) { +fn test_mark_cache_anchors(cx: &mut App) { let settings_store = SettingsStore::test(cx); LanguageModelRegistry::test(cx); cx.set_global(settings_store); - assistant_panel::init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new_model(|cx| { - Context::local( + let context = cx.new(|cx| { + AssistantContext::local( registry, None, None, @@ -1603,7 +1598,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) { ); } -fn messages(context: &Model, cx: &AppContext) -> Vec<(MessageId, Role, Range)> { +fn messages(context: &Entity, cx: &App) -> Vec<(MessageId, Role, Range)> { context .read(cx) .messages(cx) @@ -1612,8 +1607,8 @@ fn messages(context: &Model, cx: &AppContext) -> Vec<(MessageId, Role, } fn messages_cache( - context: &Model, - cx: &AppContext, + context: &Entity, + cx: &App, ) -> Vec<(MessageId, Option)> { context .read(cx) @@ -1642,8 +1637,9 @@ impl SlashCommand for FakeSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Ok(vec![])) } @@ -1657,9 +1653,10 @@ impl SlashCommand for FakeSlashCommand { _arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - _workspace: WeakView, + _workspace: WeakEntity, _delegate: Option>, - _cx: &mut WindowContext, + _window: &mut Window, + _cx: &mut App, ) -> Task { Task::ready(Ok(SlashCommandOutput { text: format!("Executed fake command: {}", self.0), diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs new file mode 100644 index 00000000000000..ad8111586a75a0 --- /dev/null +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -0,0 +1,3788 @@ +use anyhow::Result; +use assistant_settings::AssistantSettings; +use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet}; +use assistant_slash_commands::{ + selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, + FileSlashCommand, +}; +use assistant_tool::ToolWorkingSet; +use client::{proto, zed_urls}; +use collections::{hash_map, BTreeSet, HashMap, HashSet}; +use editor::{ + actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt}, + display_map::{ + BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, + CustomBlockId, FoldId, RenderBlock, ToDisplayPoint, + }, + scroll::{Autoscroll, AutoscrollStrategy}, + Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation, + ProposedChangesEditor, RowExt, ToOffset as _, ToPoint, +}; +use editor::{display_map::CreaseId, FoldPlaceholder}; +use fs::Fs; +use futures::FutureExt; +use gpui::{ + actions, div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between, + size, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry, + ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, + Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage, + SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation, + WeakEntity, +}; +use indexed_docs::IndexedDocsStore; +use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset}; +use language_model::{ + LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry, + LanguageModelToolUse, Role, +}; +use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; +use multi_buffer::MultiBufferRow; +use picker::Picker; +use project::lsp_store::LocalLspAdapterDelegate; +use project::{Project, Worktree}; +use rope::Point; +use serde::{Deserialize, Serialize}; +use settings::{update_settings_file, Settings}; +use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration}; +use text::SelectionGoal; +use ui::{ + prelude::*, ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, + Tooltip, +}; +use util::{maybe, ResultExt}; +use workspace::searchable::SearchableItemHandle; +use workspace::{ + item::{self, FollowableItem, Item, ItemHandle}, + notifications::NotificationId, + pane::{self, SaveIntent}, + searchable::{SearchEvent, SearchableItem}, + Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, + Workspace, +}; + +use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker}; +use crate::{ + AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent, + ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, + MessageMetadata, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType, +}; + +actions!( + assistant, + [ + Assist, + ConfirmCommand, + CopyCode, + CycleMessageRole, + Edit, + InsertIntoEditor, + QuoteSelection, + Split, + ToggleModelSelector, + ] +); + +#[derive(PartialEq, Clone)] +pub enum InsertDraggedFiles { + ProjectPaths(Vec), + ExternalFiles(Vec), +} + +impl_internal_actions!(assistant, [InsertDraggedFiles]); + +#[derive(Copy, Clone, Debug, PartialEq)] +struct ScrollPosition { + offset_before_cursor: gpui::Point, + cursor: Anchor, +} + +struct PatchViewState { + crease_id: CreaseId, + editor: Option, + update_task: Option>, +} + +struct PatchEditorState { + editor: WeakEntity, + opened_patch: AssistantPatch, +} + +type MessageHeader = MessageMetadata; + +#[derive(Clone)] +enum AssistError { + FileRequired, + PaymentRequired, + MaxMonthlySpendReached, + Message(SharedString), +} + +pub trait AssistantPanelDelegate { + fn active_context_editor( + &self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) -> Option>; + + fn open_saved_context( + &self, + workspace: &mut Workspace, + path: PathBuf, + window: &mut Window, + cx: &mut Context, + ) -> Task>; + + fn open_remote_context( + &self, + workspace: &mut Workspace, + context_id: ContextId, + window: &mut Window, + cx: &mut Context, + ) -> Task>>; + + fn quote_selection( + &self, + workspace: &mut Workspace, + creases: Vec<(String, String)>, + window: &mut Window, + cx: &mut Context, + ); +} + +impl dyn AssistantPanelDelegate { + /// Returns the global [`AssistantPanelDelegate`], if it exists. + pub fn try_global(cx: &App) -> Option> { + cx.try_global::() + .map(|global| global.0.clone()) + } + + /// Sets the global [`AssistantPanelDelegate`]. + pub fn set_global(delegate: Arc, cx: &mut App) { + cx.set_global(GlobalAssistantPanelDelegate(delegate)); + } +} + +struct GlobalAssistantPanelDelegate(Arc); + +impl Global for GlobalAssistantPanelDelegate {} + +pub struct ContextEditor { + context: Entity, + fs: Arc, + slash_commands: Arc, + tools: Arc, + workspace: WeakEntity, + project: Entity, + lsp_adapter_delegate: Option>, + editor: Entity, + blocks: HashMap, + image_blocks: HashSet, + scroll_position: Option, + remote_id: Option, + pending_slash_command_creases: HashMap, CreaseId>, + invoked_slash_command_creases: HashMap, + pending_tool_use_creases: HashMap, CreaseId>, + _subscriptions: Vec, + patches: HashMap, PatchViewState>, + active_patch: Option>, + last_error: Option, + show_accept_terms: bool, + pub(crate) slash_menu_handle: + PopoverMenuHandle>, + // dragged_file_worktrees is used to keep references to worktrees that were added + // when the user drag/dropped an external file onto the context editor. Since + // the worktree is not part of the project panel, it would be dropped as soon as + // the file is opened. In order to keep the worktree alive for the duration of the + // context editor, we keep a reference here. + dragged_file_worktrees: Vec>, +} + +pub const DEFAULT_TAB_TITLE: &str = "New Chat"; +const MAX_TAB_TITLE_LEN: usize = 16; + +impl ContextEditor { + pub fn for_context( + context: Entity, + fs: Arc, + workspace: WeakEntity, + project: Entity, + lsp_adapter_delegate: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let completion_provider = SlashCommandCompletionProvider::new( + context.read(cx).slash_commands().clone(), + Some(cx.entity().downgrade()), + Some(workspace.clone()), + ); + + let editor = cx.new(|cx| { + let mut editor = + Editor::for_buffer(context.read(cx).buffer().clone(), None, window, cx); + editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor.set_show_line_numbers(false, cx); + editor.set_show_scrollbars(false, cx); + editor.set_show_git_diff_gutter(false, cx); + editor.set_show_code_actions(false, cx); + editor.set_show_runnables(false, cx); + editor.set_show_wrap_guides(false, cx); + editor.set_show_indent_guides(false, cx); + editor.set_completion_provider(Some(Box::new(completion_provider))); + editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never); + editor.set_collaboration_hub(Box::new(project.clone())); + editor + }); + + let _subscriptions = vec![ + cx.observe(&context, |_, _, cx| cx.notify()), + cx.subscribe_in(&context, window, Self::handle_context_event), + cx.subscribe_in(&editor, window, Self::handle_editor_event), + cx.subscribe_in(&editor, window, Self::handle_editor_search_event), + ]; + + let sections = context.read(cx).slash_command_output_sections().to_vec(); + let patch_ranges = context.read(cx).patch_ranges().collect::>(); + let slash_commands = context.read(cx).slash_commands().clone(); + let tools = context.read(cx).tools().clone(); + let mut this = Self { + context, + slash_commands, + tools, + editor, + lsp_adapter_delegate, + blocks: Default::default(), + image_blocks: Default::default(), + scroll_position: None, + remote_id: None, + fs, + workspace, + project, + pending_slash_command_creases: HashMap::default(), + invoked_slash_command_creases: HashMap::default(), + pending_tool_use_creases: HashMap::default(), + _subscriptions, + patches: HashMap::default(), + active_patch: None, + last_error: None, + show_accept_terms: false, + slash_menu_handle: Default::default(), + dragged_file_worktrees: Vec::new(), + }; + this.update_message_headers(cx); + this.update_image_blocks(cx); + this.insert_slash_command_output_sections(sections, false, window, cx); + this.patches_updated(&Vec::new(), &patch_ranges, window, cx); + this + } + + pub fn context(&self) -> &Entity { + &self.context + } + + pub fn editor(&self) -> &Entity { + &self.editor + } + + pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context) { + let command_name = DefaultSlashCommand.name(); + self.editor.update(cx, |editor, cx| { + editor.insert(&format!("/{command_name}\n\n"), window, cx) + }); + let command = self.context.update(cx, |context, cx| { + context.reparse(cx); + context.parsed_slash_commands()[0].clone() + }); + self.run_command( + command.source_range, + &command.name, + &command.arguments, + false, + self.workspace.clone(), + window, + cx, + ); + } + + fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context) { + self.send_to_model(RequestType::Chat, window, cx); + } + + fn edit(&mut self, _: &Edit, window: &mut Window, cx: &mut Context) { + self.send_to_model(RequestType::SuggestEdits, window, cx); + } + + fn focus_active_patch(&mut self, window: &mut Window, cx: &mut Context) -> bool { + if let Some((_range, patch)) = self.active_patch() { + if let Some(editor) = patch + .editor + .as_ref() + .and_then(|state| state.editor.upgrade()) + { + editor.focus_handle(cx).focus(window); + return true; + } + } + + false + } + + fn send_to_model( + &mut self, + request_type: RequestType, + window: &mut Window, + cx: &mut Context, + ) { + let provider = LanguageModelRegistry::read_global(cx).active_provider(); + if provider + .as_ref() + .map_or(false, |provider| provider.must_accept_terms(cx)) + { + self.show_accept_terms = true; + cx.notify(); + return; + } + + if self.focus_active_patch(window, cx) { + return; + } + + self.last_error = None; + + if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) { + self.last_error = Some(AssistError::FileRequired); + cx.notify(); + } else if let Some(user_message) = self + .context + .update(cx, |context, cx| context.assist(request_type, cx)) + { + let new_selection = { + let cursor = user_message + .start + .to_offset(self.context.read(cx).buffer().read(cx)); + cursor..cursor + }; + self.editor.update(cx, |editor, cx| { + editor.change_selections( + Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), + window, + cx, + |selections| selections.select_ranges([new_selection]), + ); + }); + // Avoid scrolling to the new cursor position so the assistant's output is stable. + cx.defer_in(window, |this, _, _| this.scroll_position = None); + } + + cx.notify(); + } + + fn cancel( + &mut self, + _: &editor::actions::Cancel, + _window: &mut Window, + cx: &mut Context, + ) { + self.last_error = None; + + if self + .context + .update(cx, |context, cx| context.cancel_last_assist(cx)) + { + return; + } + + cx.propagate(); + } + + fn cycle_message_role( + &mut self, + _: &CycleMessageRole, + _window: &mut Window, + cx: &mut Context, + ) { + let cursors = self.cursors(cx); + self.context.update(cx, |context, cx| { + let messages = context + .messages_for_offsets(cursors, cx) + .into_iter() + .map(|message| message.id) + .collect(); + context.cycle_message_roles(messages, cx) + }); + } + + fn cursors(&self, cx: &mut App) -> Vec { + let selections = self + .editor + .update(cx, |editor, cx| editor.selections.all::(cx)); + selections + .into_iter() + .map(|selection| selection.head()) + .collect() + } + + pub fn insert_command(&mut self, name: &str, window: &mut Window, cx: &mut Context) { + if let Some(command) = self.slash_commands.command(name, cx) { + self.editor.update(cx, |editor, cx| { + editor.transact(window, cx, |editor, window, cx| { + editor + .change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel()); + let snapshot = editor.buffer().read(cx).snapshot(cx); + let newest_cursor = editor.selections.newest::(cx).head(); + if newest_cursor.column > 0 + || snapshot + .chars_at(newest_cursor) + .next() + .map_or(false, |ch| ch != '\n') + { + editor.move_to_end_of_line( + &MoveToEndOfLine { + stop_at_soft_wraps: false, + }, + window, + cx, + ); + editor.newline(&Newline, window, cx); + } + + editor.insert(&format!("/{name}"), window, cx); + if command.accepts_arguments() { + editor.insert(" ", window, cx); + editor.show_completions(&ShowCompletions::default(), window, cx); + } + }); + }); + if !command.requires_argument() { + self.confirm_command(&ConfirmCommand, window, cx); + } + } + } + + pub fn confirm_command( + &mut self, + _: &ConfirmCommand, + window: &mut Window, + cx: &mut Context, + ) { + if self.editor.read(cx).has_active_completions_menu() { + return; + } + + let selections = self.editor.read(cx).selections.disjoint_anchors(); + let mut commands_by_range = HashMap::default(); + let workspace = self.workspace.clone(); + self.context.update(cx, |context, cx| { + context.reparse(cx); + for selection in selections.iter() { + if let Some(command) = + context.pending_command_for_position(selection.head().text_anchor, cx) + { + commands_by_range + .entry(command.source_range.clone()) + .or_insert_with(|| command.clone()); + } + } + }); + + if commands_by_range.is_empty() { + cx.propagate(); + } else { + for command in commands_by_range.into_values() { + self.run_command( + command.source_range, + &command.name, + &command.arguments, + true, + workspace.clone(), + window, + cx, + ); + } + cx.stop_propagation(); + } + } + + #[allow(clippy::too_many_arguments)] + pub fn run_command( + &mut self, + command_range: Range, + name: &str, + arguments: &[String], + ensure_trailing_newline: bool, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(command) = self.slash_commands.command(name, cx) { + let context = self.context.read(cx); + let sections = context + .slash_command_output_sections() + .into_iter() + .filter(|section| section.is_valid(context.buffer().read(cx))) + .cloned() + .collect::>(); + let snapshot = context.buffer().read(cx).snapshot(); + let output = command.run( + arguments, + §ions, + snapshot, + workspace, + self.lsp_adapter_delegate.clone(), + window, + cx, + ); + self.context.update(cx, |context, cx| { + context.insert_command_output( + command_range, + name, + output, + ensure_trailing_newline, + cx, + ) + }); + } + } + + fn handle_context_event( + &mut self, + _: &Entity, + event: &ContextEvent, + window: &mut Window, + cx: &mut Context, + ) { + let context_editor = cx.entity().downgrade(); + + match event { + ContextEvent::MessagesEdited => { + self.update_message_headers(cx); + self.update_image_blocks(cx); + self.context.update(cx, |context, cx| { + context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); + }); + } + ContextEvent::SummaryChanged => { + cx.emit(EditorEvent::TitleChanged); + self.context.update(cx, |context, cx| { + context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); + }); + } + ContextEvent::StreamedCompletion => { + self.editor.update(cx, |editor, cx| { + if let Some(scroll_position) = self.scroll_position { + let snapshot = editor.snapshot(window, cx); + let cursor_point = scroll_position.cursor.to_display_point(&snapshot); + let scroll_top = + cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y; + editor.set_scroll_position( + point(scroll_position.offset_before_cursor.x, scroll_top), + window, + cx, + ); + } + + let new_tool_uses = self + .context + .read(cx) + .pending_tool_uses() + .into_iter() + .filter(|tool_use| { + !self + .pending_tool_use_creases + .contains_key(&tool_use.source_range) + }) + .cloned() + .collect::>(); + + let buffer = editor.buffer().read(cx).snapshot(cx); + let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap(); + let excerpt_id = *excerpt_id; + + let mut buffer_rows_to_fold = BTreeSet::new(); + + let creases = new_tool_uses + .iter() + .map(|tool_use| { + let placeholder = FoldPlaceholder { + render: render_fold_icon_button( + cx.entity().downgrade(), + IconName::PocketKnife, + tool_use.name.clone().into(), + ), + ..Default::default() + }; + let render_trailer = + move |_row, _unfold, _window: &mut Window, _cx: &mut App| { + Empty.into_any() + }; + + let start = buffer + .anchor_in_excerpt(excerpt_id, tool_use.source_range.start) + .unwrap(); + let end = buffer + .anchor_in_excerpt(excerpt_id, tool_use.source_range.end) + .unwrap(); + + let buffer_row = MultiBufferRow(start.to_point(&buffer).row); + buffer_rows_to_fold.insert(buffer_row); + + self.context.update(cx, |context, cx| { + context.insert_content( + Content::ToolUse { + range: tool_use.source_range.clone(), + tool_use: LanguageModelToolUse { + id: tool_use.id.clone(), + name: tool_use.name.clone(), + input: tool_use.input.clone(), + }, + }, + cx, + ); + }); + + Crease::inline( + start..end, + placeholder, + fold_toggle("tool-use"), + render_trailer, + ) + }) + .collect::>(); + + let crease_ids = editor.insert_creases(creases, cx); + + for buffer_row in buffer_rows_to_fold.into_iter().rev() { + editor.fold_at(&FoldAt { buffer_row }, window, cx); + } + + self.pending_tool_use_creases.extend( + new_tool_uses + .iter() + .map(|tool_use| tool_use.source_range.clone()) + .zip(crease_ids), + ); + }); + } + ContextEvent::PatchesUpdated { removed, updated } => { + self.patches_updated(removed, updated, window, cx); + } + ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => { + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).snapshot(cx); + let (&excerpt_id, _, _) = buffer.as_singleton().unwrap(); + + editor.remove_creases( + removed + .iter() + .filter_map(|range| self.pending_slash_command_creases.remove(range)), + cx, + ); + + let crease_ids = editor.insert_creases( + updated.iter().map(|command| { + let workspace = self.workspace.clone(); + let confirm_command = Arc::new({ + let context_editor = context_editor.clone(); + let command = command.clone(); + move |window: &mut Window, cx: &mut App| { + context_editor + .update(cx, |context_editor, cx| { + context_editor.run_command( + command.source_range.clone(), + &command.name, + &command.arguments, + false, + workspace.clone(), + window, + cx, + ); + }) + .ok(); + } + }); + let placeholder = FoldPlaceholder { + render: Arc::new(move |_, _, _, _| Empty.into_any()), + ..Default::default() + }; + let render_toggle = { + let confirm_command = confirm_command.clone(); + let command = command.clone(); + move |row, _, _, _window: &mut Window, _cx: &mut App| { + render_pending_slash_command_gutter_decoration( + row, + &command.status, + confirm_command.clone(), + ) + } + }; + let render_trailer = { + let command = command.clone(); + move |row, _unfold, _window: &mut Window, cx: &mut App| { + // TODO: In the future we should investigate how we can expose + // this as a hook on the `SlashCommand` trait so that we don't + // need to special-case it here. + if command.name == DocsSlashCommand::NAME { + return render_docs_slash_command_trailer( + row, + command.clone(), + cx, + ); + } + + Empty.into_any() + } + }; + + let start = buffer + .anchor_in_excerpt(excerpt_id, command.source_range.start) + .unwrap(); + let end = buffer + .anchor_in_excerpt(excerpt_id, command.source_range.end) + .unwrap(); + Crease::inline(start..end, placeholder, render_toggle, render_trailer) + }), + cx, + ); + + self.pending_slash_command_creases.extend( + updated + .iter() + .map(|command| command.source_range.clone()) + .zip(crease_ids), + ); + }) + } + ContextEvent::InvokedSlashCommandChanged { command_id } => { + self.update_invoked_slash_command(*command_id, window, cx); + } + ContextEvent::SlashCommandOutputSectionAdded { section } => { + self.insert_slash_command_output_sections([section.clone()], false, window, cx); + } + ContextEvent::UsePendingTools => { + let pending_tool_uses = self + .context + .read(cx) + .pending_tool_uses() + .into_iter() + .filter(|tool_use| tool_use.status.is_idle()) + .cloned() + .collect::>(); + + for tool_use in pending_tool_uses { + if let Some(tool) = self.tools.tool(&tool_use.name, cx) { + let task = tool.run(tool_use.input, self.workspace.clone(), window, cx); + + self.context.update(cx, |context, cx| { + context.insert_tool_output(tool_use.id.clone(), task, cx); + }); + } + } + } + ContextEvent::ToolFinished { + tool_use_id, + output_range, + } => { + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).snapshot(cx); + let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap(); + let excerpt_id = *excerpt_id; + + let placeholder = FoldPlaceholder { + render: render_fold_icon_button( + cx.entity().downgrade(), + IconName::PocketKnife, + format!("Tool Result: {tool_use_id}").into(), + ), + ..Default::default() + }; + let render_trailer = + move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any(); + + let start = buffer + .anchor_in_excerpt(excerpt_id, output_range.start) + .unwrap(); + let end = buffer + .anchor_in_excerpt(excerpt_id, output_range.end) + .unwrap(); + + let buffer_row = MultiBufferRow(start.to_point(&buffer).row); + + let crease = Crease::inline( + start..end, + placeholder, + fold_toggle("tool-use"), + render_trailer, + ); + + editor.insert_creases([crease], cx); + editor.fold_at(&FoldAt { buffer_row }, window, cx); + }); + } + ContextEvent::Operation(_) => {} + ContextEvent::ShowAssistError(error_message) => { + self.last_error = Some(AssistError::Message(error_message.clone())); + } + ContextEvent::ShowPaymentRequiredError => { + self.last_error = Some(AssistError::PaymentRequired); + } + ContextEvent::ShowMaxMonthlySpendReachedError => { + self.last_error = Some(AssistError::MaxMonthlySpendReached); + } + } + } + + fn update_invoked_slash_command( + &mut self, + command_id: InvokedSlashCommandId, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(invoked_slash_command) = + self.context.read(cx).invoked_slash_command(&command_id) + { + if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { + let run_commands_in_ranges = invoked_slash_command + .run_commands_in_ranges + .iter() + .cloned() + .collect::>(); + for range in run_commands_in_ranges { + let commands = self.context.update(cx, |context, cx| { + context.reparse(cx); + context + .pending_commands_for_range(range.clone(), cx) + .to_vec() + }); + + for command in commands { + self.run_command( + command.source_range, + &command.name, + &command.arguments, + false, + self.workspace.clone(), + window, + cx, + ); + } + } + } + } + + self.editor.update(cx, |editor, cx| { + if let Some(invoked_slash_command) = + self.context.read(cx).invoked_slash_command(&command_id) + { + if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { + let buffer = editor.buffer().read(cx).snapshot(cx); + let (&excerpt_id, _buffer_id, _buffer_snapshot) = + buffer.as_singleton().unwrap(); + + let start = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start) + .unwrap(); + let end = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end) + .unwrap(); + editor.remove_folds_with_type( + &[start..end], + TypeId::of::(), + false, + cx, + ); + + editor.remove_creases( + HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), + cx, + ); + } else if let hash_map::Entry::Vacant(entry) = + self.invoked_slash_command_creases.entry(command_id) + { + let buffer = editor.buffer().read(cx).snapshot(cx); + let (&excerpt_id, _buffer_id, _buffer_snapshot) = + buffer.as_singleton().unwrap(); + let context = self.context.downgrade(); + let crease_start = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start) + .unwrap(); + let crease_end = buffer + .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end) + .unwrap(); + let crease = Crease::inline( + crease_start..crease_end, + invoked_slash_command_fold_placeholder(command_id, context), + fold_toggle("invoked-slash-command"), + |_row, _folded, _window, _cx| Empty.into_any(), + ); + let crease_ids = editor.insert_creases([crease.clone()], cx); + editor.fold_creases(vec![crease], false, window, cx); + entry.insert(crease_ids[0]); + } else { + cx.notify() + } + } else { + editor.remove_creases( + HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)), + cx, + ); + cx.notify(); + }; + }); + } + + fn patches_updated( + &mut self, + removed: &Vec>, + updated: &Vec>, + window: &mut Window, + cx: &mut Context, + ) { + let this = cx.entity().downgrade(); + let mut editors_to_close = Vec::new(); + + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let multibuffer = &snapshot.buffer_snapshot; + let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap(); + + let mut removed_crease_ids = Vec::new(); + let mut ranges_to_unfold: Vec> = Vec::new(); + for range in removed { + if let Some(state) = self.patches.remove(range) { + let patch_start = multibuffer + .anchor_in_excerpt(excerpt_id, range.start) + .unwrap(); + let patch_end = multibuffer + .anchor_in_excerpt(excerpt_id, range.end) + .unwrap(); + + editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade())); + ranges_to_unfold.push(patch_start..patch_end); + removed_crease_ids.push(state.crease_id); + } + } + editor.unfold_ranges(&ranges_to_unfold, true, false, cx); + editor.remove_creases(removed_crease_ids, cx); + + for range in updated { + let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else { + continue; + }; + + let path_count = patch.path_count(); + let patch_start = multibuffer + .anchor_in_excerpt(excerpt_id, patch.range.start) + .unwrap(); + let patch_end = multibuffer + .anchor_in_excerpt(excerpt_id, patch.range.end) + .unwrap(); + let render_block: RenderBlock = Arc::new({ + let this = this.clone(); + let patch_range = range.clone(); + move |cx: &mut BlockContext<'_, '_>| { + let max_width = cx.max_width; + let gutter_width = cx.gutter_dimensions.full_width(); + let block_id = cx.block_id; + let selected = cx.selected; + this.update_in(cx, |this, window, cx| { + this.render_patch_block( + patch_range.clone(), + max_width, + gutter_width, + block_id, + selected, + window, + cx, + ) + }) + .ok() + .flatten() + .unwrap_or_else(|| Empty.into_any()) + } + }); + + let height = path_count as u32 + 1; + let crease = Crease::block( + patch_start..patch_end, + height, + BlockStyle::Flex, + render_block.clone(), + ); + + let should_refold; + if let Some(state) = self.patches.get_mut(&range) { + if let Some(editor_state) = &state.editor { + if editor_state.opened_patch != patch { + state.update_task = Some({ + let this = this.clone(); + cx.spawn_in(window, |_, cx| async move { + Self::update_patch_editor(this.clone(), patch, cx) + .await + .log_err(); + }) + }); + } + } + + should_refold = + snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot)); + } else { + let crease_id = editor.insert_creases([crease.clone()], cx)[0]; + self.patches.insert( + range.clone(), + PatchViewState { + crease_id, + editor: None, + update_task: None, + }, + ); + + should_refold = true; + } + + if should_refold { + editor.unfold_ranges(&[patch_start..patch_end], true, false, cx); + editor.fold_creases(vec![crease], false, window, cx); + } + } + }); + + for editor in editors_to_close { + self.close_patch_editor(editor, window, cx); + } + + self.update_active_patch(window, cx); + } + + fn insert_slash_command_output_sections( + &mut self, + sections: impl IntoIterator>, + expand_result: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).snapshot(cx); + let excerpt_id = *buffer.as_singleton().unwrap().0; + let mut buffer_rows_to_fold = BTreeSet::new(); + let mut creases = Vec::new(); + for section in sections { + let start = buffer + .anchor_in_excerpt(excerpt_id, section.range.start) + .unwrap(); + let end = buffer + .anchor_in_excerpt(excerpt_id, section.range.end) + .unwrap(); + let buffer_row = MultiBufferRow(start.to_point(&buffer).row); + buffer_rows_to_fold.insert(buffer_row); + creases.push( + Crease::inline( + start..end, + FoldPlaceholder { + render: render_fold_icon_button( + cx.entity().downgrade(), + section.icon, + section.label.clone(), + ), + merge_adjacent: false, + ..Default::default() + }, + render_slash_command_output_toggle, + |_, _, _, _| Empty.into_any_element(), + ) + .with_metadata(CreaseMetadata { + icon: section.icon, + label: section.label, + }), + ); + } + + editor.insert_creases(creases, cx); + + if expand_result { + buffer_rows_to_fold.clear(); + } + for buffer_row in buffer_rows_to_fold.into_iter().rev() { + editor.fold_at(&FoldAt { buffer_row }, window, cx); + } + }); + } + + fn handle_editor_event( + &mut self, + _: &Entity, + event: &EditorEvent, + window: &mut Window, + cx: &mut Context, + ) { + match event { + EditorEvent::ScrollPositionChanged { autoscroll, .. } => { + let cursor_scroll_position = self.cursor_scroll_position(window, cx); + if *autoscroll { + self.scroll_position = cursor_scroll_position; + } else if self.scroll_position != cursor_scroll_position { + self.scroll_position = None; + } + } + EditorEvent::SelectionsChanged { .. } => { + self.scroll_position = self.cursor_scroll_position(window, cx); + self.update_active_patch(window, cx); + } + _ => {} + } + cx.emit(event.clone()); + } + + fn active_patch(&self) -> Option<(Range, &PatchViewState)> { + let patch = self.active_patch.as_ref()?; + Some((patch.clone(), self.patches.get(&patch)?)) + } + + fn update_active_patch(&mut self, window: &mut Window, cx: &mut Context) { + let newest_cursor = self.editor.update(cx, |editor, cx| { + editor.selections.newest::(cx).head() + }); + let context = self.context.read(cx); + + let new_patch = context.patch_containing(newest_cursor, cx).cloned(); + + if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() { + return; + } + + if let Some(old_patch_range) = self.active_patch.take() { + if let Some(patch_state) = self.patches.get_mut(&old_patch_range) { + if let Some(state) = patch_state.editor.take() { + if let Some(editor) = state.editor.upgrade() { + self.close_patch_editor(editor, window, cx); + } + } + } + } + + if let Some(new_patch) = new_patch { + self.active_patch = Some(new_patch.range.clone()); + + if let Some(patch_state) = self.patches.get_mut(&new_patch.range) { + let mut editor = None; + if let Some(state) = &patch_state.editor { + if let Some(opened_editor) = state.editor.upgrade() { + editor = Some(opened_editor); + } + } + + if let Some(editor) = editor { + self.workspace + .update(cx, |workspace, cx| { + workspace.activate_item(&editor, true, false, window, cx); + }) + .ok(); + } else { + patch_state.update_task = + Some(cx.spawn_in(window, move |this, cx| async move { + Self::open_patch_editor(this, new_patch, cx).await.log_err(); + })); + } + } + } + } + + fn close_patch_editor( + &mut self, + editor: Entity, + window: &mut Window, + cx: &mut Context, + ) { + self.workspace + .update(cx, |workspace, cx| { + if let Some(pane) = workspace.pane_for(&editor) { + pane.update(cx, |pane, cx| { + let item_id = editor.entity_id(); + if !editor.read(cx).focus_handle(cx).is_focused(window) { + pane.close_item_by_id(item_id, SaveIntent::Skip, window, cx) + .detach_and_log_err(cx); + } + }); + } + }) + .ok(); + } + + async fn open_patch_editor( + this: WeakEntity, + patch: AssistantPatch, + mut cx: AsyncWindowContext, + ) -> Result<()> { + let project = this.update(&mut cx, |this, _| this.project.clone())?; + let resolved_patch = patch.resolve(project.clone(), &mut cx).await; + + let editor = cx.new_window_entity(|window, cx| { + let editor = ProposedChangesEditor::new( + patch.title.clone(), + resolved_patch + .edit_groups + .iter() + .map(|(buffer, groups)| ProposedChangeLocation { + buffer: buffer.clone(), + ranges: groups + .iter() + .map(|group| group.context_range.clone()) + .collect(), + }) + .collect(), + Some(project.clone()), + window, + cx, + ); + resolved_patch.apply(&editor, cx); + editor + })?; + + this.update_in(&mut cx, |this, window, cx| { + if let Some(patch_state) = this.patches.get_mut(&patch.range) { + patch_state.editor = Some(PatchEditorState { + editor: editor.downgrade(), + opened_patch: patch, + }); + patch_state.update_task.take(); + } + + this.workspace + .update(cx, |workspace, cx| { + workspace.add_item_to_active_pane( + Box::new(editor.clone()), + None, + false, + window, + cx, + ) + }) + .log_err(); + })?; + + Ok(()) + } + + async fn update_patch_editor( + this: WeakEntity, + patch: AssistantPatch, + mut cx: AsyncWindowContext, + ) -> Result<()> { + let project = this.update(&mut cx, |this, _| this.project.clone())?; + let resolved_patch = patch.resolve(project.clone(), &mut cx).await; + this.update_in(&mut cx, |this, window, cx| { + let patch_state = this.patches.get_mut(&patch.range)?; + + let locations = resolved_patch + .edit_groups + .iter() + .map(|(buffer, groups)| ProposedChangeLocation { + buffer: buffer.clone(), + ranges: groups + .iter() + .map(|group| group.context_range.clone()) + .collect(), + }) + .collect(); + + if let Some(state) = &mut patch_state.editor { + if let Some(editor) = state.editor.upgrade() { + editor.update(cx, |editor, cx| { + editor.set_title(patch.title.clone(), cx); + editor.reset_locations(locations, window, cx); + resolved_patch.apply(editor, cx); + }); + + state.opened_patch = patch; + } else { + patch_state.editor.take(); + } + } + patch_state.update_task.take(); + + Some(()) + })?; + Ok(()) + } + + fn handle_editor_search_event( + &mut self, + _: &Entity, + event: &SearchEvent, + _window: &mut Window, + cx: &mut Context, + ) { + cx.emit(event.clone()); + } + + fn cursor_scroll_position( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Option { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let cursor = editor.selections.newest_anchor().head(); + let cursor_row = cursor + .to_display_point(&snapshot.display_snapshot) + .row() + .as_f32(); + let scroll_position = editor + .scroll_manager + .anchor() + .scroll_position(&snapshot.display_snapshot); + + let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.); + if (scroll_position.y..scroll_bottom).contains(&cursor_row) { + Some(ScrollPosition { + cursor, + offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), + }) + } else { + None + } + }) + } + + fn esc_kbd(cx: &App) -> Div { + let colors = cx.theme().colors().clone(); + + h_flex() + .items_center() + .gap_1() + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .text_size(TextSize::XSmall.rems(cx)) + .text_color(colors.text_muted) + .child("Press") + .child( + h_flex() + .rounded_md() + .px_1() + .mr_0p5() + .border_1() + .border_color(theme::color_alpha(colors.border_variant, 0.6)) + .bg(theme::color_alpha(colors.element_background, 0.6)) + .child("esc"), + ) + .child("to cancel") + } + + fn update_message_headers(&mut self, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).snapshot(cx); + + let excerpt_id = *buffer.as_singleton().unwrap().0; + let mut old_blocks = std::mem::take(&mut self.blocks); + let mut blocks_to_remove: HashMap<_, _> = old_blocks + .iter() + .map(|(message_id, (_, block_id))| (*message_id, *block_id)) + .collect(); + let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default(); + + let render_block = |message: MessageMetadata| -> RenderBlock { + Arc::new({ + let context = self.context.clone(); + + move |cx| { + let message_id = MessageId(message.timestamp); + let llm_loading = message.role == Role::Assistant + && message.status == MessageStatus::Pending; + + let (label, spinner, note) = match message.role { + Role::User => ( + Label::new("You").color(Color::Default).into_any_element(), + None, + None, + ), + Role::Assistant => { + let base_label = Label::new("Assistant").color(Color::Info); + let mut spinner = None; + let mut note = None; + let animated_label = if llm_loading { + base_label + .with_animation( + "pulsating-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.alpha(delta), + ) + .into_any_element() + } else { + base_label.into_any_element() + }; + if llm_loading { + spinner = Some( + Icon::new(IconName::ArrowCircle) + .size(IconSize::XSmall) + .color(Color::Info) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate( + percentage(delta), + )) + }, + ) + .into_any_element(), + ); + note = Some(Self::esc_kbd(cx).into_any_element()); + } + (animated_label, spinner, note) + } + Role::System => ( + Label::new("System") + .color(Color::Warning) + .into_any_element(), + None, + None, + ), + }; + + let sender = h_flex() + .items_center() + .gap_2p5() + .child( + ButtonLike::new("role") + .style(ButtonStyle::Filled) + .child( + h_flex() + .items_center() + .gap_1p5() + .child(label) + .children(spinner), + ) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Toggle message role", + None, + "Available roles: You (User), Assistant, System", + window, + cx, + ) + }) + .on_click({ + let context = context.clone(); + move |_, _window, cx| { + context.update(cx, |context, cx| { + context.cycle_message_roles( + HashSet::from_iter(Some(message_id)), + cx, + ) + }) + } + }), + ) + .children(note); + + h_flex() + .id(("message_header", message_id.as_u64())) + .pl(cx.gutter_dimensions.full_width()) + .h_11() + .w_full() + .relative() + .gap_1p5() + .child(sender) + .children(match &message.cache { + Some(cache) if cache.is_final_anchor => match cache.status { + CacheStatus::Cached => Some( + div() + .id("cached") + .child( + Icon::new(IconName::DatabaseZap) + .size(IconSize::XSmall) + .color(Color::Hint), + ) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Context Cached", + None, + "Large messages cached to optimize performance", + window, + cx, + ) + }) + .into_any_element(), + ), + CacheStatus::Pending => Some( + div() + .child( + Icon::new(IconName::Ellipsis) + .size(IconSize::XSmall) + .color(Color::Hint), + ) + .into_any_element(), + ), + }, + _ => None, + }) + .children(match &message.status { + MessageStatus::Error(error) => Some( + Button::new("show-error", "Error") + .color(Color::Error) + .selected_label_color(Color::Error) + .selected_icon_color(Color::Error) + .icon(IconName::XCircle) + .icon_color(Color::Error) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::Start) + .tooltip(Tooltip::text("View Details")) + .on_click({ + let context = context.clone(); + let error = error.clone(); + move |_, _window, cx| { + context.update(cx, |_, cx| { + cx.emit(ContextEvent::ShowAssistError( + error.clone(), + )); + }); + } + }) + .into_any_element(), + ), + MessageStatus::Canceled => Some( + h_flex() + .gap_1() + .items_center() + .child( + Icon::new(IconName::XCircle) + .color(Color::Disabled) + .size(IconSize::XSmall), + ) + .child( + Label::new("Canceled") + .size(LabelSize::Small) + .color(Color::Disabled), + ) + .into_any_element(), + ), + _ => None, + }) + .into_any_element() + } + }) + }; + let create_block_properties = |message: &Message| BlockProperties { + height: 2, + style: BlockStyle::Sticky, + placement: BlockPlacement::Above( + buffer + .anchor_in_excerpt(excerpt_id, message.anchor_range.start) + .unwrap(), + ), + priority: usize::MAX, + render: render_block(MessageMetadata::from(message)), + }; + let mut new_blocks = vec![]; + let mut block_index_to_message = vec![]; + for message in self.context.read(cx).messages(cx) { + if let Some(_) = blocks_to_remove.remove(&message.id) { + // This is an old message that we might modify. + let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else { + debug_assert!( + false, + "old_blocks should contain a message_id we've just removed." + ); + continue; + }; + // Should we modify it? + let message_meta = MessageMetadata::from(&message); + if meta != &message_meta { + blocks_to_replace.insert(*block_id, render_block(message_meta.clone())); + *meta = message_meta; + } + } else { + // This is a new message. + new_blocks.push(create_block_properties(&message)); + block_index_to_message.push((message.id, MessageMetadata::from(&message))); + } + } + editor.replace_blocks(blocks_to_replace, None, cx); + editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx); + + let ids = editor.insert_blocks(new_blocks, None, cx); + old_blocks.extend(ids.into_iter().zip(block_index_to_message).map( + |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)), + )); + self.blocks = old_blocks; + }); + } + + /// Returns either the selected text, or the content of the Markdown code + /// block surrounding the cursor. + fn get_selection_or_code_block( + context_editor_view: &Entity, + cx: &mut Context, + ) -> Option<(String, bool)> { + const CODE_FENCE_DELIMITER: &'static str = "```"; + + let context_editor = context_editor_view.read(cx).editor.clone(); + context_editor.update(cx, |context_editor, cx| { + if context_editor.selections.newest::(cx).is_empty() { + let snapshot = context_editor.buffer().read(cx).snapshot(cx); + let (_, _, snapshot) = snapshot.as_singleton()?; + + let head = context_editor.selections.newest::(cx).head(); + let offset = snapshot.point_to_offset(head); + + let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?; + let mut text = snapshot + .text_for_range(surrounding_code_block_range) + .collect::(); + + // If there is no newline trailing the closing three-backticks, then + // tree-sitter-md extends the range of the content node to include + // the backticks. + if text.ends_with(CODE_FENCE_DELIMITER) { + text.drain((text.len() - CODE_FENCE_DELIMITER.len())..); + } + + (!text.is_empty()).then_some((text, true)) + } else { + let anchor = context_editor.selections.newest_anchor(); + let text = context_editor + .buffer() + .read(cx) + .read(cx) + .text_for_range(anchor.range()) + .collect::(); + + (!text.is_empty()).then_some((text, false)) + } + }) + } + + pub fn insert_selection( + workspace: &mut Workspace, + _: &InsertIntoEditor, + window: &mut Window, + cx: &mut Context, + ) { + let Some(assistant_panel_delegate) = ::try_global(cx) else { + return; + }; + let Some(context_editor_view) = + assistant_panel_delegate.active_context_editor(workspace, window, cx) + else { + return; + }; + let Some(active_editor_view) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + else { + return; + }; + + if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) { + active_editor_view.update(cx, |editor, cx| { + editor.insert(&text, window, cx); + editor.focus_handle(cx).focus(window); + }) + } + } + + pub fn copy_code( + workspace: &mut Workspace, + _: &CopyCode, + window: &mut Window, + cx: &mut Context, + ) { + let result = maybe!({ + let assistant_panel_delegate = ::try_global(cx)?; + let context_editor_view = + assistant_panel_delegate.active_context_editor(workspace, window, cx)?; + Self::get_selection_or_code_block(&context_editor_view, cx) + }); + let Some((text, is_code_block)) = result else { + return; + }; + + cx.write_to_clipboard(ClipboardItem::new_string(text)); + + struct CopyToClipboardToast; + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + format!( + "{} copied to clipboard.", + if is_code_block { + "Code block" + } else { + "Selection" + } + ), + ) + .autohide(), + cx, + ); + } + + pub fn insert_dragged_files( + workspace: &mut Workspace, + action: &InsertDraggedFiles, + window: &mut Window, + cx: &mut Context, + ) { + let Some(assistant_panel_delegate) = ::try_global(cx) else { + return; + }; + let Some(context_editor_view) = + assistant_panel_delegate.active_context_editor(workspace, window, cx) + else { + return; + }; + + let project = workspace.project().clone(); + + let paths = match action { + InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])), + InsertDraggedFiles::ExternalFiles(paths) => { + let tasks = paths + .clone() + .into_iter() + .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx)) + .collect::>(); + + cx.spawn(move |_, cx| async move { + let mut paths = vec![]; + let mut worktrees = vec![]; + + let opened_paths = futures::future::join_all(tasks).await; + for (worktree, project_path) in opened_paths.into_iter().flatten() { + let Ok(worktree_root_name) = + worktree.read_with(&cx, |worktree, _| worktree.root_name().to_string()) + else { + continue; + }; + + let mut full_path = PathBuf::from(worktree_root_name.clone()); + full_path.push(&project_path.path); + paths.push(full_path); + worktrees.push(worktree); + } + + (paths, worktrees) + }) + } + }; + + window + .spawn(cx, |mut cx| async move { + let (paths, dragged_file_worktrees) = paths.await; + let cmd_name = FileSlashCommand.name(); + + context_editor_view + .update_in(&mut cx, |context_editor, window, cx| { + let file_argument = paths + .into_iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>() + .join(" "); + + context_editor.editor.update(cx, |editor, cx| { + editor.insert("\n", window, cx); + editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx); + }); + + context_editor.confirm_command(&ConfirmCommand, window, cx); + + context_editor + .dragged_file_worktrees + .extend(dragged_file_worktrees); + }) + .log_err(); + }) + .detach(); + } + + pub fn quote_selection( + workspace: &mut Workspace, + _: &QuoteSelection, + window: &mut Window, + cx: &mut Context, + ) { + let Some(assistant_panel_delegate) = ::try_global(cx) else { + return; + }; + + let Some(creases) = selections_creases(workspace, cx) else { + return; + }; + + if creases.is_empty() { + return; + } + + assistant_panel_delegate.quote_selection(workspace, creases, window, cx); + } + + pub fn quote_creases( + &mut self, + creases: Vec<(String, String)>, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.insert("\n", window, cx); + for (text, crease_title) in creases { + let point = editor.selections.newest::(cx).head(); + let start_row = MultiBufferRow(point.row); + + editor.insert(&text, window, cx); + + let snapshot = editor.buffer().read(cx).snapshot(cx); + let anchor_before = snapshot.anchor_after(point); + let anchor_after = editor + .selections + .newest_anchor() + .head() + .bias_left(&snapshot); + + editor.insert("\n", window, cx); + + let fold_placeholder = + quote_selection_fold_placeholder(crease_title, cx.entity().downgrade()); + let crease = Crease::inline( + anchor_before..anchor_after, + fold_placeholder, + render_quote_selection_output_toggle, + |_, _, _, _| Empty.into_any(), + ); + editor.insert_creases(vec![crease], cx); + editor.fold_at( + &FoldAt { + buffer_row: start_row, + }, + window, + cx, + ); + } + }) + } + + fn copy(&mut self, _: &editor::actions::Copy, _window: &mut Window, cx: &mut Context) { + if self.editor.read(cx).selections.count() == 1 { + let (copied_text, metadata, _) = self.get_clipboard_contents(cx); + cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( + copied_text, + metadata, + )); + cx.stop_propagation(); + return; + } + + cx.propagate(); + } + + fn cut(&mut self, _: &editor::actions::Cut, window: &mut Window, cx: &mut Context) { + if self.editor.read(cx).selections.count() == 1 { + let (copied_text, metadata, selections) = self.get_clipboard_contents(cx); + + self.editor.update(cx, |editor, cx| { + editor.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections); + }); + this.insert("", window, cx); + cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( + copied_text, + metadata, + )); + }); + }); + + cx.stop_propagation(); + return; + } + + cx.propagate(); + } + + fn get_clipboard_contents( + &mut self, + cx: &mut Context, + ) -> (String, CopyMetadata, Vec>) { + let (snapshot, selection, creases) = self.editor.update(cx, |editor, cx| { + let mut selection = editor.selections.newest::(cx); + let snapshot = editor.buffer().read(cx).snapshot(cx); + + let is_entire_line = selection.is_empty() || editor.selections.line_mode; + if is_entire_line { + selection.start = Point::new(selection.start.row, 0); + selection.end = + cmp::min(snapshot.max_point(), Point::new(selection.start.row + 1, 0)); + selection.goal = SelectionGoal::None; + } + + let selection_start = snapshot.point_to_offset(selection.start); + + ( + snapshot.clone(), + selection.clone(), + editor.display_map.update(cx, |display_map, cx| { + display_map + .snapshot(cx) + .crease_snapshot + .creases_in_range( + MultiBufferRow(selection.start.row) + ..MultiBufferRow(selection.end.row + 1), + &snapshot, + ) + .filter_map(|crease| { + if let Crease::Inline { + range, metadata, .. + } = &crease + { + let metadata = metadata.as_ref()?; + let start = range + .start + .to_offset(&snapshot) + .saturating_sub(selection_start); + let end = range + .end + .to_offset(&snapshot) + .saturating_sub(selection_start); + + let range_relative_to_selection = start..end; + if !range_relative_to_selection.is_empty() { + return Some(SelectedCreaseMetadata { + range_relative_to_selection, + crease: metadata.clone(), + }); + } + } + None + }) + .collect::>() + }), + ) + }); + + let selection = selection.map(|point| snapshot.point_to_offset(point)); + let context = self.context.read(cx); + + let mut text = String::new(); + for message in context.messages(cx) { + if message.offset_range.start >= selection.range().end { + break; + } else if message.offset_range.end >= selection.range().start { + let range = cmp::max(message.offset_range.start, selection.range().start) + ..cmp::min(message.offset_range.end, selection.range().end); + if !range.is_empty() { + for chunk in context.buffer().read(cx).text_for_range(range) { + text.push_str(chunk); + } + if message.offset_range.end < selection.range().end { + text.push('\n'); + } + } + } + } + + (text, CopyMetadata { creases }, vec![selection]) + } + + fn paste( + &mut self, + action: &editor::actions::Paste, + window: &mut Window, + cx: &mut Context, + ) { + cx.stop_propagation(); + + let images = if let Some(item) = cx.read_from_clipboard() { + item.into_entries() + .filter_map(|entry| { + if let ClipboardEntry::Image(image) = entry { + Some(image) + } else { + None + } + }) + .collect() + } else { + Vec::new() + }; + + let metadata = if let Some(item) = cx.read_from_clipboard() { + item.entries().first().and_then(|entry| { + if let ClipboardEntry::String(text) = entry { + text.metadata_json::() + } else { + None + } + }) + } else { + None + }; + + if images.is_empty() { + self.editor.update(cx, |editor, cx| { + let paste_position = editor.selections.newest::(cx).head(); + editor.paste(action, window, cx); + + if let Some(metadata) = metadata { + let buffer = editor.buffer().read(cx).snapshot(cx); + + let mut buffer_rows_to_fold = BTreeSet::new(); + let weak_editor = cx.entity().downgrade(); + editor.insert_creases( + metadata.creases.into_iter().map(|metadata| { + let start = buffer.anchor_after( + paste_position + metadata.range_relative_to_selection.start, + ); + let end = buffer.anchor_before( + paste_position + metadata.range_relative_to_selection.end, + ); + + let buffer_row = MultiBufferRow(start.to_point(&buffer).row); + buffer_rows_to_fold.insert(buffer_row); + Crease::inline( + start..end, + FoldPlaceholder { + render: render_fold_icon_button( + weak_editor.clone(), + metadata.crease.icon, + metadata.crease.label.clone(), + ), + ..Default::default() + }, + render_slash_command_output_toggle, + |_, _, _, _| Empty.into_any(), + ) + .with_metadata(metadata.crease.clone()) + }), + cx, + ); + for buffer_row in buffer_rows_to_fold.into_iter().rev() { + editor.fold_at(&FoldAt { buffer_row }, window, cx); + } + } + }); + } else { + let mut image_positions = Vec::new(); + self.editor.update(cx, |editor, cx| { + editor.transact(window, cx, |editor, _window, cx| { + let edits = editor + .selections + .all::(cx) + .into_iter() + .map(|selection| (selection.start..selection.end, "\n")); + editor.edit(edits, cx); + + let snapshot = editor.buffer().read(cx).snapshot(cx); + for selection in editor.selections.all::(cx) { + image_positions.push(snapshot.anchor_before(selection.end)); + } + }); + }); + + self.context.update(cx, |context, cx| { + for image in images { + let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err() + else { + continue; + }; + let image_id = image.id(); + let image_task = LanguageModelImage::from_image(image, cx).shared(); + + for image_position in image_positions.iter() { + context.insert_content( + Content::Image { + anchor: image_position.text_anchor, + image_id, + image: image_task.clone(), + render_image: render_image.clone(), + }, + cx, + ); + } + } + }); + } + } + + fn update_image_blocks(&mut self, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).snapshot(cx); + let excerpt_id = *buffer.as_singleton().unwrap().0; + let old_blocks = std::mem::take(&mut self.image_blocks); + let new_blocks = self + .context + .read(cx) + .contents(cx) + .filter_map(|content| { + if let Content::Image { + anchor, + render_image, + .. + } = content + { + Some((anchor, render_image)) + } else { + None + } + }) + .filter_map(|(anchor, render_image)| { + const MAX_HEIGHT_IN_LINES: u32 = 8; + let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap(); + let image = render_image.clone(); + anchor.is_valid(&buffer).then(|| BlockProperties { + placement: BlockPlacement::Above(anchor), + height: MAX_HEIGHT_IN_LINES, + style: BlockStyle::Sticky, + render: Arc::new(move |cx| { + let image_size = size_for_image( + &image, + size( + cx.max_width - cx.gutter_dimensions.full_width(), + MAX_HEIGHT_IN_LINES as f32 * cx.line_height, + ), + ); + h_flex() + .pl(cx.gutter_dimensions.full_width()) + .child( + img(image.clone()) + .object_fit(gpui::ObjectFit::ScaleDown) + .w(image_size.width) + .h(image_size.height), + ) + .into_any_element() + }), + priority: 0, + }) + }) + .collect::>(); + + editor.remove_blocks(old_blocks, None, cx); + let ids = editor.insert_blocks(new_blocks, None, cx); + self.image_blocks = HashSet::from_iter(ids); + }); + } + + fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context) { + self.context.update(cx, |context, cx| { + let selections = self.editor.read(cx).selections.disjoint_anchors(); + for selection in selections.as_ref() { + let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); + let range = selection + .map(|endpoint| endpoint.to_offset(&buffer)) + .range(); + context.split_message(range, cx); + } + }); + } + + fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context) { + self.context.update(cx, |context, cx| { + context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx) + }); + } + + pub fn title(&self, cx: &App) -> Cow { + self.context + .read(cx) + .summary() + .map(|summary| summary.text.clone()) + .map(Cow::Owned) + .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE)) + } + + #[allow(clippy::too_many_arguments)] + fn render_patch_block( + &mut self, + range: Range, + max_width: Pixels, + gutter_width: Pixels, + id: BlockId, + selected: bool, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let snapshot = self + .editor + .update(cx, |editor, cx| editor.snapshot(window, cx)); + let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap(); + let excerpt_id = *excerpt_id; + let anchor = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id, range.start) + .unwrap(); + + let theme = cx.theme().clone(); + let patch = self.context.read(cx).patch_for_range(&range, cx)?; + let paths = patch + .paths() + .map(|p| SharedString::from(p.to_string())) + .collect::>(); + + Some( + v_flex() + .id(id) + .bg(theme.colors().editor_background) + .ml(gutter_width) + .pb_1() + .w(max_width - gutter_width) + .rounded_md() + .border_1() + .border_color(theme.colors().border_variant) + .overflow_hidden() + .hover(|style| style.border_color(theme.colors().text_accent)) + .when(selected, |this| { + this.border_color(theme.colors().text_accent) + }) + .cursor(CursorStyle::PointingHand) + .on_click(cx.listener(move |this, _, window, cx| { + this.editor.update(cx, |editor, cx| { + editor.change_selections(None, window, cx, |selections| { + selections.select_ranges(vec![anchor..anchor]); + }); + }); + this.focus_active_patch(window, cx); + })) + .child( + div() + .px_2() + .py_1() + .overflow_hidden() + .text_ellipsis() + .border_b_1() + .border_color(theme.colors().border_variant) + .bg(theme.colors().element_background) + .child( + Label::new(patch.title.clone()) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .children(paths.into_iter().map(|path| { + h_flex() + .px_2() + .pt_1() + .gap_1p5() + .child(Icon::new(IconName::File).size(IconSize::Small)) + .child(Label::new(path).size(LabelSize::Small)) + })) + .when(patch.status == AssistantPatchStatus::Pending, |div| { + div.child( + h_flex() + .pt_1() + .px_2() + .gap_1() + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::XSmall) + .color(Color::Muted) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage( + delta, + ))) + }, + ), + ) + .child( + Label::new("Generating…") + .color(Color::Muted) + .size(LabelSize::Small) + .with_animation( + "pulsating-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.alpha(delta), + ), + ), + ) + }) + .into_any(), + ) + } + + fn render_notice(&self, cx: &mut Context) -> Option { + // This was previously gated behind the `zed-pro` feature flag. Since we + // aren't planning to ship that right now, we're just hard-coding this + // value to not show the nudge. + let nudge = Some(false); + + if nudge.map_or(false, |value| value) { + Some( + h_flex() + .p_3() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().editor_background) + .justify_between() + .child( + h_flex() + .gap_3() + .child(Icon::new(IconName::ZedAssistant).color(Color::Accent)) + .child(Label::new("Zed AI is here! Get started by signing in →")), + ) + .child( + Button::new("sign-in", "Sign in") + .size(ButtonSize::Compact) + .style(ButtonStyle::Filled) + .on_click(cx.listener(|this, _event, _window, cx| { + let client = this + .workspace + .update(cx, |workspace, _| workspace.client().clone()) + .log_err(); + + if let Some(client) = client { + cx.spawn(|this, mut cx| async move { + client.authenticate_and_connect(true, &mut cx).await?; + this.update(&mut cx, |_, cx| cx.notify()) + }) + .detach_and_log_err(cx) + } + })), + ) + .into_any_element(), + ) + } else if let Some(configuration_error) = configuration_error(cx) { + let label = match configuration_error { + ConfigurationError::NoProvider => "No LLM provider selected.", + ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.", + ConfigurationError::ProviderPendingTermsAcceptance(_) => { + "LLM provider requires accepting the Terms of Service." + } + }; + Some( + h_flex() + .px_3() + .py_2() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().editor_background) + .justify_between() + .child( + h_flex() + .gap_3() + .child( + Icon::new(IconName::Warning) + .size(IconSize::Small) + .color(Color::Warning), + ) + .child(Label::new(label)), + ) + .child( + Button::new("open-configuration", "Configure Providers") + .size(ButtonSize::Compact) + .icon(Some(IconName::SlidersVertical)) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .style(ButtonStyle::Filled) + .on_click({ + let focus_handle = self.focus_handle(cx).clone(); + move |_event, window, cx| { + focus_handle.dispatch_action(&ShowConfiguration, window, cx); + } + }), + ) + .into_any_element(), + ) + } else { + None + } + } + + fn render_send_button(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx).clone(); + + let (style, tooltip) = match token_state(&self.context, cx) { + Some(TokenState::NoTokensLeft { .. }) => ( + ButtonStyle::Tinted(TintColor::Error), + Some(Tooltip::text("Token limit reached")(window, cx)), + ), + Some(TokenState::HasMoreTokens { + over_warn_threshold, + .. + }) => { + let (style, tooltip) = if over_warn_threshold { + ( + ButtonStyle::Tinted(TintColor::Warning), + Some(Tooltip::text("Token limit is close to exhaustion")( + window, cx, + )), + ) + } else { + (ButtonStyle::Filled, None) + }; + (style, tooltip) + } + None => (ButtonStyle::Filled, None), + }; + + let provider = LanguageModelRegistry::read_global(cx).active_provider(); + + let has_configuration_error = configuration_error(cx).is_some(); + let needs_to_accept_terms = self.show_accept_terms + && provider + .as_ref() + .map_or(false, |provider| provider.must_accept_terms(cx)); + let disabled = has_configuration_error || needs_to_accept_terms; + + ButtonLike::new("send_button") + .disabled(disabled) + .style(style) + .when_some(tooltip, |button, tooltip| { + button.tooltip(move |_, _| tooltip.clone()) + }) + .layer(ElevationIndex::ModalSurface) + .child(Label::new( + if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) { + "Chat" + } else { + "Send" + }, + )) + .children( + KeyBinding::for_action_in(&Assist, &focus_handle, window) + .map(|binding| binding.into_any_element()), + ) + .on_click(move |_event, window, cx| { + focus_handle.dispatch_action(&Assist, window, cx); + }) + } + + fn render_edit_button(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx).clone(); + + let (style, tooltip) = match token_state(&self.context, cx) { + Some(TokenState::NoTokensLeft { .. }) => ( + ButtonStyle::Tinted(TintColor::Error), + Some(Tooltip::text("Token limit reached")(window, cx)), + ), + Some(TokenState::HasMoreTokens { + over_warn_threshold, + .. + }) => { + let (style, tooltip) = if over_warn_threshold { + ( + ButtonStyle::Tinted(TintColor::Warning), + Some(Tooltip::text("Token limit is close to exhaustion")( + window, cx, + )), + ) + } else { + (ButtonStyle::Filled, None) + }; + (style, tooltip) + } + None => (ButtonStyle::Filled, None), + }; + + let provider = LanguageModelRegistry::read_global(cx).active_provider(); + + let has_configuration_error = configuration_error(cx).is_some(); + let needs_to_accept_terms = self.show_accept_terms + && provider + .as_ref() + .map_or(false, |provider| provider.must_accept_terms(cx)); + let disabled = has_configuration_error || needs_to_accept_terms; + + ButtonLike::new("edit_button") + .disabled(disabled) + .style(style) + .when_some(tooltip, |button, tooltip| { + button.tooltip(move |_, _| tooltip.clone()) + }) + .layer(ElevationIndex::ModalSurface) + .child(Label::new("Suggest Edits")) + .children( + KeyBinding::for_action_in(&Edit, &focus_handle, window) + .map(|binding| binding.into_any_element()), + ) + .on_click(move |_event, window, cx| { + focus_handle.dispatch_action(&Edit, window, cx); + }) + } + + fn render_inject_context_menu(&self, cx: &mut Context) -> impl IntoElement { + slash_command_picker::SlashCommandSelector::new( + self.slash_commands.clone(), + cx.entity().downgrade(), + Button::new("trigger", "Add Context") + .icon(IconName::Plus) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .tooltip(Tooltip::text("Type / to insert via keyboard")), + ) + } + + fn render_last_error(&self, cx: &mut Context) -> Option { + let last_error = self.last_error.as_ref()?; + + Some( + div() + .absolute() + .right_3() + .bottom_12() + .max_w_96() + .py_2() + .px_3() + .elevation_2(cx) + .occlude() + .child(match last_error { + AssistError::FileRequired => self.render_file_required_error(cx), + AssistError::PaymentRequired => self.render_payment_required_error(cx), + AssistError::MaxMonthlySpendReached => { + self.render_max_monthly_spend_reached_error(cx) + } + AssistError::Message(error_message) => { + self.render_assist_error(error_message, cx) + } + }) + .into_any(), + ) + } + + fn render_file_required_error(&self, cx: &mut Context) -> AnyElement { + v_flex() + .gap_0p5() + .child( + h_flex() + .gap_1p5() + .items_center() + .child(Icon::new(IconName::Warning).color(Color::Warning)) + .child( + Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM), + ), + ) + .child( + div() + .id("error-message") + .max_h_24() + .overflow_y_scroll() + .child(Label::new( + "To include files, type /file or /tab in your prompt.", + )), + ) + .child( + h_flex() + .justify_end() + .mt_1() + .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( + |this, _, _window, cx| { + this.last_error = None; + cx.notify(); + }, + ))), + ) + .into_any() + } + + fn render_payment_required_error(&self, cx: &mut Context) -> AnyElement { + const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used."; + + v_flex() + .gap_0p5() + .child( + h_flex() + .gap_1p5() + .items_center() + .child(Icon::new(IconName::XCircle).color(Color::Error)) + .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)), + ) + .child( + div() + .id("error-message") + .max_h_24() + .overflow_y_scroll() + .child(Label::new(ERROR_MESSAGE)), + ) + .child( + h_flex() + .justify_end() + .mt_1() + .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( + |this, _, _window, cx| { + this.last_error = None; + cx.open_url(&zed_urls::account_url(cx)); + cx.notify(); + }, + ))) + .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( + |this, _, _window, cx| { + this.last_error = None; + cx.notify(); + }, + ))), + ) + .into_any() + } + + fn render_max_monthly_spend_reached_error(&self, cx: &mut Context) -> AnyElement { + const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs."; + + v_flex() + .gap_0p5() + .child( + h_flex() + .gap_1p5() + .items_center() + .child(Icon::new(IconName::XCircle).color(Color::Error)) + .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)), + ) + .child( + div() + .id("error-message") + .max_h_24() + .overflow_y_scroll() + .child(Label::new(ERROR_MESSAGE)), + ) + .child( + h_flex() + .justify_end() + .mt_1() + .child( + Button::new("subscribe", "Update Monthly Spend Limit").on_click( + cx.listener(|this, _, _window, cx| { + this.last_error = None; + cx.open_url(&zed_urls::account_url(cx)); + cx.notify(); + }), + ), + ) + .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( + |this, _, _window, cx| { + this.last_error = None; + cx.notify(); + }, + ))), + ) + .into_any() + } + + fn render_assist_error( + &self, + error_message: &SharedString, + cx: &mut Context, + ) -> AnyElement { + v_flex() + .gap_0p5() + .child( + h_flex() + .gap_1p5() + .items_center() + .child(Icon::new(IconName::XCircle).color(Color::Error)) + .child( + Label::new("Error interacting with language model") + .weight(FontWeight::MEDIUM), + ), + ) + .child( + div() + .id("error-message") + .max_h_32() + .overflow_y_scroll() + .child(Label::new(error_message.clone())), + ) + .child( + h_flex() + .justify_end() + .mt_1() + .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( + |this, _, _window, cx| { + this.last_error = None; + cx.notify(); + }, + ))), + ) + .into_any() + } +} + +/// Returns the contents of the *outermost* fenced code block that contains the given offset. +fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option> { + const CODE_BLOCK_NODE: &'static str = "fenced_code_block"; + const CODE_BLOCK_CONTENT: &'static str = "code_fence_content"; + + let layer = snapshot.syntax_layers().next()?; + + let root_node = layer.node(); + let mut cursor = root_node.walk(); + + // Go to the first child for the given offset + while cursor.goto_first_child_for_byte(offset).is_some() { + // If we're at the end of the node, go to the next one. + // Example: if you have a fenced-code-block, and you're on the start of the line + // right after the closing ```, you want to skip the fenced-code-block and + // go to the next sibling. + if cursor.node().end_byte() == offset { + cursor.goto_next_sibling(); + } + + if cursor.node().start_byte() > offset { + break; + } + + // We found the fenced code block. + if cursor.node().kind() == CODE_BLOCK_NODE { + // Now we need to find the child node that contains the code. + cursor.goto_first_child(); + loop { + if cursor.node().kind() == CODE_BLOCK_CONTENT { + return Some(cursor.node().byte_range()); + } + if !cursor.goto_next_sibling() { + break; + } + } + } + } + + None +} + +fn render_fold_icon_button( + editor: WeakEntity, + icon: IconName, + label: SharedString, +) -> Arc, &mut Window, &mut App) -> AnyElement> { + Arc::new(move |fold_id, fold_range, _window, _cx| { + let editor = editor.clone(); + ButtonLike::new(fold_id) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ElevatedSurface) + .child(Icon::new(icon)) + .child(Label::new(label.clone()).single_line()) + .on_click(move |_, window, cx| { + editor + .update(cx, |editor, cx| { + let buffer_start = fold_range + .start + .to_point(&editor.buffer().read(cx).read(cx)); + let buffer_row = MultiBufferRow(buffer_start.row); + editor.unfold_at(&UnfoldAt { buffer_row }, window, cx); + }) + .ok(); + }) + .into_any_element() + }) +} + +type ToggleFold = Arc; + +fn render_slash_command_output_toggle( + row: MultiBufferRow, + is_folded: bool, + fold: ToggleFold, + _window: &mut Window, + _cx: &mut App, +) -> AnyElement { + Disclosure::new( + ("slash-command-output-fold-indicator", row.0 as u64), + !is_folded, + ) + .toggle_state(is_folded) + .on_click(move |_e, window, cx| fold(!is_folded, window, cx)) + .into_any_element() +} + +pub fn fold_toggle( + name: &'static str, +) -> impl Fn( + MultiBufferRow, + bool, + Arc, + &mut Window, + &mut App, +) -> AnyElement { + move |row, is_folded, fold, _window, _cx| { + Disclosure::new((name, row.0 as u64), !is_folded) + .toggle_state(is_folded) + .on_click(move |_e, window, cx| fold(!is_folded, window, cx)) + .into_any_element() + } +} + +fn quote_selection_fold_placeholder(title: String, editor: WeakEntity) -> FoldPlaceholder { + FoldPlaceholder { + render: Arc::new({ + move |fold_id, fold_range, _window, _cx| { + let editor = editor.clone(); + ButtonLike::new(fold_id) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ElevatedSurface) + .child(Icon::new(IconName::TextSnippet)) + .child(Label::new(title.clone()).single_line()) + .on_click(move |_, window, cx| { + editor + .update(cx, |editor, cx| { + let buffer_start = fold_range + .start + .to_point(&editor.buffer().read(cx).read(cx)); + let buffer_row = MultiBufferRow(buffer_start.row); + editor.unfold_at(&UnfoldAt { buffer_row }, window, cx); + }) + .ok(); + }) + .into_any_element() + } + }), + merge_adjacent: false, + ..Default::default() + } +} + +fn render_quote_selection_output_toggle( + row: MultiBufferRow, + is_folded: bool, + fold: ToggleFold, + _window: &mut Window, + _cx: &mut App, +) -> AnyElement { + Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded) + .toggle_state(is_folded) + .on_click(move |_e, window, cx| fold(!is_folded, window, cx)) + .into_any_element() +} + +fn render_pending_slash_command_gutter_decoration( + row: MultiBufferRow, + status: &PendingSlashCommandStatus, + confirm_command: Arc, +) -> AnyElement { + let mut icon = IconButton::new( + ("slash-command-gutter-decoration", row.0), + ui::IconName::TriangleRight, + ) + .on_click(move |_e, window, cx| confirm_command(window, cx)) + .icon_size(ui::IconSize::Small) + .size(ui::ButtonSize::None); + + match status { + PendingSlashCommandStatus::Idle => { + icon = icon.icon_color(Color::Muted); + } + PendingSlashCommandStatus::Running { .. } => { + icon = icon.toggle_state(true); + } + PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error), + } + + icon.into_any_element() +} + +fn render_docs_slash_command_trailer( + row: MultiBufferRow, + command: ParsedSlashCommand, + cx: &mut App, +) -> AnyElement { + if command.arguments.is_empty() { + return Empty.into_any(); + } + let args = DocsSlashCommandArgs::parse(&command.arguments); + + let Some(store) = args + .provider() + .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok()) + else { + return Empty.into_any(); + }; + + let Some(package) = args.package() else { + return Empty.into_any(); + }; + + let mut children = Vec::new(); + + if store.is_indexing(&package) { + children.push( + div() + .id(("crates-being-indexed", row.0)) + .child(Icon::new(IconName::ArrowCircle).with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(4)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + )) + .tooltip({ + let package = package.clone(); + Tooltip::text(format!("Indexing {package}…")) + }) + .into_any_element(), + ); + } + + if let Some(latest_error) = store.latest_error_for_package(&package) { + children.push( + div() + .id(("latest-error", row.0)) + .child( + Icon::new(IconName::Warning) + .size(IconSize::Small) + .color(Color::Warning), + ) + .tooltip(Tooltip::text(format!("Failed to index: {latest_error}"))) + .into_any_element(), + ) + } + + let is_indexing = store.is_indexing(&package); + let latest_error = store.latest_error_for_package(&package); + + if !is_indexing && latest_error.is_none() { + return Empty.into_any(); + } + + h_flex().gap_2().children(children).into_any_element() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CopyMetadata { + creases: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SelectedCreaseMetadata { + range_relative_to_selection: Range, + crease: CreaseMetadata, +} + +impl EventEmitter for ContextEditor {} +impl EventEmitter for ContextEditor {} + +impl Render for ContextEditor { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let provider = LanguageModelRegistry::read_global(cx).active_provider(); + let accept_terms = if self.show_accept_terms { + provider.as_ref().and_then(|provider| { + provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx) + }) + } else { + None + }; + + v_flex() + .key_context("ContextEditor") + .capture_action(cx.listener(ContextEditor::cancel)) + .capture_action(cx.listener(ContextEditor::save)) + .capture_action(cx.listener(ContextEditor::copy)) + .capture_action(cx.listener(ContextEditor::cut)) + .capture_action(cx.listener(ContextEditor::paste)) + .capture_action(cx.listener(ContextEditor::cycle_message_role)) + .capture_action(cx.listener(ContextEditor::confirm_command)) + .on_action(cx.listener(ContextEditor::edit)) + .on_action(cx.listener(ContextEditor::assist)) + .on_action(cx.listener(ContextEditor::split)) + .size_full() + .children(self.render_notice(cx)) + .child( + div() + .flex_grow() + .bg(cx.theme().colors().editor_background) + .child(self.editor.clone()), + ) + .when_some(accept_terms, |this, element| { + this.child( + div() + .absolute() + .right_3() + .bottom_12() + .max_w_96() + .py_2() + .px_3() + .elevation_2(cx) + .bg(cx.theme().colors().surface_background) + .occlude() + .child(element), + ) + }) + .children(self.render_last_error(cx)) + .child( + h_flex().w_full().relative().child( + h_flex() + .p_2() + .w_full() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().editor_background) + .child(h_flex().gap_1().child(self.render_inject_context_menu(cx))) + .child( + h_flex() + .w_full() + .justify_end() + .when( + AssistantSettings::get_global(cx).are_live_diffs_enabled(cx), + |buttons| { + buttons + .items_center() + .gap_1p5() + .child(self.render_edit_button(window, cx)) + .child( + Label::new("or") + .size(LabelSize::Small) + .color(Color::Muted), + ) + }, + ) + .child(self.render_send_button(window, cx)), + ), + ), + ) + } +} + +impl Focusable for ContextEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl Item for ContextEditor { + type Event = editor::EditorEvent; + + fn tab_content_text(&self, _window: &Window, cx: &App) -> Option { + Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) { + match event { + EditorEvent::Edited { .. } => { + f(item::ItemEvent::Edit); + } + EditorEvent::TitleChanged => { + f(item::ItemEvent::UpdateTab); + } + _ => {} + } + } + + fn tab_tooltip_text(&self, cx: &App) -> Option { + Some(self.title(cx).to_string().into()) + } + + fn as_searchable(&self, handle: &Entity) -> Option> { + Some(Box::new(handle.clone())) + } + + fn set_nav_history( + &mut self, + nav_history: pane::ItemNavHistory, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + Item::set_nav_history(editor, nav_history, window, cx) + }) + } + + fn navigate( + &mut self, + data: Box, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.editor + .update(cx, |editor, cx| Item::navigate(editor, data, window, cx)) + } + + fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, window, cx)) + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a Entity, + _: &'a App, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } + + fn include_in_nav_history() -> bool { + false + } +} + +impl SearchableItem for ContextEditor { + type Match = ::Match; + + fn clear_matches(&mut self, window: &mut Window, cx: &mut Context) { + self.editor.update(cx, |editor, cx| { + editor.clear_matches(window, cx); + }); + } + + fn update_matches( + &mut self, + matches: &[Self::Match], + window: &mut Window, + cx: &mut Context, + ) { + self.editor + .update(cx, |editor, cx| editor.update_matches(matches, window, cx)); + } + + fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context) -> String { + self.editor + .update(cx, |editor, cx| editor.query_suggestion(window, cx)) + } + + fn activate_match( + &mut self, + index: usize, + matches: &[Self::Match], + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.activate_match(index, matches, window, cx); + }); + } + + fn select_matches( + &mut self, + matches: &[Self::Match], + window: &mut Window, + cx: &mut Context, + ) { + self.editor + .update(cx, |editor, cx| editor.select_matches(matches, window, cx)); + } + + fn replace( + &mut self, + identifier: &Self::Match, + query: &project::search::SearchQuery, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.replace(identifier, query, window, cx) + }); + } + + fn find_matches( + &mut self, + query: Arc, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.editor + .update(cx, |editor, cx| editor.find_matches(query, window, cx)) + } + + fn active_match_index( + &mut self, + matches: &[Self::Match], + window: &mut Window, + cx: &mut Context, + ) -> Option { + self.editor.update(cx, |editor, cx| { + editor.active_match_index(matches, window, cx) + }) + } +} + +impl FollowableItem for ContextEditor { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, window: &Window, cx: &App) -> Option { + let context = self.context.read(cx); + Some(proto::view::Variant::ContextEditor( + proto::view::ContextEditor { + context_id: context.id().to_proto(), + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(window, cx) + { + Some(proto) + } else { + None + }, + }, + )) + } + + fn from_state_proto( + workspace: Entity, + id: workspace::ViewId, + state: &mut Option, + window: &mut Window, + cx: &mut App, + ) -> Option>>> { + let proto::view::Variant::ContextEditor(_) = state.as_ref()? else { + return None; + }; + let Some(proto::view::Variant::ContextEditor(state)) = state.take() else { + unreachable!() + }; + + let context_id = ContextId::from_proto(state.context_id); + let editor_state = state.editor?; + + let project = workspace.read(cx).project().clone(); + let assistant_panel_delegate = ::try_global(cx)?; + + let context_editor_task = workspace.update(cx, |workspace, cx| { + assistant_panel_delegate.open_remote_context(workspace, context_id, window, cx) + }); + + Some(window.spawn(cx, |mut cx| async move { + let context_editor = context_editor_task.await?; + context_editor + .update_in(&mut cx, |context_editor, window, cx| { + context_editor.remote_id = Some(id); + context_editor.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: editor_state.selections, + pending_selection: editor_state.pending_selection, + scroll_top_anchor: editor_state.scroll_top_anchor, + scroll_x: editor_state.scroll_y, + scroll_y: editor_state.scroll_y, + ..Default::default() + }), + window, + cx, + ) + }) + })? + .await?; + Ok(context_editor) + })) + } + + fn to_follow_event(event: &Self::Event) -> Option { + Editor::to_follow_event(event) + } + + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + window: &Window, + cx: &App, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, window, cx) + } + + fn apply_update_proto( + &mut self, + project: &Entity, + message: proto::update_view::Variant, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, window, cx) + }) + } + + fn is_project_item(&self, _window: &Window, _cx: &App) -> bool { + true + } + + fn set_leader_peer_id( + &mut self, + leader_peer_id: Option, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_peer_id(leader_peer_id, window, cx) + }) + } + + fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option { + if existing.context.read(cx).id() == self.context.read(cx).id() { + Some(item::Dedup::KeepExisting) + } else { + None + } + } +} + +pub struct ContextEditorToolbarItem { + active_context_editor: Option>, + model_summary_editor: Entity, + language_model_selector: Entity, + language_model_selector_menu_handle: PopoverMenuHandle, +} + +impl ContextEditorToolbarItem { + pub fn new( + workspace: &Workspace, + model_selector_menu_handle: PopoverMenuHandle, + model_summary_editor: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + active_context_editor: None, + model_summary_editor, + language_model_selector: cx.new(|cx| { + let fs = workspace.app_state().fs.clone(); + LanguageModelSelector::new( + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + }, + window, + cx, + ) + }), + language_model_selector_menu_handle: model_selector_menu_handle, + } + } + + fn render_remaining_tokens(&self, cx: &mut Context) -> Option { + let context = &self + .active_context_editor + .as_ref()? + .upgrade()? + .read(cx) + .context; + let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? { + TokenState::NoTokensLeft { + max_token_count, + token_count, + } => (Color::Error, token_count, max_token_count), + TokenState::HasMoreTokens { + max_token_count, + token_count, + over_warn_threshold, + } => { + let color = if over_warn_threshold { + Color::Warning + } else { + Color::Muted + }; + (color, token_count, max_token_count) + } + }; + Some( + h_flex() + .gap_0p5() + .child( + Label::new(humanize_token_count(token_count)) + .size(LabelSize::Small) + .color(token_count_color), + ) + .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) + .child( + Label::new(humanize_token_count(max_token_count)) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + } +} + +impl Render for ContextEditorToolbarItem { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let left_side = h_flex() + .group("chat-title-group") + .gap_1() + .items_center() + .flex_grow() + .child( + div() + .w_full() + .when(self.active_context_editor.is_some(), |left_side| { + left_side.child(self.model_summary_editor.clone()) + }), + ) + .child( + div().visible_on_hover("chat-title-group").child( + IconButton::new("regenerate-context", IconName::RefreshTitle) + .shape(ui::IconButtonShape::Square) + .tooltip(Tooltip::text("Regenerate Title")) + .on_click(cx.listener(move |_, _, _window, cx| { + cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary) + })), + ), + ); + let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); + let active_model = LanguageModelRegistry::read_global(cx).active_model(); + let right_side = h_flex() + .gap_2() + // TODO display this in a nicer way, once we have a design for it. + // .children({ + // let project = self + // .workspace + // .upgrade() + // .map(|workspace| workspace.read(cx).project().downgrade()); + // + // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| { + // project.and_then(|project| db.remaining_summaries(&project, cx)) + // }); + // scan_items_remaining + // .map(|remaining_items| format!("Files to scan: {}", remaining_items)) + // }) + .child( + LanguageModelSelectorPopoverMenu::new( + self.language_model_selector.clone(), + ButtonLike::new("active-model") + .style(ButtonStyle::Subtle) + .child( + h_flex() + .w_full() + .gap_0p5() + .child( + div() + .overflow_x_hidden() + .flex_grow() + .whitespace_nowrap() + .child(match (active_provider, active_model) { + (Some(provider), Some(model)) => h_flex() + .gap_1() + .child( + Icon::new( + model + .icon() + .unwrap_or_else(|| provider.icon()), + ) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child( + Label::new(model.name().0) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element(), + _ => Label::new("No model selected") + .size(LabelSize::Small) + .color(Color::Muted) + .into_any_element(), + }), + ) + .child( + Icon::new(IconName::ChevronDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ) + .tooltip(move |window, cx| { + Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx) + }), + ) + .with_handle(self.language_model_selector_menu_handle.clone()), + ) + .children(self.render_remaining_tokens(cx)); + + h_flex() + .px_0p5() + .size_full() + .gap_2() + .justify_between() + .child(left_side) + .child(right_side) + } +} + +impl ToolbarItemView for ContextEditorToolbarItem { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _window: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + self.active_context_editor = active_pane_item + .and_then(|item| item.act_as::(cx)) + .map(|editor| editor.downgrade()); + cx.notify(); + if self.active_context_editor.is_none() { + ToolbarItemLocation::Hidden + } else { + ToolbarItemLocation::PrimaryRight + } + } + + fn pane_focus_update( + &mut self, + _pane_focused: bool, + _window: &mut Window, + cx: &mut Context, + ) { + cx.notify(); + } +} + +impl EventEmitter for ContextEditorToolbarItem {} + +pub enum ContextEditorToolbarItemEvent { + RegenerateSummary, +} +impl EventEmitter for ContextEditorToolbarItem {} + +enum PendingSlashCommand {} + +fn invoked_slash_command_fold_placeholder( + command_id: InvokedSlashCommandId, + context: WeakEntity, +) -> FoldPlaceholder { + FoldPlaceholder { + constrain_width: false, + merge_adjacent: false, + render: Arc::new(move |fold_id, _, _window, cx| { + let Some(context) = context.upgrade() else { + return Empty.into_any(); + }; + + let Some(command) = context.read(cx).invoked_slash_command(&command_id) else { + return Empty.into_any(); + }; + + h_flex() + .id(fold_id) + .px_1() + .ml_6() + .gap_2() + .bg(cx.theme().colors().surface_background) + .rounded_md() + .child(Label::new(format!("/{}", command.name.clone()))) + .map(|parent| match &command.status { + InvokedSlashCommandStatus::Running(_) => { + parent.child(Icon::new(IconName::ArrowCircle).with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(4)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + )) + } + InvokedSlashCommandStatus::Error(message) => parent.child( + Label::new(format!("error: {message}")) + .single_line() + .color(Color::Error), + ), + InvokedSlashCommandStatus::Finished => parent, + }) + .into_any_element() + }), + type_tag: Some(TypeId::of::()), + } +} + +enum TokenState { + NoTokensLeft { + max_token_count: usize, + token_count: usize, + }, + HasMoreTokens { + max_token_count: usize, + token_count: usize, + over_warn_threshold: bool, + }, +} + +fn token_state(context: &Entity, cx: &App) -> Option { + const WARNING_TOKEN_THRESHOLD: f32 = 0.8; + + let model = LanguageModelRegistry::read_global(cx).active_model()?; + let token_count = context.read(cx).token_count()?; + let max_token_count = model.max_token_count(); + + let remaining_tokens = max_token_count as isize - token_count as isize; + let token_state = if remaining_tokens <= 0 { + TokenState::NoTokensLeft { + max_token_count, + token_count, + } + } else { + let over_warn_threshold = + token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD; + TokenState::HasMoreTokens { + max_token_count, + token_count, + over_warn_threshold, + } + }; + Some(token_state) +} + +fn size_for_image(data: &RenderImage, max_size: Size) -> Size { + let image_size = data + .size(0) + .map(|dimension| Pixels::from(u32::from(dimension))); + let image_ratio = image_size.width / image_size.height; + let bounds_ratio = max_size.width / max_size.height; + + if image_size.width > max_size.width || image_size.height > max_size.height { + if bounds_ratio > image_ratio { + size( + image_size.width * (max_size.height / image_size.height), + max_size.height, + ) + } else { + size( + max_size.width, + image_size.height * (max_size.width / image_size.width), + ) + } + } else { + size(image_size.width, image_size.height) + } +} + +pub enum ConfigurationError { + NoProvider, + ProviderNotAuthenticated, + ProviderPendingTermsAcceptance(Arc), +} + +fn configuration_error(cx: &App) -> Option { + let provider = LanguageModelRegistry::read_global(cx).active_provider(); + let is_authenticated = provider + .as_ref() + .map_or(false, |provider| provider.is_authenticated(cx)); + + if provider.is_some() && is_authenticated { + return None; + } + + if provider.is_none() { + return Some(ConfigurationError::NoProvider); + } + + if !is_authenticated { + return Some(ConfigurationError::ProviderNotAuthenticated); + } + + None +} + +pub fn humanize_token_count(count: usize) -> String { + match count { + 0..=999 => count.to_string(), + 1000..=9999 => { + let thousands = count / 1000; + let hundreds = (count % 1000 + 50) / 100; + if hundreds == 0 { + format!("{}k", thousands) + } else if hundreds == 10 { + format!("{}k", thousands + 1) + } else { + format!("{}.{}k", thousands, hundreds) + } + } + _ => format!("{}k", (count + 500) / 1000), + } +} + +pub fn make_lsp_adapter_delegate( + project: &Entity, + cx: &mut App, +) -> Result>> { + project.update(cx, |project, cx| { + // TODO: Find the right worktree. + let Some(worktree) = project.worktrees(cx).next() else { + return Ok(None::>); + }; + let http_client = project.client().http_client().clone(); + project.lsp_store().update(cx, |_, cx| { + Ok(Some(LocalLspAdapterDelegate::new( + project.languages().clone(), + project.environment(), + cx.weak_entity(), + &worktree, + http_client, + project.fs().clone(), + cx, + ) as Arc)) + }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::App; + use language::Buffer; + use unindent::Unindent; + + #[gpui::test] + fn test_find_code_blocks(cx: &mut App) { + let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into()); + + let buffer = cx.new(|cx| { + let text = r#" + line 0 + line 1 + ```rust + fn main() {} + ``` + line 5 + line 6 + line 7 + ```go + func main() {} + ``` + line 11 + ``` + this is plain text code block + ``` + + ```go + func another() {} + ``` + line 19 + "# + .unindent(); + let mut buffer = Buffer::local(text, cx); + buffer.set_language(Some(markdown.clone()), cx); + buffer + }); + let snapshot = buffer.read(cx).snapshot(); + + let code_blocks = vec![ + Point::new(3, 0)..Point::new(4, 0), + Point::new(9, 0)..Point::new(10, 0), + Point::new(13, 0)..Point::new(14, 0), + Point::new(17, 0)..Point::new(18, 0), + ] + .into_iter() + .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end)) + .collect::>(); + + let expected_results = vec![ + (0, None), + (1, None), + (2, Some(code_blocks[0].clone())), + (3, Some(code_blocks[0].clone())), + (4, Some(code_blocks[0].clone())), + (5, None), + (6, None), + (7, None), + (8, Some(code_blocks[1].clone())), + (9, Some(code_blocks[1].clone())), + (10, Some(code_blocks[1].clone())), + (11, None), + (12, Some(code_blocks[2].clone())), + (13, Some(code_blocks[2].clone())), + (14, Some(code_blocks[2].clone())), + (15, None), + (16, Some(code_blocks[3].clone())), + (17, Some(code_blocks[3].clone())), + (18, Some(code_blocks[3].clone())), + (19, None), + ]; + + for (row, expected) in expected_results { + let offset = snapshot.point_to_offset(Point::new(row, 0)); + let range = find_surrounding_code_block(&snapshot, offset); + assert_eq!(range, expected, "unexpected result on row {:?}", row); + } + } +} diff --git a/crates/assistant_context_editor/src/context_history.rs b/crates/assistant_context_editor/src/context_history.rs new file mode 100644 index 00000000000000..2401f6d70ba605 --- /dev/null +++ b/crates/assistant_context_editor/src/context_history.rs @@ -0,0 +1,269 @@ +use std::sync::Arc; + +use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity}; +use picker::{Picker, PickerDelegate}; +use project::Project; +use ui::utils::{format_distance_from_now, DateTimeType}; +use ui::{prelude::*, Avatar, ListItem, ListItemSpacing}; +use workspace::{Item, Workspace}; + +use crate::{ + AssistantPanelDelegate, ContextStore, RemoteContextMetadata, SavedContextMetadata, + DEFAULT_TAB_TITLE, +}; + +#[derive(Clone)] +pub enum ContextMetadata { + Remote(RemoteContextMetadata), + Saved(SavedContextMetadata), +} + +enum SavedContextPickerEvent { + Confirmed(ContextMetadata), +} + +pub struct ContextHistory { + picker: Entity>, + _subscriptions: Vec, + workspace: WeakEntity, +} + +impl ContextHistory { + pub fn new( + project: Entity, + context_store: Entity, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let picker = cx.new(|cx| { + Picker::uniform_list( + SavedContextPickerDelegate::new(project, context_store.clone()), + window, + cx, + ) + .modal(false) + .max_height(None) + }); + + let subscriptions = vec![ + cx.observe_in(&context_store, window, |this, _, window, cx| { + this.picker + .update(cx, |picker, cx| picker.refresh(window, cx)); + }), + cx.subscribe_in(&picker, window, Self::handle_picker_event), + ]; + + Self { + picker, + _subscriptions: subscriptions, + workspace, + } + } + + fn handle_picker_event( + &mut self, + _: &Entity>, + event: &SavedContextPickerEvent, + window: &mut Window, + cx: &mut Context, + ) { + let SavedContextPickerEvent::Confirmed(context) = event; + + let Some(assistant_panel_delegate) = ::try_global(cx) else { + return; + }; + + self.workspace + .update(cx, |workspace, cx| match context { + ContextMetadata::Remote(metadata) => { + assistant_panel_delegate + .open_remote_context(workspace, metadata.id.clone(), window, cx) + .detach_and_log_err(cx); + } + ContextMetadata::Saved(metadata) => { + assistant_panel_delegate + .open_saved_context(workspace, metadata.path.clone(), window, cx) + .detach_and_log_err(cx); + } + }) + .ok(); + } +} + +impl Render for ContextHistory { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child(self.picker.clone()) + } +} + +impl Focusable for ContextHistory { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter<()> for ContextHistory {} + +impl Item for ContextHistory { + type Event = (); + + fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { + Some("History".into()) + } +} + +struct SavedContextPickerDelegate { + store: Entity, + project: Entity, + matches: Vec, + selected_index: usize, +} + +impl EventEmitter for Picker {} + +impl SavedContextPickerDelegate { + fn new(project: Entity, store: Entity) -> Self { + Self { + project, + store, + matches: Vec::new(), + selected_index: 0, + } + } +} + +impl PickerDelegate for SavedContextPickerDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Search...".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + let search = self.store.read(cx).search(query, cx); + cx.spawn(|this, mut cx| async move { + let matches = search.await; + this.update(&mut cx, |this, cx| { + let host_contexts = this.delegate.store.read(cx).host_contexts(); + this.delegate.matches = host_contexts + .iter() + .cloned() + .map(ContextMetadata::Remote) + .chain(matches.into_iter().map(ContextMetadata::Saved)) + .collect(); + this.delegate.selected_index = 0; + cx.notify(); + }) + .ok(); + }) + } + + fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context>) { + if let Some(metadata) = self.matches.get(self.selected_index) { + cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone())); + } + } + + fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} + + fn render_match( + &self, + ix: usize, + selected: bool, + _window: &mut Window, + cx: &mut Context>, + ) -> Option { + let context = self.matches.get(ix)?; + let item = match context { + ContextMetadata::Remote(context) => { + let host_user = self.project.read(cx).host().and_then(|collaborator| { + self.project + .read(cx) + .user_store() + .read(cx) + .get_cached_user(collaborator.user_id) + }); + div() + .flex() + .w_full() + .justify_between() + .gap_2() + .child( + h_flex().flex_1().overflow_x_hidden().child( + Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into())) + .size(LabelSize::Small), + ), + ) + .child( + h_flex() + .gap_2() + .children(if let Some(host_user) = host_user { + vec![ + Avatar::new(host_user.avatar_uri.clone()).into_any_element(), + Label::new(format!("Shared by @{}", host_user.github_login)) + .color(Color::Muted) + .size(LabelSize::Small) + .into_any_element(), + ] + } else { + vec![Label::new("Shared by host") + .color(Color::Muted) + .size(LabelSize::Small) + .into_any_element()] + }), + ) + } + ContextMetadata::Saved(context) => div() + .flex() + .w_full() + .justify_between() + .gap_2() + .child( + h_flex() + .flex_1() + .child(Label::new(context.title.clone()).size(LabelSize::Small)) + .overflow_x_hidden(), + ) + .child( + Label::new(format_distance_from_now( + DateTimeType::Local(context.mtime), + false, + true, + true, + )) + .color(Color::Muted) + .size(LabelSize::Small), + ), + }; + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(item), + ) + } +} diff --git a/crates/assistant/src/context_store.rs b/crates/assistant_context_editor/src/context_store.rs similarity index 91% rename from crates/assistant/src/context_store.rs rename to crates/assistant_context_editor/src/context_store.rs index 92d9889f93b8c7..3ba399485875da 100644 --- a/crates/assistant/src/context_store.rs +++ b/crates/assistant_context_editor/src/context_store.rs @@ -1,5 +1,5 @@ use crate::{ - Context, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, + AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, SavedContextMetadata, }; use anyhow::{anyhow, Context as _, Result}; @@ -13,9 +13,7 @@ use context_server::{ContextServerFactoryRegistry, ContextServerTool}; use fs::Fs; use futures::StreamExt; use fuzzy::StringMatchCandidate; -use gpui::{ - AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel, -}; +use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; use language::LanguageRegistry; use paths::contexts_dir; use project::Project; @@ -33,7 +31,7 @@ use std::{ }; use util::{ResultExt, TryFutureExt}; -pub fn init(client: &AnyProtoClient) { +pub(crate) fn init(client: &AnyProtoClient) { client.add_model_message_handler(ContextStore::handle_advertise_contexts); client.add_model_request_handler(ContextStore::handle_open_context); client.add_model_request_handler(ContextStore::handle_create_context); @@ -50,7 +48,7 @@ pub struct RemoteContextMetadata { pub struct ContextStore { contexts: Vec, contexts_metadata: Vec, - context_server_manager: Model, + context_server_manager: Entity, context_server_slash_command_ids: HashMap, Vec>, context_server_tool_ids: HashMap, Vec>, host_contexts: Vec, @@ -61,7 +59,7 @@ pub struct ContextStore { telemetry: Arc, _watch_updates: Task>, client: Arc, - project: Model, + project: Entity, project_is_shared: bool, client_subscription: Option, _project_subscriptions: Vec, @@ -75,19 +73,19 @@ pub enum ContextStoreEvent { impl EventEmitter for ContextStore {} enum ContextHandle { - Weak(WeakModel), - Strong(Model), + Weak(WeakEntity), + Strong(Entity), } impl ContextHandle { - fn upgrade(&self) -> Option> { + fn upgrade(&self) -> Option> { match self { ContextHandle::Weak(weak) => weak.upgrade(), ContextHandle::Strong(strong) => Some(strong.clone()), } } - fn downgrade(&self) -> WeakModel { + fn downgrade(&self) -> WeakEntity { match self { ContextHandle::Weak(weak) => weak.clone(), ContextHandle::Strong(strong) => strong.downgrade(), @@ -97,12 +95,12 @@ impl ContextHandle { impl ContextStore { pub fn new( - project: Model, + project: Entity, prompt_builder: Arc, slash_commands: Arc, tools: Arc, - cx: &mut AppContext, - ) -> Task>> { + cx: &mut App, + ) -> Task>> { let fs = project.read(cx).fs().clone(); let languages = project.read(cx).languages().clone(); let telemetry = project.read(cx).client().telemetry().clone(); @@ -110,10 +108,10 @@ impl ContextStore { const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100); let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await; - let this = cx.new_model(|cx: &mut ModelContext| { + let this = cx.new(|cx: &mut Context| { let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx); - let context_server_manager = cx.new_model(|cx| { + let context_server_manager = cx.new(|cx| { ContextServerManager::new(context_server_factory_registry, project.clone(), cx) }); let mut this = Self { @@ -163,9 +161,9 @@ impl ContextStore { } async fn handle_advertise_contexts( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { this.host_contexts = envelope @@ -182,9 +180,9 @@ impl ContextStore { } async fn handle_open_context( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result { let context_id = ContextId::from_proto(envelope.payload.context_id); let operations = this.update(&mut cx, |this, cx| { @@ -212,9 +210,9 @@ impl ContextStore { } async fn handle_create_context( - this: Model, + this: Entity, _: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result { let (context_id, operations) = this.update(&mut cx, |this, cx| { if this.project.read(cx).is_via_collab() { @@ -240,9 +238,9 @@ impl ContextStore { } async fn handle_update_context( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { let context_id = ContextId::from_proto(envelope.payload.context_id); @@ -256,9 +254,9 @@ impl ContextStore { } async fn handle_synchronize_contexts( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result { this.update(&mut cx, |this, cx| { if this.project.read(cx).is_via_collab() { @@ -299,7 +297,7 @@ impl ContextStore { })? } - fn handle_project_changed(&mut self, _: Model, cx: &mut ModelContext) { + fn handle_project_changed(&mut self, _: Entity, cx: &mut Context) { let is_shared = self.project.read(cx).is_shared(); let was_shared = mem::replace(&mut self.project_is_shared, is_shared); if is_shared == was_shared { @@ -320,7 +318,7 @@ impl ContextStore { .client .subscribe_to_entity(remote_id) .log_err() - .map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async())); + .map(|subscription| subscription.set_model(&cx.entity(), &mut cx.to_async())); self.advertise_contexts(cx); } else { self.client_subscription = None; @@ -329,9 +327,9 @@ impl ContextStore { fn handle_project_event( &mut self, - _: Model, + _: Entity, event: &project::Event, - cx: &mut ModelContext, + cx: &mut Context, ) { match event { project::Event::Reshared => { @@ -361,9 +359,9 @@ impl ContextStore { } } - pub fn create(&mut self, cx: &mut ModelContext) -> Model { - let context = cx.new_model(|cx| { - Context::local( + pub fn create(&mut self, cx: &mut Context) -> Entity { + let context = cx.new(|cx| { + AssistantContext::local( self.languages.clone(), Some(self.project.clone()), Some(self.telemetry.clone()), @@ -379,8 +377,8 @@ impl ContextStore { pub fn create_remote_context( &mut self, - cx: &mut ModelContext, - ) -> Task>> { + cx: &mut Context, + ) -> Task>> { let project = self.project.read(cx); let Some(project_id) = project.remote_id() else { return Task::ready(Err(anyhow!("project was not remote"))); @@ -399,8 +397,8 @@ impl ContextStore { let response = request.await?; let context_id = ContextId::from_proto(response.context_id); let context_proto = response.context.context("invalid context")?; - let context = cx.new_model(|cx| { - Context::new( + let context = cx.new(|cx| { + AssistantContext::new( context_id.clone(), replica_id, capability, @@ -439,8 +437,8 @@ impl ContextStore { pub fn open_local_context( &mut self, path: PathBuf, - cx: &ModelContext, - ) -> Task>> { + cx: &Context, + ) -> Task>> { if let Some(existing_context) = self.loaded_context_for_path(&path, cx) { return Task::ready(Ok(existing_context)); } @@ -462,8 +460,8 @@ impl ContextStore { cx.spawn(|this, mut cx| async move { let saved_context = load.await?; - let context = cx.new_model(|cx| { - Context::deserialize( + let context = cx.new(|cx| { + AssistantContext::deserialize( saved_context, path.clone(), languages, @@ -486,7 +484,7 @@ impl ContextStore { }) } - fn loaded_context_for_path(&self, path: &Path, cx: &AppContext) -> Option> { + fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option> { self.contexts.iter().find_map(|context| { let context = context.upgrade()?; if context.read(cx).path() == Some(path) { @@ -497,11 +495,11 @@ impl ContextStore { }) } - pub(super) fn loaded_context_for_id( + pub fn loaded_context_for_id( &self, id: &ContextId, - cx: &AppContext, - ) -> Option> { + cx: &App, + ) -> Option> { self.contexts.iter().find_map(|context| { let context = context.upgrade()?; if context.read(cx).id() == id { @@ -515,8 +513,8 @@ impl ContextStore { pub fn open_remote_context( &mut self, context_id: ContextId, - cx: &mut ModelContext, - ) -> Task>> { + cx: &mut Context, + ) -> Task>> { let project = self.project.read(cx); let Some(project_id) = project.remote_id() else { return Task::ready(Err(anyhow!("project was not remote"))); @@ -541,8 +539,8 @@ impl ContextStore { cx.spawn(|this, mut cx| async move { let response = request.await?; let context_proto = response.context.context("invalid context")?; - let context = cx.new_model(|cx| { - Context::new( + let context = cx.new(|cx| { + AssistantContext::new( context_id.clone(), replica_id, capability, @@ -578,7 +576,7 @@ impl ContextStore { }) } - fn register_context(&mut self, context: &Model, cx: &mut ModelContext) { + fn register_context(&mut self, context: &Entity, cx: &mut Context) { let handle = if self.project_is_shared { ContextHandle::Strong(context.clone()) } else { @@ -591,9 +589,9 @@ impl ContextStore { fn handle_context_event( &mut self, - context: Model, + context: Entity, event: &ContextEvent, - cx: &mut ModelContext, + cx: &mut Context, ) { let Some(project_id) = self.project.read(cx).remote_id() else { return; @@ -618,7 +616,7 @@ impl ContextStore { } } - fn advertise_contexts(&self, cx: &AppContext) { + fn advertise_contexts(&self, cx: &App) { let Some(project_id) = self.project.read(cx).remote_id() else { return; }; @@ -652,7 +650,7 @@ impl ContextStore { .ok(); } - fn synchronize_contexts(&mut self, cx: &mut ModelContext) { + fn synchronize_contexts(&mut self, cx: &mut Context) { let Some(project_id) = self.project.read(cx).remote_id() else { return; }; @@ -707,7 +705,7 @@ impl ContextStore { .detach_and_log_err(cx); } - pub fn search(&self, query: String, cx: &AppContext) -> Task> { + pub fn search(&self, query: String, cx: &App) -> Task> { let metadata = self.contexts_metadata.clone(); let executor = cx.background_executor().clone(); cx.background_executor().spawn(async move { @@ -741,7 +739,7 @@ impl ContextStore { &self.host_contexts } - fn reload(&mut self, cx: &mut ModelContext) -> Task> { + fn reload(&mut self, cx: &mut Context) -> Task> { let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { fs.create_dir(contexts_dir()).await?; @@ -790,8 +788,8 @@ impl ContextStore { }) } - pub fn restart_context_servers(&mut self, cx: &mut ModelContext) { - cx.update_model( + pub fn restart_context_servers(&mut self, cx: &mut Context) { + cx.update_entity( &self.context_server_manager, |context_server_manager, cx| { for server in context_server_manager.servers() { @@ -803,7 +801,7 @@ impl ContextStore { ); } - fn register_context_server_handlers(&self, cx: &mut ModelContext) { + fn register_context_server_handlers(&self, cx: &mut Context) { cx.subscribe( &self.context_server_manager.clone(), Self::handle_context_server_event, @@ -813,9 +811,9 @@ impl ContextStore { fn handle_context_server_event( &mut self, - context_server_manager: Model, + context_server_manager: Entity, event: &context_server::manager::Event, - cx: &mut ModelContext, + cx: &mut Context, ) { let slash_command_working_set = self.slash_commands.clone(); let tool_working_set = self.tools.clone(); diff --git a/crates/assistant/src/patch.rs b/crates/assistant_context_editor/src/patch.rs similarity index 95% rename from crates/assistant/src/patch.rs rename to crates/assistant_context_editor/src/patch.rs index ca2df7a0e01bdd..1557d7456791f5 100644 --- a/crates/assistant/src/patch.rs +++ b/crates/assistant_context_editor/src/patch.rs @@ -2,14 +2,14 @@ use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; use editor::ProposedChangesEditor; use futures::{future, TryFutureExt as _}; -use gpui::{AppContext, AsyncAppContext, Model, SharedString}; +use gpui::{App, AsyncApp, Entity, SharedString}; use language::{AutoindentMode, Buffer, BufferSnapshot}; use project::{Project, ProjectPath}; use std::{cmp, ops::Range, path::Path, sync::Arc}; use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point}; #[derive(Clone, Debug)] -pub(crate) struct AssistantPatch { +pub struct AssistantPatch { pub range: Range, pub title: SharedString, pub edits: Arc<[Result]>, @@ -17,13 +17,13 @@ pub(crate) struct AssistantPatch { } #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(crate) enum AssistantPatchStatus { +pub enum AssistantPatchStatus { Pending, Ready, } #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct AssistantEdit { +pub struct AssistantEdit { pub path: String, pub kind: AssistantEditKind, } @@ -55,8 +55,8 @@ pub enum AssistantEditKind { } #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct ResolvedPatch { - pub edit_groups: HashMap, Vec>, +pub struct ResolvedPatch { + pub edit_groups: HashMap, Vec>, pub errors: Vec, } @@ -74,7 +74,7 @@ pub struct ResolvedEdit { } #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct AssistantPatchResolutionError { +pub struct AssistantPatchResolutionError { pub edit_ix: usize, pub message: String, } @@ -121,7 +121,7 @@ impl SearchMatrix { } impl ResolvedPatch { - pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut AppContext) { + pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut App) { for (buffer, groups) in &self.edit_groups { let branch = editor.branch_buffer_for_base(buffer).unwrap(); Self::apply_edit_groups(groups, &branch, cx); @@ -129,11 +129,7 @@ impl ResolvedPatch { editor.recalculate_all_buffer_diffs(); } - fn apply_edit_groups( - groups: &Vec, - buffer: &Model, - cx: &mut AppContext, - ) { + fn apply_edit_groups(groups: &Vec, buffer: &Entity, cx: &mut App) { let mut edits = Vec::new(); for group in groups { for suggestion in &group.edits { @@ -232,9 +228,9 @@ impl AssistantEdit { pub async fn resolve( &self, - project: Model, - mut cx: AsyncAppContext, - ) -> Result<(Model, ResolvedEdit)> { + project: Entity, + mut cx: AsyncApp, + ) -> Result<(Entity, ResolvedEdit)> { let path = self.path.clone(); let kind = self.kind.clone(); let buffer = project @@ -425,11 +421,7 @@ impl AssistantEditKind { } impl AssistantPatch { - pub(crate) async fn resolve( - &self, - project: Model, - cx: &mut AsyncAppContext, - ) -> ResolvedPatch { + pub async fn resolve(&self, project: Entity, cx: &mut AsyncApp) -> ResolvedPatch { let mut resolve_tasks = Vec::new(); for (ix, edit) in self.edits.iter().enumerate() { if let Ok(edit) = edit.as_ref() { @@ -555,7 +547,7 @@ impl Eq for AssistantPatch {} #[cfg(test)] mod tests { use super::*; - use gpui::{AppContext, Context}; + use gpui::{App, AppContext as _}; use language::{ language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher, }; @@ -565,7 +557,7 @@ mod tests { use util::test::{generate_marked_text, marked_text_ranges}; #[gpui::test] - fn test_resolve_location(cx: &mut AppContext) { + fn test_resolve_location(cx: &mut App) { assert_location_resolution( concat!( " Lorem\n", @@ -636,7 +628,7 @@ mod tests { } #[gpui::test] - fn test_resolve_edits(cx: &mut AppContext) { + fn test_resolve_edits(cx: &mut App) { init_test(cx); assert_edits( @@ -902,7 +894,7 @@ mod tests { ); } - fn init_test(cx: &mut AppContext) { + fn init_test(cx: &mut App) { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); language::init(cx); @@ -912,13 +904,9 @@ mod tests { } #[track_caller] - fn assert_location_resolution( - text_with_expected_range: &str, - query: &str, - cx: &mut AppContext, - ) { + fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) { let (text, _) = marked_text_ranges(text_with_expected_range, false); - let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx)); + let buffer = cx.new(|cx| Buffer::local(text.clone(), cx)); let snapshot = buffer.read(cx).snapshot(); let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot); let text_with_actual_range = generate_marked_text(&text, &[range], false); @@ -930,10 +918,10 @@ mod tests { old_text: String, edits: Vec, new_text: String, - cx: &mut AppContext, + cx: &mut App, ) { let buffer = - cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx)); + cx.new(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx)); let snapshot = buffer.read(cx).snapshot(); let resolved_edits = edits .into_iter() diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant_context_editor/src/slash_command.rs similarity index 72% rename from crates/assistant/src/slash_command.rs rename to crates/assistant_context_editor/src/slash_command.rs index 793d2a950a86fb..6cecc9470c163a 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant_context_editor/src/slash_command.rs @@ -1,11 +1,10 @@ -use crate::assistant_panel::ContextEditor; +use crate::context_editor::ContextEditor; use anyhow::Result; -use assistant_slash_command::AfterCompletion; pub use assistant_slash_command::SlashCommand; -use assistant_slash_command::SlashCommandWorkingSet; +use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet}; use editor::{CompletionProvider, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; -use gpui::{Model, Task, ViewContext, WeakView, WindowContext}; +use gpui::{App, Context, Entity, Task, WeakEntity, Window}; use language::{Anchor, Buffer, Documentation, LanguageServerId, ToPoint}; use parking_lot::Mutex; use project::CompletionIntent; @@ -21,25 +20,18 @@ use std::{ }; use workspace::Workspace; -pub(crate) struct SlashCommandCompletionProvider { +pub struct SlashCommandCompletionProvider { cancel_flag: Mutex>, slash_commands: Arc, - editor: Option>, - workspace: Option>, -} - -pub(crate) struct SlashCommandLine { - /// The range within the line containing the command name. - pub name: Range, - /// Ranges within the line containing the command arguments. - pub arguments: Vec>, + editor: Option>, + workspace: Option>, } impl SlashCommandCompletionProvider { pub fn new( slash_commands: Arc, - editor: Option>, - workspace: Option>, + editor: Option>, + workspace: Option>, ) -> Self { Self { cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))), @@ -54,7 +46,8 @@ impl SlashCommandCompletionProvider { command_name: &str, command_range: Range, name_range: Range, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task>> { let slash_commands = self.slash_commands.clone(); let candidates = slash_commands @@ -66,7 +59,7 @@ impl SlashCommandCompletionProvider { let command_name = command_name.to_string(); let editor = self.editor.clone(); let workspace = self.workspace.clone(); - cx.spawn(|mut cx| async move { + window.spawn(cx, |mut cx| async move { let matches = match_strings( &candidates, &command_name, @@ -77,7 +70,7 @@ impl SlashCommandCompletionProvider { ) .await; - cx.update(|cx| { + cx.update(|_, cx| { matches .into_iter() .filter_map(|mat| { @@ -99,28 +92,31 @@ impl SlashCommandCompletionProvider { let editor = editor.clone(); let workspace = workspace.clone(); Arc::new( - move |intent: CompletionIntent, cx: &mut WindowContext| { - if !requires_argument - && (!accepts_arguments || intent.is_complete()) - { - editor - .update(cx, |editor, cx| { - editor.run_command( - command_range.clone(), - &command_name, - &[], - true, - workspace.clone(), - cx, - ); - }) - .ok(); - false - } else { - requires_argument || accepts_arguments - } - }, - ) as Arc<_> + move |intent: CompletionIntent, + window: &mut Window, + cx: &mut App| { + if !requires_argument + && (!accepts_arguments || intent.is_complete()) + { + editor + .update(cx, |editor, cx| { + editor.run_command( + command_range.clone(), + &command_name, + &[], + true, + workspace.clone(), + window, + cx, + ); + }) + .ok(); + false + } else { + requires_argument || accepts_arguments + } + }, + ) as Arc<_> }); Some(project::Completion { old_range: name_range.clone(), @@ -138,6 +134,7 @@ impl SlashCommandCompletionProvider { }) } + #[allow(clippy::too_many_arguments)] fn complete_command_argument( &self, command_name: &str, @@ -145,7 +142,8 @@ impl SlashCommandCompletionProvider { command_range: Range, argument_range: Range, last_argument_range: Range, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task>> { let new_cancel_flag = Arc::new(AtomicBool::new(false)); let mut flag = self.cancel_flag.lock(); @@ -156,6 +154,7 @@ impl SlashCommandCompletionProvider { arguments, new_cancel_flag.clone(), self.workspace.clone(), + window, cx, ); let command_name: Arc = command_name.into(); @@ -183,7 +182,9 @@ impl SlashCommandCompletionProvider { let command_range = command_range.clone(); let command_name = command_name.clone(); - move |intent: CompletionIntent, cx: &mut WindowContext| { + move |intent: CompletionIntent, + window: &mut Window, + cx: &mut App| { if new_argument.after_completion.run() || intent.is_complete() { @@ -195,6 +196,7 @@ impl SlashCommandCompletionProvider { &completed_arguments, true, workspace.clone(), + window, cx, ); }) @@ -238,10 +240,11 @@ impl SlashCommandCompletionProvider { impl CompletionProvider for SlashCommandCompletionProvider { fn completions( &self, - buffer: &Model, + buffer: &Entity, buffer_position: Anchor, _: editor::CompletionContext, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task>> { let Some((name, arguments, command_range, last_argument_range)) = buffer.update(cx, |buffer, _cx| { @@ -296,30 +299,31 @@ impl CompletionProvider for SlashCommandCompletionProvider { command_range, argument_range, last_argument_range, + window, cx, ) } else { - self.complete_command_name(&name, command_range, last_argument_range, cx) + self.complete_command_name(&name, command_range, last_argument_range, window, cx) } } fn resolve_completions( &self, - _: Model, + _: Entity, _: Vec, _: Rc>>, - _: &mut ViewContext, + _: &mut Context, ) -> Task> { Task::ready(Ok(true)) } fn is_completion_trigger( &self, - buffer: &Model, + buffer: &Entity, position: language::Anchor, _text: &str, _trigger_in_words: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool { let buffer = buffer.read(cx); let position = position.to_point(buffer); @@ -336,57 +340,3 @@ impl CompletionProvider for SlashCommandCompletionProvider { false } } - -impl SlashCommandLine { - pub(crate) fn parse(line: &str) -> Option { - let mut call: Option = None; - let mut ix = 0; - for c in line.chars() { - let next_ix = ix + c.len_utf8(); - if let Some(call) = &mut call { - // The command arguments start at the first non-whitespace character - // after the command name, and continue until the end of the line. - if let Some(argument) = call.arguments.last_mut() { - if c.is_whitespace() { - if (*argument).is_empty() { - argument.start = next_ix; - argument.end = next_ix; - } else { - argument.end = ix; - call.arguments.push(next_ix..next_ix); - } - } else { - argument.end = next_ix; - } - } - // The command name ends at the first whitespace character. - else if !call.name.is_empty() { - if c.is_whitespace() { - call.arguments = vec![next_ix..next_ix]; - } else { - call.name.end = next_ix; - } - } - // The command name must begin with a letter. - else if c.is_alphabetic() { - call.name.end = next_ix; - } else { - return None; - } - } - // Commands start with a slash. - else if c == '/' { - call = Some(SlashCommandLine { - name: next_ix..next_ix, - arguments: Vec::new(), - }); - } - // The line can't contain anything before the slash except for whitespace. - else if !c.is_whitespace() { - return None; - } - ix = next_ix; - } - call - } -} diff --git a/crates/assistant/src/slash_command_picker.rs b/crates/assistant_context_editor/src/slash_command_picker.rs similarity index 85% rename from crates/assistant/src/slash_command_picker.rs rename to crates/assistant_context_editor/src/slash_command_picker.rs index d5840f41963b69..373e5f09ddcb7f 100644 --- a/crates/assistant/src/slash_command_picker.rs +++ b/crates/assistant_context_editor/src/slash_command_picker.rs @@ -1,16 +1,16 @@ use std::sync::Arc; use assistant_slash_command::SlashCommandWorkingSet; -use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView}; +use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakEntity}; use picker::{Picker, PickerDelegate, PickerEditorPosition}; use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip}; -use crate::assistant_panel::ContextEditor; +use crate::context_editor::ContextEditor; #[derive(IntoElement)] pub(super) struct SlashCommandSelector { working_set: Arc, - active_context_editor: WeakView, + active_context_editor: WeakEntity, trigger: T, } @@ -27,8 +27,8 @@ enum SlashCommandEntry { Info(SlashCommandInfo), Advert { name: SharedString, - renderer: fn(&mut WindowContext) -> AnyElement, - on_confirm: fn(&mut WindowContext), + renderer: fn(&mut Window, &mut App) -> AnyElement, + on_confirm: fn(&mut Window, &mut App), }, } @@ -44,14 +44,14 @@ impl AsRef for SlashCommandEntry { pub(crate) struct SlashCommandDelegate { all_commands: Vec, filtered_commands: Vec, - active_context_editor: WeakView, + active_context_editor: WeakEntity, selected_index: usize, } impl SlashCommandSelector { pub(crate) fn new( working_set: Arc, - active_context_editor: WeakView, + active_context_editor: WeakEntity, trigger: T, ) -> Self { SlashCommandSelector { @@ -73,18 +73,23 @@ impl PickerDelegate for SlashCommandDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1)); cx.notify(); } - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Select a command...".into() } - fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { let all_commands = self.all_commands.clone(); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { let filtered_commands = cx .background_executor() .spawn(async move { @@ -104,9 +109,9 @@ impl PickerDelegate for SlashCommandDelegate { }) .await; - this.update(&mut cx, |this, cx| { + this.update_in(&mut cx, |this, window, cx| { this.delegate.filtered_commands = filtered_commands; - this.delegate.set_selected_index(0, cx); + this.delegate.set_selected_index(0, window, cx); cx.notify(); }) .ok(); @@ -139,25 +144,25 @@ impl PickerDelegate for SlashCommandDelegate { ret } - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { if let Some(command) = self.filtered_commands.get(self.selected_index) { match command { SlashCommandEntry::Info(info) => { self.active_context_editor .update(cx, |context_editor, cx| { - context_editor.insert_command(&info.name, cx) + context_editor.insert_command(&info.name, window, cx) }) .ok(); } SlashCommandEntry::Advert { on_confirm, .. } => { - on_confirm(cx); + on_confirm(window, cx); } } cx.emit(DismissEvent); } } - fn dismissed(&mut self, _cx: &mut ViewContext>) {} + fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} fn editor_position(&self) -> PickerEditorPosition { PickerEditorPosition::End @@ -167,7 +172,8 @@ impl PickerDelegate for SlashCommandDelegate { &self, ix: usize, selected: bool, - cx: &mut ViewContext>, + window: &mut Window, + cx: &mut Context>, ) -> Option { let command_info = self.filtered_commands.get(ix)?; @@ -179,7 +185,7 @@ impl PickerDelegate for SlashCommandDelegate { .toggle_state(selected) .tooltip({ let description = info.description.clone(); - move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into() + move |_, cx| cx.new(|_| Tooltip::new(description.clone())).into() }) .child( v_flex() @@ -229,14 +235,14 @@ impl PickerDelegate for SlashCommandDelegate { .inset(true) .spacing(ListItemSpacing::Dense) .toggle_state(selected) - .child(renderer(cx)), + .child(renderer(window, cx)), ), } } } impl RenderOnce for SlashCommandSelector { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let all_models = self .working_set .featured_command_names(cx) @@ -259,7 +265,7 @@ impl RenderOnce for SlashCommandSelector { }) .chain([SlashCommandEntry::Advert { name: "create-your-command".into(), - renderer: |cx| { + renderer: |_, cx| { v_flex() .w_full() .child( @@ -293,7 +299,7 @@ impl RenderOnce for SlashCommandSelector { ) .into_any_element() }, - on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"), + on_confirm: |_, cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"), }]) .collect::>(); @@ -304,8 +310,9 @@ impl RenderOnce for SlashCommandSelector { selected_index: 0, }; - let picker_view = cx.new_view(|cx| { - let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())); + let picker_view = cx.new(|cx| { + let picker = + Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into())); picker }); @@ -314,7 +321,7 @@ impl RenderOnce for SlashCommandSelector { .update(cx, |this, _| this.slash_menu_handle.clone()) .ok(); PopoverMenu::new("model-switcher") - .menu(move |_cx| Some(picker_view.clone())) + .menu(move |_window, _cx| Some(picker_view.clone())) .trigger(self.trigger) .attach(gpui::Corner::TopLeft) .anchor(gpui::Corner::BottomLeft) diff --git a/crates/assistant_settings/Cargo.toml b/crates/assistant_settings/Cargo.toml index 32ebb6a9593fdc..4398f75ef92a71 100644 --- a/crates/assistant_settings/Cargo.toml +++ b/crates/assistant_settings/Cargo.toml @@ -21,6 +21,7 @@ lmstudio = { workspace = true, features = ["schemars"] } log.workspace = true ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } +deepseek = { workspace = true, features = ["schemars"] } schemars.workspace = true serde.workspace = true settings.workspace = true diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs index c98182b24d1cd6..3193e09ae54f2b 100644 --- a/crates/assistant_settings/src/assistant_settings.rs +++ b/crates/assistant_settings/src/assistant_settings.rs @@ -2,8 +2,9 @@ use std::sync::Arc; use ::open_ai::Model as OpenAiModel; use anthropic::Model as AnthropicModel; +use deepseek::Model as DeepseekModel; use feature_flags::FeatureFlagAppExt; -use gpui::{AppContext, Pixels}; +use gpui::{App, Pixels}; use language_model::{CloudModel, LanguageModel}; use lmstudio::Model as LmStudioModel; use ollama::Model as OllamaModel; @@ -46,6 +47,11 @@ pub enum AssistantProviderContentV1 { default_model: Option, api_url: Option, }, + #[serde(rename = "deepseek")] + DeepSeek { + default_model: Option, + api_url: Option, + }, } #[derive(Debug, Default)] @@ -62,7 +68,7 @@ pub struct AssistantSettings { } impl AssistantSettings { - pub fn are_live_diffs_enabled(&self, cx: &AppContext) -> bool { + pub fn are_live_diffs_enabled(&self, cx: &App) -> bool { cx.is_staff() || self.enable_experimental_live_diffs } } @@ -149,6 +155,12 @@ impl AssistantSettingsContent { model: model.id().to_string(), }) } + AssistantProviderContentV1::DeepSeek { default_model, .. } => { + default_model.map(|model| LanguageModelSelection { + provider: "deepseek".to_string(), + model: model.id().to_string(), + }) + } }), inline_alternatives: None, enable_experimental_live_diffs: None, @@ -253,6 +265,18 @@ impl AssistantSettingsContent { available_models, }); } + "deepseek" => { + let api_url = match &settings.provider { + Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => { + api_url.clone() + } + _ => None, + }; + settings.provider = Some(AssistantProviderContentV1::DeepSeek { + default_model: DeepseekModel::from_id(&model).ok(), + api_url, + }); + } _ => {} }, VersionedAssistantSettingsContent::V2(settings) => { @@ -341,6 +365,7 @@ fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema: "openai".into(), "zed.dev".into(), "copilot_chat".into(), + "deepseek".into(), ]), ..Default::default() } @@ -380,7 +405,7 @@ pub struct AssistantSettingsContentV1 { default_height: Option, /// The provider of the assistant service. /// - /// This can be "openai", "anthropic", "ollama", "lmstudio", "zed.dev" + /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev" /// each with their respective default models and configurations. provider: Option, } @@ -422,7 +447,7 @@ impl Settings for AssistantSettings { fn load( sources: SettingsSources, - _: &mut gpui::AppContext, + _: &mut gpui::App, ) -> anyhow::Result { let mut settings = AssistantSettings::default(); diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 75e9b454676b09..f3811dfa8c140f 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -8,7 +8,7 @@ pub use crate::slash_command_working_set::*; use anyhow::Result; use futures::stream::{self, BoxStream}; use futures::StreamExt; -use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext}; +use gpui::{App, SharedString, Task, WeakEntity, Window}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; pub use language_model::Role; use serde::{Deserialize, Serialize}; @@ -18,7 +18,7 @@ use std::{ }; use workspace::{ui::IconName, Workspace}; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { SlashCommandRegistry::default_global(cx); extension_slash_command::init(cx); } @@ -71,7 +71,7 @@ pub trait SlashCommand: 'static + Send + Sync { fn icon(&self) -> IconName { IconName::Slash } - fn label(&self, _cx: &AppContext) -> CodeLabel { + fn label(&self, _cx: &App) -> CodeLabel { CodeLabel::plain(self.name(), None) } fn description(&self) -> String; @@ -80,35 +80,32 @@ pub trait SlashCommand: 'static + Send + Sync { self: Arc, arguments: &[String], cancel: Arc, - workspace: Option>, - cx: &mut WindowContext, + workspace: Option>, + window: &mut Window, + cx: &mut App, ) -> Task>>; fn requires_argument(&self) -> bool; fn accepts_arguments(&self) -> bool { self.requires_argument() } + #[allow(clippy::too_many_arguments)] fn run( self: Arc, arguments: &[String], context_slash_command_output_sections: &[SlashCommandOutputSection], context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, // TODO: We're just using the `LspAdapterDelegate` here because that is // what the extension API is already expecting. // // It may be that `LspAdapterDelegate` needs a more general name, or // perhaps another kind of delegate is needed here. delegate: Option>, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task; } -pub type RenderFoldPlaceholder = Arc< - dyn Send - + Sync - + Fn(ElementId, Arc, &mut WindowContext) -> AnyElement, ->; - #[derive(Debug, PartialEq)] pub enum SlashCommandContent { Text { @@ -268,6 +265,67 @@ impl SlashCommandOutputSection { } } +pub struct SlashCommandLine { + /// The range within the line containing the command name. + pub name: Range, + /// Ranges within the line containing the command arguments. + pub arguments: Vec>, +} + +impl SlashCommandLine { + pub fn parse(line: &str) -> Option { + let mut call: Option = None; + let mut ix = 0; + for c in line.chars() { + let next_ix = ix + c.len_utf8(); + if let Some(call) = &mut call { + // The command arguments start at the first non-whitespace character + // after the command name, and continue until the end of the line. + if let Some(argument) = call.arguments.last_mut() { + if c.is_whitespace() { + if (*argument).is_empty() { + argument.start = next_ix; + argument.end = next_ix; + } else { + argument.end = ix; + call.arguments.push(next_ix..next_ix); + } + } else { + argument.end = next_ix; + } + } + // The command name ends at the first whitespace character. + else if !call.name.is_empty() { + if c.is_whitespace() { + call.arguments = vec![next_ix..next_ix]; + } else { + call.name.end = next_ix; + } + } + // The command name must begin with a letter. + else if c.is_alphabetic() { + call.name.end = next_ix; + } else { + return None; + } + } + // Commands start with a slash. + else if c == '/' { + call = Some(SlashCommandLine { + name: next_ix..next_ix, + arguments: Vec::new(), + }); + } + // The line can't contain anything before the slash except for whitespace. + else if !c.is_whitespace() { + return None; + } + ix = next_ix; + } + call + } +} + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs index 2279f93b1c942f..1718a750e4ef24 100644 --- a/crates/assistant_slash_command/src/extension_slash_command.rs +++ b/crates/assistant_slash_command/src/extension_slash_command.rs @@ -4,7 +4,7 @@ use std::sync::{atomic::AtomicBool, Arc}; use anyhow::Result; use async_trait::async_trait; use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate}; -use gpui::{AppContext, Task, WeakView, WindowContext}; +use gpui::{App, Task, WeakEntity, Window}; use language::{BufferSnapshot, LspAdapterDelegate}; use ui::prelude::*; use workspace::Workspace; @@ -14,7 +14,7 @@ use crate::{ SlashCommandRegistry, SlashCommandResult, }; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { let proxy = ExtensionHostProxy::default_global(cx); proxy.register_slash_command_proxy(SlashCommandRegistryProxy { slash_command_registry: SlashCommandRegistry::global(cx), @@ -97,8 +97,9 @@ impl SlashCommand for ExtensionSlashCommand { self: Arc, arguments: &[String], _cancel: Arc, - _workspace: Option>, - cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + cx: &mut App, ) -> Task>> { let command = self.command.clone(); let arguments = arguments.to_owned(); @@ -127,9 +128,10 @@ impl SlashCommand for ExtensionSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - _workspace: WeakView, + _workspace: WeakEntity, delegate: Option>, - cx: &mut WindowContext, + _window: &mut Window, + cx: &mut App, ) -> Task { let command = self.command.clone(); let arguments = arguments.to_owned(); diff --git a/crates/assistant_slash_command/src/slash_command_registry.rs b/crates/assistant_slash_command/src/slash_command_registry.rs index d8a4014cfc903d..258869840b583e 100644 --- a/crates/assistant_slash_command/src/slash_command_registry.rs +++ b/crates/assistant_slash_command/src/slash_command_registry.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use collections::{BTreeSet, HashMap}; use derive_more::{Deref, DerefMut}; use gpui::Global; -use gpui::{AppContext, ReadGlobal}; +use gpui::{App, ReadGlobal}; use parking_lot::RwLock; use crate::SlashCommand; @@ -26,14 +26,14 @@ pub struct SlashCommandRegistry { impl SlashCommandRegistry { /// Returns the global [`SlashCommandRegistry`]. - pub fn global(cx: &AppContext) -> Arc { + pub fn global(cx: &App) -> Arc { GlobalSlashCommandRegistry::global(cx).0.clone() } /// Returns the global [`SlashCommandRegistry`]. /// /// Inserts a default [`SlashCommandRegistry`] if one does not yet exist. - pub fn default_global(cx: &mut AppContext) -> Arc { + pub fn default_global(cx: &mut App) -> Arc { cx.default_global::().0.clone() } diff --git a/crates/assistant_slash_command/src/slash_command_working_set.rs b/crates/assistant_slash_command/src/slash_command_working_set.rs index 83f090a4e5d188..b920e1d2177b46 100644 --- a/crates/assistant_slash_command/src/slash_command_working_set.rs +++ b/crates/assistant_slash_command/src/slash_command_working_set.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use collections::HashMap; -use gpui::AppContext; +use gpui::App; use parking_lot::Mutex; use crate::{SlashCommand, SlashCommandRegistry}; @@ -23,7 +23,7 @@ struct WorkingSetState { } impl SlashCommandWorkingSet { - pub fn command(&self, name: &str, cx: &AppContext) -> Option> { + pub fn command(&self, name: &str, cx: &App) -> Option> { self.state .lock() .context_server_commands_by_name @@ -32,7 +32,7 @@ impl SlashCommandWorkingSet { .or_else(|| SlashCommandRegistry::global(cx).command(name)) } - pub fn command_names(&self, cx: &AppContext) -> Vec> { + pub fn command_names(&self, cx: &App) -> Vec> { let mut command_names = SlashCommandRegistry::global(cx).command_names(); command_names.extend( self.state @@ -45,7 +45,7 @@ impl SlashCommandWorkingSet { command_names } - pub fn featured_command_names(&self, cx: &AppContext) -> Vec> { + pub fn featured_command_names(&self, cx: &App) -> Vec> { SlashCommandRegistry::global(cx).featured_command_names() } diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml index 5987e85cada5a5..25d762223dede4 100644 --- a/crates/assistant_slash_commands/Cargo.toml +++ b/crates/assistant_slash_commands/Cargo.toml @@ -45,6 +45,7 @@ toml.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +worktree.workspace = true [dev-dependencies] env_logger.workspace = true diff --git a/crates/assistant_slash_commands/src/assistant_slash_commands.rs b/crates/assistant_slash_commands/src/assistant_slash_commands.rs index 8e124412b6e9bc..ea1ae5ab5a5604 100644 --- a/crates/assistant_slash_commands/src/assistant_slash_commands.rs +++ b/crates/assistant_slash_commands/src/assistant_slash_commands.rs @@ -17,7 +17,7 @@ mod symbols_command; mod tab_command; mod terminal_command; -use gpui::AppContext; +use gpui::App; use language::{CodeLabel, HighlightId}; use ui::ActiveTheme as _; @@ -40,11 +40,7 @@ pub use crate::symbols_command::*; pub use crate::tab_command::*; pub use crate::terminal_command::*; -pub fn create_label_for_command( - command_name: &str, - arguments: &[&str], - cx: &AppContext, -) -> CodeLabel { +pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel { let mut label = CodeLabel::default(); label.push_str(command_name, None); label.push_str(" ", None); diff --git a/crates/assistant_slash_commands/src/auto_command.rs b/crates/assistant_slash_commands/src/auto_command.rs index 40b627609d548a..6b9c777ff716d3 100644 --- a/crates/assistant_slash_commands/src/auto_command.rs +++ b/crates/assistant_slash_commands/src/auto_command.rs @@ -5,7 +5,7 @@ use assistant_slash_command::{ }; use feature_flags::FeatureFlag; use futures::StreamExt; -use gpui::{AppContext, AsyncAppContext, AsyncWindowContext, Task, WeakView, WindowContext}; +use gpui::{App, AsyncApp, Task, WeakEntity, Window}; use language::{CodeLabel, LspAdapterDelegate}; use language_model::{ LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, @@ -45,7 +45,7 @@ impl SlashCommand for AutoCommand { self.description() } - fn label(&self, cx: &AppContext) -> CodeLabel { + fn label(&self, cx: &App) -> CodeLabel { create_label_for_command("auto", &["--prompt"], cx) } @@ -53,8 +53,9 @@ impl SlashCommand for AutoCommand { self: Arc, _arguments: &[String], _cancel: Arc, - workspace: Option>, - cx: &mut WindowContext, + workspace: Option>, + _window: &mut Window, + cx: &mut App, ) -> Task>> { // There's no autocomplete for a prompt, since it's arbitrary text. // However, we can use this opportunity to kick off a drain of the backlog. @@ -74,9 +75,9 @@ impl SlashCommand for AutoCommand { return Task::ready(Err(anyhow!("No project indexer, cannot use /auto"))); }; - let cx: &mut AppContext = cx; + let cx: &mut App = cx; - cx.spawn(|cx: gpui::AsyncAppContext| async move { + cx.spawn(|cx: gpui::AsyncApp| async move { let task = project_index.read_with(&cx, |project_index, cx| { project_index.flush_summary_backlogs(cx) })?; @@ -96,9 +97,10 @@ impl SlashCommand for AutoCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: language::BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); @@ -115,7 +117,7 @@ impl SlashCommand for AutoCommand { return Task::ready(Err(anyhow!("no project indexer"))); }; - let task = cx.spawn(|cx: AsyncWindowContext| async move { + let task = window.spawn(cx, |cx| async move { let summaries = project_index .read_with(&cx, |project_index, cx| project_index.all_summaries(cx))? .await?; @@ -187,7 +189,7 @@ struct CommandToRun { async fn commands_for_summaries( summaries: &[FileSummary], original_prompt: &str, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> Result> { if summaries.is_empty() { log::warn!("Inferring no context because there were no summaries available."); diff --git a/crates/assistant_slash_commands/src/cargo_workspace_command.rs b/crates/assistant_slash_commands/src/cargo_workspace_command.rs index eed3b60761deb5..157c2063ed29b5 100644 --- a/crates/assistant_slash_commands/src/cargo_workspace_command.rs +++ b/crates/assistant_slash_commands/src/cargo_workspace_command.rs @@ -1,10 +1,10 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, }; use fs::Fs; -use gpui::{AppContext, Model, Task, WeakView}; +use gpui::{App, Entity, Task, WeakEntity}; use language::{BufferSnapshot, LspAdapterDelegate}; use project::{Project, ProjectPath}; use std::{ @@ -76,7 +76,7 @@ impl CargoWorkspaceSlashCommand { Ok(message) } - fn path_to_cargo_toml(project: Model, cx: &mut AppContext) -> Option> { + fn path_to_cargo_toml(project: Entity, cx: &mut App) -> Option> { let worktree = project.read(cx).worktrees(cx).next()?; let worktree = worktree.read(cx); let entry = worktree.entry_for_path("Cargo.toml")?; @@ -107,8 +107,9 @@ impl SlashCommand for CargoWorkspaceSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } @@ -122,9 +123,10 @@ impl SlashCommand for CargoWorkspaceSlashCommand { _arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _window: &mut Window, + cx: &mut App, ) -> Task { let output = workspace.update(cx, |workspace, cx| { let project = workspace.project().clone(); diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index f9baf7e67590fa..42f814791dce12 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -8,7 +8,7 @@ use context_server::{ manager::{ContextServer, ContextServerManager}, types::Prompt, }; -use gpui::{AppContext, Model, Task, WeakView, WindowContext}; +use gpui::{App, Entity, Task, WeakEntity, Window}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use std::sync::atomic::AtomicBool; use std::sync::Arc; @@ -19,14 +19,14 @@ use workspace::Workspace; use crate::create_label_for_command; pub struct ContextServerSlashCommand { - server_manager: Model, + server_manager: Entity, server_id: Arc, prompt: Prompt, } impl ContextServerSlashCommand { pub fn new( - server_manager: Model, + server_manager: Entity, server: &Arc, prompt: Prompt, ) -> Self { @@ -43,7 +43,7 @@ impl SlashCommand for ContextServerSlashCommand { self.prompt.name.clone() } - fn label(&self, cx: &AppContext) -> language::CodeLabel { + fn label(&self, cx: &App) -> language::CodeLabel { let mut parts = vec![self.prompt.name.as_str()]; if let Some(args) = &self.prompt.arguments { if let Some(arg) = args.first() { @@ -77,8 +77,9 @@ impl SlashCommand for ContextServerSlashCommand { self: Arc, arguments: &[String], _cancel: Arc, - _workspace: Option>, - cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + cx: &mut App, ) -> Task>> { let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else { return Task::ready(Err(anyhow!("Failed to complete argument"))); @@ -128,9 +129,10 @@ impl SlashCommand for ContextServerSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - _workspace: WeakView, + _workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _window: &mut Window, + cx: &mut App, ) -> Task { let server_id = self.server_id.clone(); let prompt_name = self.prompt.name.clone(); diff --git a/crates/assistant_slash_commands/src/default_command.rs b/crates/assistant_slash_commands/src/default_command.rs index 6fac51143ada83..6881f89a9e2279 100644 --- a/crates/assistant_slash_commands/src/default_command.rs +++ b/crates/assistant_slash_commands/src/default_command.rs @@ -3,7 +3,7 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, }; -use gpui::{Task, WeakView}; +use gpui::{Task, WeakEntity}; use language::{BufferSnapshot, LspAdapterDelegate}; use prompt_library::PromptStore; use std::{ @@ -36,8 +36,9 @@ impl SlashCommand for DefaultSlashCommand { self: Arc, _arguments: &[String], _cancellation_flag: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } @@ -47,9 +48,10 @@ impl SlashCommand for DefaultSlashCommand { _arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - _workspace: WeakView, + _workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _window: &mut Window, + cx: &mut App, ) -> Task { let store = PromptStore::global(cx); cx.background_executor().spawn(async move { diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs index ba6c4dde39e1d0..0cbd508d1982e5 100644 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ b/crates/assistant_slash_commands/src/delta_command.rs @@ -6,7 +6,7 @@ use assistant_slash_command::{ }; use collections::HashSet; use futures::future; -use gpui::{Task, WeakView, WindowContext}; +use gpui::{App, Task, WeakEntity, Window}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::sync::{atomic::AtomicBool, Arc}; use text::OffsetRangeExt; @@ -40,8 +40,9 @@ impl SlashCommand for DeltaSlashCommand { self: Arc, _arguments: &[String], _cancellation_flag: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } @@ -51,9 +52,10 @@ impl SlashCommand for DeltaSlashCommand { _arguments: &[String], context_slash_command_output_sections: &[SlashCommandOutputSection], context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, delegate: Option>, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task { let mut paths = HashSet::default(); let mut file_command_old_outputs = Vec::new(); @@ -77,6 +79,7 @@ impl SlashCommand for DeltaSlashCommand { context_buffer.clone(), workspace.clone(), delegate.clone(), + window, cx, )); } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index e73d58c7a7df0d..076fb3b4cc38e3 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -4,7 +4,7 @@ use assistant_slash_command::{ SlashCommandResult, }; use fuzzy::{PathMatch, StringMatchCandidate}; -use gpui::{AppContext, Model, Task, View, WeakView}; +use gpui::{App, Entity, Task, WeakEntity}; use language::{ Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate, OffsetRangeExt, ToOffset, @@ -30,8 +30,8 @@ impl DiagnosticsSlashCommand { &self, query: String, cancellation_flag: Arc, - workspace: &View, - cx: &mut AppContext, + workspace: &Entity, + cx: &mut App, ) -> Task> { if query.is_empty() { let workspace = workspace.read(cx); @@ -90,7 +90,7 @@ impl SlashCommand for DiagnosticsSlashCommand { "diagnostics".into() } - fn label(&self, cx: &AppContext) -> language::CodeLabel { + fn label(&self, cx: &App) -> language::CodeLabel { create_label_for_command("diagnostics", &[INCLUDE_WARNINGS_ARGUMENT], cx) } @@ -118,8 +118,9 @@ impl SlashCommand for DiagnosticsSlashCommand { self: Arc, arguments: &[String], cancellation_flag: Arc, - workspace: Option>, - cx: &mut WindowContext, + workspace: Option>, + _: &mut Window, + cx: &mut App, ) -> Task>> { let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else { return Task::ready(Err(anyhow!("workspace was dropped"))); @@ -172,9 +173,10 @@ impl SlashCommand for DiagnosticsSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow!("workspace was dropped"))); @@ -184,7 +186,7 @@ impl SlashCommand for DiagnosticsSlashCommand { let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx); - cx.spawn(move |_| async move { + window.spawn(cx, move |_| async move { task.await? .map(|output| output.to_event_stream()) .ok_or_else(|| anyhow!("No diagnostics found")) @@ -223,9 +225,9 @@ impl Options { } fn collect_diagnostics( - project: Model, + project: Entity, options: Options, - cx: &mut AppContext, + cx: &mut App, ) -> Task>> { let error_source = if let Some(path_matcher) = &options.path_matcher { debug_assert_eq!(path_matcher.sources().len(), 1); @@ -301,7 +303,7 @@ fn collect_diagnostics( .await .log_err() { - let snapshot = cx.read_model(&buffer, |buffer, _| buffer.snapshot())?; + let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot())?; collect_buffer_diagnostics(&mut output, &snapshot, options.include_warnings); } diff --git a/crates/assistant_slash_commands/src/docs_command.rs b/crates/assistant_slash_commands/src/docs_command.rs index 007abeb909c069..523534db92969a 100644 --- a/crates/assistant_slash_commands/src/docs_command.rs +++ b/crates/assistant_slash_commands/src/docs_command.rs @@ -8,7 +8,7 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, }; -use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView}; +use gpui::{App, BackgroundExecutor, Entity, Task, WeakEntity}; use indexed_docs::{ DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName, ProviderId, @@ -24,7 +24,7 @@ pub struct DocsSlashCommand; impl DocsSlashCommand { pub const NAME: &'static str = "docs"; - fn path_to_cargo_toml(project: Model, cx: &mut AppContext) -> Option> { + fn path_to_cargo_toml(project: Entity, cx: &mut App) -> Option> { let worktree = project.read(cx).worktrees(cx).next()?; let worktree = worktree.read(cx); let entry = worktree.entry_for_path("Cargo.toml")?; @@ -43,8 +43,8 @@ impl DocsSlashCommand { /// access the workspace so we can read the project. fn ensure_rust_doc_providers_are_registered( &self, - workspace: Option>, - cx: &mut AppContext, + workspace: Option>, + cx: &mut App, ) { let indexed_docs_registry = IndexedDocsRegistry::global(cx); if indexed_docs_registry @@ -164,8 +164,9 @@ impl SlashCommand for DocsSlashCommand { self: Arc, arguments: &[String], _cancel: Arc, - workspace: Option>, - cx: &mut WindowContext, + workspace: Option>, + _: &mut Window, + cx: &mut App, ) -> Task>> { self.ensure_rust_doc_providers_are_registered(workspace, cx); @@ -272,9 +273,10 @@ impl SlashCommand for DocsSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - _workspace: WeakView, + _workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task { if arguments.is_empty() { return Task::ready(Err(anyhow!("missing an argument"))); diff --git a/crates/assistant_slash_commands/src/fetch_command.rs b/crates/assistant_slash_commands/src/fetch_command.rs index b6b7dd57139142..142773891b4f61 100644 --- a/crates/assistant_slash_commands/src/fetch_command.rs +++ b/crates/assistant_slash_commands/src/fetch_command.rs @@ -9,7 +9,7 @@ use assistant_slash_command::{ SlashCommandResult, }; use futures::AsyncReadExt; -use gpui::{Task, WeakView}; +use gpui::{Task, WeakEntity}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler}; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::{BufferSnapshot, LspAdapterDelegate}; @@ -124,8 +124,9 @@ impl SlashCommand for FetchSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Ok(Vec::new())) } @@ -135,9 +136,10 @@ impl SlashCommand for FetchSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task { let Some(argument) = arguments.first() else { return Task::ready(Err(anyhow!("missing URL"))); diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index ce2355afcbf523..d898d82bc3f538 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -6,7 +6,7 @@ use assistant_slash_command::{ use futures::channel::mpsc; use futures::Stream; use fuzzy::PathMatch; -use gpui::{AppContext, Model, Task, View, WeakView}; +use gpui::{App, Entity, Task, WeakEntity}; use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate}; use project::{PathMatchCandidateSet, Project}; use serde::{Deserialize, Serialize}; @@ -20,6 +20,7 @@ use std::{ use ui::prelude::*; use util::ResultExt; use workspace::Workspace; +use worktree::ChildEntriesOptions; pub struct FileSlashCommand; @@ -28,8 +29,8 @@ impl FileSlashCommand { &self, query: String, cancellation_flag: Arc, - workspace: &View, - cx: &mut AppContext, + workspace: &Entity, + cx: &mut App, ) -> Task> { if query.is_empty() { let workspace = workspace.read(cx); @@ -42,7 +43,13 @@ impl FileSlashCommand { .chain(project.worktrees(cx).flat_map(|worktree| { let worktree = worktree.read(cx); let id = worktree.id(); - worktree.child_entries(Path::new("")).map(move |entry| { + let options = ChildEntriesOptions { + include_files: true, + include_dirs: true, + include_ignored: false, + }; + let entries = worktree.child_entries_with_options(Path::new(""), options); + entries.map(move |entry| { ( project::ProjectPath { worktree_id: id, @@ -134,8 +141,9 @@ impl SlashCommand for FileSlashCommand { self: Arc, arguments: &[String], cancellation_flag: Arc, - workspace: Option>, - cx: &mut WindowContext, + workspace: Option>, + _: &mut Window, + cx: &mut App, ) -> Task>> { let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else { return Task::ready(Err(anyhow!("workspace was dropped"))); @@ -187,9 +195,10 @@ impl SlashCommand for FileSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow!("workspace was dropped"))); @@ -209,9 +218,9 @@ impl SlashCommand for FileSlashCommand { } fn collect_files( - project: Model, + project: Entity, glob_inputs: &[String], - cx: &mut AppContext, + cx: &mut App, ) -> impl Stream> { let Ok(matchers) = glob_inputs .into_iter() diff --git a/crates/assistant_slash_commands/src/now_command.rs b/crates/assistant_slash_commands/src/now_command.rs index 2db21a72acaa4f..6d4ae75dd7c650 100644 --- a/crates/assistant_slash_commands/src/now_command.rs +++ b/crates/assistant_slash_commands/src/now_command.rs @@ -7,7 +7,7 @@ use assistant_slash_command::{ SlashCommandResult, }; use chrono::Local; -use gpui::{Task, WeakView}; +use gpui::{Task, WeakEntity}; use language::{BufferSnapshot, LspAdapterDelegate}; use ui::prelude::*; use workspace::Workspace; @@ -35,8 +35,9 @@ impl SlashCommand for NowSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Ok(Vec::new())) } @@ -46,9 +47,10 @@ impl SlashCommand for NowSlashCommand { _arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - _workspace: WeakView, + _workspace: WeakEntity, _delegate: Option>, - _cx: &mut WindowContext, + _window: &mut Window, + _cx: &mut App, ) -> Task { let now = Local::now(); let text = format!("Today is {now}.", now = now.to_rfc2822()); diff --git a/crates/assistant_slash_commands/src/project_command.rs b/crates/assistant_slash_commands/src/project_command.rs index bcef7e2c086291..a15dc86e280f6e 100644 --- a/crates/assistant_slash_commands/src/project_command.rs +++ b/crates/assistant_slash_commands/src/project_command.rs @@ -10,7 +10,7 @@ use assistant_slash_command::{ SlashCommandResult, }; use feature_flags::FeatureFlag; -use gpui::{AppContext, Task, WeakView, WindowContext}; +use gpui::{App, Task, WeakEntity}; use language::{Anchor, CodeLabel, LspAdapterDelegate}; use language_model::{LanguageModelRegistry, LanguageModelTool}; use prompt_library::PromptBuilder; @@ -43,7 +43,7 @@ impl SlashCommand for ProjectSlashCommand { "project".into() } - fn label(&self, cx: &AppContext) -> CodeLabel { + fn label(&self, cx: &App) -> CodeLabel { create_label_for_command("project", &[], cx) } @@ -67,8 +67,9 @@ impl SlashCommand for ProjectSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Ok(Vec::new())) } @@ -78,9 +79,10 @@ impl SlashCommand for ProjectSlashCommand { _arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], context_buffer: language::BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task { let model_registry = LanguageModelRegistry::read_global(cx); let current_model = model_registry.active_model(); @@ -97,7 +99,7 @@ impl SlashCommand for ProjectSlashCommand { return Task::ready(Err(anyhow::anyhow!("no project indexer"))); }; - cx.spawn(|mut cx| async move { + window.spawn(cx, |mut cx| async move { let current_model = current_model.ok_or_else(|| anyhow!("no model selected"))?; let prompt = diff --git a/crates/assistant_slash_commands/src/prompt_command.rs b/crates/assistant_slash_commands/src/prompt_command.rs index ed926e3e5d7761..930a7fb732f589 100644 --- a/crates/assistant_slash_commands/src/prompt_command.rs +++ b/crates/assistant_slash_commands/src/prompt_command.rs @@ -1,9 +1,9 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, }; -use gpui::{Task, WeakView}; +use gpui::{Task, WeakEntity}; use language::{BufferSnapshot, LspAdapterDelegate}; use prompt_library::PromptStore; use std::sync::{atomic::AtomicBool, Arc}; @@ -37,8 +37,9 @@ impl SlashCommand for PromptSlashCommand { self: Arc, arguments: &[String], _cancellation_flag: Arc, - _workspace: Option>, - cx: &mut WindowContext, + _workspace: Option>, + _: &mut Window, + cx: &mut App, ) -> Task>> { let store = PromptStore::global(cx); let query = arguments.to_owned().join(" "); @@ -64,9 +65,10 @@ impl SlashCommand for PromptSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - _workspace: WeakView, + _workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task { let title = arguments.to_owned().join(" "); if title.trim().is_empty() { diff --git a/crates/assistant_slash_commands/src/search_command.rs b/crates/assistant_slash_commands/src/search_command.rs index 90204ab024727e..6b75d57de09451 100644 --- a/crates/assistant_slash_commands/src/search_command.rs +++ b/crates/assistant_slash_commands/src/search_command.rs @@ -4,7 +4,7 @@ use assistant_slash_command::{ SlashCommandResult, }; use feature_flags::FeatureFlag; -use gpui::{AppContext, Task, WeakView}; +use gpui::{App, Task, WeakEntity}; use language::{CodeLabel, LspAdapterDelegate}; use semantic_index::{LoadedSearchResult, SemanticDb}; use std::{ @@ -34,7 +34,7 @@ impl SlashCommand for SearchSlashCommand { "search".into() } - fn label(&self, cx: &AppContext) -> CodeLabel { + fn label(&self, cx: &App) -> CodeLabel { create_label_for_command("search", &["--n"], cx) } @@ -58,8 +58,9 @@ impl SlashCommand for SearchSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Ok(Vec::new())) } @@ -69,9 +70,10 @@ impl SlashCommand for SearchSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: language::BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); @@ -107,7 +109,7 @@ impl SlashCommand for SearchSlashCommand { return Task::ready(Err(anyhow::anyhow!("no project indexer"))); }; - cx.spawn(|cx| async move { + window.spawn(cx, |cx| async move { let results = project_index .read_with(&cx, |project_index, cx| { project_index.search(vec![query.clone()], limit.unwrap_or(5), cx) diff --git a/crates/assistant_slash_commands/src/selection_command.rs b/crates/assistant_slash_commands/src/selection_command.rs index 29af653e8e89ac..fbe4d42bcb8609 100644 --- a/crates/assistant_slash_commands/src/selection_command.rs +++ b/crates/assistant_slash_commands/src/selection_command.rs @@ -5,8 +5,7 @@ use assistant_slash_command::{ }; use editor::Editor; use futures::StreamExt; -use gpui::{AppContext, Task, WeakView}; -use gpui::{SharedString, ViewContext, WindowContext}; +use gpui::{App, Context, SharedString, Task, WeakEntity, Window}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use std::sync::atomic::AtomicBool; use std::sync::Arc; @@ -22,7 +21,7 @@ impl SlashCommand for SelectionCommand { "selection".into() } - fn label(&self, _cx: &AppContext) -> CodeLabel { + fn label(&self, _cx: &App) -> CodeLabel { CodeLabel::plain(self.name(), None) } @@ -50,8 +49,9 @@ impl SlashCommand for SelectionCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } @@ -61,9 +61,10 @@ impl SlashCommand for SelectionCommand { _arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _window: &mut Window, + cx: &mut App, ) -> Task { let mut events = vec![]; @@ -102,7 +103,7 @@ impl SlashCommand for SelectionCommand { pub fn selections_creases( workspace: &mut workspace::Workspace, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option> { let editor = workspace .active_item(cx) diff --git a/crates/assistant_slash_commands/src/streaming_example_command.rs b/crates/assistant_slash_commands/src/streaming_example_command.rs index 663d9078faa220..ed7797843a05b1 100644 --- a/crates/assistant_slash_commands/src/streaming_example_command.rs +++ b/crates/assistant_slash_commands/src/streaming_example_command.rs @@ -9,7 +9,7 @@ use assistant_slash_command::{ }; use feature_flags::FeatureFlag; use futures::channel::mpsc; -use gpui::{Task, WeakView}; +use gpui::{Task, WeakEntity}; use language::{BufferSnapshot, LspAdapterDelegate}; use smol::stream::StreamExt; use smol::Timer; @@ -45,8 +45,9 @@ impl SlashCommand for StreamingExampleSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Ok(Vec::new())) } @@ -56,9 +57,10 @@ impl SlashCommand for StreamingExampleSlashCommand { _arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - _workspace: WeakView, + _workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task { let (events_tx, events_rx) = mpsc::unbounded(); cx.background_executor() diff --git a/crates/assistant_slash_commands/src/symbols_command.rs b/crates/assistant_slash_commands/src/symbols_command.rs index e09787a91ad9b3..d44d92058dba04 100644 --- a/crates/assistant_slash_commands/src/symbols_command.rs +++ b/crates/assistant_slash_commands/src/symbols_command.rs @@ -4,11 +4,11 @@ use assistant_slash_command::{ SlashCommandResult, }; use editor::Editor; -use gpui::{Task, WeakView}; +use gpui::{Task, WeakEntity}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::sync::Arc; use std::{path::Path, sync::atomic::AtomicBool}; -use ui::{IconName, WindowContext}; +use ui::{App, IconName, Window}; use workspace::Workspace; pub struct OutlineSlashCommand; @@ -34,8 +34,9 @@ impl SlashCommand for OutlineSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Err(anyhow!("this command does not require argument"))) } @@ -49,9 +50,10 @@ impl SlashCommand for OutlineSlashCommand { _arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task { let output = workspace.update(cx, |workspace, cx| { let Some(active_item) = workspace.active_item(cx) else { diff --git a/crates/assistant_slash_commands/src/tab_command.rs b/crates/assistant_slash_commands/src/tab_command.rs index a343ce111d88eb..6ff495bdbeec85 100644 --- a/crates/assistant_slash_commands/src/tab_command.rs +++ b/crates/assistant_slash_commands/src/tab_command.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, @@ -6,13 +6,13 @@ use assistant_slash_command::{ use collections::{HashMap, HashSet}; use editor::Editor; use futures::future::join_all; -use gpui::{Entity, Task, WeakView}; +use gpui::{Task, WeakEntity}; use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate}; use std::{ path::PathBuf, sync::{atomic::AtomicBool, Arc}, }; -use ui::{prelude::*, ActiveTheme, WindowContext}; +use ui::{prelude::*, ActiveTheme, App, Window}; use util::ResultExt; use workspace::Workspace; @@ -51,8 +51,9 @@ impl SlashCommand for TabSlashCommand { self: Arc, arguments: &[String], cancel: Arc, - workspace: Option>, - cx: &mut WindowContext, + workspace: Option>, + window: &mut Window, + cx: &mut App, ) -> Task>> { let mut has_all_tabs_completion_item = false; let argument_set = arguments @@ -82,10 +83,10 @@ impl SlashCommand for TabSlashCommand { }); let current_query = arguments.last().cloned().unwrap_or_default(); let tab_items_search = - tab_items_for_queries(workspace, &[current_query], cancel, false, cx); + tab_items_for_queries(workspace, &[current_query], cancel, false, window, cx); let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId); - cx.spawn(|_| async move { + window.spawn(cx, |_| async move { let tab_items = tab_items_search.await?; let run_command = tab_items.len() == 1; let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| { @@ -137,15 +138,17 @@ impl SlashCommand for TabSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task { let tab_items_search = tab_items_for_queries( Some(workspace), arguments, Arc::new(AtomicBool::new(false)), true, + window, cx, ); @@ -160,15 +163,16 @@ impl SlashCommand for TabSlashCommand { } fn tab_items_for_queries( - workspace: Option>, + workspace: Option>, queries: &[String], cancel: Arc, strict_match: bool, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task, BufferSnapshot, usize)>>> { let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty()); let queries = queries.to_owned(); - cx.spawn(|mut cx| async move { + window.spawn(cx, |mut cx| async move { let mut open_buffers = workspace .context("no workspace")? @@ -281,7 +285,7 @@ fn tab_items_for_queries( fn active_item_buffer( workspace: &mut Workspace, - cx: &mut ViewContext, + cx: &mut Context, ) -> anyhow::Result { let active_editor = workspace .active_item(cx) diff --git a/crates/assistant_slash_commands/src/terminal_command.rs b/crates/assistant_slash_commands/src/terminal_command.rs index 5abc93deeddf68..fe567e75016984 100644 --- a/crates/assistant_slash_commands/src/terminal_command.rs +++ b/crates/assistant_slash_commands/src/terminal_command.rs @@ -6,7 +6,7 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, }; -use gpui::{AppContext, Task, View, WeakView}; +use gpui::{App, Entity, Task, WeakEntity}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use ui::prelude::*; @@ -25,7 +25,7 @@ impl SlashCommand for TerminalSlashCommand { "terminal".into() } - fn label(&self, cx: &AppContext) -> CodeLabel { + fn label(&self, cx: &App) -> CodeLabel { create_label_for_command("terminal", &[LINE_COUNT_ARG], cx) } @@ -53,8 +53,9 @@ impl SlashCommand for TerminalSlashCommand { self: Arc, _arguments: &[String], _cancel: Arc, - _workspace: Option>, - _cx: &mut WindowContext, + _workspace: Option>, + _window: &mut Window, + _cx: &mut App, ) -> Task>> { Task::ready(Ok(Vec::new())) } @@ -64,9 +65,10 @@ impl SlashCommand for TerminalSlashCommand { arguments: &[String], _context_slash_command_output_sections: &[SlashCommandOutputSection], _context_buffer: BufferSnapshot, - workspace: WeakView, + workspace: WeakEntity, _delegate: Option>, - cx: &mut WindowContext, + _: &mut Window, + cx: &mut App, ) -> Task { let Some(workspace) = workspace.upgrade() else { return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); @@ -83,7 +85,7 @@ impl SlashCommand for TerminalSlashCommand { let lines = active_terminal .read(cx) - .model() + .entity() .read(cx) .last_n_non_empty_lines(line_count); @@ -107,9 +109,9 @@ impl SlashCommand for TerminalSlashCommand { } fn resolve_active_terminal( - workspace: &View, - cx: &WindowContext, -) -> Option> { + workspace: &Entity, + cx: &mut App, +) -> Option> { if let Some(terminal_view) = workspace .read(cx) .active_item(cx) diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs index c99349449529a4..0b4f30da7d0146 100644 --- a/crates/assistant_tool/src/assistant_tool.rs +++ b/crates/assistant_tool/src/assistant_tool.rs @@ -4,13 +4,13 @@ mod tool_working_set; use std::sync::Arc; use anyhow::Result; -use gpui::{AppContext, Task, WeakView, WindowContext}; +use gpui::{App, Task, WeakEntity, Window}; use workspace::Workspace; pub use crate::tool_registry::*; pub use crate::tool_working_set::*; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { ToolRegistry::default_global(cx); } @@ -31,7 +31,8 @@ pub trait Tool: 'static + Send + Sync { fn run( self: Arc, input: serde_json::Value, - workspace: WeakView, - cx: &mut WindowContext, + workspace: WeakEntity, + window: &mut Window, + cx: &mut App, ) -> Task>; } diff --git a/crates/assistant_tool/src/tool_registry.rs b/crates/assistant_tool/src/tool_registry.rs index d225b6a9a39efd..26b4821a6d1af0 100644 --- a/crates/assistant_tool/src/tool_registry.rs +++ b/crates/assistant_tool/src/tool_registry.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use collections::HashMap; use derive_more::{Deref, DerefMut}; use gpui::Global; -use gpui::{AppContext, ReadGlobal}; +use gpui::{App, ReadGlobal}; use parking_lot::RwLock; use crate::Tool; @@ -25,14 +25,14 @@ pub struct ToolRegistry { impl ToolRegistry { /// Returns the global [`ToolRegistry`]. - pub fn global(cx: &AppContext) -> Arc { + pub fn global(cx: &App) -> Arc { GlobalToolRegistry::global(cx).0.clone() } /// Returns the global [`ToolRegistry`]. /// /// Inserts a default [`ToolRegistry`] if one does not yet exist. - pub fn default_global(cx: &mut AppContext) -> Arc { + pub fn default_global(cx: &mut App) -> Arc { cx.default_global::().0.clone() } diff --git a/crates/assistant_tool/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs index f22f0c78810913..bbdb0f87c39d53 100644 --- a/crates/assistant_tool/src/tool_working_set.rs +++ b/crates/assistant_tool/src/tool_working_set.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use collections::HashMap; -use gpui::AppContext; +use gpui::App; use parking_lot::Mutex; use crate::{Tool, ToolRegistry}; @@ -23,7 +23,7 @@ struct WorkingSetState { } impl ToolWorkingSet { - pub fn tool(&self, name: &str, cx: &AppContext) -> Option> { + pub fn tool(&self, name: &str, cx: &App) -> Option> { self.state .lock() .context_server_tools_by_name @@ -32,7 +32,7 @@ impl ToolWorkingSet { .or_else(|| ToolRegistry::global(cx).tool(name)) } - pub fn tools(&self, cx: &AppContext) -> Vec> { + pub fn tools(&self, cx: &App) -> Vec> { let mut tools = ToolRegistry::global(cx).tools(); tools.extend( self.state diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index 7d145c61b7821c..b0afd6441f9b7a 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -1,11 +1,11 @@ mod now_tool; use assistant_tool::ToolRegistry; -use gpui::AppContext; +use gpui::App; use crate::now_tool::NowTool; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { assistant_tool::init(cx); let registry = ToolRegistry::global(cx); diff --git a/crates/assistant_tools/src/now_tool.rs b/crates/assistant_tools/src/now_tool.rs index 707f2be2bdd624..b9d22b66b48c95 100644 --- a/crates/assistant_tools/src/now_tool.rs +++ b/crates/assistant_tools/src/now_tool.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use assistant_tool::Tool; use chrono::{Local, Utc}; -use gpui::{Task, WeakView, WindowContext}; +use gpui::{App, Task, WeakEntity, Window}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -41,8 +41,9 @@ impl Tool for NowTool { fn run( self: Arc, input: serde_json::Value, - _workspace: WeakView, - _cx: &mut WindowContext, + _workspace: WeakEntity, + _window: &mut Window, + _cx: &mut App, ) -> Task> { let input: FileToolInput = match serde_json::from_value(input) { Ok(input) => input, diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs index 75bcd20cd09994..1f5256821e5df6 100644 --- a/crates/audio/src/assets.rs +++ b/crates/audio/src/assets.rs @@ -2,7 +2,7 @@ use std::{io::Cursor, sync::Arc}; use anyhow::Result; use collections::HashMap; -use gpui::{AppContext, AssetSource, Global}; +use gpui::{App, AssetSource, Global}; use rodio::{ source::{Buffered, SamplesConverter}, Decoder, Source, @@ -27,11 +27,11 @@ impl SoundRegistry { }) } - pub fn global(cx: &AppContext) -> Arc { + pub fn global(cx: &App) -> Arc { cx.global::().0.clone() } - pub(crate) fn set_global(source: impl AssetSource, cx: &mut AppContext) { + pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) { cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source))); } diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index 118abcc6046d97..55f08c38a12a1f 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,12 +1,12 @@ use assets::SoundRegistry; use derive_more::{Deref, DerefMut}; -use gpui::{AppContext, AssetSource, BorrowAppContext, Global}; +use gpui::{App, AssetSource, BorrowAppContext, Global}; use rodio::{OutputStream, OutputStreamHandle}; use util::ResultExt; mod assets; -pub fn init(source: impl AssetSource, cx: &mut AppContext) { +pub fn init(source: impl AssetSource, cx: &mut App) { SoundRegistry::set_global(source, cx); cx.set_global(GlobalAudio(Audio::new())); } @@ -59,7 +59,7 @@ impl Audio { self.output_handle.as_ref() } - pub fn play_sound(sound: Sound, cx: &mut AppContext) { + pub fn play_sound(sound: Sound, cx: &mut App) { if !cx.has_global::() { return; } @@ -72,7 +72,7 @@ impl Audio { }); } - pub fn end_call(cx: &mut AppContext) { + pub fn end_call(cx: &mut App) { if !cx.has_global::() { return; } diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 0f9999b9181190..8994f7993ce9c0 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,10 +1,9 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use client::{Client, TelemetrySettings}; use db::kvp::KEY_VALUE_STORE; use db::RELEASE_CHANNEL; use gpui::{ - actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext, - SemanticVersion, Task, WindowContext, + actions, App, AppContext as _, AsyncApp, Context, Entity, Global, SemanticVersion, Task, Window, }; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use paths::remote_servers_dir; @@ -112,7 +111,7 @@ impl Settings for AutoUpdateSetting { type FileContent = Option; - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + fn load(sources: SettingsSources, _: &mut App) -> Result { let auto_update = [sources.server, sources.release_channel, sources.user] .into_iter() .find_map(|value| value.copied().flatten()) @@ -123,24 +122,24 @@ impl Settings for AutoUpdateSetting { } #[derive(Default)] -struct GlobalAutoUpdate(Option>); +struct GlobalAutoUpdate(Option>); impl Global for GlobalAutoUpdate {} -pub fn init(http_client: Arc, cx: &mut AppContext) { +pub fn init(http_client: Arc, cx: &mut App) { AutoUpdateSetting::register(cx); - cx.observe_new_views(|workspace: &mut Workspace, _cx| { - workspace.register_action(|_, action: &Check, cx| check(action, cx)); + cx.observe_new(|workspace: &mut Workspace, _window, _cx| { + workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx)); - workspace.register_action(|_, action, cx| { + workspace.register_action(|_, action, _, cx| { view_release_notes(action, cx); }); }) .detach(); let version = release_channel::AppVersion::global(cx); - let auto_updater = cx.new_model(|cx| { + let auto_updater = cx.new(|cx| { let updater = AutoUpdater::new(version, http_client); let poll_for_updates = ReleaseChannel::try_global(cx) @@ -155,7 +154,7 @@ pub fn init(http_client: Arc, cx: &mut AppContext) { .0 .then(|| updater.start_polling(cx)); - cx.observe_global::(move |updater, cx| { + cx.observe_global::(move |updater: &mut AutoUpdater, cx| { if AutoUpdateSetting::get_global(cx).0 { if update_subscription.is_none() { update_subscription = Some(updater.start_polling(cx)) @@ -172,23 +171,25 @@ pub fn init(http_client: Arc, cx: &mut AppContext) { cx.set_global(GlobalAutoUpdate(Some(auto_updater))); } -pub fn check(_: &Check, cx: &mut WindowContext) { +pub fn check(_: &Check, window: &mut Window, cx: &mut App) { if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") { - drop(cx.prompt( + drop(window.prompt( gpui::PromptLevel::Info, "Zed was installed via a package manager.", Some(message), &["Ok"], + cx, )); return; } if let Ok(message) = env::var("ZED_UPDATE_EXPLANATION") { - drop(cx.prompt( + drop(window.prompt( gpui::PromptLevel::Info, "Zed was installed via a package manager.", Some(&message), &["Ok"], + cx, )); return; } @@ -203,16 +204,17 @@ pub fn check(_: &Check, cx: &mut WindowContext) { if let Some(updater) = AutoUpdater::get(cx) { updater.update(cx, |updater, cx| updater.poll(cx)); } else { - drop(cx.prompt( + drop(window.prompt( gpui::PromptLevel::Info, "Could not check for updates", Some("Auto-updates disabled for non-bundled app."), &["Ok"], + cx, )); } } -pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<()> { +pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> { let auto_updater = AutoUpdater::get(cx)?; let release_channel = ReleaseChannel::try_global(cx)?; @@ -236,7 +238,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<( } impl AutoUpdater { - pub fn get(cx: &mut AppContext) -> Option> { + pub fn get(cx: &mut App) -> Option> { cx.default_global::().0.clone() } @@ -249,7 +251,7 @@ impl AutoUpdater { } } - pub fn start_polling(&self, cx: &mut ModelContext) -> Task> { + pub fn start_polling(&self, cx: &mut Context) -> Task> { cx.spawn(|this, mut cx| async move { loop { this.update(&mut cx, |this, cx| this.poll(cx))?; @@ -258,7 +260,7 @@ impl AutoUpdater { }) } - pub fn poll(&mut self, cx: &mut ModelContext) { + pub fn poll(&mut self, cx: &mut Context) { if self.pending_poll.is_some() || self.status.is_updated() { return; } @@ -287,7 +289,7 @@ impl AutoUpdater { self.status.clone() } - pub fn dismiss_error(&mut self, cx: &mut ModelContext) { + pub fn dismiss_error(&mut self, cx: &mut Context) { self.status = AutoUpdateStatus::Idle; cx.notify(); } @@ -300,7 +302,7 @@ impl AutoUpdater { arch: &str, release_channel: ReleaseChannel, version: Option, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result { let this = cx.update(|cx| { cx.default_global::() @@ -344,7 +346,7 @@ impl AutoUpdater { arch: &str, release_channel: ReleaseChannel, version: Option, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result> { let this = cx.update(|cx| { cx.default_global::() @@ -371,13 +373,13 @@ impl AutoUpdater { } async fn get_release( - this: &Model, + this: &Entity, asset: &str, os: &str, arch: &str, version: Option, release_channel: Option, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result { let client = this.read_with(cx, |this, _| this.http_client.clone())?; @@ -421,17 +423,17 @@ impl AutoUpdater { } async fn get_latest_release( - this: &Model, + this: &Entity, asset: &str, os: &str, arch: &str, release_channel: Option, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result { Self::get_release(this, asset, os, arch, None, release_channel, cx).await } - async fn update(this: Model, mut cx: AsyncAppContext) -> Result<()> { + async fn update(this: Entity, mut cx: AsyncApp) -> Result<()> { let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Checking; cx.notify(); @@ -509,7 +511,7 @@ impl AutoUpdater { pub fn set_should_show_update_notification( &self, should_show: bool, - cx: &AppContext, + cx: &App, ) -> Task> { cx.background_executor().spawn(async move { if should_show { @@ -528,7 +530,7 @@ impl AutoUpdater { }) } - pub fn should_show_update_notification(&self, cx: &AppContext) -> Task> { + pub fn should_show_update_notification(&self, cx: &App) -> Task> { cx.background_executor().spawn(async move { Ok(KEY_VALUE_STORE .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)? @@ -541,7 +543,7 @@ async fn download_remote_server_binary( target_path: &PathBuf, release: JsonRelease, client: Arc, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> Result<()> { let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?; let mut temp_file = File::create(&temp).await?; @@ -561,7 +563,7 @@ async fn download_remote_server_binary( Ok(()) } -fn build_remote_server_update_request_body(cx: &AsyncAppContext) -> Result { +fn build_remote_server_update_request_body(cx: &AsyncApp) -> Result { let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| { let telemetry = Client::global(cx).telemetry().clone(); let is_staff = telemetry.is_staff(); @@ -591,7 +593,7 @@ async fn download_release( target_path: &Path, release: JsonRelease, client: Arc, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> Result<()> { let mut target_file = File::create(&target_path).await?; @@ -629,7 +631,7 @@ async fn download_release( async fn install_release_linux( temp_dir: &tempfile::TempDir, downloaded_tar_gz: PathBuf, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> Result { let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?; let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?); @@ -696,7 +698,7 @@ async fn install_release_linux( async fn install_release_macos( temp_dir: &tempfile::TempDir, downloaded_dmg: PathBuf, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> Result { let running_app_path = cx.update(|cx| cx.app_path())??; let running_app_filename = running_app_path diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 9114375e88e36e..a54ad7ac2287af 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -2,7 +2,7 @@ mod update_notification; use auto_update::AutoUpdater; use editor::{Editor, MultiBuffer}; -use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext}; +use gpui::{actions, prelude::*, App, Context, Entity, SharedString, Window}; use http_client::HttpClient; use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView}; use release_channel::{AppVersion, ReleaseChannel}; @@ -16,10 +16,10 @@ use crate::update_notification::UpdateNotification; actions!(auto_update, [ViewReleaseNotesLocally]); -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, _cx| { - workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| { - view_release_notes_locally(workspace, cx); +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _window, _cx| { + workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| { + view_release_notes_locally(workspace, window, cx); }); }) .detach(); @@ -31,7 +31,11 @@ struct ReleaseNotesBody { release_notes: String, } -fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext) { +fn view_release_notes_locally( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, +) { let release_channel = ReleaseChannel::global(cx); let url = match release_channel { @@ -60,8 +64,8 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext = MarkdownPreviewView::new( - MarkdownPreviewMode::Default, - editor, - workspace_handle, - language_registry, - Some(tab_description), - cx, - ); + let markdown_preview: Entity = + MarkdownPreviewView::new( + MarkdownPreviewMode::Default, + editor, + workspace_handle, + language_registry, + Some(tab_description), + window, + cx, + ); workspace.add_item_to_active_pane( - Box::new(view.clone()), + Box::new(markdown_preview.clone()), None, true, + window, cx, ); cx.notify(); @@ -117,12 +124,12 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext) -> Option<()> { +pub fn notify_of_any_new_update(window: &mut Window, cx: &mut Context) -> Option<()> { let updater = AutoUpdater::get(cx)?; let version = updater.read(cx).current_version(); let should_show_notification = updater.read(cx).should_show_update_notification(cx); - cx.spawn(|workspace, mut cx| async move { + cx.spawn_in(window, |workspace, mut cx| async move { let should_show_notification = should_show_notification.await?; if should_show_notification { workspace.update(&mut cx, |workspace, cx| { @@ -130,7 +137,7 @@ pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { workspace.show_notification( NotificationId::unique::(), cx, - |cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)), + |cx| cx.new(|_| UpdateNotification::new(version, workspace_handle)), ); updater.update(cx, |updater, cx| { updater diff --git a/crates/auto_update_ui/src/update_notification.rs b/crates/auto_update_ui/src/update_notification.rs index 33ac333a302698..e99e26983aff43 100644 --- a/crates/auto_update_ui/src/update_notification.rs +++ b/crates/auto_update_ui/src/update_notification.rs @@ -1,6 +1,6 @@ use gpui::{ - div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render, - SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, WeakView, + div, Context, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, + Render, SemanticVersion, StatefulInteractiveElement, Styled, WeakEntity, Window, }; use menu::Cancel; use release_channel::ReleaseChannel; @@ -12,13 +12,13 @@ use workspace::{ pub struct UpdateNotification { version: SemanticVersion, - workspace: WeakView, + workspace: WeakEntity, } impl EventEmitter for UpdateNotification {} impl Render for UpdateNotification { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let app_name = ReleaseChannel::global(cx).display_name(); v_flex() @@ -37,7 +37,9 @@ impl Render for UpdateNotification { .id("cancel") .child(Icon::new(IconName::Close)) .cursor_pointer() - .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))), + .on_click(cx.listener(|this, _, window, cx| { + this.dismiss(&menu::Cancel, window, cx) + })), ), ) .child( @@ -45,24 +47,24 @@ impl Render for UpdateNotification { .id("notes") .child(Label::new("View the release notes")) .cursor_pointer() - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, window, cx| { this.workspace .update(cx, |workspace, cx| { - crate::view_release_notes_locally(workspace, cx); + crate::view_release_notes_locally(workspace, window, cx); }) .log_err(); - this.dismiss(&menu::Cancel, cx) + this.dismiss(&menu::Cancel, window, cx) })), ) } } impl UpdateNotification { - pub fn new(version: SemanticVersion, workspace: WeakView) -> Self { + pub fn new(version: SemanticVersion, workspace: WeakEntity) -> Self { Self { version, workspace } } - pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { + pub fn dismiss(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context) { cx.emit(DismissEvent); } } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index da048d55deeaf9..29dde8618e8351 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText, - Subscription, ViewContext, + Context, Element, EventEmitter, Focusable, IntoElement, ParentElement, Render, StyledText, + Subscription, Window, }; use itertools::Itertools; use std::cmp; @@ -37,7 +37,7 @@ impl Breadcrumbs { impl EventEmitter for Breadcrumbs {} impl Render for Breadcrumbs { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { const MAX_SEGMENTS: usize = 12; let element = h_flex() @@ -72,7 +72,7 @@ impl Render for Breadcrumbs { } let highlighted_segments = segments.into_iter().map(|segment| { - let mut text_style = cx.text_style(); + let mut text_style = window.text_style(); if let Some(font) = segment.font { text_style.font_family = font.family; text_style.font_features = font.features; @@ -101,28 +101,30 @@ impl Render for Breadcrumbs { .style(ButtonStyle::Transparent) .on_click({ let editor = editor.clone(); - move |_, cx| { + move |_, window, cx| { if let Some((editor, callback)) = editor .upgrade() .zip(zed_actions::outline::TOGGLE_OUTLINE.get()) { - callback(editor.to_any(), cx); + callback(editor.to_any(), window, cx); } } }) - .tooltip(move |cx| { + .tooltip(move |window, cx| { if let Some(editor) = editor.upgrade() { let focus_handle = editor.read(cx).focus_handle(cx); Tooltip::for_action_in( "Show Symbol Outline", &zed_actions::outline::ToggleOutline, &focus_handle, + window, cx, ) } else { Tooltip::for_action( "Show Symbol Outline", &zed_actions::outline::ToggleOutline, + window, cx, ) } @@ -140,7 +142,8 @@ impl ToolbarItemView for Breadcrumbs { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> ToolbarItemLocation { cx.notify(); self.active_item = None; @@ -149,10 +152,11 @@ impl ToolbarItemView for Breadcrumbs { return ToolbarItemLocation::Hidden; }; - let this = cx.view().downgrade(); + let this = cx.entity().downgrade(); self.subscription = Some(item.subscribe_to_item_events( + window, cx, - Box::new(move |event, cx| { + Box::new(move |event, _, cx| { if let ItemEvent::UpdateBreadcrumbs = event { this.update(cx, |this, cx| { cx.notify(); @@ -170,7 +174,12 @@ impl ToolbarItemView for Breadcrumbs { item.breadcrumb_location(cx) } - fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext) { + fn pane_focus_update( + &mut self, + pane_focused: bool, + _window: &mut Window, + _: &mut Context, + ) { self.pane_focused = pane_focused; } } diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 21b2279a8f3ce4..e15fd380675421 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use gpui::AppContext; +use gpui::App; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -29,7 +29,7 @@ impl Settings for CallSettings { type FileContent = CallSettingsContent; - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } } diff --git a/crates/call/src/cross_platform/mod.rs b/crates/call/src/cross_platform/mod.rs index 7530c0b2653f72..d361dd9dced854 100644 --- a/crates/call/src/cross_platform/mod.rs +++ b/crates/call/src/cross_platform/mod.rs @@ -8,8 +8,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription, - Task, WeakModel, + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Subscription, Task, + WeakEntity, }; use postage::watch; use project::Project; @@ -23,18 +23,18 @@ pub use livekit_client::{ pub use participant::ParticipantLocation; pub use room::Room; -struct GlobalActiveCall(Model); +struct GlobalActiveCall(Entity); impl Global for GlobalActiveCall {} -pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { +pub fn init(client: Arc, user_store: Entity, cx: &mut App) { livekit_client::init( cx.background_executor().dispatcher.clone(), cx.http_client(), ); CallSettings::register(cx); - let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx)); + let active_call = cx.new(|cx| ActiveCall::new(client, user_store, cx)); cx.set_global(GlobalActiveCall(active_call)); } @@ -46,9 +46,9 @@ impl OneAtATime { /// spawn a task in the given context. /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None) /// otherwise you'll see the result of the task. - fn spawn(&mut self, cx: &mut AppContext, f: F) -> Task>> + fn spawn(&mut self, cx: &mut App, f: F) -> Task>> where - F: 'static + FnOnce(AsyncAppContext) -> Fut, + F: 'static + FnOnce(AsyncApp) -> Fut, Fut: Future>, R: 'static, { @@ -79,9 +79,9 @@ pub struct IncomingCall { /// Singleton global maintaining the user's participation in a room across workspaces. pub struct ActiveCall { - room: Option<(Model, Vec)>, - pending_room_creation: Option, Arc>>>>, - location: Option>, + room: Option<(Entity, Vec)>, + pending_room_creation: Option, Arc>>>>, + location: Option>, _join_debouncer: OneAtATime, pending_invites: HashSet, incoming_call: ( @@ -89,14 +89,14 @@ pub struct ActiveCall { watch::Receiver>, ), client: Arc, - user_store: Model, + user_store: Entity, _subscriptions: Vec, } impl EventEmitter for ActiveCall {} impl ActiveCall { - fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { + fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { Self { room: None, pending_room_creation: None, @@ -105,22 +105,22 @@ impl ActiveCall { incoming_call: watch::channel(), _join_debouncer: OneAtATime { cancel: None }, _subscriptions: vec![ - client.add_request_handler(cx.weak_model(), Self::handle_incoming_call), - client.add_message_handler(cx.weak_model(), Self::handle_call_canceled), + client.add_request_handler(cx.weak_entity(), Self::handle_incoming_call), + client.add_message_handler(cx.weak_entity(), Self::handle_call_canceled), ], client, user_store, } } - pub fn channel_id(&self, cx: &AppContext) -> Option { + pub fn channel_id(&self, cx: &App) -> Option { self.room()?.read(cx).channel_id() } async fn handle_incoming_call( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result { let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; let call = IncomingCall { @@ -145,9 +145,9 @@ impl ActiveCall { } async fn handle_call_canceled( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, _| { let mut incoming_call = this.incoming_call.0.borrow_mut(); @@ -161,11 +161,11 @@ impl ActiveCall { Ok(()) } - pub fn global(cx: &AppContext) -> Model { + pub fn global(cx: &App) -> Entity { cx.global::().0.clone() } - pub fn try_global(cx: &AppContext) -> Option> { + pub fn try_global(cx: &App) -> Option> { cx.try_global::() .map(|call| call.0.clone()) } @@ -173,8 +173,8 @@ impl ActiveCall { pub fn invite( &mut self, called_user_id: u64, - initial_project: Option>, - cx: &mut ModelContext, + initial_project: Option>, + cx: &mut Context, ) -> Task> { if !self.pending_invites.insert(called_user_id) { return Task::ready(Err(anyhow!("user was already invited"))); @@ -269,7 +269,7 @@ impl ActiveCall { pub fn cancel_invite( &mut self, called_user_id: u64, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let room_id = if let Some(room) = self.room() { room.read(cx).id() @@ -293,7 +293,7 @@ impl ActiveCall { self.incoming_call.1.clone() } - pub fn accept_incoming(&mut self, cx: &mut ModelContext) -> Task> { + pub fn accept_incoming(&mut self, cx: &mut Context) -> Task> { if self.room.is_some() { return Task::ready(Err(anyhow!("cannot join while on another call"))); } @@ -326,7 +326,7 @@ impl ActiveCall { }) } - pub fn decline_incoming(&mut self, _: &mut ModelContext) -> Result<()> { + pub fn decline_incoming(&mut self, _: &mut Context) -> Result<()> { let call = self .incoming_call .0 @@ -343,8 +343,8 @@ impl ActiveCall { pub fn join_channel( &mut self, channel_id: ChannelId, - cx: &mut ModelContext, - ) -> Task>>> { + cx: &mut Context, + ) -> Task>>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { return Task::ready(Ok(Some(room))); @@ -374,7 +374,7 @@ impl ActiveCall { }) } - pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { + pub fn hang_up(&mut self, cx: &mut Context) -> Task> { cx.notify(); self.report_call_event("Call Ended", cx); @@ -391,8 +391,8 @@ impl ActiveCall { pub fn share_project( &mut self, - project: Model, - cx: &mut ModelContext, + project: Entity, + cx: &mut Context, ) -> Task> { if let Some((room, _)) = self.room.as_ref() { self.report_call_event("Project Shared", cx); @@ -404,8 +404,8 @@ impl ActiveCall { pub fn unshare_project( &mut self, - project: Model, - cx: &mut ModelContext, + project: Entity, + cx: &mut Context, ) -> Result<()> { if let Some((room, _)) = self.room.as_ref() { self.report_call_event("Project Unshared", cx); @@ -415,14 +415,14 @@ impl ActiveCall { } } - pub fn location(&self) -> Option<&WeakModel> { + pub fn location(&self) -> Option<&WeakEntity> { self.location.as_ref() } pub fn set_location( &mut self, - project: Option<&Model>, - cx: &mut ModelContext, + project: Option<&Entity>, + cx: &mut Context, ) -> Task> { if project.is_some() || !*ZED_ALWAYS_ACTIVE { self.location = project.map(|project| project.downgrade()); @@ -433,11 +433,7 @@ impl ActiveCall { Task::ready(Ok(())) } - fn set_room( - &mut self, - room: Option>, - cx: &mut ModelContext, - ) -> Task> { + fn set_room(&mut self, room: Option>, cx: &mut Context) -> Task> { if room.as_ref() == self.room.as_ref().map(|room| &room.0) { Task::ready(Ok(())) } else { @@ -473,7 +469,7 @@ impl ActiveCall { } } - pub fn room(&self) -> Option<&Model> { + pub fn room(&self) -> Option<&Entity> { self.room.as_ref().map(|(room, _)| room) } @@ -485,7 +481,7 @@ impl ActiveCall { &self.pending_invites } - pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { + pub fn report_call_event(&self, operation: &'static str, cx: &mut App) { if let Some(room) = self.room() { let room = room.read(cx); telemetry::event!( diff --git a/crates/call/src/cross_platform/participant.rs b/crates/call/src/cross_platform/participant.rs index 16e0107158669f..de001ea9ca4c84 100644 --- a/crates/call/src/cross_platform/participant.rs +++ b/crates/call/src/cross_platform/participant.rs @@ -1,11 +1,14 @@ +#![cfg_attr(all(target_os = "windows", target_env = "gnu"), allow(unused))] + use anyhow::{anyhow, Result}; use client::{proto, ParticipantIndex, User}; use collections::HashMap; -use gpui::WeakModel; +use gpui::WeakEntity; use livekit_client::AudioStream; use project::Project; use std::sync::Arc; +#[cfg(not(all(target_os = "windows", target_env = "gnu")))] pub use livekit_client::id::TrackSid; pub use livekit_client::track::{RemoteAudioTrack, RemoteVideoTrack}; @@ -36,7 +39,7 @@ impl ParticipantLocation { #[derive(Clone, Default)] pub struct LocalParticipant { pub projects: Vec, - pub active_project: Option>, + pub active_project: Option>, pub role: proto::ChannelRole, } @@ -58,13 +61,18 @@ pub struct RemoteParticipant { pub participant_index: ParticipantIndex, pub muted: bool, pub speaking: bool, + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] pub video_tracks: HashMap, + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] pub audio_tracks: HashMap, } impl RemoteParticipant { pub fn has_video_tracks(&self) -> bool { - !self.video_tracks.is_empty() + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] + return !self.video_tracks.is_empty(); + #[cfg(all(target_os = "windows", target_env = "gnu"))] + return false; } pub fn can_write(&self) -> bool { diff --git a/crates/call/src/cross_platform/room.rs b/crates/call/src/cross_platform/room.rs index 7ad274a1dd1fb8..de7852a5a0295d 100644 --- a/crates/call/src/cross_platform/room.rs +++ b/crates/call/src/cross_platform/room.rs @@ -1,3 +1,5 @@ +#![cfg_attr(all(target_os = "windows", target_env = "gnu"), allow(unused))] + use crate::{ call_settings::CallSettings, participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, @@ -11,10 +13,9 @@ use client::{ use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; use futures::{FutureExt, StreamExt}; -use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, -}; +use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; use language::LanguageRegistry; +#[cfg(not(all(target_os = "windows", target_env = "gnu")))] use livekit::{ capture_local_audio_track, capture_local_video_track, id::ParticipantIdentity, @@ -24,6 +25,8 @@ use livekit::{ track::{TrackKind, TrackSource}, RoomEvent, RoomOptions, }; +#[cfg(all(target_os = "windows", target_env = "gnu"))] +use livekit::{publication::LocalTrackPublication, RoomEvent}; use livekit_client as livekit; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; @@ -71,8 +74,8 @@ pub struct Room { channel_id: Option, live_kit: Option, status: RoomStatus, - shared_projects: HashSet>, - joined_projects: HashSet>, + shared_projects: HashSet>, + joined_projects: HashSet>, local_participant: LocalParticipant, remote_participants: BTreeMap, pending_participants: Vec>, @@ -80,7 +83,7 @@ pub struct Room { pending_call_count: usize, leave_when_empty: bool, client: Arc, - user_store: Model, + user_store: Entity, follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, client_subscriptions: Vec, _subscriptions: Vec, @@ -101,7 +104,10 @@ impl Room { !self.shared_projects.is_empty() } - #[cfg(any(test, feature = "test-support"))] + #[cfg(all( + any(test, feature = "test-support"), + not(all(target_os = "windows", target_env = "gnu")) + ))] pub fn is_connected(&self) -> bool { if let Some(live_kit) = self.live_kit.as_ref() { live_kit.room.connection_state() == livekit::ConnectionState::Connected @@ -115,8 +121,8 @@ impl Room { channel_id: Option, livekit_connection_info: Option, client: Arc, - user_store: Model, - cx: &mut ModelContext, + user_store: Entity, + cx: &mut Context, ) -> Self { spawn_room_connection(livekit_connection_info, cx); @@ -142,7 +148,7 @@ impl Room { pending_participants: Default::default(), pending_call_count: 0, client_subscriptions: vec![ - client.add_message_handler(cx.weak_model(), Self::handle_room_updated) + client.add_message_handler(cx.weak_entity(), Self::handle_room_updated) ], _subscriptions: vec![ cx.on_release(Self::released), @@ -161,15 +167,15 @@ impl Room { pub(crate) fn create( called_user_id: u64, - initial_project: Option>, + initial_project: Option>, client: Arc, - user_store: Model, - cx: &mut AppContext, - ) -> Task>> { + user_store: Entity, + cx: &mut App, + ) -> Task>> { cx.spawn(move |mut cx| async move { let response = client.request(proto::CreateRoom {}).await?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.new_model(|cx| { + let room = cx.new(|cx| { let mut room = Self::new( room_proto.id, None, @@ -211,9 +217,9 @@ impl Room { pub(crate) async fn join_channel( channel_id: ChannelId, client: Arc, - user_store: Model, - cx: AsyncAppContext, - ) -> Result> { + user_store: Entity, + cx: AsyncApp, + ) -> Result> { Self::from_join_response( client .request(proto::JoinChannel { @@ -229,9 +235,9 @@ impl Room { pub(crate) async fn join( room_id: u64, client: Arc, - user_store: Model, - cx: AsyncAppContext, - ) -> Result> { + user_store: Entity, + cx: AsyncApp, + ) -> Result> { Self::from_join_response( client.request(proto::JoinRoom { id: room_id }).await?, client, @@ -240,13 +246,13 @@ impl Room { ) } - fn released(&mut self, cx: &mut AppContext) { + fn released(&mut self, cx: &mut App) { if self.status.is_online() { self.leave_internal(cx).detach_and_log_err(cx); } } - fn app_will_quit(&mut self, cx: &mut ModelContext) -> impl Future { + fn app_will_quit(&mut self, cx: &mut Context) -> impl Future { let task = if self.status.is_online() { let leave = self.leave_internal(cx); Some(cx.background_executor().spawn(async move { @@ -263,18 +269,18 @@ impl Room { } } - pub fn mute_on_join(cx: &AppContext) -> bool { + pub fn mute_on_join(cx: &App) -> bool { CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } fn from_join_response( response: proto::JoinRoomResponse, client: Arc, - user_store: Model, - mut cx: AsyncAppContext, - ) -> Result> { + user_store: Entity, + mut cx: AsyncApp, + ) -> Result> { let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.new_model(|cx| { + let room = cx.new(|cx| { Self::new( room_proto.id, response.channel_id.map(ChannelId), @@ -300,12 +306,12 @@ impl Room { && self.pending_call_count == 0 } - pub(crate) fn leave(&mut self, cx: &mut ModelContext) -> Task> { + pub(crate) fn leave(&mut self, cx: &mut Context) -> Task> { cx.notify(); self.leave_internal(cx) } - fn leave_internal(&mut self, cx: &mut AppContext) -> Task> { + fn leave_internal(&mut self, cx: &mut App) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } @@ -322,7 +328,7 @@ impl Room { }) } - pub(crate) fn clear_state(&mut self, cx: &mut AppContext) { + pub(crate) fn clear_state(&mut self, cx: &mut App) { for project in self.shared_projects.drain() { if let Some(project) = project.upgrade() { project.update(cx, |project, cx| { @@ -350,9 +356,9 @@ impl Room { } async fn maintain_connection( - this: WeakModel, + this: WeakEntity, client: Arc, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { let mut client_status = client.status(); loop { @@ -436,7 +442,7 @@ impl Room { )) } - fn rejoin(&mut self, cx: &mut ModelContext) -> Task> { + fn rejoin(&mut self, cx: &mut Context) -> Task> { let mut projects = HashMap::default(); let mut reshared_projects = Vec::new(); let mut rejoined_projects = Vec::new(); @@ -562,7 +568,7 @@ impl Room { &mut self, user_id: u64, role: proto::ChannelRole, - cx: &ModelContext, + cx: &Context, ) -> Task> { let client = self.client.clone(); let room_id = self.id; @@ -594,7 +600,7 @@ impl Room { } /// Returns the most 'active' projects, defined as most people in the project - pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> { + pub fn most_active_project(&self, cx: &App) -> Option<(u64, u64)> { let mut project_hosts_and_guest_counts = HashMap::, u32)>::default(); for participant in self.remote_participants.values() { match participant.location { @@ -631,9 +637,9 @@ impl Room { } async fn handle_room_updated( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { let room = envelope .payload @@ -642,7 +648,7 @@ impl Room { this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))? } - fn apply_room_update(&mut self, room: proto::Room, cx: &mut ModelContext) -> Result<()> { + fn apply_room_update(&mut self, room: proto::Room, cx: &mut Context) -> Result<()> { log::trace!( "client {:?}. room update: {:?}", self.client.user_id(), @@ -666,11 +672,13 @@ impl Room { } } - fn start_room_connection( - &self, - mut room: proto::Room, - cx: &mut ModelContext, - ) -> Task<()> { + #[cfg(all(target_os = "windows", target_env = "gnu"))] + fn start_room_connection(&self, mut room: proto::Room, cx: &mut Context) -> Task<()> { + Task::ready(()) + } + + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] + fn start_room_connection(&self, mut room: proto::Room, cx: &mut Context) -> Task<()> { // Filter ourselves out from the room's participants. let local_participant_ix = room .participants @@ -822,6 +830,7 @@ impl Room { muted: true, speaking: false, video_tracks: Default::default(), + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] audio_tracks: Default::default(), }, ); @@ -916,11 +925,7 @@ impl Room { }) } - fn livekit_room_updated( - &mut self, - event: RoomEvent, - cx: &mut ModelContext, - ) -> Result<()> { + fn livekit_room_updated(&mut self, event: RoomEvent, cx: &mut Context) -> Result<()> { log::trace!( "client {:?}. livekit event: {:?}", self.client.user_id(), @@ -928,6 +933,7 @@ impl Room { ); match event { + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] RoomEvent::TrackSubscribed { track, participant, @@ -962,6 +968,7 @@ impl Room { } } + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] RoomEvent::TrackUnsubscribed { track, participant, .. } => { @@ -989,6 +996,7 @@ impl Room { } } + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] RoomEvent::ActiveSpeakersChanged { speakers } => { let mut speaker_ids = speakers .into_iter() @@ -1005,6 +1013,7 @@ impl Room { } } + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] RoomEvent::TrackMuted { participant, publication, @@ -1029,6 +1038,7 @@ impl Room { } } + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] RoomEvent::LocalTrackUnpublished { publication, .. } => { log::info!("unpublished track {}", publication.sid()); if let Some(room) = &mut self.live_kit { @@ -1051,10 +1061,12 @@ impl Room { } } + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] RoomEvent::LocalTrackPublished { publication, .. } => { log::info!("published track {:?}", publication.sid()); } + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] RoomEvent::Disconnected { reason } => { log::info!("disconnected from room: {reason:?}"); self.leave(cx).detach_and_log_err(cx); @@ -1090,7 +1102,7 @@ impl Room { &mut self, called_user_id: u64, initial_project_id: Option, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); @@ -1124,8 +1136,8 @@ impl Room { id: u64, language_registry: Arc, fs: Arc, - cx: &mut ModelContext, - ) -> Task>> { + cx: &mut Context, + ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); cx.emit(Event::RemoteProjectJoined { project_id: id }); @@ -1149,8 +1161,8 @@ impl Room { pub fn share_project( &mut self, - project: Model, - cx: &mut ModelContext, + project: Entity, + cx: &mut Context, ) -> Task> { if let Some(project_id) = project.read(cx).remote_id() { return Task::ready(Ok(project_id)); @@ -1187,8 +1199,8 @@ impl Room { pub(crate) fn unshare_project( &mut self, - project: Model, - cx: &mut ModelContext, + project: Entity, + cx: &mut Context, ) -> Result<()> { let project_id = match project.read(cx).remote_id() { Some(project_id) => project_id, @@ -1206,8 +1218,8 @@ impl Room { pub(crate) fn set_location( &mut self, - project: Option<&Model>, - cx: &mut ModelContext, + project: Option<&Entity>, + cx: &mut Context, ) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); @@ -1284,6 +1296,13 @@ impl Room { pub fn can_use_microphone(&self) -> bool { use proto::ChannelRole::*; + #[cfg(not(any(test, feature = "test-support")))] + { + if cfg!(all(target_os = "windows", target_env = "gnu")) { + return false; + } + } + match self.local_participant.role { Admin | Member | Talker => true, Guest | Banned => false, @@ -1298,8 +1317,14 @@ impl Room { } } + #[cfg(all(target_os = "windows", target_env = "gnu"))] + pub fn share_microphone(&mut self, cx: &mut Context) -> Task> { + Task::ready(Err(anyhow!("MinGW is not supported yet"))) + } + + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] #[track_caller] - pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { + pub fn share_microphone(&mut self, cx: &mut Context) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } @@ -1375,7 +1400,13 @@ impl Room { }) } - pub fn share_screen(&mut self, cx: &mut ModelContext) -> Task> { + #[cfg(all(target_os = "windows", target_env = "gnu"))] + pub fn share_screen(&mut self, cx: &mut Context) -> Task> { + Task::ready(Err(anyhow!("MinGW is not supported yet"))) + } + + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] + pub fn share_screen(&mut self, cx: &mut Context) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } @@ -1460,7 +1491,7 @@ impl Room { }) } - pub fn toggle_mute(&mut self, cx: &mut ModelContext) { + pub fn toggle_mute(&mut self, cx: &mut Context) { if let Some(live_kit) = self.live_kit.as_mut() { // When unmuting, undeafen if the user was deafened before. let was_deafened = live_kit.deafened; @@ -1486,7 +1517,7 @@ impl Room { } } - pub fn toggle_deafen(&mut self, cx: &mut ModelContext) { + pub fn toggle_deafen(&mut self, cx: &mut Context) { if let Some(live_kit) = self.live_kit.as_mut() { // When deafening, mute the microphone if it was not already muted. // When un-deafening, unmute the microphone, unless it was explicitly muted. @@ -1504,7 +1535,7 @@ impl Room { } } - pub fn unshare_screen(&mut self, cx: &mut ModelContext) -> Result<()> { + pub fn unshare_screen(&mut self, cx: &mut Context) -> Result<()> { if self.status.is_offline() { return Err(anyhow!("room is offline")); } @@ -1522,12 +1553,15 @@ impl Room { LocalTrack::Published { track_publication, .. } => { - let local_participant = live_kit.room.local_participant(); - let sid = track_publication.sid(); - cx.background_executor() - .spawn(async move { local_participant.unpublish_track(&sid).await }) - .detach_and_log_err(cx); - cx.notify(); + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] + { + let local_participant = live_kit.room.local_participant(); + let sid = track_publication.sid(); + cx.background_executor() + .spawn(async move { local_participant.unpublish_track(&sid).await }) + .detach_and_log_err(cx); + cx.notify(); + } Audio::play_sound(Sound::StopScreenshare, cx); Ok(()) @@ -1535,13 +1569,16 @@ impl Room { } } - fn set_deafened(&mut self, deafened: bool, cx: &mut ModelContext) -> Option<()> { - let live_kit = self.live_kit.as_mut()?; - cx.notify(); - for (_, participant) in live_kit.room.remote_participants() { - for (_, publication) in participant.track_publications() { - if publication.kind() == TrackKind::Audio { - publication.set_enabled(!deafened); + fn set_deafened(&mut self, deafened: bool, cx: &mut Context) -> Option<()> { + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] + { + let live_kit = self.live_kit.as_mut()?; + cx.notify(); + for (_, participant) in live_kit.room.remote_participants() { + for (_, publication) in participant.track_publications() { + if publication.kind() == TrackKind::Audio { + publication.set_enabled(!deafened); + } } } } @@ -1549,11 +1586,7 @@ impl Room { None } - fn set_mute( - &mut self, - should_mute: bool, - cx: &mut ModelContext, - ) -> Option>> { + fn set_mute(&mut self, should_mute: bool, cx: &mut Context) -> Option>> { let live_kit = self.live_kit.as_mut()?; cx.notify(); @@ -1575,10 +1608,13 @@ impl Room { LocalTrack::Published { track_publication, .. } => { - if should_mute { - track_publication.mute() - } else { - track_publication.unmute() + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] + { + if should_mute { + track_publication.mute() + } else { + track_publication.unmute() + } } None @@ -1587,9 +1623,17 @@ impl Room { } } +#[cfg(all(target_os = "windows", target_env = "gnu"))] +fn spawn_room_connection( + livekit_connection_info: Option, + cx: &mut Context<'_, Room>, +) { +} + +#[cfg(not(all(target_os = "windows", target_env = "gnu")))] fn spawn_room_connection( livekit_connection_info: Option, - cx: &mut ModelContext<'_, Room>, + cx: &mut Context<'_, Room>, ) { if let Some(connection_info) = livekit_connection_info { cx.spawn(|this, mut cx| async move { @@ -1651,7 +1695,11 @@ struct LiveKitRoom { } impl LiveKitRoom { - fn stop_publishing(&mut self, cx: &mut ModelContext) { + #[cfg(all(target_os = "windows", target_env = "gnu"))] + fn stop_publishing(&mut self, _cx: &mut Context) {} + + #[cfg(not(all(target_os = "windows", target_env = "gnu")))] + fn stop_publishing(&mut self, cx: &mut Context) { let mut tracks_to_unpublish = Vec::new(); if let LocalTrack::Published { track_publication, .. diff --git a/crates/call/src/macos/mod.rs b/crates/call/src/macos/mod.rs index 5186a6d285a9bb..e6fb8c3cfb6e7e 100644 --- a/crates/call/src/macos/mod.rs +++ b/crates/call/src/macos/mod.rs @@ -8,8 +8,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription, - Task, WeakModel, + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Subscription, Task, + WeakEntity, }; use postage::watch; use project::Project; @@ -20,14 +20,14 @@ use std::sync::Arc; pub use participant::ParticipantLocation; pub use room::Room; -struct GlobalActiveCall(Model); +struct GlobalActiveCall(Entity); impl Global for GlobalActiveCall {} -pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { +pub fn init(client: Arc, user_store: Entity, cx: &mut App) { CallSettings::register(cx); - let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx)); + let active_call = cx.new(|cx| ActiveCall::new(client, user_store, cx)); cx.set_global(GlobalActiveCall(active_call)); } @@ -39,9 +39,9 @@ impl OneAtATime { /// spawn a task in the given context. /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None) /// otherwise you'll see the result of the task. - fn spawn(&mut self, cx: &mut AppContext, f: F) -> Task>> + fn spawn(&mut self, cx: &mut App, f: F) -> Task>> where - F: 'static + FnOnce(AsyncAppContext) -> Fut, + F: 'static + FnOnce(AsyncApp) -> Fut, Fut: Future>, R: 'static, { @@ -72,9 +72,9 @@ pub struct IncomingCall { /// Singleton global maintaining the user's participation in a room across workspaces. pub struct ActiveCall { - room: Option<(Model, Vec)>, - pending_room_creation: Option, Arc>>>>, - location: Option>, + room: Option<(Entity, Vec)>, + pending_room_creation: Option, Arc>>>>, + location: Option>, _join_debouncer: OneAtATime, pending_invites: HashSet, incoming_call: ( @@ -82,14 +82,14 @@ pub struct ActiveCall { watch::Receiver>, ), client: Arc, - user_store: Model, + user_store: Entity, _subscriptions: Vec, } impl EventEmitter for ActiveCall {} impl ActiveCall { - fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { + fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { Self { room: None, pending_room_creation: None, @@ -98,22 +98,22 @@ impl ActiveCall { incoming_call: watch::channel(), _join_debouncer: OneAtATime { cancel: None }, _subscriptions: vec![ - client.add_request_handler(cx.weak_model(), Self::handle_incoming_call), - client.add_message_handler(cx.weak_model(), Self::handle_call_canceled), + client.add_request_handler(cx.weak_entity(), Self::handle_incoming_call), + client.add_message_handler(cx.weak_entity(), Self::handle_call_canceled), ], client, user_store, } } - pub fn channel_id(&self, cx: &AppContext) -> Option { + pub fn channel_id(&self, cx: &App) -> Option { self.room()?.read(cx).channel_id() } async fn handle_incoming_call( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result { let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; let call = IncomingCall { @@ -138,9 +138,9 @@ impl ActiveCall { } async fn handle_call_canceled( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, _| { let mut incoming_call = this.incoming_call.0.borrow_mut(); @@ -154,11 +154,11 @@ impl ActiveCall { Ok(()) } - pub fn global(cx: &AppContext) -> Model { + pub fn global(cx: &App) -> Entity { cx.global::().0.clone() } - pub fn try_global(cx: &AppContext) -> Option> { + pub fn try_global(cx: &App) -> Option> { cx.try_global::() .map(|call| call.0.clone()) } @@ -166,8 +166,8 @@ impl ActiveCall { pub fn invite( &mut self, called_user_id: u64, - initial_project: Option>, - cx: &mut ModelContext, + initial_project: Option>, + cx: &mut Context, ) -> Task> { if !self.pending_invites.insert(called_user_id) { return Task::ready(Err(anyhow!("user was already invited"))); @@ -262,7 +262,7 @@ impl ActiveCall { pub fn cancel_invite( &mut self, called_user_id: u64, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let room_id = if let Some(room) = self.room() { room.read(cx).id() @@ -286,7 +286,7 @@ impl ActiveCall { self.incoming_call.1.clone() } - pub fn accept_incoming(&mut self, cx: &mut ModelContext) -> Task> { + pub fn accept_incoming(&mut self, cx: &mut Context) -> Task> { if self.room.is_some() { return Task::ready(Err(anyhow!("cannot join while on another call"))); } @@ -319,7 +319,7 @@ impl ActiveCall { }) } - pub fn decline_incoming(&mut self, _: &mut ModelContext) -> Result<()> { + pub fn decline_incoming(&mut self, _: &mut Context) -> Result<()> { let call = self .incoming_call .0 @@ -336,8 +336,8 @@ impl ActiveCall { pub fn join_channel( &mut self, channel_id: ChannelId, - cx: &mut ModelContext, - ) -> Task>>> { + cx: &mut Context, + ) -> Task>>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { return Task::ready(Ok(Some(room))); @@ -367,7 +367,7 @@ impl ActiveCall { }) } - pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { + pub fn hang_up(&mut self, cx: &mut Context) -> Task> { cx.notify(); self.report_call_event("Call Ended", cx); @@ -384,8 +384,8 @@ impl ActiveCall { pub fn share_project( &mut self, - project: Model, - cx: &mut ModelContext, + project: Entity, + cx: &mut Context, ) -> Task> { if let Some((room, _)) = self.room.as_ref() { self.report_call_event("Project Shared", cx); @@ -397,8 +397,8 @@ impl ActiveCall { pub fn unshare_project( &mut self, - project: Model, - cx: &mut ModelContext, + project: Entity, + cx: &mut Context, ) -> Result<()> { if let Some((room, _)) = self.room.as_ref() { self.report_call_event("Project Unshared", cx); @@ -408,14 +408,14 @@ impl ActiveCall { } } - pub fn location(&self) -> Option<&WeakModel> { + pub fn location(&self) -> Option<&WeakEntity> { self.location.as_ref() } pub fn set_location( &mut self, - project: Option<&Model>, - cx: &mut ModelContext, + project: Option<&Entity>, + cx: &mut Context, ) -> Task> { if project.is_some() || !*ZED_ALWAYS_ACTIVE { self.location = project.map(|project| project.downgrade()); @@ -426,11 +426,7 @@ impl ActiveCall { Task::ready(Ok(())) } - fn set_room( - &mut self, - room: Option>, - cx: &mut ModelContext, - ) -> Task> { + fn set_room(&mut self, room: Option>, cx: &mut Context) -> Task> { if room.as_ref() == self.room.as_ref().map(|room| &room.0) { Task::ready(Ok(())) } else { @@ -466,7 +462,7 @@ impl ActiveCall { } } - pub fn room(&self) -> Option<&Model> { + pub fn room(&self) -> Option<&Entity> { self.room.as_ref().map(|(room, _)| room) } @@ -478,7 +474,7 @@ impl ActiveCall { &self.pending_invites } - pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { + pub fn report_call_event(&self, operation: &'static str, cx: &mut App) { if let Some(room) = self.room() { let room = room.read(cx); telemetry::event!( diff --git a/crates/call/src/macos/participant.rs b/crates/call/src/macos/participant.rs index dc7527487fb689..1f1a63a10e0a32 100644 --- a/crates/call/src/macos/participant.rs +++ b/crates/call/src/macos/participant.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use client::ParticipantIndex; use client::{proto, User}; use collections::HashMap; -use gpui::WeakModel; +use gpui::WeakEntity; pub use livekit_client_macos::Frame; pub use livekit_client_macos::{RemoteAudioTrack, RemoteVideoTrack}; use project::Project; @@ -35,7 +35,7 @@ impl ParticipantLocation { #[derive(Clone, Default)] pub struct LocalParticipant { pub projects: Vec, - pub active_project: Option>, + pub active_project: Option>, pub role: proto::ChannelRole, } diff --git a/crates/call/src/macos/room.rs b/crates/call/src/macos/room.rs index 987c056437a6d5..b0c8ad9678788c 100644 --- a/crates/call/src/macos/room.rs +++ b/crates/call/src/macos/room.rs @@ -11,9 +11,7 @@ use client::{ use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; use futures::{FutureExt, StreamExt}; -use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, -}; +use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; use language::LanguageRegistry; use livekit_client_macos::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate}; use postage::{sink::Sink, stream::Stream, watch}; @@ -62,8 +60,8 @@ pub struct Room { channel_id: Option, live_kit: Option, status: RoomStatus, - shared_projects: HashSet>, - joined_projects: HashSet>, + shared_projects: HashSet>, + joined_projects: HashSet>, local_participant: LocalParticipant, remote_participants: BTreeMap, pending_participants: Vec>, @@ -71,7 +69,7 @@ pub struct Room { pending_call_count: usize, leave_when_empty: bool, client: Arc, - user_store: Model, + user_store: Entity, follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, client_subscriptions: Vec, _subscriptions: Vec, @@ -109,8 +107,8 @@ impl Room { channel_id: Option, live_kit_connection_info: Option, client: Arc, - user_store: Model, - cx: &mut ModelContext, + user_store: Entity, + cx: &mut Context, ) -> Self { let live_kit_room = if let Some(connection_info) = live_kit_connection_info { let room = livekit_client_macos::Room::new(); @@ -206,7 +204,7 @@ impl Room { pending_participants: Default::default(), pending_call_count: 0, client_subscriptions: vec![ - client.add_message_handler(cx.weak_model(), Self::handle_room_updated) + client.add_message_handler(cx.weak_entity(), Self::handle_room_updated) ], _subscriptions: vec![ cx.on_release(Self::released), @@ -225,15 +223,15 @@ impl Room { pub(crate) fn create( called_user_id: u64, - initial_project: Option>, + initial_project: Option>, client: Arc, - user_store: Model, - cx: &mut AppContext, - ) -> Task>> { + user_store: Entity, + cx: &mut App, + ) -> Task>> { cx.spawn(move |mut cx| async move { let response = client.request(proto::CreateRoom {}).await?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.new_model(|cx| { + let room = cx.new(|cx| { let mut room = Self::new( room_proto.id, None, @@ -275,9 +273,9 @@ impl Room { pub(crate) async fn join_channel( channel_id: ChannelId, client: Arc, - user_store: Model, - cx: AsyncAppContext, - ) -> Result> { + user_store: Entity, + cx: AsyncApp, + ) -> Result> { Self::from_join_response( client .request(proto::JoinChannel { @@ -293,9 +291,9 @@ impl Room { pub(crate) async fn join( room_id: u64, client: Arc, - user_store: Model, - cx: AsyncAppContext, - ) -> Result> { + user_store: Entity, + cx: AsyncApp, + ) -> Result> { Self::from_join_response( client.request(proto::JoinRoom { id: room_id }).await?, client, @@ -304,13 +302,13 @@ impl Room { ) } - fn released(&mut self, cx: &mut AppContext) { + fn released(&mut self, cx: &mut App) { if self.status.is_online() { self.leave_internal(cx).detach_and_log_err(cx); } } - fn app_will_quit(&mut self, cx: &mut ModelContext) -> impl Future { + fn app_will_quit(&mut self, cx: &mut Context) -> impl Future { let task = if self.status.is_online() { let leave = self.leave_internal(cx); Some(cx.background_executor().spawn(async move { @@ -327,18 +325,18 @@ impl Room { } } - pub fn mute_on_join(cx: &AppContext) -> bool { + pub fn mute_on_join(cx: &App) -> bool { CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } fn from_join_response( response: proto::JoinRoomResponse, client: Arc, - user_store: Model, - mut cx: AsyncAppContext, - ) -> Result> { + user_store: Entity, + mut cx: AsyncApp, + ) -> Result> { let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.new_model(|cx| { + let room = cx.new(|cx| { Self::new( room_proto.id, response.channel_id.map(ChannelId), @@ -364,12 +362,12 @@ impl Room { && self.pending_call_count == 0 } - pub(crate) fn leave(&mut self, cx: &mut ModelContext) -> Task> { + pub(crate) fn leave(&mut self, cx: &mut Context) -> Task> { cx.notify(); self.leave_internal(cx) } - fn leave_internal(&mut self, cx: &mut AppContext) -> Task> { + fn leave_internal(&mut self, cx: &mut App) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } @@ -386,7 +384,7 @@ impl Room { }) } - pub(crate) fn clear_state(&mut self, cx: &mut AppContext) { + pub(crate) fn clear_state(&mut self, cx: &mut App) { for project in self.shared_projects.drain() { if let Some(project) = project.upgrade() { project.update(cx, |project, cx| { @@ -414,9 +412,9 @@ impl Room { } async fn maintain_connection( - this: WeakModel, + this: WeakEntity, client: Arc, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { let mut client_status = client.status(); loop { @@ -500,7 +498,7 @@ impl Room { )) } - fn rejoin(&mut self, cx: &mut ModelContext) -> Task> { + fn rejoin(&mut self, cx: &mut Context) -> Task> { let mut projects = HashMap::default(); let mut reshared_projects = Vec::new(); let mut rejoined_projects = Vec::new(); @@ -626,7 +624,7 @@ impl Room { &mut self, user_id: u64, role: proto::ChannelRole, - cx: &ModelContext, + cx: &Context, ) -> Task> { let client = self.client.clone(); let room_id = self.id; @@ -658,7 +656,7 @@ impl Room { } /// Returns the most 'active' projects, defined as most people in the project - pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> { + pub fn most_active_project(&self, cx: &App) -> Option<(u64, u64)> { let mut project_hosts_and_guest_counts = HashMap::, u32)>::default(); for participant in self.remote_participants.values() { match participant.location { @@ -695,9 +693,9 @@ impl Room { } async fn handle_room_updated( - this: Model, + this: Entity, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { let room = envelope .payload @@ -706,11 +704,7 @@ impl Room { this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))? } - fn apply_room_update( - &mut self, - mut room: proto::Room, - cx: &mut ModelContext, - ) -> Result<()> { + fn apply_room_update(&mut self, mut room: proto::Room, cx: &mut Context) -> Result<()> { // Filter ourselves out from the room's participants. let local_participant_ix = room .participants @@ -976,11 +970,7 @@ impl Room { } } - fn live_kit_room_updated( - &mut self, - update: RoomUpdate, - cx: &mut ModelContext, - ) -> Result<()> { + fn live_kit_room_updated(&mut self, update: RoomUpdate, cx: &mut Context) -> Result<()> { match update { RoomUpdate::SubscribedToRemoteVideoTrack(track) => { let user_id = track.publisher_id().parse()?; @@ -1132,7 +1122,7 @@ impl Room { &mut self, called_user_id: u64, initial_project_id: Option, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); @@ -1166,8 +1156,8 @@ impl Room { id: u64, language_registry: Arc, fs: Arc, - cx: &mut ModelContext, - ) -> Task>> { + cx: &mut Context, + ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); cx.emit(Event::RemoteProjectJoined { project_id: id }); @@ -1191,8 +1181,8 @@ impl Room { pub fn share_project( &mut self, - project: Model, - cx: &mut ModelContext, + project: Entity, + cx: &mut Context, ) -> Task> { if let Some(project_id) = project.read(cx).remote_id() { return Task::ready(Ok(project_id)); @@ -1229,8 +1219,8 @@ impl Room { pub(crate) fn unshare_project( &mut self, - project: Model, - cx: &mut ModelContext, + project: Entity, + cx: &mut Context, ) -> Result<()> { let project_id = match project.read(cx).remote_id() { Some(project_id) => project_id, @@ -1248,8 +1238,8 @@ impl Room { pub(crate) fn set_location( &mut self, - project: Option<&Model>, - cx: &mut ModelContext, + project: Option<&Entity>, + cx: &mut Context, ) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); @@ -1340,7 +1330,7 @@ impl Room { } #[track_caller] - pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { + pub fn share_microphone(&mut self, cx: &mut Context) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } @@ -1416,7 +1406,7 @@ impl Room { }) } - pub fn share_screen(&mut self, cx: &mut ModelContext) -> Task> { + pub fn share_screen(&mut self, cx: &mut Context) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } else if self.is_screen_sharing() { @@ -1497,7 +1487,7 @@ impl Room { }) } - pub fn toggle_mute(&mut self, cx: &mut ModelContext) { + pub fn toggle_mute(&mut self, cx: &mut Context) { if let Some(live_kit) = self.live_kit.as_mut() { // When unmuting, undeafen if the user was deafened before. let was_deafened = live_kit.deafened; @@ -1525,7 +1515,7 @@ impl Room { } } - pub fn toggle_deafen(&mut self, cx: &mut ModelContext) { + pub fn toggle_deafen(&mut self, cx: &mut Context) { if let Some(live_kit) = self.live_kit.as_mut() { // When deafening, mute the microphone if it was not already muted. // When un-deafening, unmute the microphone, unless it was explicitly muted. @@ -1545,7 +1535,7 @@ impl Room { } } - pub fn unshare_screen(&mut self, cx: &mut ModelContext) -> Result<()> { + pub fn unshare_screen(&mut self, cx: &mut Context) -> Result<()> { if self.status.is_offline() { return Err(anyhow!("room is offline")); } @@ -1572,11 +1562,7 @@ impl Room { } } - fn set_deafened( - &mut self, - deafened: bool, - cx: &mut ModelContext, - ) -> Option>> { + fn set_deafened(&mut self, deafened: bool, cx: &mut Context) -> Option>> { let live_kit = self.live_kit.as_mut()?; cx.notify(); @@ -1606,11 +1592,7 @@ impl Room { })) } - fn set_mute( - &mut self, - should_mute: bool, - cx: &mut ModelContext, - ) -> Option>> { + fn set_mute(&mut self, should_mute: bool, cx: &mut Context) -> Option>> { let live_kit = self.live_kit.as_mut()?; cx.notify(); @@ -1660,7 +1642,7 @@ struct LiveKitRoom { } impl LiveKitRoom { - fn stop_publishing(&mut self, cx: &mut ModelContext) { + fn stop_publishing(&mut self, cx: &mut Context) { if let LocalTrack::Published { track_publication, .. } = mem::replace(&mut self.microphone_track, LocalTrack::None) diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index b9547bef1abb42..13b66c7dc3a7e1 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -3,7 +3,7 @@ mod channel_chat; mod channel_store; use client::{Client, UserStore}; -use gpui::{AppContext, Model}; +use gpui::{App, Entity}; use std::sync::Arc; pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; @@ -16,7 +16,7 @@ pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore}; #[cfg(test)] mod channel_store_tests; -pub fn init(client: &Arc, user_store: Model, cx: &mut AppContext) { +pub fn init(client: &Arc, user_store: Entity, cx: &mut App) { channel_store::init(client, user_store, cx); channel_buffer::init(&client.clone().into()); channel_chat::init(&client.clone().into()); diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 0a4a259648bb74..3ccfb0503ca693 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -2,7 +2,7 @@ use crate::{Channel, ChannelStore}; use anyhow::Result; use client::{ChannelId, Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashMap; -use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task}; +use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task}; use language::proto::serialize_version; use rpc::{ proto::{self, PeerId}, @@ -23,9 +23,9 @@ pub struct ChannelBuffer { pub channel_id: ChannelId, connected: bool, collaborators: HashMap, - user_store: Model, - channel_store: Model, - buffer: Model, + user_store: Entity, + channel_store: Entity, + buffer: Entity, buffer_epoch: u64, client: Arc, subscription: Option, @@ -45,10 +45,10 @@ impl ChannelBuffer { pub(crate) async fn new( channel: Arc, client: Arc, - user_store: Model, - channel_store: Model, - mut cx: AsyncAppContext, - ) -> Result> { + user_store: Entity, + channel_store: Entity, + mut cx: AsyncApp, + ) -> Result> { let response = client .request(proto::JoinChannelBuffer { channel_id: channel.id.0, @@ -62,7 +62,7 @@ impl ChannelBuffer { .map(language::proto::deserialize_operation) .collect::, _>>()?; - let buffer = cx.new_model(|cx| { + let buffer = cx.new(|cx| { let capability = channel_store.read(cx).channel_capability(channel.id); language::Buffer::remote(buffer_id, response.replica_id as u16, capability, base_text) })?; @@ -70,7 +70,7 @@ impl ChannelBuffer { let subscription = client.subscribe_to_entity(channel.id.0)?; - anyhow::Ok(cx.new_model(|cx| { + anyhow::Ok(cx.new(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); cx.on_release(Self::release).detach(); let mut this = Self { @@ -81,7 +81,7 @@ impl ChannelBuffer { collaborators: Default::default(), acknowledge_task: None, channel_id: channel.id, - subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), + subscription: Some(subscription.set_model(&cx.entity(), &mut cx.to_async())), user_store, channel_store, }; @@ -90,7 +90,7 @@ impl ChannelBuffer { })?) } - fn release(&mut self, _: &mut AppContext) { + fn release(&mut self, _: &mut App) { if self.connected { if let Some(task) = self.acknowledge_task.take() { task.detach(); @@ -103,18 +103,18 @@ impl ChannelBuffer { } } - pub fn remote_id(&self, cx: &AppContext) -> BufferId { + pub fn remote_id(&self, cx: &App) -> BufferId { self.buffer.read(cx).remote_id() } - pub fn user_store(&self) -> &Model { + pub fn user_store(&self) -> &Entity { &self.user_store } pub(crate) fn replace_collaborators( &mut self, collaborators: Vec, - cx: &mut ModelContext, + cx: &mut Context, ) { let mut new_collaborators = HashMap::default(); for collaborator in collaborators { @@ -136,9 +136,9 @@ impl ChannelBuffer { } async fn handle_update_channel_buffer( - this: Model, + this: Entity, update_channel_buffer: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { let ops = update_channel_buffer .payload @@ -157,9 +157,9 @@ impl ChannelBuffer { } async fn handle_update_channel_buffer_collaborators( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { this.replace_collaborators(message.payload.collaborators, cx); @@ -170,9 +170,9 @@ impl ChannelBuffer { fn on_buffer_update( &mut self, - _: Model, + _: Entity, event: &language::BufferEvent, - cx: &mut ModelContext, + cx: &mut Context, ) { match event { language::BufferEvent::Operation { @@ -201,7 +201,7 @@ impl ChannelBuffer { } } - pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) { + pub fn acknowledge_buffer_version(&mut self, cx: &mut Context<'_, ChannelBuffer>) { let buffer = self.buffer.read(cx); let version = buffer.version(); let buffer_id = buffer.remote_id().into(); @@ -227,7 +227,7 @@ impl ChannelBuffer { self.buffer_epoch } - pub fn buffer(&self) -> Model { + pub fn buffer(&self) -> Entity { self.buffer.clone() } @@ -235,14 +235,14 @@ impl ChannelBuffer { &self.collaborators } - pub fn channel(&self, cx: &AppContext) -> Option> { + pub fn channel(&self, cx: &App) -> Option> { self.channel_store .read(cx) .channel_for_id(self.channel_id) .cloned() } - pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { + pub(crate) fn disconnect(&mut self, cx: &mut Context) { log::info!("channel buffer {} disconnected", self.channel_id); if self.connected { self.connected = false; @@ -252,7 +252,7 @@ impl ChannelBuffer { } } - pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext) { + pub(crate) fn channel_changed(&mut self, cx: &mut Context) { cx.emit(ChannelBufferEvent::ChannelChanged); cx.notify() } @@ -261,7 +261,7 @@ impl ChannelBuffer { self.connected } - pub fn replica_id(&self, cx: &AppContext) -> u16 { + pub fn replica_id(&self, cx: &App) -> u16 { self.buffer.read(cx).replica_id() } } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index e5b5b74c16262b..7707d300f75a72 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -7,9 +7,7 @@ use client::{ }; use collections::HashSet; use futures::lock::Mutex; -use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, -}; +use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; use rand::prelude::*; use rpc::AnyProtoClient; use std::{ @@ -24,12 +22,12 @@ pub struct ChannelChat { pub channel_id: ChannelId, messages: SumTree, acknowledged_message_ids: HashSet, - channel_store: Model, + channel_store: Entity, loaded_all_messages: bool, last_acknowledged_id: Option, next_pending_message_id: usize, first_loaded_message_id: Option, - user_store: Model, + user_store: Entity, rpc: Arc, outgoing_messages_lock: Arc>, rng: StdRng, @@ -105,11 +103,11 @@ pub fn init(client: &AnyProtoClient) { impl ChannelChat { pub async fn new( channel: Arc, - channel_store: Model, - user_store: Model, + channel_store: Entity, + user_store: Entity, client: Arc, - mut cx: AsyncAppContext, - ) -> Result> { + mut cx: AsyncApp, + ) -> Result> { let channel_id = channel.id; let subscription = client.subscribe_to_entity(channel_id.0).unwrap(); @@ -119,7 +117,7 @@ impl ChannelChat { }) .await?; - let handle = cx.new_model(|cx| { + let handle = cx.new(|cx| { cx.on_release(Self::release).detach(); Self { channel_id: channel.id, @@ -134,7 +132,7 @@ impl ChannelChat { last_acknowledged_id: None, rng: StdRng::from_entropy(), first_loaded_message_id: None, - _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), + _subscription: subscription.set_model(&cx.entity(), &mut cx.to_async()), } })?; Self::handle_loaded_messages( @@ -149,7 +147,7 @@ impl ChannelChat { Ok(handle) } - fn release(&mut self, _: &mut AppContext) { + fn release(&mut self, _: &mut App) { self.rpc .send(proto::LeaveChannelChat { channel_id: self.channel_id.0, @@ -157,7 +155,7 @@ impl ChannelChat { .log_err(); } - pub fn channel(&self, cx: &AppContext) -> Option> { + pub fn channel(&self, cx: &App) -> Option> { self.channel_store .read(cx) .channel_for_id(self.channel_id) @@ -171,7 +169,7 @@ impl ChannelChat { pub fn send_message( &mut self, message: MessageParams, - cx: &mut ModelContext, + cx: &mut Context, ) -> Result>> { if message.text.trim().is_empty() { Err(anyhow!("message body can't be empty"))?; @@ -231,7 +229,7 @@ impl ChannelChat { })) } - pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext) -> Task> { + pub fn remove_message(&mut self, id: u64, cx: &mut Context) -> Task> { let response = self.rpc.request(proto::RemoveChannelMessage { channel_id: self.channel_id.0, message_id: id, @@ -249,7 +247,7 @@ impl ChannelChat { &mut self, id: u64, message: MessageParams, - cx: &mut ModelContext, + cx: &mut Context, ) -> Result>> { self.message_update( ChannelMessageId::Saved(id), @@ -274,7 +272,7 @@ impl ChannelChat { })) } - pub fn load_more_messages(&mut self, cx: &mut ModelContext) -> Option>> { + pub fn load_more_messages(&mut self, cx: &mut Context) -> Option>> { if self.loaded_all_messages { return None; } @@ -323,9 +321,9 @@ impl ChannelChat { /// /// For now, we always maintain a suffix of the channel's messages. pub async fn load_history_since_message( - chat: Model, + chat: Entity, message_id: u64, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Option { loop { let step = chat @@ -357,7 +355,7 @@ impl ChannelChat { } } - pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext) { + pub fn acknowledge_last_message(&mut self, cx: &mut Context) { if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id { if self .last_acknowledged_id @@ -378,12 +376,12 @@ impl ChannelChat { } async fn handle_loaded_messages( - this: WeakModel, - user_store: Model, + this: WeakEntity, + user_store: Entity, rpc: Arc, proto_messages: Vec, loaded_all_messages: bool, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result<()> { let loaded_messages = messages_from_proto(proto_messages, &user_store, cx).await?; @@ -437,7 +435,7 @@ impl ChannelChat { Ok(()) } - pub fn rejoin(&mut self, cx: &mut ModelContext) { + pub fn rejoin(&mut self, cx: &mut Context) { let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); let channel_id = self.channel_id; @@ -527,9 +525,9 @@ impl ChannelChat { } async fn handle_message_sent( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; let message = message @@ -551,9 +549,9 @@ impl ChannelChat { } async fn handle_message_removed( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { this.message_removed(message.payload.message_id, cx) @@ -562,9 +560,9 @@ impl ChannelChat { } async fn handle_message_updated( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; let message = message @@ -586,7 +584,7 @@ impl ChannelChat { Ok(()) } - fn insert_messages(&mut self, messages: SumTree, cx: &mut ModelContext) { + fn insert_messages(&mut self, messages: SumTree, cx: &mut Context) { if let Some((first_message, last_message)) = messages.first().zip(messages.last()) { let nonces = messages .cursor::<()>(&()) @@ -645,7 +643,7 @@ impl ChannelChat { } } - fn message_removed(&mut self, id: u64, cx: &mut ModelContext) { + fn message_removed(&mut self, id: u64, cx: &mut Context) { let mut cursor = self.messages.cursor::(&()); let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &()); if let Some(item) = cursor.item() { @@ -683,7 +681,7 @@ impl ChannelChat { body: String, mentions: Vec<(Range, u64)>, edited_at: Option, - cx: &mut ModelContext, + cx: &mut Context, ) { let mut cursor = self.messages.cursor::(&()); let mut messages = cursor.slice(&id, Bias::Left, &()); @@ -712,8 +710,8 @@ impl ChannelChat { async fn messages_from_proto( proto_messages: Vec, - user_store: &Model, - cx: &mut AsyncAppContext, + user_store: &Entity, + cx: &mut AsyncApp, ) -> Result> { let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?; let mut result = SumTree::default(); @@ -724,8 +722,8 @@ async fn messages_from_proto( impl ChannelMessage { pub async fn from_proto( message: proto::ChannelMessage, - user_store: &Model, - cx: &mut AsyncAppContext, + user_store: &Entity, + cx: &mut AsyncApp, ) -> Result { let sender = user_store .update(cx, |user_store, cx| { @@ -769,8 +767,8 @@ impl ChannelMessage { pub async fn from_proto_vec( proto_messages: Vec, - user_store: &Model, - cx: &mut AsyncAppContext, + user_store: &Entity, + cx: &mut AsyncApp, ) -> Result> { let unique_user_ids = proto_messages .iter() diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index d627d8fe15a988..8ba015f5720e0f 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -7,8 +7,8 @@ use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, User use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, SharedString, - Task, WeakModel, + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, SharedString, Task, + WeakEntity, }; use language::Capability; use rpc::{ @@ -21,9 +21,8 @@ use util::{maybe, ResultExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); -pub fn init(client: &Arc, user_store: Model, cx: &mut AppContext) { - let channel_store = - cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); +pub fn init(client: &Arc, user_store: Entity, cx: &mut App) { + let channel_store = cx.new(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); cx.set_global(GlobalChannelStore(channel_store)); } @@ -44,7 +43,7 @@ pub struct ChannelStore { opened_chats: HashMap>, client: Arc, did_subscribe: bool, - user_store: Model, + user_store: Entity, _rpc_subscriptions: [Subscription; 2], _watch_connection_status: Task>, disconnect_channel_buffers_task: Option>, @@ -69,7 +68,7 @@ pub struct ChannelState { } impl Channel { - pub fn link(&self, cx: &AppContext) -> String { + pub fn link(&self, cx: &App) -> String { format!( "{}/channel/{}-{}", ClientSettings::get_global(cx).server_url, @@ -78,7 +77,7 @@ impl Channel { ) } - pub fn notes_link(&self, heading: Option, cx: &AppContext) -> String { + pub fn notes_link(&self, heading: Option, cx: &App) -> String { self.link(cx) + "/notes" + &heading @@ -144,27 +143,23 @@ pub enum ChannelEvent { impl EventEmitter for ChannelStore {} enum OpenedModelHandle { - Open(WeakModel), - Loading(Shared, Arc>>>), + Open(WeakEntity), + Loading(Shared, Arc>>>), } -struct GlobalChannelStore(Model); +struct GlobalChannelStore(Entity); impl Global for GlobalChannelStore {} impl ChannelStore { - pub fn global(cx: &AppContext) -> Model { + pub fn global(cx: &App) -> Entity { cx.global::().0.clone() } - pub fn new( - client: Arc, - user_store: Model, - cx: &mut ModelContext, - ) -> Self { + pub fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { let rpc_subscriptions = [ - client.add_message_handler(cx.weak_model(), Self::handle_update_channels), - client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels), + client.add_message_handler(cx.weak_entity(), Self::handle_update_channels), + client.add_message_handler(cx.weak_entity(), Self::handle_update_user_channels), ]; let mut connection_status = client.status(); @@ -295,7 +290,7 @@ impl ChannelStore { self.channel_index.by_id().get(&channel_id) } - pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool { + pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &App) -> bool { if let Some(buffer) = self.opened_buffers.get(&channel_id) { if let OpenedModelHandle::Open(buffer) = buffer { return buffer.upgrade().is_some(); @@ -307,11 +302,11 @@ impl ChannelStore { pub fn open_channel_buffer( &mut self, channel_id: ChannelId, - cx: &mut ModelContext, - ) -> Task>> { + cx: &mut Context, + ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); - let channel_store = cx.handle(); + let channel_store = cx.entity(); self.open_channel_resource( channel_id, |this| &mut this.opened_buffers, @@ -323,7 +318,7 @@ impl ChannelStore { pub fn fetch_channel_messages( &self, message_ids: Vec, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task>> { let request = if message_ids.is_empty() { None @@ -384,7 +379,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, message_id: u64, - cx: &mut ModelContext, + cx: &mut Context, ) { self.channel_states .entry(channel_id) @@ -397,7 +392,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, message_id: u64, - cx: &mut ModelContext, + cx: &mut Context, ) { self.channel_states .entry(channel_id) @@ -411,7 +406,7 @@ impl ChannelStore { channel_id: ChannelId, epoch: u64, version: &clock::Global, - cx: &mut ModelContext, + cx: &mut Context, ) { self.channel_states .entry(channel_id) @@ -425,7 +420,7 @@ impl ChannelStore { channel_id: ChannelId, epoch: u64, version: &clock::Global, - cx: &mut ModelContext, + cx: &mut Context, ) { self.channel_states .entry(channel_id) @@ -437,11 +432,11 @@ impl ChannelStore { pub fn open_channel_chat( &mut self, channel_id: ChannelId, - cx: &mut ModelContext, - ) -> Task>> { + cx: &mut Context, + ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); - let this = cx.handle(); + let this = cx.entity(); self.open_channel_resource( channel_id, |this| &mut this.opened_chats, @@ -460,11 +455,11 @@ impl ChannelStore { channel_id: ChannelId, get_map: fn(&mut Self) -> &mut HashMap>, load: F, - cx: &mut ModelContext, - ) -> Task>> + cx: &mut Context, + ) -> Task>> where - F: 'static + FnOnce(Arc, AsyncAppContext) -> Fut, - Fut: Future>>, + F: 'static + FnOnce(Arc, AsyncApp) -> Fut, + Fut: Future>>, T: 'static, { let task = loop { @@ -572,7 +567,7 @@ impl ChannelStore { &self, name: &str, parent_id: Option, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let client = self.client.clone(); let name = name.trim_start_matches('#').to_owned(); @@ -614,7 +609,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, to: ChannelId, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let client = self.client.clone(); cx.spawn(move |_, _| async move { @@ -633,7 +628,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, visibility: ChannelVisibility, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let client = self.client.clone(); cx.spawn(move |_, _| async move { @@ -653,7 +648,7 @@ impl ChannelStore { channel_id: ChannelId, user_id: UserId, role: proto::ChannelRole, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { if !self.outgoing_invites.insert((channel_id, user_id)) { return Task::ready(Err(anyhow!("invite request already in progress"))); @@ -685,7 +680,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, user_id: u64, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { if !self.outgoing_invites.insert((channel_id, user_id)) { return Task::ready(Err(anyhow!("invite request already in progress"))); @@ -715,7 +710,7 @@ impl ChannelStore { channel_id: ChannelId, user_id: UserId, role: proto::ChannelRole, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { if !self.outgoing_invites.insert((channel_id, user_id)) { return Task::ready(Err(anyhow!("member request already in progress"))); @@ -746,7 +741,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, new_name: &str, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let client = self.client.clone(); let name = new_name.to_string(); @@ -783,7 +778,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, accept: bool, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let client = self.client.clone(); cx.background_executor().spawn(async move { @@ -801,7 +796,7 @@ impl ChannelStore { channel_id: ChannelId, query: String, limit: u16, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.downgrade(); @@ -851,9 +846,9 @@ impl ChannelStore { } async fn handle_update_channels( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, _| { this.update_channels_tx @@ -864,9 +859,9 @@ impl ChannelStore { } async fn handle_update_user_channels( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { for buffer_version in message.payload.observed_channel_buffer_version { @@ -896,7 +891,7 @@ impl ChannelStore { }) } - fn handle_connect(&mut self, cx: &mut ModelContext) -> Task> { + fn handle_connect(&mut self, cx: &mut Context) -> Task> { self.channel_index.clear(); self.channel_invitations.clear(); self.channel_participants.clear(); @@ -1011,7 +1006,7 @@ impl ChannelStore { }) } - fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext) { + fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut Context) { cx.notify(); self.did_subscribe = false; self.disconnect_channel_buffers_task.get_or_insert_with(|| { @@ -1039,7 +1034,7 @@ impl ChannelStore { pub(crate) fn update_channels( &mut self, payload: proto::UpdateChannels, - cx: &mut ModelContext, + cx: &mut Context, ) -> Option>> { if !payload.remove_channel_invitations.is_empty() { self.channel_invitations diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index ef657d37397b3f..779849df246fc6 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -3,13 +3,13 @@ use crate::channel_chat::ChannelChatEvent; use super::*; use client::{test::FakeServer, Client, UserStore}; use clock::FakeSystemClock; -use gpui::{AppContext, Context, Model, SemanticVersion, TestAppContext}; +use gpui::{App, AppContext as _, Entity, SemanticVersion, TestAppContext}; use http_client::FakeHttpClient; use rpc::proto::{self}; use settings::SettingsStore; #[gpui::test] -fn test_update_channels(cx: &mut AppContext) { +fn test_update_channels(cx: &mut App) { let channel_store = init_test(cx); update_channels( @@ -77,7 +77,7 @@ fn test_update_channels(cx: &mut AppContext) { } #[gpui::test] -fn test_dangling_channel_paths(cx: &mut AppContext) { +fn test_dangling_channel_paths(cx: &mut App) { let channel_store = init_test(cx); update_channels( @@ -343,7 +343,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { }); } -fn init_test(cx: &mut AppContext) -> Model { +fn init_test(cx: &mut App) -> Entity { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); release_channel::init(SemanticVersion::default(), cx); @@ -352,7 +352,7 @@ fn init_test(cx: &mut AppContext) -> Model { let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_404_response(); let client = Client::new(clock, http.clone(), cx); - let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); client::init(&client, cx); crate::init(&client, user_store, cx); @@ -361,9 +361,9 @@ fn init_test(cx: &mut AppContext) -> Model { } fn update_channels( - channel_store: &Model, + channel_store: &Entity, message: proto::UpdateChannels, - cx: &mut AppContext, + cx: &mut App, ) { let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx)); assert!(task.is_none()); @@ -371,9 +371,9 @@ fn update_channels( #[track_caller] fn assert_channels( - channel_store: &Model, + channel_store: &Entity, expected_channels: &[(usize, String)], - cx: &mut AppContext, + cx: &mut App, ) { let actual = channel_store.update(cx, |store, _| { store diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2ecda7d86963b6..5585709d56fa59 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -3,7 +3,7 @@ allow(dead_code) )] -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use clap::Parser; use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake}; use collections::HashMap; @@ -536,7 +536,7 @@ mod windows { #[cfg(target_os = "macos")] mod mac_os { - use anyhow::{anyhow, Context, Result}; + use anyhow::{anyhow, Context as _, Result}; use core_foundation::{ array::{CFArray, CFIndex}, string::kCFStringEncodingUTF8, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f64dbc110ef44d..0bc71cb2da02ce 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -19,7 +19,7 @@ use futures::{ channel::oneshot, future::BoxFuture, AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt, }; -use gpui::{actions, AppContext, AsyncAppContext, Global, Model, Task, WeakModel}; +use gpui::{actions, App, AsyncApp, Entity, Global, Task, WeakEntity}; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use parking_lot::RwLock; use postage::watch; @@ -104,7 +104,7 @@ impl Settings for ClientSettings { type FileContent = ClientSettingsContent; - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + fn load(sources: SettingsSources, _: &mut App) -> Result { let mut result = sources.json_merge::()?; if let Some(server_url) = &*ZED_SERVER_URL { result.server_url.clone_from(server_url) @@ -128,7 +128,7 @@ impl Settings for ProxySettings { type FileContent = ProxySettingsContent; - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + fn load(sources: SettingsSources, _: &mut App) -> Result { Ok(Self { proxy: sources .user @@ -139,13 +139,13 @@ impl Settings for ProxySettings { } } -pub fn init_settings(cx: &mut AppContext) { +pub fn init_settings(cx: &mut App) { TelemetrySettings::register(cx); ClientSettings::register(cx); ProxySettings::register(cx); } -pub fn init(client: &Arc, cx: &mut AppContext) { +pub fn init(client: &Arc, cx: &mut App) { let client = Arc::downgrade(client); cx.on_action({ let client = client.clone(); @@ -199,9 +199,8 @@ pub struct Client { #[allow(clippy::type_complexity)] #[cfg(any(test, feature = "test-support"))] - authenticate: RwLock< - Option Task>>>, - >, + authenticate: + RwLock Task>>>>, #[allow(clippy::type_complexity)] #[cfg(any(test, feature = "test-support"))] @@ -213,7 +212,7 @@ pub struct Client { + Sync + Fn( &Credentials, - &AsyncAppContext, + &AsyncApp, ) -> Task>, >, >, @@ -313,7 +312,7 @@ trait CredentialsProvider { /// Reads the credentials from the provider. fn read_credentials<'a>( &'a self, - cx: &'a AsyncAppContext, + cx: &'a AsyncApp, ) -> Pin> + 'a>>; /// Writes the credentials to the provider. @@ -321,13 +320,13 @@ trait CredentialsProvider { &'a self, user_id: u64, access_token: String, - cx: &'a AsyncAppContext, + cx: &'a AsyncApp, ) -> Pin> + 'a>>; /// Deletes the credentials from the provider. fn delete_credentials<'a>( &'a self, - cx: &'a AsyncAppContext, + cx: &'a AsyncApp, ) -> Pin> + 'a>>; } @@ -380,7 +379,7 @@ pub struct PendingEntitySubscription { } impl PendingEntitySubscription { - pub fn set_model(mut self, model: &Model, cx: &AsyncAppContext) -> Subscription { + pub fn set_model(mut self, model: &Entity, cx: &AsyncApp) -> Subscription { self.consumed = true; let mut handlers = self.client.handler_set.lock(); let id = (TypeId::of::(), self.remote_id); @@ -456,7 +455,7 @@ impl settings::Settings for TelemetrySettings { type FileContent = TelemetrySettingsContent; - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + fn load(sources: SettingsSources, _: &mut App) -> Result { Ok(Self { diagnostics: sources .user @@ -483,7 +482,7 @@ impl Client { pub fn new( clock: Arc, http: Arc, - cx: &mut AppContext, + cx: &mut App, ) -> Arc { let use_zed_development_auth = match ReleaseChannel::try_global(cx) { Some(ReleaseChannel::Dev) => *ZED_DEVELOPMENT_AUTH, @@ -518,7 +517,7 @@ impl Client { }) } - pub fn production(cx: &mut AppContext) -> Arc { + pub fn production(cx: &mut App) -> Arc { let clock = Arc::new(clock::RealSystemClock); let http = Arc::new(HttpClientWithUrl::new_uri( cx.http_client(), @@ -552,7 +551,7 @@ impl Client { #[cfg(any(test, feature = "test-support"))] pub fn override_authenticate(&self, authenticate: F) -> &Self where - F: 'static + Send + Sync + Fn(&AsyncAppContext) -> Task>, + F: 'static + Send + Sync + Fn(&AsyncApp) -> Task>, { *self.authenticate.write() = Some(Box::new(authenticate)); self @@ -564,7 +563,7 @@ impl Client { F: 'static + Send + Sync - + Fn(&Credentials, &AsyncAppContext) -> Task>, + + Fn(&Credentials, &AsyncApp) -> Task>, { *self.establish_connection.write() = Some(Box::new(connect)); self @@ -576,10 +575,10 @@ impl Client { self } - pub fn global(cx: &AppContext) -> Arc { + pub fn global(cx: &App) -> Arc { cx.global::().0.clone() } - pub fn set_global(client: Arc, cx: &mut AppContext) { + pub fn set_global(client: Arc, cx: &mut App) { cx.set_global(GlobalClient(client)) } @@ -603,7 +602,7 @@ impl Client { self.state.read().status.1.clone() } - fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { + fn set_status(self: &Arc, status: Status, cx: &AsyncApp) { log::info!("set status on client {}: {:?}", self.id(), status); let mut state = self.state.write(); *state.status.0.borrow_mut() = status; @@ -678,13 +677,13 @@ impl Client { #[track_caller] pub fn add_message_handler( self: &Arc, - entity: WeakModel, + entity: WeakEntity, handler: H, ) -> Subscription where M: EnvelopedMessage, E: 'static, - H: 'static + Sync + Fn(Model, TypedEnvelope, AsyncAppContext) -> F + Send + Sync, + H: 'static + Sync + Fn(Entity, TypedEnvelope, AsyncApp) -> F + Send + Sync, F: 'static + Future>, { self.add_message_handler_impl(entity, move |model, message, _, cx| { @@ -694,7 +693,7 @@ impl Client { fn add_message_handler_impl( self: &Arc, - entity: WeakModel, + entity: WeakEntity, handler: H, ) -> Subscription where @@ -702,7 +701,7 @@ impl Client { E: 'static, H: 'static + Sync - + Fn(Model, TypedEnvelope, AnyProtoClient, AsyncAppContext) -> F + + Fn(Entity, TypedEnvelope, AnyProtoClient, AsyncApp) -> F + Send + Sync, F: 'static + Future>, @@ -739,13 +738,13 @@ impl Client { pub fn add_request_handler( self: &Arc, - model: WeakModel, + model: WeakEntity, handler: H, ) -> Subscription where M: RequestMessage, E: 'static, - H: 'static + Sync + Fn(Model, TypedEnvelope, AsyncAppContext) -> F + Send + Sync, + H: 'static + Sync + Fn(Entity, TypedEnvelope, AsyncApp) -> F + Send + Sync, F: 'static + Future>, { self.add_message_handler_impl(model, move |handle, envelope, this, cx| { @@ -770,7 +769,7 @@ impl Client { } } - pub async fn has_credentials(&self, cx: &AsyncAppContext) -> bool { + pub async fn has_credentials(&self, cx: &AsyncApp) -> bool { self.credentials_provider .read_credentials(cx) .await @@ -781,7 +780,7 @@ impl Client { pub async fn authenticate_and_connect( self: &Arc, try_provider: bool, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> anyhow::Result<()> { let was_disconnected = match *self.status().borrow() { Status::SignedOut => true, @@ -883,11 +882,7 @@ impl Client { } } - async fn set_connection( - self: &Arc, - conn: Connection, - cx: &AsyncAppContext, - ) -> Result<()> { + async fn set_connection(self: &Arc, conn: Connection, cx: &AsyncApp) -> Result<()> { let executor = cx.background_executor(); log::debug!("add connection to peer"); let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn, { @@ -981,7 +976,7 @@ impl Client { Ok(()) } - fn authenticate(self: &Arc, cx: &AsyncAppContext) -> Task> { + fn authenticate(self: &Arc, cx: &AsyncApp) -> Task> { #[cfg(any(test, feature = "test-support"))] if let Some(callback) = self.authenticate.read().as_ref() { return callback(cx); @@ -993,7 +988,7 @@ impl Client { fn establish_connection( self: &Arc, credentials: &Credentials, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> Task> { #[cfg(any(test, feature = "test-support"))] if let Some(callback) = self.establish_connection.read().as_ref() { @@ -1052,7 +1047,7 @@ impl Client { fn establish_websocket_connection( self: &Arc, credentials: &Credentials, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> Task> { let release_channel = cx .update(|cx| ReleaseChannel::try_global(cx)) @@ -1174,10 +1169,7 @@ impl Client { }) } - pub fn authenticate_with_browser( - self: &Arc, - cx: &AsyncAppContext, - ) -> Task> { + pub fn authenticate_with_browser(self: &Arc, cx: &AsyncApp) -> Task> { let http = self.http.clone(); let this = self.clone(); cx.spawn(|cx| async move { @@ -1412,7 +1404,7 @@ impl Client { }) } - pub async fn sign_out(self: &Arc, cx: &AsyncAppContext) { + pub async fn sign_out(self: &Arc, cx: &AsyncApp) { self.state.write().credentials = None; self.disconnect(cx); @@ -1424,12 +1416,12 @@ impl Client { } } - pub fn disconnect(self: &Arc, cx: &AsyncAppContext) { + pub fn disconnect(self: &Arc, cx: &AsyncApp) { self.peer.teardown(); self.set_status(Status::SignedOut, cx); } - pub fn reconnect(self: &Arc, cx: &AsyncAppContext) { + pub fn reconnect(self: &Arc, cx: &AsyncApp) { self.peer.teardown(); self.set_status(Status::ConnectionLost, cx); } @@ -1528,11 +1520,7 @@ impl Client { } } - fn handle_message( - self: &Arc, - message: Box, - cx: &AsyncAppContext, - ) { + fn handle_message(self: &Arc, message: Box, cx: &AsyncApp) { let sender_id = message.sender_id(); let request_id = message.message_id(); let type_name = message.payload_type_name(); @@ -1640,7 +1628,7 @@ struct DevelopmentCredentialsProvider { impl CredentialsProvider for DevelopmentCredentialsProvider { fn read_credentials<'a>( &'a self, - _cx: &'a AsyncAppContext, + _cx: &'a AsyncApp, ) -> Pin> + 'a>> { async move { if IMPERSONATE_LOGIN.is_some() { @@ -1663,7 +1651,7 @@ impl CredentialsProvider for DevelopmentCredentialsProvider { &'a self, user_id: u64, access_token: String, - _cx: &'a AsyncAppContext, + _cx: &'a AsyncApp, ) -> Pin> + 'a>> { async move { let json = serde_json::to_string(&DevelopmentCredentials { @@ -1680,7 +1668,7 @@ impl CredentialsProvider for DevelopmentCredentialsProvider { fn delete_credentials<'a>( &'a self, - _cx: &'a AsyncAppContext, + _cx: &'a AsyncApp, ) -> Pin> + 'a>> { async move { Ok(std::fs::remove_file(&self.path)?) }.boxed_local() } @@ -1692,7 +1680,7 @@ struct KeychainCredentialsProvider; impl CredentialsProvider for KeychainCredentialsProvider { fn read_credentials<'a>( &'a self, - cx: &'a AsyncAppContext, + cx: &'a AsyncApp, ) -> Pin> + 'a>> { async move { if IMPERSONATE_LOGIN.is_some() { @@ -1717,7 +1705,7 @@ impl CredentialsProvider for KeychainCredentialsProvider { &'a self, user_id: u64, access_token: String, - cx: &'a AsyncAppContext, + cx: &'a AsyncApp, ) -> Pin> + 'a>> { async move { cx.update(move |cx| { @@ -1734,7 +1722,7 @@ impl CredentialsProvider for KeychainCredentialsProvider { fn delete_credentials<'a>( &'a self, - cx: &'a AsyncAppContext, + cx: &'a AsyncApp, ) -> Pin> + 'a>> { async move { cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))? @@ -1751,7 +1739,7 @@ pub const ZED_URL_SCHEME: &str = "zed"; /// /// Returns a [`Some`] containing the unprefixed link if the link is a Zed link. /// Returns [`None`] otherwise. -pub fn parse_zed_link<'a>(link: &'a str, cx: &AppContext) -> Option<&'a str> { +pub fn parse_zed_link<'a>(link: &'a str, cx: &App) -> Option<&'a str> { let server_url = &ClientSettings::get_global(cx).server_url; if let Some(stripped) = link .strip_prefix(server_url) @@ -1775,7 +1763,7 @@ mod tests { use crate::test::FakeServer; use clock::FakeSystemClock; - use gpui::{BackgroundExecutor, Context, TestAppContext}; + use gpui::{AppContext as _, BackgroundExecutor, TestAppContext}; use http_client::FakeHttpClient; use parking_lot::Mutex; use proto::TypedEnvelope; @@ -1961,7 +1949,7 @@ mod tests { let (done_tx1, done_rx1) = smol::channel::unbounded(); let (done_tx2, done_rx2) = smol::channel::unbounded(); AnyProtoClient::from(client.clone()).add_model_message_handler( - move |model: Model, _: TypedEnvelope, mut cx| { + move |model: Entity, _: TypedEnvelope, mut cx| { match model.update(&mut cx, |model, _| model.id).unwrap() { 1 => done_tx1.try_send(()).unwrap(), 2 => done_tx2.try_send(()).unwrap(), @@ -1970,15 +1958,15 @@ mod tests { async { Ok(()) } }, ); - let model1 = cx.new_model(|_| TestModel { + let model1 = cx.new(|_| TestModel { id: 1, subscription: None, }); - let model2 = cx.new_model(|_| TestModel { + let model2 = cx.new(|_| TestModel { id: 2, subscription: None, }); - let model3 = cx.new_model(|_| TestModel { + let model3 = cx.new(|_| TestModel { id: 3, subscription: None, }); @@ -2018,7 +2006,7 @@ mod tests { }); let server = FakeServer::for_client(user_id, &client, cx).await; - let model = cx.new_model(|_| TestModel::default()); + let model = cx.new(|_| TestModel::default()); let (done_tx1, _done_rx1) = smol::channel::unbounded(); let (done_tx2, done_rx2) = smol::channel::unbounded(); let subscription1 = client.add_message_handler( @@ -2053,11 +2041,11 @@ mod tests { }); let server = FakeServer::for_client(user_id, &client, cx).await; - let model = cx.new_model(|_| TestModel::default()); + let model = cx.new(|_| TestModel::default()); let (done_tx, done_rx) = smol::channel::unbounded(); let subscription = client.add_message_handler( model.clone().downgrade(), - move |model: Model, _: TypedEnvelope, mut cx| { + move |model: Entity, _: TypedEnvelope, mut cx| { model .update(&mut cx, |model, _| model.subscription.take()) .unwrap(); diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 26f793ba575c05..56ed6e6c036f00 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -6,7 +6,7 @@ use clock::SystemClock; use collections::{HashMap, HashSet}; use futures::channel::mpsc; use futures::{Future, StreamExt}; -use gpui::{AppContext, BackgroundExecutor, Task}; +use gpui::{App, BackgroundExecutor, Task}; use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request}; use parking_lot::Mutex; use release_channel::ReleaseChannel; @@ -178,7 +178,7 @@ impl Telemetry { pub fn new( clock: Arc, client: Arc, - cx: &mut AppContext, + cx: &mut App, ) -> Arc { let release_channel = ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name()); @@ -299,7 +299,7 @@ impl Telemetry { system_id: Option, installation_id: Option, session_id: String, - cx: &AppContext, + cx: &App, ) { let mut state = self.state.lock(); state.system_id = system_id.map(|id| id.into()); diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 5a93c5edd984c8..4b158b6ab1ef5e 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -2,7 +2,7 @@ use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore use anyhow::{anyhow, Result}; use chrono::Duration; use futures::{stream::BoxStream, StreamExt}; -use gpui::{BackgroundExecutor, Context, Model, TestAppContext}; +use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext}; use parking_lot::Mutex; use rpc::{ proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, @@ -203,8 +203,8 @@ impl FakeServer { &self, client: Arc, cx: &mut TestAppContext, - ) -> Model { - let user_store = cx.new_model(|cx| UserStore::new(client, cx)); + ) -> Entity { + let user_store = cx.new(|cx| UserStore::new(client, cx)); assert_eq!( self.receive::() .await diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 17da72695450f1..91af9a1a7f680f 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,12 +1,11 @@ use super::{proto, Client, Status, TypedEnvelope}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use chrono::{DateTime, Utc}; use collections::{hash_map::Entry, HashMap, HashSet}; use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, Future, StreamExt}; use gpui::{ - AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, SharedUri, Task, - WeakModel, + App, AsyncApp, Context, Entity, EventEmitter, SharedString, SharedUri, Task, WeakEntity, }; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; @@ -106,7 +105,7 @@ pub struct UserStore { client: Weak, _maintain_contacts: Task<()>, _maintain_current_user: Task>, - weak_self: WeakModel, + weak_self: WeakEntity, } #[derive(Clone)] @@ -122,6 +121,9 @@ pub enum Event { }, ShowContacts, ParticipantIndicesChanged, + TermsStatusUpdated { + accepted: bool, + }, } #[derive(Clone, Copy)] @@ -140,14 +142,14 @@ enum UpdateContacts { } impl UserStore { - pub fn new(client: Arc, cx: &ModelContext) -> Self { + pub fn new(client: Arc, cx: &Context) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscriptions = vec![ - client.add_message_handler(cx.weak_model(), Self::handle_update_plan), - client.add_message_handler(cx.weak_model(), Self::handle_update_contacts), - client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info), - client.add_message_handler(cx.weak_model(), Self::handle_show_contacts), + client.add_message_handler(cx.weak_entity(), Self::handle_update_plan), + client.add_message_handler(cx.weak_entity(), Self::handle_update_contacts), + client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info), + client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts), ]; Self { users: Default::default(), @@ -210,10 +212,24 @@ impl UserStore { staff, ); - this.update(cx, |this, _| { - this.set_current_user_accepted_tos_at( - info.accepted_tos_at, - ); + this.update(cx, |this, cx| { + let accepted_tos_at = { + #[cfg(debug_assertions)] + if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() + { + None + } else { + info.accepted_tos_at + } + + #[cfg(not(debug_assertions))] + info.accepted_tos_at + }; + + this.set_current_user_accepted_tos_at(accepted_tos_at); + cx.emit(Event::TermsStatusUpdated { + accepted: accepted_tos_at.is_some(), + }); }) } else { anyhow::Ok(()) @@ -246,7 +262,7 @@ impl UserStore { Ok(()) }), pending_contact_requests: Default::default(), - weak_self: cx.weak_model(), + weak_self: cx.weak_entity(), } } @@ -257,9 +273,9 @@ impl UserStore { } async fn handle_update_invite_info( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { this.invite_info = Some(InviteInfo { @@ -272,9 +288,9 @@ impl UserStore { } async fn handle_show_contacts( - this: Model, + this: Entity, _: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?; Ok(()) @@ -285,9 +301,9 @@ impl UserStore { } async fn handle_update_contacts( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, _| { this.update_contacts_tx @@ -298,9 +314,9 @@ impl UserStore { } async fn handle_update_plan( - this: Model, + this: Entity, message: TypedEnvelope, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { this.current_plan = Some(message.payload.plan()); @@ -309,11 +325,7 @@ impl UserStore { Ok(()) } - fn update_contacts( - &mut self, - message: UpdateContacts, - cx: &ModelContext, - ) -> Task> { + fn update_contacts(&mut self, message: UpdateContacts, cx: &Context) -> Task> { match message { UpdateContacts::Wait(barrier) => { drop(barrier); @@ -487,16 +499,12 @@ impl UserStore { pub fn request_contact( &mut self, responder_id: u64, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx) } - pub fn remove_contact( - &mut self, - user_id: u64, - cx: &mut ModelContext, - ) -> Task> { + pub fn remove_contact(&mut self, user_id: u64, cx: &mut Context) -> Task> { self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx) } @@ -510,7 +518,7 @@ impl UserStore { &mut self, requester_id: u64, accept: bool, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { self.perform_contact_request( requester_id, @@ -529,7 +537,7 @@ impl UserStore { pub fn dismiss_contact_request( &self, requester_id: u64, - cx: &ModelContext, + cx: &Context, ) -> Task> { let client = self.client.upgrade(); cx.spawn(move |_, _| async move { @@ -548,7 +556,7 @@ impl UserStore { &mut self, user_id: u64, request: T, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let client = self.client.upgrade(); *self.pending_contact_requests.entry(user_id).or_insert(0) += 1; @@ -598,7 +606,7 @@ impl UserStore { pub fn get_users( &self, user_ids: Vec, - cx: &ModelContext, + cx: &Context, ) -> Task>>> { let mut user_ids_to_fetch = user_ids.clone(); user_ids_to_fetch.retain(|id| !self.users.contains_key(id)); @@ -633,7 +641,7 @@ impl UserStore { pub fn fuzzy_search_users( &self, query: String, - cx: &ModelContext, + cx: &Context, ) -> Task>>> { self.load_users(proto::FuzzySearchUsers { query }, cx) } @@ -642,7 +650,7 @@ impl UserStore { self.users.get(&user_id).cloned() } - pub fn get_user_optimistic(&self, user_id: u64, cx: &ModelContext) -> Option> { + pub fn get_user_optimistic(&self, user_id: u64, cx: &Context) -> Option> { if let Some(user) = self.users.get(&user_id).cloned() { return Some(user); } @@ -651,7 +659,7 @@ impl UserStore { None } - pub fn get_user(&self, user_id: u64, cx: &ModelContext) -> Task>> { + pub fn get_user(&self, user_id: u64, cx: &Context) -> Task>> { if let Some(user) = self.users.get(&user_id).cloned() { return Task::ready(Ok(user)); } @@ -691,7 +699,7 @@ impl UserStore { .map(|accepted_tos_at| accepted_tos_at.is_some()) } - pub fn accept_terms_of_service(&self, cx: &ModelContext) -> Task> { + pub fn accept_terms_of_service(&self, cx: &Context) -> Task> { if self.current_user().is_none() { return Task::ready(Err(anyhow!("no current user"))); }; @@ -704,8 +712,9 @@ impl UserStore { .await .context("error accepting tos")?; - this.update(&mut cx, |this, _| { - this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at)) + this.update(&mut cx, |this, cx| { + this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at)); + cx.emit(Event::TermsStatusUpdated { accepted: true }); }) } else { Err(anyhow!("client not found")) @@ -722,7 +731,7 @@ impl UserStore { fn load_users( &self, request: impl RequestMessage, - cx: &ModelContext, + cx: &Context, ) -> Task>>> { let client = self.client.clone(); cx.spawn(|this, mut cx| async move { @@ -756,7 +765,7 @@ impl UserStore { pub fn set_participant_indices( &mut self, participant_indices: HashMap, - cx: &mut ModelContext, + cx: &mut Context, ) { if participant_indices != self.participant_indices { self.participant_indices = participant_indices; @@ -771,7 +780,7 @@ impl UserStore { pub fn participant_names( &self, user_ids: impl Iterator, - cx: &AppContext, + cx: &App, ) -> HashMap { let mut ret = HashMap::default(); let mut missing_user_ids = Vec::new(); @@ -809,8 +818,8 @@ impl User { impl Contact { async fn from_proto( contact: proto::Contact, - user_store: &Model, - cx: &mut AsyncAppContext, + user_store: &Entity, + cx: &mut AsyncApp, ) -> Result { let user = user_store .update(cx, |user_store, cx| { diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index a5b27cf288e2ee..bfdae468fbb6cc 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -4,16 +4,16 @@ //! links appropriate for the environment (e.g., by linking to a local copy of //! zed.dev in development). -use gpui::AppContext; +use gpui::App; use settings::Settings; use crate::ClientSettings; -fn server_url(cx: &AppContext) -> &str { +fn server_url(cx: &App) -> &str { &ClientSettings::get_global(cx).server_url } /// Returns the URL to the account page on zed.dev. -pub fn account_url(cx: &AppContext) -> String { +pub fn account_url(cx: &App) -> String { format!("{server_url}/account", server_url = server_url(cx)) } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index ab63e280c9f90d..b901e82dae33fe 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -79,6 +79,7 @@ uuid.workspace = true [dev-dependencies] assistant = { workspace = true, features = ["test-support"] } +assistant_context_editor.workspace = true assistant_slash_command.workspace = true assistant_tool.workspace = true async-trait.workspace = true diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index bd60ee0cd08a2c..b5cf278f706a1b 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -3,7 +3,7 @@ use crate::{ rpc::Principal, AppState, Error, Result, }; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, Context as _}; use axum::{ http::{self, Request, StatusCode}, middleware::Next, diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 86e0112f075e8e..a0f5fa371cf742 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -407,7 +407,7 @@ async fn build_kinesis_client(config: &Config) -> anyhow::Result() < 0.1; + let api_url = state .config .prediction_api_url @@ -489,7 +491,7 @@ async fn predict_edits( max_tokens: 2048, temperature: 0., prediction: Some(fireworks::Prediction::Content { - content: params.input_excerpt, + content: params.input_excerpt.clone(), }), rewrite_speculation: Some(true), }, @@ -536,18 +538,38 @@ async fn predict_edits( let kinesis_client = state.kinesis_client.clone(); let kinesis_stream = state.config.kinesis_stream.clone(); let model = model.clone(); + let output = choice.text.clone(); + async move { - SnowflakeRow::new( - "Fireworks Completion Requested", - claims.metrics_id, - claims.is_staff, - claims.system_id.clone(), + let properties = if sample_input_output { json!({ "model": model.to_string(), "headers": response.headers, "usage": response.completion.usage, "duration": duration.as_secs_f64(), - }), + "prompt": prompt, + "input_excerpt": params.input_excerpt, + "input_events": params.input_events, + "outline": params.outline, + "output": output, + "is_sampled": true, + }) + } else { + json!({ + "model": model.to_string(), + "headers": response.headers, + "usage": response.completion.usage, + "duration": duration.as_secs_f64(), + "is_sampled": false, + }) + }; + + SnowflakeRow::new( + "Fireworks Completion Requested", + claims.metrics_id, + claims.is_staff, + claims.system_id.clone(), + properties, ) .write(&kinesis_client, &kinesis_stream) .await diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 9e5c3dd0485021..d552bba68e0e60 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -6,6 +6,7 @@ use axum::{ routing::get, Extension, Router, }; + use collab::api::billing::sync_llm_usage_with_stripe_periodically; use collab::api::CloudflareIpCountryHeader; use collab::llm::{db::LlmDatabase, log_usage_periodically}; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fa116c8cc54938..b5bab509d469c6 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -333,6 +333,9 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) + .add_request_handler( + forward_mutating_project_request::, + ) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) diff --git a/crates/collab/src/seed.rs b/crates/collab/src/seed.rs index d57fc9a2566f1b..ce5d99bbe063a1 100644 --- a/crates/collab/src/seed.rs +++ b/crates/collab/src/seed.rs @@ -1,6 +1,6 @@ use crate::db::{self, ChannelRole, NewUserParams}; -use anyhow::Context; +use anyhow::Context as _; use chrono::{DateTime, Utc}; use db::Database; use serde::{de::DeserializeOwned, Deserialize}; diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs index 126db988a1ea8a..9f561ab1b2f10e 100644 --- a/crates/collab/src/stripe_billing.rs +++ b/crates/collab/src/stripe_billing.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::{llm, Cents, Result}; -use anyhow::Context; +use anyhow::Context as _; use chrono::{Datelike, Utc}; use collections::HashMap; use serde::{Deserialize, Serialize}; diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 4dbee3032b66af..b9f0f79a9e23bc 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use call::Room; use client::ChannelId; -use gpui::{Model, TestAppContext}; +use gpui::{Entity, TestAppContext}; mod channel_buffer_tests; mod channel_guest_tests; @@ -34,7 +34,7 @@ struct RoomParticipants { pending: Vec, } -fn room_participants(room: &Model, cx: &mut TestAppContext) -> RoomParticipants { +fn room_participants(room: &Entity, cx: &mut TestAppContext) -> RoomParticipants { room.read_with(cx, |room, _| { let mut remote = room .remote_participants() @@ -52,7 +52,7 @@ fn room_participants(room: &Model, cx: &mut TestAppContext) -> RoomPartici }) } -fn channel_id(room: &Model, cx: &mut TestAppContext) -> Option { +fn channel_id(room: &Entity, cx: &mut TestAppContext) -> Option { cx.read(|cx| room.read(cx).channel_id()) } diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index b5bfd0f03b9ec7..83bb8966b7e02c 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -9,7 +9,7 @@ use collab_ui::channel_view::ChannelView; use collections::HashMap; use editor::{Anchor, Editor, ToOffset}; use futures::future; -use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; +use gpui::{BackgroundExecutor, Context, Entity, TestAppContext, Window}; use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; use serde_json::json; use std::ops::Range; @@ -161,43 +161,43 @@ async fn test_channel_notes_participant_indices( // Clients A, B, and C open the channel notes let channel_view_a = cx_a - .update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx)) + .update(|window, cx| ChannelView::open(channel_id, None, workspace_a.clone(), window, cx)) .await .unwrap(); let channel_view_b = cx_b - .update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx)) + .update(|window, cx| ChannelView::open(channel_id, None, workspace_b.clone(), window, cx)) .await .unwrap(); let channel_view_c = cx_c - .update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx)) + .update(|window, cx| ChannelView::open(channel_id, None, workspace_c.clone(), window, cx)) .await .unwrap(); // Clients A, B, and C all insert and select some text - channel_view_a.update(cx_a, |notes, cx| { + channel_view_a.update_in(cx_a, |notes, window, cx| { notes.editor.update(cx, |editor, cx| { - editor.insert("a", cx); - editor.change_selections(None, cx, |selections| { + editor.insert("a", window, cx); + editor.change_selections(None, window, cx, |selections| { selections.select_ranges(vec![0..1]); }); }); }); executor.run_until_parked(); - channel_view_b.update(cx_b, |notes, cx| { + channel_view_b.update_in(cx_b, |notes, window, cx| { notes.editor.update(cx, |editor, cx| { - editor.move_down(&Default::default(), cx); - editor.insert("b", cx); - editor.change_selections(None, cx, |selections| { + editor.move_down(&Default::default(), window, cx); + editor.insert("b", window, cx); + editor.change_selections(None, window, cx, |selections| { selections.select_ranges(vec![1..2]); }); }); }); executor.run_until_parked(); - channel_view_c.update(cx_c, |notes, cx| { + channel_view_c.update_in(cx_c, |notes, window, cx| { notes.editor.update(cx, |editor, cx| { - editor.move_down(&Default::default(), cx); - editor.insert("c", cx); - editor.change_selections(None, cx, |selections| { + editor.move_down(&Default::default(), window, cx); + editor.insert("c", window, cx); + editor.change_selections(None, window, cx, |selections| { selections.select_ranges(vec![2..3]); }); }); @@ -206,9 +206,9 @@ async fn test_channel_notes_participant_indices( // Client A sees clients B and C without assigned colors, because they aren't // in a call together. executor.run_until_parked(); - channel_view_a.update(cx_a, |notes, cx| { + channel_view_a.update_in(cx_a, |notes, window, cx| { notes.editor.update(cx, |editor, cx| { - assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); + assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], window, cx); }); }); @@ -222,20 +222,22 @@ async fn test_channel_notes_participant_indices( // Clients A and B see each other with two different assigned colors. Client C // still doesn't have a color. executor.run_until_parked(); - channel_view_a.update(cx_a, |notes, cx| { + channel_view_a.update_in(cx_a, |notes, window, cx| { notes.editor.update(cx, |editor, cx| { assert_remote_selections( editor, &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], + window, cx, ); }); }); - channel_view_b.update(cx_b, |notes, cx| { + channel_view_b.update_in(cx_b, |notes, window, cx| { notes.editor.update(cx, |editor, cx| { assert_remote_selections( editor, &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], + window, cx, ); }); @@ -252,8 +254,8 @@ async fn test_channel_notes_participant_indices( // Clients A and B open the same file. executor.start_waiting(); let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx) }) .await .unwrap() @@ -261,32 +263,32 @@ async fn test_channel_notes_participant_indices( .unwrap(); executor.start_waiting(); let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, window, cx) }) .await .unwrap() .downcast::() .unwrap(); - editor_a.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |selections| { + editor_a.update_in(cx_a, |editor, window, cx| { + editor.change_selections(None, window, cx, |selections| { selections.select_ranges(vec![0..1]); }); }); - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |selections| { + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(None, window, cx, |selections| { selections.select_ranges(vec![2..3]); }); }); executor.run_until_parked(); // Clients A and B see each other with the same colors as in the channel notes. - editor_a.update(cx_a, |editor, cx| { - assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); + editor_a.update_in(cx_a, |editor, window, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], window, cx); }); - editor_b.update(cx_b, |editor, cx| { - assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); + editor_b.update_in(cx_b, |editor, window, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], window, cx); }); } @@ -294,9 +296,10 @@ async fn test_channel_notes_participant_indices( fn assert_remote_selections( editor: &mut Editor, expected_selections: &[(Option, Range)], - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - let snapshot = editor.snapshot(cx); + let snapshot = editor.snapshot(window, cx); let range = Anchor::min()..Anchor::max(); let remote_selections = snapshot .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) @@ -641,9 +644,9 @@ async fn test_channel_buffer_changes( }); // Closing the buffer should re-enable change tracking - cx_b.update(|cx| { + cx_b.update(|window, cx| { workspace_b.update(cx, |workspace, cx| { - workspace.close_all_items_and_panes(&Default::default(), cx) + workspace.close_all_items_and_panes(&Default::default(), window, cx) }); }); deterministic.run_until_parked(); @@ -691,6 +694,6 @@ fn assert_collaborators(collaborators: &HashMap, ids: &[Op ); } -fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { +fn buffer_text(channel_buffer: &Entity, cx: &mut TestAppContext) -> String { channel_buffer.read_with(cx, |buffer, _| buffer.text()) } diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index ebde1d9e5073c3..0e8d856221f92d 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -107,7 +107,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test }); assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx))); assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx))); - cx_b.update(|cx_b| { + cx_b.update(|_window, cx_b| { assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone())); }); assert!(room_b @@ -135,7 +135,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx))); // B sees themselves as muted, and can unmute. - cx_b.update(|cx_b| { + cx_b.update(|_window, cx_b| { assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone())); }); room_b.read_with(cx_b, |room, _| assert!(room.is_muted())); diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 08d2ae7ed164ed..dbc5cd86c25827 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -1,7 +1,7 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use channel::{ChannelChat, ChannelMessageId, MessageParams}; use collab_ui::chat_panel::ChatPanel; -use gpui::{BackgroundExecutor, Model, TestAppContext}; +use gpui::{BackgroundExecutor, Entity, TestAppContext}; use rpc::Notification; use workspace::dock::Panel; @@ -295,7 +295,7 @@ async fn test_remove_channel_message( } #[track_caller] -fn assert_messages(chat: &Model, messages: &[&str], cx: &mut TestAppContext) { +fn assert_messages(chat: &Entity, messages: &[&str], cx: &mut TestAppContext) { assert_eq!( chat.read_with(cx, |chat, _| { chat.messages() @@ -356,10 +356,10 @@ async fn test_channel_message_changes( let project_b = client_b.build_empty_local_project(cx_b); let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - let chat_panel_b = workspace_b.update(cx_b, ChatPanel::new); + let chat_panel_b = workspace_b.update_in(cx_b, ChatPanel::new); chat_panel_b - .update(cx_b, |chat_panel, cx| { - chat_panel.set_active(true, cx); + .update_in(cx_b, |chat_panel, window, cx| { + chat_panel.set_active(true, window, cx); chat_panel.select_channel(channel_id, None, cx) }) .await @@ -367,7 +367,7 @@ async fn test_channel_message_changes( executor.run_until_parked(); - let b_has_messages = cx_b.update(|cx| { + let b_has_messages = cx_b.update(|_, cx| { client_b .channel_store() .read(cx) @@ -384,7 +384,7 @@ async fn test_channel_message_changes( executor.run_until_parked(); - let b_has_messages = cx_b.update(|cx| { + let b_has_messages = cx_b.update(|_, cx| { client_b .channel_store() .read(cx) @@ -394,8 +394,8 @@ async fn test_channel_message_changes( assert!(!b_has_messages); // Sending a message while the chat is closed should change the flag. - chat_panel_b.update(cx_b, |chat_panel, cx| { - chat_panel.set_active(false, cx); + chat_panel_b.update_in(cx_b, |chat_panel, window, cx| { + chat_panel.set_active(false, window, cx); }); // Sending a message while the chat is open should not change the flag. @@ -406,7 +406,7 @@ async fn test_channel_message_changes( executor.run_until_parked(); - let b_has_messages = cx_b.update(|cx| { + let b_has_messages = cx_b.update(|_, cx| { client_b .channel_store() .read(cx) @@ -416,7 +416,7 @@ async fn test_channel_message_changes( assert!(b_has_messages); // Closing the chat should re-enable change tracking - cx_b.update(|_| drop(chat_panel_b)); + cx_b.update(|_, _| drop(chat_panel_b)); channel_chat_a .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) @@ -425,7 +425,7 @@ async fn test_channel_message_changes( executor.run_until_parked(); - let b_has_messages = cx_b.update(|cx| { + let b_has_messages = cx_b.update(|_, cx| { client_b .channel_store() .read(cx) diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index d9fdab71e86a62..74b3f79d64dd8f 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -7,7 +7,7 @@ use call::ActiveCall; use channel::{ChannelMembership, ChannelStore}; use client::{ChannelId, User}; use futures::future::try_join_all; -use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext}; +use gpui::{BackgroundExecutor, Entity, SharedString, TestAppContext}; use rpc::{ proto::{self, ChannelRole}, RECEIVE_TIMEOUT, @@ -1401,7 +1401,7 @@ struct ExpectedChannel { #[track_caller] fn assert_channel_invitations( - channel_store: &Model, + channel_store: &Entity, cx: &TestAppContext, expected_channels: &[ExpectedChannel], ) { @@ -1423,7 +1423,7 @@ fn assert_channel_invitations( #[track_caller] fn assert_channels( - channel_store: &Model, + channel_store: &Entity, cx: &TestAppContext, expected_channels: &[ExpectedChannel], ) { @@ -1444,7 +1444,7 @@ fn assert_channels( #[track_caller] fn assert_channels_list_shape( - channel_store: &Model, + channel_store: &Entity, cx: &TestAppContext, expected_channels: &[(ChannelId, usize)], ) { diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index ff2ea023c4b660..5bf62d9ad1cebe 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -7,7 +7,7 @@ use dap::{ use dap::{Scope, Variable}; use debugger_ui::{debugger_panel::DebugPanel, variable_list::VariableContainer}; use editor::Editor; -use gpui::{TestAppContext, View, VisualTestContext}; +use gpui::{Entity, TestAppContext, VisualTestContext}; use project::ProjectPath; use serde_json::json; use std::sync::Arc; @@ -35,14 +35,16 @@ pub fn init_test(cx: &mut gpui::TestAppContext) { }); } -pub async fn add_debugger_panel(workspace: &View, cx: &mut VisualTestContext) { +async fn add_debugger_panel(workspace: &Entity, cx: &mut VisualTestContext) { let debugger_panel = workspace - .update(cx, |_, cx| cx.spawn(DebugPanel::load)) + .update_in(cx, |_workspace, window, cx| { + cx.spawn_in(window, DebugPanel::load) + }) .await .unwrap(); - workspace.update(cx, |workspace, cx| { - workspace.add_panel(debugger_panel, cx); + workspace.update_in(cx, |workspace, window, cx| { + workspace.add_panel(debugger_panel, window, cx); }); } @@ -87,6 +89,8 @@ async fn test_debug_panel_item_opens_on_remote( add_debugger_panel(&workspace_a, cx_a).await; add_debugger_panel(&workspace_b, cx_b).await; + cx_b.run_until_parked(); + let task = project_a.update(cx_a, |project, cx| { project.dap_store().update(cx, |store, cx| { store.start_debug_session( @@ -199,6 +203,8 @@ async fn test_active_debug_panel_item_set_on_join_project( add_debugger_panel(&workspace_a, cx_a).await; + cx_a.run_until_parked(); + let task = project_a.update(cx_a, |project, cx| { project.dap_store().update(cx, |store, cx| { store.start_debug_session( @@ -255,9 +261,12 @@ async fn test_active_debug_panel_item_set_on_join_project( cx_a.run_until_parked(); let project_b = client_b.join_remote_project(project_id, cx_b).await; + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); add_debugger_panel(&workspace_b, cx_b).await; + cx_b.run_until_parked(); + active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) .await @@ -974,24 +983,24 @@ async fn test_updated_breakpoints_send_to_dap( // Client B opens an editor. let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path(project_path.clone(), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path(project_path.clone(), None, true, window, cx) }) .await .unwrap() .downcast::() .unwrap(); - editor_b.update(cx_b, |editor, cx| { - editor.move_down(&editor::actions::MoveDown, cx); - editor.move_down(&editor::actions::MoveDown, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); // Client A opens an editor. let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path(project_path.clone(), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path(project_path.clone(), None, true, window, cx) }) .await .unwrap() @@ -1020,10 +1029,10 @@ async fn test_updated_breakpoints_send_to_dap( .await; // remove the breakpoint that client B added - editor_a.update(cx_a, |editor, cx| { - editor.move_down(&editor::actions::MoveDown, cx); - editor.move_down(&editor::actions::MoveDown, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor_a.update_in(cx_a, |editor, window, cx| { + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); cx_a.run_until_parked(); @@ -1075,10 +1084,10 @@ async fn test_updated_breakpoints_send_to_dap( .await; // Add our own breakpoint now - editor_a.update(cx_a, |editor, cx| { - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); - editor.move_up(&editor::actions::MoveUp, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor_a.update_in(cx_a, |editor, window, cx| { + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); + editor.move_up(&editor::actions::MoveUp, window, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); cx_a.run_until_parked(); @@ -1326,7 +1335,11 @@ async fn test_module_list( .unwrap(); let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + add_debugger_panel(&workspace_c, cx_c).await; + + cx_c.run_until_parked(); + cx_c.run_until_parked(); workspace_c.update(cx_c, |workspace, cx| { @@ -1812,6 +1825,8 @@ async fn test_variable_list( let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); add_debugger_panel(&workspace_c, cx_c).await; + + cx_c.run_until_parked(); cx_c.run_until_parked(); let last_join_remote_item = workspace_c.update(cx_c, |workspace, cx| { @@ -1912,19 +1927,20 @@ async fn test_ignore_breakpoints( add_debugger_panel(&workspace_b, cx_b).await; let local_editor = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path(project_path.clone(), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path(project_path.clone(), None, true, window, cx) }) .await .unwrap() .downcast::() .unwrap(); - local_editor.update(cx_a, |editor, cx| { - editor.move_down(&editor::actions::MoveDown, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 2 - editor.move_down(&editor::actions::MoveDown, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 3 + local_editor.update_in(cx_a, |editor, window, cx| { + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); // Line 2 + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); + // Line 3 }); cx_a.run_until_parked(); @@ -2139,8 +2155,8 @@ async fn test_ignore_breakpoints( .await; let remote_editor = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path(project_path.clone(), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path(project_path.clone(), None, true, window, cx) }) .await .unwrap() @@ -2149,8 +2165,9 @@ async fn test_ignore_breakpoints( called_set_breakpoints.store(false, std::sync::atomic::Ordering::SeqCst); - remote_editor.update(cx_b, |editor, cx| { - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 1 + remote_editor.update_in(cx_b, |editor, window, cx| { + // Line 1 + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); cx_a.run_until_parked(); @@ -2223,8 +2240,11 @@ async fn test_ignore_breakpoints( .unwrap(); let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + add_debugger_panel(&workspace_c, cx_c).await; + cx_c.run_until_parked(); + let last_join_remote_item = workspace_c.update(cx_c, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 7fe56e2d11e853..51ea07912d92c6 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -10,7 +10,7 @@ use editor::{ ToggleCodeActions, Undo, }, test::editor_test_context::{AssertionContextManager, EditorTestContext}, - Editor, + Editor, RowInfo, }; use fs::Fs; use futures::StreamExt; @@ -20,7 +20,6 @@ use language::{ language_settings::{AllLanguageSettings, InlayHintSettings}, FakeLspAdapter, }; -use multi_buffer::MultiBufferRow; use project::{ project_settings::{InlineBlameSettings, ProjectSettings}, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, @@ -82,14 +81,21 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer())); - let workspace_b = cx_b - .add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx)); + let workspace_b = cx_b.add_window(|window, cx| { + Workspace::new( + None, + project_b.clone(), + client_b.app_state.clone(), + window, + cx, + ) + }); let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); - let workspace_b_view = workspace_b.root_view(cx_b).unwrap(); + let workspace_b_view = workspace_b.root(cx_b).unwrap(); let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "b.txt"), None, true, cx) + .update(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "b.txt"), None, true, window, cx) }) .unwrap() .await @@ -98,10 +104,10 @@ async fn test_host_disconnect( .unwrap(); //TODO: focus - assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); - editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window))); + editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx)); - cx_b.update(|cx| { + cx_b.update(|_, cx| { assert!(workspace_b_view.read(cx).is_edited()); }); @@ -121,7 +127,7 @@ async fn test_host_disconnect( // Ensure client B's edited state is reset and that the whole window is blurred. workspace_b - .update(cx_b, |workspace, cx| { + .update(cx_b, |workspace, _, cx| { assert!(workspace.active_modal::(cx).is_some()); assert!(!workspace.is_edited()); }) @@ -129,8 +135,8 @@ async fn test_host_disconnect( // Ensure client B is not prompted to save edits when closing window after disconnecting. let can_close = workspace_b - .update(cx_b, |workspace, cx| { - workspace.prepare_to_close(CloseIntent::Quit, cx) + .update(cx_b, |workspace, window, cx| { + workspace.prepare_to_close(CloseIntent::Quit, window, cx) }) .unwrap() .await @@ -201,11 +207,12 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( .await .unwrap(); let cx_a = cx_a.add_empty_window(); - let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); + let editor_a = cx_a + .new_window_entity(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx)); let mut editor_cx_a = EditorTestContext { cx: cx_a.clone(), - window: cx_a.handle(), + window: cx_a.window_handle(), editor: editor_a, assertion_cx: AssertionContextManager::new(), }; @@ -216,11 +223,12 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); + let editor_b = cx_b + .new_window_entity(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx)); let mut editor_cx_b = EditorTestContext { cx: cx_b.clone(), - window: cx_b.handle(), + window: cx_b.window_handle(), editor: editor_b, assertion_cx: AssertionContextManager::new(), }; @@ -232,8 +240,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( editor_cx_b.set_selections_state(indoc! {" Some textˇ "}); - editor_cx_a - .update_editor(|editor, cx| editor.newline_above(&editor::actions::NewlineAbove, cx)); + editor_cx_a.update_editor(|editor, window, cx| { + editor.newline_above(&editor::actions::NewlineAbove, window, cx) + }); executor.run_until_parked(); editor_cx_a.assert_editor_state(indoc! {" ˇ @@ -253,8 +262,9 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( Some textˇ "}); - editor_cx_a - .update_editor(|editor, cx| editor.newline_below(&editor::actions::NewlineBelow, cx)); + editor_cx_a.update_editor(|editor, window, cx| { + editor.newline_below(&editor::actions::NewlineBelow, window, cx) + }); executor.run_until_parked(); editor_cx_a.assert_editor_state(indoc! {" @@ -317,8 +327,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .await .unwrap(); let cx_b = cx_b.add_empty_window(); - let editor_b = - cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)); + let editor_b = cx_b.new_window_entity(|window, cx| { + Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx) + }); let fake_language_server = fake_language_servers.next().await.unwrap(); cx_a.background_executor.run_until_parked(); @@ -328,11 +339,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu }); // Type a completion trigger character as the guest. - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(".", cx); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.handle_input(".", window, cx); }); - cx_b.focus_view(&editor_b); + cx_b.focus(&editor_b); // Receive a completion request as the host's language server. // Return some completions from the host's language server. @@ -394,9 +405,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu }); // Confirm a completion on the guest. - editor_b.update(cx_b, |editor, cx| { + editor_b.update_in(cx_b, |editor, window, cx| { assert!(editor.context_menu_visible()); - editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); + editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx); assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); }); @@ -441,10 +452,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu // Now we do a second completion, this time to ensure that documentation/snippets are // resolved - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([46..46])); - editor.handle_input("; a", cx); - editor.handle_input(".", cx); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([46..46])); + editor.handle_input("; a", window, cx); + editor.handle_input(".", window, cx); }); buffer_b.read_with(cx_b, |buffer, _| { @@ -508,18 +519,18 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu completion_response.next().await.unwrap(); - editor_b.update(cx_b, |editor, cx| { + editor_b.update_in(cx_b, |editor, window, cx| { assert!(editor.context_menu_visible()); - editor.context_menu_first(&ContextMenuFirst {}, cx); + editor.context_menu_first(&ContextMenuFirst {}, window, cx); }); resolve_completion_response.next().await.unwrap(); cx_b.executor().run_until_parked(); // When accepting the completion, the snippet is insert. - editor_b.update(cx_b, |editor, cx| { + editor_b.update_in(cx_b, |editor, window, cx| { assert!(editor.context_menu_visible()); - editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); + editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx); assert_eq!( editor.text(cx), "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }" @@ -569,8 +580,8 @@ async fn test_collaborating_with_code_actions( let project_b = client_b.join_remote_project(project_id, cx_b).await; let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, window, cx) }) .await .unwrap() @@ -593,12 +604,12 @@ async fn test_collaborating_with_code_actions( requests.next().await; // Move cursor to a location that contains code actions. - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| { + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(1, 31)..Point::new(1, 31)]) }); }); - cx_b.focus_view(&editor_b); + cx_b.focus(&editor_b); let mut requests = fake_language_server .handle_request::(|params, _| async move { @@ -658,11 +669,12 @@ async fn test_collaborating_with_code_actions( requests.next().await; // Toggle code actions and wait for them to display. - editor_b.update(cx_b, |editor, cx| { + editor_b.update_in(cx_b, |editor, window, cx| { editor.toggle_code_actions( &ToggleCodeActions { deployed_from_indicator: None, }, + window, cx, ); }); @@ -674,8 +686,8 @@ async fn test_collaborating_with_code_actions( // Confirming the code action will trigger a resolve request. let confirm_action = editor_b - .update(cx_b, |editor, cx| { - Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx) + .update_in(cx_b, |editor, window, cx| { + Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx) }) .unwrap(); fake_language_server.handle_request::( @@ -726,14 +738,14 @@ async fn test_collaborating_with_code_actions( .downcast::() .unwrap() }); - code_action_editor.update(cx_b, |editor, cx| { + code_action_editor.update_in(cx_b, |editor, window, cx| { assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!( editor.text(cx), "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }" ); - editor.redo(&Redo, cx); + editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n"); }); } @@ -785,8 +797,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "one.rs"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "one.rs"), None, true, window, cx) }) .await .unwrap() @@ -795,9 +807,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T let fake_language_server = fake_language_servers.next().await.unwrap(); // Move cursor to a location that can be renamed. - let prepare_rename = editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([7..7])); - editor.rename(&Rename, cx).unwrap() + let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([7..7])); + editor.rename(&Rename, window, cx).unwrap() }); fake_language_server @@ -835,12 +847,12 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T }); // Cancel the rename, and repeat the same, but use selections instead of cursor movement - editor_b.update(cx_b, |editor, cx| { - editor.cancel(&editor::actions::Cancel, cx); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.cancel(&editor::actions::Cancel, window, cx); }); - let prepare_rename = editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([7..8])); - editor.rename(&Rename, cx).unwrap() + let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([7..8])); + editor.rename(&Rename, window, cx).unwrap() }); fake_language_server @@ -876,8 +888,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T }); }); - let confirm_rename = editor_b.update(cx_b, |editor, cx| { - Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap() + let confirm_rename = editor_b.update_in(cx_b, |editor, window, cx| { + Editor::confirm_rename(editor, &ConfirmRename, window, cx).unwrap() }); fake_language_server .handle_request::(|params, _| async move { @@ -935,17 +947,17 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T workspace.active_item_as::(cx).unwrap() }); - rename_editor.update(cx_b, |editor, cx| { + rename_editor.update_in(cx_b, |editor, window, cx| { assert_eq!( editor.text(cx), "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" ); - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!( editor.text(cx), "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;" ); - editor.redo(&Redo, cx); + editor.redo(&Redo, window, cx); assert_eq!( editor.text(cx), "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;" @@ -953,12 +965,12 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T }); // Ensure temporary rename edits cannot be undone/redone. - editor_b.update(cx_b, |editor, cx| { - editor.undo(&Undo, cx); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "const ONE: usize = 1;"); - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "const ONE: usize = 1;"); - editor.redo(&Redo, cx); + editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "const THREE: usize = 1;"); }) } @@ -1193,7 +1205,8 @@ async fn test_share_project( .await .unwrap(); - let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx)); + let editor_b = + cx_b.new_window_entity(|window, cx| Editor::for_buffer(buffer_b, None, window, cx)); // Client A sees client B's selection executor.run_until_parked(); @@ -1207,7 +1220,9 @@ async fn test_share_project( }); // Edit the buffer as client B and see that edit as client A. - editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.handle_input("ok, ", window, cx) + }); executor.run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { @@ -1234,7 +1249,7 @@ async fn test_share_project( let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await; // Client B closes the editor, and client A sees client B's selections removed. - cx_b.update(move |_| drop(editor_b)); + cx_b.update(move |_, _| drop(editor_b)); executor.run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { @@ -1298,7 +1313,9 @@ async fn test_on_input_format_from_host_to_guest( .await .unwrap(); let cx_a = cx_a.add_empty_window(); - let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)); + let editor_a = cx_a.new_window_entity(|window, cx| { + Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx) + }); let fake_language_server = fake_language_servers.next().await.unwrap(); executor.run_until_parked(); @@ -1330,10 +1347,10 @@ async fn test_on_input_format_from_host_to_guest( .unwrap(); // Type a on type formatting trigger character as the guest. - cx_a.focus_view(&editor_a); - editor_a.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(">", cx); + cx_a.focus(&editor_a); + editor_a.update_in(cx_a, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.handle_input(">", window, cx); }); executor.run_until_parked(); @@ -1343,9 +1360,9 @@ async fn test_on_input_format_from_host_to_guest( }); // Undo should remove LSP edits first - editor_a.update(cx_a, |editor, cx| { + editor_a.update_in(cx_a, |editor, window, cx| { assert_eq!(editor.text(cx), "fn main() { a>~< }"); - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "fn main() { a> }"); }); executor.run_until_parked(); @@ -1354,9 +1371,9 @@ async fn test_on_input_format_from_host_to_guest( assert_eq!(buffer.text(), "fn main() { a> }") }); - editor_a.update(cx_a, |editor, cx| { + editor_a.update_in(cx_a, |editor, window, cx| { assert_eq!(editor.text(cx), "fn main() { a> }"); - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "fn main() { a }"); }); executor.run_until_parked(); @@ -1418,16 +1435,18 @@ async fn test_on_input_format_from_guest_to_host( .await .unwrap(); let cx_b = cx_b.add_empty_window(); - let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)); + let editor_b = cx_b.new_window_entity(|window, cx| { + Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx) + }); let fake_language_server = fake_language_servers.next().await.unwrap(); executor.run_until_parked(); // Type a on type formatting trigger character as the guest. - cx_b.focus_view(&editor_b); - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input(":", cx); + cx_b.focus(&editor_b); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.handle_input(":", window, cx); }); // Receive an OnTypeFormatting request as the host's language server. @@ -1466,9 +1485,9 @@ async fn test_on_input_format_from_guest_to_host( }); // Undo should remove LSP edits first - editor_b.update(cx_b, |editor, cx| { + editor_b.update_in(cx_b, |editor, window, cx| { assert_eq!(editor.text(cx), "fn main() { a:~: }"); - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "fn main() { a: }"); }); executor.run_until_parked(); @@ -1477,9 +1496,9 @@ async fn test_on_input_format_from_guest_to_host( assert_eq!(buffer.text(), "fn main() { a: }") }); - editor_b.update(cx_b, |editor, cx| { + editor_b.update_in(cx_b, |editor, window, cx| { assert_eq!(editor.text(cx), "fn main() { a: }"); - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "fn main() { a }"); }); executor.run_until_parked(); @@ -1572,6 +1591,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); + executor.run_until_parked(); + // Client B joins the project let project_b = client_b.join_remote_project(project_id, cx_b).await; active_call_b @@ -1590,8 +1611,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, window, cx) }) .await .unwrap() @@ -1640,8 +1661,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( }); let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, window, cx) }) .await .unwrap() @@ -1658,11 +1679,11 @@ async fn test_mutual_editor_inlay_hint_cache_update( }); let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - editor_b.update(cx_b, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); - editor.handle_input(":", cx); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone())); + editor.handle_input(":", window, cx); }); - cx_b.focus_view(&editor_b); + cx_b.focus(&editor_b); executor.run_until_parked(); editor_a.update(cx_a, |editor, _| { @@ -1679,11 +1700,11 @@ async fn test_mutual_editor_inlay_hint_cache_update( }); let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; - editor_a.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([13..13])); - editor.handle_input("a change to increment both buffers' versions", cx); + editor_a.update_in(cx_a, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.handle_input("a change to increment both buffers' versions", window, cx); }); - cx_a.focus_view(&editor_a); + cx_a.focus(&editor_a); executor.run_until_parked(); editor_a.update(cx_a, |editor, _| { @@ -1816,8 +1837,8 @@ async fn test_inlay_hint_refresh_is_forwarded( cx_a.background_executor.start_waiting(); let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, window, cx) }) .await .unwrap() @@ -1825,8 +1846,8 @@ async fn test_inlay_hint_refresh_is_forwarded( .unwrap(); let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, window, cx) }) .await .unwrap() @@ -1986,8 +2007,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA // Create editor_a let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "file.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "file.txt"), None, true, window, cx) }) .await .unwrap() @@ -1998,8 +2019,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let project_b = client_b.join_remote_project(project_id, cx_b).await; let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "file.txt"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "file.txt"), None, true, window, cx) }) .await .unwrap() @@ -2007,9 +2028,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA .unwrap(); // client_b now requests git blame for the open buffer - editor_b.update(cx_b, |editor_b, cx| { + editor_b.update_in(cx_b, |editor_b, window, cx| { assert!(editor_b.blame().is_none()); - editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx); + editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, window, cx); }); cx_a.executor().run_until_parked(); @@ -2019,7 +2040,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); @@ -2047,7 +2076,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA // editor_b updates the file, which gets sent to client_a, which updates git blame, // which gets back to client_b. - editor_b.update(cx_b, |editor_b, cx| { + editor_b.update_in(cx_b, |editor_b, _, cx| { editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx); }); @@ -2058,7 +2087,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); @@ -2074,7 +2111,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA }); // Now editor_a also updates the file - editor_a.update(cx_a, |editor_a, cx| { + editor_a.update_in(cx_a, |editor_a, _, cx| { editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx); }); @@ -2085,7 +2122,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA let blame = editor_b.blame().expect("editor_b should have blame now"); let entries = blame.update(cx, |blame, cx| { blame - .blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx) + .blame_for_rows( + &(0..4) + .map(|row| RowInfo { + buffer_row: Some(row), + ..Default::default() + }) + .collect::>(), + cx, + ) .collect::>() }); @@ -2152,19 +2197,21 @@ async fn test_collaborating_with_editorconfig( .await .unwrap(); let cx_a = cx_a.add_empty_window(); - let main_editor_a = - cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx)); - let other_editor_a = - cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx)); + let main_editor_a = cx_a.new_window_entity(|window, cx| { + Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx) + }); + let other_editor_a = cx_a.new_window_entity(|window, cx| { + Editor::for_buffer(other_buffer_a, Some(project_a), window, cx) + }); let mut main_editor_cx_a = EditorTestContext { cx: cx_a.clone(), - window: cx_a.handle(), + window: cx_a.window_handle(), editor: main_editor_a, assertion_cx: AssertionContextManager::new(), }; let mut other_editor_cx_a = EditorTestContext { cx: cx_a.clone(), - window: cx_a.handle(), + window: cx_a.window_handle(), editor: other_editor_a, assertion_cx: AssertionContextManager::new(), }; @@ -2184,19 +2231,21 @@ async fn test_collaborating_with_editorconfig( .await .unwrap(); let cx_b = cx_b.add_empty_window(); - let main_editor_b = - cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx)); - let other_editor_b = - cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx)); + let main_editor_b = cx_b.new_window_entity(|window, cx| { + Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx) + }); + let other_editor_b = cx_b.new_window_entity(|window, cx| { + Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx) + }); let mut main_editor_cx_b = EditorTestContext { cx: cx_b.clone(), - window: cx_b.handle(), + window: cx_b.window_handle(), editor: main_editor_b, assertion_cx: AssertionContextManager::new(), }; let mut other_editor_cx_b = EditorTestContext { cx: cx_b.clone(), - window: cx_b.handle(), + window: cx_b.window_handle(), editor: other_editor_b, assertion_cx: AssertionContextManager::new(), }; @@ -2393,8 +2442,8 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte // Client A opens an editor. let editor_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path(project_path.clone(), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path(project_path.clone(), None, true, window, cx) }) .await .unwrap() @@ -2403,8 +2452,8 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte // Client B opens same editor as A. let editor_b = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path(project_path.clone(), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path(project_path.clone(), None, true, window, cx) }) .await .unwrap() @@ -2415,8 +2464,8 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte cx_b.run_until_parked(); // Client A adds breakpoint on line (1) - editor_a.update(cx_a, |editor, cx| { - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor_a.update_in(cx_a, |editor, window, cx| { + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); cx_a.run_until_parked(); @@ -2446,10 +2495,10 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte assert_eq!(breakpoints_a, breakpoints_b); // Client B adds breakpoint on line(2) - editor_b.update(cx_b, |editor, cx| { - editor.move_down(&editor::actions::MoveDown, cx); - editor.move_down(&editor::actions::MoveDown, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); cx_a.run_until_parked(); @@ -2479,10 +2528,10 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte assert_eq!(2, breakpoints_a.get(&project_path).unwrap().len()); // Client A removes last added breakpoint from client B - editor_a.update(cx_a, |editor, cx| { - editor.move_down(&editor::actions::MoveDown, cx); - editor.move_down(&editor::actions::MoveDown, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor_a.update_in(cx_a, |editor, window, cx| { + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.move_down(&editor::actions::MoveDown, window, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); cx_a.run_until_parked(); @@ -2512,10 +2561,10 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte assert_eq!(1, breakpoints_a.get(&project_path).unwrap().len()); // Client B removes first added breakpoint by client A - editor_b.update(cx_b, |editor, cx| { - editor.move_up(&editor::actions::MoveUp, cx); - editor.move_up(&editor::actions::MoveUp, cx); - editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor_b.update_in(cx_b, |editor, window, cx| { + editor.move_up(&editor::actions::MoveUp, window, cx); + editor.move_up(&editor::actions::MoveUp, window, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); cx_a.run_until_parked(); @@ -2556,12 +2605,12 @@ fn tab_undo_assert( cx_b.assert_editor_state(expected_initial); if a_tabs { - cx_a.update_editor(|editor, cx| { - editor.tab(&editor::actions::Tab, cx); + cx_a.update_editor(|editor, window, cx| { + editor.tab(&editor::actions::Tab, window, cx); }); } else { - cx_b.update_editor(|editor, cx| { - editor.tab(&editor::actions::Tab, cx); + cx_b.update_editor(|editor, window, cx| { + editor.tab(&editor::actions::Tab, window, cx); }); } @@ -2572,12 +2621,12 @@ fn tab_undo_assert( cx_b.assert_editor_state(expected_tabbed); if a_tabs { - cx_a.update_editor(|editor, cx| { - editor.undo(&editor::actions::Undo, cx); + cx_a.update_editor(|editor, window, cx| { + editor.undo(&editor::actions::Undo, window, cx); }); } else { - cx_b.update_editor(|editor, cx| { - editor.undo(&editor::actions::Undo, cx); + cx_b.update_editor(|editor, window, cx| { + editor.undo(&editor::actions::Undo, window, cx); }); } cx_a.run_until_parked(); diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 4de368d2ea7756..58aed662b20f5a 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -8,8 +8,8 @@ use collab_ui::{ }; use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{ - point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext, - View, VisualContext, VisualTestContext, + point, AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, + TestAppContext, VisualTestContext, }; use language::Capability; use project::WorktreeSettings; @@ -77,23 +77,23 @@ async fn test_basic_following( let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - cx_b.update(|cx| { - assert!(cx.is_window_active()); + cx_b.update(|window, _| { + assert!(window.is_window_active()); }); // Client A opens some editors. let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); let editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, window, cx) }) .await .unwrap() .downcast::() .unwrap(); let editor_a2 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, window, cx) }) .await .unwrap() @@ -102,8 +102,8 @@ async fn test_basic_following( // Client B opens an editor. let editor_b1 = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, window, cx) }) .await .unwrap() @@ -116,22 +116,24 @@ async fn test_basic_following( let peer_id_d = client_d.peer_id().unwrap(); // Client A updates their selections in those editors - editor_a1.update(cx_a, |editor, cx| { - editor.handle_input("a", cx); - editor.handle_input("b", cx); - editor.handle_input("c", cx); - editor.select_left(&Default::default(), cx); + editor_a1.update_in(cx_a, |editor, window, cx| { + editor.handle_input("a", window, cx); + editor.handle_input("b", window, cx); + editor.handle_input("c", window, cx); + editor.select_left(&Default::default(), window, cx); assert_eq!(editor.selections.ranges(cx), vec![3..2]); }); - editor_a2.update(cx_a, |editor, cx| { - editor.handle_input("d", cx); - editor.handle_input("e", cx); - editor.select_left(&Default::default(), cx); + editor_a2.update_in(cx_a, |editor, window, cx| { + editor.handle_input("d", window, cx); + editor.handle_input("e", window, cx); + editor.select_left(&Default::default(), window, cx); assert_eq!(editor.selections.ranges(cx), vec![2..1]); }); // When client B starts following client A, only the active view state is replicated to client B. - workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(peer_id_a, window, cx) + }); cx_c.executor().run_until_parked(); let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { @@ -165,7 +167,9 @@ async fn test_basic_following( drop(project_c); // Client C also follows client A. - workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx)); + workspace_c.update_in(cx_c, |workspace, window, cx| { + workspace.follow(peer_id_a, window, cx) + }); cx_d.executor().run_until_parked(); let active_call_d = cx_d.read(ActiveCall::global); @@ -188,8 +192,8 @@ async fn test_basic_following( } // Client C unfollows client A. - workspace_c.update(cx_c, |workspace, cx| { - workspace.unfollow(peer_id_a, cx).unwrap(); + workspace_c.update_in(cx_c, |workspace, window, cx| { + workspace.unfollow(peer_id_a, window, cx).unwrap(); }); // All clients see that clients B is following client A. @@ -203,7 +207,9 @@ async fn test_basic_following( } // Client C re-follows client A. - workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx)); + workspace_c.update_in(cx_c, |workspace, window, cx| { + workspace.follow(peer_id_a, window, cx) + }); // All clients see that clients B and C are following client A. cx_c.executor().run_until_parked(); @@ -216,9 +222,13 @@ async fn test_basic_following( } // Client D follows client B, then switches to following client C. - workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_b, cx)); + workspace_d.update_in(cx_d, |workspace, window, cx| { + workspace.follow(peer_id_b, window, cx) + }); cx_a.executor().run_until_parked(); - workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_c, cx)); + workspace_d.update_in(cx_d, |workspace, window, cx| { + workspace.follow(peer_id_c, window, cx) + }); // All clients see that D is following C cx_a.executor().run_until_parked(); @@ -235,8 +245,8 @@ async fn test_basic_following( // Client C closes the project. let weak_workspace_c = workspace_c.downgrade(); - workspace_c.update(cx_c, |workspace, cx| { - workspace.close_window(&Default::default(), cx); + workspace_c.update_in(cx_c, |workspace, window, cx| { + workspace.close_window(&Default::default(), window, cx); }); executor.run_until_parked(); // are you sure you want to leave the call? @@ -260,8 +270,8 @@ async fn test_basic_following( } // When client A activates a different editor, client B does so as well. - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a1, true, true, cx) + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.activate_item(&editor_a1, true, true, window, cx) }); executor.run_until_parked(); workspace_b.update(cx_b, |workspace, cx| { @@ -272,7 +282,7 @@ async fn test_basic_following( }); // When client A opens a multibuffer, client B does so as well. - let multibuffer_a = cx_a.new_model(|cx| { + let multibuffer_a = cx_a.new(|cx| { let buffer_a1 = project_a.update(cx, |project, cx| { project .get_open_buffer(&(worktree_id, "1.txt").into(), cx) @@ -302,11 +312,11 @@ async fn test_basic_following( ); result }); - let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { - let editor = cx.new_view(|cx| { - Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx) + let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| { + let editor = cx.new(|cx| { + Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, window, cx) }); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); editor }); executor.run_until_parked(); @@ -324,8 +334,8 @@ async fn test_basic_following( // When client A navigates back and forth, client B does so as well. workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_back(workspace.active_pane().downgrade(), cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) .await .unwrap(); @@ -338,8 +348,8 @@ async fn test_basic_following( }); workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_back(workspace.active_pane().downgrade(), cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.go_back(workspace.active_pane().downgrade(), window, cx) }) .await .unwrap(); @@ -352,8 +362,8 @@ async fn test_basic_following( }); workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_forward(workspace.active_pane().downgrade(), cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.go_forward(workspace.active_pane().downgrade(), window, cx) }) .await .unwrap(); @@ -366,8 +376,8 @@ async fn test_basic_following( }); // Changes to client A's editor are reflected on client B. - editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); + editor_a1.update_in(cx_a, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2])); }); executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); executor.run_until_parked(); @@ -377,13 +387,15 @@ async fn test_basic_following( assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); }); - editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); + editor_a1.update_in(cx_a, |editor, window, cx| { + editor.set_text("TWO", window, cx) + }); executor.run_until_parked(); editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); - editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([3..3])); - editor.set_scroll_position(point(0., 100.), cx); + editor_a1.update_in(cx_a, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([3..3])); + editor.set_scroll_position(point(0., 100.), window, cx); }); executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); executor.run_until_parked(); @@ -392,11 +404,11 @@ async fn test_basic_following( }); // After unfollowing, client B stops receiving updates from client A. - workspace_b.update(cx_b, |workspace, cx| { - workspace.unfollow(peer_id_a, cx).unwrap() + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.unfollow(peer_id_a, window, cx).unwrap() }); - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a2, true, true, cx) + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.activate_item(&editor_a2, true, true, window, cx) }); executor.run_until_parked(); assert_eq!( @@ -408,14 +420,16 @@ async fn test_basic_following( ); // Client A starts following client B. - workspace_a.update(cx_a, |workspace, cx| workspace.follow(peer_id_b, cx)); + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.follow(peer_id_b, window, cx) + }); executor.run_until_parked(); assert_eq!( workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), Some(peer_id_b) ); assert_eq!( - workspace_a.update(cx_a, |workspace, cx| workspace + workspace_a.update_in(cx_a, |workspace, _, cx| workspace .active_item(cx) .unwrap() .item_id()), @@ -471,8 +485,8 @@ async fn test_basic_following( }); // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.activate_item(&multibuffer_editor_b, true, true, cx) + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.activate_item(&multibuffer_editor_b, true, true, window, cx) }); executor.run_until_parked(); workspace_a.update(cx_a, |workspace, cx| { @@ -483,10 +497,10 @@ async fn test_basic_following( }); // Client B activates a panel, and the previously-opened screen-sharing item gets activated. - let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx)); - workspace_b.update(cx_b, |workspace, cx| { - workspace.add_panel(panel, cx); - workspace.toggle_panel_focus::(cx); + let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.add_panel(panel, window, cx); + workspace.toggle_panel_focus::(window, cx); }); executor.run_until_parked(); assert_eq!( @@ -498,8 +512,8 @@ async fn test_basic_following( ); // Toggling the focus back to the pane causes client A to return to the multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_panel_focus::(cx); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.toggle_panel_focus::(window, cx); }); executor.run_until_parked(); workspace_a.update(cx_a, |workspace, cx| { @@ -511,10 +525,10 @@ async fn test_basic_following( // Client B activates an item that doesn't implement following, // so the previously-opened screen-sharing item gets activated. - let unfollowable_item = cx_b.new_view(TestItem::new); - workspace_b.update(cx_b, |workspace, cx| { + let unfollowable_item = cx_b.new(TestItem::new); + workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(unfollowable_item), true, true, None, cx) + pane.add_item(Box::new(unfollowable_item), true, true, None, window, cx) }) }); executor.run_until_parked(); @@ -593,19 +607,19 @@ async fn test_following_tab_order( //Open 1, 3 in that order on client A workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, window, cx) }) .await .unwrap(); workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "3.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, window, cx) }) .await .unwrap(); - let pane_paths = |pane: &View, cx: &mut VisualTestContext| { + let pane_paths = |pane: &Entity, cx: &mut VisualTestContext| { pane.update(cx, |pane, cx| { pane.items() .map(|item| { @@ -624,13 +638,15 @@ async fn test_following_tab_order( assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); //Follow client B as client A - workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx)); + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.follow(client_b_id, window, cx) + }); executor.run_until_parked(); //Open just 2 on client B workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, window, cx) }) .await .unwrap(); @@ -641,8 +657,8 @@ async fn test_following_tab_order( //Open just 1 on client B workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, window, cx) }) .await .unwrap(); @@ -701,8 +717,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client A opens a file. let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, window, cx) }) .await .unwrap() @@ -712,8 +728,8 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client B opens a different file. let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, window, cx) }) .await .unwrap() @@ -721,24 +737,38 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T .unwrap(); // Clients A and B follow each other in split panes - workspace_a.update(cx_a, |workspace, cx| { - workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.split_and_clone( + workspace.active_pane().clone(), + SplitDirection::Right, + window, + cx, + ); }); - workspace_a.update(cx_a, |workspace, cx| { - workspace.follow(client_b.peer_id().unwrap(), cx) + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.follow(client_b.peer_id().unwrap(), window, cx) }); executor.run_until_parked(); - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.split_and_clone( + workspace.active_pane().clone(), + SplitDirection::Right, + window, + cx, + ); }); - workspace_b.update(cx_b, |workspace, cx| { - workspace.follow(client_a.peer_id().unwrap(), cx) + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(client_a.peer_id().unwrap(), window, cx) }); executor.run_until_parked(); // Clients A and B return focus to the original files they had open - workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); - workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.activate_next_pane(window, cx) + }); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.activate_next_pane(window, cx) + }); executor.run_until_parked(); // Both clients see the other client's focused file in their right pane. @@ -775,15 +805,15 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Clients A and B each open a new file. workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "3.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, window, cx) }) .await .unwrap(); workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "4.txt"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "4.txt"), None, true, window, cx) }) .await .unwrap(); @@ -831,7 +861,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T ); // Client A focuses their right pane, in which they're following client B. - workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.activate_next_pane(window, cx) + }); executor.run_until_parked(); // Client B sees that client A is now looking at the same file as them. @@ -877,7 +909,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client B focuses their right pane, in which they're following client A, // who is following them. - workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.activate_next_pane(window, cx) + }); executor.run_until_parked(); // Client A sees that client B is now looking at the same file as them. @@ -923,9 +957,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client B focuses a file that they previously followed A to, breaking // the follow. - workspace_b.update(cx_b, |workspace, cx| { + workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -974,9 +1008,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client B closes tabs, some of which were originally opened by client A, // and some of which were originally opened by client B. - workspace_b.update(cx_b, |workspace, cx| { + workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.close_inactive_items(&Default::default(), cx) + pane.close_inactive_items(&Default::default(), window, cx) .unwrap() .detach(); }); @@ -1022,14 +1056,14 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T ); // Client B follows client A again. - workspace_b.update(cx_b, |workspace, cx| { - workspace.follow(client_a.peer_id().unwrap(), cx) + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(client_a.peer_id().unwrap(), window, cx) }); executor.run_until_parked(); // Client A cycles through some tabs. - workspace_a.update(cx_a, |workspace, cx| { + workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1071,9 +1105,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T ] ); - workspace_a.update(cx_a, |workspace, cx| { + workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1118,9 +1152,9 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T ] ); - workspace_a.update(cx_a, |workspace, cx| { + workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_prev_item(true, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1215,8 +1249,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); let _editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, window, cx) }) .await .unwrap() @@ -1228,7 +1262,9 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont let leader_id = project_b.update(cx_b, |project, _| { project.collaborators().values().next().unwrap().peer_id }); - workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(leader_id, window, cx) + }); executor.run_until_parked(); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), @@ -1243,15 +1279,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont }); // When client B moves, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| { - editor.move_right(&editor::actions::MoveRight, cx) + editor_b2.update_in(cx_b, |editor, window, cx| { + editor.move_right(&editor::actions::MoveRight, window, cx) }); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), None ); - workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(leader_id, window, cx) + }); executor.run_until_parked(); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), @@ -1259,13 +1297,15 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont ); // When client B edits, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); + editor_b2.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx)); assert_eq!( - workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + workspace_b.update_in(cx_b, |workspace, _, _| workspace.leader_for_pane(&pane_b)), None ); - workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(leader_id, window, cx) + }); executor.run_until_parked(); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), @@ -1273,15 +1313,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont ); // When client B scrolls, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| { - editor.set_scroll_position(point(0., 3.), cx) + editor_b2.update_in(cx_b, |editor, window, cx| { + editor.set_scroll_position(point(0., 3.), window, cx) }); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), None ); - workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(leader_id, window, cx) + }); executor.run_until_parked(); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), @@ -1289,15 +1331,17 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont ); // When client B activates a different pane, it continues following client A in the original pane. - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx) }); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), Some(leader_id) ); - workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.activate_next_pane(window, cx) + }); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), Some(leader_id) @@ -1305,8 +1349,8 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont // When client B activates a different item in the original pane, it automatically stops following client A. workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, window, cx) }) .await .unwrap(); @@ -1352,8 +1396,12 @@ async fn test_peers_simultaneously_following_each_other( project.collaborators().values().next().unwrap().peer_id }); - workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx)); - workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx)); + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.follow(client_b_id, window, cx) + }); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(client_a_id, window, cx) + }); executor.run_until_parked(); workspace_a.update(cx_a, |workspace, _| { @@ -1434,8 +1482,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut .unwrap(); workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id_a, "w.rs"), None, true, window, cx) }) .await .unwrap(); @@ -1443,8 +1491,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut executor.run_until_parked(); assert_eq!(visible_push_notifications(cx_b).len(), 1); - workspace_b.update(cx_b, |workspace, cx| { - workspace.follow(client_a.peer_id().unwrap(), cx) + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(client_a.peer_id().unwrap(), window, cx) }); executor.run_until_parked(); @@ -1490,8 +1538,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut // b moves to x.rs in a's project, and a follows workspace_b_project_a - .update(&mut cx_b2, |workspace, cx| { - workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) + .update_in(&mut cx_b2, |workspace, window, cx| { + workspace.open_path((worktree_id_a, "x.rs"), None, true, window, cx) }) .await .unwrap(); @@ -1505,8 +1553,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut ); }); - workspace_a.update(cx_a, |workspace, cx| { - workspace.follow(client_b.peer_id().unwrap(), cx) + workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.follow(client_b.peer_id().unwrap(), window, cx) }); executor.run_until_parked(); @@ -1522,8 +1570,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut // b moves to y.rs in b's project, a is still following but can't yet see workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id_b, "y.rs"), None, true, cx) + .update_in(cx_b, |workspace, window, cx| { + workspace.open_path((worktree_id_b, "y.rs"), None, true, window, cx) }) .await .unwrap(); @@ -1544,7 +1592,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut executor.run_until_parked(); assert_eq!(visible_push_notifications(cx_a).len(), 1); - cx_a.update(|cx| { + cx_a.update(|_, cx| { workspace::join_in_room_project( project_b_id, client_b.user_id().unwrap(), @@ -1607,8 +1655,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T }); // b should follow a to position 1 - editor_a.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([1..1])) + editor_a.update_in(cx_a, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([1..1])) }); cx_a.executor() .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); @@ -1618,7 +1666,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T }); // a unshares the project - cx_a.update(|cx| { + cx_a.update(|_, cx| { let project = workspace_a.read(cx).project().clone(); ActiveCall::global(cx).update(cx, |call, cx| { call.unshare_project(project, cx).unwrap(); @@ -1627,8 +1675,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T cx_a.run_until_parked(); // b should not follow a to position 2 - editor_a.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([2..2])) + editor_a.update_in(cx_a, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([2..2])) }); cx_a.executor() .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); @@ -1636,7 +1684,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T editor_b.update(cx_b, |editor, cx| { assert_eq!(editor.selections.ranges(cx), vec![1..1]) }); - cx_b.update(|cx| { + cx_b.update(|_, cx| { let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx); let participant = room.remote_participants().get(&client_a.id()).unwrap(); assert_eq!(participant.location, ParticipantLocation::UnsharedProject) @@ -1703,16 +1751,16 @@ async fn test_following_into_excluded_file( // Client A opens editors for a regular file and an excluded file. let editor_for_regular = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, window, cx) }) .await .unwrap() .downcast::() .unwrap(); let editor_for_excluded_a = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, window, cx) }) .await .unwrap() @@ -1720,22 +1768,24 @@ async fn test_following_into_excluded_file( .unwrap(); // Client A updates their selections in those editors - editor_for_regular.update(cx_a, |editor, cx| { - editor.handle_input("a", cx); - editor.handle_input("b", cx); - editor.handle_input("c", cx); - editor.select_left(&Default::default(), cx); + editor_for_regular.update_in(cx_a, |editor, window, cx| { + editor.handle_input("a", window, cx); + editor.handle_input("b", window, cx); + editor.handle_input("c", window, cx); + editor.select_left(&Default::default(), window, cx); assert_eq!(editor.selections.ranges(cx), vec![3..2]); }); - editor_for_excluded_a.update(cx_a, |editor, cx| { - editor.select_all(&Default::default(), cx); - editor.handle_input("new commit message", cx); - editor.select_left(&Default::default(), cx); + editor_for_excluded_a.update_in(cx_a, |editor, window, cx| { + editor.select_all(&Default::default(), window, cx); + editor.handle_input("new commit message", window, cx); + editor.select_left(&Default::default(), window, cx); assert_eq!(editor.selections.ranges(cx), vec![18..17]); }); // When client B starts following client A, currently visible file is replicated - workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + workspace_b.update_in(cx_b, |workspace, window, cx| { + workspace.follow(peer_id_a, window, cx) + }); executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); executor.run_until_parked(); @@ -1755,15 +1805,15 @@ async fn test_following_into_excluded_file( vec![18..17] ); - editor_for_excluded_a.update(cx_a, |editor, cx| { - editor.select_right(&Default::default(), cx); + editor_for_excluded_a.update_in(cx_a, |editor, window, cx| { + editor.select_right(&Default::default(), window, cx); }); executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); executor.run_until_parked(); // Changes from B to the excluded file are replicated in A's editor - editor_for_excluded_b.update(cx_b, |editor, cx| { - editor.handle_input("\nCo-Authored-By: B ", cx); + editor_for_excluded_b.update_in(cx_b, |editor, window, cx| { + editor.handle_input("\nCo-Authored-By: B ", window, cx); }); executor.run_until_parked(); editor_for_excluded_a.update(cx_a, |editor, cx| { @@ -1774,13 +1824,11 @@ async fn test_following_into_excluded_file( }); } -fn visible_push_notifications( - cx: &mut TestAppContext, -) -> Vec> { +fn visible_push_notifications(cx: &mut TestAppContext) -> Vec> { let mut ret = Vec::new(); for window in cx.windows() { window - .update(cx, |window, _| { + .update(cx, |window, _, _| { if let Ok(handle) = window.downcast::() { ret.push(handle) } @@ -1821,7 +1869,7 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec }) } -fn pane_summaries(workspace: &View, cx: &mut VisualTestContext) -> Vec { +fn pane_summaries(workspace: &Entity, cx: &mut VisualTestContext) -> Vec { workspace.update(cx, |workspace, cx| { let active_pane = workspace.active_pane(); workspace @@ -1924,14 +1972,14 @@ async fn test_following_to_channel_notes_without_a_shared_project( // Client A opens the notes for channel 1. let channel_notes_1_a = cx_a - .update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx)) + .update(|window, cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), window, cx)) .await .unwrap(); - channel_notes_1_a.update(cx_a, |notes, cx| { + channel_notes_1_a.update_in(cx_a, |notes, window, cx| { assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); notes.editor.update(cx, |editor, cx| { - editor.insert("Hello from A.", cx); - editor.change_selections(None, cx, |selections| { + editor.insert("Hello from A.", window, cx); + editor.change_selections(None, window, cx, |selections| { selections.select_ranges(vec![3..4]); }); }); @@ -1939,9 +1987,9 @@ async fn test_following_to_channel_notes_without_a_shared_project( // Client B follows client A. workspace_b - .update(cx_b, |workspace, cx| { + .update_in(cx_b, |workspace, window, cx| { workspace - .start_following(client_a.peer_id().unwrap(), cx) + .start_following(client_a.peer_id().unwrap(), window, cx) .unwrap() }) .await @@ -1971,7 +2019,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( // Client A opens the notes for channel 2. let channel_notes_2_a = cx_a - .update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx)) + .update(|window, cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), window, cx)) .await .unwrap(); channel_notes_2_a.update(cx_a, |notes, cx| { @@ -1997,8 +2045,8 @@ async fn test_following_to_channel_notes_without_a_shared_project( // Client A opens a local buffer in their unshared project. let _unshared_editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) + .update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, window, cx) }) .await .unwrap() @@ -2027,7 +2075,7 @@ pub(crate) async fn join_channel( } async fn share_workspace( - workspace: &View, + workspace: &Entity, cx: &mut VisualTestContext, ) -> anyhow::Result { let project = workspace.update(cx, |workspace, _| workspace.project().clone()); @@ -2069,9 +2117,9 @@ async fn test_following_to_channel_notes_other_workspace( // a opens a second workspace and the channel notes let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await; - cx_a2.update(|cx| cx.activate_window()); + cx_a2.update(|window, _| window.activate_window()); cx_a2 - .update(|cx| ChannelView::open(channel, None, workspace_a2, cx)) + .update(|window, cx| ChannelView::open(channel, None, workspace_a2, window, cx)) .await .unwrap(); cx_a2.run_until_parked(); @@ -2083,7 +2131,7 @@ async fn test_following_to_channel_notes_other_workspace( }); // a returns to the shared project - cx_a.update(|cx| cx.activate_window()); + cx_a.update(|window, _| window.activate_window()); cx_a.run_until_parked(); workspace_a.update(cx_a, |workspace, cx| { @@ -2141,7 +2189,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut // a opens a file in a new window let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await; - cx_a2.update(|cx| cx.activate_window()); + cx_a2.update(|window, _| window.activate_window()); cx_a2.simulate_keystrokes("cmd-p"); cx_a2.run_until_parked(); cx_a2.simulate_keystrokes("3 enter"); @@ -2152,7 +2200,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut cx_a.run_until_parked(); // a returns to the shared project - cx_a.update(|cx| cx.activate_window()); + cx_a.update(|window, _| window.activate_window()); cx_a.run_until_parked(); workspace_a.update(cx_a, |workspace, cx| { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 37fff19a0344e7..fbc1943c97a317 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6,7 +6,7 @@ use crate::{ }, }; use anyhow::{anyhow, Result}; -use assistant::ContextStore; +use assistant_context_editor::ContextStore; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; use call::{room, ActiveCall, ParticipantLocation, Room}; @@ -18,7 +18,7 @@ use prompt_library::PromptBuilder; use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode}; use gpui::{ - px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent, + px, size, App, BackgroundExecutor, Entity, Modifiers, MouseButton, MouseDownEvent, TestAppContext, UpdateGlobal, }; use language::{ @@ -2073,7 +2073,7 @@ async fn test_mute_deafen( } fn participant_audio_state( - room: &Model, + room: &Entity, cx: &TestAppContext, ) -> Vec { room.read_with(cx, |room, _| { @@ -2252,7 +2252,7 @@ async fn test_room_location( ); fn participant_locations( - room: &Model, + room: &Entity, cx: &TestAppContext, ) -> Vec<(String, ParticipantLocation)> { room.read_with(cx, |room, _| { @@ -2593,7 +2593,7 @@ async fn test_git_diff_base_change( change_set_local_a.read_with(cx_a, |change_set, cx| { let buffer = buffer_local_a.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( @@ -2621,7 +2621,7 @@ async fn test_git_diff_base_change( change_set_remote_a.read_with(cx_b, |change_set, cx| { let buffer = buffer_remote_a.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( @@ -2643,7 +2643,7 @@ async fn test_git_diff_base_change( change_set_local_a.read_with(cx_a, |change_set, cx| { let buffer = buffer_local_a.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( @@ -2657,7 +2657,7 @@ async fn test_git_diff_base_change( change_set_remote_a.read_with(cx_b, |change_set, cx| { let buffer = buffer_remote_a.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( @@ -2703,7 +2703,7 @@ async fn test_git_diff_base_change( change_set_local_b.read_with(cx_a, |change_set, cx| { let buffer = buffer_local_b.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( @@ -2730,7 +2730,7 @@ async fn test_git_diff_base_change( change_set_remote_b.read_with(cx_b, |change_set, cx| { let buffer = buffer_remote_b.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( @@ -2752,7 +2752,7 @@ async fn test_git_diff_base_change( change_set_local_b.read_with(cx_a, |change_set, cx| { let buffer = buffer_local_b.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( @@ -2766,7 +2766,7 @@ async fn test_git_diff_base_change( change_set_remote_b.read_with(cx_b, |change_set, cx| { let buffer = buffer_remote_b.read(cx); assert_eq!( - change_set.base_text_string(cx).as_deref(), + change_set.base_text_string().as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( @@ -2821,7 +2821,7 @@ async fn test_git_branch_name( executor.run_until_parked(); #[track_caller] - fn assert_branch(branch_name: Option>, project: &Project, cx: &AppContext) { + fn assert_branch(branch_name: Option>, project: &Project, cx: &App) { let branch_name = branch_name.map(Into::into); let worktrees = project.visible_worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -2931,7 +2931,7 @@ async fn test_git_status_sync( file: &impl AsRef, status: Option, project: &Project, - cx: &AppContext, + cx: &App, ) { let file = file.as_ref(); let worktrees = project.visible_worktrees(cx).collect::>(); @@ -6167,7 +6167,7 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) { cx.simulate_resize(size(px(300.), px(300.))); cx.simulate_keystrokes("cmd-n cmd-n cmd-n"); - cx.update(|cx| cx.refresh()); + cx.update(|window, _cx| window.refresh()); let tab_bounds = cx.debug_bounds("TAB-2").unwrap(); let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap(); @@ -6260,14 +6260,14 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); - let get_path = |pane: &Pane, idx: usize, cx: &AppContext| { + let get_path = |pane: &Pane, idx: usize, cx: &App| { pane.item_for_index(idx).unwrap().project_path(cx).unwrap() }; // Opening item 3 as a "permanent" tab workspace - .update(cx, |workspace, cx| { - workspace.open_path(path_3.clone(), None, false, cx) + .update_in(cx, |workspace, window, cx| { + workspace.open_path(path_3.clone(), None, false, window, cx) }) .await .unwrap(); @@ -6283,8 +6283,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { // Open item 1 as preview workspace - .update(cx, |workspace, cx| { - workspace.open_path_preview(path_1.clone(), None, true, true, cx) + .update_in(cx, |workspace, window, cx| { + workspace.open_path_preview(path_1.clone(), None, true, true, window, cx) }) .await .unwrap(); @@ -6304,8 +6304,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { // Open item 2 as preview workspace - .update(cx, |workspace, cx| { - workspace.open_path_preview(path_2.clone(), None, true, true, cx) + .update_in(cx, |workspace, window, cx| { + workspace.open_path_preview(path_2.clone(), None, true, true, window, cx) }) .await .unwrap(); @@ -6325,7 +6325,9 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { // Going back should show item 1 as preview workspace - .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx)) + .update_in(cx, |workspace, window, cx| { + workspace.go_back(pane.downgrade(), window, cx) + }) .await .unwrap(); @@ -6343,10 +6345,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { }); // Closing item 1 - pane.update(cx, |pane, cx| { + pane.update_in(cx, |pane, window, cx| { pane.close_item_by_id( pane.active_item().unwrap().item_id(), workspace::SaveIntent::Skip, + window, cx, ) }) @@ -6364,7 +6367,9 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { // Going back should show item 1 as preview workspace - .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx)) + .update_in(cx, |workspace, window, cx| { + workspace.go_back(pane.downgrade(), window, cx) + }) .await .unwrap(); @@ -6382,9 +6387,9 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { }); // Close permanent tab - pane.update(cx, |pane, cx| { + pane.update_in(cx, |pane, window, cx| { let id = pane.items().next().unwrap().item_id(); - pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx) + pane.close_item_by_id(id, workspace::SaveIntent::Skip, window, cx) }) .await .unwrap(); @@ -6431,8 +6436,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { // Open item 2 as preview in right pane workspace - .update(cx, |workspace, cx| { - workspace.open_path_preview(path_2.clone(), None, true, true, cx) + .update_in(cx, |workspace, window, cx| { + workspace.open_path_preview(path_2.clone(), None, true, true, window, cx) }) .await .unwrap(); @@ -6463,14 +6468,14 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { }); // Focus left pane - workspace.update(cx, |workspace, cx| { - workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx) + workspace.update_in(cx, |workspace, window, cx| { + workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx) }); // Open item 2 as preview in left pane workspace - .update(cx, |workspace, cx| { - workspace.open_path_preview(path_2.clone(), None, true, true, cx) + .update_in(cx, |workspace, window, cx| { + workspace.open_path_preview(path_2.clone(), None, true, true, window, cx) }) .await .unwrap(); diff --git a/crates/collab/src/tests/notification_tests.rs b/crates/collab/src/tests/notification_tests.rs index ddbc1d197b3388..cdec32463260f8 100644 --- a/crates/collab/src/tests/notification_tests.rs +++ b/crates/collab/src/tests/notification_tests.rs @@ -21,14 +21,14 @@ async fn test_notifications( let notification_events_b = Arc::new(Mutex::new(Vec::new())); client_a.notification_store().update(cx_a, |_, cx| { let events = notification_events_a.clone(); - cx.subscribe(&cx.handle(), move |_, _, event, _| { + cx.subscribe(&cx.entity(), move |_, _, event, _| { events.lock().push(event.clone()); }) .detach() }); client_b.notification_store().update(cx_b, |_, cx| { let events = notification_events_b.clone(); - cx.subscribe(&cx.handle(), move |_, _, event, _| { + cx.subscribe(&cx.entity(), move |_, _, event, _| { events.lock().push(event.clone()); }) .detach() diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 66bae0e9d445fa..b250473b61663a 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -7,7 +7,7 @@ use collections::{BTreeMap, HashMap}; use editor::Bias; use fs::{FakeFs, Fs as _}; use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode}; -use gpui::{BackgroundExecutor, Model, TestAppContext}; +use gpui::{BackgroundExecutor, Entity, TestAppContext}; use language::{ range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16, }; @@ -1342,7 +1342,7 @@ impl RandomizedTest for ProjectCollaborationTest { .get_unstaged_changes(host_buffer.read(cx).remote_id()) .unwrap() .read(cx) - .base_text_string(cx) + .base_text_string() }); let guest_diff_base = guest_project.read_with(client_cx, |project, cx| { project @@ -1351,7 +1351,7 @@ impl RandomizedTest for ProjectCollaborationTest { .get_unstaged_changes(guest_buffer.read(cx).remote_id()) .unwrap() .read(cx) - .base_text_string(cx) + .base_text_string() }); assert_eq!( guest_diff_base, host_diff_base, @@ -1475,10 +1475,10 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation fn buffer_for_full_path( client: &TestClient, - project: &Model, + project: &Entity, full_path: &PathBuf, cx: &TestAppContext, -) -> Option> { +) -> Option> { client .buffers_for_project(project) .iter() @@ -1494,7 +1494,7 @@ fn project_for_root_name( client: &TestClient, root_name: &str, cx: &TestAppContext, -) -> Option> { +) -> Option> { if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) { return Some(client.local_projects()[ix].clone()); } @@ -1506,7 +1506,7 @@ fn project_for_root_name( } fn project_ix_for_root_name( - projects: &[Model], + projects: &[Entity], root_name: &str, cx: &TestAppContext, ) -> Option { @@ -1518,7 +1518,7 @@ fn project_ix_for_root_name( }) } -fn root_name_for_project(project: &Model, cx: &TestAppContext) -> String { +fn root_name_for_project(project: &Entity, cx: &TestAppContext) -> String { project.read_with(cx, |project, cx| { project .visible_worktrees(cx) @@ -1531,7 +1531,7 @@ fn root_name_for_project(project: &Model, cx: &TestAppContext) -> Strin } fn project_path_for_full_path( - project: &Model, + project: &Entity, full_path: &Path, cx: &TestAppContext, ) -> Option { @@ -1552,7 +1552,7 @@ fn project_path_for_full_path( } async fn ensure_project_shared( - project: &Model, + project: &Entity, client: &TestClient, cx: &mut TestAppContext, ) { @@ -1585,7 +1585,7 @@ async fn ensure_project_shared( } } -fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option> { +fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option> { client .local_projects() .deref() diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 30fa54935eb10f..c251204459b0ee 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -4,7 +4,9 @@ use collections::HashSet; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs as _}; use futures::StreamExt as _; -use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _}; +use gpui::{ + AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _, +}; use http_client::BlockedHttpClient; use language::{ language_settings::{ @@ -73,7 +75,7 @@ async fn test_sharing_an_ssh_remote_project( let remote_http_client = Arc::new(BlockedHttpClient); let node = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(server_cx.executor())); - let _headless_project = server_cx.new_model(|cx| { + let _headless_project = server_cx.new(|cx| { client::init_settings(cx); HeadlessProject::new( HeadlessAppState { @@ -240,7 +242,7 @@ async fn test_ssh_collaboration_git_branches( let remote_http_client = Arc::new(BlockedHttpClient); let node = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(server_cx.executor())); - let headless_project = server_cx.new_model(|cx| { + let headless_project = server_cx.new(|cx| { client::init_settings(cx); HeadlessProject::new( HeadlessAppState { @@ -398,7 +400,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( // User A connects to the remote project via SSH. server_cx.update(HeadlessProject::init); let remote_http_client = Arc::new(BlockedHttpClient); - let _headless_project = server_cx.new_model(|cx| { + let _headless_project = server_cx.new(|cx| { client::init_settings(cx); HeadlessProject::new( HeadlessAppState { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 5d0231153d0a17..e3d2ffb07911fb 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -17,7 +17,7 @@ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; use git::GitHostingProviderRegistry; -use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext}; +use gpui::{AppContext as _, BackgroundExecutor, Entity, Task, TestAppContext, VisualTestContext}; use http_client::FakeHttpClient; use language::LanguageRegistry; use node_runtime::NodeRuntime; @@ -64,17 +64,17 @@ pub struct TestServer { pub struct TestClient { pub username: String, pub app_state: Arc, - channel_store: Model, - notification_store: Model, + channel_store: Entity, + notification_store: Entity, state: RefCell, } #[derive(Default)] struct TestClientState { - local_projects: Vec>, - dev_server_projects: Vec>, - buffers: HashMap, HashSet>>, - channel_buffers: HashSet>, + local_projects: Vec>, + dev_server_projects: Vec>, + buffers: HashMap, HashSet>>, + channel_buffers: HashSet>, } pub struct ContactsSummary { @@ -274,10 +274,10 @@ impl TestServer { git_hosting_provider_registry .register_hosting_provider(Arc::new(git_hosting_providers::Github)); - let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); - let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); - let session = cx.new_model(|cx| AppSession::new(Session::test(), cx)); + let session = cx.new(|cx| AppSession::new(Session::test(), cx)); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), @@ -308,7 +308,7 @@ impl TestServer { settings::KeymapFile::load_asset_allow_partial_failure(os_keymap, cx).unwrap(), ); language_model::LanguageModelRegistry::test(cx); - assistant::context_store::init(&client.clone().into()); + assistant_context_editor::init(client.clone(), cx); }); client @@ -596,15 +596,15 @@ impl TestClient { self.app_state.fs.as_fake() } - pub fn channel_store(&self) -> &Model { + pub fn channel_store(&self) -> &Entity { &self.channel_store } - pub fn notification_store(&self) -> &Model { + pub fn notification_store(&self) -> &Entity { &self.notification_store } - pub fn user_store(&self) -> &Model { + pub fn user_store(&self) -> &Entity { &self.app_state.user_store } @@ -639,19 +639,19 @@ impl TestClient { .await; } - pub fn local_projects(&self) -> impl Deref>> + '_ { + pub fn local_projects(&self) -> impl Deref>> + '_ { Ref::map(self.state.borrow(), |state| &state.local_projects) } - pub fn dev_server_projects(&self) -> impl Deref>> + '_ { + pub fn dev_server_projects(&self) -> impl Deref>> + '_ { Ref::map(self.state.borrow(), |state| &state.dev_server_projects) } - pub fn local_projects_mut(&self) -> impl DerefMut>> + '_ { + pub fn local_projects_mut(&self) -> impl DerefMut>> + '_ { RefMut::map(self.state.borrow_mut(), |state| &mut state.local_projects) } - pub fn dev_server_projects_mut(&self) -> impl DerefMut>> + '_ { + pub fn dev_server_projects_mut(&self) -> impl DerefMut>> + '_ { RefMut::map(self.state.borrow_mut(), |state| { &mut state.dev_server_projects }) @@ -659,8 +659,8 @@ impl TestClient { pub fn buffers_for_project<'a>( &'a self, - project: &Model, - ) -> impl DerefMut>> + 'a { + project: &Entity, + ) -> impl DerefMut>> + 'a { RefMut::map(self.state.borrow_mut(), |state| { state.buffers.entry(project.clone()).or_default() }) @@ -668,12 +668,12 @@ impl TestClient { pub fn buffers( &self, - ) -> impl DerefMut, HashSet>>> + '_ + ) -> impl DerefMut, HashSet>>> + '_ { RefMut::map(self.state.borrow_mut(), |state| &mut state.buffers) } - pub fn channel_buffers(&self) -> impl DerefMut>> + '_ { + pub fn channel_buffers(&self) -> impl DerefMut>> + '_ { RefMut::map(self.state.borrow_mut(), |state| &mut state.channel_buffers) } @@ -703,7 +703,7 @@ impl TestClient { &self, root_path: impl AsRef, cx: &mut TestAppContext, - ) -> (Model, WorktreeId) { + ) -> (Entity, WorktreeId) { let project = self.build_empty_local_project(cx); let (worktree, _) = project .update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx)) @@ -718,9 +718,9 @@ impl TestClient { pub async fn build_ssh_project( &self, root_path: impl AsRef, - ssh: Model, + ssh: Entity, cx: &mut TestAppContext, - ) -> (Model, WorktreeId) { + ) -> (Entity, WorktreeId) { let project = cx.update(|cx| { Project::ssh( ssh, @@ -739,7 +739,7 @@ impl TestClient { (project, worktree.read_with(cx, |tree, _| tree.id())) } - pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Model { + pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Entity { self.fs() .insert_tree( "/a", @@ -755,17 +755,17 @@ impl TestClient { pub async fn host_workspace( &self, - workspace: &View, + workspace: &Entity, channel_id: ChannelId, cx: &mut VisualTestContext, ) { - cx.update(|cx| { + cx.update(|_, cx| { let active_call = ActiveCall::global(cx); active_call.update(cx, |call, cx| call.join_channel(channel_id, cx)) }) .await .unwrap(); - cx.update(|cx| { + cx.update(|_, cx| { let active_call = ActiveCall::global(cx); let project = workspace.read(cx).project().clone(); active_call.update(cx, |call, cx| call.share_project(project, cx)) @@ -779,7 +779,7 @@ impl TestClient { &'a self, channel_id: ChannelId, cx: &'a mut TestAppContext, - ) -> (View, &'a mut VisualTestContext) { + ) -> (Entity, &'a mut VisualTestContext) { cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx)) .await .unwrap(); @@ -788,7 +788,7 @@ impl TestClient { self.active_workspace(cx) } - pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model { + pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Entity { cx.update(|cx| { Project::local( self.client().clone(), @@ -806,7 +806,7 @@ impl TestClient { &self, host_project_id: u64, guest_cx: &mut TestAppContext, - ) -> Model { + ) -> Entity { let active_call = guest_cx.read(ActiveCall::global); let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone()); room.update(guest_cx, |room, cx| { @@ -823,47 +823,47 @@ impl TestClient { pub fn build_workspace<'a>( &'a self, - project: &Model, + project: &Entity, cx: &'a mut TestAppContext, - ) -> (View, &'a mut VisualTestContext) { - cx.add_window_view(|cx| { - cx.activate_window(); - Workspace::new(None, project.clone(), self.app_state.clone(), cx) + ) -> (Entity, &'a mut VisualTestContext) { + cx.add_window_view(|window, cx| { + window.activate_window(); + Workspace::new(None, project.clone(), self.app_state.clone(), window, cx) }) } pub async fn build_test_workspace<'a>( &'a self, cx: &'a mut TestAppContext, - ) -> (View, &'a mut VisualTestContext) { + ) -> (Entity, &'a mut VisualTestContext) { let project = self.build_test_project(cx).await; - cx.add_window_view(|cx| { - cx.activate_window(); - Workspace::new(None, project.clone(), self.app_state.clone(), cx) + cx.add_window_view(|window, cx| { + window.activate_window(); + Workspace::new(None, project.clone(), self.app_state.clone(), window, cx) }) } pub fn active_workspace<'a>( &'a self, cx: &'a mut TestAppContext, - ) -> (View, &'a mut VisualTestContext) { + ) -> (Entity, &'a mut VisualTestContext) { let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); - let view = window.root_view(cx).unwrap(); + let model = window.root(cx).unwrap(); let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut(); // it might be nice to try and cleanup these at the end of each test. - (view, cx) + (model, cx) } } pub fn open_channel_notes( channel_id: ChannelId, cx: &mut VisualTestContext, -) -> Task>> { - let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); - let view = window.root_view(cx).unwrap(); +) -> Task>> { + let window = cx.update(|_, cx| cx.active_window().unwrap().downcast::().unwrap()); + let model = window.root(cx).unwrap(); - cx.update(|cx| ChannelView::open(channel_id, None, view.clone(), cx)) + cx.update(|window, cx| ChannelView::open(channel_id, None, model.clone(), window, cx)) } impl Drop for TestClient { diff --git a/crates/collab/src/user_backfiller.rs b/crates/collab/src/user_backfiller.rs index 277e9dc80e3535..dcabe8d2167786 100644 --- a/crates/collab/src/user_backfiller.rs +++ b/crates/collab/src/user_backfiller.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use chrono::{DateTime, Utc}; use util::ResultExt; diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index e12e4b326276ae..a4e86a9f2f518b 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -11,9 +11,8 @@ use editor::{ EditorEvent, }; use gpui::{ - actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, FocusableView, Model, - Pixels, Point, Render, Subscription, Task, View, ViewContext, VisualContext as _, WeakView, - WindowContext, + actions, AnyView, App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, + Render, Subscription, Task, VisualContext as _, WeakEntity, Window, }; use project::Project; use rpc::proto::ChannelVisibility; @@ -33,16 +32,16 @@ use workspace::{ actions!(collab, [CopyLink]); -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { workspace::FollowableViewRegistry::register::(cx) } pub struct ChannelView { - pub editor: View, - workspace: WeakView, - project: Model, - channel_store: Model, - channel_buffer: Model, + pub editor: Entity, + workspace: WeakEntity, + project: Entity, + channel_store: Entity, + channel_buffer: Entity, remote_id: Option, _editor_event_subscription: Subscription, _reparse_subscription: Option, @@ -52,20 +51,22 @@ impl ChannelView { pub fn open( channel_id: ChannelId, link_position: Option, - workspace: View, - cx: &mut WindowContext, - ) -> Task>> { + workspace: Entity, + window: &mut Window, + cx: &mut App, + ) -> Task>> { let pane = workspace.read(cx).active_pane().clone(); let channel_view = Self::open_in_pane( channel_id, link_position, pane.clone(), workspace.clone(), + window, cx, ); - cx.spawn(|mut cx| async move { + window.spawn(cx, |mut cx| async move { let channel_view = channel_view.await?; - pane.update(&mut cx, |pane, cx| { + pane.update_in(&mut cx, |pane, window, cx| { telemetry::event!( "Channel Notes Opened", channel_id, @@ -74,7 +75,7 @@ impl ChannelView { .room() .map(|r| r.read(cx).id()) ); - pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); + pane.add_item(Box::new(channel_view.clone()), true, true, None, window, cx); })?; anyhow::Ok(channel_view) }) @@ -83,15 +84,16 @@ impl ChannelView { pub fn open_in_pane( channel_id: ChannelId, link_position: Option, - pane: View, - workspace: View, - cx: &mut WindowContext, - ) -> Task>> { - let channel_view = Self::load(channel_id, workspace, cx); - cx.spawn(|mut cx| async move { + pane: Entity, + workspace: Entity, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + let channel_view = Self::load(channel_id, workspace, window, cx); + window.spawn(cx, |mut cx| async move { let channel_view = channel_view.await?; - pane.update(&mut cx, |pane, cx| { + pane.update_in(&mut cx, |pane, window, cx| { let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx); let existing_view = pane @@ -104,7 +106,12 @@ impl ChannelView { { if let Some(link_position) = link_position { existing_view.update(cx, |channel_view, cx| { - channel_view.focus_position_from_link(link_position, true, cx) + channel_view.focus_position_from_link( + link_position, + true, + window, + cx, + ) }); } return existing_view; @@ -115,15 +122,27 @@ impl ChannelView { // replace that. if let Some(existing_item) = existing_view { if let Some(ix) = pane.index_for_item(&existing_item) { - pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx) - .detach(); - pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx); + pane.close_item_by_id( + existing_item.entity_id(), + SaveIntent::Skip, + window, + cx, + ) + .detach(); + pane.add_item( + Box::new(channel_view.clone()), + true, + true, + Some(ix), + window, + cx, + ); } } if let Some(link_position) = link_position { channel_view.update(cx, |channel_view, cx| { - channel_view.focus_position_from_link(link_position, true, cx) + channel_view.focus_position_from_link(link_position, true, window, cx) }); } @@ -134,9 +153,10 @@ impl ChannelView { pub fn load( channel_id: ChannelId, - workspace: View, - cx: &mut WindowContext, - ) -> Task>> { + workspace: Entity, + window: &mut Window, + cx: &mut App, + ) -> Task>> { let weak_workspace = workspace.downgrade(); let workspace = workspace.read(cx); let project = workspace.project().to_owned(); @@ -146,7 +166,7 @@ impl ChannelView { let channel_buffer = channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); - cx.spawn(|mut cx| async move { + window.spawn(cx, |mut cx| async move { let channel_buffer = channel_buffer.await?; let markdown = markdown.await.log_err(); @@ -160,9 +180,15 @@ impl ChannelView { }) })?; - cx.new_view(|cx| { - let mut this = - Self::new(project, weak_workspace, channel_store, channel_buffer, cx); + cx.new_window_entity(|window, cx| { + let mut this = Self::new( + project, + weak_workspace, + channel_store, + channel_buffer, + window, + cx, + ); this.acknowledge_buffer_version(cx); this }) @@ -170,25 +196,28 @@ impl ChannelView { } pub fn new( - project: Model, - workspace: WeakView, - channel_store: Model, - channel_buffer: Model, - cx: &mut ViewContext, + project: Entity, + workspace: WeakEntity, + channel_store: Entity, + channel_buffer: Entity, + window: &mut Window, + cx: &mut Context, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); - let this = cx.view().downgrade(); - let editor = cx.new_view(|cx| { - let mut editor = Editor::for_buffer(buffer, None, cx); + let this = cx.entity().downgrade(); + let editor = cx.new(|cx| { + let mut editor = Editor::for_buffer(buffer, None, window, cx); editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( channel_buffer.clone(), ))); - editor.set_custom_context_menu(move |_, position, cx| { + editor.set_custom_context_menu(move |_, position, window, cx| { let this = this.clone(); - Some(ui::ContextMenu::build(cx, move |menu, _| { - menu.entry("Copy link to section", None, move |cx| { - this.update(cx, |this, cx| this.copy_link_for_position(position, cx)) - .ok(); + Some(ui::ContextMenu::build(window, cx, move |menu, _, _| { + menu.entry("Copy link to section", None, move |window, cx| { + this.update(cx, |this, cx| { + this.copy_link_for_position(position, window, cx) + }) + .ok(); }) })) }); @@ -197,7 +226,7 @@ impl ChannelView { let _editor_event_subscription = cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone())); - cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + cx.subscribe_in(&channel_buffer, window, Self::handle_channel_buffer_event) .detach(); Self { @@ -216,10 +245,13 @@ impl ChannelView { &mut self, position: String, first_attempt: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let position = Channel::slug(&position).to_lowercase(); - let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx)); + let snapshot = self + .editor + .update(cx, |editor, cx| editor.snapshot(window, cx)); if let Some(outline) = snapshot.buffer_snapshot.outline(None) { if let Some(item) = outline @@ -228,7 +260,7 @@ impl ChannelView { .find(|item| &Channel::slug(&item.text).to_lowercase() == &position) { self.editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::focused()), cx, |s| { + editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| { s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)]) }) }); @@ -239,12 +271,13 @@ impl ChannelView { if !first_attempt { return; } - self._reparse_subscription = Some(cx.subscribe( + self._reparse_subscription = Some(cx.subscribe_in( &self.editor, - move |this, _, e: &EditorEvent, cx| { + window, + move |this, _, e: &EditorEvent, window, cx| { match e { EditorEvent::Reparsed(_) => { - this.focus_position_from_link(position.clone(), false, cx); + this.focus_position_from_link(position.clone(), false, window, cx); this._reparse_subscription.take(); } EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => { @@ -256,15 +289,22 @@ impl ChannelView { )); } - fn copy_link(&mut self, _: &CopyLink, cx: &mut ViewContext) { + fn copy_link(&mut self, _: &CopyLink, window: &mut Window, cx: &mut Context) { let position = self .editor .update(cx, |editor, cx| editor.selections.newest_display(cx).start); - self.copy_link_for_position(position, cx) + self.copy_link_for_position(position, window, cx) } - fn copy_link_for_position(&self, position: DisplayPoint, cx: &mut ViewContext) { - let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx)); + fn copy_link_for_position( + &self, + position: DisplayPoint, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self + .editor + .update(cx, |editor, cx| editor.snapshot(window, cx)); let mut closest_heading = None; @@ -298,15 +338,16 @@ impl ChannelView { .ok(); } - pub fn channel(&self, cx: &AppContext) -> Option> { + pub fn channel(&self, cx: &App) -> Option> { self.channel_buffer.read(cx).channel(cx) } fn handle_channel_buffer_event( &mut self, - _: Model, + _: &Entity, event: &ChannelBufferEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { @@ -320,7 +361,7 @@ impl ChannelView { }); } ChannelBufferEvent::BufferEdited => { - if self.editor.read(cx).is_focused(cx) { + if self.editor.read(cx).is_focused(window) { self.acknowledge_buffer_version(cx); } else { self.channel_store.update(cx, |store, cx| { @@ -338,7 +379,7 @@ impl ChannelView { } } - fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext) { + fn acknowledge_buffer_version(&mut self, cx: &mut Context) { self.channel_store.update(cx, |store, cx| { let channel_buffer = self.channel_buffer.read(cx); store.acknowledge_notes_version( @@ -357,7 +398,7 @@ impl ChannelView { impl EventEmitter for ChannelView {} impl Render for ChannelView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .size_full() .on_action(cx.listener(Self::copy_link)) @@ -365,8 +406,8 @@ impl Render for ChannelView { } } -impl FocusableView for ChannelView { - fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { +impl Focusable for ChannelView { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.editor.read(cx).focus_handle(cx) } } @@ -377,8 +418,8 @@ impl Item for ChannelView { fn act_as_type<'a>( &'a self, type_id: TypeId, - self_handle: &'a View, - _: &'a AppContext, + self_handle: &'a Entity, + _: &'a App, ) -> Option { if type_id == TypeId::of::() { Some(self_handle.to_any()) @@ -389,7 +430,7 @@ impl Item for ChannelView { } } - fn tab_icon(&self, cx: &WindowContext) -> Option { + fn tab_icon(&self, _: &Window, cx: &App) -> Option { let channel = self.channel(cx)?; let icon = match channel.visibility { ChannelVisibility::Public => IconName::Public, @@ -399,7 +440,7 @@ impl Item for ChannelView { Some(Icon::new(icon)) } - fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> gpui::AnyElement { + fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> gpui::AnyElement { let (channel_name, status) = if let Some(channel) = self.channel(cx) { let status = match ( self.channel_buffer.read(cx).buffer().read(cx).read_only(), @@ -439,38 +480,52 @@ impl Item for ChannelView { fn clone_on_split( &self, _: Option, - cx: &mut ViewContext, - ) -> Option> { - Some(cx.new_view(|cx| { + window: &mut Window, + cx: &mut Context, + ) -> Option> { + Some(cx.new(|cx| { Self::new( self.project.clone(), self.workspace.clone(), self.channel_store.clone(), self.channel_buffer.clone(), + window, cx, ) })) } - fn is_singleton(&self, _cx: &AppContext) -> bool { + fn is_singleton(&self, _cx: &App) -> bool { false } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + fn navigate( + &mut self, + data: Box, + window: &mut Window, + cx: &mut Context, + ) -> bool { self.editor - .update(cx, |editor, cx| editor.navigate(data, cx)) + .update(cx, |editor, cx| editor.navigate(data, window, cx)) } - fn deactivated(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, Item::deactivated) + fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { + self.editor + .update(cx, |item, cx| item.deactivated(window, cx)) } - fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + fn set_nav_history( + &mut self, + history: ItemNavHistory, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + Item::set_nav_history(editor, history, window, cx) + }) } - fn as_searchable(&self, _: &View) -> Option> { + fn as_searchable(&self, _: &Entity) -> Option> { Some(Box::new(self.editor.clone())) } @@ -478,7 +533,7 @@ impl Item for ChannelView { true } - fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { + fn pixel_position_of_cursor(&self, cx: &App) -> Option> { self.editor.read(cx).pixel_position_of_cursor(cx) } @@ -492,7 +547,7 @@ impl FollowableItem for ChannelView { self.remote_id } - fn to_state_proto(&self, cx: &WindowContext) -> Option { + fn to_state_proto(&self, window: &Window, cx: &App) -> Option { let channel_buffer = self.channel_buffer.read(cx); if !channel_buffer.is_connected() { return None; @@ -502,7 +557,7 @@ impl FollowableItem for ChannelView { proto::view::ChannelView { channel_id: channel_buffer.channel_id.0, editor: if let Some(proto::view::Variant::Editor(proto)) = - self.editor.read(cx).to_state_proto(cx) + self.editor.read(cx).to_state_proto(window, cx) { Some(proto) } else { @@ -513,11 +568,12 @@ impl FollowableItem for ChannelView { } fn from_state_proto( - workspace: View, + workspace: Entity, remote_id: workspace::ViewId, state: &mut Option, - cx: &mut WindowContext, - ) -> Option>>> { + window: &mut Window, + cx: &mut App, + ) -> Option>>> { let Some(proto::view::Variant::ChannelView(_)) = state else { return None; }; @@ -525,12 +581,12 @@ impl FollowableItem for ChannelView { unreachable!() }; - let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx); + let open = ChannelView::load(ChannelId(state.channel_id), workspace, window, cx); - Some(cx.spawn(|mut cx| async move { + Some(window.spawn(cx, |mut cx| async move { let this = open.await?; - let task = this.update(&mut cx, |this, cx| { + let task = this.update_in(&mut cx, |this, window, cx| { this.remote_id = Some(remote_id); if let Some(state) = state.editor { @@ -545,6 +601,7 @@ impl FollowableItem for ChannelView { scroll_y: state.scroll_y, ..Default::default() }), + window, cx, ) })) @@ -565,31 +622,38 @@ impl FollowableItem for ChannelView { &self, event: &EditorEvent, update: &mut Option, - cx: &WindowContext, + window: &Window, + cx: &App, ) -> bool { self.editor .read(cx) - .add_event_to_update_proto(event, update, cx) + .add_event_to_update_proto(event, update, window, cx) } fn apply_update_proto( &mut self, - project: &Model, + project: &Entity, message: proto::update_view::Variant, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> gpui::Task> { self.editor.update(cx, |editor, cx| { - editor.apply_update_proto(project, message, cx) + editor.apply_update_proto(project, message, window, cx) }) } - fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + fn set_leader_peer_id( + &mut self, + leader_peer_id: Option, + window: &mut Window, + cx: &mut Context, + ) { self.editor.update(cx, |editor, cx| { - editor.set_leader_peer_id(leader_peer_id, cx) + editor.set_leader_peer_id(leader_peer_id, window, cx) }) } - fn is_project_item(&self, _cx: &WindowContext) -> bool { + fn is_project_item(&self, _window: &Window, _cx: &App) -> bool { false } @@ -597,7 +661,7 @@ impl FollowableItem for ChannelView { Editor::to_follow_event(event) } - fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option { + fn dedup(&self, existing: &Self, _: &Window, cx: &App) -> Option { let existing = existing.channel_buffer.read(cx); if self.channel_buffer.read(cx).channel_id == existing.channel_id { if existing.is_connected() { @@ -611,21 +675,18 @@ impl FollowableItem for ChannelView { } } -struct ChannelBufferCollaborationHub(Model); +struct ChannelBufferCollaborationHub(Entity); impl CollaborationHub for ChannelBufferCollaborationHub { - fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap { self.0.read(cx).collaborators() } - fn user_participant_indices<'a>( - &self, - cx: &'a AppContext, - ) -> &'a HashMap { + fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap { self.0.read(cx).user_store().read(cx).participant_indices() } - fn user_names(&self, cx: &AppContext) -> HashMap { + fn user_names(&self, cx: &App) -> HashMap { let user_ids = self.collaborators(cx).values().map(|c| c.user_id); self.0 .read(cx) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index daa1f1440d376d..306bcd53bbede3 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -7,10 +7,10 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::{actions, Editor}; use gpui::{ - actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, ClipboardItem, - CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight, - HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, Stateful, Subscription, - Task, View, ViewContext, VisualContext, WeakView, + actions, div, list, prelude::*, px, Action, App, AsyncWindowContext, ClipboardItem, Context, + CursorStyle, DismissEvent, ElementId, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, + HighlightStyle, ListOffset, ListScrollEvent, ListState, Render, Stateful, Subscription, Task, + WeakEntity, Window, }; use language::LanguageRegistry; use menu::Confirm; @@ -36,10 +36,10 @@ mod message_editor; const MESSAGE_LOADING_THRESHOLD: usize = 50; const CHAT_PANEL_KEY: &str = "ChatPanel"; -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, _| { - workspace.register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _| { + workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { + workspace.toggle_panel_focus::(window, cx); }); }) .detach(); @@ -47,11 +47,11 @@ pub fn init(cx: &mut AppContext) { pub struct ChatPanel { client: Arc, - channel_store: Model, + channel_store: Entity, languages: Arc, message_list: ListState, - active_chat: Option<(Model, Subscription)>, - message_editor: View, + active_chat: Option<(Entity, Subscription)>, + message_editor: Entity, local_timezone: UtcOffset, fs: Arc, width: Option, @@ -74,37 +74,46 @@ struct SerializedChatPanel { actions!(chat_panel, [ToggleFocus]); impl ChatPanel { - pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { + pub fn new( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) -> Entity { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let channel_store = ChannelStore::global(cx); let user_store = workspace.app_state().user_store.clone(); let languages = workspace.app_state().languages.clone(); - let input_editor = cx.new_view(|cx| { + let input_editor = cx.new(|cx| { MessageEditor::new( languages.clone(), user_store.clone(), None, - cx.new_view(|cx| Editor::auto_height(4, cx)), + cx.new(|cx| Editor::auto_height(4, window, cx)), + window, cx, ) }); - cx.new_view(|cx: &mut ViewContext| { - let view = cx.view().downgrade(); - let message_list = - ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| { - if let Some(view) = view.upgrade() { - view.update(cx, |view, cx| { - view.render_message(ix, cx).into_any_element() + cx.new(|cx| { + let model = cx.entity().downgrade(); + let message_list = ListState::new( + 0, + gpui::ListAlignment::Bottom, + px(1000.), + move |ix, window, cx| { + if let Some(model) = model.upgrade() { + model.update(cx, |this: &mut Self, cx| { + this.render_message(ix, window, cx).into_any_element() }) } else { div().into_any() } - }); + }, + ); - message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| { + message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, _, cx| { if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { this.load_more_messages(cx); } @@ -172,7 +181,7 @@ impl ChatPanel { }) } - pub fn channel_id(&self, cx: &AppContext) -> Option { + pub fn channel_id(&self, cx: &App) -> Option { self.active_chat .as_ref() .map(|(chat, _)| chat.read(cx).channel_id) @@ -182,14 +191,14 @@ impl ChatPanel { self.is_scrolled_to_bottom } - pub fn active_chat(&self) -> Option> { + pub fn active_chat(&self) -> Option> { self.active_chat.as_ref().map(|(chat, _)| chat.clone()) } pub fn load( - workspace: WeakView, + workspace: WeakEntity, cx: AsyncWindowContext, - ) -> Task>> { + ) -> Task>> { cx.spawn(|mut cx| async move { let serialized_panel = if let Some(panel) = cx .background_executor() @@ -203,8 +212,8 @@ impl ChatPanel { None }; - workspace.update(&mut cx, |workspace, cx| { - let panel = Self::new(workspace, cx); + workspace.update_in(&mut cx, |workspace, window, cx| { + let panel = Self::new(workspace, window, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|r| r.round()); @@ -216,7 +225,7 @@ impl ChatPanel { }) } - fn serialize(&mut self, cx: &mut ViewContext) { + fn serialize(&mut self, cx: &mut Context) { let width = self.width; self.pending_serialization = cx.background_executor().spawn( async move { @@ -232,7 +241,7 @@ impl ChatPanel { ); } - fn set_active_chat(&mut self, chat: Model, cx: &mut ViewContext) { + fn set_active_chat(&mut self, chat: Entity, cx: &mut Context) { if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { self.markdown_data.clear(); self.message_list.reset(chat.read(cx).message_count()); @@ -249,9 +258,9 @@ impl ChatPanel { fn channel_did_change( &mut self, - _: Model, + _: Entity, event: &ChannelChatEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { match event { ChannelChatEvent::MessagesUpdated { @@ -284,7 +293,7 @@ impl ChatPanel { cx.notify(); } - fn acknowledge_last_message(&mut self, cx: &mut ViewContext) { + fn acknowledge_last_message(&mut self, cx: &mut Context) { if self.active && self.is_scrolled_to_bottom { if let Some((chat, _)) = &self.active_chat { if let Some(channel_id) = self.channel_id(cx) { @@ -305,7 +314,7 @@ impl ChatPanel { &mut self, message_id: Option, reply_to_message: &Option, - cx: &mut ViewContext, + cx: &mut Context, ) -> impl IntoElement { let reply_to_message = match reply_to_message { None => { @@ -369,8 +378,8 @@ impl ChatPanel { ), ) .cursor(CursorStyle::PointingHand) - .tooltip(|cx| Tooltip::text("Go to message", cx)) - .on_click(cx.listener(move |chat_panel, _, cx| { + .tooltip(Tooltip::text("Go to message")) + .on_click(cx.listener(move |chat_panel, _, _, cx| { if let Some(channel_id) = current_channel_id { chat_panel .select_channel(channel_id, reply_to_message_id.into(), cx) @@ -380,7 +389,12 @@ impl ChatPanel { ) } - fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { + fn render_message( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { let active_chat = &self.active_chat.as_ref().unwrap().0; let (message, is_continuation_from_previous, is_admin) = active_chat.update(cx, |active_chat, cx| { @@ -530,7 +544,7 @@ impl ChatPanel { .w_full() .text_ui_sm(cx) .id(element_id) - .child(text.element("body".into(), cx)), + .child(text.element("body".into(), window, cx)), ) .when(self.has_open_menu(message_id), |el| { el.bg(cx.theme().colors().element_selected) @@ -560,7 +574,7 @@ impl ChatPanel { }, ) .child( - self.render_popover_buttons(cx, message_id, can_delete_message, can_edit_message) + self.render_popover_buttons(message_id, can_delete_message, can_edit_message, cx) .mt_neg_2p5(), ) } @@ -572,7 +586,7 @@ impl ChatPanel { } } - fn render_popover_button(&self, cx: &ViewContext, child: Stateful
) -> Div { + fn render_popover_button(&self, cx: &mut Context, child: Stateful
) -> Div { div() .w_6() .bg(cx.theme().colors().element_background) @@ -582,10 +596,10 @@ impl ChatPanel { fn render_popover_buttons( &self, - cx: &ViewContext, message_id: Option, can_delete_message: bool, can_edit_message: bool, + cx: &mut Context, ) -> Div { h_flex() .absolute() @@ -606,16 +620,16 @@ impl ChatPanel { .id("reply") .child( IconButton::new(("reply", message_id), IconName::ReplyArrowRight) - .on_click(cx.listener(move |this, _, cx| { + .on_click(cx.listener(move |this, _, window, cx| { this.cancel_edit_message(cx); this.message_editor.update(cx, |editor, cx| { editor.set_reply_to_message_id(message_id); - editor.focus_handle(cx).focus(cx); + window.focus(&editor.focus_handle(cx)); }) })), ) - .tooltip(|cx| Tooltip::text("Reply", cx)), + .tooltip(Tooltip::text("Reply")), ), ) }) @@ -628,7 +642,7 @@ impl ChatPanel { .id("edit") .child( IconButton::new(("edit", message_id), IconName::Pencil) - .on_click(cx.listener(move |this, _, cx| { + .on_click(cx.listener(move |this, _, window, cx| { this.message_editor.update(cx, |editor, cx| { editor.clear_reply_to_message_id(); @@ -655,18 +669,18 @@ impl ChatPanel { }); editor.set_edit_message_id(message_id); - editor.focus_handle(cx).focus(cx); + editor.focus_handle(cx).focus(window); } }) })), ) - .tooltip(|cx| Tooltip::text("Edit", cx)), + .tooltip(Tooltip::text("Edit")), ), ) }) }) .when_some(message_id, |el, message_id| { - let this = cx.view().clone(); + let this = cx.entity().clone(); el.child( self.render_popover_button( @@ -678,34 +692,36 @@ impl ChatPanel { ("trigger", message_id), IconName::Ellipsis, )) - .menu(move |cx| { + .menu(move |window, cx| { Some(Self::render_message_menu( &this, message_id, can_delete_message, + window, cx, )) }), ) .id("more") - .tooltip(|cx| Tooltip::text("More", cx)), + .tooltip(Tooltip::text("More")), ), ) }) } fn render_message_menu( - this: &View, + this: &Entity, message_id: u64, can_delete_message: bool, - cx: &mut WindowContext, - ) -> View { + window: &mut Window, + cx: &mut App, + ) -> Entity { let menu = { - ContextMenu::build(cx, move |menu, cx| { + ContextMenu::build(window, cx, move |menu, window, _| { menu.entry( "Copy message text", None, - cx.handler_for(this, move |this, cx| { + window.handler_for(this, move |this, _, cx| { if let Some(message) = this.active_chat().and_then(|active_chat| { active_chat.read(cx).find_loaded_message(message_id) }) { @@ -718,15 +734,21 @@ impl ChatPanel { menu.entry( "Delete message", None, - cx.handler_for(this, move |this, cx| this.remove_message(message_id, cx)), + window.handler_for(this, move |this, _, cx| { + this.remove_message(message_id, cx) + }), ) }) }) }; this.update(cx, |this, cx| { - let subscription = cx.subscribe(&menu, |this: &mut Self, _, _: &DismissEvent, _| { - this.open_context_menu = None; - }); + let subscription = cx.subscribe_in( + &menu, + window, + |this: &mut Self, _, _: &DismissEvent, _, _| { + this.open_context_menu = None; + }, + ); this.open_context_menu = Some((message_id, subscription)); }); menu @@ -737,7 +759,7 @@ impl ChatPanel { current_user_id: u64, message: &channel::ChannelMessage, local_timezone: UtcOffset, - cx: &AppContext, + cx: &App, ) -> RichText { let mentions = message .mentions @@ -777,19 +799,19 @@ impl ChatPanel { ); rich_text.custom_ranges.push(range); - rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, cx| { - Some(Tooltip::text(edit_timestamp_text.clone(), cx)) + rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, _, cx| { + Some(Tooltip::simple(edit_timestamp_text.clone(), cx)) }) } } rich_text } - fn send(&mut self, _: &Confirm, cx: &mut ViewContext) { + fn send(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { if let Some((chat, _)) = self.active_chat.as_ref() { let message = self .message_editor - .update(cx, |editor, cx| editor.take_message(cx)); + .update(cx, |editor, cx| editor.take_message(window, cx)); if let Some(id) = self.message_editor.read(cx).edit_message_id() { self.message_editor.update(cx, |editor, _| { @@ -811,13 +833,13 @@ impl ChatPanel { } } - fn remove_message(&mut self, id: u64, cx: &mut ViewContext) { + fn remove_message(&mut self, id: u64, cx: &mut Context) { if let Some((chat, _)) = self.active_chat.as_ref() { chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach()) } } - fn load_more_messages(&mut self, cx: &mut ViewContext) { + fn load_more_messages(&mut self, cx: &mut Context) { if let Some((chat, _)) = self.active_chat.as_ref() { chat.update(cx, |channel, cx| { if let Some(task) = channel.load_more_messages(cx) { @@ -831,7 +853,7 @@ impl ChatPanel { &mut self, selected_channel_id: ChannelId, scroll_to_message_id: Option, - cx: &mut ViewContext, + cx: &mut Context, ) -> Task> { let open_chat = self .active_chat @@ -857,20 +879,18 @@ impl ChatPanel { if let Some(message_id) = scroll_to_message_id { if let Some(item_ix) = - ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone()) + ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone()) .await { this.update(&mut cx, |this, cx| { if let Some(highlight_message_id) = highlight_message_id { - let task = cx.spawn({ - |this, mut cx| async move { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(&mut cx, |this, cx| { - this.highlighted_message.take(); - cx.notify(); - }) - .ok(); - } + let task = cx.spawn(|this, mut cx| async move { + cx.background_executor().timer(Duration::from_secs(2)).await; + this.update(&mut cx, |this, cx| { + this.highlighted_message.take(); + cx.notify(); + }) + .ok(); }); this.highlighted_message = Some((highlight_message_id, task)); @@ -891,12 +911,12 @@ impl ChatPanel { }) } - fn close_reply_preview(&mut self, cx: &mut ViewContext) { + fn close_reply_preview(&mut self, cx: &mut Context) { self.message_editor .update(cx, |editor, _| editor.clear_reply_to_message_id()); } - fn cancel_edit_message(&mut self, cx: &mut ViewContext) { + fn cancel_edit_message(&mut self, cx: &mut Context) { self.message_editor.update(cx, |editor, cx| { // only clear the editor input if we were editing a message if editor.edit_message_id().is_none() { @@ -919,7 +939,7 @@ impl ChatPanel { } impl Render for ChatPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let channel_id = self .active_chat .as_ref() @@ -971,11 +991,12 @@ impl Render for ChatPanel { .full_width() .key_binding(KeyBinding::for_action( &collab_panel::ToggleFocus, - cx, + window, )) - .on_click(|_, cx| { - cx.dispatch_action( + .on_click(|_, window, cx| { + window.dispatch_action( collab_panel::ToggleFocus.boxed_clone(), + cx, ) }), ), @@ -999,8 +1020,8 @@ impl Render for ChatPanel { .child( IconButton::new("cancel-edit-message", IconName::Close) .shape(ui::IconButtonShape::Square) - .tooltip(|cx| Tooltip::text("Cancel edit message", cx)) - .on_click(cx.listener(move |this, _, cx| { + .tooltip(Tooltip::text("Cancel edit message")) + .on_click(cx.listener(move |this, _, _, cx| { this.cancel_edit_message(cx); })), ), @@ -1045,7 +1066,7 @@ impl Render for ChatPanel { ) .when_some(channel_id, |this, channel_id| { this.cursor_pointer().on_click(cx.listener( - move |chat_panel, _, cx| { + move |chat_panel, _, _, cx| { chat_panel .select_channel( channel_id, @@ -1061,8 +1082,8 @@ impl Render for ChatPanel { .child( IconButton::new("close-reply-preview", IconName::Close) .shape(ui::IconButtonShape::Square) - .tooltip(|cx| Tooltip::text("Close reply", cx)) - .on_click(cx.listener(move |this, _, cx| { + .tooltip(Tooltip::text("Close reply")) + .on_click(cx.listener(move |this, _, _, cx| { this.close_reply_preview(cx); })), ), @@ -1073,7 +1094,7 @@ impl Render for ChatPanel { Some( h_flex() .p_2() - .on_action(cx.listener(|this, _: &actions::Cancel, cx| { + .on_action(cx.listener(|this, _: &actions::Cancel, _, cx| { this.cancel_edit_message(cx); this.close_reply_preview(cx); })) @@ -1085,8 +1106,8 @@ impl Render for ChatPanel { } } -impl FocusableView for ChatPanel { - fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { +impl Focusable for ChatPanel { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { if self.active_chat.is_some() { self.message_editor.read(cx).focus_handle(cx) } else { @@ -1096,7 +1117,7 @@ impl FocusableView for ChatPanel { } impl Panel for ChatPanel { - fn position(&self, cx: &WindowContext) -> DockPosition { + fn position(&self, _: &Window, cx: &App) -> DockPosition { ChatPanelSettings::get_global(cx).dock } @@ -1104,7 +1125,7 @@ impl Panel for ChatPanel { matches!(position, DockPosition::Left | DockPosition::Right) } - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { settings::update_settings_file::( self.fs.clone(), cx, @@ -1112,18 +1133,18 @@ impl Panel for ChatPanel { ); } - fn size(&self, cx: &WindowContext) -> Pixels { + fn size(&self, _: &Window, cx: &App) -> Pixels { self.width .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width) } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, _: &mut Window, cx: &mut Context) { self.width = size; self.serialize(cx); cx.notify(); } - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context) { self.active = active; if active { self.acknowledge_last_message(cx); @@ -1134,7 +1155,7 @@ impl Panel for ChatPanel { "ChatPanel" } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, _window: &Window, cx: &App) -> Option { let show_icon = match ChatPanelSettings::get_global(cx).button { ChatPanelButton::Never => false, ChatPanelButton::Always => true, @@ -1151,7 +1172,7 @@ impl Panel for ChatPanel { show_icon.then(|| ui::IconName::MessageBubbles) } - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> { Some("Chat Panel") } @@ -1159,7 +1180,7 @@ impl Panel for ChatPanel { Box::new(ToggleFocus) } - fn starts_open(&self, cx: &WindowContext) -> bool { + fn starts_open(&self, _: &Window, cx: &App) -> bool { ActiveCall::global(cx) .read(cx) .room() @@ -1183,7 +1204,7 @@ mod tests { use util::test::marked_text_ranges; #[gpui::test] - fn test_render_markdown_with_mentions(cx: &mut AppContext) { + fn test_render_markdown_with_mentions(cx: &mut App) { let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false); let message = channel::ChannelMessage { @@ -1240,7 +1261,7 @@ mod tests { } #[gpui::test] - fn test_render_markdown_with_auto_detect_links(cx: &mut AppContext) { + fn test_render_markdown_with_auto_detect_links(cx: &mut App) { let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let message = channel::ChannelMessage { id: ChannelMessageId::Saved(0), @@ -1289,7 +1310,7 @@ mod tests { } #[gpui::test] - fn test_render_markdown_with_auto_detect_links_and_additional_formatting(cx: &mut AppContext) { + fn test_render_markdown_with_auto_detect_links_and_additional_formatting(cx: &mut App) { let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let message = channel::ChannelMessage { id: ChannelMessageId::Saved(0), diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 7a05e6bce404da..4f0959b575e161 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -1,12 +1,12 @@ -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use channel::{ChannelChat, ChannelStore, MessageParams}; use client::{UserId, UserStore}; use collections::HashSet; use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model, - Render, Task, TextStyle, View, ViewContext, WeakView, + AsyncApp, AsyncWindowContext, Context, Entity, Focusable, FontStyle, FontWeight, + HighlightStyle, IntoElement, Render, Task, TextStyle, WeakEntity, Window, }; use language::{ language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, @@ -42,24 +42,25 @@ static MENTIONS_SEARCH: LazyLock = LazyLock::new(|| { }); pub struct MessageEditor { - pub editor: View, - user_store: Model, - channel_chat: Option>, + pub editor: Entity, + user_store: Entity, + channel_chat: Option>, mentions: Vec, mentions_task: Option>, reply_to_message_id: Option, edit_message_id: Option, } -struct MessageEditorCompletionProvider(WeakView); +struct MessageEditorCompletionProvider(WeakEntity); impl CompletionProvider for MessageEditorCompletionProvider { fn completions( &self, - buffer: &Model, + buffer: &Entity, buffer_position: language::Anchor, _: editor::CompletionContext, - cx: &mut ViewContext, + _window: &mut Window, + cx: &mut Context, ) -> Task>> { let Some(handle) = self.0.upgrade() else { return Task::ready(Ok(Vec::new())); @@ -71,21 +72,21 @@ impl CompletionProvider for MessageEditorCompletionProvider { fn resolve_completions( &self, - _buffer: Model, + _buffer: Entity, _completion_indices: Vec, _completions: Rc>>, - _cx: &mut ViewContext, + _cx: &mut Context, ) -> Task> { Task::ready(Ok(false)) } fn is_completion_trigger( &self, - _buffer: &Model, + _buffer: &Entity, _position: language::Anchor, text: &str, _trigger_in_words: bool, - _cx: &mut ViewContext, + _cx: &mut Context, ) -> bool { text == "@" } @@ -94,12 +95,13 @@ impl CompletionProvider for MessageEditorCompletionProvider { impl MessageEditor { pub fn new( language_registry: Arc, - user_store: Model, - channel_chat: Option>, - editor: View, - cx: &mut ViewContext, + user_store: Entity, + channel_chat: Option>, + editor: Entity, + window: &mut Window, + cx: &mut Context, ) -> Self { - let this = cx.view().downgrade(); + let this = cx.entity().downgrade(); editor.update(cx, |editor, cx| { editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_use_autoclose(false); @@ -121,9 +123,10 @@ impl MessageEditor { .as_singleton() .expect("message editor must be singleton"); - cx.subscribe(&buffer, Self::on_buffer_event).detach(); - cx.observe_global::(|view, cx| { - view.editor.update(cx, |editor, cx| { + cx.subscribe_in(&buffer, window, Self::on_buffer_event) + .detach(); + cx.observe_global::(|this, cx| { + this.editor.update(cx, |editor, cx| { editor.set_auto_replace_emoji_shortcode( MessageEditorSettings::get_global(cx) .auto_replace_emoji_shortcode @@ -134,7 +137,7 @@ impl MessageEditor { .detach(); let markdown = language_registry.language_for_name("Markdown"); - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { let markdown = markdown.await.context("failed to load Markdown language")?; buffer.update(&mut cx, |buffer, cx| { buffer.set_language(Some(markdown), cx) @@ -177,7 +180,7 @@ impl MessageEditor { self.edit_message_id = None; } - pub fn set_channel_chat(&mut self, chat: Model, cx: &mut ViewContext) { + pub fn set_channel_chat(&mut self, chat: Entity, cx: &mut Context) { let channel_id = chat.read(cx).channel_id; self.channel_chat = Some(chat); let channel_name = ChannelStore::global(cx) @@ -193,7 +196,7 @@ impl MessageEditor { }); } - pub fn take_message(&mut self, cx: &mut ViewContext) -> MessageParams { + pub fn take_message(&mut self, window: &mut Window, cx: &mut Context) -> MessageParams { self.editor.update(cx, |editor, cx| { let highlights = editor.text_highlights::(cx); let text = editor.text(cx); @@ -208,7 +211,7 @@ impl MessageEditor { Vec::new() }; - editor.clear(cx); + editor.clear(window, cx); self.mentions.clear(); let reply_to_message_id = std::mem::take(&mut self.reply_to_message_id); @@ -222,13 +225,14 @@ impl MessageEditor { fn on_buffer_event( &mut self, - buffer: Model, + buffer: &Entity, event: &language::BufferEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event { let buffer = buffer.read(cx).snapshot(); - self.mentions_task = Some(cx.spawn(|this, cx| async move { + self.mentions_task = Some(cx.spawn_in(window, |this, cx| async move { cx.background_executor() .timer(MENTIONS_DEBOUNCE_INTERVAL) .await; @@ -239,9 +243,9 @@ impl MessageEditor { fn completions( &mut self, - buffer: &Model, + buffer: &Entity, end_anchor: Anchor, - cx: &mut ViewContext, + cx: &mut Context, ) -> Task>> { if let Some((start_anchor, query, candidates)) = self.collect_mention_candidates(buffer, end_anchor, cx) @@ -281,7 +285,7 @@ impl MessageEditor { } async fn resolve_completions_for_candidates( - cx: &AsyncWindowContext, + cx: &AsyncApp, query: &str, candidates: &[StringMatchCandidate], range: Range, @@ -336,9 +340,9 @@ impl MessageEditor { fn collect_mention_candidates( &mut self, - buffer: &Model, + buffer: &Entity, end_anchor: Anchor, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option<(Anchor, String, Vec)> { let end_offset = end_anchor.to_offset(buffer.read(cx)); @@ -385,9 +389,9 @@ impl MessageEditor { fn collect_emoji_candidates( &mut self, - buffer: &Model, + buffer: &Entity, end_anchor: Anchor, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option<(Anchor, String, &'static [StringMatchCandidate])> { static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock> = LazyLock::new(|| { @@ -445,7 +449,7 @@ impl MessageEditor { } async fn find_mentions( - this: WeakView, + this: WeakEntity, buffer: BufferSnapshot, mut cx: AsyncWindowContext, ) { @@ -499,13 +503,13 @@ impl MessageEditor { .ok(); } - pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + pub(crate) fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.editor.read(cx).focus_handle(cx) } } impl Render for MessageEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if self.editor.read(cx).read_only(cx) { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index b9fa39ed969dc3..c9eaec4e3b702d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -11,12 +11,11 @@ use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorElement, EditorStyle}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement, - AppContext, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div, - EventEmitter, FocusHandle, FocusableView, FontStyle, InteractiveElement, IntoElement, - ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, - Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, - WeakView, + actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement, App, + AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent, Div, Entity, + EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement, ListOffset, + ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, SharedString, + Styled, Subscription, Task, TextStyle, WeakEntity, Window, }; use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev}; use project::{Fs, Project}; @@ -62,21 +61,22 @@ struct ChannelMoveClipboard { const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel"; -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, _| { - workspace.register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _| { + workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { + workspace.toggle_panel_focus::(window, cx); }); - workspace.register_action(|_, _: &OpenChannelNotes, cx| { + workspace.register_action(|_, _: &OpenChannelNotes, window, cx| { let channel_id = ActiveCall::global(cx) .read(cx) .room() .and_then(|room| room.read(cx).channel_id()); if let Some(channel_id) = channel_id { - let workspace = cx.view().clone(); - cx.window_context().defer(move |cx| { - ChannelView::open(channel_id, None, workspace, cx).detach_and_log_err(cx) + let workspace = cx.entity().clone(); + window.defer(cx, move |window, cx| { + ChannelView::open(channel_id, None, workspace, window, cx) + .detach_and_log_err(cx) }); } }); @@ -111,22 +111,22 @@ pub struct CollabPanel { focus_handle: FocusHandle, channel_clipboard: Option, pending_serialization: Task>, - context_menu: Option<(View, Point, Subscription)>, + context_menu: Option<(Entity, Point, Subscription)>, list_state: ListState, - filter_editor: View, - channel_name_editor: View, + filter_editor: Entity, + channel_name_editor: Entity, channel_editing_state: Option, entries: Vec, selection: Option, - channel_store: Model, - user_store: Model, + channel_store: Entity, + user_store: Entity, client: Arc, - project: Model, + project: Entity, match_candidates: Vec, subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, - workspace: WeakView, + workspace: WeakEntity, } #[derive(Serialize, Deserialize)] @@ -190,10 +190,14 @@ enum ListEntry { } impl CollabPanel { - pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { - cx.new_view(|cx| { - let filter_editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); + pub fn new( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + cx.new(|cx| { + let filter_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter...", cx); editor }); @@ -215,31 +219,39 @@ impl CollabPanel { }) .detach(); - let channel_name_editor = cx.new_view(Editor::single_line); - - cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| { - if let editor::EditorEvent::Blurred = event { - if let Some(state) = &this.channel_editing_state { - if state.pending_name().is_some() { - return; + let channel_name_editor = cx.new(|cx| Editor::single_line(window, cx)); + + cx.subscribe_in( + &channel_name_editor, + window, + |this: &mut Self, _, event, window, cx| { + if let editor::EditorEvent::Blurred = event { + if let Some(state) = &this.channel_editing_state { + if state.pending_name().is_some() { + return; + } } + this.take_editing_state(window, cx); + this.update_entries(false, cx); + cx.notify(); } - this.take_editing_state(cx); - this.update_entries(false, cx); - cx.notify(); - } - }) + }, + ) .detach(); - let view = cx.view().downgrade(); - let list_state = - ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { - if let Some(view) = view.upgrade() { - view.update(cx, |view, cx| view.render_list_entry(ix, cx)) + let model = cx.entity().downgrade(); + let list_state = ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, window, cx| { + if let Some(model) = model.upgrade() { + model.update(cx, |this, cx| this.render_list_entry(ix, window, cx)) } else { div().into_any() } - }); + }, + ); let mut this = Self { width: None, @@ -278,12 +290,13 @@ impl CollabPanel { })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); - this.subscriptions.push(cx.subscribe( + this.subscriptions.push(cx.subscribe_in( &this.channel_store, - |this, _channel_store, e, cx| match e { + window, + |this, _channel_store, e, window, cx| match e { ChannelEvent::ChannelCreated(channel_id) | ChannelEvent::ChannelRenamed(channel_id) => { - if this.take_editing_state(cx) { + if this.take_editing_state(window, cx) { this.update_entries(false, cx); this.selection = this.entries.iter().position(|entry| { if let ListEntry::Channel { channel, .. } = entry { @@ -302,9 +315,9 @@ impl CollabPanel { } pub async fn load( - workspace: WeakView, + workspace: WeakEntity, mut cx: AsyncWindowContext, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let serialized_panel = cx .background_executor() .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) }) @@ -317,8 +330,8 @@ impl CollabPanel { .log_err() .flatten(); - workspace.update(&mut cx, |workspace, cx| { - let panel = CollabPanel::new(workspace, cx); + workspace.update_in(&mut cx, |workspace, window, cx| { + let panel = CollabPanel::new(workspace, window, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); @@ -335,7 +348,7 @@ impl CollabPanel { }) } - fn serialize(&mut self, cx: &mut ViewContext) { + fn serialize(&mut self, cx: &mut Context) { let width = self.width; let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background_executor().spawn( @@ -361,7 +374,7 @@ impl CollabPanel { self.list_state.scroll_to_reveal_item(ix) } - fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { + fn update_entries(&mut self, select_same_item: bool, cx: &mut Context) { let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); let query = self.filter_editor.read(cx).text(cx); @@ -799,7 +812,7 @@ impl CollabPanel { is_pending: bool, role: proto::ChannelRole, is_selected: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> ListItem { let user_id = user.id; let is_current_user = @@ -819,8 +832,8 @@ impl CollabPanel { } else if is_current_user { IconButton::new("leave-call", IconName::Exit) .style(ButtonStyle::Subtle) - .on_click(move |_, cx| Self::leave_call(cx)) - .tooltip(|cx| Tooltip::text("Leave Call", cx)) + .on_click(move |_, window, cx| Self::leave_call(window, cx)) + .tooltip(Tooltip::text("Leave Call")) .into_any_element() } else if role == proto::ChannelRole::Guest { Label::new("Guest").color(Color::Muted).into_any_element() @@ -835,20 +848,29 @@ impl CollabPanel { if role == proto::ChannelRole::Guest { return el; } - el.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) - .on_click(cx.listener(move |this, _, cx| { + el.tooltip(Tooltip::text(tooltip.clone())) + .on_click(cx.listener(move |this, _, window, cx| { this.workspace - .update(cx, |workspace, cx| workspace.follow(peer_id, cx)) + .update(cx, |workspace, cx| workspace.follow(peer_id, window, cx)) .ok(); })) }) .when(is_call_admin, |el| { - el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { - this.deploy_participant_context_menu(event.position, user_id, role, cx) - })) + el.on_secondary_mouse_down(cx.listener( + move |this, event: &MouseDownEvent, window, cx| { + this.deploy_participant_context_menu( + event.position, + user_id, + role, + window, + cx, + ) + }, + )) }) } + #[allow(clippy::too_many_arguments)] fn render_participant_project( &self, project_id: u64, @@ -856,7 +878,8 @@ impl CollabPanel { host_user_id: u64, is_last: bool, is_selected: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> impl IntoElement { let project_name: SharedString = if worktree_root_names.is_empty() { "untitled".to_string() @@ -867,23 +890,28 @@ impl CollabPanel { ListItem::new(project_id as usize) .toggle_state(is_selected) - .on_click(cx.listener(move |this, _, cx| { + .on_click(cx.listener(move |this, _, window, cx| { this.workspace .update(cx, |workspace, cx| { let app_state = workspace.app_state().clone(); workspace::join_in_room_project(project_id, host_user_id, app_state, cx) - .detach_and_prompt_err("Failed to join project", cx, |_, _| None); + .detach_and_prompt_err( + "Failed to join project", + window, + cx, + |_, _, _| None, + ); }) .ok(); })) .start_slot( h_flex() .gap_1() - .child(render_tree_branch(is_last, false, cx)) + .child(render_tree_branch(is_last, false, window, cx)) .child(IconButton::new(0, IconName::Folder)), ) .child(Label::new(project_name.clone())) - .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) + .tooltip(Tooltip::text(format!("Open {}", project_name))) } fn render_participant_screen( @@ -891,7 +919,8 @@ impl CollabPanel { peer_id: Option, is_last: bool, is_selected: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> impl IntoElement { let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); @@ -900,26 +929,26 @@ impl CollabPanel { .start_slot( h_flex() .gap_1() - .child(render_tree_branch(is_last, false, cx)) + .child(render_tree_branch(is_last, false, window, cx)) .child(IconButton::new(0, IconName::Screen)), ) .child(Label::new("Screen")) .when_some(peer_id, |this, _| { - this.on_click(cx.listener(move |this, _, cx| { + this.on_click(cx.listener(move |this, _, window, cx| { this.workspace .update(cx, |workspace, cx| { - workspace.open_shared_screen(peer_id.unwrap(), cx) + workspace.open_shared_screen(peer_id.unwrap(), window, cx) }) .ok(); })) - .tooltip(move |cx| Tooltip::text("Open shared screen", cx)) + .tooltip(Tooltip::text("Open shared screen")) }) } - fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { + fn take_editing_state(&mut self, window: &mut Window, cx: &mut Context) -> bool { if self.channel_editing_state.take().is_some() { self.channel_name_editor.update(cx, |editor, cx| { - editor.set_text("", cx); + editor.set_text("", window, cx); }); true } else { @@ -931,20 +960,21 @@ impl CollabPanel { &self, channel_id: ChannelId, is_selected: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> impl IntoElement { let channel_store = self.channel_store.read(cx); let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id); ListItem::new("channel-notes") .toggle_state(is_selected) - .on_click(cx.listener(move |this, _, cx| { - this.open_channel_notes(channel_id, cx); + .on_click(cx.listener(move |this, _, window, cx| { + this.open_channel_notes(channel_id, window, cx); })) .start_slot( h_flex() .relative() .gap_1() - .child(render_tree_branch(false, true, cx)) + .child(render_tree_branch(false, true, window, cx)) .child(IconButton::new(0, IconName::File)) .children(has_channel_buffer_changed.then(|| { div() @@ -956,27 +986,28 @@ impl CollabPanel { })), ) .child(Label::new("notes")) - .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) + .tooltip(Tooltip::text("Open Channel Notes")) } fn render_channel_chat( &self, channel_id: ChannelId, is_selected: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> impl IntoElement { let channel_store = self.channel_store.read(cx); let has_messages_notification = channel_store.has_new_messages(channel_id); ListItem::new("channel-chat") .toggle_state(is_selected) - .on_click(cx.listener(move |this, _, cx| { - this.join_channel_chat(channel_id, cx); + .on_click(cx.listener(move |this, _, window, cx| { + this.join_channel_chat(channel_id, window, cx); })) .start_slot( h_flex() .relative() .gap_1() - .child(render_tree_branch(false, false, cx)) + .child(render_tree_branch(false, false, window, cx)) .child(IconButton::new(0, IconName::MessageBubbles)) .children(has_messages_notification.then(|| { div() @@ -988,7 +1019,7 @@ impl CollabPanel { })), ) .child(Label::new("chat")) - .tooltip(move |cx| Tooltip::text("Open Chat", cx)) + .tooltip(Tooltip::text("Open Chat")) } fn has_subchannels(&self, ix: usize) -> bool { @@ -1006,9 +1037,10 @@ impl CollabPanel { position: Point, user_id: u64, role: proto::ChannelRole, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - let this = cx.view().clone(); + let this = cx.entity().clone(); if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker || role == proto::ChannelRole::Member) @@ -1016,12 +1048,12 @@ impl CollabPanel { return; } - let context_menu = ContextMenu::build(cx, |mut context_menu, cx| { + let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, _| { if role == proto::ChannelRole::Guest { context_menu = context_menu.entry( "Grant Mic Access", None, - cx.handler_for(&this, move |_, cx| { + window.handler_for(&this, move |_, window, cx| { ActiveCall::global(cx) .update(cx, |call, cx| { let Some(room) = call.room() else { @@ -1035,7 +1067,12 @@ impl CollabPanel { ) }) }) - .detach_and_prompt_err("Failed to grant mic access", cx, |_, _| None) + .detach_and_prompt_err( + "Failed to grant mic access", + window, + cx, + |_, _, _| None, + ) }), ); } @@ -1043,7 +1080,7 @@ impl CollabPanel { context_menu = context_menu.entry( "Grant Write Access", None, - cx.handler_for(&this, move |_, cx| { + window.handler_for(&this, move |_, window, cx| { ActiveCall::global(cx) .update(cx, |call, cx| { let Some(room) = call.room() else { @@ -1057,7 +1094,7 @@ impl CollabPanel { ) }) }) - .detach_and_prompt_err("Failed to grant write access", cx, |e, _| { + .detach_and_prompt_err("Failed to grant write access", window, cx, |e, _, _| { match e.error_code() { ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()), _ => None, @@ -1075,7 +1112,7 @@ impl CollabPanel { context_menu = context_menu.entry( label, None, - cx.handler_for(&this, move |_, cx| { + window.handler_for(&this, move |_, window, cx| { ActiveCall::global(cx) .update(cx, |call, cx| { let Some(room) = call.room() else { @@ -1089,7 +1126,12 @@ impl CollabPanel { ) }) }) - .detach_and_prompt_err("Failed to revoke access", cx, |_, _| None) + .detach_and_prompt_err( + "Failed to revoke access", + window, + cx, + |_, _, _| None, + ) }), ); } @@ -1097,17 +1139,20 @@ impl CollabPanel { context_menu }); - cx.focus_view(&context_menu); - let subscription = - cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + window.focus(&context_menu.focus_handle(cx)); + let subscription = cx.subscribe_in( + &context_menu, + window, + |this, _, _: &DismissEvent, window, cx| { if this.context_menu.as_ref().is_some_and(|context_menu| { - context_menu.0.focus_handle(cx).contains_focused(cx) + context_menu.0.focus_handle(cx).contains_focused(window, cx) }) { - cx.focus_self(); + cx.focus_self(window); } this.context_menu.take(); cx.notify(); - }); + }, + ); self.context_menu = Some((context_menu, position, subscription)); } @@ -1116,7 +1161,8 @@ impl CollabPanel { position: Point, channel_id: ChannelId, ix: usize, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { self.channel_store @@ -1124,9 +1170,9 @@ impl CollabPanel { .channel_for_id(clipboard.channel_id) .map(|channel| channel.name.clone()) }); - let this = cx.view().clone(); + let this = cx.entity().clone(); - let context_menu = ContextMenu::build(cx, |mut context_menu, cx| { + let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, cx| { if self.has_subchannels(ix) { let expand_action_name = if self.is_channel_collapsed(channel_id) { "Expand Subchannels" @@ -1136,8 +1182,8 @@ impl CollabPanel { context_menu = context_menu.entry( expand_action_name, None, - cx.handler_for(&this, move |this, cx| { - this.toggle_channel_collapsed(channel_id, cx) + window.handler_for(&this, move |this, window, cx| { + this.toggle_channel_collapsed(channel_id, window, cx) }), ); } @@ -1146,21 +1192,21 @@ impl CollabPanel { .entry( "Open Notes", None, - cx.handler_for(&this, move |this, cx| { - this.open_channel_notes(channel_id, cx) + window.handler_for(&this, move |this, window, cx| { + this.open_channel_notes(channel_id, window, cx) }), ) .entry( "Open Chat", None, - cx.handler_for(&this, move |this, cx| { - this.join_channel_chat(channel_id, cx) + window.handler_for(&this, move |this, window, cx| { + this.join_channel_chat(channel_id, window, cx) }), ) .entry( "Copy Channel Link", None, - cx.handler_for(&this, move |this, cx| { + window.handler_for(&this, move |this, _, cx| { this.copy_channel_link(channel_id, cx) }), ); @@ -1173,20 +1219,24 @@ impl CollabPanel { .entry( "New Subchannel", None, - cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)), + window.handler_for(&this, move |this, window, cx| { + this.new_subchannel(channel_id, window, cx) + }), ) .entry( "Rename", Some(Box::new(SecondaryConfirm)), - cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)), + window.handler_for(&this, move |this, window, cx| { + this.rename_channel(channel_id, window, cx) + }), ); if let Some(channel_name) = clipboard_channel_name { context_menu = context_menu.separator().entry( format!("Move '#{}' here", channel_name), None, - cx.handler_for(&this, move |this, cx| { - this.move_channel_on_clipboard(channel_id, cx) + window.handler_for(&this, move |this, window, cx| { + this.move_channel_on_clipboard(channel_id, window, cx) }), ); } @@ -1195,24 +1245,27 @@ impl CollabPanel { context_menu = context_menu.separator().entry( "Manage Members", None, - cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)), + window.handler_for(&this, move |this, window, cx| { + this.manage_members(channel_id, window, cx) + }), ) } else { context_menu = context_menu.entry( "Move this channel", None, - cx.handler_for(&this, move |this, cx| { - this.start_move_channel(channel_id, cx) + window.handler_for(&this, move |this, window, cx| { + this.start_move_channel(channel_id, window, cx) }), ); if self.channel_store.read(cx).is_public_channel(channel_id) { context_menu = context_menu.separator().entry( "Make Channel Private", None, - cx.handler_for(&this, move |this, cx| { + window.handler_for(&this, move |this, window, cx| { this.set_channel_visibility( channel_id, ChannelVisibility::Members, + window, cx, ) }), @@ -1221,10 +1274,11 @@ impl CollabPanel { context_menu = context_menu.separator().entry( "Make Channel Public", None, - cx.handler_for(&this, move |this, cx| { + window.handler_for(&this, move |this, window, cx| { this.set_channel_visibility( channel_id, ChannelVisibility::Public, + window, cx, ) }), @@ -1235,7 +1289,9 @@ impl CollabPanel { context_menu = context_menu.entry( "Delete", None, - cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)), + window.handler_for(&this, move |this, window, cx| { + this.remove_channel(channel_id, window, cx) + }), ); } @@ -1246,24 +1302,29 @@ impl CollabPanel { context_menu = context_menu.entry( "Leave Channel", None, - cx.handler_for(&this, move |this, cx| this.leave_channel(channel_id, cx)), + window.handler_for(&this, move |this, window, cx| { + this.leave_channel(channel_id, window, cx) + }), ); } context_menu }); - cx.focus_view(&context_menu); - let subscription = - cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + window.focus(&context_menu.focus_handle(cx)); + let subscription = cx.subscribe_in( + &context_menu, + window, + |this, _, _: &DismissEvent, window, cx| { if this.context_menu.as_ref().is_some_and(|context_menu| { - context_menu.0.focus_handle(cx).contains_focused(cx) + context_menu.0.focus_handle(cx).contains_focused(window, cx) }) { - cx.focus_self(); + cx.focus_self(window); } this.context_menu.take(); cx.notify(); - }); + }, + ); self.context_menu = Some((context_menu, position, subscription)); cx.notify(); @@ -1273,12 +1334,13 @@ impl CollabPanel { &mut self, position: Point, contact: Arc, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - let this = cx.view().clone(); + let this = cx.entity().clone(); let in_room = ActiveCall::global(cx).read(cx).room().is_some(); - let context_menu = ContextMenu::build(cx, |mut context_menu, _| { + let context_menu = ContextMenu::build(window, cx, |mut context_menu, _, _| { let user_id = contact.user.id; if contact.online && !contact.busy { @@ -1289,9 +1351,9 @@ impl CollabPanel { }; context_menu = context_menu.entry(label, None, { let this = this.clone(); - move |cx| { + move |window, cx| { this.update(cx, |this, cx| { - this.call(user_id, cx); + this.call(user_id, window, cx); }); } }); @@ -1299,34 +1361,42 @@ impl CollabPanel { context_menu.entry("Remove Contact", None, { let this = this.clone(); - move |cx| { + move |window, cx| { this.update(cx, |this, cx| { - this.remove_contact(contact.user.id, &contact.user.github_login, cx); + this.remove_contact( + contact.user.id, + &contact.user.github_login, + window, + cx, + ); }); } }) }); - cx.focus_view(&context_menu); - let subscription = - cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + window.focus(&context_menu.focus_handle(cx)); + let subscription = cx.subscribe_in( + &context_menu, + window, + |this, _, _: &DismissEvent, window, cx| { if this.context_menu.as_ref().is_some_and(|context_menu| { - context_menu.0.focus_handle(cx).contains_focused(cx) + context_menu.0.focus_handle(cx).contains_focused(window, cx) }) { - cx.focus_self(); + cx.focus_self(window); } this.context_menu.take(); cx.notify(); - }); + }, + ); self.context_menu = Some((context_menu, position, subscription)); cx.notify(); } - fn reset_filter_editor_text(&mut self, cx: &mut ViewContext) -> bool { + fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context) -> bool { self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { - editor.set_text("", cx); + editor.set_text("", window, cx); true } else { false @@ -1334,11 +1404,11 @@ impl CollabPanel { }) } - fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if self.take_editing_state(cx) { - cx.focus_view(&self.filter_editor); - } else if !self.reset_filter_editor_text(cx) { - self.focus_handle.focus(cx); + fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { + if self.take_editing_state(window, cx) { + window.focus(&self.filter_editor.focus_handle(cx)); + } else if !self.reset_filter_editor_text(window, cx) { + self.focus_handle.focus(window); } if self.context_menu.is_some() { @@ -1349,7 +1419,7 @@ impl CollabPanel { self.update_entries(false, cx); } - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context) { let ix = self.selection.map_or(0, |ix| ix + 1); if ix < self.entries.len() { self.selection = Some(ix); @@ -1361,7 +1431,7 @@ impl CollabPanel { cx.notify(); } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + fn select_prev(&mut self, _: &SelectPrev, _: &mut Window, cx: &mut Context) { let ix = self.selection.take().unwrap_or(0); if ix > 0 { self.selection = Some(ix - 1); @@ -1373,8 +1443,8 @@ impl CollabPanel { cx.notify(); } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if self.confirm_channel_edit(cx) { + fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { + if self.confirm_channel_edit(window, cx) { return; } @@ -1382,9 +1452,9 @@ impl CollabPanel { if let Some(entry) = self.entries.get(selection) { match entry { ListEntry::Header(section) => match section { - Section::ActiveCall => Self::leave_call(cx), - Section::Channels => self.new_root_channel(cx), - Section::Contacts => self.toggle_contact_finder(cx), + Section::ActiveCall => Self::leave_call(window, cx), + Section::Channels => self.new_root_channel(window, cx), + Section::Contacts => self.toggle_contact_finder(window, cx), Section::ContactRequests | Section::Online | Section::Offline @@ -1394,7 +1464,7 @@ impl CollabPanel { }, ListEntry::Contact { contact, calling } => { if contact.online && !contact.busy && !calling { - self.call(contact.user.id, cx); + self.call(contact.user.id, window, cx); } } ListEntry::ParticipantProject { @@ -1412,8 +1482,9 @@ impl CollabPanel { ) .detach_and_prompt_err( "Failed to join project", + window, cx, - |_, _| None, + |_, _, _| None, ); } } @@ -1423,7 +1494,7 @@ impl CollabPanel { }; if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { - workspace.open_shared_screen(*peer_id, cx) + workspace.open_shared_screen(*peer_id, window, cx) }); } } @@ -1439,32 +1510,32 @@ impl CollabPanel { }) .unwrap_or(false); if is_active { - self.open_channel_notes(channel.id, cx) + self.open_channel_notes(channel.id, window, cx) } else { - self.join_channel(channel.id, cx) + self.join_channel(channel.id, window, cx) } } - ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), + ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx), ListEntry::CallParticipant { user, peer_id, .. } => { if Some(user) == self.user_store.read(cx).current_user().as_ref() { - Self::leave_call(cx); + Self::leave_call(window, cx); } else if let Some(peer_id) = peer_id { self.workspace - .update(cx, |workspace, cx| workspace.follow(*peer_id, cx)) + .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx)) .ok(); } } ListEntry::IncomingRequest(user) => { - self.respond_to_contact_request(user.id, true, cx) + self.respond_to_contact_request(user.id, true, window, cx) } ListEntry::ChannelInvite(channel) => { self.respond_to_channel_invite(channel.id, true, cx) } ListEntry::ChannelNotes { channel_id } => { - self.open_channel_notes(*channel_id, cx) + self.open_channel_notes(*channel_id, window, cx) } ListEntry::ChannelChat { channel_id } => { - self.join_channel_chat(*channel_id, cx) + self.join_channel_chat(*channel_id, window, cx) } ListEntry::OutgoingRequest(_) => {} ListEntry::ChannelEditor { .. } => {} @@ -1473,15 +1544,15 @@ impl CollabPanel { } } - fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { + fn insert_space(&mut self, _: &InsertSpace, window: &mut Window, cx: &mut Context) { if self.channel_editing_state.is_some() { self.channel_name_editor.update(cx, |editor, cx| { - editor.insert(" ", cx); + editor.insert(" ", window, cx); }); } } - fn confirm_channel_edit(&mut self, cx: &mut ViewContext) -> bool { + fn confirm_channel_edit(&mut self, window: &mut Window, cx: &mut Context) -> bool { if let Some(editing_state) = &mut self.channel_editing_state { match editing_state { ChannelEditingState::Create { @@ -1500,23 +1571,30 @@ impl CollabPanel { channel_store.create_channel(&channel_name, *location, cx) }); if location.is_none() { - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { let channel_id = create.await?; - this.update(&mut cx, |this, cx| { + this.update_in(&mut cx, |this, window, cx| { this.show_channel_modal( channel_id, channel_modal::Mode::InviteMembers, + window, cx, ) }) }) .detach_and_prompt_err( "Failed to create channel", + window, cx, - |_, _| None, + |_, _, _| None, ); } else { - create.detach_and_prompt_err("Failed to create channel", cx, |_, _| None); + create.detach_and_prompt_err( + "Failed to create channel", + window, + cx, + |_, _, _| None, + ); } cx.notify(); } @@ -1538,14 +1616,14 @@ impl CollabPanel { cx.notify(); } } - cx.focus_self(); + cx.focus_self(window); true } else { false } } - fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { + fn toggle_section_expanded(&mut self, section: Section, cx: &mut Context) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { self.collapsed_sections.remove(ix); } else { @@ -1557,7 +1635,8 @@ impl CollabPanel { fn collapse_selected_channel( &mut self, _: &CollapseSelectedChannel, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { return; @@ -1567,10 +1646,15 @@ impl CollabPanel { return; } - self.toggle_channel_collapsed(channel_id, cx); + self.toggle_channel_collapsed(channel_id, window, cx); } - fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + fn expand_selected_channel( + &mut self, + _: &ExpandSelectedChannel, + window: &mut Window, + cx: &mut Context, + ) { let Some(id) = self.selected_channel().map(|channel| channel.id) else { return; }; @@ -1579,10 +1663,15 @@ impl CollabPanel { return; } - self.toggle_channel_collapsed(id, cx) + self.toggle_channel_collapsed(id, window, cx) } - fn toggle_channel_collapsed(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + fn toggle_channel_collapsed( + &mut self, + channel_id: ChannelId, + window: &mut Window, + cx: &mut Context, + ) { match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); @@ -1594,39 +1683,39 @@ impl CollabPanel { self.serialize(cx); self.update_entries(true, cx); cx.notify(); - cx.focus_self(); + cx.focus_self(window); } fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { self.collapsed_channels.binary_search(&channel_id).is_ok() } - fn leave_call(cx: &mut WindowContext) { + fn leave_call(window: &mut Window, cx: &mut App) { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_prompt_err("Failed to hang up", cx, |_, _| None); + .detach_and_prompt_err("Failed to hang up", window, cx, |_, _, _| None); } - fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { + fn toggle_contact_finder(&mut self, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx| { - let mut finder = ContactFinder::new(self.user_store.clone(), cx); - finder.set_query(self.filter_editor.read(cx).text(cx), cx); + workspace.toggle_modal(window, cx, |window, cx| { + let mut finder = ContactFinder::new(self.user_store.clone(), window, cx); + finder.set_query(self.filter_editor.read(cx).text(cx), window, cx); finder }); }); } } - fn new_root_channel(&mut self, cx: &mut ViewContext) { + fn new_root_channel(&mut self, window: &mut Window, cx: &mut Context) { self.channel_editing_state = Some(ChannelEditingState::Create { location: None, pending_name: None, }); self.update_entries(false, cx); self.select_channel_editor(); - cx.focus_view(&self.channel_name_editor); + window.focus(&self.channel_name_editor.focus_handle(cx)); cx.notify(); } @@ -1637,7 +1726,12 @@ impl CollabPanel { }); } - fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + fn new_subchannel( + &mut self, + channel_id: ChannelId, + window: &mut Window, + cx: &mut Context, + ) { self.collapsed_channels .retain(|channel| *channel != channel_id); self.channel_editing_state = Some(ChannelEditingState::Create { @@ -1646,27 +1740,42 @@ impl CollabPanel { }); self.update_entries(false, cx); self.select_channel_editor(); - cx.focus_view(&self.channel_name_editor); + window.focus(&self.channel_name_editor.focus_handle(cx)); cx.notify(); } - fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { - self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx); + fn manage_members( + &mut self, + channel_id: ChannelId, + window: &mut Window, + cx: &mut Context, + ) { + self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, window, cx); } - fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext) { + fn remove_selected_channel(&mut self, _: &Remove, window: &mut Window, cx: &mut Context) { if let Some(channel) = self.selected_channel() { - self.remove_channel(channel.id, cx) + self.remove_channel(channel.id, window, cx) } } - fn rename_selected_channel(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext) { + fn rename_selected_channel( + &mut self, + _: &SecondaryConfirm, + window: &mut Window, + cx: &mut Context, + ) { if let Some(channel) = self.selected_channel() { - self.rename_channel(channel.id, cx); + self.rename_channel(channel.id, window, cx); } } - fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + fn rename_channel( + &mut self, + channel_id: ChannelId, + window: &mut Window, + cx: &mut Context, + ) { let channel_store = self.channel_store.read(cx); if !channel_store.is_channel_admin(channel_id) { return; @@ -1677,10 +1786,10 @@ impl CollabPanel { pending_name: None, }); self.channel_name_editor.update(cx, |editor, cx| { - editor.set_text(channel.name.clone(), cx); - editor.select_all(&Default::default(), cx); + editor.set_text(channel.name.clone(), window, cx); + editor.select_all(&Default::default(), window, cx); }); - cx.focus_view(&self.channel_name_editor); + window.focus(&self.channel_name_editor.focus_handle(cx)); self.update_entries(false, cx); self.select_channel_editor(); } @@ -1690,13 +1799,14 @@ impl CollabPanel { &mut self, channel_id: ChannelId, visibility: ChannelVisibility, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { self.channel_store .update(cx, |channel_store, cx| { channel_store.set_channel_visibility(channel_id, visibility, cx) }) - .detach_and_prompt_err("Failed to set channel visibility", cx, |e, _| match e.error_code() { + .detach_and_prompt_err("Failed to set channel visibility", window, cx, |e, _, _| match e.error_code() { ErrorCode::BadPublicNesting => if e.error_tag("direction") == Some("parent") { Some("To make a channel public, its parent channel must be public.".to_string()) @@ -1707,50 +1817,81 @@ impl CollabPanel { }); } - fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext) { + fn start_move_channel( + &mut self, + channel_id: ChannelId, + _window: &mut Window, + _cx: &mut Context, + ) { self.channel_clipboard = Some(ChannelMoveClipboard { channel_id }); } - fn start_move_selected_channel(&mut self, _: &StartMoveChannel, cx: &mut ViewContext) { + fn start_move_selected_channel( + &mut self, + _: &StartMoveChannel, + window: &mut Window, + cx: &mut Context, + ) { if let Some(channel) = self.selected_channel() { - self.start_move_channel(channel.id, cx); + self.start_move_channel(channel.id, window, cx); } } fn move_channel_on_clipboard( &mut self, to_channel_id: ChannelId, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if let Some(clipboard) = self.channel_clipboard.take() { - self.move_channel(clipboard.channel_id, to_channel_id, cx) + self.move_channel(clipboard.channel_id, to_channel_id, window, cx) } } - fn move_channel(&self, channel_id: ChannelId, to: ChannelId, cx: &mut ViewContext) { + fn move_channel( + &self, + channel_id: ChannelId, + to: ChannelId, + window: &mut Window, + cx: &mut Context, + ) { self.channel_store .update(cx, |channel_store, cx| { channel_store.move_channel(channel_id, to, cx) }) - .detach_and_prompt_err("Failed to move channel", cx, |e, _| match e.error_code() { - ErrorCode::BadPublicNesting => { - Some("Public channels must have public parents".into()) - } - ErrorCode::CircularNesting => Some("You cannot move a channel into itself".into()), - ErrorCode::WrongMoveTarget => { - Some("You cannot move a channel into a different root channel".into()) + .detach_and_prompt_err("Failed to move channel", window, cx, |e, _, _| { + match e.error_code() { + ErrorCode::BadPublicNesting => { + Some("Public channels must have public parents".into()) + } + ErrorCode::CircularNesting => { + Some("You cannot move a channel into itself".into()) + } + ErrorCode::WrongMoveTarget => { + Some("You cannot move a channel into a different root channel".into()) + } + _ => None, } - _ => None, }) } - fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + fn open_channel_notes( + &mut self, + channel_id: ChannelId, + window: &mut Window, + cx: &mut Context, + ) { if let Some(workspace) = self.workspace.upgrade() { - ChannelView::open(channel_id, None, workspace, cx).detach(); + ChannelView::open(channel_id, None, workspace, window, cx).detach(); } } - fn show_inline_context_menu(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { + fn show_inline_context_menu( + &mut self, + _: &menu::SecondaryConfirm, + window: &mut Window, + cx: &mut Context, + ) { let Some(bounds) = self .selection .and_then(|ix| self.list_state.bounds_for_item(ix)) @@ -1763,6 +1904,7 @@ impl CollabPanel { bounds.center(), channel.id, self.selection.unwrap(), + window, cx, ); cx.stop_propagation(); @@ -1770,7 +1912,7 @@ impl CollabPanel { }; if let Some(contact) = self.selected_contact() { - self.deploy_contact_context_menu(bounds.center(), contact, cx); + self.deploy_contact_context_menu(bounds.center(), contact, window, cx); cx.stop_propagation(); } } @@ -1797,20 +1939,22 @@ impl CollabPanel { &mut self, channel_id: ChannelId, mode: channel_modal::Mode, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let workspace = self.workspace.clone(); let user_store = self.user_store.clone(); let channel_store = self.channel_store.clone(); - cx.spawn(|_, mut cx| async move { - workspace.update(&mut cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx| { + cx.spawn_in(window, |_, mut cx| async move { + workspace.update_in(&mut cx, |workspace, window, cx| { + workspace.toggle_modal(window, cx, |window, cx| { ChannelModal::new( user_store.clone(), channel_store.clone(), channel_id, mode, + window, cx, ) }); @@ -1819,7 +1963,7 @@ impl CollabPanel { .detach(); } - fn leave_channel(&self, channel_id: ChannelId, cx: &mut ViewContext) { + fn leave_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context) { let Some(user_id) = self.user_store.read(cx).current_user().map(|u| u.id) else { return; }; @@ -1827,13 +1971,14 @@ impl CollabPanel { return; }; let prompt_message = format!("Are you sure you want to leave \"#{}\"?", channel.name); - let answer = cx.prompt( + let answer = window.prompt( PromptLevel::Warning, &prompt_message, None, &["Leave", "Cancel"], + cx, ); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { if answer.await? != 0 { return Ok(()); } @@ -1844,29 +1989,36 @@ impl CollabPanel { })? .await }) - .detach_and_prompt_err("Failed to leave channel", cx, |_, _| None) + .detach_and_prompt_err("Failed to leave channel", window, cx, |_, _, _| None) } - fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + fn remove_channel( + &mut self, + channel_id: ChannelId, + window: &mut Window, + cx: &mut Context, + ) { let channel_store = self.channel_store.clone(); if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { let prompt_message = format!( "Are you sure you want to remove the channel \"{}\"?", channel.name ); - let answer = cx.prompt( + let answer = window.prompt( PromptLevel::Warning, &prompt_message, None, &["Remove", "Cancel"], + cx, ); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { if answer.await? == 0 { channel_store .update(&mut cx, |channels, _| channels.remove_channel(channel_id))? .await .notify_async_err(&mut cx); - this.update(&mut cx, |_, cx| cx.focus_self()).ok(); + this.update_in(&mut cx, |_, window, cx| cx.focus_self(window)) + .ok(); } anyhow::Ok(()) }) @@ -1874,19 +2026,26 @@ impl CollabPanel { } } - fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { + fn remove_contact( + &mut self, + user_id: u64, + github_login: &str, + window: &mut Window, + cx: &mut Context, + ) { let user_store = self.user_store.clone(); let prompt_message = format!( "Are you sure you want to remove \"{}\" from your contacts?", github_login ); - let answer = cx.prompt( + let answer = window.prompt( PromptLevel::Warning, &prompt_message, None, &["Remove", "Cancel"], + cx, ); - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { if answer.await? == 0 { user_store .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))? @@ -1895,27 +2054,33 @@ impl CollabPanel { } anyhow::Ok(()) }) - .detach_and_prompt_err("Failed to remove contact", cx, |_, _| None); + .detach_and_prompt_err("Failed to remove contact", window, cx, |_, _, _| None); } fn respond_to_contact_request( &mut self, user_id: u64, accept: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { self.user_store .update(cx, |store, cx| { store.respond_to_contact_request(user_id, accept, cx) }) - .detach_and_prompt_err("Failed to respond to contact request", cx, |_, _| None); + .detach_and_prompt_err( + "Failed to respond to contact request", + window, + cx, + |_, _, _| None, + ); } fn respond_to_channel_invite( &mut self, channel_id: ChannelId, accept: bool, - cx: &mut ViewContext, + cx: &mut Context, ) { self.channel_store .update(cx, |store, cx| { @@ -1924,19 +2089,19 @@ impl CollabPanel { .detach(); } - fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext) { + fn call(&mut self, recipient_user_id: u64, window: &mut Window, cx: &mut Context) { ActiveCall::global(cx) .update(cx, |call, cx| { call.invite(recipient_user_id, Some(self.project.clone()), cx) }) - .detach_and_prompt_err("Call failed", cx, |_, _| None); + .detach_and_prompt_err("Call failed", window, cx, |_, _, _| None); } - fn join_channel(&self, channel_id: ChannelId, cx: &mut ViewContext) { + fn join_channel(&self, channel_id: ChannelId, window: &mut Window, cx: &mut Context) { let Some(workspace) = self.workspace.upgrade() else { return; }; - let Some(handle) = cx.window_handle().downcast::() else { + let Some(handle) = window.window_handle().downcast::() else { return; }; workspace::join_channel( @@ -1945,27 +2110,32 @@ impl CollabPanel { Some(handle), cx, ) - .detach_and_prompt_err("Failed to join channel", cx, |_, _| None) + .detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None) } - fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + fn join_channel_chat( + &mut self, + channel_id: ChannelId, + window: &mut Window, + cx: &mut Context, + ) { let Some(workspace) = self.workspace.upgrade() else { return; }; - cx.window_context().defer(move |cx| { + window.defer(cx, move |window, cx| { workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.focus_panel::(cx) { + if let Some(panel) = workspace.focus_panel::(window, cx) { panel.update(cx, |panel, cx| { panel .select_channel(channel_id, None, cx) - .detach_and_notify_err(cx); + .detach_and_notify_err(window, cx); }); } }); }); } - fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut Context) { let channel_store = self.channel_store.read(cx); let Some(channel) = channel_store.channel_for_id(channel_id) else { return; @@ -1974,7 +2144,7 @@ impl CollabPanel { cx.write_to_clipboard(item) } - fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { + fn render_signed_out(&mut self, cx: &mut Context) -> Div { let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more."; v_flex() @@ -1991,9 +2161,9 @@ impl CollabPanel { .icon_position(IconPosition::Start) .style(ButtonStyle::Filled) .full_width() - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, window, cx| { let client = this.client.clone(); - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { client .authenticate_and_connect(true, &cx) .await @@ -2012,7 +2182,12 @@ impl CollabPanel { ) } - fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + fn render_list_entry( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { let entry = &self.entries[ix]; let is_selected = self.selection == Some(ix); @@ -2041,9 +2216,9 @@ impl CollabPanel { } => self .render_channel(channel, *depth, *has_children, is_selected, ix, cx) .into_any_element(), - ListEntry::ChannelEditor { depth } => { - self.render_channel_editor(*depth, cx).into_any_element() - } + ListEntry::ChannelEditor { depth } => self + .render_channel_editor(*depth, window, cx) + .into_any_element(), ListEntry::ChannelInvite(channel) => self .render_channel_invite(channel, is_selected, cx) .into_any_element(), @@ -2067,22 +2242,23 @@ impl CollabPanel { *host_user_id, *is_last, is_selected, + window, cx, ) .into_any_element(), ListEntry::ParticipantScreen { peer_id, is_last } => self - .render_participant_screen(*peer_id, *is_last, is_selected, cx) + .render_participant_screen(*peer_id, *is_last, is_selected, window, cx) .into_any_element(), ListEntry::ChannelNotes { channel_id } => self - .render_channel_notes(*channel_id, is_selected, cx) + .render_channel_notes(*channel_id, is_selected, window, cx) .into_any_element(), ListEntry::ChannelChat { channel_id } => self - .render_channel_chat(*channel_id, is_selected, cx) + .render_channel_chat(*channel_id, is_selected, window, cx) .into_any_element(), } } - fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { + fn render_signed_in(&mut self, _: &mut Window, cx: &mut Context) -> Div { self.channel_store.update(cx, |channel_store, _| { channel_store.initialize(); }); @@ -2102,8 +2278,8 @@ impl CollabPanel { fn render_filter_input( &self, - editor: &View, - cx: &mut ViewContext, + editor: &Entity, + cx: &mut Context, ) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { @@ -2137,7 +2313,7 @@ impl CollabPanel { section: Section, is_selected: bool, is_collapsed: bool, - cx: &ViewContext, + cx: &mut Context, ) -> impl IntoElement { let mut channel_link = None; let mut channel_tooltip_text = None; @@ -2184,23 +2360,25 @@ impl CollabPanel { .icon_size(IconSize::Small) .size(ButtonSize::None) .visible_on_hover("section-header") - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { let item = ClipboardItem::new_string(channel_link_copy.clone()); cx.write_to_clipboard(item) }) - .tooltip(|cx| Tooltip::text("Copy channel link", cx)) + .tooltip(Tooltip::text("Copy channel link")) .into_any_element() }), Section::Contacts => Some( IconButton::new("add-contact", IconName::Plus) - .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) - .tooltip(|cx| Tooltip::text("Search for new contact", cx)) + .on_click( + cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)), + ) + .tooltip(Tooltip::text("Search for new contact")) .into_any_element(), ), Section::Channels => Some( IconButton::new("add-channel", IconName::Plus) - .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) - .tooltip(|cx| Tooltip::text("Create a channel", cx)) + .on_click(cx.listener(|this, _, window, cx| this.new_root_channel(window, cx))) + .tooltip(Tooltip::text("Create a channel")) .into_any_element(), ), _ => None, @@ -2217,11 +2395,11 @@ impl CollabPanel { h_flex().w_full().group("section-header").child( ListHeader::new(text) .when(can_collapse, |header| { - header - .toggle(Some(!is_collapsed)) - .on_toggle(cx.listener(move |this, _, cx| { + header.toggle(Some(!is_collapsed)).on_toggle(cx.listener( + move |this, _, _, cx| { this.toggle_section_expanded(section, cx); - })) + }, + )) }) .inset(true) .end_slot::(button) @@ -2234,7 +2412,7 @@ impl CollabPanel { contact: &Arc, calling: bool, is_selected: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> impl IntoElement { let online = contact.online; let busy = contact.busy || calling; @@ -2258,10 +2436,11 @@ impl CollabPanel { .visible_on_hover("") .on_click(cx.listener({ let contact = contact.clone(); - move |this, event: &ClickEvent, cx| { + move |this, event: &ClickEvent, window, cx| { this.deploy_contact_context_menu( event.down.position, contact.clone(), + window, cx, ); } @@ -2271,8 +2450,8 @@ impl CollabPanel { ) .on_secondary_mouse_down(cx.listener({ let contact = contact.clone(); - move |this, event: &MouseDownEvent, cx| { - this.deploy_contact_context_menu(event.position, contact.clone(), cx); + move |this, event: &MouseDownEvent, window, cx| { + this.deploy_contact_context_menu(event.position, contact.clone(), window, cx); } })) .start_slot( @@ -2292,7 +2471,7 @@ impl CollabPanel { .id(github_login.clone()) .group("") .child(item) - .tooltip(move |cx| { + .tooltip(move |_, cx| { let text = if !online { format!(" {} is offline", &github_login) } else if busy { @@ -2305,7 +2484,7 @@ impl CollabPanel { format!("Call {}", &github_login) } }; - Tooltip::text(text, cx) + Tooltip::simple(text, cx) }) } @@ -2314,7 +2493,7 @@ impl CollabPanel { user: &Arc, is_incoming: bool, is_selected: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> impl IntoElement { let github_login = SharedString::from(user.github_login.clone()); let user_id = user.id; @@ -2328,26 +2507,26 @@ impl CollabPanel { let controls = if is_incoming { vec![ IconButton::new("decline-contact", IconName::Close) - .on_click(cx.listener(move |this, _, cx| { - this.respond_to_contact_request(user_id, false, cx); + .on_click(cx.listener(move |this, _, window, cx| { + this.respond_to_contact_request(user_id, false, window, cx); })) .icon_color(color) - .tooltip(|cx| Tooltip::text("Decline invite", cx)), + .tooltip(Tooltip::text("Decline invite")), IconButton::new("accept-contact", IconName::Check) - .on_click(cx.listener(move |this, _, cx| { - this.respond_to_contact_request(user_id, true, cx); + .on_click(cx.listener(move |this, _, window, cx| { + this.respond_to_contact_request(user_id, true, window, cx); })) .icon_color(color) - .tooltip(|cx| Tooltip::text("Accept invite", cx)), + .tooltip(Tooltip::text("Accept invite")), ] } else { let github_login = github_login.clone(); vec![IconButton::new("remove_contact", IconName::Close) - .on_click(cx.listener(move |this, _, cx| { - this.remove_contact(user_id, &github_login, cx); + .on_click(cx.listener(move |this, _, window, cx| { + this.remove_contact(user_id, &github_login, window, cx); })) .icon_color(color) - .tooltip(|cx| Tooltip::text("Cancel invite", cx))] + .tooltip(Tooltip::text("Cancel invite"))] }; ListItem::new(github_login.clone()) @@ -2368,7 +2547,7 @@ impl CollabPanel { &self, channel: &Arc, is_selected: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> ListItem { let channel_id = channel.id; let response_is_pending = self @@ -2383,17 +2562,17 @@ impl CollabPanel { let controls = [ IconButton::new("reject-invite", IconName::Close) - .on_click(cx.listener(move |this, _, cx| { + .on_click(cx.listener(move |this, _, _, cx| { this.respond_to_channel_invite(channel_id, false, cx); })) .icon_color(color) - .tooltip(|cx| Tooltip::text("Decline invite", cx)), + .tooltip(Tooltip::text("Decline invite")), IconButton::new("accept-invite", IconName::Check) - .on_click(cx.listener(move |this, _, cx| { + .on_click(cx.listener(move |this, _, _, cx| { this.respond_to_channel_invite(channel_id, true, cx); })) .icon_color(color) - .tooltip(|cx| Tooltip::text("Accept invite", cx)), + .tooltip(Tooltip::text("Accept invite")), ]; ListItem::new(("channel-invite", channel.id.0 as usize)) @@ -2412,16 +2591,12 @@ impl CollabPanel { ) } - fn render_contact_placeholder( - &self, - is_selected: bool, - cx: &mut ViewContext, - ) -> ListItem { + fn render_contact_placeholder(&self, is_selected: bool, cx: &mut Context) -> ListItem { ListItem::new("contact-placeholder") .child(Icon::new(IconName::Plus)) .child(Label::new("Add a Contact")) .toggle_state(is_selected) - .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) + .on_click(cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx))) } fn render_channel( @@ -2431,7 +2606,7 @@ impl CollabPanel { has_children: bool, is_selected: bool, ix: usize, - cx: &mut ViewContext, + cx: &mut Context, ) -> impl IntoElement { let channel_id = channel.id; @@ -2492,15 +2667,15 @@ impl CollabPanel { .flex() .w_full() .when(!channel.is_root_channel(), |el| { - el.on_drag(channel.clone(), move |channel, _, cx| { - cx.new_view(|_| DraggedChannelView { + el.on_drag(channel.clone(), move |channel, _, _, cx| { + cx.new(|_| DraggedChannelView { channel: channel.clone(), width, }) }) }) .drag_over::({ - move |style, dragged_channel: &Channel, cx| { + move |style, dragged_channel: &Channel, _window, cx| { if dragged_channel.root_id() == root_id { style.bg(cx.theme().colors().ghost_element_hover) } else { @@ -2508,12 +2683,14 @@ impl CollabPanel { } } }) - .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| { - if dragged_channel.root_id() != root_id { - return; - } - this.move_channel(dragged_channel.id, channel_id, cx); - })) + .on_drop( + cx.listener(move |this, dragged_channel: &Channel, window, cx| { + if dragged_channel.root_id() != root_id { + return; + } + this.move_channel(dragged_channel.id, channel_id, window, cx); + }), + ) .child( ListItem::new(channel_id.0 as usize) // Add one level of depth for the disclosure arrow. @@ -2521,21 +2698,25 @@ impl CollabPanel { .indent_step_size(px(20.)) .toggle_state(is_selected || is_active) .toggle(disclosed) - .on_toggle( - cx.listener(move |this, _, cx| { - this.toggle_channel_collapsed(channel_id, cx) - }), - ) - .on_click(cx.listener(move |this, _, cx| { + .on_toggle(cx.listener(move |this, _, window, cx| { + this.toggle_channel_collapsed(channel_id, window, cx) + })) + .on_click(cx.listener(move |this, _, window, cx| { if is_active { - this.open_channel_notes(channel_id, cx) + this.open_channel_notes(channel_id, window, cx) } else { - this.join_channel(channel_id, cx) + this.join_channel(channel_id, window, cx) } })) .on_secondary_mouse_down(cx.listener( - move |this, event: &MouseDownEvent, cx| { - this.deploy_channel_context_menu(event.position, channel_id, ix, cx) + move |this, event: &MouseDownEvent, window, cx| { + this.deploy_channel_context_menu( + event.position, + channel_id, + ix, + window, + cx, + ) }, )) .start_slot( @@ -2582,10 +2763,10 @@ impl CollabPanel { } else { Color::Muted }) - .on_click(cx.listener(move |this, _, cx| { - this.join_channel_chat(channel_id, cx) + .on_click(cx.listener(move |this, _, window, cx| { + this.join_channel_chat(channel_id, window, cx) })) - .tooltip(|cx| Tooltip::text("Open channel chat", cx)) + .tooltip(Tooltip::text("Open channel chat")) .visible_on_hover(""), ) .child( @@ -2598,18 +2779,18 @@ impl CollabPanel { } else { Color::Muted }) - .on_click(cx.listener(move |this, _, cx| { - this.open_channel_notes(channel_id, cx) + .on_click(cx.listener(move |this, _, window, cx| { + this.open_channel_notes(channel_id, window, cx) })) - .tooltip(|cx| Tooltip::text("Open channel notes", cx)) + .tooltip(Tooltip::text("Open channel notes")) .visible_on_hover(""), ), ), ) .tooltip({ let channel_store = self.channel_store.clone(); - move |cx| { - cx.new_view(|_| JoinChannelTooltip { + move |_window, cx| { + cx.new(|_| JoinChannelTooltip { channel_store: channel_store.clone(), channel_id, has_notes_notification, @@ -2619,7 +2800,12 @@ impl CollabPanel { }) } - fn render_channel_editor(&self, depth: usize, _cx: &mut ViewContext) -> impl IntoElement { + fn render_channel_editor( + &self, + depth: usize, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { let item = ListItem::new("channel-editor") .inset(false) // Add one level of depth for the disclosure arrow. @@ -2643,22 +2829,27 @@ impl CollabPanel { } } -fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement { - let rem_size = cx.rem_size(); - let line_height = cx.text_style().line_height_in_pixels(rem_size); +fn render_tree_branch( + is_last: bool, + overdraw: bool, + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { + let rem_size = window.rem_size(); + let line_height = window.text_style().line_height_in_pixels(rem_size); let width = rem_size * 1.5; let thickness = px(1.); let color = cx.theme().colors().text; canvas( - |_, _| {}, - move |bounds, _, cx| { + |_, _, _| {}, + move |bounds, _, window, _| { let start_x = (bounds.left() + bounds.right() - thickness) / 2.; let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.; let right = bounds.right(); let top = bounds.top(); - cx.paint_quad(fill( + window.paint_quad(fill( Bounds::from_corners( point(start_x, top), point( @@ -2672,7 +2863,7 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> ), color, )); - cx.paint_quad(fill( + window.paint_quad(fill( Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), color, )); @@ -2683,7 +2874,7 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> } impl Render for CollabPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .key_context("CollabPanel") .on_action(cx.listener(CollabPanel::cancel)) @@ -2702,7 +2893,7 @@ impl Render for CollabPanel { .child(if self.user_store.read(cx).current_user().is_none() { self.render_signed_out(cx) } else { - self.render_signed_in(cx) + self.render_signed_in(window, cx) }) .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( @@ -2719,7 +2910,7 @@ impl Render for CollabPanel { impl EventEmitter for CollabPanel {} impl Panel for CollabPanel { - fn position(&self, cx: &WindowContext) -> DockPosition { + fn position(&self, _window: &Window, cx: &App) -> DockPosition { CollaborationPanelSettings::get_global(cx).dock } @@ -2727,7 +2918,12 @@ impl Panel for CollabPanel { matches!(position, DockPosition::Left | DockPosition::Right) } - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + fn set_position( + &mut self, + position: DockPosition, + _window: &mut Window, + cx: &mut Context, + ) { settings::update_settings_file::( self.fs.clone(), cx, @@ -2735,24 +2931,24 @@ impl Panel for CollabPanel { ); } - fn size(&self, cx: &WindowContext) -> Pixels { + fn size(&self, _window: &Window, cx: &App) -> Pixels { self.width .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width) } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, _: &mut Window, cx: &mut Context) { self.width = size; self.serialize(cx); cx.notify(); } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, _window: &Window, cx: &App) -> Option { CollaborationPanelSettings::get_global(cx) .button .then_some(ui::IconName::UserGroup) } - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { Some("Collab Panel") } @@ -2769,8 +2965,8 @@ impl Panel for CollabPanel { } } -impl FocusableView for CollabPanel { - fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { +impl Focusable for CollabPanel { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.filter_editor.focus_handle(cx).clone() } } @@ -2882,7 +3078,7 @@ struct DraggedChannelView { } impl Render for DraggedChannelView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); h_flex() .font_family(ui_font) @@ -2906,15 +3102,15 @@ impl Render for DraggedChannelView { } struct JoinChannelTooltip { - channel_store: Model, + channel_store: Entity, channel_id: ChannelId, #[allow(unused)] has_notes_notification: bool, } impl Render for JoinChannelTooltip { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - tooltip_container(cx, |container, cx| { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + tooltip_container(window, cx, |container, _, cx| { let participants = self .channel_store .read(cx) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index a4a7acd1e5e9e8..5feb0466077e4e 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -5,9 +5,8 @@ use client::{ }; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, anchored, deferred, div, AppContext, ClipboardItem, DismissEvent, EventEmitter, - FocusableView, Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, + actions, anchored, deferred, div, App, ClipboardItem, Context, DismissEvent, Entity, + EventEmitter, Focusable, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, }; use picker::{Picker, PickerDelegate}; use std::sync::Arc; @@ -26,22 +25,23 @@ actions!( ); pub struct ChannelModal { - picker: View>, - channel_store: Model, + picker: Entity>, + channel_store: Entity, channel_id: ChannelId, } impl ChannelModal { pub fn new( - user_store: Model, - channel_store: Model, + user_store: Entity, + channel_store: Entity, channel_id: ChannelId, mode: Mode, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { cx.observe(&channel_store, |_, _, cx| cx.notify()).detach(); - let channel_modal = cx.view().downgrade(); - let picker = cx.new_view(|cx| { + let channel_modal = cx.entity().downgrade(); + let picker = cx.new(|cx| { Picker::uniform_list( ChannelModalDelegate { channel_modal, @@ -57,6 +57,7 @@ impl ChannelModal { has_all_members: false, mode, }, + window, cx, ) .modal(false) @@ -69,27 +70,32 @@ impl ChannelModal { } } - fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext) { + fn toggle_mode(&mut self, _: &ToggleMode, window: &mut Window, cx: &mut Context) { let mode = match self.picker.read(cx).delegate.mode { Mode::ManageMembers => Mode::InviteMembers, Mode::InviteMembers => Mode::ManageMembers, }; - self.set_mode(mode, cx); + self.set_mode(mode, window, cx); } - fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext) { + fn set_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context) { self.picker.update(cx, |picker, cx| { let delegate = &mut picker.delegate; delegate.mode = mode; delegate.selected_index = 0; - picker.set_query("", cx); - picker.update_matches(picker.query(cx), cx); + picker.set_query("", window, cx); + picker.update_matches(picker.query(cx), window, cx); cx.notify() }); cx.notify() } - fn set_channel_visibility(&mut self, selection: &ToggleState, cx: &mut ViewContext) { + fn set_channel_visibility( + &mut self, + selection: &ToggleState, + _: &mut Window, + cx: &mut Context, + ) { self.channel_store.update(cx, |channel_store, cx| { channel_store .set_channel_visibility( @@ -105,7 +111,7 @@ impl ChannelModal { }); } - fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { cx.emit(DismissEvent); } } @@ -113,14 +119,14 @@ impl ChannelModal { impl EventEmitter for ChannelModal {} impl ModalView for ChannelModal {} -impl FocusableView for ChannelModal { - fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { +impl Focusable for ChannelModal { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.picker.focus_handle(cx) } } impl Render for ChannelModal { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let channel_store = self.channel_store.read(cx); let Some(channel) = channel_store.channel_for_id(self.channel_id) else { return div(); @@ -169,7 +175,7 @@ impl Render for ChannelModal { Some( Button::new("copy-link", "Copy Link") .label_size(LabelSize::Small) - .on_click(cx.listener(move |this, _, cx| { + .on_click(cx.listener(move |this, _, _, cx| { if let Some(channel) = this .channel_store .read(cx) @@ -197,8 +203,8 @@ impl Render for ChannelModal { this.border_color(cx.theme().colors().border) }) .child(Label::new("Manage Members")) - .on_click(cx.listener(|this, _, cx| { - this.set_mode(Mode::ManageMembers, cx); + .on_click(cx.listener(|this, _, window, cx| { + this.set_mode(Mode::ManageMembers, window, cx); })), ) .child( @@ -212,8 +218,8 @@ impl Render for ChannelModal { this.border_color(cx.theme().colors().border) }) .child(Label::new("Invite Members")) - .on_click(cx.listener(|this, _, cx| { - this.set_mode(Mode::InviteMembers, cx); + .on_click(cx.listener(|this, _, window, cx| { + this.set_mode(Mode::InviteMembers, window, cx); })), ), ), @@ -229,24 +235,24 @@ pub enum Mode { } pub struct ChannelModalDelegate { - channel_modal: WeakView, + channel_modal: WeakEntity, matching_users: Vec>, matching_member_indices: Vec, - user_store: Model, - channel_store: Model, + user_store: Entity, + channel_store: Entity, channel_id: ChannelId, selected_index: usize, mode: Mode, match_candidates: Vec, members: Vec, has_all_members: bool, - context_menu: Option<(View, Subscription)>, + context_menu: Option<(Entity, Subscription)>, } impl PickerDelegate for ChannelModalDelegate { type ListItem = ListItem; - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Search collaborator by username...".into() } @@ -261,11 +267,21 @@ impl PickerDelegate for ChannelModalDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _: &mut Context>, + ) { self.selected_index = ix; } - fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { match self.mode { Mode::ManageMembers => { if self.has_all_members { @@ -284,7 +300,7 @@ impl PickerDelegate for ChannelModalDelegate { cx.background_executor().clone(), )); - cx.spawn(|picker, mut cx| async move { + cx.spawn_in(window, |picker, mut cx| async move { picker .update(&mut cx, |picker, cx| { let delegate = &mut picker.delegate; @@ -300,7 +316,7 @@ impl PickerDelegate for ChannelModalDelegate { let search_members = self.channel_store.update(cx, |store, cx| { store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx) }); - cx.spawn(|picker, mut cx| async move { + cx.spawn_in(window, |picker, mut cx| async move { async { let members = search_members.await?; picker.update(&mut cx, |picker, cx| { @@ -322,7 +338,7 @@ impl PickerDelegate for ChannelModalDelegate { let search_users = self .user_store .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); - cx.spawn(|picker, mut cx| async move { + cx.spawn_in(window, |picker, mut cx| async move { async { let users = search_users.await?; picker.update(&mut cx, |picker, cx| { @@ -338,26 +354,26 @@ impl PickerDelegate for ChannelModalDelegate { } } - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { if let Some(selected_user) = self.user_at_index(self.selected_index) { if Some(selected_user.id) == self.user_store.read(cx).current_user().map(|user| user.id) { return; } match self.mode { - Mode::ManageMembers => self.show_context_menu(self.selected_index, cx), + Mode::ManageMembers => self.show_context_menu(self.selected_index, window, cx), Mode::InviteMembers => match self.member_status(selected_user.id, cx) { Some(proto::channel_member::Kind::Invitee) => { - self.remove_member(selected_user.id, cx); + self.remove_member(selected_user.id, window, cx); } Some(proto::channel_member::Kind::Member) => {} - None => self.invite_member(selected_user, cx), + None => self.invite_member(selected_user, window, cx), }, } } } - fn dismissed(&mut self, cx: &mut ViewContext>) { + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { if self.context_menu.is_none() { self.channel_modal .update(cx, |_, cx| { @@ -371,7 +387,8 @@ impl PickerDelegate for ChannelModalDelegate { &self, ix: usize, selected: bool, - cx: &mut ViewContext>, + _: &mut Window, + cx: &mut Context>, ) -> Option { let user = self.user_at_index(ix)?; let membership = self.member_at_index(ix); @@ -434,11 +451,7 @@ impl PickerDelegate for ChannelModalDelegate { } impl ChannelModalDelegate { - fn member_status( - &self, - user_id: UserId, - cx: &AppContext, - ) -> Option { + fn member_status(&self, user_id: UserId, cx: &App) -> Option { self.members .iter() .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind)) @@ -470,33 +483,39 @@ impl ChannelModalDelegate { &mut self, user_id: UserId, new_role: ChannelRole, - cx: &mut ViewContext>, + window: &mut Window, + cx: &mut Context>, ) -> Option<()> { let update = self.channel_store.update(cx, |store, cx| { store.set_member_role(self.channel_id, user_id, new_role, cx) }); - cx.spawn(|picker, mut cx| async move { + cx.spawn_in(window, |picker, mut cx| async move { update.await?; - picker.update(&mut cx, |picker, cx| { + picker.update_in(&mut cx, |picker, window, cx| { let this = &mut picker.delegate; if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) { member.role = new_role; } - cx.focus_self(); + cx.focus_self(window); cx.notify(); }) }) - .detach_and_prompt_err("Failed to update role", cx, |_, _| None); + .detach_and_prompt_err("Failed to update role", window, cx, |_, _, _| None); Some(()) } - fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext>) -> Option<()> { + fn remove_member( + &mut self, + user_id: UserId, + window: &mut Window, + cx: &mut Context>, + ) -> Option<()> { let update = self.channel_store.update(cx, |store, cx| { store.remove_member(self.channel_id, user_id, cx) }); - cx.spawn(|picker, mut cx| async move { + cx.spawn_in(window, |picker, mut cx| async move { update.await?; - picker.update(&mut cx, |picker, cx| { + picker.update_in(&mut cx, |picker, window, cx| { let this = &mut picker.delegate; if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) { this.members.remove(ix); @@ -514,20 +533,25 @@ impl ChannelModalDelegate { .selected_index .min(this.matching_member_indices.len().saturating_sub(1)); - picker.focus(cx); + picker.focus(window, cx); cx.notify(); }) }) - .detach_and_prompt_err("Failed to remove member", cx, |_, _| None); + .detach_and_prompt_err("Failed to remove member", window, cx, |_, _, _| None); Some(()) } - fn invite_member(&mut self, user: Arc, cx: &mut ViewContext>) { + fn invite_member( + &mut self, + user: Arc, + window: &mut Window, + cx: &mut Context>, + ) { let invite_member = self.channel_store.update(cx, |store, cx| { store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx) }); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { invite_member.await?; this.update(&mut cx, |this, cx| { @@ -544,25 +568,30 @@ impl ChannelModalDelegate { cx.notify(); }) }) - .detach_and_prompt_err("Failed to invite member", cx, |_, _| None); + .detach_and_prompt_err("Failed to invite member", window, cx, |_, _, _| None); } - fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext>) { + fn show_context_menu( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context>, + ) { let Some(membership) = self.member_at_index(ix) else { return; }; let user_id = membership.user.id; - let picker = cx.view().clone(); - let context_menu = ContextMenu::build(cx, |mut menu, _cx| { + let picker = cx.entity().clone(); + let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| { let role = membership.role; if role == ChannelRole::Admin || role == ChannelRole::Member { let picker = picker.clone(); - menu = menu.entry("Demote to Guest", None, move |cx| { + menu = menu.entry("Demote to Guest", None, move |window, cx| { picker.update(cx, |picker, cx| { picker .delegate - .set_user_role(user_id, ChannelRole::Guest, cx); + .set_user_role(user_id, ChannelRole::Guest, window, cx); }) }); } @@ -575,22 +604,22 @@ impl ChannelModalDelegate { "Demote to Member" }; - menu = menu.entry(label, None, move |cx| { + menu = menu.entry(label, None, move |window, cx| { picker.update(cx, |picker, cx| { picker .delegate - .set_user_role(user_id, ChannelRole::Member, cx); + .set_user_role(user_id, ChannelRole::Member, window, cx); }) }); } if role == ChannelRole::Member || role == ChannelRole::Guest { let picker = picker.clone(); - menu = menu.entry("Promote to Admin", None, move |cx| { + menu = menu.entry("Promote to Admin", None, move |window, cx| { picker.update(cx, |picker, cx| { picker .delegate - .set_user_role(user_id, ChannelRole::Admin, cx); + .set_user_role(user_id, ChannelRole::Admin, window, cx); }) }); }; @@ -598,20 +627,24 @@ impl ChannelModalDelegate { menu = menu.separator(); menu = menu.entry("Remove from Channel", None, { let picker = picker.clone(); - move |cx| { + move |window, cx| { picker.update(cx, |picker, cx| { - picker.delegate.remove_member(user_id, cx); + picker.delegate.remove_member(user_id, window, cx); }) } }); menu }); - cx.focus_view(&context_menu); - let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| { - picker.delegate.context_menu = None; - picker.focus(cx); - cx.notify(); - }); + window.focus(&context_menu.focus_handle(cx)); + let subscription = cx.subscribe_in( + &context_menu, + window, + |picker, _, _: &DismissEvent, window, cx| { + picker.delegate.context_menu = None; + picker.focus(window, cx); + cx.notify(); + }, + ); self.context_menu = Some((context_menu, subscription)); } } diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index 96fe44092d53ae..64b2f6f5b277d5 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -1,7 +1,7 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{ - AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ParentElement as _, - Render, Styled, Task, View, ViewContext, VisualContext, WeakView, + App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement as _, + Render, Styled, Task, WeakEntity, Window, }; use picker::{Picker, PickerDelegate}; use std::sync::Arc; @@ -10,31 +10,31 @@ use util::{ResultExt as _, TryFutureExt}; use workspace::ModalView; pub struct ContactFinder { - picker: View>, + picker: Entity>, } impl ContactFinder { - pub fn new(user_store: Model, cx: &mut ViewContext) -> Self { + pub fn new(user_store: Entity, window: &mut Window, cx: &mut Context) -> Self { let delegate = ContactFinderDelegate { - parent: cx.view().downgrade(), + parent: cx.entity().downgrade(), user_store, potential_contacts: Arc::from([]), selected_index: 0, }; - let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); Self { picker } } - pub fn set_query(&mut self, query: String, cx: &mut ViewContext) { + pub fn set_query(&mut self, query: String, window: &mut Window, cx: &mut Context) { self.picker.update(cx, |picker, cx| { - picker.set_query(query, cx); + picker.set_query(query, window, cx); }); } } impl Render for ContactFinder { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .elevation_3(cx) .child( @@ -53,17 +53,17 @@ impl Render for ContactFinder { } pub struct ContactFinderDelegate { - parent: WeakView, + parent: WeakEntity, potential_contacts: Arc<[Arc]>, - user_store: Model, + user_store: Entity, selected_index: usize, } impl EventEmitter for ContactFinder {} impl ModalView for ContactFinder {} -impl FocusableView for ContactFinder { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for ContactFinder { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } @@ -79,20 +79,30 @@ impl PickerDelegate for ContactFinderDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _: &mut Context>, + ) { self.selected_index = ix; } - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Search collaborator by username...".into() } - fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { let search_users = self .user_store .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); - cx.spawn(|picker, mut cx| async move { + cx.spawn_in(window, |picker, mut cx| async move { async { let potential_contacts = search_users.await?; picker.update(&mut cx, |picker, cx| { @@ -106,7 +116,7 @@ impl PickerDelegate for ContactFinderDelegate { }) } - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _: bool, _: &mut Window, cx: &mut Context>) { if let Some(user) = self.potential_contacts.get(self.selected_index) { let user_store = self.user_store.read(cx); match user_store.contact_request_status(user) { @@ -125,7 +135,7 @@ impl PickerDelegate for ContactFinderDelegate { } } - fn dismissed(&mut self, cx: &mut ViewContext>) { + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { self.parent .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); @@ -135,7 +145,8 @@ impl PickerDelegate for ContactFinderDelegate { &self, ix: usize, selected: bool, - cx: &mut ViewContext>, + _: &mut Window, + cx: &mut Context>, ) -> Option { let user = &self.potential_contacts[ix]; let request_status = self.user_store.read(cx).contact_request_status(user); diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 148e4806cd10a4..dbc408741cda1d 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -9,7 +9,7 @@ use std::{rc::Rc, sync::Arc}; pub use collab_panel::CollabPanel; use gpui::{ - point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds, + point, App, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, }; use panel_settings::MessageEditorSettings; @@ -21,7 +21,7 @@ use settings::Settings; use ui::px; use workspace::AppState; -pub fn init(app_state: &Arc, cx: &mut AppContext) { +pub fn init(app_state: &Arc, cx: &mut App) { CollaborationPanelSettings::register(cx); ChatPanelSettings::register(cx); NotificationPanelSettings::register(cx); @@ -38,7 +38,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { fn notification_window_options( screen: Rc, size: Size, - cx: &AppContext, + cx: &App, ) -> WindowOptions { let notification_margin_width = px(16.); let notification_margin_height = px(-48.); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 574316fa4a586e..3b9ba33d6dd026 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -6,11 +6,10 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ - actions, div, img, list, px, AnyElement, AppContext, AsyncWindowContext, CursorStyle, - DismissEvent, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, - StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, div, img, list, px, AnyElement, App, AsyncWindowContext, Context, CursorStyle, + DismissEvent, Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, + IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, Render, + StatefulInteractiveElement, Styled, Task, WeakEntity, Window, }; use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; @@ -36,16 +35,16 @@ const NOTIFICATION_PANEL_KEY: &str = "NotificationPanel"; pub struct NotificationPanel { client: Arc, - user_store: Model, - channel_store: Model, - notification_store: Model, + user_store: Entity, + channel_store: Entity, + notification_store: Entity, fs: Arc, width: Option, active: bool, notification_list: ListState, pending_serialization: Task>, subscriptions: Vec, - workspace: WeakView, + workspace: WeakEntity, current_notification_toast: Option<(u64, Task<()>)>, local_timezone: UtcOffset, focus_handle: FocusHandle, @@ -75,28 +74,32 @@ pub struct NotificationPresenter { actions!(notification_panel, [ToggleFocus]); -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, _| { - workspace.register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _| { + workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { + workspace.toggle_panel_focus::(window, cx); }); }) .detach(); } impl NotificationPanel { - pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { + pub fn new( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) -> Entity { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let user_store = workspace.app_state().user_store.clone(); let workspace_handle = workspace.weak_handle(); - cx.new_view(|cx: &mut ViewContext| { + cx.new(|cx| { let mut status = client.status(); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { while (status.next().await).is_some() { if this - .update(&mut cx, |_, cx| { + .update(&mut cx, |_: &mut Self, cx| { cx.notify(); }) .is_err() @@ -107,17 +110,18 @@ impl NotificationPanel { }) .detach(); - let view = cx.view().downgrade(); + let model = cx.entity().downgrade(); let notification_list = - ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| { - view.upgrade() - .and_then(|view| { - view.update(cx, |this, cx| this.render_notification(ix, cx)) + ListState::new(0, ListAlignment::Top, px(1000.), move |ix, window, cx| { + model + .upgrade() + .and_then(|model| { + model.update(cx, |this, cx| this.render_notification(ix, window, cx)) }) .unwrap_or_else(|| div().into_any()) }); notification_list.set_scroll_handler(cx.listener( - |this, event: &ListScrollEvent, cx| { + |this, event: &ListScrollEvent, _, cx| { if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD { if let Some(task) = this .notification_store @@ -149,27 +153,34 @@ impl NotificationPanel { unseen_notifications: Vec::new(), }; - let mut old_dock_position = this.position(cx); + let mut old_dock_position = this.position(window, cx); this.subscriptions.extend([ cx.observe(&this.notification_store, |_, _, cx| cx.notify()), - cx.subscribe(&this.notification_store, Self::on_notification_event), - cx.observe_global::(move |this: &mut Self, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(Event::DockPositionChanged); - } - cx.notify(); - }), + cx.subscribe_in( + &this.notification_store, + window, + Self::on_notification_event, + ), + cx.observe_global_in::( + window, + move |this: &mut Self, window, cx| { + let new_dock_position = this.position(window, cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(Event::DockPositionChanged); + } + cx.notify(); + }, + ), ]); this }) } pub fn load( - workspace: WeakView, + workspace: WeakEntity, cx: AsyncWindowContext, - ) -> Task>> { + ) -> Task>> { cx.spawn(|mut cx| async move { let serialized_panel = if let Some(panel) = cx .background_executor() @@ -183,8 +194,8 @@ impl NotificationPanel { None }; - workspace.update(&mut cx, |workspace, cx| { - let panel = Self::new(workspace, cx); + workspace.update_in(&mut cx, |workspace, window, cx| { + let panel = Self::new(workspace, window, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); @@ -196,7 +207,7 @@ impl NotificationPanel { }) } - fn serialize(&mut self, cx: &mut ViewContext) { + fn serialize(&mut self, cx: &mut Context) { let width = self.width; self.pending_serialization = cx.background_executor().spawn( async move { @@ -212,7 +223,12 @@ impl NotificationPanel { ); } - fn render_notification(&mut self, ix: usize, cx: &mut ViewContext) -> Option { + fn render_notification( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context, + ) -> Option { let entry = self.notification_store.read(cx).notification_at(ix)?; let notification_id = entry.id; let now = OffsetDateTime::now_utc(); @@ -229,7 +245,7 @@ impl NotificationPanel { let notification = entry.notification.clone(); if self.active && !entry.is_read { - self.did_render_notification(notification_id, ¬ification, cx); + self.did_render_notification(notification_id, ¬ification, window, cx); } let relative_timestamp = time_format::format_localized_timestamp( @@ -259,8 +275,8 @@ impl NotificationPanel { .when(can_navigate, |el| { el.cursor(CursorStyle::PointingHand).on_click({ let notification = notification.clone(); - cx.listener(move |this, _, cx| { - this.did_click_notification(¬ification, cx) + cx.listener(move |this, _, window, cx| { + this.did_click_notification(¬ification, window, cx) }) }) }) @@ -288,8 +304,8 @@ impl NotificationPanel { .rounded_md() }) .child(Label::new(relative_timestamp).color(Color::Muted)) - .tooltip(move |cx| { - Tooltip::text(absolute_timestamp.clone(), cx) + .tooltip(move |_, cx| { + Tooltip::simple(absolute_timestamp.clone(), cx) }), ) .children(if let Some(is_accepted) = response { @@ -307,9 +323,9 @@ impl NotificationPanel { .justify_end() .child(Button::new("decline", "Decline").on_click({ let notification = notification.clone(); - let view = cx.view().clone(); - move |_, cx| { - view.update(cx, |this, cx| { + let model = cx.entity().clone(); + move |_, _, cx| { + model.update(cx, |this, cx| { this.respond_to_notification( notification.clone(), false, @@ -320,9 +336,9 @@ impl NotificationPanel { })) .child(Button::new("accept", "Accept").on_click({ let notification = notification.clone(); - let view = cx.view().clone(); - move |_, cx| { - view.update(cx, |this, cx| { + let model = cx.entity().clone(); + move |_, _, cx| { + model.update(cx, |this, cx| { this.respond_to_notification( notification.clone(), true, @@ -344,7 +360,7 @@ impl NotificationPanel { fn present_notification( &self, entry: &NotificationEntry, - cx: &AppContext, + cx: &App, ) -> Option { let user_store = self.user_store.read(cx); let channel_store = self.channel_store.read(cx); @@ -415,7 +431,8 @@ impl NotificationPanel { &mut self, notification_id: u64, notification: &Notification, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let should_mark_as_read = match notification { Notification::ContactRequestAccepted { .. } => true, @@ -429,7 +446,7 @@ impl NotificationPanel { .entry(notification_id) .or_insert_with(|| { let client = self.client.clone(); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { cx.background_executor().timer(MARK_AS_READ_DELAY).await; client .request(proto::MarkNotificationRead { notification_id }) @@ -443,7 +460,12 @@ impl NotificationPanel { } } - fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext) { + fn did_click_notification( + &mut self, + notification: &Notification, + window: &mut Window, + cx: &mut Context, + ) { if let Notification::ChannelMessageMention { message_id, channel_id, @@ -451,9 +473,9 @@ impl NotificationPanel { } = notification.clone() { if let Some(workspace) = self.workspace.upgrade() { - cx.window_context().defer(move |cx| { + window.defer(cx, move |window, cx| { workspace.update(cx, |workspace, cx| { - if let Some(panel) = workspace.focus_panel::(cx) { + if let Some(panel) = workspace.focus_panel::(window, cx) { panel.update(cx, |panel, cx| { panel .select_channel(ChannelId(channel_id), Some(message_id), cx) @@ -466,7 +488,7 @@ impl NotificationPanel { } } - fn is_showing_notification(&self, notification: &Notification, cx: &ViewContext) -> bool { + fn is_showing_notification(&self, notification: &Notification, cx: &mut Context) -> bool { if !self.active { return false; } @@ -490,16 +512,17 @@ impl NotificationPanel { fn on_notification_event( &mut self, - _: Model, + _: &Entity, event: &NotificationEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { NotificationEvent::NewNotification { entry } => { if !self.is_showing_notification(&entry.notification, cx) { self.unseen_notifications.push(entry.clone()); } - self.add_toast(entry, cx); + self.add_toast(entry, window, cx); } NotificationEvent::NotificationRemoved { entry } | NotificationEvent::NotificationRead { entry } => { @@ -516,7 +539,12 @@ impl NotificationPanel { } } - fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { + fn add_toast( + &mut self, + entry: &NotificationEntry, + window: &mut Window, + cx: &mut Context, + ) { if self.is_showing_notification(&entry.notification, cx) { return; } @@ -529,7 +557,7 @@ impl NotificationPanel { let notification_id = entry.id; self.current_notification_toast = Some(( notification_id, - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { cx.background_executor().timer(TOAST_DURATION).await; this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx)) .ok(); @@ -542,8 +570,8 @@ impl NotificationPanel { workspace.dismiss_notification(&id, cx); workspace.show_notification(id, cx, |cx| { - let workspace = cx.view().downgrade(); - cx.new_view(|_| NotificationToast { + let workspace = cx.entity().downgrade(); + cx.new(|_| NotificationToast { notification_id, actor, text, @@ -554,7 +582,7 @@ impl NotificationPanel { .ok(); } - fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext) { + fn remove_toast(&mut self, notification_id: u64, cx: &mut Context) { if let Some((current_id, _)) = &self.current_notification_toast { if *current_id == notification_id { self.current_notification_toast.take(); @@ -572,7 +600,8 @@ impl NotificationPanel { &mut self, notification: Notification, response: bool, - cx: &mut ViewContext, + + cx: &mut Context, ) { self.notification_store.update(cx, |store, cx| { store.respond_to_notification(notification, response, cx); @@ -581,7 +610,7 @@ impl NotificationPanel { } impl Render for NotificationPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .child( @@ -611,15 +640,16 @@ impl Render for NotificationPanel { .full_width() .on_click({ let client = self.client.clone(); - move |_, cx| { + move |_, window, cx| { let client = client.clone(); - cx.spawn(move |cx| async move { - client - .authenticate_and_connect(true, &cx) - .log_err() - .await; - }) - .detach() + window + .spawn(cx, move |cx| async move { + client + .authenticate_and_connect(true, &cx) + .log_err() + .await; + }) + .detach() } }), ) @@ -648,8 +678,8 @@ impl Render for NotificationPanel { } } -impl FocusableView for NotificationPanel { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { +impl Focusable for NotificationPanel { + fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } @@ -662,7 +692,7 @@ impl Panel for NotificationPanel { "NotificationPanel" } - fn position(&self, cx: &WindowContext) -> DockPosition { + fn position(&self, _: &Window, cx: &App) -> DockPosition { NotificationPanelSettings::get_global(cx).dock } @@ -670,7 +700,7 @@ impl Panel for NotificationPanel { matches!(position, DockPosition::Left | DockPosition::Right) } - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { settings::update_settings_file::( self.fs.clone(), cx, @@ -678,18 +708,18 @@ impl Panel for NotificationPanel { ); } - fn size(&self, cx: &WindowContext) -> Pixels { + fn size(&self, _: &Window, cx: &App) -> Pixels { self.width .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width) } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, _: &mut Window, cx: &mut Context) { self.width = size; self.serialize(cx); cx.notify(); } - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context) { self.active = active; if self.active { @@ -702,7 +732,7 @@ impl Panel for NotificationPanel { } } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, _: &Window, cx: &App) -> Option { let show_button = NotificationPanelSettings::get_global(cx).button; if !show_button { return None; @@ -715,11 +745,11 @@ impl Panel for NotificationPanel { Some(IconName::BellDot) } - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { Some("Notification Panel") } - fn icon_label(&self, cx: &WindowContext) -> Option { + fn icon_label(&self, _window: &Window, cx: &App) -> Option { let count = self.notification_store.read(cx).unread_notification_count(); if count == 0 { None @@ -741,21 +771,25 @@ pub struct NotificationToast { notification_id: u64, actor: Option>, text: String, - workspace: WeakView, + workspace: WeakEntity, } impl NotificationToast { - fn focus_notification_panel(&self, cx: &mut ViewContext) { + fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context) { let workspace = self.workspace.clone(); let notification_id = self.notification_id; - cx.window_context().defer(move |cx| { + window.defer(cx, move |window, cx| { workspace .update(cx, |workspace, cx| { - if let Some(panel) = workspace.focus_panel::(cx) { + if let Some(panel) = workspace.focus_panel::(window, cx) { panel.update(cx, |panel, cx| { let store = panel.notification_store.read(cx); if let Some(entry) = store.notification_for_id(notification_id) { - panel.did_click_notification(&entry.clone().notification, cx); + panel.did_click_notification( + &entry.clone().notification, + window, + cx, + ); } }); } @@ -766,7 +800,7 @@ impl NotificationToast { } impl Render for NotificationToast { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let user = self.actor.clone(); h_flex() @@ -778,10 +812,10 @@ impl Render for NotificationToast { .child(Label::new(self.text.clone())) .child( IconButton::new("close", IconName::Close) - .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) - .on_click(cx.listener(|this, _, cx| { - this.focus_notification_panel(cx); + .on_click(cx.listener(|this, _, window, cx| { + this.focus_notification_panel(window, cx); cx.emit(DismissEvent); })) } diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index 7759fef52059fb..f5597895dd62ea 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -5,14 +5,14 @@ pub mod project_shared_notification; #[cfg(feature = "stories")] mod stories; -use gpui::AppContext; +use gpui::App; use std::sync::Arc; use workspace::AppState; #[cfg(feature = "stories")] pub use stories::*; -pub fn init(app_state: &Arc, cx: &mut AppContext) { +pub fn init(app_state: &Arc, cx: &mut App) { incoming_call_notification::init(app_state, cx); project_shared_notification::init(app_state, cx); } diff --git a/crates/collab_ui/src/notifications/collab_notification.rs b/crates/collab_ui/src/notifications/collab_notification.rs index 14dae9cd2c3a3d..6b2c5dd0454e30 100644 --- a/crates/collab_ui/src/notifications/collab_notification.rs +++ b/crates/collab_ui/src/notifications/collab_notification.rs @@ -32,7 +32,7 @@ impl ParentElement for CollabNotification { } impl RenderOnce for CollabNotification { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { h_flex() .text_ui(cx) .justify_between() diff --git a/crates/collab_ui/src/notifications/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs index cca67cb5e7cf21..1ee4fd480f1076 100644 --- a/crates/collab_ui/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui/src/notifications/incoming_call_notification.rs @@ -2,14 +2,14 @@ use crate::notification_window_options; use crate::notifications::collab_notification::CollabNotification; use call::{ActiveCall, IncomingCall}; use futures::StreamExt; -use gpui::{prelude::*, AppContext, WindowHandle}; +use gpui::{prelude::*, App, WindowHandle}; use std::sync::{Arc, Weak}; use ui::{prelude::*, Button, Label}; use util::ResultExt; use workspace::AppState; -pub fn init(app_state: &Arc, cx: &mut AppContext) { +pub fn init(app_state: &Arc, cx: &mut App) { let app_state = Arc::downgrade(app_state); let mut incoming_call = ActiveCall::global(cx).read(cx).incoming(); cx.spawn(|mut cx| async move { @@ -17,8 +17,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { while let Some(incoming_call) = incoming_call.next().await { for window in notification_windows.drain(..) { window - .update(&mut cx, |_, cx| { - cx.remove_window(); + .update(&mut cx, |_, window, _| { + window.remove_window(); }) .log_err(); } @@ -36,8 +36,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { .log_err() { let window = cx - .open_window(options, |cx| { - cx.new_view(|_| { + .open_window(options, |_, cx| { + cx.new(|_| { IncomingCallNotification::new( incoming_call.clone(), app_state.clone(), @@ -67,14 +67,14 @@ impl IncomingCallNotificationState { Self { call, app_state } } - fn respond(&self, accept: bool, cx: &mut AppContext) { + fn respond(&self, accept: bool, cx: &mut App) { let active_call = ActiveCall::global(cx); if accept { let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); let caller_user_id = self.call.calling_user.id; let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id); let app_state = self.app_state.clone(); - let cx: &mut AppContext = cx; + let cx: &mut App = cx; cx.spawn(|cx| async move { join.await?; if let Some(project_id) = initial_project_id { @@ -111,19 +111,19 @@ impl IncomingCallNotification { } impl Render for IncomingCallNotification { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let ui_font = theme::setup_ui_font(cx); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let ui_font = theme::setup_ui_font(window, cx); div().size_full().font(ui_font).child( CollabNotification::new( self.state.call.calling_user.avatar_uri.clone(), Button::new("accept", "Accept").on_click({ let state = self.state.clone(); - move |_, cx| state.respond(true, cx) + move |_, _, cx| state.respond(true, cx) }), Button::new("decline", "Decline").on_click({ let state = self.state.clone(); - move |_, cx| state.respond(false, cx) + move |_, _, cx| state.respond(false, cx) }), ) .child(v_flex().overflow_hidden().child(Label::new(format!( diff --git a/crates/collab_ui/src/notifications/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs index 4a55799674de37..d5880dc4b96649 100644 --- a/crates/collab_ui/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui/src/notifications/project_shared_notification.rs @@ -3,14 +3,14 @@ use crate::notifications::collab_notification::CollabNotification; use call::{room, ActiveCall}; use client::User; use collections::HashMap; -use gpui::{AppContext, Size}; +use gpui::{App, Size}; use std::sync::{Arc, Weak}; use ui::{prelude::*, Button, Label}; use util::ResultExt; use workspace::AppState; -pub fn init(app_state: &Arc, cx: &mut AppContext) { +pub fn init(app_state: &Arc, cx: &mut App) { let app_state = Arc::downgrade(app_state); let active_call = ActiveCall::global(cx); let mut notification_windows = HashMap::default(); @@ -28,8 +28,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { for screen in cx.displays() { let options = notification_window_options(screen, window_size, cx); let Some(window) = cx - .open_window(options, |cx| { - cx.new_view(|_| { + .open_window(options, |_, cx| { + cx.new(|_| { ProjectSharedNotification::new( owner.clone(), *project_id, @@ -55,8 +55,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { if let Some(windows) = notification_windows.remove(project_id) { for window in windows { window - .update(cx, |_, cx| { - cx.remove_window(); + .update(cx, |_, window, _| { + window.remove_window(); }) .ok(); } @@ -67,8 +67,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { for (_, windows) in notification_windows.drain() { for window in windows { window - .update(cx, |_, cx| { - cx.remove_window(); + .update(cx, |_, window, _| { + window.remove_window(); }) .ok(); } @@ -101,14 +101,14 @@ impl ProjectSharedNotification { } } - fn join(&mut self, cx: &mut ViewContext) { + fn join(&mut self, cx: &mut Context) { if let Some(app_state) = self.app_state.upgrade() { workspace::join_in_room_project(self.project_id, self.owner.id, app_state, cx) .detach_and_log_err(cx); } } - fn dismiss(&mut self, cx: &mut ViewContext) { + fn dismiss(&mut self, cx: &mut Context) { if let Some(active_room) = ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned()) { @@ -122,18 +122,20 @@ impl ProjectSharedNotification { } impl Render for ProjectSharedNotification { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let ui_font = theme::setup_ui_font(cx); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let ui_font = theme::setup_ui_font(window, cx); div().size_full().font(ui_font).child( CollabNotification::new( self.owner.avatar_uri.clone(), - Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| { + Button::new("open", "Open").on_click(cx.listener(move |this, _event, _, cx| { this.join(cx); })), - Button::new("dismiss", "Dismiss").on_click(cx.listener(move |this, _event, cx| { - this.dismiss(cx); - })), + Button::new("dismiss", "Dismiss").on_click(cx.listener( + move |this, _event, _, cx| { + this.dismiss(cx); + }, + )), ) .child(Label::new(self.owner.github_login.clone())) .child(Label::new(format!( diff --git a/crates/collab_ui/src/notifications/stories/collab_notification.rs b/crates/collab_ui/src/notifications/stories/collab_notification.rs index c70837444b0189..ca939bcda7b1a6 100644 --- a/crates/collab_ui/src/notifications/stories/collab_notification.rs +++ b/crates/collab_ui/src/notifications/stories/collab_notification.rs @@ -7,7 +7,7 @@ use crate::notifications::collab_notification::CollabNotification; pub struct CollabNotificationStory; impl Render for CollabNotificationStory { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { let window_container = |width, height| div().w(px(width)).h(px(height)); Story::container() diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 06c43b5611e43b..3133894d806c2b 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -126,7 +126,7 @@ impl Settings for CollaborationPanelSettings { fn load( sources: SettingsSources, - _: &mut gpui::AppContext, + _: &mut gpui::App, ) -> anyhow::Result { sources.json_merge() } @@ -139,7 +139,7 @@ impl Settings for ChatPanelSettings { fn load( sources: SettingsSources, - _: &mut gpui::AppContext, + _: &mut gpui::App, ) -> anyhow::Result { sources.json_merge() } @@ -152,7 +152,7 @@ impl Settings for NotificationPanelSettings { fn load( sources: SettingsSources, - _: &mut gpui::AppContext, + _: &mut gpui::App, ) -> anyhow::Result { sources.json_merge() } @@ -165,7 +165,7 @@ impl Settings for MessageEditorSettings { fn load( sources: SettingsSources, - _: &mut gpui::AppContext, + _: &mut gpui::App, ) -> anyhow::Result { sources.json_merge() } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index ce0c36300e51f9..d4bfe38d564a26 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -11,8 +11,8 @@ use command_palette_hooks::{ }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global, - ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView, + Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, + ParentElement, Render, Styled, Task, UpdateGlobal, WeakEntity, Window, }; use picker::{Picker, PickerDelegate}; use postage::{sink::Sink, stream::Stream}; @@ -22,17 +22,17 @@ use util::ResultExt; use workspace::{ModalView, Workspace, WorkspaceSettings}; use zed_actions::{command_palette::Toggle, OpenZedUrl}; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { client::init_settings(cx); cx.set_global(HitCounts::default()); command_palette_hooks::init(cx); - cx.observe_new_views(CommandPalette::register).detach(); + cx.observe_new(CommandPalette::register).detach(); } impl ModalView for CommandPalette {} pub struct CommandPalette { - picker: View>, + picker: Entity>, } /// Removes subsequent whitespace characters and double colons from the query. @@ -59,24 +59,40 @@ fn normalize_query(input: &str) -> String { } impl CommandPalette { - fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, _: &Toggle, cx| Self::toggle(workspace, "", cx)); + fn register( + workspace: &mut Workspace, + _window: Option<&mut Window>, + _: &mut Context, + ) { + workspace.register_action(|workspace, _: &Toggle, window, cx| { + Self::toggle(workspace, "", window, cx) + }); } - pub fn toggle(workspace: &mut Workspace, query: &str, cx: &mut ViewContext) { - let Some(previous_focus_handle) = cx.focused() else { + pub fn toggle( + workspace: &mut Workspace, + query: &str, + window: &mut Window, + cx: &mut Context, + ) { + let Some(previous_focus_handle) = window.focused(cx) else { return; }; - workspace.toggle_modal(cx, move |cx| { - CommandPalette::new(previous_focus_handle, query, cx) + workspace.toggle_modal(window, cx, move |window, cx| { + CommandPalette::new(previous_focus_handle, query, window, cx) }); } - fn new(previous_focus_handle: FocusHandle, query: &str, cx: &mut ViewContext) -> Self { + fn new( + previous_focus_handle: FocusHandle, + query: &str, + window: &mut Window, + cx: &mut Context, + ) -> Self { let filter = CommandPaletteFilter::try_global(cx); - let commands = cx - .available_actions() + let commands = window + .available_actions(cx) .into_iter() .filter_map(|action| { if filter.is_some_and(|filter| filter.is_hidden(&*action)) { @@ -91,38 +107,38 @@ impl CommandPalette { .collect(); let delegate = - CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle); + CommandPaletteDelegate::new(cx.entity().downgrade(), commands, previous_focus_handle); - let picker = cx.new_view(|cx| { - let picker = Picker::uniform_list(delegate, cx); - picker.set_query(query, cx); + let picker = cx.new(|cx| { + let picker = Picker::uniform_list(delegate, window, cx); + picker.set_query(query, window, cx); picker }); Self { picker } } - pub fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + pub fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context) { self.picker - .update(cx, |picker, cx| picker.set_query(query, cx)) + .update(cx, |picker, cx| picker.set_query(query, window, cx)) } } impl EventEmitter for CommandPalette {} -impl FocusableView for CommandPalette { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for CommandPalette { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl Render for CommandPalette { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { v_flex().w(rems(34.)).child(self.picker.clone()) } } pub struct CommandPaletteDelegate { - command_palette: WeakView, + command_palette: WeakEntity, all_commands: Vec, commands: Vec, matches: Vec, @@ -158,7 +174,7 @@ impl Global for HitCounts {} impl CommandPaletteDelegate { fn new( - command_palette: WeakView, + command_palette: WeakEntity, commands: Vec, previous_focus_handle: FocusHandle, ) -> Self { @@ -178,7 +194,7 @@ impl CommandPaletteDelegate { query: String, mut commands: Vec, mut matches: Vec, - cx: &mut ViewContext>, + cx: &mut Context>, ) { self.updating_matches.take(); @@ -232,7 +248,7 @@ impl CommandPaletteDelegate { impl PickerDelegate for CommandPaletteDelegate { type ListItem = ListItem; - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { "Execute a command...".into() } @@ -244,14 +260,20 @@ impl PickerDelegate for CommandPaletteDelegate { self.selected_ix } - fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _: &mut Context>, + ) { self.selected_ix = ix; } fn update_matches( &mut self, mut query: String, - cx: &mut ViewContext>, + window: &mut Window, + cx: &mut Context>, ) -> gpui::Task<()> { let settings = WorkspaceSettings::get_global(cx); if let Some(alias) = settings.command_aliases.get(&query) { @@ -304,7 +326,7 @@ impl PickerDelegate for CommandPaletteDelegate { }); self.updating_matches = Some((task, rx.clone())); - cx.spawn(move |picker, mut cx| async move { + cx.spawn_in(window, move |picker, mut cx| async move { let Some((commands, matches)) = rx.recv().await else { return; }; @@ -323,7 +345,8 @@ impl PickerDelegate for CommandPaletteDelegate { &mut self, query: String, duration: Duration, - cx: &mut ViewContext>, + _: &mut Window, + cx: &mut Context>, ) -> bool { let Some((task, rx)) = self.updating_matches.take() else { return true; @@ -344,20 +367,19 @@ impl PickerDelegate for CommandPaletteDelegate { } } - fn dismissed(&mut self, cx: &mut ViewContext>) { + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { self.command_palette .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); } - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { if self.matches.is_empty() { - self.dismissed(cx); + self.dismissed(window, cx); return; } let action_ix = self.matches[self.selected_ix].candidate_id; let command = self.commands.swap_remove(action_ix); - telemetry::event!( "Action Invoked", source = "command palette", @@ -369,16 +391,17 @@ impl PickerDelegate for CommandPaletteDelegate { *hit_counts.0.entry(command.name).or_default() += 1; }); let action = command.action; - cx.focus(&self.previous_focus_handle); - self.dismissed(cx); - cx.dispatch_action(action); + window.focus(&self.previous_focus_handle); + self.dismissed(window, cx); + window.dispatch_action(action, cx); } fn render_match( &self, ix: usize, selected: bool, - cx: &mut ViewContext>, + window: &mut Window, + _: &mut Context>, ) -> Option { let r#match = self.matches.get(ix)?; let command = self.commands.get(r#match.candidate_id)?; @@ -399,7 +422,7 @@ impl PickerDelegate for CommandPaletteDelegate { .children(KeyBinding::for_action_in( &*command.action, &self.previous_focus_handle, - cx, + window, )), ), ) @@ -490,17 +513,18 @@ mod tests { async fn test_command_palette(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_text("abc", cx); + let editor = cx.new_window_entity(|window, cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text("abc", window, cx); editor }); - workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx); - editor.update(cx, |editor, cx| editor.focus(cx)) + workspace.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx))) }); cx.simulate_keystrokes("cmd-shift-p"); @@ -535,7 +559,7 @@ mod tests { }); // Add namespace filter, and redeploy the palette - cx.update(|cx| { + cx.update(|_window, cx| { CommandPaletteFilter::update_global(cx, |filter, _| { filter.hide_namespace("editor"); }); @@ -560,17 +584,18 @@ mod tests { async fn test_normalized_matches(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_text("abc", cx); + let editor = cx.new_window_entity(|window, cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text("abc", window, cx); editor }); - workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx); - editor.update(cx, |editor, cx| editor.focus(cx)) + workspace.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx))) }); // Test normalize (trimming whitespace and double colons) @@ -595,14 +620,17 @@ mod tests { async fn test_go_to_line(cx: &mut TestAppContext) { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.simulate_keystrokes("cmd-n"); let editor = workspace.update(cx, |workspace, cx| { workspace.active_item_as::(cx).unwrap() }); - editor.update(cx, |editor, cx| editor.set_text("1\n2\n3\n4\n5\n6\n", cx)); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("1\n2\n3\n4\n5\n6\n", window, cx) + }); cx.simulate_keystrokes("cmd-shift-p"); cx.simulate_input("go to line: Toggle"); @@ -614,8 +642,8 @@ mod tests { cx.simulate_keystrokes("3 enter"); - editor.update(cx, |editor, cx| { - assert!(editor.focus_handle(cx).is_focused(cx)); + editor.update_in(cx, |editor, window, cx| { + assert!(editor.focus_handle(cx).is_focused(window)); assert_eq!( editor.selections.last::(cx).range().start, Point::new(2, 0) diff --git a/crates/command_palette_hooks/src/command_palette_hooks.rs b/crates/command_palette_hooks/src/command_palette_hooks.rs index e113fe59ca88ef..3ea94bd10f9c5e 100644 --- a/crates/command_palette_hooks/src/command_palette_hooks.rs +++ b/crates/command_palette_hooks/src/command_palette_hooks.rs @@ -6,10 +6,10 @@ use std::any::TypeId; use collections::HashSet; use derive_more::{Deref, DerefMut}; -use gpui::{Action, AppContext, BorrowAppContext, Global}; +use gpui::{Action, App, BorrowAppContext, Global}; /// Initializes the command palette hooks. -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { cx.set_global(GlobalCommandPaletteFilter::default()); cx.set_global(GlobalCommandPaletteInterceptor::default()); } @@ -31,20 +31,20 @@ impl Global for GlobalCommandPaletteFilter {} impl CommandPaletteFilter { /// Returns the global [`CommandPaletteFilter`], if one is set. - pub fn try_global(cx: &AppContext) -> Option<&CommandPaletteFilter> { + pub fn try_global(cx: &App) -> Option<&CommandPaletteFilter> { cx.try_global::() .map(|filter| &filter.0) } /// Returns a mutable reference to the global [`CommandPaletteFilter`]. - pub fn global_mut(cx: &mut AppContext) -> &mut Self { + pub fn global_mut(cx: &mut App) -> &mut Self { cx.global_mut::() } /// Updates the global [`CommandPaletteFilter`] using the given closure. - pub fn update_global(cx: &mut AppContext, update: F) + pub fn update_global(cx: &mut App, update: F) where - F: FnOnce(&mut Self, &mut AppContext), + F: FnOnce(&mut Self, &mut App), { if cx.has_global::() { cx.update_global(|this: &mut GlobalCommandPaletteFilter, cx| update(&mut this.0, cx)) @@ -93,6 +93,7 @@ impl CommandPaletteFilter { } /// The result of intercepting a command palette command. +#[derive(Debug)] pub struct CommandInterceptResult { /// The action produced as a result of the interception. pub action: Box, @@ -107,7 +108,7 @@ pub struct CommandInterceptResult { /// An interceptor for the command palette. #[derive(Default)] pub struct CommandPaletteInterceptor( - Option Option>>, + Option Option>>, ); #[derive(Default)] @@ -117,21 +118,21 @@ impl Global for GlobalCommandPaletteInterceptor {} impl CommandPaletteInterceptor { /// Returns the global [`CommandPaletteInterceptor`], if one is set. - pub fn try_global(cx: &AppContext) -> Option<&CommandPaletteInterceptor> { + pub fn try_global(cx: &App) -> Option<&CommandPaletteInterceptor> { cx.try_global::() .map(|interceptor| &interceptor.0) } /// Updates the global [`CommandPaletteInterceptor`] using the given closure. - pub fn update_global(cx: &mut AppContext, update: F) -> R + pub fn update_global(cx: &mut App, update: F) -> R where - F: FnOnce(&mut Self, &mut AppContext) -> R, + F: FnOnce(&mut Self, &mut App) -> R, { cx.update_global(|this: &mut GlobalCommandPaletteInterceptor, cx| update(&mut this.0, cx)) } /// Intercepts the given query from the command palette. - pub fn intercept(&self, query: &str, cx: &AppContext) -> Option { + pub fn intercept(&self, query: &str, cx: &App) -> Option { let handler = self.0.as_ref()?; (handler)(query, cx) @@ -145,10 +146,7 @@ impl CommandPaletteInterceptor { /// Sets the global interceptor. /// /// This will override the previous interceptor, if it exists. - pub fn set( - &mut self, - handler: Box Option>, - ) { + pub fn set(&mut self, handler: Box Option>) { self.0 = Some(handler); } } diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 64aabb00e836cd..021b7389f5f8e5 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -1,7 +1,7 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, FutureExt}; -use gpui::{AsyncAppContext, BackgroundExecutor, Task}; +use gpui::{AsyncApp, BackgroundExecutor, Task}; use parking_lot::Mutex; use postage::barrier; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -33,7 +33,7 @@ pub const INVALID_PARAMS: i32 = -32602; pub const INTERNAL_ERROR: i32 = -32603; type ResponseHandler = Box)>; -type NotificationHandler = Box; +type NotificationHandler = Box; #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(untagged)] @@ -144,7 +144,7 @@ impl Client { pub fn new( server_id: ContextServerId, binary: ModelContextServerBinary, - cx: AsyncAppContext, + cx: AsyncApp, ) -> Result { log::info!( "starting context server (executable={:?}, args={:?})", @@ -232,7 +232,7 @@ impl Client { stdout: Stdout, notification_handlers: Arc>>, response_handlers: Arc>>>, - cx: AsyncAppContext, + cx: AsyncApp, ) -> anyhow::Result<()> where Stdout: AsyncRead + Unpin + Send + 'static, @@ -400,7 +400,7 @@ impl Client { pub fn on_notification(&self, method: &'static str, f: F) where - F: 'static + Send + FnMut(Value, AsyncAppContext), + F: 'static + Send + FnMut(Value, AsyncApp), { self.notification_handlers .lock() diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index 84c08d7b2a9836..6189ea4e822bd4 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -8,7 +8,7 @@ pub mod types; use command_palette_hooks::CommandPaletteFilter; pub use context_server_settings::{ContextServerSettings, ServerCommand, ServerConfig}; -use gpui::{actions, AppContext}; +use gpui::{actions, App}; pub use crate::context_server_tool::ContextServerTool; pub use crate::registry::ContextServerFactoryRegistry; @@ -18,7 +18,7 @@ actions!(context_servers, [Restart]); /// The namespace for the context servers actions. pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers"; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { context_server_settings::init(cx); ContextServerFactoryRegistry::default_global(cx); extension_context_server::init(cx); diff --git a/crates/context_server/src/context_server_tool.rs b/crates/context_server/src/context_server_tool.rs index 161b61b22f23e1..d91c8e2e64392b 100644 --- a/crates/context_server/src/context_server_tool.rs +++ b/crates/context_server/src/context_server_tool.rs @@ -2,20 +2,20 @@ use std::sync::Arc; use anyhow::{anyhow, bail}; use assistant_tool::Tool; -use gpui::{Model, Task, WindowContext}; +use gpui::{App, Entity, Task, Window}; use crate::manager::ContextServerManager; use crate::types; pub struct ContextServerTool { - server_manager: Model, + server_manager: Entity, server_id: Arc, tool: types::Tool, } impl ContextServerTool { pub fn new( - server_manager: Model, + server_manager: Entity, server_id: impl Into>, tool: types::Tool, ) -> Self { @@ -51,8 +51,9 @@ impl Tool for ContextServerTool { fn run( self: std::sync::Arc, input: serde_json::Value, - _workspace: gpui::WeakView, - cx: &mut WindowContext, + _workspace: gpui::WeakEntity, + _: &mut Window, + cx: &mut App, ) -> gpui::Task> { if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) { cx.foreground_executor().spawn({ diff --git a/crates/context_server/src/extension_context_server.rs b/crates/context_server/src/extension_context_server.rs index 36fecd2af3dfb3..efbbc6d594e913 100644 --- a/crates/context_server/src/extension_context_server.rs +++ b/crates/context_server/src/extension_context_server.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate}; -use gpui::{AppContext, Model}; +use gpui::{App, Entity}; use crate::{ContextServerFactoryRegistry, ServerCommand}; @@ -15,7 +15,7 @@ impl ProjectDelegate for ExtensionProject { } } -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { let proxy = ExtensionHostProxy::default_global(cx); proxy.register_context_server_proxy(ContextServerFactoryRegistryProxy { context_server_factory_registry: ContextServerFactoryRegistry::global(cx), @@ -23,16 +23,11 @@ pub fn init(cx: &mut AppContext) { } struct ContextServerFactoryRegistryProxy { - context_server_factory_registry: Model, + context_server_factory_registry: Entity, } impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy { - fn register_context_server( - &self, - extension: Arc, - id: Arc, - cx: &mut AppContext, - ) { + fn register_context_server(&self, extension: Arc, id: Arc, cx: &mut App) { self.context_server_factory_registry .update(cx, |registry, _| { registry.register_server_factory( diff --git a/crates/context_server/src/manager.rs b/crates/context_server/src/manager.rs index febbee1cdf4f09..1441548b04820c 100644 --- a/crates/context_server/src/manager.rs +++ b/crates/context_server/src/manager.rs @@ -20,7 +20,7 @@ use std::sync::Arc; use anyhow::{bail, Result}; use collections::HashMap; use command_palette_hooks::CommandPaletteFilter; -use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Task, WeakModel}; +use gpui::{AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity}; use log; use parking_lot::RwLock; use project::Project; @@ -61,7 +61,7 @@ impl ContextServer { self.client.read().clone() } - pub async fn start(self: Arc, cx: &AsyncAppContext) -> Result<()> { + pub async fn start(self: Arc, cx: &AsyncApp) -> Result<()> { log::info!("starting context server {}", self.id); let Some(command) = &self.config.command else { bail!("no command specified for server {}", self.id); @@ -104,8 +104,8 @@ impl ContextServer { pub struct ContextServerManager { servers: HashMap, Arc>, - project: Model, - registry: Model, + project: Entity, + registry: Entity, update_servers_task: Option>>, needs_server_update: bool, _subscriptions: Vec, @@ -120,9 +120,9 @@ impl EventEmitter for ContextServerManager {} impl ContextServerManager { pub fn new( - registry: Model, - project: Model, - cx: &mut ModelContext, + registry: Entity, + project: Entity, + cx: &mut Context, ) -> Self { let mut this = Self { _subscriptions: vec![ @@ -143,7 +143,7 @@ impl ContextServerManager { this } - fn available_context_servers_changed(&mut self, cx: &mut ModelContext) { + fn available_context_servers_changed(&mut self, cx: &mut Context) { if self.update_servers_task.is_some() { self.needs_server_update = true; } else { @@ -183,7 +183,7 @@ impl ContextServerManager { pub fn restart_server( &mut self, id: &Arc, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let id = id.clone(); cx.spawn(|this, mut cx| async move { @@ -214,7 +214,7 @@ impl ContextServerManager { .collect() } - async fn maintain_servers(this: WeakModel, mut cx: AsyncAppContext) -> Result<()> { + async fn maintain_servers(this: WeakEntity, mut cx: AsyncApp) -> Result<()> { let mut desired_servers = HashMap::default(); let (registry, project) = this.update(&mut cx, |this, cx| { diff --git a/crates/context_server/src/registry.rs b/crates/context_server/src/registry.rs index a4d0f9a8043a7d..e11a9ffbb968da 100644 --- a/crates/context_server/src/registry.rs +++ b/crates/context_server/src/registry.rs @@ -2,16 +2,15 @@ use std::sync::Arc; use anyhow::Result; use collections::HashMap; -use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ReadGlobal, Task}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Global, ReadGlobal, Task}; use project::Project; use crate::ServerCommand; -pub type ContextServerFactory = Arc< - dyn Fn(Model, &AsyncAppContext) -> Task> + Send + Sync + 'static, ->; +pub type ContextServerFactory = + Arc, &AsyncApp) -> Task> + Send + Sync + 'static>; -struct GlobalContextServerFactoryRegistry(Model); +struct GlobalContextServerFactoryRegistry(Entity); impl Global for GlobalContextServerFactoryRegistry {} @@ -22,16 +21,16 @@ pub struct ContextServerFactoryRegistry { impl ContextServerFactoryRegistry { /// Returns the global [`ContextServerFactoryRegistry`]. - pub fn global(cx: &AppContext) -> Model { + pub fn global(cx: &App) -> Entity { GlobalContextServerFactoryRegistry::global(cx).0.clone() } /// Returns the global [`ContextServerFactoryRegistry`]. /// /// Inserts a default [`ContextServerFactoryRegistry`] if one does not yet exist. - pub fn default_global(cx: &mut AppContext) -> Model { + pub fn default_global(cx: &mut App) -> Entity { if !cx.has_global::() { - let registry = cx.new_model(|_| Self::new()); + let registry = cx.new(|_| Self::new()); cx.set_global(GlobalContextServerFactoryRegistry(registry)); } cx.global::().0.clone() diff --git a/crates/context_server_settings/src/context_server_settings.rs b/crates/context_server_settings/src/context_server_settings.rs index 68969ca7951efb..d91a15ecfb4b55 100644 --- a/crates/context_server_settings/src/context_server_settings.rs +++ b/crates/context_server_settings/src/context_server_settings.rs @@ -1,14 +1,14 @@ use std::sync::Arc; use collections::HashMap; -use gpui::AppContext; +use gpui::App; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { ContextServerSettings::register(cx); } @@ -54,7 +54,7 @@ impl Settings for ContextServerSettings { fn load( sources: SettingsSources, - _: &mut gpui::AppContext, + _: &mut gpui::App, ) -> anyhow::Result { sources.json_merge() } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 67280765f66434..a95b2efeb03c7a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -11,8 +11,8 @@ use collections::{HashMap, HashSet}; use command_palette_hooks::CommandPaletteFilter; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ - actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model, - ModelContext, Task, WeakModel, + actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Task, + WeakEntity, }; use http_client::github::get_release_by_tag_name; use http_client::HttpClient; @@ -58,11 +58,11 @@ pub fn init( fs: Arc, http: Arc, node_runtime: NodeRuntime, - cx: &mut AppContext, + cx: &mut App, ) { copilot_chat::init(fs, http.clone(), cx); - let copilot = cx.new_model({ + let copilot = cx.new({ let node_runtime = node_runtime.clone(); move |cx| Copilot::start(new_server_id, http, node_runtime, cx) }); @@ -209,8 +209,8 @@ struct RegisteredBuffer { impl RegisteredBuffer { fn report_changes( &mut self, - buffer: &Model, - cx: &mut ModelContext, + buffer: &Entity, + cx: &mut Context, ) -> oneshot::Receiver<(i32, BufferSnapshot)> { let (done_tx, done_rx) = oneshot::channel(); @@ -304,7 +304,7 @@ pub struct Copilot { http: Arc, node_runtime: NodeRuntime, server: CopilotServer, - buffers: HashSet>, + buffers: HashSet>, server_id: LanguageServerId, _subscription: gpui::Subscription, } @@ -317,17 +317,17 @@ pub enum Event { impl EventEmitter for Copilot {} -struct GlobalCopilot(Model); +struct GlobalCopilot(Entity); impl Global for GlobalCopilot {} impl Copilot { - pub fn global(cx: &AppContext) -> Option> { + pub fn global(cx: &App) -> Option> { cx.try_global::() .map(|model| model.0.clone()) } - pub fn set_global(copilot: Model, cx: &mut AppContext) { + pub fn set_global(copilot: Entity, cx: &mut App) { cx.set_global(GlobalCopilot(copilot)); } @@ -335,7 +335,7 @@ impl Copilot { new_server_id: LanguageServerId, http: Arc, node_runtime: NodeRuntime, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { let mut this = Self { server_id: new_server_id, @@ -351,10 +351,7 @@ impl Copilot { this } - fn shutdown_language_server( - &mut self, - _cx: &mut ModelContext, - ) -> impl Future { + fn shutdown_language_server(&mut self, _cx: &mut Context) -> impl Future { let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) { CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })), _ => None, @@ -367,7 +364,7 @@ impl Copilot { } } - fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { + fn enable_or_disable_copilot(&mut self, cx: &mut Context) { let server_id = self.server_id; let http = self.http.clone(); let node_runtime = self.node_runtime.clone(); @@ -390,7 +387,7 @@ impl Copilot { } #[cfg(any(test, feature = "test-support"))] - pub fn fake(cx: &mut gpui::TestAppContext) -> (Model, lsp::FakeLanguageServer) { + pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity, lsp::FakeLanguageServer) { use lsp::FakeLanguageServer; use node_runtime::NodeRuntime; @@ -407,7 +404,7 @@ impl Copilot { ); let http = http_client::FakeHttpClient::create(|_| async { unreachable!() }); let node_runtime = NodeRuntime::unavailable(); - let this = cx.new_model(|cx| Self { + let this = cx.new(|cx| Self { server_id: LanguageServerId(0), http: http.clone(), node_runtime, @@ -426,8 +423,8 @@ impl Copilot { new_server_id: LanguageServerId, http: Arc, node_runtime: NodeRuntime, - this: WeakModel, - mut cx: AsyncAppContext, + this: WeakEntity, + mut cx: AsyncApp, ) { let start_language_server = async { let server_path = get_copilot_lsp(http).await?; @@ -513,7 +510,7 @@ impl Copilot { .ok(); } - pub fn sign_in(&mut self, cx: &mut ModelContext) -> Task> { + pub fn sign_in(&mut self, cx: &mut Context) -> Task> { if let CopilotServer::Running(server) = &mut self.server { let task = match &server.sign_in_status { SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(), @@ -598,7 +595,7 @@ impl Copilot { } } - pub fn sign_out(&mut self, cx: &mut ModelContext) -> Task> { + pub fn sign_out(&mut self, cx: &mut Context) -> Task> { self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx); if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server { let server = server.clone(); @@ -613,7 +610,7 @@ impl Copilot { } } - pub fn reinstall(&mut self, cx: &mut ModelContext) -> Task<()> { + pub fn reinstall(&mut self, cx: &mut Context) -> Task<()> { let start_task = cx .spawn({ let http = self.http.clone(); @@ -643,7 +640,7 @@ impl Copilot { } } - pub fn register_buffer(&mut self, buffer: &Model, cx: &mut ModelContext) { + pub fn register_buffer(&mut self, buffer: &Entity, cx: &mut Context) { let weak_buffer = buffer.downgrade(); self.buffers.insert(weak_buffer.clone()); @@ -699,9 +696,9 @@ impl Copilot { fn handle_buffer_event( &mut self, - buffer: Model, + buffer: Entity, event: &language::BufferEvent, - cx: &mut ModelContext, + cx: &mut Context, ) -> Result<()> { if let Ok(server) = self.server.as_running() { if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) @@ -760,7 +757,7 @@ impl Copilot { Ok(()) } - fn unregister_buffer(&mut self, buffer: &WeakModel) { + fn unregister_buffer(&mut self, buffer: &WeakEntity) { if let Ok(server) = self.server.as_running() { if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) { server @@ -777,9 +774,9 @@ impl Copilot { pub fn completions( &mut self, - buffer: &Model, + buffer: &Entity, position: T, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task>> where T: ToPointUtf16, @@ -789,9 +786,9 @@ impl Copilot { pub fn completions_cycling( &mut self, - buffer: &Model, + buffer: &Entity, position: T, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task>> where T: ToPointUtf16, @@ -802,7 +799,7 @@ impl Copilot { pub fn accept_completion( &mut self, completion: &Completion, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let server = match self.server.as_authenticated() { Ok(server) => server, @@ -823,7 +820,7 @@ impl Copilot { pub fn discard_completions( &mut self, completions: &[Completion], - cx: &mut ModelContext, + cx: &mut Context, ) -> Task> { let server = match self.server.as_authenticated() { Ok(server) => server, @@ -846,9 +843,9 @@ impl Copilot { fn request_completions( &mut self, - buffer: &Model, + buffer: &Entity, position: T, - cx: &mut ModelContext, + cx: &mut Context, ) -> Task>> where R: 'static @@ -937,11 +934,7 @@ impl Copilot { } } - fn update_sign_in_status( - &mut self, - lsp_status: request::SignInStatus, - cx: &mut ModelContext, - ) { + fn update_sign_in_status(&mut self, lsp_status: request::SignInStatus, cx: &mut Context) { self.buffers.retain(|buffer| buffer.is_upgradable()); if let Ok(server) = self.server.as_running() { @@ -983,7 +976,7 @@ fn id_for_language(language: Option<&Arc>) -> String { .unwrap_or_else(|| "plaintext".to_string()) } -fn uri_for_buffer(buffer: &Model, cx: &AppContext) -> lsp::Url { +fn uri_for_buffer(buffer: &Entity, cx: &App) -> lsp::Url { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { lsp::Url::from_file_path(file.abs_path(cx)).unwrap() } else { @@ -1073,7 +1066,7 @@ mod tests { async fn test_buffer_management(cx: &mut TestAppContext) { let (copilot, mut lsp) = Copilot::fake(cx); - let buffer_1 = cx.new_model(|cx| Buffer::local("Hello", cx)); + let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx)); let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64()) .parse() .unwrap(); @@ -1091,7 +1084,7 @@ mod tests { } ); - let buffer_2 = cx.new_model(|cx| Buffer::local("Goodbye", cx)); + let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx)); let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64()) .parse() .unwrap(); @@ -1246,11 +1239,11 @@ mod tests { &self.path } - fn full_path(&self, _: &AppContext) -> PathBuf { + fn full_path(&self, _: &App) -> PathBuf { unimplemented!() } - fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr { + fn file_name<'a>(&'a self, _: &'a App) -> &'a std::ffi::OsStr { unimplemented!() } @@ -1258,11 +1251,11 @@ mod tests { unimplemented!() } - fn to_proto(&self, _: &AppContext) -> rpc::proto::File { + fn to_proto(&self, _: &App) -> rpc::proto::File { unimplemented!() } - fn worktree_id(&self, _: &AppContext) -> settings::WorktreeId { + fn worktree_id(&self, _: &App) -> settings::WorktreeId { settings::WorktreeId::from_usize(0) } @@ -1272,15 +1265,15 @@ mod tests { } impl language::LocalFile for File { - fn abs_path(&self, _: &AppContext) -> PathBuf { + fn abs_path(&self, _: &App) -> PathBuf { self.abs_path.clone() } - fn load(&self, _: &AppContext) -> Task> { + fn load(&self, _: &App) -> Task> { unimplemented!() } - fn load_bytes(&self, _cx: &AppContext) -> Task>> { + fn load_bytes(&self, _cx: &App) -> Task>> { unimplemented!() } } diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index 4391f0a955684c..56872169d59654 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Result}; use chrono::DateTime; use fs::Fs; use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt}; -use gpui::{prelude::*, AppContext, AsyncAppContext, Global}; +use gpui::{prelude::*, App, AsyncApp, Global}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; use paths::home_dir; use serde::{Deserialize, Serialize}; @@ -181,7 +181,7 @@ impl TryFrom for ApiToken { } } -struct GlobalCopilotChat(gpui::Model); +struct GlobalCopilotChat(gpui::Entity); impl Global for GlobalCopilotChat {} @@ -191,8 +191,8 @@ pub struct CopilotChat { client: Arc, } -pub fn init(fs: Arc, client: Arc, cx: &mut AppContext) { - let copilot_chat = cx.new_model(|cx| CopilotChat::new(fs, client, cx)); +pub fn init(fs: Arc, client: Arc, cx: &mut App) { + let copilot_chat = cx.new(|cx| CopilotChat::new(fs, client, cx)); cx.set_global(GlobalCopilotChat(copilot_chat)); } @@ -215,12 +215,12 @@ fn copilot_chat_config_paths() -> [PathBuf; 2] { } impl CopilotChat { - pub fn global(cx: &AppContext) -> Option> { + pub fn global(cx: &App) -> Option> { cx.try_global::() .map(|model| model.0.clone()) } - pub fn new(fs: Arc, client: Arc, cx: &AppContext) -> Self { + pub fn new(fs: Arc, client: Arc, cx: &App) -> Self { let config_paths = copilot_chat_config_paths(); let resolve_config_path = { @@ -268,7 +268,7 @@ impl CopilotChat { pub async fn stream_completion( request: Request, - mut cx: AsyncAppContext, + mut cx: AsyncApp, ) -> Result>> { let Some(this) = cx.update(|cx| Self::global(cx)).ok().flatten() else { return Err(anyhow!("Copilot chat is not enabled")); diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 79443f8d3a6ceb..0dc03e40375f50 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1,6 +1,6 @@ use crate::{Completion, Copilot}; use anyhow::Result; -use gpui::{AppContext, EntityId, Model, ModelContext, Task}; +use gpui::{App, Context, Entity, EntityId, Task}; use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider}; use language::{ language_settings::{all_language_settings, AllLanguageSettings}, @@ -19,11 +19,11 @@ pub struct CopilotCompletionProvider { file_extension: Option, pending_refresh: Option>>, pending_cycling_refresh: Option>>, - copilot: Model, + copilot: Entity, } impl CopilotCompletionProvider { - pub fn new(copilot: Model) -> Self { + pub fn new(copilot: Entity) -> Self { Self { cycled: false, buffer_id: None, @@ -73,9 +73,9 @@ impl InlineCompletionProvider for CopilotCompletionProvider { fn is_enabled( &self, - buffer: &Model, + buffer: &Entity, cursor_position: language::Anchor, - cx: &AppContext, + cx: &App, ) -> bool { if !self.copilot.read(cx).status().is_authorized() { return false; @@ -90,10 +90,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider { fn refresh( &mut self, - buffer: Model, + buffer: Entity, cursor_position: language::Anchor, debounce: bool, - cx: &mut ModelContext, + cx: &mut Context, ) { let copilot = self.copilot.clone(); self.pending_refresh = Some(cx.spawn(|this, mut cx| async move { @@ -139,10 +139,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider { fn cycle( &mut self, - buffer: Model, + buffer: Entity, cursor_position: language::Anchor, direction: Direction, - cx: &mut ModelContext, + cx: &mut Context, ) { if self.cycled { match direction { @@ -194,7 +194,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { } } - fn accept(&mut self, cx: &mut ModelContext) { + fn accept(&mut self, cx: &mut Context) { if let Some(completion) = self.active_completion() { self.copilot .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) @@ -202,7 +202,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { } } - fn discard(&mut self, cx: &mut ModelContext) { + fn discard(&mut self, cx: &mut Context) { let settings = AllLanguageSettings::get_global(cx); let copilot_enabled = settings.inline_completions_enabled(None, None, cx); @@ -220,9 +220,9 @@ impl InlineCompletionProvider for CopilotCompletionProvider { fn suggest( &mut self, - buffer: &Model, + buffer: &Entity, cursor_position: language::Anchor, - cx: &mut ModelContext, + cx: &mut Context, ) -> Option { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); @@ -256,6 +256,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { let position = cursor_position.bias_right(buffer); Some(InlineCompletion { edits: vec![(position..position, completion_text.into())], + edit_preview: None, }) } } else { @@ -279,7 +280,7 @@ mod tests { }; use fs::FakeFs; use futures::StreamExt; - use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal}; + use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, @@ -308,9 +309,9 @@ mod tests { cx, ) .await; - let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); - cx.update_editor(|editor, cx| { - editor.set_inline_completion_provider(Some(copilot_provider), cx) + let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot)); + cx.update_editor(|editor, window, cx| { + editor.set_inline_completion_provider(Some(copilot_provider), window, cx) }); cx.set_state(indoc! {" @@ -338,7 +339,7 @@ mod tests { vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert!(editor.context_menu_visible()); assert!(!editor.context_menu_contains_inline_completion()); assert!(!editor.has_active_inline_completion()); @@ -349,7 +350,7 @@ mod tests { // Confirming a non-copilot completion inserts it and hides the context menu, without showing // the copilot suggestion afterwards. editor - .confirm_completion(&Default::default(), cx) + .confirm_completion(&Default::default(), window, cx) .unwrap() .detach(); assert!(!editor.context_menu_visible()); @@ -385,7 +386,7 @@ mod tests { vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_inline_completion()); // Since only the copilot is available, it's shown inline @@ -396,7 +397,7 @@ mod tests { // Ensure existing inline completion is interpolated when inserting again. cx.simulate_keystroke("c"); executor.run_until_parked(); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); assert!(!editor.context_menu_contains_inline_completion()); assert!(editor.has_active_inline_completion()); @@ -415,7 +416,7 @@ mod tests { vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_inline_completion()); assert!(!editor.context_menu_contains_inline_completion()); @@ -423,19 +424,19 @@ mod tests { assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // Canceling should remove the active Copilot suggestion. - editor.cancel(&Default::default(), cx); + editor.cancel(&Default::default(), window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // After canceling, tabbing shouldn't insert the previously shown suggestion. - editor.tab(&Default::default(), cx); + editor.tab(&Default::default(), window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. - editor.undo(&Default::default(), cx); + editor.undo(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); @@ -443,25 +444,25 @@ mod tests { // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // AcceptInlineCompletion when there is an active suggestion inserts it. - editor.accept_inline_completion(&Default::default(), cx); + editor.accept_inline_completion(&Default::default(), window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. - editor.undo(&Default::default(), cx); + editor.undo(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // Hide suggestion. - editor.cancel(&Default::default(), cx); + editor.cancel(&Default::default(), window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); @@ -470,16 +471,16 @@ mod tests { // If an edit occurs outside of this editor but no suggestion is being shown, // we won't make it visible. cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, _, cx| { assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); // Reset the editor to verify how suggestions behave when tabbing on leading indentation. - cx.update_editor(|editor, cx| { - editor.set_text("fn foo() {\n \n}", cx); - editor.change_selections(None, cx, |s| { + cx.update_editor(|editor, window, cx| { + editor.set_text("fn foo() {\n \n}", window, cx); + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) }); }); @@ -493,21 +494,23 @@ mod tests { vec![], ); - cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx)); + cx.update_editor(|editor, window, cx| { + editor.next_inline_completion(&Default::default(), window, cx) + }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); assert_eq!(editor.text(cx), "fn foo() {\n \n}"); // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. - editor.tab(&Default::default(), cx); + editor.tab(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "fn foo() {\n \n}"); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); // Using AcceptInlineCompletion again accepts the suggestion. - editor.accept_inline_completion(&Default::default(), cx); + editor.accept_inline_completion(&Default::default(), window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); @@ -534,9 +537,9 @@ mod tests { cx, ) .await; - let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); - cx.update_editor(|editor, cx| { - editor.set_inline_completion_provider(Some(copilot_provider), cx) + let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot)); + cx.update_editor(|editor, window, cx| { + editor.set_inline_completion_provider(Some(copilot_provider), window, cx) }); // Setup the editor with a completion request. @@ -565,17 +568,17 @@ mod tests { vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert!(editor.has_active_inline_completion()); // Accepting the first word of the suggestion should only accept the first word and still show the rest. - editor.accept_partial_inline_completion(&Default::default(), cx); + editor.accept_partial_inline_completion(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); // Accepting next word should accept the non-word and copilot suggestion should be gone - editor.accept_partial_inline_completion(&Default::default(), cx); + editor.accept_partial_inline_completion(&Default::default(), window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); @@ -607,11 +610,11 @@ mod tests { vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert!(editor.has_active_inline_completion()); // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest. - editor.accept_partial_inline_completion(&Default::default(), cx); + editor.accept_partial_inline_completion(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n"); assert_eq!( @@ -620,7 +623,7 @@ mod tests { ); // Accepting next word should accept the next word and copilot suggestion should still exist - editor.accept_partial_inline_completion(&Default::default(), cx); + editor.accept_partial_inline_completion(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n"); assert_eq!( @@ -629,7 +632,7 @@ mod tests { ); // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone - editor.accept_partial_inline_completion(&Default::default(), cx); + editor.accept_partial_inline_completion(&Default::default(), window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n"); assert_eq!( @@ -658,9 +661,9 @@ mod tests { cx, ) .await; - let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); - cx.update_editor(|editor, cx| { - editor.set_inline_completion_provider(Some(copilot_provider), cx) + let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot)); + cx.update_editor(|editor, window, cx| { + editor.set_inline_completion_provider(Some(copilot_provider), window, cx) }); cx.set_state(indoc! {" @@ -678,31 +681,33 @@ mod tests { }], vec![], ); - cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx)); + cx.update_editor(|editor, window, cx| { + editor.next_inline_completion(&Default::default(), window, cx) + }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntw\nthree\n"); - editor.backspace(&Default::default(), cx); + editor.backspace(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\nt\nthree\n"); - editor.backspace(&Default::default(), cx); + editor.backspace(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); // Deleting across the original suggestion range invalidates it. - editor.backspace(&Default::default(), cx); + editor.backspace(&Default::default(), window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\nthree\n"); assert_eq!(editor.text(cx), "one\nthree\n"); // Undoing the deletion restores the suggestion. - editor.undo(&Default::default(), cx); + editor.undo(&Default::default(), window, cx); assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); @@ -715,9 +720,9 @@ mod tests { let (copilot, copilot_lsp) = Copilot::fake(cx); - let buffer_1 = cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local("c = 3\nd = 4\n", cx)); - let multibuffer = cx.new_model(|cx| { + let buffer_1 = cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx)); + let buffer_2 = cx.new(|cx| Buffer::local("c = 3\nd = 4\n", cx)); + let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite); multibuffer.push_excerpts( buffer_1.clone(), @@ -737,12 +742,18 @@ mod tests { ); multibuffer }); - let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx)); - editor.update(cx, |editor, cx| editor.focus(cx)).unwrap(); - let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); + let editor = cx + .add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, true, window, cx)); + editor + .update(cx, |editor, window, cx| { + use gpui::Focusable; + window.focus(&editor.focus_handle(cx)); + }) + .unwrap(); + let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot)); editor - .update(cx, |editor, cx| { - editor.set_inline_completion_provider(Some(copilot_provider), cx) + .update(cx, |editor, window, cx| { + editor.set_inline_completion_provider(Some(copilot_provider), window, cx) }) .unwrap(); @@ -755,15 +766,15 @@ mod tests { }], vec![], ); - _ = editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, window, cx| { // Ensure copilot suggestions are shown for the first excerpt. - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(1, 5)..Point::new(1, 5)]) }); - editor.next_inline_completion(&Default::default(), cx); + editor.next_inline_completion(&Default::default(), window, cx); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - _ = editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, _, cx| { assert!(editor.has_active_inline_completion()); assert_eq!( editor.display_text(cx), @@ -781,9 +792,9 @@ mod tests { }], vec![], ); - _ = editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, window, cx| { // Move to another excerpt, ensuring the suggestion gets cleared. - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) }); assert!(!editor.has_active_inline_completion()); @@ -794,7 +805,7 @@ mod tests { assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); // Type a character, ensuring we don't even try to interpolate the previous suggestion. - editor.handle_input(" ", cx); + editor.handle_input(" ", window, cx); assert!(!editor.has_active_inline_completion()); assert_eq!( editor.display_text(cx), @@ -805,7 +816,7 @@ mod tests { // Ensure the new suggestion is displayed when the debounce timeout expires. executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - _ = editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, _, cx| { assert!(editor.has_active_inline_completion()); assert_eq!( editor.display_text(cx), @@ -834,9 +845,9 @@ mod tests { cx, ) .await; - let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); - cx.update_editor(|editor, cx| { - editor.set_inline_completion_provider(Some(copilot_provider), cx) + let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot)); + cx.update_editor(|editor, window, cx| { + editor.set_inline_completion_provider(Some(copilot_provider), window, cx) }); cx.set_state(indoc! {" @@ -863,9 +874,11 @@ mod tests { }], vec![], ); - cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx)); + cx.update_editor(|editor, window, cx| { + editor.next_inline_completion(&Default::default(), window, cx) + }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); @@ -892,7 +905,7 @@ mod tests { vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, _, cx| { assert!(!editor.context_menu_visible()); assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); @@ -919,7 +932,7 @@ mod tests { vec![], ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, _, cx| { assert!(editor.context_menu_visible()); assert!(!editor.context_menu_contains_inline_completion()); assert!(!editor.has_active_inline_completion(),); @@ -962,7 +975,7 @@ mod tests { .await .unwrap(); - let multibuffer = cx.new_model(|cx| { + let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite); multibuffer.push_excerpts( private_buffer.clone(), @@ -982,12 +995,18 @@ mod tests { ); multibuffer }); - let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx)); - editor.update(cx, |editor, cx| editor.focus(cx)).unwrap(); - let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); + let editor = cx + .add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, true, window, cx)); + editor + .update(cx, |editor, window, cx| { + use gpui::Focusable; + window.focus(&editor.focus_handle(cx)) + }) + .unwrap(); + let copilot_provider = cx.new(|_| CopilotCompletionProvider::new(copilot)); editor - .update(cx, |editor, cx| { - editor.set_inline_completion_provider(Some(copilot_provider), cx) + .update(cx, |editor, window, cx| { + editor.set_inline_completion_provider(Some(copilot_provider), window, cx) }) .unwrap(); @@ -1007,21 +1026,21 @@ mod tests { }, ); - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |selections| { selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) }); - editor.refresh_inline_completion(true, false, cx); + editor.refresh_inline_completion(true, false, window, cx); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); assert!(copilot_requests.try_next().is_err()); - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(5, 0)..Point::new(5, 0)]) }); - editor.refresh_inline_completion(true, false, cx); + editor.refresh_inline_completion(true, false, window, cx); }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 68f0eed577d855..626da984a9722e 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,8 +1,8 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - div, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle, - FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Render, - Styled, Subscription, ViewContext, + div, App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle, + Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled, + Subscription, Window, }; use ui::{prelude::*, Button, Label, Vector, VectorName}; use util::ResultExt as _; @@ -13,21 +13,17 @@ const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot"; struct CopilotStartingToast; -pub fn initiate_sign_in(cx: &mut WindowContext) { +pub fn initiate_sign_in(window: &mut Window, cx: &mut App) { let Some(copilot) = Copilot::global(cx) else { return; }; let status = copilot.read(cx).status(); - let Some(workspace) = cx.window_handle().downcast::() else { + let Some(workspace) = window.root::().flatten() else { return; }; match status { Status::Starting { task } => { - let Some(workspace) = cx.window_handle().downcast::() else { - return; - }; - - let Ok(workspace) = workspace.update(cx, |workspace, cx| { + workspace.update(cx, |workspace, cx| { workspace.show_toast( Toast::new( NotificationId::unique::(), @@ -35,11 +31,9 @@ pub fn initiate_sign_in(cx: &mut WindowContext) { ), cx, ); - workspace.weak_handle() - }) else { - return; - }; + }); + let workspace = workspace.downgrade(); cx.spawn(|mut cx| async move { task.await; if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() { @@ -69,11 +63,11 @@ pub fn initiate_sign_in(cx: &mut WindowContext) { } _ => { copilot.update(cx, |this, cx| this.sign_in(cx)).detach(); - workspace - .update(cx, |this, cx| { - this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx)); - }) - .ok(); + workspace.update(cx, |this, cx| { + this.toggle_modal(window, cx, |_, cx| { + CopilotCodeVerification::new(&copilot, cx) + }); + }); } } } @@ -85,8 +79,8 @@ pub struct CopilotCodeVerification { _subscription: Subscription, } -impl FocusableView for CopilotCodeVerification { - fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { +impl Focusable for CopilotCodeVerification { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } @@ -95,7 +89,7 @@ impl EventEmitter for CopilotCodeVerification {} impl ModalView for CopilotCodeVerification {} impl CopilotCodeVerification { - pub fn new(copilot: &Model, cx: &mut ViewContext) -> Self { + pub fn new(copilot: &Entity, cx: &mut Context) -> Self { let status = copilot.read(cx).status(); Self { status, @@ -113,15 +107,12 @@ impl CopilotCodeVerification { } } - pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { + pub fn set_status(&mut self, status: Status, cx: &mut Context) { self.status = status; cx.notify(); } - fn render_device_code( - data: &PromptUserDeviceFlow, - cx: &mut ViewContext, - ) -> impl IntoElement { + fn render_device_code(data: &PromptUserDeviceFlow, cx: &mut Context) -> impl IntoElement { let copied = cx .read_from_clipboard() .map(|item| item.text().as_ref() == Some(&data.user_code)) @@ -136,9 +127,9 @@ impl CopilotCodeVerification { .justify_between() .on_mouse_down(gpui::MouseButton::Left, { let user_code = data.user_code.clone(); - move |_, cx| { + move |_, window, cx| { cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone())); - cx.refresh(); + window.refresh(); } }) .child(div().flex_1().child(Label::new(data.user_code.clone()))) @@ -152,7 +143,8 @@ impl CopilotCodeVerification { fn render_prompting_modal( connect_clicked: bool, data: &PromptUserDeviceFlow, - cx: &mut ViewContext, + + cx: &mut Context, ) -> impl Element { let connect_button_label = if connect_clicked { "Waiting for connection..." @@ -177,7 +169,7 @@ impl CopilotCodeVerification { Button::new("connect-button", connect_button_label) .on_click({ let verification_uri = data.verification_uri.clone(); - cx.listener(move |this, _, cx| { + cx.listener(move |this, _, _window, cx| { cx.open_url(&verification_uri); this.connect_clicked = true; }) @@ -188,10 +180,10 @@ impl CopilotCodeVerification { .child( Button::new("copilot-enable-cancel-button", "Cancel") .full_width() - .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) } - fn render_enabled_modal(cx: &mut ViewContext) -> impl Element { + fn render_enabled_modal(cx: &mut Context) -> impl Element { v_flex() .gap_2() .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large)) @@ -201,11 +193,11 @@ impl CopilotCodeVerification { .child( Button::new("copilot-enabled-done-button", "Done") .full_width() - .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) } - fn render_unauthorized_modal(cx: &mut ViewContext) -> impl Element { + fn render_unauthorized_modal(cx: &mut Context) -> impl Element { v_flex() .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large)) @@ -215,12 +207,12 @@ impl CopilotCodeVerification { .child( Button::new("copilot-subscribe-button", "Subscribe on GitHub") .full_width() - .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)), + .on_click(|_, _, cx| cx.open_url(COPILOT_SIGN_UP_URL)), ) .child( Button::new("copilot-subscribe-cancel-button", "Cancel") .full_width() - .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) } @@ -232,7 +224,7 @@ impl CopilotCodeVerification { } impl Render for CopilotCodeVerification { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let prompt = match &self.status { Status::SigningIn { prompt: Some(prompt), @@ -260,11 +252,11 @@ impl Render for CopilotCodeVerification { .items_center() .p_4() .gap_2() - .on_action(cx.listener(|_, _: &menu::Cancel, cx| { + .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| { cx.emit(DismissEvent); })) - .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| { - cx.focus(&this.focus_handle); + .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _| { + window.focus(&this.focus_handle); })) .child( Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.)) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 2071a6a71b121f..2d78a453b7d228 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -8,7 +8,7 @@ use dap_types::{ requests::Request, }; use futures::{channel::oneshot, select, FutureExt as _}; -use gpui::{AppContext, AsyncAppContext, BackgroundExecutor}; +use gpui::{App, AsyncApp, BackgroundExecutor}; use smol::channel::{Receiver, Sender}; use std::{ hash::Hash, @@ -53,7 +53,7 @@ impl DebugAdapterClient { id: DebugAdapterClientId, adapter: Arc, binary: DebugAdapterBinary, - cx: &AsyncAppContext, + cx: &AsyncApp, ) -> Self { let transport_delegate = TransportDelegate::new(adapter.transport()); @@ -67,9 +67,9 @@ impl DebugAdapterClient { } } - pub async fn reconnect(&mut self, message_handler: F, cx: &mut AsyncAppContext) -> Result<()> + pub async fn reconnect(&mut self, message_handler: F, cx: &mut AsyncApp) -> Result<()> where - F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, { let (server_rx, server_tx) = self.transport_delegate.reconnect(cx).await?; log::info!("Successfully reconnected to debug adapter"); @@ -95,9 +95,9 @@ impl DebugAdapterClient { }) } - pub async fn start(&mut self, message_handler: F, cx: &mut AsyncAppContext) -> Result<()> + pub async fn start(&mut self, message_handler: F, cx: &mut AsyncApp) -> Result<()> where - F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, { let (server_rx, server_tx) = self.transport_delegate.start(&self.binary, cx).await?; log::info!("Successfully connected to debug adapter"); @@ -128,10 +128,10 @@ impl DebugAdapterClient { server_rx: Receiver, client_tx: Sender, mut event_handler: F, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result<()> where - F: FnMut(Message, &mut AppContext) + 'static + Send + Sync + Clone, + F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, { let result = loop { let message = match server_rx.recv().await { diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index 657cd3b9b125b9..5b83927dfe8ebe 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -1,5 +1,5 @@ use dap_types::SteppingGranularity; -use gpui::{AppContext, Global}; +use gpui::{App, Global}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -51,10 +51,7 @@ impl Settings for DebuggerSettings { type FileContent = Self; - fn load( - sources: SettingsSources, - _: &mut AppContext, - ) -> anyhow::Result { + fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { sources.json_merge() } } diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs index 745b0ec5244f53..ce7f60a1fc8960 100644 --- a/crates/dap/src/session.rs +++ b/crates/dap/src/session.rs @@ -1,5 +1,5 @@ use collections::HashMap; -use gpui::ModelContext; +use gpui::Context; use std::sync::Arc; use task::DebugAdapterConfig; @@ -39,17 +39,13 @@ impl LocalDebugSession { pub fn update_configuration( &mut self, f: impl FnOnce(&mut DebugAdapterConfig), - cx: &mut ModelContext, + cx: &mut Context, ) { f(&mut self.configuration); cx.notify(); } - pub fn add_client( - &mut self, - client: Arc, - cx: &mut ModelContext, - ) { + pub fn add_client(&mut self, client: Arc, cx: &mut Context) { self.clients.insert(client.id(), client); cx.notify(); } @@ -57,7 +53,7 @@ impl LocalDebugSession { pub fn remove_client( &mut self, client_id: &DebugAdapterClientId, - cx: &mut ModelContext, + cx: &mut Context, ) -> Option> { let client = self.clients.remove(client_id); cx.notify(); @@ -149,11 +145,12 @@ impl DebugSession { } } - pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut ModelContext) { + pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut Context) { match self { DebugSession::Local(local) => local.ignore_breakpoints = ignore, DebugSession::Remote(remote) => remote.ignore_breakpoints = ignore, } + cx.notify(); } } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index d0c7fa64d0af34..2e95f639a9b8c2 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -5,7 +5,7 @@ use dap_types::{ ErrorResponse, }; use futures::{channel::oneshot, select, AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _}; -use gpui::AsyncAppContext; +use gpui::AsyncApp; use settings::Settings as _; use smallvec::SmallVec; use smol::{ @@ -89,7 +89,7 @@ impl TransportDelegate { pub(crate) async fn reconnect( &mut self, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result<(Receiver, Sender)> { self.start_handlers(self.transport.reconnect(cx).await?, cx) .await @@ -98,7 +98,7 @@ impl TransportDelegate { pub(crate) async fn start( &mut self, binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result<(Receiver, Sender)> { self.start_handlers(self.transport.start(binary, cx).await?, cx) .await @@ -107,7 +107,7 @@ impl TransportDelegate { async fn start_handlers( &mut self, mut params: TransportPipe, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result<(Receiver, Sender)> { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); @@ -477,17 +477,13 @@ impl TransportDelegate { #[async_trait(?Send)] pub trait Transport: 'static + Send + Sync + Any { - async fn start( - &self, - binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, - ) -> Result; + async fn start(&self, binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result; fn has_adapter_logs(&self) -> bool; async fn kill(&self) -> Result<()>; - async fn reconnect(&self, _: &mut AsyncAppContext) -> Result { + async fn reconnect(&self, _: &mut AsyncApp) -> Result { bail!("Cannot reconnect to adapter") } @@ -529,7 +525,7 @@ impl TcpTransport { #[async_trait(?Send)] impl Transport for TcpTransport { - async fn reconnect(&self, cx: &mut AsyncAppContext) -> Result { + async fn reconnect(&self, cx: &mut AsyncApp) -> Result { let address = SocketAddrV4::new(self.host, self.port); let timeout = self.timeout.unwrap_or_else(|| { @@ -567,11 +563,7 @@ impl Transport for TcpTransport { )) } - async fn start( - &self, - binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, - ) -> Result { + async fn start(&self, binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result { let mut command = util::command::new_smol_command(&binary.command); if let Some(cwd) = &binary.cwd { @@ -666,11 +658,7 @@ impl StdioTransport { #[async_trait(?Send)] impl Transport for StdioTransport { - async fn start( - &self, - binary: &DebugAdapterBinary, - _: &mut AsyncAppContext, - ) -> Result { + async fn start(&self, binary: &DebugAdapterBinary, _: &mut AsyncApp) -> Result { let mut command = util::command::new_smol_command(&binary.command); if let Some(cwd) = &binary.cwd { @@ -820,7 +808,7 @@ impl FakeTransport { #[cfg(any(test, feature = "test-support"))] #[async_trait(?Send)] impl Transport for FakeTransport { - async fn reconnect(&self, cx: &mut AsyncAppContext) -> Result { + async fn reconnect(&self, cx: &mut AsyncApp) -> Result { self.start( &DebugAdapterBinary { command: "command".into(), @@ -836,7 +824,7 @@ impl Transport for FakeTransport { async fn start( &self, _binary: &DebugAdapterBinary, - cx: &mut AsyncAppContext, + cx: &mut AsyncApp, ) -> Result { use dap_types::requests::{Request, RunInTerminal, StartDebugging}; use serde_json::json; diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 98fca60d6312c9..b825855dbe0c51 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -3,8 +3,8 @@ pub mod query; // Re-export pub use anyhow; -use anyhow::Context; -use gpui::AppContext; +use anyhow::Context as _; +use gpui::App; pub use indoc::indoc; pub use paths::database_dir; pub use smol; @@ -188,7 +188,7 @@ macro_rules! define_connection { }; } -pub fn write_and_log(cx: &AppContext, db_write: impl FnOnce() -> F + Send + 'static) +pub fn write_and_log(cx: &App, db_write: impl FnOnce() -> F + Send + 'static) where F: Future> + Send, { diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index b111ecb1f8bb93..9789f1e53e1b98 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -10,9 +10,8 @@ use futures::{ StreamExt, }; use gpui::{ - actions, div, AppContext, Context, Empty, EventEmitter, FocusHandle, FocusableView, - IntoElement, Model, ModelContext, ParentElement, Render, SharedString, Styled, Subscription, - View, ViewContext, VisualContext, WeakModel, WindowContext, + actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; use project::{search::SearchQuery, Project}; use settings::Settings as _; @@ -30,17 +29,17 @@ use workspace::{ }; struct DapLogView { - editor: View, + editor: Entity, focus_handle: FocusHandle, - log_store: Model, + log_store: Entity, editor_subscriptions: Vec, current_view: Option<(DebugAdapterClientId, LogKind)>, - project: Model, + project: Entity, _subscriptions: Vec, } struct LogStore { - projects: HashMap, ProjectState>, + projects: HashMap, ProjectState>, debug_clients: HashMap, rpc_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, adapter_log_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, @@ -99,7 +98,7 @@ impl DebugAdapterState { } impl LogStore { - fn new(cx: &ModelContext) -> Self { + fn new(cx: &Context) -> Self { let (rpc_tx, mut rpc_rx) = unbounded::<(DebugAdapterClientId, IoKind, String)>(); cx.spawn(|this, mut cx| async move { while let Some((client_id, io_kind, message)) = rpc_rx.next().await { @@ -143,7 +142,7 @@ impl LogStore { client_id: DebugAdapterClientId, io_kind: IoKind, message: &str, - cx: &mut ModelContext, + cx: &mut Context, ) { self.add_debug_client_message(client_id, io_kind, message.to_string(), cx); } @@ -153,12 +152,12 @@ impl LogStore { client_id: DebugAdapterClientId, io_kind: IoKind, message: &str, - cx: &mut ModelContext, + cx: &mut Context, ) { self.add_debug_client_log(client_id, io_kind, message.to_string(), cx); } - pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { + pub fn add_project(&mut self, project: &Entity, cx: &mut Context) { let weak_project = project.downgrade(); self.projects.insert( project.downgrade(), @@ -201,7 +200,7 @@ impl LogStore { id: DebugAdapterClientId, io_kind: IoKind, message: String, - cx: &mut ModelContext, + cx: &mut Context, ) { let Some(debug_client_state) = self.get_debug_adapter_state(id) else { return; @@ -233,7 +232,7 @@ impl LogStore { id: DebugAdapterClientId, io_kind: IoKind, message: String, - cx: &mut ModelContext, + cx: &mut Context, ) { let Some(debug_client_state) = self.get_debug_adapter_state(id) else { return; @@ -263,7 +262,7 @@ impl LogStore { id: DebugAdapterClientId, message: String, kind: LogKind, - cx: &mut ModelContext, + cx: &mut Context, ) { while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT { log_lines.pop_front(); @@ -290,7 +289,7 @@ impl LogStore { fn add_debug_client( &mut self, client_id: DebugAdapterClientId, - session_and_client: Option<(Model, Arc)>, + session_and_client: Option<(Entity, Arc)>, ) -> Option<&mut DebugAdapterState> { let client_state = self .debug_clients @@ -323,11 +322,7 @@ impl LogStore { Some(client_state) } - fn remove_debug_client( - &mut self, - client_id: DebugAdapterClientId, - cx: &mut ModelContext, - ) { + fn remove_debug_client(&mut self, client_id: DebugAdapterClientId, cx: &mut Context) { self.debug_clients.remove(&client_id); cx.notify(); } @@ -354,7 +349,7 @@ impl LogStore { } pub struct DapLogToolbarItemView { - log_view: Option>, + log_view: Option>, } impl DapLogToolbarItemView { @@ -364,7 +359,7 @@ impl DapLogToolbarItemView { } impl Render for DapLogToolbarItemView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let Some(log_view) = self.log_view.clone() else { return Empty.into_any_element(); }; @@ -402,15 +397,15 @@ impl Render for DapLogToolbarItemView { }) .unwrap_or_else(|| "No adapter selected".into()), )) - .menu(move |cx| { + .menu(move |mut window, cx| { let log_view = log_view.clone(); let menu_rows = menu_rows.clone(); - ContextMenu::build(cx, move |mut menu, cx| { + ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| { for row in menu_rows.into_iter() { menu = menu.header(format!("{}. {}", row.session_id.0, row.session_name)); for sub_item in row.clients.into_iter() { - menu = menu.custom_row(move |_| { + menu = menu.custom_row(move |_window, _cx| { div() .w_full() .pl_2() @@ -426,29 +421,33 @@ impl Render for DapLogToolbarItemView { if sub_item.has_adapter_logs { menu = menu.custom_entry( - move |_| { + move |_window, _cx| { div() .w_full() .pl_4() .child(Label::new(ADAPTER_LOGS)) .into_any_element() }, - cx.handler_for(&log_view, move |view, cx| { - view.show_log_messages_for_adapter(sub_item.client_id, cx); + window.handler_for(&log_view, move |view, window, cx| { + view.show_log_messages_for_adapter( + sub_item.client_id, + window, + cx, + ); }), ); } menu = menu.custom_entry( - move |_| { + move |_window, _cx| { div() .w_full() .pl_4() .child(Label::new(RPC_MESSAGES)) .into_any_element() }, - cx.handler_for(&log_view, move |view, cx| { - view.show_rpc_trace_for_server(sub_item.client_id, cx); + window.handler_for(&log_view, move |view, window, cx| { + view.show_rpc_trace_for_server(sub_item.client_id, window, cx); }), ); } @@ -465,12 +464,12 @@ impl Render for DapLogToolbarItemView { div() .child( Button::new("clear_log_button", "Clear").on_click(cx.listener( - |this, _, cx| { + |this, _, window, cx| { if let Some(log_view) = this.log_view.as_ref() { log_view.update(cx, |log_view, cx| { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); - editor.clear(cx); + editor.clear(window, cx); editor.set_read_only(true); }); }) @@ -490,7 +489,8 @@ impl ToolbarItemView for DapLogToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn workspace::item::ItemHandle>, - cx: &mut ViewContext, + _window: &mut Window, + cx: &mut Context, ) -> workspace::ToolbarItemLocation { if let Some(item) = active_pane_item { if let Some(log_view) = item.downcast::() { @@ -508,15 +508,16 @@ impl ToolbarItemView for DapLogToolbarItemView { impl DapLogView { pub fn new( - project: Model, - log_store: Model, - cx: &mut ViewContext, + project: Entity, + log_store: Entity, + window: &mut Window, + cx: &mut Context, ) -> Self { - let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), cx); + let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx); let focus_handle = cx.focus_handle(); - let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { + let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event { Event::NewLogEntry { id, entry, kind } => { if log_view.current_view == Some((*id, *kind)) { log_view.editor.update(cx, |editor, cx| { @@ -548,32 +549,29 @@ impl DapLogView { fn editor_for_logs( log_contents: String, - cx: &mut ViewContext, - ) -> (View, Vec) { - let editor = cx.new_view(|cx| { - let mut editor = Editor::multi_line(cx); - editor.set_text(log_contents, cx); - editor.move_to_end(&editor::actions::MoveToEnd, cx); + window: &mut Window, + cx: &mut Context, + ) -> (Entity, Vec) { + let editor = cx.new(|cx| { + let mut editor = Editor::multi_line(window, cx); + editor.set_text(log_contents, window, cx); + editor.move_to_end(&editor::actions::MoveToEnd, window, cx); editor.set_read_only(true); - editor.set_show_inline_completions(Some(false), cx); + editor.set_show_inline_completions(Some(false), window, cx); editor }); let editor_subscription = cx.subscribe( &editor, - |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, DapLogView>| { - cx.emit(event.clone()) - }, + |_, _, event: &EditorEvent, cx: &mut Context<'_, DapLogView>| cx.emit(event.clone()), ); let search_subscription = cx.subscribe( &editor, - |_, _, event: &SearchEvent, cx: &mut ViewContext<'_, DapLogView>| { - cx.emit(event.clone()) - }, + |_, _, event: &SearchEvent, cx: &mut Context<'_, DapLogView>| cx.emit(event.clone()), ); (editor, vec![editor_subscription, search_subscription]) } - fn menu_items(&self, cx: &AppContext) -> Option> { + fn menu_items(&self, cx: &App) -> Option> { let mut menu_items = self .project .read(cx) @@ -611,7 +609,8 @@ impl DapLogView { fn show_rpc_trace_for_server( &mut self, client_id: DebugAdapterClientId, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let rpc_log = self.log_store.update(cx, |log_store, _| { log_store @@ -620,7 +619,7 @@ impl DapLogView { }); if let Some(rpc_log) = rpc_log { self.current_view = Some((client_id, LogKind::Rpc)); - let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, cx); + let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); let language = self.project.read(cx).languages().language_for_name("JSON"); editor .read(cx) @@ -630,7 +629,7 @@ impl DapLogView { .expect("log buffer should be a singleton") .update(cx, |_, cx| { cx.spawn({ - let buffer = cx.handle(); + let buffer = cx.entity(); |_, mut cx| async move { let language = language.await.ok(); buffer.update(&mut cx, |buffer, cx| { @@ -646,13 +645,14 @@ impl DapLogView { cx.notify(); } - cx.focus(&self.focus_handle); + cx.focus_self(window); } fn show_log_messages_for_adapter( &mut self, client_id: DebugAdapterClientId, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let message_log = self.log_store.update(cx, |log_store, _| { log_store @@ -661,7 +661,7 @@ impl DapLogView { }); if let Some(message_log) = message_log { self.current_view = Some((client_id, LogKind::Adapter)); - let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, cx); + let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx); editor .read(cx) .buffer() @@ -674,7 +674,7 @@ impl DapLogView { cx.notify(); } - cx.focus(&self.focus_handle); + cx.focus_self(window); } } @@ -708,18 +708,23 @@ const ADAPTER_LOGS: &str = "Adapter Logs"; const RPC_MESSAGES: &str = "RPC Messages"; impl Render for DapLogView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - self.editor - .update(cx, |editor, cx| editor.render(cx).into_any_element()) + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.editor.update(cx, |editor, cx| { + editor.render(window, cx).into_any_element() + }) } } actions!(debug, [OpenDebuggerAdapterLogs]); -pub fn init(cx: &mut AppContext) { - let log_store = cx.new_model(|cx| LogStore::new(cx)); +pub fn init(cx: &mut App) { + let log_store = cx.new(|cx| LogStore::new(cx)); + + cx.observe_new(move |workspace: &mut Workspace, window, cx| { + let Some(_window) = window else { + return; + }; - cx.observe_new_views(move |workspace: &mut Workspace, cx| { let project = workspace.project(); if project.read(cx).is_local() { log_store.update(cx, |store, cx| { @@ -728,15 +733,16 @@ pub fn init(cx: &mut AppContext) { } let log_store = log_store.clone(); - workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, cx| { + workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, window, cx| { let project = workspace.project().read(cx); if project.is_local() { workspace.add_item_to_active_pane( - Box::new(cx.new_view(|cx| { - DapLogView::new(workspace.project().clone(), log_store.clone(), cx) + Box::new(cx.new(|cx| { + DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx) })), None, true, + window, cx, ); } @@ -752,7 +758,7 @@ impl Item for DapLogView { Editor::to_item_events(event, f) } - fn tab_content_text(&self, _cx: &WindowContext) -> Option { + fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { Some("DAP Logs".into()) } @@ -760,7 +766,7 @@ impl Item for DapLogView { None } - fn as_searchable(&self, handle: &View) -> Option> { + fn as_searchable(&self, handle: &Entity) -> Option> { Some(Box::new(handle.clone())) } } @@ -768,43 +774,63 @@ impl Item for DapLogView { impl SearchableItem for DapLogView { type Match = ::Match; - fn clear_matches(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |e, cx| e.clear_matches(cx)) + fn clear_matches(&mut self, window: &mut Window, cx: &mut Context) { + self.editor.update(cx, |e, cx| e.clear_matches(window, cx)) } - fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { + fn update_matches( + &mut self, + matches: &[Self::Match], + window: &mut Window, + cx: &mut Context, + ) { self.editor - .update(cx, |e, cx| e.update_matches(matches, cx)) + .update(cx, |e, cx| e.update_matches(matches, window, cx)) } - fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - self.editor.update(cx, |e, cx| e.query_suggestion(cx)) + fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context) -> String { + self.editor + .update(cx, |e, cx| e.query_suggestion(window, cx)) } fn activate_match( &mut self, index: usize, matches: &[Self::Match], - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { self.editor - .update(cx, |e, cx| e.activate_match(index, matches, cx)) + .update(cx, |e, cx| e.activate_match(index, matches, window, cx)) } - fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { + fn select_matches( + &mut self, + matches: &[Self::Match], + window: &mut Window, + cx: &mut Context, + ) { self.editor - .update(cx, |e, cx| e.select_matches(matches, cx)) + .update(cx, |e, cx| e.select_matches(matches, window, cx)) } fn find_matches( &mut self, query: Arc, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> gpui::Task> { - self.editor.update(cx, |e, cx| e.find_matches(query, cx)) + self.editor + .update(cx, |e, cx| e.find_matches(query, window, cx)) } - fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + fn replace( + &mut self, + _: &Self::Match, + _: &SearchQuery, + _window: &mut Window, + _: &mut Context, + ) { // Since DAP Log is read-only, it doesn't make sense to support replace operation. } @@ -821,15 +847,16 @@ impl SearchableItem for DapLogView { fn active_match_index( &mut self, matches: &[Self::Match], - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option { self.editor - .update(cx, |e, cx| e.active_match_index(matches, cx)) + .update(cx, |e, cx| e.active_match_index(matches, window, cx)) } } -impl FocusableView for DapLogView { - fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { +impl Focusable for DapLogView { + fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } diff --git a/crates/debugger_tools/src/debugger_tools.rs b/crates/debugger_tools/src/debugger_tools.rs index 744e90669bf402..da7a43b53b534e 100644 --- a/crates/debugger_tools/src/debugger_tools.rs +++ b/crates/debugger_tools/src/debugger_tools.rs @@ -1,8 +1,8 @@ mod dap_log; pub use dap_log::*; -use gpui::AppContext; +use gpui::App; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { dap_log::init(cx); } diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index ea3162f0390ee1..feabb90d41dd64 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -1,13 +1,13 @@ use dap::client::DebugAdapterClientId; use dap::session::DebugSessionId; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{DismissEvent, EventEmitter, FocusableView, Render, View}; -use gpui::{Model, Subscription}; +use gpui::Subscription; +use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; use picker::{Picker, PickerDelegate}; use project::dap_store::DapStore; use std::sync::Arc; use sysinfo::System; -use ui::{prelude::*, ViewContext}; +use ui::{prelude::*, Context}; use ui::{ListItem, ListItemSpacing}; use workspace::ModalView; @@ -23,7 +23,7 @@ pub(crate) struct AttachModalDelegate { matches: Vec, session_id: DebugSessionId, placeholder_text: Arc, - dap_store: Model, + dap_store: Entity, client_id: DebugAdapterClientId, candidates: Option>, } @@ -32,7 +32,7 @@ impl AttachModalDelegate { pub fn new( session_id: DebugSessionId, client_id: DebugAdapterClientId, - dap_store: Model, + dap_store: Entity, ) -> Self { Self { client_id, @@ -48,19 +48,21 @@ impl AttachModalDelegate { pub(crate) struct AttachModal { _subscription: Subscription, - pub(crate) picker: View>, + pub(crate) picker: Entity>, } impl AttachModal { pub fn new( session_id: &DebugSessionId, client_id: &DebugAdapterClientId, - dap_store: Model, - cx: &mut ViewContext, + dap_store: Entity, + window: &mut Window, + cx: &mut Context, ) -> Self { - let picker = cx.new_view(|cx| { + let picker = cx.new(|cx| { Picker::uniform_list( AttachModalDelegate::new(*session_id, *client_id, dap_store), + window, cx, ) }); @@ -75,7 +77,7 @@ impl AttachModal { } impl Render for AttachModal { - fn render(&mut self, _: &mut ViewContext) -> impl ui::IntoElement { + fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl ui::IntoElement { v_flex() .key_context("AttachModal") .w(rems(34.)) @@ -85,8 +87,8 @@ impl Render for AttachModal { impl EventEmitter for AttachModal {} -impl FocusableView for AttachModal { - fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { +impl Focusable for AttachModal { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.picker.read(cx).focus_handle(cx) } } @@ -104,18 +106,24 @@ impl PickerDelegate for AttachModalDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _: &mut Context>, + ) { self.selected_index = ix; } - fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> std::sync::Arc { + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc { self.placeholder_text.clone() } fn update_matches( &mut self, query: String, - cx: &mut ViewContext>, + _window: &mut Window, + cx: &mut Context>, ) -> gpui::Task<()> { cx.spawn(|this, mut cx| async move { let Some(processes) = this @@ -197,7 +205,7 @@ impl PickerDelegate for AttachModalDelegate { }) } - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context>) { let candidate = self .matches .get(self.selected_index()) @@ -218,7 +226,7 @@ impl PickerDelegate for AttachModalDelegate { cx.emit(DismissEvent); } - fn dismissed(&mut self, cx: &mut ViewContext>) { + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { self.selected_index = 0; self.candidates.take(); @@ -233,7 +241,8 @@ impl PickerDelegate for AttachModalDelegate { &self, ix: usize, selected: bool, - _: &mut ViewContext>, + _window: &mut Window, + _: &mut Context>, ) -> Option { let candidates = self.candidates.as_ref()?; let hit = &self.matches[ix]; @@ -260,10 +269,7 @@ impl PickerDelegate for AttachModalDelegate { #[allow(dead_code)] #[cfg(any(test, feature = "test-support"))] -pub(crate) fn procss_names( - modal: &AttachModal, - cx: &mut gpui::ViewContext, -) -> Vec { +pub(crate) fn procss_names(modal: &AttachModal, cx: &mut Context) -> Vec { modal.picker.update(cx, |picker, _| { picker .delegate diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index d51644cde16015..91f08db1832931 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -8,7 +8,7 @@ use editor::{ Anchor, CompletionProvider, Editor, EditorElement, EditorStyle, FoldPlaceholder, }; use fuzzy::StringMatchCandidate; -use gpui::{Model, Render, Subscription, Task, TextStyle, View, ViewContext, WeakView}; +use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16}; use menu::Confirm; use project::{dap_store::DapStore, Completion}; @@ -27,26 +27,27 @@ pub struct OutputGroup { pub struct Console { groups: Vec, - console: View, - query_bar: View, - dap_store: Model, + console: Entity, + query_bar: Entity, + dap_store: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, - variable_list: View, - stack_frame_list: View, + variable_list: Entity, + stack_frame_list: Entity, } impl Console { pub fn new( - stack_frame_list: &View, + stack_frame_list: &Entity, client_id: &DebugAdapterClientId, - variable_list: View, - dap_store: Model, - cx: &mut ViewContext, + variable_list: Entity, + dap_store: Entity, + window: &mut Window, + cx: &mut Context, ) -> Self { - let console = cx.new_view(|cx| { - let mut editor = Editor::multi_line(cx); - editor.move_to_end(&editor::actions::MoveToEnd, cx); + let console = cx.new(|cx| { + let mut editor = Editor::multi_line(window, cx); + editor.move_to_end(&editor::actions::MoveToEnd, window, cx); editor.set_read_only(true); editor.set_show_gutter(true, cx); editor.set_show_runnables(false, cx); @@ -58,13 +59,13 @@ impl Console { editor.set_use_autoclose(false); editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); - editor.set_show_inline_completions(Some(false), cx); + editor.set_show_inline_completions(Some(false), window, cx); editor }); - let this = cx.view().downgrade(); - let query_bar = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); + let this = cx.weak_entity(); + let query_bar = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Evaluate an expression", cx); editor.set_use_autoclose(false); editor.set_show_gutter(false, cx); @@ -91,20 +92,20 @@ impl Console { } #[cfg(any(test, feature = "test-support"))] - pub fn editor(&self) -> &View { + pub fn editor(&self) -> &Entity { &self.console } #[cfg(any(test, feature = "test-support"))] - pub fn query_bar(&self) -> &View { + pub fn query_bar(&self) -> &Entity { &self.query_bar } fn handle_stack_frame_list_events( &mut self, - _: View, + _: Entity, event: &StackFrameListEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { match event { StackFrameListEvent::SelectedStackFrameChanged => cx.notify(), @@ -112,7 +113,7 @@ impl Console { } } - pub fn add_message(&mut self, event: OutputEvent, cx: &mut ViewContext) { + pub fn add_message(&mut self, event: OutputEvent, window: &mut Window, cx: &mut Context) { self.console.update(cx, |console, cx| { let output = event.output.trim_end().to_string(); @@ -136,8 +137,8 @@ impl Console { }; console.set_read_only(false); - console.move_to_end(&editor::actions::MoveToEnd, cx); - console.insert(format!("{}{}\n", indent, output).as_str(), cx); + console.move_to_end(&editor::actions::MoveToEnd, window, cx); + console.insert(format!("{}{}\n", indent, output).as_str(), window, cx); console.set_read_only(true); let end = snapshot.anchor_before(snapshot.max_point()); @@ -178,7 +179,7 @@ impl Console { console.insert_creases(creases.clone(), cx); if group.collapsed { - console.fold_creases(creases, false, cx); + console.fold_creases(creases, false, window, cx); } } } @@ -195,7 +196,7 @@ impl Console { FoldPlaceholder { render: Arc::new({ let placeholder = placeholder.clone(); - move |_id, _range, _cx| { + move |_id, _range, _window, _cx| { ButtonLike::new("output-group-placeholder") .style(ButtonStyle::Transparent) .layer(ElevationIndex::ElevatedSurface) @@ -205,21 +206,21 @@ impl Console { }), ..Default::default() }, - move |row, is_folded, fold, _cx| { + move |row, is_folded, fold, _window, _cx| { Disclosure::new(("output-group", row.0 as u64), !is_folded) .toggle_state(is_folded) - .on_click(move |_event, cx| fold(!is_folded, cx)) + .on_click(move |_event, window, cx| fold(!is_folded, window, cx)) .into_any_element() }, - move |_id, _range, _cx| gpui::Empty.into_any_element(), + move |_id, _range, _window, _cx| gpui::Empty.into_any_element(), ) } - pub fn evaluate(&mut self, _: &Confirm, cx: &mut ViewContext) { + pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { let expression = self.query_bar.update(cx, |editor, cx| { let expression = editor.text(cx); - editor.clear(cx); + editor.clear(window, cx); expression }); @@ -235,33 +236,37 @@ impl Console { ) }); - cx.spawn(|this, mut cx| async move { - let response = evaluate_task.await?; - - this.update(&mut cx, |console, cx| { - console.add_message( - OutputEvent { - category: None, - output: response.result, - group: None, - variables_reference: Some(response.variables_reference), - source: None, - line: None, - column: None, - data: None, - }, - cx, - ); + let weak_console = cx.weak_entity(); + + window + .spawn(cx, |mut cx| async move { + let response = evaluate_task.await?; + + weak_console.update_in(&mut cx, |console, window, cx| { + console.add_message( + OutputEvent { + category: None, + output: response.result, + group: None, + variables_reference: Some(response.variables_reference), + source: None, + line: None, + column: None, + data: None, + }, + window, + cx, + ); - console.variable_list.update(cx, |variable_list, cx| { - variable_list.invalidate(cx); + console.variable_list.update(cx, |variable_list, cx| { + variable_list.invalidate(window, cx); + }) }) }) - }) - .detach_and_log_err(cx); + .detach_and_log_err(cx); } - fn render_console(&self, cx: &ViewContext) -> impl IntoElement { + fn render_console(&self, cx: &Context) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if self.console.read(cx).read_only(cx) { @@ -288,7 +293,7 @@ impl Console { ) } - fn render_query_bar(&self, cx: &ViewContext) -> impl IntoElement { + fn render_query_bar(&self, cx: &Context) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if self.console.read(cx).read_only(cx) { @@ -318,7 +323,7 @@ impl Console { } impl Render for Console { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .key_context("DebugConsole") .on_action(cx.listener(Self::evaluate)) @@ -333,15 +338,16 @@ impl Render for Console { } } -struct ConsoleQueryBarCompletionProvider(WeakView); +struct ConsoleQueryBarCompletionProvider(WeakEntity); impl CompletionProvider for ConsoleQueryBarCompletionProvider { fn completions( &self, - buffer: &Model, + buffer: &Entity, buffer_position: language::Anchor, _trigger: editor::CompletionContext, - cx: &mut ViewContext, + _window: &mut Window, + cx: &mut Context, ) -> gpui::Task>> { let Some(console) = self.0.upgrade() else { return Task::ready(Ok(Vec::new())); @@ -364,32 +370,32 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { fn resolve_completions( &self, - _buffer: Model, + _buffer: Entity, _completion_indices: Vec, _completions: Rc>>, - _cx: &mut ViewContext, + _cx: &mut Context, ) -> gpui::Task> { Task::ready(Ok(false)) } fn apply_additional_edits_for_completion( &self, - _buffer: Model, + _buffer: Entity, _completions: Rc>>, _completion_index: usize, _push_to_history: bool, - _cx: &mut ViewContext, + _cx: &mut Context, ) -> gpui::Task>> { Task::ready(Ok(None)) } fn is_completion_trigger( &self, - _buffer: &Model, + _buffer: &Entity, _position: language::Anchor, _text: &str, _trigger_in_words: bool, - _cx: &mut ViewContext, + _cx: &mut Context, ) -> bool { true } @@ -398,10 +404,10 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { impl ConsoleQueryBarCompletionProvider { fn variable_list_completions( &self, - console: &View, - buffer: &Model, + console: &Entity, + buffer: &Entity, buffer_position: language::Anchor, - cx: &mut ViewContext, + cx: &mut Context, ) -> gpui::Task>> { let (variables, string_matches) = console.update(cx, |console, cx| { let mut variables = HashMap::new(); @@ -474,10 +480,10 @@ impl ConsoleQueryBarCompletionProvider { fn client_completions( &self, - console: &View, - buffer: &Model, + console: &Entity, + buffer: &Entity, buffer_position: language::Anchor, - cx: &mut ViewContext, + cx: &mut Context, ) -> gpui::Task>> { let text = buffer.read(cx).text(); let start_position = buffer.read(cx).anchor_before(0); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9d588cd06e8138..73a66aa6a6fb35 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -14,8 +14,8 @@ use dap::{ TerminatedEvent, ThreadEvent, ThreadEventReason, }; use gpui::{ - actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, - Model, Subscription, Task, View, ViewContext, WeakView, + actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle, + Focusable, Subscription, Task, WeakEntity, }; use project::{ dap_store::{DapStore, DapStoreEvent}, @@ -113,31 +113,36 @@ impl ThreadStatus { pub struct DebugPanel { size: Pixels, - pane: View, + pane: Entity, focus_handle: FocusHandle, - dap_store: Model, - workspace: WeakView, + dap_store: Entity, + workspace: WeakEntity, _subscriptions: Vec, message_queue: HashMap>, - thread_states: BTreeMap<(DebugAdapterClientId, u64), Model>, + thread_states: BTreeMap<(DebugAdapterClientId, u64), Entity>, } impl DebugPanel { - pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> View { - cx.new_view(|cx| { - let pane = cx.new_view(|cx| { + pub fn new( + workspace: &Workspace, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + cx.new(|cx| { + let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.weak_handle(), workspace.project().clone(), Default::default(), None, None, + window, cx, ); pane.set_can_split(None); pane.set_can_navigate(true, cx); pane.display_nav_history_buttons(None); - pane.set_should_display_tab_bar(|_| true); + pane.set_should_display_tab_bar(|_window, _cx| true); pane.set_close_pane_if_empty(false, cx); pane @@ -148,12 +153,12 @@ impl DebugPanel { let _subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe(&pane, Self::handle_pane_event), - cx.subscribe(&dap_store, Self::on_dap_store_event), - cx.subscribe(&project, { - move |this: &mut Self, _, event, cx| match event { + cx.subscribe_in(&pane, window, Self::handle_pane_event), + cx.subscribe_in(&dap_store, window, Self::on_dap_store_event), + cx.subscribe_in(&project, window, { + move |this: &mut Self, _, event, window, cx| match event { project::Event::DebugClientStarted((session_id, client_id)) => { - this.handle_debug_client_started(session_id, client_id, cx); + this.handle_debug_client_started(session_id, client_id, window, cx); } project::Event::DebugClientEvent { session_id, @@ -161,7 +166,9 @@ impl DebugPanel { message, } => match message { Message::Event(event) => { - this.handle_debug_client_events(session_id, client_id, event, cx); + this.handle_debug_client_events( + session_id, client_id, event, window, cx, + ); } Message::Request(request) => { if StartDebugging::COMMAND == request.command { @@ -178,6 +185,7 @@ impl DebugPanel { client_id, request.seq, request.arguments.clone(), + window, cx, ); } @@ -194,7 +202,7 @@ impl DebugPanel { cx.notify(); } project::Event::SetDebugClient(set_debug_client) => { - this.handle_set_debug_panel_item(set_debug_client, cx); + this.handle_set_debug_panel_item(set_debug_client, window, cx); } _ => {} } @@ -220,7 +228,12 @@ impl DebugPanel { .update(cx, |this, _| this.remote_event_queue()) { while let Some(dap_event) = dap_event_queue.pop_front() { - debug_panel.on_dap_store_event(debug_panel.dap_store.clone(), &dap_event, cx); + debug_panel.on_dap_store_event( + &debug_panel.dap_store.clone(), + &dap_event, + window, + cx, + ); } } @@ -229,12 +242,12 @@ impl DebugPanel { } pub fn load( - workspace: WeakView, + workspace: WeakEntity, cx: AsyncWindowContext, - ) -> Task>> { + ) -> Task>> { cx.spawn(|mut cx| async move { - workspace.update(&mut cx, |workspace, cx| { - let debug_panel = DebugPanel::new(workspace, cx); + workspace.update_in(&mut cx, |workspace, window, cx| { + let debug_panel = DebugPanel::new(workspace, window, cx); cx.observe(&debug_panel, |_, debug_panel, cx| { let (has_active_session, support_step_back) = @@ -293,14 +306,11 @@ impl DebugPanel { } #[cfg(any(test, feature = "test-support"))] - pub fn dap_store(&self) -> Model { + pub fn dap_store(&self) -> Entity { self.dap_store.clone() } - pub fn active_debug_panel_item( - &self, - cx: &mut ViewContext, - ) -> Option> { + pub fn active_debug_panel_item(&self, cx: &Context) -> Option> { self.pane .read(cx) .active_item() @@ -311,8 +321,8 @@ impl DebugPanel { &self, client_id: &DebugAdapterClientId, thread_id: u64, - cx: &mut ViewContext, - ) -> Option> { + cx: &mut Context, + ) -> Option> { self.pane .read(cx) .items() @@ -326,9 +336,10 @@ impl DebugPanel { fn handle_pane_event( &mut self, - _: View, + _: &Entity, event: &pane::Event, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { pane::Event::RemovedItem { item } => { @@ -354,7 +365,7 @@ impl DebugPanel { pane::Event::AddItem { item } => { self.workspace .update(cx, |workspace, cx| { - item.added_to_pane(workspace, self.pane.clone(), cx) + item.added_to_pane(workspace, self.pane.clone(), window, cx) }) .ok(); } @@ -366,7 +377,7 @@ impl DebugPanel { if let Some(active_item) = self.pane.read(cx).active_item() { if let Some(debug_item) = active_item.downcast::() { debug_item.update(cx, |panel, cx| { - panel.go_to_current_stack_frame(cx); + panel.go_to_current_stack_frame(window, cx); }); } } @@ -381,7 +392,7 @@ impl DebugPanel { client_id: &DebugAdapterClientId, seq: u64, request_args: Option, - cx: &mut ViewContext, + cx: &mut Context, ) { let args = if let Some(args) = request_args { serde_json::from_value(args.clone()).ok() @@ -402,7 +413,8 @@ impl DebugPanel { client_id: &DebugAdapterClientId, seq: u64, request_args: Option, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let request_args = request_args.and_then(|request_args| { serde_json::from_value::(request_args).ok() @@ -474,6 +486,7 @@ impl DebugPanel { title: request_args.title, }, task::RevealStrategy::Always, + window, cx, ); @@ -568,7 +581,8 @@ impl DebugPanel { &self, session_id: &DebugSessionId, client_id: &DebugAdapterClientId, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let Some(session) = self .dap_store @@ -583,7 +597,7 @@ impl DebugPanel { let client_id = *client_id; let workspace = self.workspace.clone(); let request_type = session.configuration().request.clone(); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { let task = this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { store.initialize(&session_id, &client_id, cx) @@ -611,13 +625,14 @@ impl DebugPanel { task.await } else { - this.update(&mut cx, |this, cx| { + this.update_in(&mut cx, |this, window, cx| { workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx| { + workspace.toggle_modal(window, cx, |window, cx| { AttachModal::new( &session_id, &client_id, this.dap_store.clone(), + window, cx, ) }) @@ -651,13 +666,16 @@ impl DebugPanel { session_id: &DebugSessionId, client_id: &DebugAdapterClientId, event: &Events, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { Events::Initialized(event) => { self.handle_initialized_event(&session_id, &client_id, event, cx) } - Events::Stopped(event) => self.handle_stopped_event(&session_id, &client_id, event, cx), + Events::Stopped(event) => { + self.handle_stopped_event(&session_id, &client_id, event, window, cx) + } Events::Continued(event) => self.handle_continued_event(&client_id, event, cx), Events::Exited(event) => self.handle_exited_event(&client_id, event, cx), Events::Terminated(event) => { @@ -686,7 +704,7 @@ impl DebugPanel { session_id: &DebugSessionId, client_id: &DebugAdapterClientId, capabilities: &Option, - cx: &mut ViewContext, + cx: &mut Context, ) { if let Some(capabilities) = capabilities { self.dap_store.update(cx, |store, cx| { @@ -723,7 +741,7 @@ impl DebugPanel { &mut self, client_id: &DebugAdapterClientId, event: &ContinuedEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { cx.emit(DebugPanelEvent::Continued((*client_id, event.clone()))); } @@ -733,7 +751,8 @@ impl DebugPanel { session_id: &DebugSessionId, client_id: &DebugAdapterClientId, event: &StoppedEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let Some(thread_id) = event.thread_id else { return; @@ -753,14 +772,14 @@ impl DebugPanel { let session_name = SharedString::from(session_name); - cx.spawn({ + cx.spawn_in(window, { let event = event.clone(); |this, mut cx| async move { - let workspace = this.update(&mut cx, |this, cx| { + let workspace = this.update_in(&mut cx, |this, window, cx| { let thread_state = this .thread_states .entry((client_id, thread_id)) - .or_insert(cx.new_model(|_| ThreadState::default())) + .or_insert(cx.new(|_| ThreadState::default())) .clone(); thread_state.update(cx, |thread_state, _| { @@ -770,9 +789,9 @@ impl DebugPanel { let existing_item = this.debug_panel_item_by_client(&client_id, thread_id, cx); if existing_item.is_none() { - let debug_panel = cx.view().clone(); + let debug_panel = cx.entity().clone(); this.pane.update(cx, |pane, cx| { - let tab = cx.new_view(|cx| { + let tab = cx.new(|cx| { DebugPanelItem::new( debug_panel, this.workspace.clone(), @@ -782,11 +801,12 @@ impl DebugPanel { &client_id, session_name, thread_id, + window, cx, ) }); - pane.add_item(Box::new(tab), true, true, None, cx); + pane.add_item(Box::new(tab), true, true, None, window, cx); }); if let Some(message_queue) = this.message_queue.get(&client_id) { @@ -814,9 +834,9 @@ impl DebugPanel { this.workspace.clone() })?; - cx.update(|cx| { + cx.update(|window, cx| { workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(cx); + workspace.focus_panel::(window, cx); }) }) } @@ -828,7 +848,7 @@ impl DebugPanel { &mut self, client_id: &DebugAdapterClientId, event: &ThreadEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { let thread_id = event.thread_id; @@ -843,10 +863,8 @@ impl DebugPanel { } if event.reason == ThreadEventReason::Started { - self.thread_states.insert( - (*client_id, thread_id), - cx.new_model(|_| ThreadState::default()), - ); + self.thread_states + .insert((*client_id, thread_id), cx.new(|_| ThreadState::default())); } cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); @@ -857,7 +875,7 @@ impl DebugPanel { &mut self, client_id: &DebugAdapterClientId, _: &ExitedEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { cx.emit(DebugPanelEvent::Exited(*client_id)); } @@ -867,7 +885,7 @@ impl DebugPanel { session_id: &DebugSessionId, client_id: &DebugAdapterClientId, event: &Option, - cx: &mut ViewContext, + cx: &mut Context, ) { let restart_args = event.clone().and_then(|e| e.restart); @@ -904,7 +922,7 @@ impl DebugPanel { &mut self, client_id: &DebugAdapterClientId, event: &OutputEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { self.message_queue .entry(*client_id) @@ -916,16 +934,17 @@ impl DebugPanel { fn on_dap_store_event( &mut self, - _: Model, + _: &Entity, event: &project::dap_store::DapStoreEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { project::dap_store::DapStoreEvent::SetDebugPanelItem(set_debug_panel_item) => { - self.handle_set_debug_panel_item(set_debug_panel_item, cx); + self.handle_set_debug_panel_item(set_debug_panel_item, window, cx); } project::dap_store::DapStoreEvent::UpdateDebugAdapter(debug_adapter_update) => { - self.handle_debug_adapter_update(debug_adapter_update, cx); + self.handle_debug_adapter_update(debug_adapter_update, window, cx); } project::dap_store::DapStoreEvent::UpdateThreadStatus(thread_status_update) => { self.handle_thread_status_update(thread_status_update, cx); @@ -937,7 +956,7 @@ impl DebugPanel { pub(crate) fn handle_thread_status_update( &mut self, update: &proto::UpdateThreadStatus, - cx: &mut ViewContext, + cx: &mut Context, ) { if let Some(thread_state) = self.thread_states.get_mut(&( DebugAdapterClientId::from_proto(update.client_id), @@ -954,7 +973,8 @@ impl DebugPanel { pub(crate) fn handle_debug_adapter_update( &mut self, update: &UpdateDebugAdapter, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let client_id = DebugAdapterClientId::from_proto(update.client_id); let thread_id = update.thread_id; @@ -992,7 +1012,7 @@ impl DebugPanel { this.update_adapter(update, cx); if is_active_item { - this.go_to_current_stack_frame(cx); + this.go_to_current_stack_frame(window, cx); } }); } @@ -1001,13 +1021,14 @@ impl DebugPanel { pub(crate) fn handle_set_debug_panel_item( &mut self, payload: &SetDebuggerPanelItem, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let session_id = DebugSessionId::from_proto(payload.session_id); let client_id = DebugAdapterClientId::from_proto(payload.client_id); let thread_id = payload.thread_id; let thread_state = payload.thread_state.clone().unwrap(); - let thread_state = cx.new_model(|_| ThreadState::from_proto(thread_state)); + let thread_state = cx.new(|_| ThreadState::from_proto(thread_state)); let mut existing_item = self .pane @@ -1024,9 +1045,9 @@ impl DebugPanel { self.thread_states .insert((client_id, thread_id), thread_state.clone()); - let debug_panel = cx.view().clone(); + let debug_panel = cx.entity().clone(); let debug_panel_item = self.pane.update(cx, |pane, cx| { - let debug_panel_item = cx.new_view(|cx| { + let debug_panel_item = cx.new(|cx| { DebugPanelItem::new( debug_panel, self.workspace.clone(), @@ -1036,6 +1057,7 @@ impl DebugPanel { &client_id, payload.session_name.clone().into(), thread_id, + window, cx, ) }); @@ -1045,7 +1067,14 @@ impl DebugPanel { dap_store.add_client_to_session(session_id, client_id); }); - pane.add_item(Box::new(debug_panel_item.clone()), true, true, None, cx); + pane.add_item( + Box::new(debug_panel_item.clone()), + true, + true, + None, + window, + cx, + ); debug_panel_item }); @@ -1063,7 +1092,7 @@ impl DebugPanel { &mut self, client_id: &DebugAdapterClientId, event: &ModuleEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { cx.emit(DebugPanelEvent::Module((*client_id, event.clone()))); } @@ -1072,7 +1101,7 @@ impl DebugPanel { &mut self, client_id: &DebugAdapterClientId, event: &LoadedSourceEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { cx.emit(DebugPanelEvent::LoadedSource((*client_id, event.clone()))); } @@ -1082,7 +1111,7 @@ impl DebugPanel { session_id: &DebugSessionId, client_id: &DebugAdapterClientId, event: &CapabilitiesEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { self.dap_store.update(cx, |store, cx| { store.update_capabilities_for_client(session_id, client_id, &event.capabilities, cx); @@ -1096,14 +1125,14 @@ impl EventEmitter for DebugPanel {} impl EventEmitter for DebugPanel {} impl EventEmitter for DebugPanel {} -impl FocusableView for DebugPanel { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { +impl Focusable for DebugPanel { + fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Panel for DebugPanel { - fn pane(&self) -> Option> { + fn pane(&self) -> Option> { Some(self.pane.clone()) } @@ -1111,7 +1140,7 @@ impl Panel for DebugPanel { "DebugPanel" } - fn position(&self, _cx: &WindowContext) -> DockPosition { + fn position(&self, _window: &Window, _cx: &App) -> DockPosition { DockPosition::Bottom } @@ -1119,13 +1148,19 @@ impl Panel for DebugPanel { position == DockPosition::Bottom } - fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {} + fn set_position( + &mut self, + _position: DockPosition, + _window: &mut Window, + _cx: &mut Context, + ) { + } - fn size(&self, _cx: &WindowContext) -> Pixels { + fn size(&self, _window: &Window, _cx: &App) -> Pixels { self.size } - fn set_size(&mut self, size: Option, _cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, _window: &mut Window, _cx: &mut Context) { self.size = size.unwrap(); } @@ -1133,11 +1168,11 @@ impl Panel for DebugPanel { Some(proto::PanelId::DebugPanel) } - fn icon(&self, _cx: &WindowContext) -> Option { + fn icon(&self, _window: &Window, _cx: &App) -> Option { Some(IconName::Debug) } - fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str> { + fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> { if DebuggerSettings::get_global(cx).button { Some("Debug Panel") } else { @@ -1155,7 +1190,7 @@ impl Panel for DebugPanel { } impl Render for DebugPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .key_context("DebugPanel") .track_focus(&self.focus_handle) @@ -1181,8 +1216,8 @@ impl Render for DebugPanel { "Choose a debugger", ) .label_size(LabelSize::Small) - .on_click(move |_, cx| { - cx.dispatch_action(Start.boxed_clone()); + .on_click(move |_, _window, cx| { + cx.dispatch_action(&Start); }) ), ), diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index a645578f3fda1f..cb34644d692034 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -14,13 +14,12 @@ use dap::{ }; use editor::Editor; use gpui::{ - AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, - View, WeakView, + AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; use project::dap_store::DapStore; use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem, UpdateDebugAdapter}; use settings::Settings; -use ui::{prelude::*, Indicator, Tooltip, WindowContext}; +use ui::{prelude::*, Indicator, Tooltip}; use util::ResultExt as _; use workspace::{ item::{self, Item, ItemEvent}, @@ -63,68 +62,72 @@ impl ThreadItem { pub struct DebugPanelItem { thread_id: u64, - console: View, + console: Entity, focus_handle: FocusHandle, remote_id: Option, session_name: SharedString, - dap_store: Model, + dap_store: Entity, session_id: DebugSessionId, show_console_indicator: bool, - module_list: View, + module_list: Entity, active_thread_item: ThreadItem, - workspace: WeakView, + workspace: WeakEntity, client_id: DebugAdapterClientId, - thread_state: Model, - variable_list: View, + thread_state: Entity, + variable_list: Entity, _subscriptions: Vec, - stack_frame_list: View, - loaded_source_list: View, + stack_frame_list: Entity, + loaded_source_list: Entity, } impl DebugPanelItem { #[allow(clippy::too_many_arguments)] pub fn new( - debug_panel: View, - workspace: WeakView, - dap_store: Model, - thread_state: Model, + debug_panel: Entity, + workspace: WeakEntity, + dap_store: Entity, + thread_state: Entity, session_id: &DebugSessionId, client_id: &DebugAdapterClientId, session_name: SharedString, thread_id: u64, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); - let this = cx.view().clone(); - let stack_frame_list = cx.new_view(|cx| { + let this = cx.entity(); + + let stack_frame_list = cx.new(|cx| { StackFrameList::new( - &workspace, &this, &dap_store, client_id, session_id, thread_id, cx, + &workspace, &this, &dap_store, client_id, session_id, thread_id, window, cx, ) }); - let variable_list = cx.new_view(|cx| { + let variable_list = cx.new(|cx| { VariableList::new( &stack_frame_list, dap_store.clone(), &client_id, session_id, + window, cx, ) }); let module_list = - cx.new_view(|cx| ModuleList::new(dap_store.clone(), &client_id, &session_id, cx)); + cx.new(|cx| ModuleList::new(dap_store.clone(), &client_id, &session_id, cx)); let loaded_source_list = - cx.new_view(|cx| LoadedSourceList::new(&this, dap_store.clone(), &client_id, cx)); + cx.new(|cx| LoadedSourceList::new(&this, dap_store.clone(), &client_id, cx)); - let console = cx.new_view(|cx| { + let console = cx.new(|cx| { Console::new( &stack_frame_list, client_id, variable_list.clone(), dap_store.clone(), + window, cx, ) }); @@ -132,8 +135,8 @@ impl DebugPanelItem { cx.observe(&module_list, |_, _, cx| cx.notify()).detach(); let _subscriptions = vec![ - cx.subscribe(&debug_panel, { - move |this: &mut Self, _, event: &DebugPanelEvent, cx| { + cx.subscribe_in(&debug_panel, window, { + move |this: &mut Self, _, event: &DebugPanelEvent, window, cx| { match event { DebugPanelEvent::Stopped { client_id, @@ -144,7 +147,7 @@ impl DebugPanelItem { this.handle_thread_event(client_id, event, cx) } DebugPanelEvent::Output((client_id, event)) => { - this.handle_output_event(client_id, event, cx) + this.handle_output_event(client_id, event, window, cx) } DebugPanelEvent::Module((client_id, event)) => { this.handle_module_event(client_id, event, cx) @@ -198,7 +201,7 @@ impl DebugPanelItem { } } - pub(crate) fn to_proto(&self, project_id: u64, cx: &AppContext) -> SetDebuggerPanelItem { + pub(crate) fn to_proto(&self, project_id: u64, cx: &App) -> SetDebuggerPanelItem { let thread_state = Some(self.thread_state.read(cx).to_proto()); let variable_list = Some(self.variable_list.read(cx).to_proto()); let stack_frame_list = Some(self.stack_frame_list.read(cx).to_proto()); @@ -219,7 +222,7 @@ impl DebugPanelItem { } } - pub(crate) fn from_proto(&mut self, state: &SetDebuggerPanelItem, cx: &mut ViewContext) { + pub(crate) fn from_proto(&mut self, state: &SetDebuggerPanelItem, cx: &mut Context) { self.thread_state.update(cx, |thread_state, _| { let (status, stopped) = state .thread_state @@ -253,7 +256,7 @@ impl DebugPanelItem { cx.notify(); } - pub fn update_thread_state_status(&mut self, status: ThreadStatus, cx: &mut ViewContext) { + pub fn update_thread_state_status(&mut self, status: ThreadStatus, cx: &mut Context) { self.thread_state.update(cx, |thread_state, cx| { thread_state.status = status; @@ -276,7 +279,7 @@ impl DebugPanelItem { &mut self, client_id: &DebugAdapterClientId, event: &ContinuedEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { if self.should_skip_event(client_id, event.thread_id) { return; @@ -290,7 +293,7 @@ impl DebugPanelItem { client_id: &DebugAdapterClientId, event: &StoppedEvent, go_to_stack_frame: bool, - cx: &mut ViewContext, + cx: &mut Context, ) { if self.should_skip_event(client_id, event.thread_id.unwrap_or(self.thread_id)) { return; @@ -309,7 +312,7 @@ impl DebugPanelItem { &mut self, client_id: &DebugAdapterClientId, event: &ThreadEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { if self.should_skip_event(client_id, event.thread_id) { return; @@ -322,7 +325,8 @@ impl DebugPanelItem { &mut self, client_id: &DebugAdapterClientId, event: &OutputEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if self.should_skip_event(client_id, self.thread_id) { return; @@ -340,7 +344,7 @@ impl DebugPanelItem { } self.console.update(cx, |console, cx| { - console.add_message(event.clone(), cx); + console.add_message(event.clone(), window, cx); }); self.show_console_indicator = true; cx.notify(); @@ -350,7 +354,7 @@ impl DebugPanelItem { &mut self, client_id: &DebugAdapterClientId, event: &ModuleEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { if self.should_skip_event(client_id, self.thread_id) { return; @@ -365,7 +369,7 @@ impl DebugPanelItem { &mut self, client_id: &DebugAdapterClientId, event: &LoadedSourceEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { if self.should_skip_event(client_id, self.thread_id) { return; @@ -380,7 +384,7 @@ impl DebugPanelItem { fn handle_client_shutdown_event( &mut self, client_id: &DebugAdapterClientId, - cx: &mut ViewContext, + cx: &mut Context, ) { if self.should_skip_event(client_id, self.thread_id) { return; @@ -398,7 +402,7 @@ impl DebugPanelItem { fn handle_client_exited_and_terminated_event( &mut self, client_id: &DebugAdapterClientId, - cx: &mut ViewContext, + cx: &mut Context, ) { if Self::should_skip_event(self, client_id, self.thread_id) { return; @@ -416,7 +420,7 @@ impl DebugPanelItem { fn handle_capabilities_changed_event( &mut self, client_id: &DebugAdapterClientId, - cx: &mut ViewContext, + cx: &mut Context, ) { if Self::should_skip_event(self, client_id, self.thread_id) { return; @@ -437,11 +441,7 @@ impl DebugPanelItem { } } - pub(crate) fn update_adapter( - &mut self, - update: &UpdateDebugAdapter, - cx: &mut ViewContext, - ) { + pub(crate) fn update_adapter(&mut self, update: &UpdateDebugAdapter, cx: &mut Context) { if let Some(update_variant) = update.variant.as_ref() { match update_variant { proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => { @@ -482,41 +482,41 @@ impl DebugPanelItem { } #[cfg(any(test, feature = "test-support"))] - pub fn stack_frame_list(&self) -> &View { + pub fn stack_frame_list(&self) -> &Entity { &self.stack_frame_list } #[cfg(any(test, feature = "test-support"))] - pub fn console(&self) -> &View { + pub fn console(&self) -> &Entity { &self.console } #[cfg(any(test, feature = "test-support"))] - pub fn module_list(&self) -> &View { + pub fn module_list(&self) -> &Entity { &self.module_list } #[cfg(any(test, feature = "test-support"))] - pub fn variable_list(&self) -> &View { + pub fn variable_list(&self) -> &Entity { &self.variable_list } #[cfg(any(test, feature = "test-support"))] - pub fn thread_state(&self) -> &Model { + pub fn thread_state(&self) -> &Entity { &self.thread_state } #[cfg(any(test, feature = "test-support"))] - pub fn are_breakpoints_ignored(&self, cx: &AppContext) -> bool { + pub fn are_breakpoints_ignored(&self, cx: &App) -> bool { self.dap_store .read_with(cx, |dap, cx| dap.ignore_breakpoints(&self.session_id, cx)) } - pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { + pub fn capabilities(&self, cx: &mut Context) -> Capabilities { self.dap_store.read(cx).capabilities_by_id(&self.client_id) } - fn clear_highlights(&self, cx: &mut ViewContext) { + fn clear_highlights(&self, cx: &mut Context) { if let Some((_, project_path, _)) = self.dap_store.read(cx).active_debug_line() { self.workspace .update(cx, |workspace, cx| { @@ -536,7 +536,7 @@ impl DebugPanelItem { } } - pub fn go_to_current_stack_frame(&self, cx: &mut ViewContext) { + pub fn go_to_current_stack_frame(&self, window: &mut Window, cx: &mut Context) { self.stack_frame_list.update(cx, |stack_frame_list, cx| { if let Some(stack_frame) = stack_frame_list .stack_frames() @@ -545,7 +545,7 @@ impl DebugPanelItem { .cloned() { stack_frame_list - .select_stack_frame(&stack_frame, true, cx) + .select_stack_frame(&stack_frame, true, window, cx) .detach_and_log_err(cx); } }); @@ -555,7 +555,7 @@ impl DebugPanelItem { &self, label: &SharedString, thread_item: ThreadItem, - cx: &mut ViewContext, + cx: &mut Context, ) -> AnyElement { let has_indicator = matches!(thread_item, ThreadItem::Console) && self.show_console_indicator; @@ -574,7 +574,7 @@ impl DebugPanelItem { .child(Button::new(label.clone(), label.clone())) .when(has_indicator, |this| this.child(Indicator::dot())), ) - .on_click(cx.listener(move |this, _, cx| { + .on_click(cx.listener(move |this, _, _window, cx| { this.active_thread_item = thread_item.clone(); if matches!(this.active_thread_item, ThreadItem::Console) { @@ -586,7 +586,7 @@ impl DebugPanelItem { .into_any_element() } - pub fn continue_thread(&mut self, cx: &mut ViewContext) { + pub fn continue_thread(&mut self, cx: &mut Context) { self.update_thread_state_status(ThreadStatus::Running, cx); let task = self.dap_store.update(cx, |store, cx| { @@ -604,7 +604,7 @@ impl DebugPanelItem { .detach(); } - pub fn step_over(&mut self, cx: &mut ViewContext) { + pub fn step_over(&mut self, cx: &mut Context) { self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity; @@ -623,7 +623,7 @@ impl DebugPanelItem { .detach(); } - pub fn step_in(&mut self, cx: &mut ViewContext) { + pub fn step_in(&mut self, cx: &mut Context) { self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity; @@ -642,7 +642,7 @@ impl DebugPanelItem { .detach(); } - pub fn step_out(&mut self, cx: &mut ViewContext) { + pub fn step_out(&mut self, cx: &mut Context) { self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity; @@ -661,7 +661,7 @@ impl DebugPanelItem { .detach(); } - pub fn step_back(&mut self, cx: &mut ViewContext) { + pub fn step_back(&mut self, cx: &mut Context) { self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity; @@ -680,7 +680,7 @@ impl DebugPanelItem { .detach(); } - pub fn restart_client(&self, cx: &mut ViewContext) { + pub fn restart_client(&self, cx: &mut Context) { self.dap_store.update(cx, |store, cx| { store .restart(&self.client_id, None, cx) @@ -688,7 +688,7 @@ impl DebugPanelItem { }); } - pub fn pause_thread(&self, cx: &mut ViewContext) { + pub fn pause_thread(&self, cx: &mut Context) { self.dap_store.update(cx, |store, cx| { store .pause_thread(&self.client_id, self.thread_id, cx) @@ -696,7 +696,7 @@ impl DebugPanelItem { }); } - pub fn stop_thread(&self, cx: &mut ViewContext) { + pub fn stop_thread(&self, cx: &mut Context) { self.dap_store.update(cx, |store, cx| { store .terminate_threads( @@ -709,7 +709,7 @@ impl DebugPanelItem { }); } - pub fn disconnect_client(&self, cx: &mut ViewContext) { + pub fn disconnect_client(&self, cx: &mut Context) { self.dap_store.update(cx, |store, cx| { store .disconnect_client(&self.client_id, cx) @@ -717,7 +717,7 @@ impl DebugPanelItem { }); } - pub fn toggle_ignore_breakpoints(&mut self, cx: &mut ViewContext) { + pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context) { self.workspace .update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { @@ -732,8 +732,8 @@ impl DebugPanelItem { impl EventEmitter for DebugPanelItem {} -impl FocusableView for DebugPanelItem { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { +impl Focusable for DebugPanelItem { + fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } @@ -744,7 +744,8 @@ impl Item for DebugPanelItem { fn tab_content( &self, params: workspace::item::TabContentParams, - _: &WindowContext, + _window: &Window, + _: &App, ) -> AnyElement { Label::new(format!("{} - Thread {}", self.session_name, self.thread_id)) .color(if params.selected { @@ -755,7 +756,7 @@ impl Item for DebugPanelItem { .into_any_element() } - fn tab_tooltip_text(&self, cx: &AppContext) -> Option { + fn tab_tooltip_text(&self, cx: &App) -> Option { Some(SharedString::from(format!( "{} Thread {} - {:?}", self.session_name, @@ -777,16 +778,17 @@ impl FollowableItem for DebugPanelItem { self.remote_id } - fn to_state_proto(&self, _cx: &WindowContext) -> Option { + fn to_state_proto(&self, _window: &Window, _cx: &App) -> Option { None } fn from_state_proto( - _workspace: View, + _workspace: Entity, _remote_id: ViewId, _state: &mut Option, - _cx: &mut WindowContext, - ) -> Option>>> { + _window: &mut Window, + _cx: &mut App, + ) -> Option>>> { None } @@ -794,7 +796,8 @@ impl FollowableItem for DebugPanelItem { &self, _event: &Self::Event, _update: &mut Option, - _cx: &WindowContext, + _window: &Window, + _cx: &App, ) -> bool { // update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default())); @@ -803,21 +806,32 @@ impl FollowableItem for DebugPanelItem { fn apply_update_proto( &mut self, - _project: &Model, + _project: &Entity, _message: proto::update_view::Variant, - _cx: &mut ViewContext, + _window: &mut Window, + _cx: &mut Context, ) -> gpui::Task> { Task::ready(Ok(())) } - fn set_leader_peer_id(&mut self, _leader_peer_id: Option, _cx: &mut ViewContext) { + fn set_leader_peer_id( + &mut self, + _leader_peer_id: Option, + _window: &mut Window, + _cx: &mut Context, + ) { } fn to_follow_event(_event: &Self::Event) -> Option { None } - fn dedup(&self, existing: &Self, _cx: &WindowContext) -> Option { + fn dedup( + &self, + existing: &Self, + _window: &Window, + _cx: &App, + ) -> Option { if existing.client_id == self.client_id && existing.thread_id == self.thread_id { Some(item::Dedup::KeepExisting) } else { @@ -825,13 +839,13 @@ impl FollowableItem for DebugPanelItem { } } - fn is_project_item(&self, _cx: &WindowContext) -> bool { + fn is_project_item(&self, _window: &Window, _cx: &App) -> bool { true } } impl Render for DebugPanelItem { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let thread_status = self.thread_state.read(cx).status; let active_thread_item = &self.active_thread_item; @@ -858,21 +872,23 @@ impl Render for DebugPanelItem { this.child( IconButton::new("debug-pause", IconName::DebugPause) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.pause_thread(cx); })) - .tooltip(move |cx| Tooltip::text("Pause program", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Pause program")(window, cx) + }), ) } else { this.child( IconButton::new("debug-continue", IconName::DebugContinue) .icon_size(IconSize::Small) - .on_click( - cx.listener(|this, _, cx| this.continue_thread(cx)), - ) + .on_click(cx.listener(|this, _, _window, cx| { + this.continue_thread(cx) + })) .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| { - Tooltip::text("Continue program", cx) + .tooltip(move |window, cx| { + Tooltip::text("Continue program")(window, cx) }), ) } @@ -881,74 +897,86 @@ impl Render for DebugPanelItem { this.child( IconButton::new("debug-step-back", IconName::DebugStepBack) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.step_back(cx); })) .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Step back", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Step back")(window, cx) + }), ) }) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.step_over(cx); })) .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Step over", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Step over")(window, cx) + }), ) .child( IconButton::new("debug-step-in", IconName::DebugStepInto) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.step_in(cx); })) .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Step in", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Step in")(window, cx) + }), ) .child( IconButton::new("debug-step-out", IconName::DebugStepOut) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.step_out(cx); })) .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |cx| Tooltip::text("Step out", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Step out")(window, cx) + }), ) .child( IconButton::new("debug-restart", IconName::DebugRestart) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.restart_client(cx); })) .disabled( !capabilities.supports_restart_request.unwrap_or_default(), ) - .tooltip(move |cx| Tooltip::text("Restart", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Restart")(window, cx) + }), ) .child( IconButton::new("debug-stop", IconName::DebugStop) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.stop_thread(cx); })) .disabled( thread_status != ThreadStatus::Stopped && thread_status != ThreadStatus::Running, ) - .tooltip(move |cx| Tooltip::text("Stop", cx)), + .tooltip(move |window, cx| Tooltip::text("Stop")(window, cx)), ) .child( IconButton::new("debug-disconnect", IconName::DebugDisconnect) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.disconnect_client(cx); })) .disabled( thread_status == ThreadStatus::Exited || thread_status == ThreadStatus::Ended, ) - .tooltip(move |cx| Tooltip::text("Disconnect", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Disconnect")(window, cx) + }), ) .child( IconButton::new( @@ -962,14 +990,16 @@ impl Render for DebugPanelItem { }, ) .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(|this, _, _window, cx| { this.toggle_ignore_breakpoints(cx); })) .disabled( thread_status == ThreadStatus::Exited || thread_status == ThreadStatus::Ended, ) - .tooltip(move |cx| Tooltip::text("Ignore breakpoints", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Ignore breakpoints")(window, cx) + }), ), ) .child( diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index fe6e1340a67fec..e79233a2e0d214 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,9 +1,8 @@ use dap::debugger_settings::DebuggerSettings; use debugger_panel::{DebugPanel, ToggleFocus}; use debugger_panel_item::DebugPanelItem; -use gpui::AppContext; +use gpui::App; use settings::Settings; -use ui::ViewContext; use workspace::{ Continue, Pause, Restart, ShutdownDebugAdapters, Start, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, Workspace, @@ -21,118 +20,111 @@ pub mod variable_list; #[cfg(test)] mod tests; -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { DebuggerSettings::register(cx); workspace::FollowableViewRegistry::register::(cx); - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { - workspace - .register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); - }) - .register_action(|workspace: &mut Workspace, _: &Start, cx| { - tasks_ui::toggle_modal(workspace, None, task::TaskModal::DebugModal, cx) - .detach(); - }) - .register_action(|workspace: &mut Workspace, _: &ShutdownDebugAdapters, cx| { + cx.observe_new(|workspace: &mut Workspace, window, _cx| { + let Some(_) = window else { + return; + }; + + workspace + .register_action(|workspace, _: &ToggleFocus, window, cx| { + workspace.toggle_panel_focus::(window, cx); + }) + .register_action(|workspace: &mut Workspace, _: &Start, window, cx| { + tasks_ui::toggle_modal(workspace, None, task::TaskModal::DebugModal, window, cx) + .detach(); + }) + .register_action( + |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| { workspace.project().update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { store.shutdown_sessions(cx).detach(); }) }) - }) - .register_action(|workspace: &mut Workspace, _: &Stop, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); + }, + ) + .register_action(|workspace: &mut Workspace, _: &Stop, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; - active_item.update(cx, |item, cx| item.stop_thread(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &Continue, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); + active_item.update(cx, |item, cx| item.stop_thread(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &Continue, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; - active_item.update(cx, |item, cx| item.continue_thread(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &StepInto, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); + active_item.update(cx, |item, cx| item.continue_thread(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &StepInto, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; - active_item.update(cx, |item, cx| item.step_in(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &StepBack, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); + active_item.update(cx, |item, cx| item.step_in(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &StepBack, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; - active_item.update(cx, |item, cx| item.step_back(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &StepOut, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); + active_item.update(cx, |item, cx| item.step_back(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &StepOut, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; - active_item.update(cx, |item, cx| item.step_out(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &StepOver, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); + active_item.update(cx, |item, cx| item.step_out(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &StepOver, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; - active_item.update(cx, |item, cx| item.step_over(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &Restart, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); + active_item.update(cx, |item, cx| item.step_over(cx)) + }); + }) + .register_action(|workspace: &mut Workspace, _: &Restart, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; - active_item.update(cx, |item, cx| item.restart_client(cx)) - }); - }) - .register_action( - |workspace: &mut Workspace, _: &ToggleIgnoreBreakpoints, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) - }); - }, - ) - .register_action(|workspace: &mut Workspace, _: &Pause, cx| { + active_item.update(cx, |item, cx| item.restart_client(cx)) + }); + }) + .register_action( + |workspace: &mut Workspace, _: &ToggleIgnoreBreakpoints, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |panel, cx| { @@ -140,10 +132,21 @@ pub fn init(cx: &mut AppContext) { return; }; - active_item.update(cx, |item, cx| item.pause_thread(cx)) + active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) }); + }, + ) + .register_action(|workspace: &mut Workspace, _: &Pause, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + + debug_panel.update(cx, |panel, cx| { + let Some(active_item) = panel.active_debug_panel_item(cx) else { + return; + }; + + active_item.update(cx, |item, cx| item.pause_thread(cx)) }); - }, - ) + }); + }) .detach(); } diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs index 761a33be05e5d4..90726b3615c1ca 100644 --- a/crates/debugger_ui/src/loaded_source_list.rs +++ b/crates/debugger_ui/src/loaded_source_list.rs @@ -1,8 +1,6 @@ use anyhow::Result; use dap::{client::DebugAdapterClientId, LoadedSourceEvent, Source}; -use gpui::{ - list, AnyElement, FocusHandle, FocusableView, ListState, Model, Subscription, Task, View, -}; +use gpui::{list, AnyElement, Entity, FocusHandle, Focusable, ListState, Subscription, Task}; use project::dap_store::DapStore; use ui::prelude::*; @@ -12,27 +10,34 @@ pub struct LoadedSourceList { list: ListState, sources: Vec, focus_handle: FocusHandle, - dap_store: Model, + dap_store: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, } impl LoadedSourceList { pub fn new( - debug_panel_item: &View, - dap_store: Model, + debug_panel_item: &Entity, + dap_store: Entity, client_id: &DebugAdapterClientId, - cx: &mut ViewContext, + cx: &mut Context, ) -> Self { - let weakview = cx.view().downgrade(); + let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); - let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { - weakview - .upgrade() - .map(|view| view.update(cx, |this, cx| this.render_entry(ix, cx))) - .unwrap_or(div().into_any()) - }); + let list = ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, _window, cx| { + weak_entity + .upgrade() + .map(|loaded_sources| { + loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx)) + }) + .unwrap_or(div().into_any()) + }, + ); let _subscriptions = vec![cx.subscribe(debug_panel_item, Self::handle_debug_panel_item_event)]; @@ -49,9 +54,9 @@ impl LoadedSourceList { fn handle_debug_panel_item_event( &mut self, - _: View, + _: Entity, event: &debugger_panel_item::DebugPanelItemEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { match event { DebugPanelItemEvent::Stopped { .. } => { @@ -61,11 +66,7 @@ impl LoadedSourceList { } } - pub fn on_loaded_source_event( - &mut self, - event: &LoadedSourceEvent, - cx: &mut ViewContext, - ) { + pub fn on_loaded_source_event(&mut self, event: &LoadedSourceEvent, cx: &mut Context) { match event.reason { dap::LoadedSourceEventReason::New => self.sources.push(event.source.clone()), dap::LoadedSourceEventReason::Changed => { @@ -97,7 +98,7 @@ impl LoadedSourceList { cx.notify(); } - fn fetch_loaded_sources(&self, cx: &mut ViewContext) -> Task> { + fn fetch_loaded_sources(&self, cx: &mut Context) -> Task> { let task = self .dap_store .update(cx, |store, cx| store.loaded_sources(&self.client_id, cx)); @@ -114,7 +115,7 @@ impl LoadedSourceList { }) } - fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { let source = &self.sources[ix]; v_flex() @@ -139,14 +140,14 @@ impl LoadedSourceList { } } -impl FocusableView for LoadedSourceList { - fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { +impl Focusable for LoadedSourceList { + fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for LoadedSourceList { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { div() .size_full() .p_1() diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 67b09d54e02b80..eb165e5211f266 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -3,7 +3,7 @@ use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, Module, ModuleEvent, }; -use gpui::{list, AnyElement, FocusHandle, FocusableView, ListState, Model, Task}; +use gpui::{list, AnyElement, Entity, FocusHandle, Focusable, ListState, Task}; use project::dap_store::DapStore; use rpc::proto::{DebuggerModuleList, UpdateDebugAdapter}; use ui::prelude::*; @@ -13,27 +13,32 @@ pub struct ModuleList { list: ListState, modules: Vec, focus_handle: FocusHandle, - dap_store: Model, + dap_store: Entity, client_id: DebugAdapterClientId, session_id: DebugSessionId, } impl ModuleList { pub fn new( - dap_store: Model, + dap_store: Entity, client_id: &DebugAdapterClientId, session_id: &DebugSessionId, - cx: &mut ViewContext, + cx: &mut Context, ) -> Self { - let weakview = cx.view().downgrade(); + let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); - let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { - weakview - .upgrade() - .map(|view| view.update(cx, |this, cx| this.render_entry(ix, cx))) - .unwrap_or(div().into_any()) - }); + let list = ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, _window, cx| { + weak_entity + .upgrade() + .map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx))) + .unwrap_or(div().into_any()) + }, + ); let this = Self { list, @@ -53,7 +58,7 @@ impl ModuleList { pub(crate) fn set_from_proto( &mut self, module_list: &DebuggerModuleList, - cx: &mut ViewContext, + cx: &mut Context, ) { self.modules = module_list .modules @@ -78,7 +83,7 @@ impl ModuleList { } } - pub fn on_module_event(&mut self, event: &ModuleEvent, cx: &mut ViewContext) { + pub fn on_module_event(&mut self, event: &ModuleEvent, cx: &mut Context) { match event.reason { dap::ModuleEventReason::New => self.modules.push(event.module.clone()), dap::ModuleEventReason::Changed => { @@ -102,7 +107,7 @@ impl ModuleList { cx.background_executor().spawn(task).detach(); } - fn fetch_modules(&self, cx: &mut ViewContext) -> Task> { + fn fetch_modules(&self, cx: &mut Context) -> Task> { let task = self .dap_store .update(cx, |store, cx| store.modules(&self.client_id, cx)); @@ -120,7 +125,7 @@ impl ModuleList { }) } - fn propagate_updates(&self, cx: &ViewContext) { + fn propagate_updates(&self, cx: &Context) { if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { let request = UpdateDebugAdapter { session_id: self.session_id.to_proto(), @@ -136,7 +141,7 @@ impl ModuleList { } } - fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { let module = &self.modules[ix]; v_flex() @@ -156,14 +161,14 @@ impl ModuleList { } } -impl FocusableView for ModuleList { - fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { +impl Focusable for ModuleList { + fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for ModuleList { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { div() .size_full() .p_1() diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index d4ff83682501c6..b4dd9c834c9ab3 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -6,13 +6,12 @@ use dap::proto_conversions::ProtoConversion; use dap::session::DebugSessionId; use dap::StackFrame; use gpui::{ - list, AnyElement, EventEmitter, FocusHandle, ListState, Subscription, Task, View, WeakView, + list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, + WeakEntity, }; -use gpui::{FocusableView, Model}; use project::dap_store::DapStore; use project::ProjectPath; use rpc::proto::{DebuggerStackFrameList, UpdateDebugAdapter}; -use ui::ViewContext; use ui::{prelude::*, Tooltip}; use util::ResultExt; use workspace::Workspace; @@ -31,11 +30,11 @@ pub struct StackFrameList { list: ListState, focus_handle: FocusHandle, session_id: DebugSessionId, - dap_store: Model, + dap_store: Entity, current_stack_frame_id: u64, stack_frames: Vec, entries: Vec, - workspace: WeakView, + workspace: WeakEntity, client_id: DebugAdapterClientId, _subscriptions: Vec, fetch_stack_frames_task: Option>>, @@ -48,27 +47,39 @@ pub enum StackFrameEntry { } impl StackFrameList { + #[allow(clippy::too_many_arguments)] pub fn new( - workspace: &WeakView, - debug_panel_item: &View, - dap_store: &Model, + workspace: &WeakEntity, + debug_panel_item: &Entity, + dap_store: &Entity, client_id: &DebugAdapterClientId, session_id: &DebugSessionId, thread_id: u64, - cx: &mut ViewContext, + window: &Window, + cx: &mut Context, ) -> Self { - let weakview = cx.view().downgrade(); + let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); - let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { - weakview - .upgrade() - .map(|view| view.update(cx, |this, cx| this.render_entry(ix, cx))) - .unwrap_or(div().into_any()) - }); + let list = ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, _window, cx| { + weak_entity + .upgrade() + .map(|stack_frame_list| { + stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx)) + }) + .unwrap_or(div().into_any()) + }, + ); - let _subscriptions = - vec![cx.subscribe(debug_panel_item, Self::handle_debug_panel_item_event)]; + let _subscriptions = vec![cx.subscribe_in( + debug_panel_item, + window, + Self::handle_debug_panel_item_event, + )]; Self { list, @@ -102,7 +113,7 @@ impl StackFrameList { pub(crate) fn set_from_proto( &mut self, stack_frame_list: DebuggerStackFrameList, - cx: &mut ViewContext, + cx: &mut Context, ) { self.thread_id = stack_frame_list.thread_id; self.client_id = DebugAdapterClientId::from_proto(stack_frame_list.client_id); @@ -135,20 +146,21 @@ impl StackFrameList { fn handle_debug_panel_item_event( &mut self, - _: View, + _: &Entity, event: &debugger_panel_item::DebugPanelItemEvent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { Stopped { go_to_stack_frame } => { - self.fetch_stack_frames(*go_to_stack_frame, cx); + self.fetch_stack_frames(*go_to_stack_frame, window, cx); } _ => {} } } - pub fn invalidate(&mut self, cx: &mut ViewContext) { - self.fetch_stack_frames(true, cx); + pub fn invalidate(&mut self, window: &mut Window, cx: &mut Context) { + self.fetch_stack_frames(true, window, cx); } fn build_entries(&mut self) { @@ -180,7 +192,12 @@ impl StackFrameList { self.list.reset(self.entries.len()); } - fn fetch_stack_frames(&mut self, go_to_stack_frame: bool, cx: &mut ViewContext) { + fn fetch_stack_frames( + &mut self, + go_to_stack_frame: bool, + window: &Window, + cx: &mut Context, + ) { // If this is a remote debug session we never need to fetch stack frames ourselves // because the host will fetch and send us stack frames whenever there's a stop event if self.dap_store.read(cx).as_remote().is_some() { @@ -191,10 +208,10 @@ impl StackFrameList { store.stack_frames(&self.client_id, self.thread_id, cx) }); - self.fetch_stack_frames_task = Some(cx.spawn(|this, mut cx| async move { + self.fetch_stack_frames_task = Some(cx.spawn_in(window, |this, mut cx| async move { let mut stack_frames = task.await?; - let task = this.update(&mut cx, |this, cx| { + let task = this.update_in(&mut cx, |this, window, cx| { std::mem::swap(&mut this.stack_frames, &mut stack_frames); this.build_entries(); @@ -207,7 +224,7 @@ impl StackFrameList { .cloned() .ok_or_else(|| anyhow!("No stack frame found to select"))?; - anyhow::Ok(this.select_stack_frame(&stack_frame, go_to_stack_frame, cx)) + anyhow::Ok(this.select_stack_frame(&stack_frame, go_to_stack_frame, window, cx)) })?; task?.await?; @@ -222,7 +239,8 @@ impl StackFrameList { &mut self, stack_frame: &StackFrame, go_to_stack_frame: bool, - cx: &mut ViewContext, + window: &Window, + cx: &mut Context, ) -> Task> { self.current_stack_frame_id = stack_frame.id; @@ -253,12 +271,19 @@ impl StackFrameList { return Task::ready(Err(anyhow!("Project path not found"))); }; - cx.spawn({ + cx.spawn_in(window, { let client_id = self.client_id; move |this, mut cx| async move { - this.update(&mut cx, |this, cx| { + this.update_in(&mut cx, |this, window, cx| { this.workspace.update(cx, |workspace, cx| { - workspace.open_path_preview(project_path.clone(), None, false, true, cx) + workspace.open_path_preview( + project_path.clone(), + None, + false, + true, + window, + cx, + ) }) })?? .await?; @@ -275,7 +300,7 @@ impl StackFrameList { pub fn project_path_from_stack_frame( &self, stack_frame: &StackFrame, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option { let path = stack_frame.source.as_ref().and_then(|s| s.path.as_ref())?; @@ -288,7 +313,7 @@ impl StackFrameList { .ok()? } - pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut ViewContext) { + pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context) { self.dap_store.update(cx, |store, cx| { store .restart_stack_frame(&self.client_id, stack_frame_id, cx) @@ -296,11 +321,7 @@ impl StackFrameList { }); } - fn render_normal_entry( - &self, - stack_frame: &StackFrame, - cx: &mut ViewContext, - ) -> AnyElement { + fn render_normal_entry(&self, stack_frame: &StackFrame, cx: &mut Context) -> AnyElement { let source = stack_frame.source.clone(); let is_selected_frame = stack_frame.id == self.current_stack_frame_id; @@ -330,8 +351,8 @@ impl StackFrameList { .id(("stack-frame", stack_frame.id)) .tooltip({ let formatted_path = formatted_path.clone(); - move |cx| { - cx.new_view(|_| { + move |_window, app| { + app.new(|_| { let mut tooltip = Tooltip::new(formatted_path.clone()); if let Some(origin) = &origin { @@ -349,8 +370,8 @@ impl StackFrameList { }) .on_click(cx.listener({ let stack_frame = stack_frame.clone(); - move |this, _, cx| { - this.select_stack_frame(&stack_frame, true, cx) + move |this, _, window, cx| { + this.select_stack_frame(&stack_frame, true, window, cx) .detach_and_log_err(cx); } })) @@ -400,11 +421,13 @@ impl StackFrameList { .icon_size(IconSize::Small) .on_click(cx.listener({ let stack_frame_id = stack_frame.id; - move |this, _, cx| { + move |this, _, _window, cx| { this.restart_stack_frame(stack_frame_id, cx); } })) - .tooltip(move |cx| Tooltip::text("Restart Stack Frame", cx)), + .tooltip(move |window, cx| { + Tooltip::text("Restart Stack Frame")(window, cx) + }), ), ) }, @@ -416,7 +439,7 @@ impl StackFrameList { &mut self, ix: usize, stack_frames: &Vec, - cx: &mut ViewContext, + cx: &mut Context, ) { self.entries.splice( ix..ix + 1, @@ -432,7 +455,7 @@ impl StackFrameList { &self, ix: usize, stack_frames: &Vec, - cx: &mut ViewContext, + cx: &mut Context, ) -> AnyElement { let first_stack_frame = &stack_frames[0]; @@ -445,7 +468,7 @@ impl StackFrameList { .p_1() .on_click(cx.listener({ let stack_frames = stack_frames.clone(); - move |this, _, cx| { + move |this, _, _window, cx| { this.expand_collapsed_entry(ix, &stack_frames, cx); } })) @@ -468,7 +491,7 @@ impl StackFrameList { .into_any() } - fn render_entry(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + fn render_entry(&self, ix: usize, cx: &mut Context) -> AnyElement { match &self.entries[ix] { StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx), StackFrameEntry::Collapsed(stack_frames) => { @@ -479,7 +502,7 @@ impl StackFrameList { } impl Render for StackFrameList { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div() .size_full() .p_1() @@ -487,8 +510,8 @@ impl Render for StackFrameList { } } -impl FocusableView for StackFrameList { - fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { +impl Focusable for StackFrameList { + fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle { self.focus_handle.clone() } } diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 2ab4e9740da399..6cc3265f100d0f 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -1,4 +1,4 @@ -use gpui::{Model, TestAppContext, View, WindowHandle}; +use gpui::{Entity, TestAppContext, WindowHandle}; use project::Project; use settings::SettingsStore; use terminal_view::terminal_panel::TerminalPanel; @@ -32,38 +32,39 @@ pub fn init_test(cx: &mut gpui::TestAppContext) { } pub async fn init_test_workspace( - project: &Model, + project: &Entity, cx: &mut TestAppContext, ) -> WindowHandle { - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace_handle = + cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let debugger_panel = window - .update(cx, |_, cx| cx.spawn(DebugPanel::load)) + let debugger_panel = workspace_handle + .update(cx, |_, window, cx| cx.spawn_in(window, DebugPanel::load)) .unwrap() .await .expect("Failed to load debug panel"); - let terminal_panel = window - .update(cx, |_, cx| cx.spawn(TerminalPanel::load)) + let terminal_panel = workspace_handle + .update(cx, |_, window, cx| cx.spawn_in(window, TerminalPanel::load)) .unwrap() .await .expect("Failed to load terminal panel"); - window - .update(cx, |workspace, cx| { - workspace.add_panel(debugger_panel, cx); - workspace.add_panel(terminal_panel, cx); + workspace_handle + .update(cx, |workspace, window, cx| { + workspace.add_panel(debugger_panel, window, cx); + workspace.add_panel(terminal_panel, window, cx); }) .unwrap(); - window + workspace_handle } pub fn active_debug_panel_item( workspace: WindowHandle, cx: &mut TestAppContext, -) -> View { +) -> Entity { workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index dfdbfd0bddf52d..8cbc84e185e6d3 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -77,7 +77,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te // assert we didn't show the attach modal workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { assert!(workspace.active_modal::(cx).is_none()); }) .unwrap(); @@ -160,7 +160,7 @@ async fn test_show_attach_modal_and_select_process( // assert we show the attach modal workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let attach_modal = workspace.active_modal::(cx).unwrap(); let names = attach_modal.update(cx, |modal, cx| attach_modal::procss_names(&modal, cx)); @@ -177,7 +177,7 @@ async fn test_show_attach_modal_and_select_process( // assert attach modal was dismissed workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { assert!(workspace.active_modal::(cx).is_none()); }) .unwrap(); @@ -255,7 +255,7 @@ async fn test_shutdown_session_when_modal_is_dismissed( // assert we show the attach modal workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let attach_modal = workspace.active_modal::(cx).unwrap(); let names = attach_modal.update(cx, |modal, cx| attach_modal::procss_names(&modal, cx)); @@ -272,7 +272,7 @@ async fn test_shutdown_session_when_modal_is_dismissed( // assert attach modal was dismissed workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { assert!(workspace.active_modal::(cx).is_none()); }) .unwrap(); diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index be0b9c0f78869c..14caf9667cb6dd 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -106,7 +106,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp // assert we have output from before the thread stopped workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -151,7 +151,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp // assert we have output from before and after the thread stopped workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -740,13 +740,15 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp cx.run_until_parked(); - active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - debug_panel_item.console().update(cx, |console, cx| { - console.query_bar().update(cx, |query_bar, cx| { - query_bar.set_text(format!("$variable1 = {}", NEW_VALUE), cx); - }); + active_debug_panel_item(workspace, cx).update_in(cx, |debug_panel_item, window, cx| { + debug_panel_item.console().update(cx, |console, item_cx| { + console + .query_bar() + .update(item_cx, |query_bar, console_cx| { + query_bar.set_text(format!("$variable1 = {}", NEW_VALUE), window, console_cx); + }); - console.evaluate(&menu::Confirm, cx); + console.evaluate(&menu::Confirm, window, item_cx); }); }); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 411453e3189f40..4aaccf08f104c4 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -82,7 +82,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test // assert we don't have a debug panel item yet workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |this, cx| { @@ -108,7 +108,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test // assert we added a debug panel item workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -133,7 +133,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test // assert we don't have a debug panel item anymore because the client shutdown workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |this, cx| { @@ -199,7 +199,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( // assert we don't have a debug panel item yet workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |this, cx| { @@ -225,7 +225,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( // assert we added a debug panel item workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -256,7 +256,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( // assert we added a debug panel item workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -281,7 +281,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( // assert we don't have a debug panel item anymore because the client shutdown workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |this, cx| { @@ -347,7 +347,7 @@ async fn test_client_can_open_multiple_thread_panels( // assert we don't have a debug panel item yet workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |this, cx| { @@ -373,7 +373,7 @@ async fn test_client_can_open_multiple_thread_panels( // assert we added a debug panel item workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -404,7 +404,7 @@ async fn test_client_can_open_multiple_thread_panels( // assert we added a debug panel item and the new one is active workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -429,7 +429,7 @@ async fn test_client_can_open_multiple_thread_panels( // assert we don't have a debug panel item anymore because the client shutdown workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |this, cx| { @@ -517,7 +517,7 @@ async fn test_handle_successful_run_in_terminal_reverse_request( ); workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let terminal_panel = workspace.panel::(cx).unwrap(); let panel = terminal_panel.read(cx).pane().unwrap().read(cx); @@ -623,7 +623,7 @@ async fn test_handle_error_run_in_terminal_reverse_request( ); workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let terminal_panel = workspace.panel::(cx).unwrap(); assert_eq!( @@ -990,7 +990,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved( let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let worktree_id = workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }) @@ -1022,12 +1022,13 @@ async fn test_send_breakpoints_when_editor_has_been_saved( .await .unwrap(); - let (editor, cx) = cx.add_window_view(|cx| { + let (editor, cx) = cx.add_window_view(|window, cx| { Editor::new( EditorMode::Full, MultiBuffer::build_from_buffer(buffer, cx), Some(project.clone()), true, + window, cx, ) }); @@ -1094,9 +1095,9 @@ async fn test_send_breakpoints_when_editor_has_been_saved( }) .await; - editor.update(cx, |editor, cx| { - editor.move_down(&actions::MoveDown, cx); - editor.toggle_breakpoint(&actions::ToggleBreakpoint, cx); + editor.update_in(cx, |editor, window, cx| { + editor.move_down(&actions::MoveDown, window, cx); + editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); }); cx.run_until_parked(); @@ -1134,13 +1135,15 @@ async fn test_send_breakpoints_when_editor_has_been_saved( }) .await; - editor.update(cx, |editor, cx| { - editor.move_up(&actions::MoveUp, cx); - editor.insert("new text\n", cx); + editor.update_in(cx, |editor, window, cx| { + editor.move_up(&actions::MoveUp, window, cx); + editor.insert("new text\n", window, cx); }); editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .await .unwrap(); @@ -1181,7 +1184,7 @@ async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let worktree_id = workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { workspace.project().update(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }) @@ -1333,7 +1336,7 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails( }); let (session, client) = task.await.unwrap(); - let session_id = cx.update(|cx| session.read(cx).id()); + let session_id = cx.update(|_window, cx| session.read(cx).id()); client .on_request::(move |_, _| { diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 415878bb476a6f..87c9e675588476 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -157,7 +157,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( cx.run_until_parked(); workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -321,7 +321,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC cx.run_until_parked(); workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -345,7 +345,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC assert_eq!( vec![2..3], editors[0].update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); + let snapshot = editor.snapshot(window, cx); editor .highlighted_rows::() @@ -361,7 +361,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC .unwrap(); let stack_frame_list = workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -373,14 +373,14 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC // select second stack frame stack_frame_list - .update(cx, |stack_frame_list, cx| { - stack_frame_list.select_stack_frame(&stack_frames[1], true, cx) + .update_in(cx, |stack_frame_list, window, cx| { + stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx) }) .await .unwrap(); workspace - .update(cx, |workspace, cx| { + .update(cx, |workspace, window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -404,7 +404,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC assert_eq!( vec![0..1], editors[0].update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); + let snapshot = editor.snapshot(window, cx); editor .highlighted_rows::() diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 25a866d6072ef6..2d593b08f82be3 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -9,7 +9,7 @@ use dap::{ requests::{Disconnect, Initialize, Launch, Scopes, StackTrace, Variables}, Scope, StackFrame, Variable, }; -use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use gpui::{BackgroundExecutor, Focusable, TestAppContext, VisualTestContext}; use menu::{SelectFirst, SelectNext}; use project::{FakeFs, Project}; use serde_json::json; @@ -744,8 +744,11 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp cx.run_until_parked(); - active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - debug_panel_item.variable_list().focus_handle(cx).focus(cx); + active_debug_panel_item(workspace, cx).update_in(cx, |debug_panel_item, window, cx| { + debug_panel_item + .variable_list() + .focus_handle(cx) + .focus(window); }); cx.dispatch_action(SelectFirst); diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 855870d260367d..d33fe62c9dfd6a 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -6,9 +6,8 @@ use dap::{ }; use editor::{actions::SelectAll, Editor, EditorEvent}; use gpui::{ - actions, anchored, deferred, list, AnyElement, ClipboardItem, DismissEvent, FocusHandle, - FocusableView, Hsla, ListOffset, ListState, Model, MouseDownEvent, Point, Subscription, Task, - View, + actions, anchored, deferred, list, AnyElement, ClipboardItem, Context, DismissEvent, Entity, + FocusHandle, Focusable, Hsla, ListOffset, ListState, MouseDownEvent, Point, Subscription, Task, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; use project::dap_store::DapStore; @@ -331,41 +330,47 @@ type ScopeId = u64; pub struct VariableList { list: ListState, focus_handle: FocusHandle, - dap_store: Model, + dap_store: Entity, session_id: DebugSessionId, open_entries: Vec, client_id: DebugAdapterClientId, - set_variable_editor: View, + set_variable_editor: Entity, _subscriptions: Vec, selection: Option, - stack_frame_list: View, + stack_frame_list: Entity, scopes: HashMap>, set_variable_state: Option, fetch_variables_task: Option>>, entries: HashMap>, variables: BTreeMap<(StackFrameId, ScopeId), ScopeVariableIndex>, - open_context_menu: Option<(View, Point, Subscription)>, + open_context_menu: Option<(Entity, Point, Subscription)>, } impl VariableList { pub fn new( - stack_frame_list: &View, - dap_store: Model, + stack_frame_list: &Entity, + dap_store: Entity, client_id: &DebugAdapterClientId, session_id: &DebugSessionId, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { - let weakview = cx.view().downgrade(); + let weak_variable_list = cx.weak_entity(); let focus_handle = cx.focus_handle(); - let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { - weakview - .upgrade() - .map(|view| view.update(cx, |this, cx| this.render_entry(ix, cx))) - .unwrap_or(div().into_any()) - }); + let list = ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, _window, cx| { + weak_variable_list + .upgrade() + .map(|var_list| var_list.update(cx, |this, cx| this.render_entry(ix, cx))) + .unwrap_or(div().into_any()) + }, + ); - let set_variable_editor = cx.new_view(Editor::single_line); + let set_variable_editor = cx.new(|cx| Editor::single_line(window, cx)); cx.subscribe( &set_variable_editor, @@ -432,7 +437,7 @@ impl VariableList { pub(crate) fn set_from_proto( &mut self, state: &proto::DebuggerVariableList, - cx: &mut ViewContext, + cx: &mut Context, ) { self.variables = state .variables @@ -501,9 +506,9 @@ impl VariableList { fn handle_stack_frame_list_events( &mut self, - _: View, + _: Entity, event: &StackFrameListEvent, - cx: &mut ViewContext, + cx: &mut Context, ) { match event { StackFrameListEvent::SelectedStackFrameChanged => { @@ -552,7 +557,7 @@ impl VariableList { .collect() } - pub fn completion_variables(&self, cx: &mut ViewContext) -> Vec { + pub fn completion_variables(&self, cx: &mut Context) -> Vec { let stack_frame_id = self.stack_frame_list.read(cx).first_stack_frame_id(); self.variables @@ -561,7 +566,7 @@ impl VariableList { .collect() } - fn render_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); let Some(entries) = self.entries.get(&stack_frame_id) else { @@ -599,7 +604,7 @@ impl VariableList { scope_id: u64, variable: &Variable, depth: usize, - cx: &mut ViewContext, + cx: &mut Context, ) { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); @@ -663,7 +668,7 @@ impl VariableList { })) } - pub fn toggle_entry(&mut self, entry_id: &OpenEntry, cx: &mut ViewContext) { + pub fn toggle_entry(&mut self, entry_id: &OpenEntry, cx: &mut Context) { match self.open_entries.binary_search(&entry_id) { Ok(ix) => { self.open_entries.remove(ix); @@ -680,7 +685,7 @@ impl VariableList { &mut self, open_first_scope: bool, keep_open_entries: bool, - cx: &mut ViewContext, + cx: &mut Context, ) { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); @@ -830,7 +835,7 @@ impl VariableList { container_reference: u64, depth: usize, open_entries: &Vec, - cx: &mut ViewContext, + cx: &mut Context, ) -> Task>> { let stack_frame_list = self.stack_frame_list.read(cx); let thread_id = stack_frame_list.thread_id(); @@ -891,7 +896,7 @@ impl VariableList { &self, stack_frame_id: u64, open_entries: &Vec, - cx: &mut ViewContext, + cx: &mut Context, ) -> Task, HashMap>)>> { let scopes_task = self.dap_store.update(cx, |store, cx| { store.scopes(&self.client_id, stack_frame_id, cx) @@ -917,7 +922,7 @@ impl VariableList { }) } - fn fetch_variables(&mut self, cx: &mut ViewContext) { + fn fetch_variables(&mut self, cx: &mut Context) { if self.dap_store.read(cx).upstream_client().is_some() { return; } @@ -996,10 +1001,9 @@ impl VariableList { scope: &Scope, variable: &Variable, position: Point, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - let this = cx.view().clone(); - let support_set_variable = self .dap_store .read(cx) @@ -1007,67 +1011,60 @@ impl VariableList { .supports_set_variable .unwrap_or_default(); - let context_menu = ContextMenu::build(cx, |menu, cx| { - menu.entry( - "Copy name", - None, - cx.handler_for(&this, { - let variable_name = variable.name.clone(); - move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(variable_name.clone())) - } - }), - ) - .entry( - "Copy value", - None, - cx.handler_for(&this, { - let variable_value = variable.value.clone(); - let variable_name = variable.name.clone(); - let evaluate_name = variable.evaluate_name.clone(); - let source = scope.source.clone(); - move |this, cx| { - this.dap_store.update(cx, |dap_store, cx| { - if dap_store - .capabilities_by_id(&this.client_id) - .supports_clipboard_context - .unwrap_or_default() - { - let task = dap_store.evaluate( - &this.client_id, - this.stack_frame_list.read(cx).current_stack_frame_id(), - evaluate_name.clone().unwrap_or(variable_name.clone()), - dap::EvaluateArgumentsContext::Clipboard, - source.clone(), - cx, - ); - - cx.spawn(|_, cx| async move { - let response = task.await?; - - cx.update(|cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - response.result, - )) - }) + let this = cx.entity(); + + let context_menu = ContextMenu::build(window, cx, |menu, window, _cx| { + menu.entry("Copy name", None, { + let variable_name = variable.name.clone(); + move |_window, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(variable_name.clone())) + } + }) + .entry("Copy value", None, { + let source = scope.source.clone(); + let variable_value = variable.value.clone(); + let variable_name = variable.name.clone(); + let evaluate_name = variable.evaluate_name.clone(); + + window.handler_for(&this.clone(), move |this, _window, cx| { + this.dap_store.update(cx, |dap_store, cx| { + if dap_store + .capabilities_by_id(&this.client_id) + .supports_clipboard_context + .unwrap_or_default() + { + let task = dap_store.evaluate( + &this.client_id, + this.stack_frame_list.read(cx).current_stack_frame_id(), + evaluate_name.clone().unwrap_or(variable_name.clone()), + dap::EvaluateArgumentsContext::Clipboard, + source.clone(), + cx, + ); + + cx.spawn(|_, cx| async move { + let response = task.await?; + + cx.update(|cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + response.result, + )) }) - .detach_and_log_err(cx); - } else { - cx.write_to_clipboard(ClipboardItem::new_string( - variable_value.clone(), - )) - } - }); - } - }), - ) + }) + .detach_and_log_err(cx); + } else { + cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone())) + } + }); + }) + }) .when_some( variable.memory_reference.clone(), |menu, memory_reference| { menu.entry( "Copy memory reference", None, - cx.handler_for(&this, move |_, cx| { + window.handler_for(&this, move |_, _window, cx| { cx.write_to_clipboard(ClipboardItem::new_string( memory_reference.clone(), )) @@ -1075,14 +1072,14 @@ impl VariableList { ) }, ) - .when(support_set_variable, move |menu| { + .when(support_set_variable, |menu| { let variable = variable.clone(); let scope = scope.clone(); menu.entry( "Set value", None, - cx.handler_for(&this, move |this, cx| { + window.handler_for(&this, move |this, window, cx| { this.set_variable_state = Some(SetVariableState { parent_variables_reference, name: variable.name.clone(), @@ -1093,9 +1090,9 @@ impl VariableList { }); this.set_variable_editor.update(cx, |editor, cx| { - editor.set_text(variable.value.clone(), cx); - editor.select_all(&SelectAll, cx); - editor.focus(cx); + editor.set_text(variable.value.clone(), window, cx); + editor.select_all(&SelectAll, window, cx); + window.focus(&editor.focus_handle(cx)) }); this.build_entries(false, true, cx); @@ -1104,22 +1101,25 @@ impl VariableList { }) }); - cx.focus_view(&context_menu); - let subscription = - cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + cx.focus_view(&context_menu, window); + let subscription = cx.subscribe_in( + &context_menu, + window, + |this, _entity, _event: &DismissEvent, window, cx| { if this.open_context_menu.as_ref().is_some_and(|context_menu| { - context_menu.0.focus_handle(cx).contains_focused(cx) + context_menu.0.focus_handle(cx).contains_focused(window, cx) }) { - cx.focus_self(); + cx.focus_self(window); } this.open_context_menu.take(); cx.notify(); - }); + }, + ); self.open_context_menu = Some((context_menu, position, subscription)); } - fn cancel_set_variable_value(&mut self, cx: &mut ViewContext) { + fn cancel_set_variable_value(&mut self, cx: &mut Context) { if self.set_variable_state.take().is_none() { return; }; @@ -1127,11 +1127,11 @@ impl VariableList { self.build_entries(false, true, cx); } - fn set_variable_value(&mut self, _: &Confirm, cx: &mut ViewContext) { + fn set_variable_value(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { let new_variable_value = self.set_variable_editor.update(cx, |editor, cx| { let new_variable_value = editor.text(cx); - editor.clear(cx); + editor.clear(window, cx); new_variable_value }); @@ -1158,24 +1158,24 @@ impl VariableList { ) }); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { set_value_task.await?; - this.update(&mut cx, |this, cx| { + this.update_in(&mut cx, |this, window, cx| { this.build_entries(false, true, cx); - this.invalidate(cx); + this.invalidate(window, cx); }) }) .detach_and_log_err(cx); } - pub fn invalidate(&mut self, cx: &mut ViewContext) { + pub fn invalidate(&mut self, window: &mut Window, cx: &mut Context) { self.stack_frame_list.update(cx, |stack_frame_list, cx| { - stack_frame_list.invalidate(cx); + stack_frame_list.invalidate(window, cx); }); } - fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext) { + fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context) { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); if let Some(entries) = self.entries.get(&stack_frame_id) { self.selection = entries.first().cloned(); @@ -1183,7 +1183,7 @@ impl VariableList { }; } - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context) { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); if let Some(entries) = self.entries.get(&stack_frame_id) { self.selection = entries.last().cloned(); @@ -1191,7 +1191,7 @@ impl VariableList { }; } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + fn select_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context) { if let Some(selection) = &self.selection { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); if let Some(entries) = self.entries.get(&stack_frame_id) { @@ -1201,11 +1201,11 @@ impl VariableList { } } } else { - self.select_first(&SelectFirst, cx); + self.select_first(&SelectFirst, window, cx); } } - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { if let Some(selection) = &self.selection { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); if let Some(entries) = self.entries.get(&stack_frame_id) { @@ -1215,11 +1215,16 @@ impl VariableList { } } } else { - self.select_first(&SelectFirst, cx); + self.select_first(&SelectFirst, window, cx); } } - fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { + fn collapse_selected_entry( + &mut self, + _: &CollapseSelectedEntry, + window: &mut Window, + cx: &mut Context, + ) { if let Some(selection) = &self.selection { match selection { VariableListEntry::Scope(scope) => { @@ -1228,7 +1233,7 @@ impl VariableList { }; if self.open_entries.binary_search(entry_id).is_err() { - self.select_prev(&SelectPrev, cx); + self.select_prev(&SelectPrev, window, cx); } else { self.toggle_entry(entry_id, cx); } @@ -1246,7 +1251,7 @@ impl VariableList { }; if self.open_entries.binary_search(entry_id).is_err() { - self.select_prev(&SelectPrev, cx); + self.select_prev(&SelectPrev, window, cx); } else { self.toggle_variable( scope.variables_reference, @@ -1261,7 +1266,12 @@ impl VariableList { } } - fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { + fn expand_selected_entry( + &mut self, + _: &ExpandSelectedEntry, + window: &mut Window, + cx: &mut Context, + ) { if let Some(selection) = &self.selection { match selection { VariableListEntry::Scope(scope) => { @@ -1270,7 +1280,7 @@ impl VariableList { }; if self.open_entries.binary_search(entry_id).is_ok() { - self.select_next(&SelectNext, cx); + self.select_next(&SelectNext, window, cx); } else { self.toggle_entry(entry_id, cx); } @@ -1288,7 +1298,7 @@ impl VariableList { }; if self.open_entries.binary_search(entry_id).is_ok() { - self.select_next(&SelectNext, cx); + self.select_next(&SelectNext, window, cx); } else { self.toggle_variable( scope.variables_reference, @@ -1307,7 +1317,7 @@ impl VariableList { &self, depth: usize, state: &SetVariableState, - cx: &mut ViewContext, + cx: &mut Context, ) -> AnyElement { div() .h_4() @@ -1328,14 +1338,14 @@ impl VariableList { scope_id: u64, variable: &Variable, depth: usize, - cx: &mut ViewContext, + cx: &mut Context, ) { self.toggle_variable(scope_id, variable, depth, cx); } #[track_caller] #[cfg(any(test, feature = "test-support"))] - pub fn assert_visual_entries(&self, expected: Vec<&str>, cx: &ViewContext) { + pub fn assert_visual_entries(&self, expected: Vec<&str>, cx: &Context) { const INDENT: &'static str = " "; let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); @@ -1407,7 +1417,7 @@ impl VariableList { depth: usize, has_children: bool, is_selected: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> AnyElement { let scope_id = scope.variables_reference; let entry_id = OpenEntry::Variable { @@ -1444,7 +1454,7 @@ impl VariableList { .on_click(cx.listener({ let scope = scope.clone(); let variable = variable.clone(); - move |this, _, cx| { + move |this, _, _window, cx| { this.selection = Some(VariableListEntry::Variable { depth, has_children, @@ -1468,18 +1478,21 @@ impl VariableList { .when(has_children, |list_item| { list_item.on_toggle(cx.listener({ let variable = variable.clone(); - move |this, _, cx| this.toggle_variable(scope_id, &variable, depth, cx) + move |this, _, _window, cx| { + this.toggle_variable(scope_id, &variable, depth, cx) + } })) }) .on_secondary_mouse_down(cx.listener({ let scope = scope.clone(); let variable = variable.clone(); - move |this, event: &MouseDownEvent, cx| { + move |this, event: &MouseDownEvent, window, cx| { this.deploy_variable_context_menu( container_reference, &scope, &variable, event.position, + window, cx, ) } @@ -1500,12 +1513,7 @@ impl VariableList { .into_any() } - fn render_scope( - &self, - scope: &Scope, - is_selected: bool, - cx: &mut ViewContext, - ) -> AnyElement { + fn render_scope(&self, scope: &Scope, is_selected: bool, cx: &mut Context) -> AnyElement { let element_id = scope.variables_reference; let entry_id = OpenEntry::Scope { @@ -1537,7 +1545,7 @@ impl VariableList { .hover(|style| style.bg(bg_hover_color)) .on_click(cx.listener({ let scope = scope.clone(); - move |this, _, cx| { + move |this, _, _window, cx| { this.selection = Some(VariableListEntry::Scope(scope.clone())); cx.notify(); } @@ -1552,21 +1560,23 @@ impl VariableList { .indent_step_size(px(20.)) .always_show_disclosure_icon(true) .toggle(disclosed) - .on_toggle(cx.listener(move |this, _, cx| this.toggle_entry(&entry_id, cx))) + .on_toggle( + cx.listener(move |this, _, _window, cx| this.toggle_entry(&entry_id, cx)), + ) .child(div().text_ui(cx).w_full().child(scope.name.clone())), ) .into_any() } } -impl FocusableView for VariableList { - fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { +impl Focusable for VariableList { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for VariableList { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .key_context("VariableList") .id("variable-list") @@ -1579,9 +1589,11 @@ impl Render for VariableList { .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::expand_selected_entry)) .on_action(cx.listener(Self::collapse_selected_entry)) - .on_action(cx.listener(|this, _: &editor::actions::Cancel, cx| { - this.cancel_set_variable_value(cx) - })) + .on_action( + cx.listener(|this, _: &editor::actions::Cancel, _window, cx| { + this.cancel_set_variable_value(cx) + }), + ) .child(list(self.list.clone()).gap_1_5().size_full()) .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { deferred( @@ -1601,7 +1613,7 @@ struct EntryColors { marked_active: Hsla, } -fn get_entry_color(cx: &ViewContext) -> EntryColors { +fn get_entry_color(cx: &Context) -> EntryColors { let colors = cx.theme().colors(); EntryColors { diff --git a/crates/deepseek/Cargo.toml b/crates/deepseek/Cargo.toml new file mode 100644 index 00000000000000..25e8f2f25c8f6c --- /dev/null +++ b/crates/deepseek/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "deepseek" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/deepseek.rs" + +[features] +default = [] +schemars = ["dep:schemars"] + +[dependencies] +anyhow.workspace = true +futures.workspace = true +http_client.workspace = true +schemars = { workspace = true, optional = true } +serde.workspace = true +serde_json.workspace = true diff --git a/crates/deepseek/LICENSE-GPL b/crates/deepseek/LICENSE-GPL new file mode 120000 index 00000000000000..89e542f750cd38 --- /dev/null +++ b/crates/deepseek/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/deepseek/src/deepseek.rs b/crates/deepseek/src/deepseek.rs new file mode 100644 index 00000000000000..777cf696d8e047 --- /dev/null +++ b/crates/deepseek/src/deepseek.rs @@ -0,0 +1,301 @@ +use anyhow::{anyhow, Result}; +use futures::{ + io::BufReader, + stream::{BoxStream, StreamExt}, + AsyncBufReadExt, AsyncReadExt, +}; +use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::convert::TryFrom; + +pub const DEEPSEEK_API_URL: &str = "https://api.deepseek.com"; + +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Role { + User, + Assistant, + System, + Tool, +} + +impl TryFrom for Role { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + match value.as_str() { + "user" => Ok(Self::User), + "assistant" => Ok(Self::Assistant), + "system" => Ok(Self::System), + "tool" => Ok(Self::Tool), + _ => Err(anyhow!("invalid role '{value}'")), + } + } +} + +impl From for String { + fn from(val: Role) -> Self { + match val { + Role::User => "user".to_owned(), + Role::Assistant => "assistant".to_owned(), + Role::System => "system".to_owned(), + Role::Tool => "tool".to_owned(), + } + } +} + +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub enum Model { + #[serde(rename = "deepseek-chat")] + #[default] + Chat, + #[serde(rename = "deepseek-reasoner")] + Reasoner, + #[serde(rename = "custom")] + Custom { + name: String, + /// The name displayed in the UI, such as in the assistant panel model dropdown menu. + display_name: Option, + max_tokens: usize, + max_output_tokens: Option, + }, +} + +impl Model { + pub fn from_id(id: &str) -> Result { + match id { + "deepseek-chat" => Ok(Self::Chat), + "deepseek-reasoner" => Ok(Self::Reasoner), + _ => Err(anyhow!("invalid model id")), + } + } + + pub fn id(&self) -> &str { + match self { + Self::Chat => "deepseek-chat", + Self::Reasoner => "deepseek-reasoner", + Self::Custom { name, .. } => name, + } + } + + pub fn display_name(&self) -> &str { + match self { + Self::Chat => "DeepSeek Chat", + Self::Reasoner => "DeepSeek Reasoner", + Self::Custom { + name, display_name, .. + } => display_name.as_ref().unwrap_or(name).as_str(), + } + } + + pub fn max_token_count(&self) -> usize { + match self { + Self::Chat | Self::Reasoner => 64_000, + Self::Custom { max_tokens, .. } => *max_tokens, + } + } + + pub fn max_output_tokens(&self) -> Option { + match self { + Self::Chat => Some(8_192), + Self::Reasoner => Some(8_192), + Self::Custom { + max_output_tokens, .. + } => *max_output_tokens, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Request { + pub model: String, + pub messages: Vec, + pub stream: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_tokens: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub temperature: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub response_format: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tools: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ResponseFormat { + Text, + #[serde(rename = "json_object")] + JsonObject, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ToolDefinition { + Function { function: FunctionDefinition }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FunctionDefinition { + pub name: String, + pub description: Option, + pub parameters: Option, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(tag = "role", rename_all = "lowercase")] +pub enum RequestMessage { + Assistant { + content: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + tool_calls: Vec, + }, + User { + content: String, + }, + System { + content: String, + }, + Tool { + content: String, + tool_call_id: String, + }, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct ToolCall { + pub id: String, + #[serde(flatten)] + pub content: ToolCallContent, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum ToolCallContent { + Function { function: FunctionContent }, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct FunctionContent { + pub name: String, + pub arguments: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Response { + pub id: String, + pub object: String, + pub created: u64, + pub model: String, + pub choices: Vec, + pub usage: Usage, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_content: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Usage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + pub total_tokens: u32, + #[serde(default)] + pub prompt_cache_hit_tokens: u32, + #[serde(default)] + pub prompt_cache_miss_tokens: u32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Choice { + pub index: u32, + pub message: RequestMessage, + pub finish_reason: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct StreamResponse { + pub id: String, + pub object: String, + pub created: u64, + pub model: String, + pub choices: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct StreamChoice { + pub index: u32, + pub delta: StreamDelta, + pub finish_reason: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct StreamDelta { + pub role: Option, + pub content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_content: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ToolCallChunk { + pub index: usize, + pub id: Option, + pub function: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FunctionChunk { + pub name: Option, + pub arguments: Option, +} + +pub async fn stream_completion( + client: &dyn HttpClient, + api_url: &str, + api_key: &str, + request: Request, +) -> Result>> { + let uri = format!("{api_url}/v1/chat/completions"); + let request_builder = HttpRequest::builder() + .method(Method::POST) + .uri(uri) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)); + + let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; + let mut response = client.send(request).await?; + + if response.status().is_success() { + let reader = BufReader::new(response.into_body()); + Ok(reader + .lines() + .filter_map(|line| async move { + match line { + Ok(line) => { + let line = line.strip_prefix("data: ")?; + if line == "[DONE]" { + None + } else { + match serde_json::from_str(line) { + Ok(response) => Some(Ok(response)), + Err(error) => Some(Err(anyhow!(error))), + } + } + } + Err(error) => Some(Err(anyhow!(error))), + } + }) + .boxed()) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + Err(anyhow!( + "Failed to connect to DeepSeek API: {} {}", + response.status(), + body, + )) + } +} diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 71f96e9f9bc31e..d9bbb9dae9578b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -15,10 +15,9 @@ use editor::{ Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, }; use gpui::{ - actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle, - FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, - Render, SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext, - WeakView, WindowContext, + actions, div, svg, AnyElement, AnyView, App, Context, Entity, EventEmitter, FocusHandle, + Focusable, Global, HighlightStyle, InteractiveElement, IntoElement, ParentElement, Render, + SharedString, Styled, StyledText, Subscription, Task, WeakEntity, Window, }; use language::{ Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity, @@ -52,19 +51,18 @@ actions!(diagnostics, [Deploy, ToggleWarnings]); struct IncludeWarnings(bool); impl Global for IncludeWarnings {} -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { ProjectDiagnosticsSettings::register(cx); - cx.observe_new_views(ProjectDiagnosticsEditor::register) - .detach(); + cx.observe_new(ProjectDiagnosticsEditor::register).detach(); } struct ProjectDiagnosticsEditor { - project: Model, - workspace: WeakView, + project: Entity, + workspace: WeakEntity, focus_handle: FocusHandle, - editor: View, + editor: Entity, summary: DiagnosticSummary, - excerpts: Model, + excerpts: Entity, path_states: Vec, paths_to_update: BTreeSet<(ProjectPath, Option)>, include_warnings: bool, @@ -92,7 +90,7 @@ impl EventEmitter for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); impl Render for ProjectDiagnosticsEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let child = if self.path_states.is_empty() { div() .key_context("EmptyPane") @@ -116,25 +114,30 @@ impl Render for ProjectDiagnosticsEditor { } impl ProjectDiagnosticsEditor { - fn register(workspace: &mut Workspace, _: &mut ViewContext) { + fn register( + workspace: &mut Workspace, + _window: Option<&mut Window>, + _: &mut Context, + ) { workspace.register_action(Self::deploy); } fn new_with_context( context: u32, include_warnings: bool, - project_handle: Model, - workspace: WeakView, - cx: &mut ViewContext, + project_handle: Entity, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, ) -> Self { let project_event_subscription = - cx.subscribe(&project_handle, |this, project, event, cx| match event { + cx.subscribe_in(&project_handle, window, |this, project, event, window, cx| match event { project::Event::DiskBasedDiagnosticsStarted { .. } => { cx.notify(); } project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { log::debug!("disk based diagnostics finished for server {language_server_id}"); - this.update_stale_excerpts(cx); + this.update_stale_excerpts(window, cx); } project::Event::DiagnosticsUpdated { language_server_id, @@ -145,45 +148,58 @@ impl ProjectDiagnosticsEditor { this.summary = project.read(cx).diagnostic_summary(false, cx); cx.emit(EditorEvent::TitleChanged); - if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) { + if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) { log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); } else { log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); - this.update_stale_excerpts(cx); + this.update_stale_excerpts(window, cx); } } _ => {} }); let focus_handle = cx.focus_handle(); - cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx)) - .detach(); - cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx)) - .detach(); - - let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).capability())); - let editor = cx.new_view(|cx| { - let mut editor = - Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), true, cx); + cx.on_focus_in(&focus_handle, window, |this, window, cx| { + this.focus_in(window, cx) + }) + .detach(); + cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| { + this.focus_out(window, cx) + }) + .detach(); + + let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability())); + let editor = cx.new(|cx| { + let mut editor = Editor::for_multibuffer( + excerpts.clone(), + Some(project_handle.clone()), + true, + window, + cx, + ); editor.set_vertical_scroll_margin(5, cx); editor }); - cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { - cx.emit(event.clone()); - match event { - EditorEvent::Focused => { - if this.path_states.is_empty() { - cx.focus(&this.focus_handle); + cx.subscribe_in( + &editor, + window, + |this, _editor, event: &EditorEvent, window, cx| { + cx.emit(event.clone()); + match event { + EditorEvent::Focused => { + if this.path_states.is_empty() { + window.focus(&this.focus_handle); + } } + EditorEvent::Blurred => this.update_stale_excerpts(window, cx), + _ => {} } - EditorEvent::Blurred => this.update_stale_excerpts(cx), - _ => {} - } - }) + }, + ) .detach(); - cx.observe_global::(|this, cx| { + cx.observe_global_in::(window, |this, window, cx| { this.include_warnings = cx.global::().0; - this.update_all_excerpts(cx); + this.update_all_excerpts(window, cx); }) .detach(); @@ -202,16 +218,16 @@ impl ProjectDiagnosticsEditor { update_excerpts_task: None, _subscription: project_event_subscription, }; - this.update_all_excerpts(cx); + this.update_all_excerpts(window, cx); this } - fn update_stale_excerpts(&mut self, cx: &mut ViewContext) { + fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context) { if self.update_excerpts_task.is_some() { return; } let project_handle = self.project.clone(); - self.update_excerpts_task = Some(cx.spawn(|this, mut cx| async move { + self.update_excerpts_task = Some(cx.spawn_in(window, |this, mut cx| async move { cx.background_executor() .timer(DIAGNOSTICS_UPDATE_DEBOUNCE) .await; @@ -232,8 +248,8 @@ impl ProjectDiagnosticsEditor { .await .log_err() { - this.update(&mut cx, |this, cx| { - this.update_excerpts(path, language_server_id, buffer, cx); + this.update_in(&mut cx, |this, window, cx| { + this.update_excerpts(path, language_server_id, buffer, window, cx); })?; } } @@ -242,65 +258,74 @@ impl ProjectDiagnosticsEditor { } fn new( - project_handle: Model, + project_handle: Entity, include_warnings: bool, - workspace: WeakView, - cx: &mut ViewContext, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, ) -> Self { Self::new_with_context( editor::DEFAULT_MULTIBUFFER_CONTEXT, include_warnings, project_handle, workspace, + window, cx, ) } - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + fn deploy( + workspace: &mut Workspace, + _: &Deploy, + window: &mut Window, + cx: &mut Context, + ) { if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_item(&existing, true, true, cx); + workspace.activate_item(&existing, true, true, window, cx); } else { - let workspace_handle = cx.view().downgrade(); + let workspace_handle = cx.entity().downgrade(); let include_warnings = match cx.try_global::() { Some(include_warnings) => include_warnings.0, None => ProjectDiagnosticsSettings::get_global(cx).include_warnings, }; - let diagnostics = cx.new_view(|cx| { + let diagnostics = cx.new(|cx| { ProjectDiagnosticsEditor::new( workspace.project().clone(), include_warnings, workspace_handle, + window, cx, ) }); - workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx); + workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, window, cx); } } - fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext) { + fn toggle_warnings(&mut self, _: &ToggleWarnings, window: &mut Window, cx: &mut Context) { self.include_warnings = !self.include_warnings; cx.set_global(IncludeWarnings(self.include_warnings)); - self.update_all_excerpts(cx); + self.update_all_excerpts(window, cx); cx.notify(); } - fn focus_in(&mut self, cx: &mut ViewContext) { - if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() { - self.editor.focus_handle(cx).focus(cx) + fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { + if self.focus_handle.is_focused(window) && !self.path_states.is_empty() { + self.editor.focus_handle(cx).focus(window) } } - fn focus_out(&mut self, cx: &mut ViewContext) { - if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) { - self.update_stale_excerpts(cx); + fn focus_out(&mut self, window: &mut Window, cx: &mut Context) { + if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window) + { + self.update_stale_excerpts(window, cx); } } /// Enqueue an update of all excerpts. Updates all paths that either /// currently have diagnostics or are currently present in this view. - fn update_all_excerpts(&mut self, cx: &mut ViewContext) { + fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context) { self.project.update(cx, |project, cx| { let mut paths = project .diagnostic_summaries(false, cx) @@ -315,15 +340,16 @@ impl ProjectDiagnosticsEditor { paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None))); self.paths_to_update = paths; }); - self.update_stale_excerpts(cx); + self.update_stale_excerpts(window, cx); } fn update_excerpts( &mut self, path_to_update: ProjectPath, server_to_update: Option, - buffer: Model, - cx: &mut ViewContext, + buffer: Entity, + window: &mut Window, + cx: &mut Context, ) { let was_empty = self.path_states.is_empty(); let snapshot = buffer.read(cx).snapshot(); @@ -579,12 +605,12 @@ impl ProjectDiagnosticsEditor { } else { groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice(); new_excerpt_ids_by_selection_id = - editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh()); + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.refresh()); selections = editor.selections.all::(cx); } // If any selection has lost its position, move it to start of the next primary diagnostic. - let snapshot = editor.snapshot(cx); + let snapshot = editor.snapshot(window, cx); for selection in &mut selections { if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) { let group_ix = match groups.binary_search_by(|probe| { @@ -610,19 +636,19 @@ impl ProjectDiagnosticsEditor { } } } - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select(selections); }); Some(()) }); if self.path_states.is_empty() { - if self.editor.focus_handle(cx).is_focused(cx) { - cx.focus(&self.focus_handle); + if self.editor.focus_handle(cx).is_focused(window) { + window.focus(&self.focus_handle); } - } else if self.focus_handle.is_focused(cx) { + } else if self.focus_handle.is_focused(window) { let focus_handle = self.editor.focus_handle(cx); - cx.focus(&focus_handle); + window.focus(&focus_handle); } #[cfg(test)] @@ -632,7 +658,7 @@ impl ProjectDiagnosticsEditor { } #[cfg(test)] - fn check_invariants(&self, cx: &mut ViewContext) { + fn check_invariants(&self, cx: &mut Context) { let mut excerpts = Vec::new(); for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() { if let Some(file) = buffer.file() { @@ -652,8 +678,8 @@ impl ProjectDiagnosticsEditor { } } -impl FocusableView for ProjectDiagnosticsEditor { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { +impl Focusable for ProjectDiagnosticsEditor { + fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } @@ -665,20 +691,26 @@ impl Item for ProjectDiagnosticsEditor { Editor::to_item_events(event, f) } - fn deactivated(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| editor.deactivated(cx)); + fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { + self.editor + .update(cx, |editor, cx| editor.deactivated(window, cx)); } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + fn navigate( + &mut self, + data: Box, + window: &mut Window, + cx: &mut Context, + ) -> bool { self.editor - .update(cx, |editor, cx| editor.navigate(data, cx)) + .update(cx, |editor, cx| editor.navigate(data, window, cx)) } - fn tab_tooltip_text(&self, _: &AppContext) -> Option { + fn tab_tooltip_text(&self, _: &App) -> Option { Some("Project Diagnostics".into()) } - fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { h_flex() .gap_1() .when( @@ -723,17 +755,22 @@ impl Item for ProjectDiagnosticsEditor { fn for_each_project_item( &self, - cx: &AppContext, + cx: &App, f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { self.editor.for_each_project_item(cx, f) } - fn is_singleton(&self, _: &AppContext) -> bool { + fn is_singleton(&self, _: &App) -> bool { false } - fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + fn set_nav_history( + &mut self, + nav_history: ItemNavHistory, + _: &mut Window, + cx: &mut Context, + ) { self.editor.update(cx, |editor, _| { editor.set_nav_history(Some(nav_history)); }); @@ -742,64 +779,73 @@ impl Item for ProjectDiagnosticsEditor { fn clone_on_split( &self, _workspace_id: Option, - cx: &mut ViewContext, - ) -> Option> + window: &mut Window, + cx: &mut Context, + ) -> Option> where Self: Sized, { - Some(cx.new_view(|cx| { + Some(cx.new(|cx| { ProjectDiagnosticsEditor::new( self.project.clone(), self.include_warnings, self.workspace.clone(), + window, cx, ) })) } - fn is_dirty(&self, cx: &AppContext) -> bool { + fn is_dirty(&self, cx: &App) -> bool { self.excerpts.read(cx).is_dirty(cx) } - fn has_deleted_file(&self, cx: &AppContext) -> bool { + fn has_deleted_file(&self, cx: &App) -> bool { self.excerpts.read(cx).has_deleted_file(cx) } - fn has_conflict(&self, cx: &AppContext) -> bool { + fn has_conflict(&self, cx: &App) -> bool { self.excerpts.read(cx).has_conflict(cx) } - fn can_save(&self, _: &AppContext) -> bool { + fn can_save(&self, _: &App) -> bool { true } fn save( &mut self, format: bool, - project: Model, - cx: &mut ViewContext, + project: Entity, + window: &mut Window, + cx: &mut Context, ) -> Task> { - self.editor.save(format, project, cx) + self.editor.save(format, project, window, cx) } fn save_as( &mut self, - _: Model, + _: Entity, _: ProjectPath, - _: &mut ViewContext, + _window: &mut Window, + _: &mut Context, ) -> Task> { unreachable!() } - fn reload(&mut self, project: Model, cx: &mut ViewContext) -> Task> { - self.editor.reload(project, cx) + fn reload( + &mut self, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.editor.reload(project, window, cx) } fn act_as_type<'a>( &'a self, type_id: TypeId, - self_handle: &'a View, - _: &'a AppContext, + self_handle: &'a Entity, + _: &'a App, ) -> Option { if type_id == TypeId::of::() { Some(self_handle.to_any()) @@ -810,21 +856,27 @@ impl Item for ProjectDiagnosticsEditor { } } - fn as_searchable(&self, _: &View) -> Option> { + fn as_searchable(&self, _: &Entity) -> Option> { Some(Box::new(self.editor.clone())) } - fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft } - fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { self.editor.breadcrumbs(theme, cx) } - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); + fn added_to_workspace( + &mut self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.added_to_workspace(workspace, window, cx) + }); } } @@ -840,7 +892,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { h_flex() .id(DIAGNOSTIC_HEADER) .block_mouse_down() - .h(2. * cx.line_height()) + .h(2. * cx.window.line_height()) .w_full() .px_9() .justify_between() @@ -854,7 +906,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .map(|stack| { stack.child( svg() - .size(cx.text_style().font_size) + .size(cx.window.text_style().font_size) .flex_none() .map(|icon| { if diagnostic.severity == DiagnosticSeverity::ERROR { @@ -872,7 +924,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .gap_1() .child( StyledText::new(message.clone()).with_highlights( - &cx.text_style(), + &cx.window.text_style(), code_ranges .iter() .map(|range| (range.clone(), highlight_style)), @@ -929,7 +981,7 @@ fn context_range_for_entry( entry: &DiagnosticEntry, context: u32, snapshot: &BufferSnapshot, - cx: &AppContext, + cx: &App, ) -> Range { if let Some(rows) = heuristic_syntactic_expand( entry.range.clone(), @@ -960,7 +1012,7 @@ fn heuristic_syntactic_expand<'a>( input_range: Range, max_row_count: u32, snapshot: &'a BufferSnapshot, - cx: &'a AppContext, + cx: &'a App, ) -> Option> { let input_row_count = input_range.end.row - input_range.start.row; if input_row_count > max_row_count { diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 804be2e6b88ac6..705c70e982fadb 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -61,7 +61,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { let language_server_id = LanguageServerId(0); let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*window, cx); let workspace = window.root(cx).unwrap(); @@ -150,18 +150,20 @@ async fn test_diagnostics(cx: &mut TestAppContext) { }); // Open the project diagnostics view while there are already diagnostics. - let view = window.build_view(cx, |cx| { + let diagnostics = window.build_model(cx, |window, cx| { ProjectDiagnosticsEditor::new_with_context( 1, true, project.clone(), workspace.downgrade(), + window, cx, ) }); - let editor = view.update(cx, |view, _| view.editor.clone()); + let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone()); - view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + diagnostics + .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) .await; assert_eq!( editor_blocks(&editor, cx), @@ -251,7 +253,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) { lsp_store.disk_based_diagnostics_finished(language_server_id, cx); }); - view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + diagnostics + .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) .await; assert_eq!( editor_blocks(&editor, cx), @@ -370,7 +373,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) { lsp_store.disk_based_diagnostics_finished(language_server_id, cx); }); - view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + diagnostics + .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) .await; assert_eq!( editor_blocks(&editor, cx), @@ -477,20 +481,21 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { let server_id_2 = LanguageServerId(101); let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*window, cx); let workspace = window.root(cx).unwrap(); - let view = window.build_view(cx, |cx| { + let diagnostics = window.build_model(cx, |window, cx| { ProjectDiagnosticsEditor::new_with_context( 1, true, project.clone(), workspace.downgrade(), + window, cx, ) }); - let editor = view.update(cx, |view, _| view.editor.clone()); + let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone()); // Two language servers start updating diagnostics lsp_store.update(cx, |lsp_store, cx| { @@ -754,25 +759,26 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*window, cx); let workspace = window.root(cx).unwrap(); - let mutated_view = window.build_view(cx, |cx| { + let mutated_diagnostics = window.build_model(cx, |window, cx| { ProjectDiagnosticsEditor::new_with_context( 1, true, project.clone(), workspace.downgrade(), + window, cx, ) }); - workspace.update(cx, |workspace, cx| { - workspace.add_item_to_center(Box::new(mutated_view.clone()), cx); + workspace.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx); }); - mutated_view.update(cx, |view, cx| { - assert!(view.focus_handle.is_focused(cx)); + mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| { + assert!(diagnostics.focus_handle.is_focused(window)); }); let mut next_group_id = 0; @@ -858,16 +864,19 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { } log::info!("updating mutated diagnostics view"); - mutated_view.update(cx, |view, cx| view.update_stale_excerpts(cx)); + mutated_diagnostics.update_in(cx, |diagnostics, window, cx| { + diagnostics.update_stale_excerpts(window, cx) + }); cx.run_until_parked(); log::info!("constructing reference diagnostics view"); - let reference_view = window.build_view(cx, |cx| { + let reference_diagnostics = window.build_model(cx, |window, cx| { ProjectDiagnosticsEditor::new_with_context( 1, true, project.clone(), workspace.downgrade(), + window, cx, ) }); @@ -875,8 +884,8 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); cx.run_until_parked(); - let mutated_excerpts = get_diagnostics_excerpts(&mutated_view, cx); - let reference_excerpts = get_diagnostics_excerpts(&reference_view, cx); + let mutated_excerpts = get_diagnostics_excerpts(&mutated_diagnostics, cx); + let reference_excerpts = get_diagnostics_excerpts(&reference_diagnostics, cx); for ((path, language_server_id), diagnostics) in current_diagnostics { for diagnostic in diagnostics { @@ -917,13 +926,13 @@ struct ExcerptInfo { } fn get_diagnostics_excerpts( - view: &View, + diagnostics: &Entity, cx: &mut VisualTestContext, ) -> Vec { - view.update(cx, |view, cx| { + diagnostics.update(cx, |diagnostics, cx| { let mut result = vec![]; let mut excerpt_indices_by_id = HashMap::default(); - view.excerpts.update(cx, |multibuffer, cx| { + diagnostics.excerpts.update(cx, |multibuffer, cx| { let snapshot = multibuffer.snapshot(cx); for (id, buffer, range) in snapshot.excerpts() { excerpt_indices_by_id.insert(id, result.len()); @@ -940,7 +949,7 @@ fn get_diagnostics_excerpts( } }); - for state in &view.path_states { + for state in &diagnostics.path_states { for group in &state.diagnostic_groups { for (ix, excerpt_id) in group.excerpts.iter().enumerate() { let excerpt_ix = excerpt_indices_by_id[excerpt_id]; @@ -1043,58 +1052,63 @@ const FILE_HEADER: &str = "file header"; const EXCERPT_HEADER: &str = "excerpt header"; fn editor_blocks( - editor: &View, + editor: &Entity, cx: &mut VisualTestContext, ) -> Vec<(DisplayRow, SharedString)> { let mut blocks = Vec::new(); - cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - blocks.extend( - snapshot - .blocks_in_range(DisplayRow(0)..snapshot.max_point().row()) - .filter_map(|(row, block)| { - let block_id = block.id(); - let name: SharedString = match block { - Block::Custom(block) => { - let mut element = block.render(&mut BlockContext { - context: cx, - anchor_x: px(0.), - gutter_dimensions: &GutterDimensions::default(), - line_height: px(0.), - em_width: px(0.), - max_width: px(0.), - block_id, - selected: false, - editor_style: &editor::EditorStyle::default(), - }); - let element = element.downcast_mut::>().unwrap(); - element - .interactivity() - .element_id - .clone()? - .try_into() - .ok()? - } - - Block::FoldedBuffer { .. } => FILE_HEADER.into(), - Block::ExcerptBoundary { - starts_new_buffer, .. - } => { - if *starts_new_buffer { - FILE_HEADER.into() - } else { - EXCERPT_HEADER.into() + cx.draw( + gpui::Point::default(), + AvailableSpace::min_size(), + |window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + blocks.extend( + snapshot + .blocks_in_range(DisplayRow(0)..snapshot.max_point().row()) + .filter_map(|(row, block)| { + let block_id = block.id(); + let name: SharedString = match block { + Block::Custom(block) => { + let mut element = block.render(&mut BlockContext { + app: cx, + window, + anchor_x: px(0.), + gutter_dimensions: &GutterDimensions::default(), + line_height: px(0.), + em_width: px(0.), + max_width: px(0.), + block_id, + selected: false, + editor_style: &editor::EditorStyle::default(), + }); + let element = element.downcast_mut::>().unwrap(); + element + .interactivity() + .element_id + .clone()? + .try_into() + .ok()? } - } - }; - Some((row, name)) - }), - ) - }); + Block::FoldedBuffer { .. } => FILE_HEADER.into(), + Block::ExcerptBoundary { + starts_new_buffer, .. + } => { + if *starts_new_buffer { + FILE_HEADER.into() + } else { + EXCERPT_HEADER.into() + } + } + }; - div().into_any() - }); + Some((row, name)) + }), + ) + }); + + div().into_any() + }, + ); blocks } diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 715cccc02af815..bf3e1c7595ebcc 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,11 +1,11 @@ use std::time::Duration; -use editor::{AnchorRangeExt, Editor}; +use editor::Editor; use gpui::{ - EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View, - ViewContext, WeakView, + Context, Entity, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, + WeakEntity, Window, }; -use language::{Diagnostic, DiagnosticEntry}; +use language::Diagnostic; use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; @@ -13,15 +13,15 @@ use crate::{Deploy, ProjectDiagnosticsEditor}; pub struct DiagnosticIndicator { summary: project::DiagnosticSummary, - active_editor: Option>, - workspace: WeakView, + active_editor: Option>, + workspace: WeakEntity, current_diagnostic: Option, _observe_active_editor: Option, diagnostics_update: Task<()>, } impl Render for DiagnosticIndicator { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { (0, 0) => h_flex().map(|this| { this.child( @@ -67,11 +67,16 @@ impl Render for DiagnosticIndicator { Some( Button::new("diagnostic_message", message) .label_size(LabelSize::Small) - .tooltip(|cx| { - Tooltip::for_action("Next Diagnostic", &editor::actions::GoToDiagnostic, cx) + .tooltip(|window, cx| { + Tooltip::for_action( + "Next Diagnostic", + &editor::actions::GoToDiagnostic, + window, + cx, + ) }) - .on_click(cx.listener(|this, _, cx| { - this.go_to_next_diagnostic(cx); + .on_click(cx.listener(|this, _, window, cx| { + this.go_to_next_diagnostic(window, cx); })) .into_any_element(), ) @@ -87,11 +92,18 @@ impl Render for DiagnosticIndicator { .child( ButtonLike::new("diagnostic-indicator") .child(diagnostic_indicator) - .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx)) - .on_click(cx.listener(|this, _, cx| { + .tooltip(|window, cx| { + Tooltip::for_action("Project Diagnostics", &Deploy, window, cx) + }) + .on_click(cx.listener(|this, _, window, cx| { if let Some(workspace) = this.workspace.upgrade() { workspace.update(cx, |workspace, cx| { - ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx) + ProjectDiagnosticsEditor::deploy( + workspace, + &Default::default(), + window, + cx, + ) }) } })), @@ -101,7 +113,7 @@ impl Render for DiagnosticIndicator { } impl DiagnosticIndicator { - pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + pub fn new(workspace: &Workspace, cx: &mut Context) -> Self { let project = workspace.project(); cx.subscribe(project, |this, project, event, cx| match event { project::Event::DiskBasedDiagnosticsStarted { .. } => { @@ -133,41 +145,38 @@ impl DiagnosticIndicator { } } - fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext) { + fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context) { if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) { editor.update(cx, |editor, cx| { - editor.go_to_diagnostic_impl(editor::Direction::Next, cx); + editor.go_to_diagnostic_impl(editor::Direction::Next, window, cx); }) } } - fn update(&mut self, editor: View, cx: &mut ViewContext) { + fn update(&mut self, editor: Entity, window: &mut Window, cx: &mut Context) { let (buffer, cursor_position) = editor.update(cx, |editor, cx| { let buffer = editor.buffer().read(cx).snapshot(cx); let cursor_position = editor.selections.newest::(cx).head(); (buffer, cursor_position) }); let new_diagnostic = buffer - .diagnostics_in_range(cursor_position..cursor_position, false) - .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry { - diagnostic, - range: range.to_offset(&buffer), - }) + .diagnostics_in_range::<_, usize>(cursor_position..cursor_position) .filter(|entry| !entry.range.is_empty()) .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len())) .map(|entry| entry.diagnostic); if new_diagnostic != self.current_diagnostic { - self.diagnostics_update = cx.spawn(|diagnostics_indicator, mut cx| async move { - cx.background_executor() - .timer(Duration::from_millis(50)) - .await; - diagnostics_indicator - .update(&mut cx, |diagnostics_indicator, cx| { - diagnostics_indicator.current_diagnostic = new_diagnostic; - cx.notify(); - }) - .ok(); - }); + self.diagnostics_update = + cx.spawn_in(window, |diagnostics_indicator, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(50)) + .await; + diagnostics_indicator + .update(&mut cx, |diagnostics_indicator, cx| { + diagnostics_indicator.current_diagnostic = new_diagnostic; + cx.notify(); + }) + .ok(); + }); } } } @@ -178,12 +187,13 @@ impl StatusItemView for DiagnosticIndicator { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { self.active_editor = Some(editor.downgrade()); - self._observe_active_editor = Some(cx.observe(&editor, Self::update)); - self.update(editor, cx); + self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update)); + self.update(editor, window, cx); } else { self.active_editor = None; self.current_diagnostic = None; diff --git a/crates/diagnostics/src/project_diagnostics_settings.rs b/crates/diagnostics/src/project_diagnostics_settings.rs index 55879d0c426e2b..50d0949b737c08 100644 --- a/crates/diagnostics/src/project_diagnostics_settings.rs +++ b/crates/diagnostics/src/project_diagnostics_settings.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use gpui::AppContext; +use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -22,7 +22,7 @@ impl Settings for ProjectDiagnosticsSettings { const KEY: Option<&'static str> = Some("diagnostics"); type FileContent = ProjectDiagnosticsSettingsContent; - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + fn load(sources: SettingsSources, _: &mut App) -> Result { sources.json_merge() } } diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index f624225e8aeb5c..26e4ff20f8f4c1 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,15 +1,15 @@ use crate::ProjectDiagnosticsEditor; -use gpui::{EventEmitter, ParentElement, Render, View, ViewContext, WeakView}; +use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; use ui::{IconButton, IconButtonShape, IconName, Tooltip}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub struct ToolbarControls { - editor: Option>, + editor: Option>, } impl Render for ToolbarControls { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let mut include_warnings = false; let mut has_stale_excerpts = false; let mut is_updating = false; @@ -47,11 +47,11 @@ impl Render for ToolbarControls { .icon_color(Color::Info) .shape(IconButtonShape::Square) .disabled(is_updating) - .tooltip(move |cx| Tooltip::text("Update excerpts", cx)) - .on_click(cx.listener(|this, _, cx| { + .tooltip(Tooltip::text("Update excerpts")) + .on_click(cx.listener(|this, _, window, cx| { if let Some(diagnostics) = this.diagnostics() { diagnostics.update(cx, |diagnostics, cx| { - diagnostics.update_all_excerpts(cx); + diagnostics.update_all_excerpts(window, cx); }); } })), @@ -61,11 +61,11 @@ impl Render for ToolbarControls { IconButton::new("toggle-warnings", IconName::Warning) .icon_color(warning_color) .shape(IconButtonShape::Square) - .tooltip(move |cx| Tooltip::text(tooltip, cx)) - .on_click(cx.listener(|this, _, cx| { + .tooltip(Tooltip::text(tooltip)) + .on_click(cx.listener(|this, _, window, cx| { if let Some(editor) = this.diagnostics() { editor.update(cx, |editor, cx| { - editor.toggle_warnings(&Default::default(), cx); + editor.toggle_warnings(&Default::default(), window, cx); }); } })), @@ -79,7 +79,8 @@ impl ToolbarItemView for ToolbarControls { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, - _: &mut ViewContext, + _window: &mut Window, + _: &mut Context, ) -> ToolbarItemLocation { if let Some(pane_item) = active_pane_item.as_ref() { if let Some(editor) = pane_item.downcast::() { @@ -105,7 +106,7 @@ impl ToolbarControls { ToolbarControls { editor: None } } - fn diagnostics(&self) -> Option> { + fn diagnostics(&self) -> Option> { self.editor.as_ref()?.upgrade() } } diff --git a/crates/docs_preprocessor/src/docs_preprocessor.rs b/crates/docs_preprocessor/src/docs_preprocessor.rs index cd019c2c97caf9..0511aedb43eec0 100644 --- a/crates/docs_preprocessor/src/docs_preprocessor.rs +++ b/crates/docs_preprocessor/src/docs_preprocessor.rs @@ -32,8 +32,9 @@ impl PreprocessorContext { _ => return None, }; - keymap.sections().find_map(|section| { - section.bindings().find_map(|(keystroke, a)| { + // Find the binding in reverse order, as the last binding takes precedence. + keymap.sections().rev().find_map(|section| { + section.bindings().rev().find_map(|(keystroke, a)| { if a.to_string() == action { Some(keystroke.to_string()) } else { diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 488b99c55dd586..f1e862851b6478 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use clap::{Arg, ArgMatches, Command}; use docs_preprocessor::ZedDocsPreprocessor; use mdbook::preprocess::{CmdPreprocessor, Preprocessor}; diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 2f3cee8717cf81..7f7f75b2207406 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -89,6 +89,7 @@ url.workspace = true util.workspace = true uuid.workspace = true workspace.workspace = true +zed_predict_tos.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 0069b03c333524..06cc851fc1f25c 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -318,6 +318,7 @@ gpui::actions!( OpenProposedChangesEditor, OpenDocs, OpenPermalinkToLine, + OpenSelectionsInMultibuffer, OpenUrl, Outdent, AutoIndent, @@ -374,11 +375,12 @@ gpui::actions!( ToggleAutoSignatureHelp, ToggleGitBlame, ToggleGitBlameInline, - ToggleHunkDiff, ToggleIndentGuides, ToggleInlayHints, ToggleInlineCompletions, ToggleLineNumbers, + SwapSelectionEnds, + SetMark, ToggleRelativeLineNumbers, ToggleSelectionMenu, ToggleSoftWrap, @@ -397,3 +399,4 @@ gpui::actions!( action_as!(go_to_line, ToggleGoToLine as Toggle); action_with_deprecated_aliases!(editor, OpenSelectedFilename, ["editor::OpenFile"]); +action_with_deprecated_aliases!(editor, ToggleSelectedDiffHunks, ["editor::ToggleDiffHunk"]); diff --git a/crates/editor/src/blame_entry_tooltip.rs b/crates/editor/src/blame_entry_tooltip.rs index 3b3d3e4630ce71..755f63cc4078da 100644 --- a/crates/editor/src/blame_entry_tooltip.rs +++ b/crates/editor/src/blame_entry_tooltip.rs @@ -2,8 +2,8 @@ use futures::Future; use git::blame::BlameEntry; use git::Oid; use gpui::{ - AppContext, Asset, ClipboardItem, Element, ParentElement, Render, ScrollHandle, - StatefulInteractiveElement, WeakView, + App, Asset, ClipboardItem, Element, ParentElement, Render, ScrollHandle, + StatefulInteractiveElement, WeakEntity, }; use settings::Settings; use std::hash::Hash; @@ -27,7 +27,11 @@ impl<'a> CommitAvatar<'a> { } impl<'a> CommitAvatar<'a> { - fn render(&'a self, cx: &mut ViewContext) -> Option { + fn render( + &'a self, + window: &mut Window, + cx: &mut Context, + ) -> Option { let remote = self .details .and_then(|details| details.remote.as_ref()) @@ -35,7 +39,7 @@ impl<'a> CommitAvatar<'a> { let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha); - let element = match cx.use_asset::(&avatar_url) { + let element = match window.use_asset::(&avatar_url, cx) { // Loading or no avatar found None | Some(None) => Icon::new(IconName::Person) .color(Color::Muted) @@ -73,7 +77,7 @@ impl Asset for CommitAvatarAsset { fn load( source: Self::Source, - cx: &mut AppContext, + cx: &mut App, ) -> impl Future + Send + 'static { let client = cx.http_client(); @@ -91,7 +95,7 @@ pub(crate) struct BlameEntryTooltip { blame_entry: BlameEntry, details: Option, editor_style: EditorStyle, - workspace: Option>, + workspace: Option>, scroll_handle: ScrollHandle, } @@ -100,7 +104,7 @@ impl BlameEntryTooltip { blame_entry: BlameEntry, details: Option, style: &EditorStyle, - workspace: Option>, + workspace: Option>, ) -> Self { Self { editor_style: style.clone(), @@ -113,8 +117,9 @@ impl BlameEntryTooltip { } impl Render for BlameEntryTooltip { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let avatar = CommitAvatar::new(self.details.as_ref(), self.blame_entry.sha).render(cx); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let avatar = + CommitAvatar::new(self.details.as_ref(), self.blame_entry.sha).render(window, cx); let author = self .blame_entry @@ -149,11 +154,11 @@ impl Render for BlameEntryTooltip { .and_then(|details| details.pull_request.clone()); let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; - let message_max_height = cx.line_height() * 12 + (ui_font_size / 0.4); + let message_max_height = window.line_height() * 12 + (ui_font_size / 0.4); - tooltip_container(cx, move |this, cx| { + tooltip_container(window, cx, move |this, _, cx| { this.occlude() - .on_mouse_move(|_, cx| cx.stop_propagation()) + .on_mouse_move(|_, _, cx| cx.stop_propagation()) .child( v_flex() .w(gpui::rems(30.)) @@ -208,7 +213,7 @@ impl Render for BlameEntryTooltip { .icon_color(Color::Muted) .icon_position(IconPosition::Start) .style(ButtonStyle::Subtle) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.stop_propagation(); cx.open_url(pr.url.as_str()) }), @@ -235,7 +240,7 @@ impl Render for BlameEntryTooltip { .as_ref() .and_then(|details| details.permalink.clone()), |this, url| { - this.on_click(move |_, cx| { + this.on_click(move |_, _, cx| { cx.stop_propagation(); cx.open_url(url.as_str()) }) @@ -247,7 +252,7 @@ impl Render for BlameEntryTooltip { .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(Color::Muted) - .on_click(move |_, cx| { + .on_click(move |_, _, cx| { cx.stop_propagation(); cx.write_to_clipboard( ClipboardItem::new_string(full_sha.clone()), diff --git a/crates/editor/src/blink_manager.rs b/crates/editor/src/blink_manager.rs index 9d828c4de98c9c..3320cf6932145c 100644 --- a/crates/editor/src/blink_manager.rs +++ b/crates/editor/src/blink_manager.rs @@ -1,5 +1,5 @@ use crate::EditorSettings; -use gpui::ModelContext; +use gpui::Context; use settings::Settings; use settings::SettingsStore; use smol::Timer; @@ -15,7 +15,7 @@ pub struct BlinkManager { } impl BlinkManager { - pub fn new(blink_interval: Duration, cx: &mut ModelContext) -> Self { + pub fn new(blink_interval: Duration, cx: &mut Context) -> Self { // Make sure we blink the cursors if the setting is re-enabled cx.observe_global::(move |this, cx| { this.blink_cursors(this.blink_epoch, cx) @@ -37,7 +37,7 @@ impl BlinkManager { self.blink_epoch } - pub fn pause_blinking(&mut self, cx: &mut ModelContext) { + pub fn pause_blinking(&mut self, cx: &mut Context) { self.show_cursor(cx); let epoch = self.next_blink_epoch(); @@ -49,14 +49,14 @@ impl BlinkManager { .detach(); } - fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext) { + fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut Context) { if epoch == self.blink_epoch { self.blinking_paused = false; self.blink_cursors(epoch, cx); } } - fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext) { + fn blink_cursors(&mut self, epoch: usize, cx: &mut Context) { if EditorSettings::get_global(cx).cursor_blink { if epoch == self.blink_epoch && self.enabled && !self.blinking_paused { self.visible = !self.visible; @@ -78,14 +78,14 @@ impl BlinkManager { } } - pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) { + pub fn show_cursor(&mut self, cx: &mut Context<'_, BlinkManager>) { if !self.visible { self.visible = true; cx.notify(); } } - pub fn enable(&mut self, cx: &mut ModelContext) { + pub fn enable(&mut self, cx: &mut Context) { if self.enabled { return; } @@ -97,7 +97,7 @@ impl BlinkManager { self.blink_cursors(self.blink_epoch, cx); } - pub fn disable(&mut self, _cx: &mut ModelContext) { + pub fn disable(&mut self, _cx: &mut Context) { self.visible = false; self.enabled = false; } diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 28aafc1f46c9f4..d85e6bedf5f6eb 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -1,5 +1,5 @@ use anyhow::Context as _; -use gpui::{View, ViewContext, WindowContext}; +use gpui::{App, Context, Entity, Window}; use language::Language; use url::Url; @@ -16,7 +16,8 @@ fn is_c_language(language: &Language) -> bool { pub fn switch_source_header( editor: &mut Editor, _: &SwitchSourceHeader, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let Some(project) = &editor.project else { return; @@ -49,7 +50,7 @@ pub fn switch_source_header( cx, ) }); - cx.spawn(|_editor, mut cx| async move { + cx.spawn_in(window, |_editor, mut cx| async move { let switch_source_header = switch_source_header_task .await .with_context(|| format!("Switch source/header LSP request for path \"{source_file}\" failed"))?; @@ -70,8 +71,8 @@ pub fn switch_source_header( })?; workspace - .update(&mut cx, |workspace, view_cx| { - workspace.open_abs_path(path, false, view_cx) + .update_in(&mut cx, |workspace, window, cx| { + workspace.open_abs_path(path, false, window, cx) }) .with_context(|| { format!( @@ -84,11 +85,11 @@ pub fn switch_source_header( .detach_and_log_err(cx); } -pub fn apply_related_actions(editor: &View, cx: &mut WindowContext) { +pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: &mut App) { if editor.update(cx, |e, cx| { find_specific_language_server_in_selection(e, cx, is_c_language, CLANGD_SERVER_NAME) .is_some() }) { - register_action(editor, cx, switch_source_header); + register_action(editor, window, switch_source_header); } } diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index f498d24615d81d..7238fc65fe78bc 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1,8 +1,8 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement, - BackgroundExecutor, Div, FontWeight, ListSizingBehavior, Model, ScrollStrategy, SharedString, - Size, StrikethroughStyle, StyledText, UniformListScrollHandle, ViewContext, WeakView, + BackgroundExecutor, Div, Entity, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString, + Size, StrikethroughStyle, StyledText, UniformListScrollHandle, WeakEntity, }; use language::Buffer; use language::{CodeLabel, Documentation}; @@ -46,7 +46,7 @@ impl CodeContextMenu { pub fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool { if self.visible() { match self { @@ -62,7 +62,7 @@ impl CodeContextMenu { pub fn select_prev( &mut self, provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool { if self.visible() { match self { @@ -78,7 +78,7 @@ impl CodeContextMenu { pub fn select_next( &mut self, provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool { if self.visible() { match self { @@ -94,7 +94,7 @@ impl CodeContextMenu { pub fn select_last( &mut self, provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool { if self.visible() { match self { @@ -125,11 +125,17 @@ impl CodeContextMenu { &self, style: &EditorStyle, max_height_in_lines: u32, - cx: &mut ViewContext, + y_flipped: bool, + window: &mut Window, + cx: &mut Context, ) -> AnyElement { match self { - CodeContextMenu::Completions(menu) => menu.render(style, max_height_in_lines, cx), - CodeContextMenu::CodeActions(menu) => menu.render(style, max_height_in_lines, cx), + CodeContextMenu::Completions(menu) => { + menu.render(style, max_height_in_lines, y_flipped, window, cx) + } + CodeContextMenu::CodeActions(menu) => { + menu.render(style, max_height_in_lines, y_flipped, window, cx) + } } } @@ -137,8 +143,8 @@ impl CodeContextMenu { &self, style: &EditorStyle, max_size: Size, - workspace: Option>, - cx: &mut ViewContext, + workspace: Option>, + cx: &mut Context, ) -> Option { match self { CodeContextMenu::Completions(menu) => menu.render_aside(style, max_size, workspace, cx), @@ -157,7 +163,7 @@ pub struct CompletionsMenu { pub id: CompletionId, sort_completions: bool, pub initial_position: Anchor, - pub buffer: Model, + pub buffer: Entity, pub completions: Rc>>, match_candidates: Rc<[StringMatchCandidate]>, pub entries: Rc>>, @@ -180,7 +186,7 @@ impl CompletionsMenu { sort_completions: bool, show_completion_documentation: bool, initial_position: Anchor, - buffer: Model, + buffer: Entity, completions: Box<[Completion]>, ) -> Self { let match_candidates = completions @@ -210,7 +216,7 @@ impl CompletionsMenu { sort_completions: bool, choices: &Vec, selection: Range, - buffer: Model, + buffer: Entity, ) -> Self { let completions = choices .iter() @@ -266,33 +272,40 @@ impl CompletionsMenu { fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, + cx: &mut Context, ) { - self.update_selection_index(0, provider, cx); + let index = if self.scroll_handle.y_flipped() { + self.entries.borrow().len() - 1 + } else { + 0 + }; + self.update_selection_index(index, provider, cx); } - fn select_prev( - &mut self, - provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, - ) { - self.update_selection_index(self.prev_match_index(), provider, cx); + fn select_last(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context) { + let index = if self.scroll_handle.y_flipped() { + 0 + } else { + self.entries.borrow().len() - 1 + }; + self.update_selection_index(index, provider, cx); } - fn select_next( - &mut self, - provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, - ) { - self.update_selection_index(self.next_match_index(), provider, cx); + fn select_prev(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context) { + let index = if self.scroll_handle.y_flipped() { + self.next_match_index() + } else { + self.prev_match_index() + }; + self.update_selection_index(index, provider, cx); } - fn select_last( - &mut self, - provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, - ) { - let index = self.entries.borrow().len() - 1; + fn select_next(&mut self, provider: Option<&dyn CompletionProvider>, cx: &mut Context) { + let index = if self.scroll_handle.y_flipped() { + self.prev_match_index() + } else { + self.next_match_index() + }; self.update_selection_index(index, provider, cx); } @@ -300,7 +313,7 @@ impl CompletionsMenu { &mut self, match_index: usize, provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, + cx: &mut Context, ) { if self.selected_item != match_index { self.selected_item = match_index; @@ -336,6 +349,11 @@ impl CompletionsMenu { } _ => { entries.insert(0, hint); + // When `y_flipped`, need to scroll to bring it into view. + if self.selected_item == 0 { + self.scroll_handle + .scroll_to_item(self.selected_item, ScrollStrategy::Top); + } } } } @@ -343,7 +361,7 @@ impl CompletionsMenu { pub fn resolve_visible_completions( &mut self, provider: Option<&dyn CompletionProvider>, - cx: &mut ViewContext, + cx: &mut Context, ) { if !self.resolve_completions { return; @@ -439,7 +457,9 @@ impl CompletionsMenu { &self, style: &EditorStyle, max_height_in_lines: u32, - cx: &mut ViewContext, + y_flipped: bool, + window: &mut Window, + cx: &mut Context, ) -> AnyElement { let completions = self.completions.borrow_mut(); let show_completion_documentation = self.show_completion_documentation; @@ -475,10 +495,10 @@ impl CompletionsMenu { let last_rendered_range = self.last_rendered_range.clone(); let style = style.clone(); let list = uniform_list( - cx.view().clone(), + cx.entity().clone(), "completions", self.entries.borrow().len(), - move |_editor, range, cx| { + move |_editor, range, _window, cx| { last_rendered_range.borrow_mut().replace(range.clone()); let start_ix = range.start; let completions_guard = completions.borrow_mut(); @@ -561,12 +581,13 @@ impl CompletionsMenu { ListItem::new(mat.candidate_id) .inset(true) .toggle_state(item_ix == selected_item) - .on_click(cx.listener(move |editor, _event, cx| { + .on_click(cx.listener(move |editor, _event, window, cx| { cx.stop_propagation(); if let Some(task) = editor.confirm_completion( &ConfirmCompletion { item_ix: Some(item_ix), }, + window, cx, ) { task.detach_and_log_err(cx) @@ -616,6 +637,25 @@ impl CompletionsMenu { ) })), ), + CompletionEntry::InlineCompletionHint( + hint @ InlineCompletionMenuHint::PendingTermsAcceptance, + ) => div().min_w(px(250.)).max_w(px(500.)).child( + ListItem::new("inline-completion") + .inset(true) + .toggle_state(item_ix == selected_item) + .start_slot(Icon::new(IconName::ZedPredict)) + .child( + base_label.child( + StyledText::new(hint.label()) + .with_highlights(&style.text, None), + ), + ) + .on_click(cx.listener(move |editor, _event, window, cx| { + cx.stop_propagation(); + editor.toggle_zed_predict_tos(window, cx); + })), + ), + CompletionEntry::InlineCompletionHint( hint @ InlineCompletionMenuHint::Loaded { .. }, ) => div().min_w(px(250.)).max_w(px(500.)).child( @@ -629,10 +669,11 @@ impl CompletionsMenu { .with_highlights(&style.text, None), ), ) - .on_click(cx.listener(move |editor, _event, cx| { + .on_click(cx.listener(move |editor, _event, window, cx| { cx.stop_propagation(); editor.accept_inline_completion( &AcceptInlineCompletion {}, + window, cx, ); })), @@ -643,8 +684,9 @@ impl CompletionsMenu { }, ) .occlude() - .max_h(max_height_in_lines as f32 * cx.line_height()) + .max_h(max_height_in_lines as f32 * window.line_height()) .track_scroll(self.scroll_handle.clone()) + .y_flipped(y_flipped) .with_width_from_item(widest_completion_ix) .with_sizing_behavior(ListSizingBehavior::Infer); @@ -655,8 +697,8 @@ impl CompletionsMenu { &self, style: &EditorStyle, max_size: Size, - workspace: Option>, - cx: &mut ViewContext, + workspace: Option>, + cx: &mut Context, ) -> Option { if !self.show_completion_documentation { return None; @@ -686,13 +728,13 @@ impl CompletionsMenu { } CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => { match text { - InlineCompletionText::Edit { text, highlights } => div() + InlineCompletionText::Edit(highlighted_edits) => div() .mx_1() .rounded_md() .bg(cx.theme().colors().editor_background) .child( - gpui::StyledText::new(text.clone()) - .with_highlights(&style.text, highlights.clone()), + gpui::StyledText::new(highlighted_edits.text.clone()) + .with_highlights(&style.text, highlighted_edits.highlights.clone()), ), InlineCompletionText::Move(text) => div().child(text.clone()), } @@ -815,18 +857,23 @@ impl CompletionsMenu { drop(completions); let mut entries = self.entries.borrow_mut(); - if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first() { + let new_selection = if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first() + { entries.truncate(1); if inline_completion_was_selected || matches.is_empty() { - self.selected_item = 0; + 0 } else { - self.selected_item = 1; + 1 } } else { entries.truncate(0); - self.selected_item = 0; - } + 0 + }; entries.extend(matches.into_iter().map(CompletionEntry::Match)); + self.selected_item = new_selection; + // Scroll to 0 even if the LSP completion is the only one selected. This keeps the display + // consistent when y_flipped. + self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); } } @@ -954,47 +1001,71 @@ impl CodeActionsItem { pub struct CodeActionsMenu { pub actions: CodeActionContents, - pub buffer: Model, + pub buffer: Entity, pub selected_item: usize, pub scroll_handle: UniformListScrollHandle, pub deployed_from_indicator: Option, } impl CodeActionsMenu { - fn select_first(&mut self, cx: &mut ViewContext) { - self.selected_item = 0; + fn select_first(&mut self, cx: &mut Context) { + self.selected_item = if self.scroll_handle.y_flipped() { + self.actions.len() - 1 + } else { + 0 + }; self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); cx.notify() } - fn select_prev(&mut self, cx: &mut ViewContext) { - if self.selected_item > 0 { - self.selected_item -= 1; + fn select_last(&mut self, cx: &mut Context) { + self.selected_item = if self.scroll_handle.y_flipped() { + 0 } else { - self.selected_item = self.actions.len() - 1; - } + self.actions.len() - 1 + }; self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); - cx.notify(); + cx.notify() } - fn select_next(&mut self, cx: &mut ViewContext) { - if self.selected_item + 1 < self.actions.len() { - self.selected_item += 1; + fn select_prev(&mut self, cx: &mut Context) { + self.selected_item = if self.scroll_handle.y_flipped() { + self.next_match_index() } else { - self.selected_item = 0; - } + self.prev_match_index() + }; self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); cx.notify(); } - fn select_last(&mut self, cx: &mut ViewContext) { - self.selected_item = self.actions.len() - 1; + fn select_next(&mut self, cx: &mut Context) { + self.selected_item = if self.scroll_handle.y_flipped() { + self.prev_match_index() + } else { + self.next_match_index() + }; self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); - cx.notify() + cx.notify(); + } + + fn prev_match_index(&self) -> usize { + if self.selected_item > 0 { + self.selected_item - 1 + } else { + self.actions.len() - 1 + } + } + + fn next_match_index(&self) -> usize { + if self.selected_item + 1 < self.actions.len() { + self.selected_item + 1 + } else { + 0 + } } fn visible(&self) -> bool { @@ -1013,15 +1084,17 @@ impl CodeActionsMenu { &self, _style: &EditorStyle, max_height_in_lines: u32, - cx: &mut ViewContext, + y_flipped: bool, + window: &mut Window, + cx: &mut Context, ) -> AnyElement { let actions = self.actions.clone(); let selected_item = self.selected_item; let list = uniform_list( - cx.view().clone(), + cx.entity().clone(), "code_actions_menu", self.actions.len(), - move |_this, range, cx| { + move |_this, range, _, cx| { actions .iter() .skip(range.start) @@ -1036,12 +1109,13 @@ impl CodeActionsMenu { .inset(true) .toggle_state(selected) .when_some(action.as_code_action(), |this, action| { - this.on_click(cx.listener(move |editor, _, cx| { + this.on_click(cx.listener(move |editor, _, window, cx| { cx.stop_propagation(); if let Some(task) = editor.confirm_code_action( &ConfirmCodeAction { item_ix: Some(item_ix), }, + window, cx, ) { task.detach_and_log_err(cx) @@ -1060,12 +1134,13 @@ impl CodeActionsMenu { ) }) .when_some(action.as_task(), |this, task| { - this.on_click(cx.listener(move |editor, _, cx| { + this.on_click(cx.listener(move |editor, _, window, cx| { cx.stop_propagation(); if let Some(task) = editor.confirm_code_action( &ConfirmCodeAction { item_ix: Some(item_ix), }, + window, cx, ) { task.detach_and_log_err(cx) @@ -1086,8 +1161,9 @@ impl CodeActionsMenu { }, ) .occlude() - .max_h(max_height_in_lines as f32 * cx.line_height()) + .max_h(max_height_in_lines as f32 * window.line_height()) .track_scroll(self.scroll_handle.clone()) + .y_flipped(y_flipped) .with_width_from_item( self.actions .iter() diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index ec4fa4dc6d2a46..417345f32c888a 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -30,8 +30,8 @@ use crate::{ hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt, }; pub use block_map::{ - Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, - BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, + Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement, + BlockPoint, BlockProperties, BlockRows, BlockStyle, CustomBlockId, RenderBlock, StickyHeaderExcerpt, }; use block_map::{BlockRow, BlockSnapshot}; @@ -39,9 +39,7 @@ use collections::{HashMap, HashSet}; pub use crease_map::*; pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint}; use fold_map::{FoldMap, FoldSnapshot}; -use gpui::{ - AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle, -}; +use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle}; pub use inlay_map::Inlay; use inlay_map::{InlayMap, InlaySnapshot}; pub use inlay_map::{InlayOffset, InlayPoint}; @@ -53,7 +51,7 @@ use language::{ use lsp::DiagnosticSeverity; use multi_buffer::{ Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, - ToOffset, ToPoint, + RowInfo, ToOffset, ToPoint, }; use serde::Deserialize; use std::{ @@ -67,8 +65,8 @@ use std::{ }; use sum_tree::{Bias, TreeMap}; use tab_map::{TabMap, TabSnapshot}; -use text::LineIndent; -use ui::{px, SharedString, WindowContext}; +use text::{BufferId, LineIndent}; +use ui::{px, SharedString}; use unicode_segmentation::UnicodeSegmentation; use wrap_map::{WrapMap, WrapSnapshot}; @@ -78,8 +76,6 @@ pub enum FoldStatus { Foldable, } -pub type RenderFoldToggle = Arc AnyElement>; - pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } @@ -93,7 +89,7 @@ type InlayHighlights = TreeMap, + buffer: Entity, buffer_subscription: BufferSubscription, /// Decides where the [`Inlay`]s should be displayed. inlay_map: InlayMap, @@ -102,7 +98,7 @@ pub struct DisplayMap { /// Keeps track of hard tabs in a buffer. tab_map: TabMap, /// Handles soft wrapping. - wrap_map: Model, + wrap_map: Entity, /// Tracks custom blocks such as diagnostics that should be displayed within buffer. block_map: BlockMap, /// Regions of text that should be highlighted. @@ -119,7 +115,7 @@ pub struct DisplayMap { impl DisplayMap { #[allow(clippy::too_many_arguments)] pub fn new( - buffer: Model, + buffer: Entity, font: Font, font_size: Pixels, wrap_width: Option, @@ -128,7 +124,7 @@ impl DisplayMap { excerpt_header_height: u32, excerpt_footer_height: u32, fold_placeholder: FoldPlaceholder, - cx: &mut ModelContext, + cx: &mut Context, ) -> Self { let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); @@ -166,7 +162,7 @@ impl DisplayMap { } } - pub fn snapshot(&mut self, cx: &mut ModelContext) -> DisplaySnapshot { + pub fn snapshot(&mut self, cx: &mut Context) -> DisplaySnapshot { let buffer_snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits); @@ -194,7 +190,7 @@ impl DisplayMap { } } - pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut ModelContext) { + pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut Context) { self.fold( other .folds_in_range(0..other.buffer_snapshot.len()) @@ -210,11 +206,7 @@ impl DisplayMap { } /// Creates folds for the given creases. - pub fn fold( - &mut self, - creases: Vec>, - cx: &mut ModelContext, - ) { + pub fn fold(&mut self, creases: Vec>, cx: &mut Context) { let buffer_snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); @@ -286,7 +278,7 @@ impl DisplayMap { &mut self, ranges: impl IntoIterator>, type_id: TypeId, - cx: &mut ModelContext, + cx: &mut Context, ) { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); @@ -311,7 +303,7 @@ impl DisplayMap { &mut self, ranges: impl IntoIterator>, inclusive: bool, - cx: &mut ModelContext, + cx: &mut Context, ) { let snapshot = self.buffer.read(cx).snapshot(cx); let offset_ranges = ranges @@ -338,7 +330,7 @@ impl DisplayMap { block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive); } - pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext) { + pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut Context) { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); @@ -352,7 +344,7 @@ impl DisplayMap { block_map.fold_buffer(buffer_id, self.buffer.read(cx), cx) } - pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext) { + pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut Context) { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); @@ -366,14 +358,18 @@ impl DisplayMap { block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx) } - pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool { + pub(crate) fn is_buffer_folded(&self, buffer_id: language::BufferId) -> bool { self.block_map.folded_buffers.contains(&buffer_id) } + pub(crate) fn folded_buffers(&self) -> &HashSet { + &self.block_map.folded_buffers + } + pub fn insert_creases( &mut self, creases: impl IntoIterator>, - cx: &mut ModelContext, + cx: &mut Context, ) -> Vec { let snapshot = self.buffer.read(cx).snapshot(cx); self.crease_map.insert(creases, &snapshot) @@ -382,7 +378,7 @@ impl DisplayMap { pub fn remove_creases( &mut self, crease_ids: impl IntoIterator, - cx: &mut ModelContext, + cx: &mut Context, ) { let snapshot = self.buffer.read(cx).snapshot(cx); self.crease_map.remove(crease_ids, &snapshot) @@ -391,7 +387,7 @@ impl DisplayMap { pub fn insert_blocks( &mut self, blocks: impl IntoIterator>, - cx: &mut ModelContext, + cx: &mut Context, ) -> Vec { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); @@ -406,11 +402,7 @@ impl DisplayMap { block_map.insert(blocks) } - pub fn resize_blocks( - &mut self, - heights: HashMap, - cx: &mut ModelContext, - ) { + pub fn resize_blocks(&mut self, heights: HashMap, cx: &mut Context) { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); @@ -428,7 +420,7 @@ impl DisplayMap { self.block_map.replace_blocks(renderers); } - pub fn remove_blocks(&mut self, ids: HashSet, cx: &mut ModelContext) { + pub fn remove_blocks(&mut self, ids: HashSet, cx: &mut Context) { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); @@ -445,7 +437,7 @@ impl DisplayMap { pub fn row_for_block( &mut self, block_id: CustomBlockId, - cx: &mut ModelContext, + cx: &mut Context, ) -> Option { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); @@ -500,12 +492,12 @@ impl DisplayMap { cleared } - pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut ModelContext) -> bool { + pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut Context) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font_with_size(font, font_size, cx)) } - pub fn set_wrap_width(&self, width: Option, cx: &mut ModelContext) -> bool { + pub fn set_wrap_width(&self, width: Option, cx: &mut Context) -> bool { self.wrap_map .update(cx, |map, cx| map.set_wrap_width(width, cx)) } @@ -518,7 +510,7 @@ impl DisplayMap { &mut self, to_remove: Vec, to_insert: Vec, - cx: &mut ModelContext, + cx: &mut Context, ) { if to_remove.is_empty() && to_insert.is_empty() { return; @@ -543,7 +535,7 @@ impl DisplayMap { self.block_map.read(snapshot, edits); } - fn tab_size(buffer: &Model, cx: &mut ModelContext) -> NonZeroU32 { + fn tab_size(buffer: &Entity, cx: &App) -> NonZeroU32 { let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx)); let language = buffer .and_then(|buffer| buffer.language()) @@ -553,7 +545,7 @@ impl DisplayMap { } #[cfg(test)] - pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool { + pub fn is_rewrapping(&self, cx: &gpui::App) -> bool { self.wrap_map.read(cx).is_rewrapping() } @@ -715,13 +707,8 @@ impl DisplaySnapshot { self.buffer_snapshot.len() == 0 } - pub fn buffer_rows( - &self, - start_row: DisplayRow, - ) -> impl Iterator> + '_ { - self.block_snapshot - .buffer_rows(BlockRow(start_row.0)) - .map(|row| row.map(MultiBufferRow)) + pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator + '_ { + self.block_snapshot.row_infos(BlockRow(start_row.0)) } pub fn widest_line_number(&self) -> u32 { @@ -1463,7 +1450,7 @@ pub mod tests { use crate::{movement, test::marked_display_snapshot}; use block_map::BlockPlacement; use gpui::{ - div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla, Rgba, + div, font, observe, px, App, AppContext as _, BorrowAppContext, Element, Hsla, Rgba, }; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, @@ -1519,7 +1506,7 @@ pub mod tests { } }); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer.clone(), font("Helvetica"), @@ -1760,16 +1747,16 @@ pub mod tests { let editor = cx.editor.clone(); let window = cx.window; - _ = cx.update_window(window, |_, cx| { + _ = cx.update_window(window, |_, window, cx| { let text_layout_details = - editor.update(cx, |editor, cx| editor.text_layout_details(cx)); + editor.update(cx, |editor, _cx| editor.text_layout_details(window)); let font_size = px(12.0); let wrap_width = Some(px(64.)); let text = "one two three four five\nsix seven eight"; let buffer = MultiBuffer::build_simple(text, cx); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer.clone(), font("Helvetica"), @@ -1873,14 +1860,14 @@ pub mod tests { } #[gpui::test] - fn test_text_chunks(cx: &mut gpui::AppContext) { + fn test_text_chunks(cx: &mut gpui::App) { init_test(cx, |_| {}); let text = sample_text(6, 6, 'a'); let buffer = MultiBuffer::build_simple(&text, cx); let font_size = px(14.0); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer.clone(), font("Helvetica"), @@ -1970,13 +1957,13 @@ pub mod tests { cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); - let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let font_size = px(14.0); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer, font("Helvetica"), @@ -2073,12 +2060,12 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer, font("Courier"), @@ -2147,7 +2134,7 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.new_model(|cx| Buffer::local(text, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx)); buffer.update(cx, |buffer, cx| { buffer.update_diagnostics( @@ -2168,10 +2155,10 @@ pub mod tests { ) }); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer, font("Courier"), @@ -2260,7 +2247,7 @@ pub mod tests { let buffer = cx.update(|cx| MultiBuffer::build_simple("abcde\nfghij\nklmno\npqrst", cx)); let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer.clone(), font("Courier"), @@ -2398,13 +2385,13 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let font_size = px(16.0); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer, font("Courier"), @@ -2481,14 +2468,14 @@ pub mod tests { let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); - let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); let font_size = px(16.0); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer, font("Courier"), @@ -2539,10 +2526,10 @@ pub mod tests { } #[gpui::test] - fn test_clip_point(cx: &mut gpui::AppContext) { + fn test_clip_point(cx: &mut gpui::App) { init_test(cx, |_| {}); - fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) { + fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::App) { let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); match bias { @@ -2589,10 +2576,10 @@ pub mod tests { } #[gpui::test] - fn test_clip_at_line_ends(cx: &mut gpui::AppContext) { + fn test_clip_at_line_ends(cx: &mut gpui::App) { init_test(cx, |_| {}); - fn assert(text: &str, cx: &mut gpui::AppContext) { + fn assert(text: &str, cx: &mut gpui::App) { let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); unmarked_snapshot.clip_at_line_ends = true; assert_eq!( @@ -2608,13 +2595,13 @@ pub mod tests { } #[gpui::test] - fn test_creases(cx: &mut gpui::AppContext) { + fn test_creases(cx: &mut gpui::App) { init_test(cx, |_| {}); let text = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll"; let buffer = MultiBuffer::build_simple(text, cx); let font_size = px(14.0); - cx.new_model(|cx| { + cx.new(|cx| { let mut map = DisplayMap::new( buffer.clone(), font("Helvetica"), @@ -2635,8 +2622,8 @@ pub mod tests { [Crease::inline( range, FoldPlaceholder::test(), - |_row, _status, _toggle, _cx| div(), - |_row, _status, _cx| div(), + |_row, _status, _toggle, _window, _cx| div(), + |_row, _status, _window, _cx| div(), )], &map.buffer.read(cx).snapshot(cx), ); @@ -2646,14 +2633,14 @@ pub mod tests { } #[gpui::test] - fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) { + fn test_tabs_with_multibyte_chars(cx: &mut gpui::App) { init_test(cx, |_| {}); let text = "✅\t\tα\nβ\t\n🏀β\t\tγ"; let buffer = MultiBuffer::build_simple(text, cx); let font_size = px(14.0); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer.clone(), font("Helvetica"), @@ -2725,12 +2712,12 @@ pub mod tests { } #[gpui::test] - fn test_max_point(cx: &mut gpui::AppContext) { + fn test_max_point(cx: &mut gpui::App) { init_test(cx, |_| {}); let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); let font_size = px(14.0); - let map = cx.new_model(|cx| { + let map = cx.new(|cx| { DisplayMap::new( buffer.clone(), font("Helvetica"), @@ -2752,9 +2739,9 @@ pub mod tests { fn syntax_chunks( rows: Range, - map: &Model, + map: &Entity, theme: &SyntaxTheme, - cx: &mut AppContext, + cx: &mut App, ) -> Vec<(String, Option)> { chunks(rows, map, theme, cx) .into_iter() @@ -2764,9 +2751,9 @@ pub mod tests { fn chunks( rows: Range, - map: &Model, + map: &Entity, theme: &SyntaxTheme, - cx: &mut AppContext, + cx: &mut App, ) -> Vec<(String, Option, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let mut chunks: Vec<(String, Option, Option)> = Vec::new(); @@ -2786,7 +2773,7 @@ pub mod tests { chunks } - fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + fn init_test(cx: &mut App, f: impl Fn(&mut AllLanguageSettingsContent)) { let settings = SettingsStore::test(cx); cx.set_global(settings); language::init(cx); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 7de2797079a2da..7715471a1f3f91 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -4,11 +4,11 @@ use super::{ }; use crate::{EditorStyle, GutterDimensions}; use collections::{Bound, HashMap, HashSet}; -use gpui::{AnyElement, AppContext, EntityId, Pixels, WindowContext}; +use gpui::{AnyElement, App, EntityId, Pixels, Window}; use language::{Chunk, Patch, Point}; use multi_buffer::{ - Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToOffset, - ToPoint as _, + Anchor, ExcerptId, ExcerptInfo, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, RowInfo, + ToOffset, ToPoint as _, }; use parking_lot::Mutex; use std::{ @@ -225,8 +225,12 @@ pub enum BlockStyle { Sticky, } +#[derive(gpui::AppContext, gpui::VisualContext)] pub struct BlockContext<'a, 'b> { - pub context: &'b mut WindowContext<'a>, + #[window] + pub window: &'a mut Window, + #[app] + pub app: &'b mut App, pub anchor_x: Pixels, pub max_width: Pixels, pub gutter_dimensions: &'b GutterDimensions, @@ -399,9 +403,9 @@ pub struct BlockChunks<'a> { } #[derive(Clone)] -pub struct BlockBufferRows<'a> { +pub struct BlockRows<'a> { transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, - input_buffer_rows: wrap_map::WrapBufferRows<'a>, + input_rows: wrap_map::WrapRows<'a>, output_row: BlockRow, started: bool, } @@ -777,14 +781,12 @@ impl BlockMap { if let Some(new_buffer_id) = new_buffer_id { let first_excerpt = excerpt_boundary.next.clone().unwrap(); if folded_buffers.contains(&new_buffer_id) { - let mut buffer_end = Point::new(excerpt_boundary.row.0, 0) - + excerpt_boundary.next.as_ref().unwrap().text_summary.lines; + let mut last_excerpt_end_row = first_excerpt.end_row; while let Some(next_boundary) = boundaries.peek() { if let Some(next_excerpt_boundary) = &next_boundary.next { if next_excerpt_boundary.buffer_id == new_buffer_id { - buffer_end = Point::new(next_boundary.row.0, 0) - + next_excerpt_boundary.text_summary.lines; + last_excerpt_end_row = next_excerpt_boundary.end_row; } else { break; } @@ -793,7 +795,15 @@ impl BlockMap { boundaries.next(); } - let wrap_end_row = wrap_snapshot.make_wrap_point(buffer_end, Bias::Right).row(); + let wrap_end_row = wrap_snapshot + .make_wrap_point( + Point::new( + last_excerpt_end_row.0, + buffer.line_len(last_excerpt_end_row), + ), + Bias::Right, + ) + .row(); return Some(( BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)), @@ -1226,22 +1236,12 @@ impl<'a> BlockMapWriter<'a> { self.remove(blocks_to_remove); } - pub fn fold_buffer( - &mut self, - buffer_id: BufferId, - multi_buffer: &MultiBuffer, - cx: &AppContext, - ) { + pub fn fold_buffer(&mut self, buffer_id: BufferId, multi_buffer: &MultiBuffer, cx: &App) { self.0.folded_buffers.insert(buffer_id); self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx); } - pub fn unfold_buffer( - &mut self, - buffer_id: BufferId, - multi_buffer: &MultiBuffer, - cx: &AppContext, - ) { + pub fn unfold_buffer(&mut self, buffer_id: BufferId, multi_buffer: &MultiBuffer, cx: &App) { self.0.folded_buffers.remove(&buffer_id); self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx); } @@ -1250,7 +1250,7 @@ impl<'a> BlockMapWriter<'a> { &mut self, buffer_id: BufferId, multi_buffer: &MultiBuffer, - cx: &AppContext, + cx: &App, ) { let wrap_snapshot = self.0.wrap_snapshot.borrow().clone(); @@ -1360,7 +1360,7 @@ impl BlockSnapshot { } } - pub(super) fn buffer_rows(&self, start_row: BlockRow) -> BlockBufferRows { + pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&start_row, Bias::Right, &()); let (output_start, input_start) = cursor.start(); @@ -1373,9 +1373,9 @@ impl BlockSnapshot { 0 }; let input_start_row = input_start.0 + overshoot; - BlockBufferRows { + BlockRows { transforms: cursor, - input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row), + input_rows: self.wrap_snapshot.row_infos(input_start_row), output_row: start_row, started: false, } @@ -1480,7 +1480,7 @@ impl BlockSnapshot { } BlockId::ExcerptBoundary(next_excerpt_id) => { if let Some(next_excerpt_id) = next_excerpt_id { - let excerpt_range = buffer.range_for_excerpt::(next_excerpt_id)?; + let excerpt_range = buffer.range_for_excerpt(next_excerpt_id)?; self.wrap_snapshot .make_wrap_point(excerpt_range.start, Bias::Left) } else { @@ -1488,10 +1488,9 @@ impl BlockSnapshot { .make_wrap_point(buffer.max_point(), Bias::Left) } } - BlockId::FoldedBuffer(excerpt_id) => self.wrap_snapshot.make_wrap_point( - buffer.range_for_excerpt::(excerpt_id)?.start, - Bias::Left, - ), + BlockId::FoldedBuffer(excerpt_id) => self + .wrap_snapshot + .make_wrap_point(buffer.range_for_excerpt(excerpt_id)?.start, Bias::Left), }; let wrap_row = WrapRow(wrap_point.row()); @@ -1757,7 +1756,6 @@ impl<'a> BlockChunks<'a> { pub struct StickyHeaderExcerpt<'a> { pub excerpt: &'a ExcerptInfo, pub next_excerpt_controls_present: bool, - // TODO az remove option pub next_buffer_row: Option, } @@ -1833,8 +1831,8 @@ impl<'a> Iterator for BlockChunks<'a> { } } -impl<'a> Iterator for BlockBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for BlockRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { if self.started { @@ -1863,7 +1861,7 @@ impl<'a> Iterator for BlockBufferRows<'a> { .as_ref() .map_or(true, |block| block.is_replacement()) { - self.input_buffer_rows.seek(self.transforms.start().1 .0); + self.input_rows.seek(self.transforms.start().1 .0); } } @@ -1871,15 +1869,15 @@ impl<'a> Iterator for BlockBufferRows<'a> { if let Some(block) = transform.block.as_ref() { if block.is_replacement() && self.transforms.start().0 == self.output_row { if matches!(block, Block::FoldedBuffer { .. }) { - Some(None) + Some(RowInfo::default()) } else { - Some(self.input_buffer_rows.next().unwrap()) + Some(self.input_rows.next().unwrap()) } } else { - Some(None) + Some(RowInfo::default()) } } else { - Some(self.input_buffer_rows.next().unwrap()) + Some(self.input_rows.next().unwrap()) } } } @@ -1930,16 +1928,16 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow { } impl<'a> Deref for BlockContext<'a, '_> { - type Target = WindowContext<'a>; + type Target = App; fn deref(&self) -> &Self::Target { - self.context + self.app } } impl DerefMut for BlockContext<'_, '_> { fn deref_mut(&mut self) -> &mut Self::Target { - self.context + self.app } } @@ -1997,7 +1995,7 @@ mod tests { use crate::display_map::{ fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap, }; - use gpui::{div, font, px, AppContext, Context as _, Element}; + use gpui::{div, font, px, App, AppContext as _, Element}; use itertools::Itertools; use language::{Buffer, Capability}; use multi_buffer::{ExcerptRange, MultiBuffer}; @@ -2154,7 +2152,10 @@ mod tests { ); assert_eq!( - snapshot.buffer_rows(BlockRow(0)).collect::>(), + snapshot + .row_infos(BlockRow(0)) + .map(|row_info| row_info.buffer_row) + .collect::>(), &[ Some(0), None, @@ -2188,15 +2189,15 @@ mod tests { } #[gpui::test] - fn test_multibuffer_headers_and_footers(cx: &mut AppContext) { + fn test_multibuffer_headers_and_footers(cx: &mut App) { init_test(cx); - let buffer1 = cx.new_model(|cx| Buffer::local("Buffer 1", cx)); - let buffer2 = cx.new_model(|cx| Buffer::local("Buffer 2", cx)); - let buffer3 = cx.new_model(|cx| Buffer::local("Buffer 3", cx)); + let buffer1 = cx.new(|cx| Buffer::local("Buffer 1", cx)); + let buffer2 = cx.new(|cx| Buffer::local("Buffer 2", cx)); + let buffer3 = cx.new(|cx| Buffer::local("Buffer 3", cx)); let mut excerpt_ids = Vec::new(); - let multi_buffer = cx.new_model(|cx| { + let multi_buffer = cx.new(|cx| { let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite); excerpt_ids.extend(multi_buffer.push_excerpts( buffer1.clone(), @@ -2604,7 +2605,10 @@ mod tests { "\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2680,7 +2684,10 @@ mod tests { "\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2755,7 +2762,10 @@ mod tests { "\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2820,7 +2830,10 @@ mod tests { ); assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n"); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2874,7 +2887,10 @@ mod tests { "Should have extra newline for 111 buffer, due to a new block added when it was folded" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2928,7 +2944,10 @@ mod tests { "Should have a single, first buffer left after folding" ); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![ None, None, @@ -2998,7 +3017,10 @@ mod tests { ); assert_eq!(blocks_snapshot.text(), "\n"); assert_eq!( - blocks_snapshot.buffer_rows(BlockRow(0)).collect::>(), + blocks_snapshot + .row_infos(BlockRow(0)) + .map(|i| i.buffer_row) + .collect::>(), vec![None, None], "When fully folded, should be no buffer rows" ); @@ -3296,7 +3318,8 @@ mod tests { let mut sorted_blocks_iter = expected_blocks.into_iter().peekable(); let input_buffer_rows = buffer_snapshot - .buffer_rows(MultiBufferRow(0)) + .row_infos(MultiBufferRow(0)) + .map(|row| row.buffer_row) .collect::>(); let mut expected_buffer_rows = Vec::new(); let mut expected_text = String::new(); @@ -3451,7 +3474,8 @@ mod tests { ); assert_eq!( blocks_snapshot - .buffer_rows(BlockRow(start_row as u32)) + .row_infos(BlockRow(start_row as u32)) + .map(|row_info| row_info.buffer_row) .collect::>(), &expected_buffer_rows[start_row..], "incorrect buffer_rows starting at row {:?}", @@ -3622,7 +3646,7 @@ mod tests { } } - fn init_test(cx: &mut gpui::AppContext) { + fn init_test(cx: &mut gpui::App) { let settings = SettingsStore::test(cx); cx.set_global(settings); theme::init(theme::LoadThemes::JustBase, cx); diff --git a/crates/editor/src/display_map/crease_map.rs b/crates/editor/src/display_map/crease_map.rs index 77d1401e75581d..af11a30ea097e4 100644 --- a/crates/editor/src/display_map/crease_map.rs +++ b/crates/editor/src/display_map/crease_map.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, fmt::Debug, ops::Range, sync::Arc}; use sum_tree::{Bias, SeekTarget, SumTree}; use text::Point; -use ui::{IconName, SharedString, WindowContext}; +use ui::{App, IconName, SharedString, Window}; use crate::{BlockStyle, FoldPlaceholder, RenderBlock}; @@ -117,12 +117,13 @@ type RenderToggleFn = Arc< + Fn( MultiBufferRow, bool, - Arc, - &mut WindowContext, + Arc, + &mut Window, + &mut App, ) -> AnyElement, >; type RenderTrailerFn = - Arc AnyElement>; + Arc AnyElement>; #[derive(Clone)] pub enum Crease { @@ -185,26 +186,27 @@ impl Crease { + Fn( MultiBufferRow, bool, - Arc, - &mut WindowContext, + Arc, + &mut Window, + &mut App, ) -> ToggleElement + 'static, ToggleElement: IntoElement, RenderTrailer: 'static + Send + Sync - + Fn(MultiBufferRow, bool, &mut WindowContext) -> TrailerElement + + Fn(MultiBufferRow, bool, &mut Window, &mut App) -> TrailerElement + 'static, TrailerElement: IntoElement, { Crease::Inline { range, placeholder, - render_toggle: Some(Arc::new(move |row, folded, toggle, cx| { - render_toggle(row, folded, toggle, cx).into_any_element() + render_toggle: Some(Arc::new(move |row, folded, toggle, window, cx| { + render_toggle(row, folded, toggle, window, cx).into_any_element() })), - render_trailer: Some(Arc::new(move |row, folded, cx| { - render_trailer(row, folded, cx).into_any_element() + render_trailer: Some(Arc::new(move |row, folded, window, cx| { + render_trailer(row, folded, window, cx).into_any_element() })), metadata: None, } @@ -387,11 +389,11 @@ impl SeekTarget<'_, ItemSummary, ItemSummary> for Anchor { #[cfg(test)] mod test { use super::*; - use gpui::{div, AppContext}; + use gpui::{div, App}; use multi_buffer::MultiBuffer; #[gpui::test] - fn test_insert_and_remove_creases(cx: &mut AppContext) { + fn test_insert_and_remove_creases(cx: &mut App) { let text = "line1\nline2\nline3\nline4\nline5"; let buffer = MultiBuffer::build_simple(text, cx); let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); @@ -402,14 +404,14 @@ mod test { Crease::inline( snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)), FoldPlaceholder::test(), - |_row, _folded, _toggle, _cx| div(), - |_row, _folded, _cx| div(), + |_row, _folded, _toggle, _window, _cx| div(), + |_row, _folded, _window, _cx| div(), ), Crease::inline( snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)), FoldPlaceholder::test(), - |_row, _folded, _toggle, _cx| div(), - |_row, _folded, _cx| div(), + |_row, _folded, _toggle, _window, _cx| div(), + |_row, _folded, _window, _cx| div(), ), ]; let crease_ids = crease_map.insert(creases, &snapshot); @@ -438,7 +440,7 @@ mod test { } #[gpui::test] - fn test_creases_in_range(cx: &mut AppContext) { + fn test_creases_in_range(cx: &mut App) { let text = "line1\nline2\nline3\nline4\nline5\nline6\nline7"; let buffer = MultiBuffer::build_simple(text, cx); let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); @@ -448,20 +450,20 @@ mod test { Crease::inline( snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)), FoldPlaceholder::test(), - |_row, _folded, _toggle, _cx| div(), - |_row, _folded, _cx| div(), + |_row, _folded, _toggle, _window, _cx| div(), + |_row, _folded, _window, _cx| div(), ), Crease::inline( snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)), FoldPlaceholder::test(), - |_row, _folded, _toggle, _cx| div(), - |_row, _folded, _cx| div(), + |_row, _folded, _toggle, _window, _cx| div(), + |_row, _folded, _window, _cx| div(), ), Crease::inline( snapshot.anchor_before(Point::new(5, 0))..snapshot.anchor_after(Point::new(5, 5)), FoldPlaceholder::test(), - |_row, _folded, _toggle, _cx| div(), - |_row, _folded, _cx| div(), + |_row, _folded, _toggle, _window, _cx| div(), + |_row, _folded, _window, _cx| div(), ), ]; crease_map.insert(creases, &snapshot); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index c4bb4080e2613f..6ec334c975cc61 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -2,9 +2,11 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, Highlights, }; -use gpui::{AnyElement, ElementId, WindowContext}; +use gpui::{AnyElement, App, ElementId, Window}; use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary}; -use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset}; +use multi_buffer::{ + Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, +}; use std::{ any::TypeId, cmp::{self, Ordering}, @@ -19,7 +21,8 @@ use util::post_inc; #[derive(Clone)] pub struct FoldPlaceholder { /// Creates an element to represent this fold's placeholder. - pub render: Arc, &mut WindowContext) -> AnyElement>, + pub render: + Arc, &mut Window, &mut App) -> AnyElement>, /// If true, the element is constrained to the shaped width of an ellipsis. pub constrain_width: bool, /// If true, merges the fold with an adjacent one. @@ -31,7 +34,7 @@ pub struct FoldPlaceholder { impl Default for FoldPlaceholder { fn default() -> Self { Self { - render: Arc::new(|_, _, _| gpui::Empty.into_any_element()), + render: Arc::new(|_, _, _, _| gpui::Empty.into_any_element()), constrain_width: true, merge_adjacent: true, type_tag: None, @@ -43,7 +46,7 @@ impl FoldPlaceholder { #[cfg(any(test, feature = "test-support"))] pub fn test() -> Self { Self { - render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()), + render: Arc::new(|_id, _range, _window, _cx| gpui::Empty.into_any_element()), constrain_width: true, merge_adjacent: true, type_tag: None, @@ -336,9 +339,7 @@ impl FoldMap { let mut folds = self.snapshot.folds.iter().peekable(); while let Some(fold) = folds.next() { if let Some(next_fold) = folds.peek() { - let comparison = fold - .range - .cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer); + let comparison = fold.range.cmp(&next_fold.range, self.snapshot.buffer()); assert!(comparison.is_le()); } } @@ -485,7 +486,8 @@ impl FoldMap { (fold.placeholder.render)( fold_id, fold.range.0.clone(), - cx, + cx.window, + cx.context, ) }), constrain_width: fold.placeholder.constrain_width, @@ -578,6 +580,10 @@ pub struct FoldSnapshot { } impl FoldSnapshot { + pub fn buffer(&self) -> &MultiBufferSnapshot { + &self.inlay_snapshot.buffer + } + #[cfg(test)] pub fn text(&self) -> String { self.chunks(FoldOffset(0)..self.len(), false, Highlights::default()) @@ -673,7 +679,7 @@ impl FoldSnapshot { (line_end - line_start) as u32 } - pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows { + pub fn row_infos(&self, start_row: u32) -> FoldRows { if start_row > self.transforms.summary().output.lines.row { panic!("invalid display row {}", start_row); } @@ -684,11 +690,11 @@ impl FoldSnapshot { let overshoot = fold_point.0 - cursor.start().0 .0; let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot); - let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row()); + let input_rows = self.inlay_snapshot.row_infos(inlay_point.row()); - FoldBufferRows { + FoldRows { fold_point, - input_buffer_rows, + input_rows, cursor, } } @@ -843,8 +849,8 @@ fn push_isomorphic(transforms: &mut SumTree, summary: TextSummary) { transforms.update_last( |last| { if !last.is_fold() { - last.summary.input += summary.clone(); - last.summary.output += summary.clone(); + last.summary.input += summary; + last.summary.output += summary; did_merge = true; } }, @@ -854,7 +860,7 @@ fn push_isomorphic(transforms: &mut SumTree, summary: TextSummary) { transforms.push( Transform { summary: TransformSummary { - input: summary.clone(), + input: summary, output: summary, }, placeholder: None, @@ -1134,25 +1140,25 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { } #[derive(Clone)] -pub struct FoldBufferRows<'a> { +pub struct FoldRows<'a> { cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, - input_buffer_rows: InlayBufferRows<'a>, + input_rows: InlayBufferRows<'a>, fold_point: FoldPoint, } -impl<'a> FoldBufferRows<'a> { +impl<'a> FoldRows<'a> { pub(crate) fn seek(&mut self, row: u32) { let fold_point = FoldPoint::new(row, 0); self.cursor.seek(&fold_point, Bias::Left, &()); let overshoot = fold_point.0 - self.cursor.start().0 .0; let inlay_point = InlayPoint(self.cursor.start().1 .0 + overshoot); - self.input_buffer_rows.seek(inlay_point.row()); + self.input_rows.seek(inlay_point.row()); self.fold_point = fold_point; } } -impl<'a> Iterator for FoldBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for FoldRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { let mut traversed_fold = false; @@ -1166,11 +1172,11 @@ impl<'a> Iterator for FoldBufferRows<'a> { if self.cursor.item().is_some() { if traversed_fold { - self.input_buffer_rows.seek(self.cursor.start().1.row()); - self.input_buffer_rows.next(); + self.input_rows.seek(self.cursor.start().1 .0.row); + self.input_rows.next(); } *self.fold_point.row_mut() += 1; - self.input_buffer_rows.next() + self.input_rows.next() } else { None } @@ -1391,7 +1397,7 @@ mod tests { use Bias::{Left, Right}; #[gpui::test] - fn test_basic_folds(cx: &mut gpui::AppContext) { + fn test_basic_folds(cx: &mut gpui::App) { init_test(cx); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); @@ -1470,7 +1476,7 @@ mod tests { } #[gpui::test] - fn test_adjacent_folds(cx: &mut gpui::AppContext) { + fn test_adjacent_folds(cx: &mut gpui::App) { init_test(cx); let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); @@ -1529,7 +1535,7 @@ mod tests { } #[gpui::test] - fn test_overlapping_folds(cx: &mut gpui::AppContext) { + fn test_overlapping_folds(cx: &mut gpui::App) { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); @@ -1546,7 +1552,7 @@ mod tests { } #[gpui::test] - fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) { + fn test_merging_folds_via_edit(cx: &mut gpui::App) { init_test(cx); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); @@ -1573,7 +1579,7 @@ mod tests { } #[gpui::test] - fn test_folds_in_range(cx: &mut gpui::AppContext) { + fn test_folds_in_range(cx: &mut gpui::App) { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); @@ -1604,7 +1610,7 @@ mod tests { } #[gpui::test(iterations = 100)] - fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) { + fn test_random_folds(cx: &mut gpui::App, mut rng: StdRng) { init_test(cx); let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) @@ -1683,12 +1689,12 @@ mod tests { .row(); expected_buffer_rows.extend( inlay_snapshot - .buffer_rows(prev_row) + .row_infos(prev_row) .take((1 + fold_start - prev_row) as usize), ); prev_row = 1 + fold_end; } - expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row)); + expected_buffer_rows.extend(inlay_snapshot.row_infos(prev_row)); assert_eq!( expected_buffer_rows.len(), @@ -1777,7 +1783,7 @@ mod tests { let mut fold_row = 0; while fold_row < expected_buffer_rows.len() as u32 { assert_eq!( - snapshot.buffer_rows(fold_row).collect::>(), + snapshot.row_infos(fold_row).collect::>(), expected_buffer_rows[(fold_row as usize)..], "wrong buffer rows starting at fold row {}", fold_row, @@ -1875,7 +1881,7 @@ mod tests { } #[gpui::test] - fn test_buffer_rows(cx: &mut gpui::AppContext) { + fn test_buffer_rows(cx: &mut gpui::App) { let text = sample_text(6, 6, 'a') + "\n"; let buffer = MultiBuffer::build_simple(&text, cx); @@ -1892,13 +1898,22 @@ mod tests { let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n"); assert_eq!( - snapshot.buffer_rows(0).collect::>(), + snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), [Some(0), Some(3), Some(5), Some(6)] ); - assert_eq!(snapshot.buffer_rows(3).collect::>(), [Some(6)]); + assert_eq!( + snapshot + .row_infos(3) + .map(|info| info.buffer_row) + .collect::>(), + [Some(6)] + ); } - fn init_test(cx: &mut gpui::AppContext) { + fn init_test(cx: &mut gpui::App) { let store = SettingsStore::test(cx); cx.set_global(store); } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index cbf75943bac101..02b0d21ef6e092 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,7 +1,9 @@ use crate::{HighlightStyles, InlayId}; use collections::BTreeSet; use language::{Chunk, Edit, Point, TextSummary}; -use multi_buffer::{Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset}; +use multi_buffer::{ + Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset, +}; use std::{ cmp, ops::{Add, AddAssign, Range, Sub, SubAssign}, @@ -67,11 +69,11 @@ impl Inlay { impl sum_tree::Item for Transform { type Summary = TransformSummary; - fn summary(&self, _cx: &()) -> Self::Summary { + fn summary(&self, _: &()) -> Self::Summary { match self { Transform::Isomorphic(summary) => TransformSummary { - input: summary.clone(), - output: summary.clone(), + input: *summary, + output: *summary, }, Transform::Inlay(inlay) => TransformSummary { input: TextSummary::default(), @@ -362,14 +364,14 @@ impl<'a> InlayBufferRows<'a> { } impl<'a> Iterator for InlayBufferRows<'a> { - type Item = Option; + type Item = RowInfo; fn next(&mut self) -> Option { let buffer_row = if self.inlay_row == 0 { self.buffer_rows.next().unwrap() } else { match self.transforms.item()? { - Transform::Inlay(_) => None, + Transform::Inlay(_) => Default::default(), Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(), } }; @@ -448,7 +450,7 @@ impl InlayMap { new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &()); if let Some(Transform::Isomorphic(transform)) = cursor.item() { if cursor.end(&()).0 == buffer_edit.old.start { - push_isomorphic(&mut new_transforms, transform.clone()); + push_isomorphic(&mut new_transforms, *transform); cursor.next(&()); } } @@ -892,7 +894,7 @@ impl InlaySnapshot { } pub fn text_summary(&self) -> TextSummary { - self.transforms.summary().output.clone() + self.transforms.summary().output } pub fn text_summary_for_range(&self, range: Range) -> TextSummary { @@ -945,7 +947,7 @@ impl InlaySnapshot { summary } - pub fn buffer_rows(&self, row: u32) -> InlayBufferRows<'_> { + pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> { let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); let inlay_point = InlayPoint::new(row, 0); cursor.seek(&inlay_point, Bias::Left, &()); @@ -967,7 +969,7 @@ impl InlaySnapshot { InlayBufferRows { transforms: cursor, inlay_row: inlay_point.row(), - buffer_rows: self.buffer.buffer_rows(buffer_row), + buffer_rows: self.buffer.row_infos(buffer_row), max_buffer_row, } } @@ -1068,7 +1070,7 @@ mod tests { hover_links::InlayHighlight, InlayId, MultiBuffer, }; - use gpui::{AppContext, HighlightStyle}; + use gpui::{App, HighlightStyle}; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; @@ -1161,7 +1163,7 @@ mod tests { } #[gpui::test] - fn test_basic_inlays(cx: &mut AppContext) { + fn test_basic_inlays(cx: &mut App) { let buffer = MultiBuffer::build_simple("abcdefghi", cx); let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); @@ -1449,7 +1451,7 @@ mod tests { } #[gpui::test] - fn test_inlay_buffer_rows(cx: &mut AppContext) { + fn test_inlay_buffer_rows(cx: &mut App) { let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx); let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi"); @@ -1477,13 +1479,16 @@ mod tests { ); assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi"); assert_eq!( - inlay_snapshot.buffer_rows(0).collect::>(), + inlay_snapshot + .row_infos(0) + .map(|info| info.buffer_row) + .collect::>(), vec![Some(0), None, Some(1), None, None, Some(2)] ); } #[gpui::test(iterations = 100)] - fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) { + fn test_random_inlays(cx: &mut App, mut rng: StdRng) { init_test(cx); let operations = env::var("OPERATIONS") @@ -1548,7 +1553,7 @@ mod tests { } assert_eq!(inlay_snapshot.text(), expected_text.to_string()); - let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::>(); + let expected_buffer_rows = inlay_snapshot.row_infos(0).collect::>(); assert_eq!( expected_buffer_rows.len() as u32, expected_text.max_point().row + 1 @@ -1556,7 +1561,7 @@ mod tests { for row_start in 0..expected_buffer_rows.len() { assert_eq!( inlay_snapshot - .buffer_rows(row_start as u32) + .row_infos(row_start as u32) .collect::>(), &expected_buffer_rows[row_start..], "incorrect buffer rows starting at {}", @@ -1787,7 +1792,7 @@ mod tests { } } - fn init_test(cx: &mut AppContext) { + fn init_test(cx: &mut App) { let store = SettingsStore::test(cx); cx.set_global(store); theme::init(theme::LoadThemes::JustBase, cx); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 86fa492712a066..824f848aed6015 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -272,8 +272,8 @@ impl TabSnapshot { } } - pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> { - self.fold_snapshot.buffer_rows(row) + pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> { + self.fold_snapshot.row_infos(row) } #[cfg(test)] @@ -608,7 +608,7 @@ mod tests { use rand::{prelude::StdRng, Rng}; #[gpui::test] - fn test_expand_tabs(cx: &mut gpui::AppContext) { + fn test_expand_tabs(cx: &mut gpui::App) { let buffer = MultiBuffer::build_simple("", cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); @@ -621,7 +621,7 @@ mod tests { } #[gpui::test] - fn test_long_lines(cx: &mut gpui::AppContext) { + fn test_long_lines(cx: &mut gpui::App) { let max_expansion_column = 12; let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM"; let output = "A BC DEF G HI J K L M"; @@ -669,7 +669,7 @@ mod tests { } #[gpui::test] - fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::AppContext) { + fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::App) { let max_expansion_column = 8; let input = "abcdefg⋯hij"; @@ -684,7 +684,7 @@ mod tests { } #[gpui::test] - fn test_marking_tabs(cx: &mut gpui::AppContext) { + fn test_marking_tabs(cx: &mut gpui::App) { let input = "\t \thello"; let buffer = MultiBuffer::build_simple(input, cx); @@ -735,7 +735,7 @@ mod tests { } #[gpui::test(iterations = 100)] - fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) { + fn test_random_tabs(cx: &mut gpui::App, mut rng: StdRng) { let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); let len = rng.gen_range(0..30); let buffer = if rng.gen() { diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index ceb91ce0ab24c5..c1510cd23156c6 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,11 +1,11 @@ use super::{ - fold_map::FoldBufferRows, + fold_map::FoldRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, Highlights, }; -use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task}; +use gpui::{App, AppContext as _, Context, Entity, Font, LineWrapper, Pixels, Task}; use language::{Chunk, Point}; -use multi_buffer::MultiBufferSnapshot; +use multi_buffer::{MultiBufferSnapshot, RowInfo}; use smol::future::yield_now; use std::sync::LazyLock; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; @@ -60,16 +60,16 @@ pub struct WrapChunks<'a> { } #[derive(Clone)] -pub struct WrapBufferRows<'a> { - input_buffer_rows: FoldBufferRows<'a>, - input_buffer_row: Option, +pub struct WrapRows<'a> { + input_buffer_rows: FoldRows<'a>, + input_buffer_row: RowInfo, output_row: u32, soft_wrapped: bool, max_output_row: u32, transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, } -impl<'a> WrapBufferRows<'a> { +impl<'a> WrapRows<'a> { pub(crate) fn seek(&mut self, start_row: u32) { self.transforms .seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); @@ -90,9 +90,9 @@ impl WrapMap { font: Font, font_size: Pixels, wrap_width: Option, - cx: &mut AppContext, - ) -> (Model, WrapSnapshot) { - let handle = cx.new_model(|cx| { + cx: &mut App, + ) -> (Entity, WrapSnapshot) { + let handle = cx.new(|cx| { let mut this = Self { font_with_size: (font, font_size), wrap_width: None, @@ -119,7 +119,7 @@ impl WrapMap { &mut self, tab_snapshot: TabSnapshot, edits: Vec, - cx: &mut ModelContext, + cx: &mut Context, ) -> (WrapSnapshot, Patch) { if self.wrap_width.is_some() { self.pending_edits.push_back((tab_snapshot, edits)); @@ -138,7 +138,7 @@ impl WrapMap { &mut self, font: Font, font_size: Pixels, - cx: &mut ModelContext, + cx: &mut Context, ) -> bool { let font_with_size = (font, font_size); @@ -151,11 +151,7 @@ impl WrapMap { } } - pub fn set_wrap_width( - &mut self, - wrap_width: Option, - cx: &mut ModelContext, - ) -> bool { + pub fn set_wrap_width(&mut self, wrap_width: Option, cx: &mut Context) -> bool { if wrap_width == self.wrap_width { return false; } @@ -165,7 +161,7 @@ impl WrapMap { true } - fn rewrap(&mut self, cx: &mut ModelContext) { + fn rewrap(&mut self, cx: &mut Context) { self.background_task.take(); self.interpolated_edits.clear(); self.pending_edits.clear(); @@ -236,7 +232,7 @@ impl WrapMap { } } - fn flush_edits(&mut self, cx: &mut ModelContext) { + fn flush_edits(&mut self, cx: &mut Context) { if !self.snapshot.interpolated { let mut to_remove_len = 0; for (tab_snapshot, _) in &self.pending_edits { @@ -717,7 +713,7 @@ impl WrapSnapshot { self.transforms.summary().output.longest_row } - pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows { + pub fn row_infos(&self, start_row: u32) -> WrapRows { let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); let mut input_row = transforms.start().1.row(); @@ -725,9 +721,9 @@ impl WrapSnapshot { input_row += start_row - transforms.start().0.row(); } let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic()); - let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row); + let mut input_buffer_rows = self.tab_snapshot.rows(input_row); let input_buffer_row = input_buffer_rows.next().unwrap(); - WrapBufferRows { + WrapRows { transforms, input_buffer_row, input_buffer_rows, @@ -847,7 +843,7 @@ impl WrapSnapshot { } let text = language::Rope::from(self.text().as_str()); - let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0); + let mut input_buffer_rows = self.tab_snapshot.rows(0); let mut expected_buffer_rows = Vec::new(); let mut prev_tab_row = 0; for display_row in 0..=self.max_point().row() { @@ -855,7 +851,7 @@ impl WrapSnapshot { if tab_point.row() == prev_tab_row && display_row != 0 { expected_buffer_rows.push(None); } else { - expected_buffer_rows.push(input_buffer_rows.next().unwrap()); + expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row); } prev_tab_row = tab_point.row(); @@ -864,7 +860,8 @@ impl WrapSnapshot { for start_display_row in 0..expected_buffer_rows.len() { assert_eq!( - self.buffer_rows(start_display_row as u32) + self.row_infos(start_display_row as u32) + .map(|row_info| row_info.buffer_row) .collect::>(), &expected_buffer_rows[start_display_row..], "invalid buffer_rows({}..)", @@ -958,8 +955,8 @@ impl<'a> Iterator for WrapChunks<'a> { } } -impl<'a> Iterator for WrapBufferRows<'a> { - type Item = Option; +impl<'a> Iterator for WrapRows<'a> { + type Item = RowInfo; fn next(&mut self) -> Option { if self.output_row > self.max_output_row { @@ -968,6 +965,7 @@ impl<'a> Iterator for WrapBufferRows<'a> { let buffer_row = self.input_buffer_row; let soft_wrapped = self.soft_wrapped; + let diff_status = self.input_buffer_row.diff_status; self.output_row += 1; self.transforms @@ -979,7 +977,15 @@ impl<'a> Iterator for WrapBufferRows<'a> { self.soft_wrapped = true; } - Some(if soft_wrapped { None } else { buffer_row }) + Some(if soft_wrapped { + RowInfo { + buffer_row: None, + multibuffer_row: None, + diff_status, + } + } else { + buffer_row + }) } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 75f7846ae6311b..234d7b558af168 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,7 +25,6 @@ mod git; mod highlight_matching_bracket; mod hover_links; mod hover_popover; -mod hunk_diff; mod indent_guides; mod inlay_hint_cache; pub mod items; @@ -56,7 +55,7 @@ use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; use client::{Collaborator, ParticipantIndex}; use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; +use collections::{BTreeMap, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use display_map::*; pub use display_map::{DisplayPoint, FoldPlaceholder}; @@ -70,6 +69,7 @@ pub use element::{ }; use futures::{future, FutureExt}; use fuzzy::StringMatchCandidate; +use zed_predict_tos::ZedPredictTos; use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, @@ -77,20 +77,17 @@ use code_context_menus::{ }; use git::blame::GitBlame; use gpui::{ - div, impl_actions, point, prelude::*, px, relative, size, Action, AnyElement, AppContext, + div, impl_actions, point, prelude::*, px, relative, size, Action, AnyElement, App, AsyncWindowContext, AvailableSpace, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, - DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, FocusableView, FontId, - FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, ModelContext, + DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, + Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, - UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle, - WeakView, WindowContext, + UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; use hover_popover::{hide_hover, HoverState}; -pub(crate) use hunk_diff::HoveredHunk; -use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion::Direction; @@ -100,8 +97,9 @@ use itertools::Itertools; use language::{ language_settings::{self, all_language_settings, language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, - CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt, - Point, Selection, SelectionGoal, TransactionId, + CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize, + Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, + TreeSitterOptions, }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; @@ -123,17 +121,17 @@ use lsp::{ LanguageServerId, LanguageServerName, }; +use language::BufferSnapshot; use movement::TextLayoutDetails; pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, - ToPoint, + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo, + ToOffset, ToPoint, }; use multi_buffer::{ ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16, }; use parking_lot::Mutex; use project::{ - buffer_store::BufferChangeSet, dap_store::{Breakpoint, BreakpointKind, DapStore}, lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, project_settings::{GitGutterSetting, ProjectSettings}, @@ -169,7 +167,7 @@ use text::{BufferId, OffsetUtf16, Rope}; use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings}; use ui::{ h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, - PopoverMenuHandle, Tooltip, + Tooltip, }; use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::item::{ItemHandle, PreviewTabsSettings}; @@ -201,8 +199,8 @@ pub fn render_parsed_markdown( element_id: impl Into, parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, - workspace: Option>, - cx: &mut WindowContext, + workspace: Option>, + cx: &mut App, ) -> InteractiveText { let code_span_background_color = cx .theme() @@ -246,18 +244,21 @@ pub fn render_parsed_markdown( element_id, StyledText::new(parsed.text.clone()).with_highlights(&editor_style.text, highlights), ) - .on_click(link_ranges, move |clicked_range_ix, cx| { - match &links[clicked_range_ix] { + .on_click( + link_ranges, + move |clicked_range_ix, window, cx| match &links[clicked_range_ix] { markdown::Link::Web { url } => cx.open_url(url), markdown::Link::Path { path } => { if let Some(workspace) = &workspace { _ = workspace.update(cx, |workspace, cx| { - workspace.open_abs_path(path.clone(), false, cx).detach(); + workspace + .open_abs_path(path.clone(), false, window, cx) + .detach(); }); } } - } - }) + }, + ) } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -276,7 +277,6 @@ impl InlayId { } pub enum DebugCurrentRowHighlight {} -enum DiffRowHighlight {} enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} @@ -297,19 +297,19 @@ impl Navigated { } } -pub fn init_settings(cx: &mut AppContext) { +pub fn init_settings(cx: &mut App) { EditorSettings::register(cx); } -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &mut App) { init_settings(cx); workspace::register_project_item::(cx); workspace::FollowableViewRegistry::register::(cx); workspace::register_serializable_item::(cx); - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { + cx.observe_new( + |workspace: &mut Workspace, _: Option<&mut Window>, _cx: &mut Context| { workspace.register_action(Editor::new_file); workspace.register_action(Editor::new_file_vertical); workspace.register_action(Editor::new_file_horizontal); @@ -320,18 +320,28 @@ pub fn init(cx: &mut AppContext) { cx.on_action(move |_: &workspace::NewFile, cx| { let app_state = workspace::AppState::global(cx); if let Some(app_state) = app_state.upgrade() { - workspace::open_new(Default::default(), app_state, cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) + workspace::open_new( + Default::default(), + app_state, + cx, + |workspace, window, cx| { + Editor::new_file(workspace, &Default::default(), window, cx) + }, + ) .detach(); } }); cx.on_action(move |_: &workspace::NewWindow, cx| { let app_state = workspace::AppState::global(cx); if let Some(app_state) = app_state.upgrade() { - workspace::open_new(Default::default(), app_state, cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) + workspace::open_new( + Default::default(), + app_state, + cx, + |workspace, window, cx| { + Editor::new_file(workspace, &Default::default(), window, cx) + }, + ) .detach(); } }); @@ -435,7 +445,7 @@ impl Default for EditorStyle { } } -pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle { +pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { let show_background = language_settings::language_settings(None, None, cx) .inlay_hints .show_background; @@ -447,7 +457,7 @@ pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle { } } -pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles { +pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { InlineCompletionStyles { insertion: HighlightStyle { color: Some(cx.theme().status().predictive), @@ -466,6 +476,7 @@ type CompletionId = usize; enum InlineCompletionMenuHint { Loading, Loaded { text: InlineCompletionText }, + PendingTermsAcceptance, None, } @@ -475,6 +486,7 @@ impl InlineCompletionMenuHint { InlineCompletionMenuHint::Loading | InlineCompletionMenuHint::Loaded { .. } => { "Edit Prediction" } + InlineCompletionMenuHint::PendingTermsAcceptance => "Accept Terms of Service", InlineCompletionMenuHint::None => "No Prediction", } } @@ -483,14 +495,22 @@ impl InlineCompletionMenuHint { #[derive(Clone, Debug)] enum InlineCompletionText { Move(SharedString), - Edit { - text: SharedString, - highlights: Vec<(Range, HighlightStyle)>, - }, + Edit(HighlightedEdits), +} + +pub(crate) enum EditDisplayMode { + TabAccept, + DiffPopover, + Inline, } enum InlineCompletion { - Edit(Vec<(Range, String)>), + Edit { + edits: Vec<(Range, String)>, + edit_preview: Option, + display_mode: EditDisplayMode, + snapshot: BufferSnapshot, + }, Move(Anchor), } @@ -502,6 +522,11 @@ struct InlineCompletionState { enum InlineCompletionHighlight {} +pub enum MenuInlineCompletionsPolicy { + Never, + ByProvider, +} + #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)] struct EditorActionId(usize); @@ -519,7 +544,7 @@ impl EditorActionId { // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range]>); -type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range]>); +type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range]>); #[derive(Default)] struct ScrollbarMarkerState { @@ -573,7 +598,7 @@ struct BufferOffset(usize); // Addons allow storing per-editor state in other crates (e.g. Vim) pub trait Addon: 'static { - fn extend_key_context(&self, _: &mut KeyContext, _: &AppContext) {} + fn extend_key_context(&self, _: &mut KeyContext, _: &App) {} fn to_any(&self) -> &dyn std::any::Any; } @@ -584,17 +609,17 @@ pub enum IsVimMode { No, } -/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`] +/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. /// /// See the [module level documentation](self) for more information. pub struct Editor { focus_handle: FocusHandle, last_focused_descendant: Option, /// The text buffer being edited - buffer: Model, + buffer: Entity, /// Map of how text in the buffer should be displayed. /// Handles soft wraps, folds, fake inlay text insertions, etc. - pub display_map: Model, + pub display_map: Entity, pub selections: SelectionsCollection, pub scroll_manager: ScrollManager, /// When inline assist editors are linked, they all render cursors because @@ -612,11 +637,11 @@ pub struct Editor { active_diagnostics: Option, soft_wrap_mode_override: Option, - project: Option>, + project: Option>, semantics_provider: Option>, completion_provider: Option>, collaboration_hub: Option>, - blink_manager: Model, + blink_manager: Entity, show_cursor_names: bool, hovered_cursors: HashMap>, pub show_local_selections: bool, @@ -641,7 +666,6 @@ pub struct Editor { nav_history: Option, context_menu: RefCell>, mouse_context_menu: Option, - hunk_controls_menu_handle: PopoverMenuHandle, completion_tasks: Vec<(CompletionId, Task>)>, signature_help_state: SignatureHelpState, auto_signature_help: Option, @@ -658,7 +682,7 @@ pub struct Editor { current_line_highlight: Option, collapse_matches: bool, autoindent_mode: Option, - workspace: Option<(WeakView, Option)>, + workspace: Option<(WeakEntity, Option)>, input_enabled: bool, use_modal_editing: bool, read_only: bool, @@ -674,8 +698,8 @@ pub struct Editor { // inline completions based on its mode. enable_inline_completions: bool, show_inline_completions_override: Option, + menu_inline_completions_policy: MenuInlineCompletionsPolicy, inlay_hint_cache: InlayHintCache, - diff_map: DiffMap, next_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, @@ -683,7 +707,8 @@ pub struct Editor { style: Option, text_style_refinement: Option, next_editor_action_id: EditorActionId, - editor_actions: Rc)>>>>, + editor_actions: + Rc)>>>>, use_autoclose: bool, use_auto_surround: bool, auto_replace_emoji_shortcode: bool, @@ -693,19 +718,24 @@ pub struct Editor { git_blame_inline_enabled: bool, serialize_dirty_buffers: bool, show_selection_menu: Option, - blame: Option>, + blame: Option>, blame_subscription: Option, custom_context_menu: Option< Box< dyn 'static - + Fn(&mut Self, DisplayPoint, &mut ViewContext) -> Option>, + + Fn( + &mut Self, + DisplayPoint, + &mut Window, + &mut Context, + ) -> Option>, >, >, last_bounds: Option>, expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - pub dap_store: Option>, + pub dap_store: Option>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown @@ -716,6 +746,7 @@ pub struct Editor { next_scroll_position: NextScrollCursorCenterTopBottom, addons: HashMap>, registered_buffers: HashMap, + selection_mark_mode: bool, toggle_fold_multiple_buffers: Task<()>, _scroll_cursor_center_top_bottom_task: Task<()>, } @@ -749,7 +780,6 @@ pub struct EditorSnapshot { git_blame_gutter_max_author_length: Option, pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, - diff_map: DiffMapSnapshot, is_focused: bool, scroll_anchor: ScrollAnchor, ongoing_scroll: OngoingScroll, @@ -942,7 +972,7 @@ struct SnippetState { pub struct RenameState { pub range: Range, pub old_name: Arc, - pub editor: View, + pub editor: Entity, block_id: CustomBlockId, } @@ -1032,74 +1062,92 @@ enum JumpData { }, } +pub enum MultibufferSelectionMode { + First, + All, +} + impl Editor { - pub fn single_line(cx: &mut ViewContext) -> Self { - let buffer = cx.new_model(|cx| Buffer::local("", cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + pub fn single_line(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( EditorMode::SingleLine { auto_width: false }, buffer, None, false, + window, cx, ) } - pub fn multi_line(cx: &mut ViewContext) -> Self { - let buffer = cx.new_model(|cx| Buffer::local("", cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::Full, buffer, None, false, cx) + pub fn multi_line(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, None, false, window, cx) } - pub fn auto_width(cx: &mut ViewContext) -> Self { - let buffer = cx.new_model(|cx| Buffer::local("", cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + pub fn auto_width(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( EditorMode::SingleLine { auto_width: true }, buffer, None, false, + window, cx, ) } - pub fn auto_height(max_lines: usize, cx: &mut ViewContext) -> Self { - let buffer = cx.new_model(|cx| Buffer::local("", cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( EditorMode::AutoHeight { max_lines }, buffer, None, false, + window, cx, ) } pub fn for_buffer( - buffer: Model, - project: Option>, - cx: &mut ViewContext, + buffer: Entity, + project: Option>, + window: &mut Window, + cx: &mut Context, ) -> Self { - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::Full, buffer, project, false, cx) + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, project, false, window, cx) } pub fn for_multibuffer( - buffer: Model, - project: Option>, + buffer: Entity, + project: Option>, show_excerpt_controls: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { - Self::new(EditorMode::Full, buffer, project, show_excerpt_controls, cx) + Self::new( + EditorMode::Full, + buffer, + project, + show_excerpt_controls, + window, + cx, + ) } - pub fn clone(&self, cx: &mut ViewContext) -> Self { + pub fn clone(&self, window: &mut Window, cx: &mut Context) -> Self { let show_excerpt_controls = self.display_map.read(cx).show_excerpt_controls(); let mut clone = Self::new( self.mode, self.buffer.clone(), self.project.clone(), show_excerpt_controls, + window, cx, ); self.display_map.update(cx, |display_map, cx| { @@ -1116,17 +1164,18 @@ impl Editor { pub fn new( mode: EditorMode, - buffer: Model, - project: Option>, + buffer: Entity, + project: Option>, show_excerpt_controls: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { - let style = cx.text_style(); - let font_size = style.font_size.to_pixels(cx.rem_size()); - let editor = cx.view().downgrade(); + let style = window.text_style(); + let font_size = style.font_size.to_pixels(window.rem_size()); + let editor = cx.entity().downgrade(); let fold_placeholder = FoldPlaceholder { constrain_width: true, - render: Arc::new(move |fold_id, fold_range, cx| { + render: Arc::new(move |fold_id, fold_range, _, cx| { let editor = editor.clone(); div() .id(fold_id) @@ -1137,8 +1186,8 @@ impl Editor { .size_full() .cursor_pointer() .child("⋯") - .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) - .on_click(move |_, cx| { + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(move |_, _window, cx| { editor .update(cx, |editor, cx| { editor.unfold_ranges( @@ -1156,7 +1205,7 @@ impl Editor { merge_adjacent: true, ..Default::default() }; - let display_map = cx.new_model(|cx| { + let display_map = cx.new(|cx| { DisplayMap::new( buffer.clone(), style.font(), @@ -1173,7 +1222,7 @@ impl Editor { let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); - let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); + let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) .then(|| language_settings::SoftWrap::None); @@ -1182,31 +1231,41 @@ impl Editor { if mode == EditorMode::Full { if let Some(project) = project.as_ref() { if buffer.read(cx).is_singleton() { - project_subscriptions.push(cx.observe(project, |_, _, cx| { + project_subscriptions.push(cx.observe_in(project, window, |_, _, _, cx| { cx.emit(EditorEvent::TitleChanged); })); } - project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { - if let project::Event::RefreshInlayHints = event { - editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - } else if let project::Event::SnippetEdit(id, snippet_edits) = event { - if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { - let focus_handle = editor.focus_handle(cx); - if focus_handle.is_focused(cx) { - let snapshot = buffer.read(cx).snapshot(); - for (range, snippet) in snippet_edits { - let editor_range = - language::range_from_lsp(*range).to_offset(&snapshot); - editor - .insert_snippet(&[editor_range], snippet.clone(), cx) - .ok(); + project_subscriptions.push(cx.subscribe_in( + project, + window, + |editor, _, event, window, cx| { + if let project::Event::RefreshInlayHints = event { + editor + .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + } else if let project::Event::SnippetEdit(id, snippet_edits) = event { + if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { + let focus_handle = editor.focus_handle(cx); + if focus_handle.is_focused(window) { + let snapshot = buffer.read(cx).snapshot(); + for (range, snippet) in snippet_edits { + let editor_range = + language::range_from_lsp(*range).to_offset(&snapshot); + editor + .insert_snippet( + &[editor_range], + snippet.clone(), + window, + cx, + ) + .ok(); + } } } + } else if let project::Event::ActiveDebugLineChanged = event { + editor.go_to_active_debug_line(window, cx); } - } else if let project::Event::ActiveDebugLineChanged = event { - editor.go_to_active_debug_line(cx); - } - })); + }, + )); if let Some(task_inventory) = project .read(cx) .task_store() @@ -1214,9 +1273,13 @@ impl Editor { .task_inventory() .cloned() { - project_subscriptions.push(cx.observe(&task_inventory, |editor, _, cx| { - editor.tasks_update_task = Some(editor.refresh_runnables(cx)); - })); + project_subscriptions.push(cx.observe_in( + &task_inventory, + window, + |editor, _, window, cx| { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); + }, + )); } } } @@ -1226,12 +1289,14 @@ impl Editor { let inlay_hint_settings = inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, Self::handle_focus).detach(); - cx.on_focus_in(&focus_handle, Self::handle_focus_in) + cx.on_focus(&focus_handle, window, Self::handle_focus) + .detach(); + cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) + .detach(); + cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) .detach(); - cx.on_focus_out(&focus_handle, Self::handle_focus_out) + cx.on_blur(&focus_handle, window, Self::handle_blur) .detach(); - cx.on_blur(&focus_handle, Self::handle_blur).detach(); let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) { Some(false) @@ -1246,7 +1311,12 @@ impl Editor { }; let mut code_action_providers = Vec::new(); if let Some(project) = project.clone() { - get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx); + get_unstaged_changes_for_buffers( + &project, + buffer.read(cx).all_buffers(), + buffer.clone(), + cx, + ); code_action_providers.push(Rc::new(project) as Rc<_>); } @@ -1296,7 +1366,6 @@ impl Editor { nav_history: None, context_menu: RefCell::new(None), mouse_context_menu: None, - hunk_controls_menu_handle: PopoverMenuHandle::default(), completion_tasks: Default::default(), signature_help_state: SignatureHelpState::default(), auto_signature_help: None, @@ -1330,7 +1399,7 @@ impl Editor { inline_completion_provider: None, active_inline_completion: None, inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), - diff_map: DiffMap::default(), + gutter_hovered: false, pixel_position_of_newest_cursor: None, last_bounds: None, @@ -1343,6 +1412,7 @@ impl Editor { editor_actions: Rc::default(), show_inline_completions_override: None, enable_inline_completions: true, + menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, custom_context_menu: None, show_git_blame_gutter: false, show_git_blame_inline: false, @@ -1359,12 +1429,12 @@ impl Editor { gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe(&buffer, Self::on_buffer_event), - cx.observe(&display_map, Self::on_display_map_changed), + cx.subscribe_in(&buffer, window, Self::on_buffer_event), + cx.observe_in(&display_map, window, Self::on_display_map_changed), cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global::(Self::settings_changed), - cx.observe_window_activation(|editor, cx| { - let active = cx.is_window_active(); + cx.observe_global_in::(window, Self::settings_changed), + cx.observe_window_activation(window, |editor, window, cx| { + let active = window.is_window_active(); editor.blink_manager.update(cx, |blink_manager, cx| { if active { blink_manager.enable(cx); @@ -1383,14 +1453,15 @@ impl Editor { addons: HashMap::default(), registered_buffers: HashMap::default(), _scroll_cursor_center_top_bottom_task: Task::ready(()), + selection_mark_mode: false, toggle_fold_multiple_buffers: Task::ready(()), text_style_refinement: None, }; - this.tasks_update_task = Some(this.refresh_runnables(cx)); + this.tasks_update_task = Some(this.refresh_runnables(window, cx)); this._subscriptions.extend(project_subscriptions); - this.end_selection(cx); - this.scroll_manager.show_scrollbar(cx); + this.end_selection(window, cx); + this.scroll_manager.show_scrollbar(window, cx); if mode == EditorMode::Full { let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); @@ -1398,10 +1469,10 @@ impl Editor { if this.git_blame_inline_enabled { this.git_blame_inline_enabled = true; - this.start_git_blame_inline(false, cx); + this.start_git_blame_inline(false, window, cx); } - this.go_to_active_debug_line(cx); + this.go_to_active_debug_line(window, cx); if let Some(buffer) = buffer.read(cx).as_singleton() { if let Some(project) = this.project.as_ref() { @@ -1419,13 +1490,13 @@ impl Editor { this } - pub fn mouse_menu_is_focused(&self, cx: &WindowContext) -> bool { + pub fn mouse_menu_is_focused(&self, window: &mut Window, cx: &mut App) -> bool { self.mouse_context_menu .as_ref() - .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(cx)) + .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) } - fn key_context(&self, cx: &ViewContext) -> KeyContext { + fn key_context(&self, window: &mut Window, cx: &mut Context) -> KeyContext { let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); let mode = match self.mode { @@ -1455,8 +1526,8 @@ impl Editor { } // Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused. - if !self.focus_handle(cx).contains_focused(cx) - || (self.is_focused(cx) || self.mouse_menu_is_focused(cx)) + if !self.focus_handle(cx).contains_focused(window, cx) + || (self.is_focused(window) || self.mouse_menu_is_focused(window, cx)) { for addon in self.addons.values() { addon.extend_key_context(&mut key_context, cx) @@ -1477,13 +1548,8 @@ impl Editor { key_context.add("inline_completion"); } - if !self - .selections - .disjoint - .iter() - .all(|selection| selection.start == selection.end) - { - key_context.add("selection"); + if self.selection_mark_mode { + key_context.add("selection_mode"); } key_context @@ -1492,12 +1558,14 @@ impl Editor { pub fn new_file( workspace: &mut Workspace, _: &workspace::NewFile, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - Self::new_in_workspace(workspace, cx).detach_and_prompt_err( + Self::new_in_workspace(workspace, window, cx).detach_and_prompt_err( "Failed to create buffer", + window, cx, - |e, _| match e.error_code() { + |e, _, _| match e.error_code() { ErrorCode::RemoteUpgradeRequired => Some(format!( "The remote instance of Zed does not support this yet. It must be upgraded to {}", e.error_tag("required").unwrap_or("the latest version") @@ -1509,17 +1577,18 @@ impl Editor { pub fn new_in_workspace( workspace: &mut Workspace, - cx: &mut ViewContext, - ) -> Task>> { + window: &mut Window, + cx: &mut Context, + ) -> Task>> { let project = workspace.project().clone(); let create = project.update(cx, |project, cx| project.create_buffer(cx)); - cx.spawn(|workspace, mut cx| async move { + cx.spawn_in(window, |workspace, mut cx| async move { let buffer = create.await?; - workspace.update(&mut cx, |workspace, cx| { + workspace.update_in(&mut cx, |workspace, window, cx| { let editor = - cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx); + cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); editor }) }) @@ -1528,46 +1597,52 @@ impl Editor { fn new_file_vertical( workspace: &mut Workspace, _: &workspace::NewFileSplitVertical, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - Self::new_file_in_direction(workspace, SplitDirection::vertical(cx), cx) + Self::new_file_in_direction(workspace, SplitDirection::vertical(cx), window, cx) } fn new_file_horizontal( workspace: &mut Workspace, _: &workspace::NewFileSplitHorizontal, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), cx) + Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), window, cx) } fn new_file_in_direction( workspace: &mut Workspace, direction: SplitDirection, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let project = workspace.project().clone(); let create = project.update(cx, |project, cx| project.create_buffer(cx)); - cx.spawn(|workspace, mut cx| async move { + cx.spawn_in(window, |workspace, mut cx| async move { let buffer = create.await?; - workspace.update(&mut cx, move |workspace, cx| { + workspace.update_in(&mut cx, move |workspace, window, cx| { workspace.split_item( direction, Box::new( - cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), + cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)), ), + window, cx, ) })?; anyhow::Ok(()) }) - .detach_and_prompt_err("Failed to create buffer", cx, |e, _| match e.error_code() { - ErrorCode::RemoteUpgradeRequired => Some(format!( + .detach_and_prompt_err("Failed to create buffer", window, cx, |e, _, _| { + match e.error_code() { + ErrorCode::RemoteUpgradeRequired => Some(format!( "The remote instance of Zed does not support this yet. It must be upgraded to {}", e.error_tag("required").unwrap_or("the latest version") )), - _ => None, + _ => None, + } }); } @@ -1575,19 +1650,19 @@ impl Editor { self.leader_peer_id } - pub fn buffer(&self) -> &Model { + pub fn buffer(&self) -> &Entity { &self.buffer } - pub fn workspace(&self) -> Option> { + pub fn workspace(&self) -> Option> { self.workspace.as_ref()?.0.upgrade() } - pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { + pub fn title<'a>(&self, cx: &'a App) -> Cow<'a, str> { self.buffer().read(cx).title(cx) } - pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot { + pub fn snapshot(&self, window: &mut Window, cx: &mut App) -> EditorSnapshot { let git_blame_gutter_max_author_length = self .render_git_blame_gutter(cx) .then(|| { @@ -1613,8 +1688,7 @@ impl Editor { scroll_anchor: self.scroll_manager.anchor(), ongoing_scroll: self.scroll_manager.ongoing_scroll(), placeholder_text: self.placeholder_text.clone(), - diff_map: self.diff_map.snapshot(), - is_focused: self.focus_handle.is_focused(cx), + is_focused: self.focus_handle.is_focused(window), current_line_highlight: self .current_line_highlight .unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight), @@ -1622,22 +1696,18 @@ impl Editor { } } - pub fn language_at(&self, point: T, cx: &AppContext) -> Option> { + pub fn language_at(&self, point: T, cx: &App) -> Option> { self.buffer.read(cx).language_at(point, cx) } - pub fn file_at( - &self, - point: T, - cx: &AppContext, - ) -> Option> { + pub fn file_at(&self, point: T, cx: &App) -> Option> { self.buffer.read(cx).read(cx).file_at(point).cloned() } pub fn active_excerpt( &self, - cx: &AppContext, - ) -> Option<(ExcerptId, Model, Range)> { + cx: &App, + ) -> Option<(ExcerptId, Entity, Range)> { self.buffer .read(cx) .excerpt_containing(self.selections.newest_anchor().head(), cx) @@ -1658,7 +1728,12 @@ impl Editor { pub fn set_custom_context_menu( &mut self, f: impl 'static - + Fn(&mut Self, DisplayPoint, &mut ViewContext) -> Option>, + + Fn( + &mut Self, + DisplayPoint, + &mut Window, + &mut Context, + ) -> Option>, ) { self.custom_context_menu = Some(Box::new(f)) } @@ -1677,31 +1752,32 @@ impl Editor { pub fn set_inline_completion_provider( &mut self, - provider: Option>, - cx: &mut ViewContext, + provider: Option>, + window: &mut Window, + cx: &mut Context, ) where T: InlineCompletionProvider, { self.inline_completion_provider = provider.map(|provider| RegisteredInlineCompletionProvider { - _subscription: cx.observe(&provider, |this, _, cx| { - if this.focus_handle.is_focused(cx) { - this.update_visible_inline_completion(cx); + _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { + if this.focus_handle.is_focused(window) { + this.update_visible_inline_completion(window, cx); } }), provider: Arc::new(provider), }); - self.refresh_inline_completion(false, false, cx); + self.refresh_inline_completion(false, false, window, cx); } - pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> { + pub fn placeholder_text(&self) -> Option<&str> { self.placeholder_text.as_deref() } pub fn set_placeholder_text( &mut self, placeholder_text: impl Into>, - cx: &mut ViewContext, + cx: &mut Context, ) { let placeholder_text = Some(placeholder_text.into()); if self.placeholder_text != placeholder_text { @@ -1710,7 +1786,7 @@ impl Editor { } } - pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { + pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut Context) { self.cursor_shape = cursor_shape; // Disrupt blink for immediate user feedback that the cursor shape has changed @@ -1730,7 +1806,7 @@ impl Editor { self.collapse_matches = collapse_matches; } - pub fn register_buffers_with_language_servers(&mut self, cx: &mut ViewContext) { + pub fn register_buffers_with_language_servers(&mut self, cx: &mut Context) { let buffers = self.buffer.read(cx).all_buffers(); let Some(lsp_store) = self.lsp_store(cx) else { return; @@ -1753,7 +1829,7 @@ impl Editor { range.clone() } - pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context) { if self.display_map.read(cx).clip_at_line_ends != clip { self.display_map .update(cx, |map, _| map.clip_at_line_ends = clip); @@ -1764,7 +1840,7 @@ impl Editor { self.input_enabled = input_enabled; } - pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut ViewContext) { + pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut Context) { self.enable_inline_completions = enabled; if !self.enable_inline_completions { self.take_active_inline_completion(cx); @@ -1772,6 +1848,10 @@ impl Editor { } } + pub fn set_menu_inline_completions_policy(&mut self, value: MenuInlineCompletionsPolicy) { + self.menu_inline_completions_policy = value; + } + pub fn set_autoindent(&mut self, autoindent: bool) { if autoindent { self.autoindent_mode = Some(AutoindentMode::EachLine); @@ -1780,7 +1860,7 @@ impl Editor { } } - pub fn read_only(&self, cx: &AppContext) -> bool { + pub fn read_only(&self, cx: &App) -> bool { self.read_only || self.buffer.read(cx).read_only() } @@ -1803,10 +1883,11 @@ impl Editor { pub fn toggle_inline_completions( &mut self, _: &ToggleInlineCompletions, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if self.show_inline_completions_override.is_some() { - self.set_show_inline_completions(None, cx); + self.set_show_inline_completions(None, window, cx); } else { let cursor = self.selections.newest_anchor().head(); if let Some((buffer, cursor_buffer_position)) = @@ -1814,7 +1895,7 @@ impl Editor { { let show_inline_completions = !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx); - self.set_show_inline_completions(Some(show_inline_completions), cx); + self.set_show_inline_completions(Some(show_inline_completions), window, cx); } } } @@ -1822,13 +1903,14 @@ impl Editor { pub fn set_show_inline_completions( &mut self, show_inline_completions: Option, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { self.show_inline_completions_override = show_inline_completions; - self.refresh_inline_completion(false, true, cx); + self.refresh_inline_completion(false, true, window, cx); } - pub fn inline_completions_enabled(&self, cx: &AppContext) -> bool { + pub fn inline_completions_enabled(&self, cx: &App) -> bool { let cursor = self.selections.newest_anchor().head(); if let Some((buffer, buffer_position)) = self.buffer.read(cx).text_anchor_for_position(cursor, cx) @@ -1841,9 +1923,9 @@ impl Editor { fn should_show_inline_completions( &self, - buffer: &Model, + buffer: &Entity, buffer_position: language::Anchor, - cx: &AppContext, + cx: &App, ) -> bool { if !self.snippet_stack.is_empty() { return false; @@ -1866,9 +1948,9 @@ impl Editor { fn inline_completions_disabled_in_scope( &self, - buffer: &Model, + buffer: &Entity, buffer_position: language::Anchor, - cx: &AppContext, + cx: &App, ) -> bool { let snapshot = buffer.read(cx).snapshot(); let settings = snapshot.settings_at(buffer_position, cx); @@ -1898,9 +1980,10 @@ impl Editor { local: bool, old_cursor_position: &Anchor, show_completions: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - cx.invalidate_character_coordinates(); + window.invalidate_character_coordinates(); // Copy selections to primary selection buffer #[cfg(any(target_os = "linux", target_os = "freebsd"))] @@ -1925,7 +2008,7 @@ impl Editor { } } - if self.focus_handle.is_focused(cx) && self.leader_peer_id.is_none() { + if self.focus_handle.is_focused(window) && self.leader_peer_id.is_none() { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections( &self.selections.disjoint_anchors(), @@ -1946,7 +2029,7 @@ impl Editor { self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.snippet_stack .invalidate(&self.selections.disjoint_anchors(), buffer); - self.take_rename(false, cx); + self.take_rename(false, window, cx); let new_cursor_position = self.selections.newest_anchor().head(); @@ -2002,11 +2085,11 @@ impl Editor { .detach(); if show_completions { - self.show_completions(&ShowCompletions { trigger: None }, cx); + self.show_completions(&ShowCompletions { trigger: None }, window, cx); } } else { drop(context_menu); - self.hide_context_menu(cx); + self.hide_context_menu(window, cx); } } else { drop(context_menu); @@ -2019,13 +2102,13 @@ impl Editor { { self.available_code_actions.take(); } - self.refresh_code_actions(cx); + self.refresh_code_actions(window, cx); self.refresh_document_highlights(cx); - refresh_matching_bracket_highlights(self, cx); - self.update_visible_inline_completion(cx); - linked_editing_ranges::refresh_linked_ranges(self, cx); + refresh_matching_bracket_highlights(self, window, cx); + self.update_visible_inline_completion(window, cx); + linked_editing_ranges::refresh_linked_ranges(self, window, cx); if self.git_blame_inline_enabled { - self.start_inline_blame_timer(cx); + self.start_inline_blame_timer(window, cx); } } @@ -2041,17 +2124,19 @@ impl Editor { pub fn change_selections( &mut self, autoscroll: Option, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, ) -> R { - self.change_selections_inner(autoscroll, true, cx, change) + self.change_selections_inner(autoscroll, true, window, cx, change) } pub fn change_selections_inner( &mut self, autoscroll: Option, request_completions: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, ) -> R { let old_cursor_position = self.selections.newest_anchor().head(); @@ -2063,14 +2148,14 @@ impl Editor { if let Some(autoscroll) = autoscroll { self.request_autoscroll(autoscroll, cx); } - self.selections_did_change(true, &old_cursor_position, request_completions, cx); + self.selections_did_change(true, &old_cursor_position, request_completions, window, cx); if self.should_open_signature_help_automatically( &old_cursor_position, self.signature_help_state.backspace_pressed(), cx, ) { - self.show_signature_help(&ShowSignatureHelp, cx); + self.show_signature_help(&ShowSignatureHelp, window, cx); } self.signature_help_state.set_backspace_pressed(false); } @@ -2078,7 +2163,7 @@ impl Editor { result } - pub fn edit(&mut self, edits: I, cx: &mut ViewContext) + pub fn edit(&mut self, edits: I, cx: &mut Context) where I: IntoIterator, T)>, S: ToOffset, @@ -2092,7 +2177,7 @@ impl Editor { .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } - pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ViewContext) + pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut Context) where I: IntoIterator, T)>, S: ToOffset, @@ -2111,7 +2196,7 @@ impl Editor { &mut self, edits: I, original_indent_columns: Vec, - cx: &mut ViewContext, + cx: &mut Context, ) where I: IntoIterator, T)>, S: ToOffset, @@ -2132,30 +2217,30 @@ impl Editor { }); } - fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext) { - self.hide_context_menu(cx); + fn select(&mut self, phase: SelectPhase, window: &mut Window, cx: &mut Context) { + self.hide_context_menu(window, cx); match phase { SelectPhase::Begin { position, add, click_count, - } => self.begin_selection(position, add, click_count, cx), + } => self.begin_selection(position, add, click_count, window, cx), SelectPhase::BeginColumnar { position, goal_column, reset, - } => self.begin_columnar_selection(position, goal_column, reset, cx), + } => self.begin_columnar_selection(position, goal_column, reset, window, cx), SelectPhase::Extend { position, click_count, - } => self.extend_selection(position, click_count, cx), + } => self.extend_selection(position, click_count, window, cx), SelectPhase::Update { position, goal_column, scroll_delta, - } => self.update_selection(position, goal_column, scroll_delta, cx), - SelectPhase::End => self.end_selection(cx), + } => self.update_selection(position, goal_column, scroll_delta, window, cx), + SelectPhase::End => self.end_selection(window, cx), } } @@ -2163,11 +2248,12 @@ impl Editor { &mut self, position: DisplayPoint, click_count: usize, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let tail = self.selections.newest::(cx).tail(); - self.begin_selection(position, false, click_count, cx); + self.begin_selection(position, false, click_count, window, cx); let position = position.to_offset(&display_map, Bias::Left); let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); @@ -2189,7 +2275,7 @@ impl Editor { _ => {} } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.set_pending(pending_selection, pending_mode) }); } @@ -2199,11 +2285,12 @@ impl Editor { position: DisplayPoint, add: bool, click_count: usize, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - if !self.focus_handle.is_focused(cx) { + if !self.focus_handle.is_focused(window) { self.last_focused_descendant = None; - cx.focus(&self.focus_handle); + window.focus(&self.focus_handle); } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -2273,7 +2360,7 @@ impl Editor { let selections_count = self.selections.count(); - self.change_selections(auto_scroll.then(Autoscroll::newest), cx, |s| { + self.change_selections(auto_scroll.then(Autoscroll::newest), window, cx, |s| { if let Some(point_to_delete) = point_to_delete { s.delete(point_to_delete); @@ -2297,11 +2384,12 @@ impl Editor { position: DisplayPoint, goal_column: u32, reset: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - if !self.focus_handle.is_focused(cx) { + if !self.focus_handle.is_focused(window) { self.last_focused_descendant = None; - cx.focus(&self.focus_handle); + window.focus(&self.focus_handle); } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -2311,7 +2399,7 @@ impl Editor { .buffer_snapshot .anchor_before(position.to_point(&display_map)); - self.change_selections(Some(Autoscroll::newest()), cx, |s| { + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { s.clear_disjoint(); s.set_pending_anchor_range( pointer_position..pointer_position, @@ -2329,6 +2417,7 @@ impl Editor { position, goal_column, &display_map, + window, cx, ); } @@ -2339,13 +2428,14 @@ impl Editor { position: DisplayPoint, goal_column: u32, scroll_delta: gpui::Point, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if let Some(tail) = self.columnar_selection_tail.as_ref() { let tail = tail.to_display_point(&display_map); - self.select_columns(tail, position, goal_column, &display_map, cx); + self.select_columns(tail, position, goal_column, &display_map, window, cx); } else if let Some(mut pending) = self.selections.pending_anchor() { let buffer = self.buffer.read(cx).snapshot(cx); let head; @@ -2419,7 +2509,7 @@ impl Editor { pending.reversed = false; } - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { s.set_pending(pending, mode); }); } else { @@ -2427,15 +2517,15 @@ impl Editor { return; } - self.apply_scroll_delta(scroll_delta, cx); + self.apply_scroll_delta(scroll_delta, window, cx); cx.notify(); } - fn end_selection(&mut self, cx: &mut ViewContext) { + fn end_selection(&mut self, window: &mut Window, cx: &mut Context) { self.columnar_selection_tail.take(); if self.selections.pending_anchor().is_some() { let selections = self.selections.all::(cx); - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { s.select(selections); s.clear_pending(); }); @@ -2448,7 +2538,8 @@ impl Editor { head: DisplayPoint, goal_column: u32, display_map: &DisplaySnapshot, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let start_row = cmp::min(tail.row(), head.row()); let end_row = cmp::max(tail.row(), head.row()); @@ -2477,7 +2568,7 @@ impl Editor { }) .collect::>(); - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { s.select_ranges(selection_ranges); }); cx.notify(); @@ -2497,17 +2588,19 @@ impl Editor { self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() } - pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { + self.selection_mark_mode = false; + if self.clear_expanded_diff_hunks(cx) { cx.notify(); return; } - if self.dismiss_menus_and_popups(true, cx) { + if self.dismiss_menus_and_popups(true, window, cx) { return; } if self.mode == EditorMode::Full - && self.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()) + && self.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel()) { return; } @@ -2518,9 +2611,10 @@ impl Editor { pub fn dismiss_menus_and_popups( &mut self, should_report_inline_completion_event: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> bool { - if self.take_rename(false, cx).is_some() { + if self.take_rename(false, window, cx).is_some() { return true; } @@ -2532,9 +2626,9 @@ impl Editor { return true; } - if self.hide_context_menu(cx).is_some() { + if self.hide_context_menu(window, cx).is_some() { if self.show_inline_completions_in_menu(cx) && self.has_active_inline_completion() { - self.update_visible_inline_completion(cx); + self.update_visible_inline_completion(window, cx); } return true; } @@ -2562,8 +2656,8 @@ impl Editor { fn linked_editing_ranges_for( &self, selection: Range, - cx: &AppContext, - ) -> Option, Vec>>> { + cx: &App, + ) -> Option, Vec>>> { if self.linked_edit_ranges.is_empty() { return None; } @@ -2616,7 +2710,7 @@ impl Editor { Some(linked_edits) } - pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { + pub fn handle_input(&mut self, text: &str, window: &mut Window, cx: &mut Context) { let text: Arc = text.into(); if self.read_only(cx) { @@ -2854,7 +2948,7 @@ impl Editor { drop(snapshot); - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |buffer, cx| { buffer.edit(edits, this.autoindent_mode.clone(), cx); }); @@ -2917,13 +3011,13 @@ impl Editor { } let had_active_inline_completion = this.has_active_inline_completion(); - this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| { + this.change_selections_inner(Some(Autoscroll::fit()), false, window, cx, |s| { s.select(new_selections) }); if !bracket_inserted { if let Some(on_type_format_task) = - this.trigger_on_type_formatting(text.to_string(), cx) + this.trigger_on_type_formatting(text.to_string(), window, cx) { on_type_format_task.detach_and_log_err(cx); } @@ -2934,14 +3028,14 @@ impl Editor { && (editor_settings.auto_signature_help || editor_settings.show_signature_help_after_edits) { - this.show_signature_help(&ShowSignatureHelp, cx); + this.show_signature_help(&ShowSignatureHelp, window, cx); } let trigger_in_words = this.show_inline_completions_in_menu(cx) || !had_active_inline_completion; - this.trigger_completion_on_input(&text, trigger_in_words, cx); - linked_editing_ranges::refresh_linked_ranges(this, cx); - this.refresh_inline_completion(true, false, cx); + this.trigger_completion_on_input(&text, trigger_in_words, window, cx); + linked_editing_ranges::refresh_linked_ranges(this, window, cx); + this.refresh_inline_completion(true, false, window, cx); }); } @@ -2992,8 +3086,8 @@ impl Editor { Some(chars.iter().collect()) } - pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { - self.transact(cx, |this, cx| { + pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context) { + self.transact(window, cx, |this, window, cx| { let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { let selections = this.selections.all::(cx); let multi_buffer = this.buffer.read(cx); @@ -3126,12 +3220,14 @@ impl Editor { }) .collect(); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); - this.refresh_inline_completion(true, false, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections) + }); + this.refresh_inline_completion(true, false, window, cx); }); } - pub fn newline_above(&mut self, _: &NewlineAbove, cx: &mut ViewContext) { + pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context) { let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -3150,10 +3246,10 @@ impl Editor { rows.push(row + rows_inserted as u32); } - self.transact(cx, |editor, cx| { + self.transact(window, cx, |editor, window, cx| { editor.edit(edits, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let mut index = 0; s.move_cursors_with(|map, _, _| { let row = rows[index]; @@ -3188,7 +3284,7 @@ impl Editor { }); } - pub fn newline_below(&mut self, _: &NewlineBelow, cx: &mut ViewContext) { + pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context) { let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -3210,10 +3306,10 @@ impl Editor { rows.push(row + rows_inserted); } - self.transact(cx, |editor, cx| { + self.transact(window, cx, |editor, window, cx| { editor.edit(edits, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let mut index = 0; s.move_cursors_with(|map, _, _| { let row = rows[index]; @@ -3248,25 +3344,26 @@ impl Editor { }); } - pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { + pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context) { let autoindent = text.is_empty().not().then(|| AutoindentMode::Block { original_indent_columns: Vec::new(), }); - self.insert_with_autoindent_mode(text, autoindent, cx); + self.insert_with_autoindent_mode(text, autoindent, window, cx); } fn insert_with_autoindent_mode( &mut self, text: &str, autoindent_mode: Option, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if self.read_only(cx) { return; } let text: Arc = text.into(); - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { let old_selections = this.selections.all_adjusted(cx); let selection_anchors = this.buffer.update(cx, |buffer, cx| { let anchors = { @@ -3289,9 +3386,11 @@ impl Editor { anchors }); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_anchors(selection_anchors); - }) + }); + + cx.notify(); }); } @@ -3299,17 +3398,19 @@ impl Editor { &mut self, text: &str, trigger_in_words: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if self.is_completion_trigger(text, trigger_in_words, cx) { self.show_completions( &ShowCompletions { trigger: Some(text.to_owned()).filter(|x| !x.is_empty()), }, + window, cx, ); } else { - self.hide_context_menu(cx); + self.hide_context_menu(window, cx); } } @@ -3317,7 +3418,7 @@ impl Editor { &self, text: &str, trigger_in_words: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool { let position = self.selections.newest_anchor().head(); let multibuffer = self.buffer.read(cx); @@ -3343,7 +3444,7 @@ impl Editor { /// If any empty selections is touching the start of its innermost containing autoclose /// region, expand it to select the brackets. - fn select_autoclose_pair(&mut self, cx: &mut ViewContext) { + fn select_autoclose_pair(&mut self, window: &mut Window, cx: &mut Context) { let selections = self.selections.all::(cx); let buffer = self.buffer.read(cx).read(cx); let new_selections = self @@ -3403,7 +3504,9 @@ impl Editor { .collect(); drop(buffer); - self.change_selections(None, cx, |selections| selections.select(new_selections)); + self.change_selections(None, window, cx, |selections| { + selections.select(new_selections) + }); } /// Iterate the given selections, and for each one, find the smallest surrounding @@ -3478,7 +3581,12 @@ impl Editor { } } - pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext) { + pub fn toggle_inlay_hints( + &mut self, + _: &ToggleInlayHints, + _: &mut Window, + cx: &mut Context, + ) { self.refresh_inlay_hints( InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled), cx, @@ -3489,7 +3597,7 @@ impl Editor { self.inlay_hint_cache.enabled } - fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext) { + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { if self.semantics_provider.is_none() || self.mode != EditorMode::Full { return; } @@ -3570,7 +3678,7 @@ impl Editor { } } - fn visible_inlay_hints(&self, cx: &ViewContext) -> Vec { + fn visible_inlay_hints(&self, cx: &Context) -> Vec { self.display_map .read(cx) .current_inlays() @@ -3582,8 +3690,8 @@ impl Editor { pub fn excerpts_for_inlay_hints_query( &self, restrict_to_languages: Option<&HashSet>>, - cx: &mut ViewContext, - ) -> HashMap, clock::Global, Range)> { + cx: &mut Context, + ) -> HashMap, clock::Global, Range)> { let Some(project) = self.project.as_ref() else { return HashMap::default(); }; @@ -3604,9 +3712,9 @@ impl Editor { multi_buffer_snapshot .range_to_buffer_ranges(multi_buffer_visible_range) .into_iter() - .filter(|(_, excerpt_visible_range)| !excerpt_visible_range.is_empty()) - .filter_map(|(excerpt, excerpt_visible_range)| { - let buffer_file = project::File::from_dyn(excerpt.buffer().file())?; + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) + .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| { + let buffer_file = project::File::from_dyn(buffer.file())?; let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; let worktree_entry = buffer_worktree .read(cx) @@ -3615,17 +3723,17 @@ impl Editor { return None; } - let language = excerpt.buffer().language()?; + let language = buffer.language()?; if let Some(restrict_to_languages) = restrict_to_languages { if !restrict_to_languages.contains(language) { return None; } } Some(( - excerpt.id(), + excerpt_id, ( - multi_buffer.buffer(excerpt.buffer_id()).unwrap(), - excerpt.buffer().version().clone(), + multi_buffer.buffer(buffer.remote_id()).unwrap(), + buffer.version().clone(), excerpt_visible_range, ), )) @@ -3633,11 +3741,11 @@ impl Editor { .collect() } - pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails { + pub fn text_layout_details(&self, window: &mut Window) -> TextLayoutDetails { TextLayoutDetails { - text_system: cx.text_system().clone(), + text_system: window.text_system().clone(), editor_style: self.style.clone().unwrap(), - rem_size: cx.rem_size(), + rem_size: window.rem_size(), scroll_anchor: self.scroll_manager.anchor(), visible_rows: self.visible_line_count(), vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin, @@ -3648,7 +3756,7 @@ impl Editor { &self, to_remove: Vec, to_insert: Vec, - cx: &mut ViewContext, + cx: &mut Context, ) { self.display_map.update(cx, |display_map, cx| { display_map.splice_inlays(to_remove, to_insert, cx) @@ -3659,7 +3767,8 @@ impl Editor { fn trigger_on_type_formatting( &self, input: String, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option>> { if input.len() != 1 { return None; @@ -3699,7 +3808,7 @@ impl Editor { cx, ) }); - Some(cx.spawn(|editor, mut cx| async move { + Some(cx.spawn_in(window, |editor, mut cx| async move { if let Some(transaction) = on_type_formatting.await? { if push_to_client_history { buffer @@ -3716,7 +3825,12 @@ impl Editor { })) } - pub fn show_completions(&mut self, options: &ShowCompletions, cx: &mut ViewContext) { + pub fn show_completions( + &mut self, + options: &ShowCompletions, + window: &mut Window, + cx: &mut Context, + ) { if self.pending_rename.is_some() { return; } @@ -3730,6 +3844,9 @@ impl Editor { } let position = self.selections.newest_anchor().head(); + if position.diff_base_anchor.is_some() { + return; + } let (buffer, buffer_position) = if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { output @@ -3760,11 +3877,12 @@ impl Editor { }), trigger_kind, }; - let completions = provider.completions(&buffer, buffer_position, completion_context, cx); + let completions = + provider.completions(&buffer, buffer_position, completion_context, window, cx); let sort_completions = provider.sort_completions(); let id = post_inc(&mut self.next_completion_id); - let task = cx.spawn(|editor, mut cx| { + let task = cx.spawn_in(window, |editor, mut cx| { async move { editor.update(&mut cx, |this, _| { this.completion_tasks.retain(|(task_id, _)| *task_id >= id); @@ -3788,7 +3906,7 @@ impl Editor { None }; - editor.update(&mut cx, |editor, cx| { + editor.update_in(&mut cx, |editor, window, cx| { match editor.context_menu.borrow().as_ref() { None => {} Some(CodeContextMenu::Completions(prev_menu)) => { @@ -3799,12 +3917,12 @@ impl Editor { _ => return, } - if editor.focus_handle.is_focused(cx) && menu.is_some() { + if editor.focus_handle.is_focused(window) && menu.is_some() { let mut menu = menu.unwrap(); menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx); if editor.show_inline_completions_in_menu(cx) { - if let Some(hint) = editor.inline_completion_menu_hint(cx) { + if let Some(hint) = editor.inline_completion_menu_hint(window, cx) { menu.show_inline_completion_hint(hint); } } else { @@ -3818,12 +3936,12 @@ impl Editor { } else if editor.completion_tasks.len() <= 1 { // If there are no more completion tasks and the last menu was // empty, we should hide it. - let was_hidden = editor.hide_context_menu(cx).is_none(); + let was_hidden = editor.hide_context_menu(window, cx).is_none(); // If it was already hidden and we don't show inline // completions in the menu, we should also show the // inline-completion when available. if was_hidden && editor.show_inline_completions_in_menu(cx) { - editor.update_visible_inline_completion(cx); + editor.update_visible_inline_completion(window, cx); } } })?; @@ -3839,24 +3957,35 @@ impl Editor { pub fn confirm_completion( &mut self, action: &ConfirmCompletion, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option>> { - self.do_completion(action.item_ix, CompletionIntent::Complete, cx) + self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) } pub fn compose_completion( &mut self, action: &ComposeCompletion, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option>> { - self.do_completion(action.item_ix, CompletionIntent::Compose, cx) + self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) + } + + fn toggle_zed_predict_tos(&mut self, window: &mut Window, cx: &mut Context) { + let (Some(workspace), Some(project)) = (self.workspace(), self.project.as_ref()) else { + return; + }; + + ZedPredictTos::toggle(workspace, project.read(cx).user_store().clone(), window, cx); } fn do_completion( &mut self, item_ix: Option, intent: CompletionIntent, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option>> { use language::ToOffset as _; @@ -3872,7 +4001,15 @@ impl Editor { Some(CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::None)) => { drop(entries); drop(context_menu); - self.context_menu_next(&Default::default(), cx); + self.context_menu_next(&Default::default(), window, cx); + return Some(Task::ready(Ok(()))); + } + Some(CompletionEntry::InlineCompletionHint( + InlineCompletionMenuHint::PendingTermsAcceptance, + )) => { + drop(entries); + drop(context_menu); + self.toggle_zed_predict_tos(window, cx); return Some(Task::ready(Ok(()))); } _ => {} @@ -3881,7 +4018,7 @@ impl Editor { } let completions_menu = - if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? { + if let CodeContextMenu::Completions(menu) = self.hide_context_menu(window, cx)? { menu } else { return None; @@ -3891,7 +4028,7 @@ impl Editor { let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; let mat = match mat { CompletionEntry::InlineCompletionHint(_) => { - self.accept_inline_completion(&AcceptInlineCompletion, cx); + self.accept_inline_completion(&AcceptInlineCompletion, window, cx); cx.stop_propagation(); return Some(Task::ready(Ok(()))); } @@ -4003,7 +4140,7 @@ impl Editor { text: text.into(), }); - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { if let Some(mut snippet) = snippet { snippet.text = text.to_string(); for tabstop in snippet @@ -4015,7 +4152,7 @@ impl Editor { tabstop.end -= common_prefix_len as isize; } - this.insert_snippet(&ranges, snippet, cx).log_err(); + this.insert_snippet(&ranges, snippet, window, cx).log_err(); } else { this.buffer.update(cx, |buffer, cx| { buffer.edit( @@ -4042,15 +4179,15 @@ impl Editor { }) } - this.refresh_inline_completion(true, false, cx); + this.refresh_inline_completion(true, false, window, cx); }); let show_new_completions_on_confirm = completion .confirm .as_ref() - .map_or(false, |confirm| confirm(intent, cx)); + .map_or(false, |confirm| confirm(intent, window, cx)); if show_new_completions_on_confirm { - self.show_completions(&ShowCompletions { trigger: None }, cx); + self.show_completions(&ShowCompletions { trigger: None }, window, cx); } let provider = self.completion_provider.as_ref()?; @@ -4067,7 +4204,7 @@ impl Editor { if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help { // After the code completion is finished, users often want to know what signatures are needed. // so we should automatically call signature_help - self.show_signature_help(&ShowSignatureHelp, cx); + self.show_signature_help(&ShowSignatureHelp, window, cx); } Some(cx.foreground_executor().spawn(async move { @@ -4076,7 +4213,12 @@ impl Editor { })) } - pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { + pub fn toggle_code_actions( + &mut self, + action: &ToggleCodeActions, + window: &mut Window, + cx: &mut Context, + ) { let mut context_menu = self.context_menu.borrow_mut(); if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { if code_actions.deployed_from_indicator == action.deployed_from_indicator { @@ -4091,18 +4233,18 @@ impl Editor { } } drop(context_menu); - let snapshot = self.snapshot(cx); + let snapshot = self.snapshot(window, cx); let deployed_from_indicator = action.deployed_from_indicator; let mut task = self.code_actions_task.take(); let action = action.clone(); - cx.spawn(|editor, mut cx| async move { + cx.spawn_in(window, |editor, mut cx| async move { while let Some(prev_task) = task { prev_task.await.log_err(); task = editor.update(&mut cx, |this, _| this.code_actions_task.take())?; } - let spawned_test_task = editor.update(&mut cx, |editor, cx| { - if editor.focus_handle.is_focused(cx) { + let spawned_test_task = editor.update_in(&mut cx, |editor, window, cx| { + if editor.focus_handle.is_focused(window) { let multibuffer_point = action .deployed_from_indicator .map(|row| DisplayPoint::new(row, 0).to_point(&snapshot)) @@ -4150,7 +4292,7 @@ impl Editor { Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) }); - Some(cx.spawn(|editor, mut cx| async move { + Some(cx.spawn_in(window, |editor, mut cx| async move { let task_context = match task_context { Some(task_context) => task_context.await, None => None, @@ -4171,7 +4313,7 @@ impl Editor { && code_actions .as_ref() .map_or(true, |actions| actions.is_empty()); - if let Ok(task) = editor.update(&mut cx, |editor, cx| { + if let Ok(task) = editor.update_in(&mut cx, |editor, window, cx| { *editor.context_menu.borrow_mut() = Some(CodeContextMenu::CodeActions(CodeActionsMenu { buffer, @@ -4186,6 +4328,7 @@ impl Editor { if spawn_straight_away { if let Some(task) = editor.confirm_code_action( &ConfirmCodeAction { item_ix: Some(0) }, + window, cx, ) { cx.notify(); @@ -4216,13 +4359,15 @@ impl Editor { pub fn confirm_code_action( &mut self, action: &ConfirmCodeAction, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option>> { - let actions_menu = if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(cx)? { - menu - } else { - return None; - }; + let actions_menu = + if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { + menu + } else { + return None; + }; let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); let action = actions_menu.actions.get(action_ix)?; let title = action.label(); @@ -4249,9 +4394,9 @@ impl Editor { provider, } => { let apply_code_action = - provider.apply_code_action(buffer, action, excerpt_id, true, cx); + provider.apply_code_action(buffer, action, excerpt_id, true, window, cx); let workspace = workspace.downgrade(); - Some(cx.spawn(|editor, cx| async move { + Some(cx.spawn_in(window, |editor, cx| async move { let project_transaction = apply_code_action.await?; Self::open_project_transaction( &editor, @@ -4267,14 +4412,14 @@ impl Editor { } pub async fn open_project_transaction( - this: &WeakView, - workspace: WeakView, + this: &WeakEntity, + workspace: WeakEntity, transaction: ProjectTransaction, title: String, mut cx: AsyncWindowContext, ) -> Result<()> { let mut entries = transaction.0.into_iter().collect::>(); - cx.update(|cx| { + cx.update(|_, cx| { entries.sort_unstable_by_key(|(buffer, _)| { buffer.read(cx).file().map(|f| f.path().clone()) }); @@ -4314,7 +4459,7 @@ impl Editor { } let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.new_model(|cx| { + let excerpt_buffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(Capability::ReadWrite).with_title(title); for (buffer_handle, transaction) in &entries { let buffer = buffer_handle.read(cx); @@ -4333,11 +4478,11 @@ impl Editor { multibuffer })?; - workspace.update(&mut cx, |workspace, cx| { + workspace.update_in(&mut cx, |workspace, window, cx| { let project = workspace.project().clone(); - let editor = - cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, cx)); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx); + let editor = cx + .new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, window, cx)); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); editor.update(cx, |editor, cx| { editor.highlight_background::( &ranges_to_highlight, @@ -4358,7 +4503,8 @@ impl Editor { pub fn add_code_action_provider( &mut self, provider: Rc, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if self .code_action_providers @@ -4369,35 +4515,43 @@ impl Editor { } self.code_action_providers.push(provider); - self.refresh_code_actions(cx); + self.refresh_code_actions(window, cx); } - pub fn remove_code_action_provider(&mut self, id: Arc, cx: &mut ViewContext) { + pub fn remove_code_action_provider( + &mut self, + id: Arc, + window: &mut Window, + cx: &mut Context, + ) { self.code_action_providers .retain(|provider| provider.id() != id); - self.refresh_code_actions(cx); + self.refresh_code_actions(window, cx); } - fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { + fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) -> Option<()> { let buffer = self.buffer.read(cx); let newest_selection = self.selections.newest_anchor().clone(); + if newest_selection.head().diff_base_anchor.is_some() { + return None; + } let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?; if start_buffer != end_buffer { return None; } - self.code_actions_task = Some(cx.spawn(|this, mut cx| async move { + self.code_actions_task = Some(cx.spawn_in(window, |this, mut cx| async move { cx.background_executor() .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) .await; - let (providers, tasks) = this.update(&mut cx, |this, cx| { + let (providers, tasks) = this.update_in(&mut cx, |this, window, cx| { let providers = this.code_action_providers.clone(); let tasks = this .code_action_providers .iter() - .map(|provider| provider.code_actions(&start_buffer, start..end, cx)) + .map(|provider| provider.code_actions(&start_buffer, start..end, window, cx)) .collect::>(); (providers, tasks) })?; @@ -4435,23 +4589,24 @@ impl Editor { None } - fn start_inline_blame_timer(&mut self, cx: &mut ViewContext) { + fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context) { if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() { self.show_git_blame_inline = false; - self.show_git_blame_inline_delay_task = Some(cx.spawn(|this, mut cx| async move { - cx.background_executor().timer(delay).await; + self.show_git_blame_inline_delay_task = + Some(cx.spawn_in(window, |this, mut cx| async move { + cx.background_executor().timer(delay).await; - this.update(&mut cx, |this, cx| { - this.show_git_blame_inline = true; - cx.notify(); - }) - .log_err(); - })); + this.update(&mut cx, |this, cx| { + this.show_git_blame_inline = true; + cx.notify(); + }) + .log_err(); + })); } } - fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> Option<()> { + fn refresh_document_highlights(&mut self, cx: &mut Context) -> Option<()> { if self.pending_rename.is_some() { return None; } @@ -4522,10 +4677,12 @@ impl Editor { buffer_id, excerpt_id, text_anchor: start, + diff_base_anchor: None, }..Anchor { buffer_id, excerpt_id, text_anchor: end, + diff_base_anchor: None, }; if highlight.kind == lsp::DocumentHighlightKind::WRITE { write_ranges.push(range); @@ -4557,7 +4714,8 @@ impl Editor { &mut self, debounce: bool, user_requested: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option<()> { let provider = self.inline_completion_provider()?; let cursor = self.selections.newest_anchor().head(); @@ -4567,14 +4725,14 @@ impl Editor { if !user_requested && (!self.enable_inline_completions || !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx) - || !self.is_focused(cx) + || !self.is_focused(window) || buffer.read(cx).is_empty()) { self.discard_inline_completion(false, cx); return None; } - self.update_visible_inline_completion(cx); + self.update_visible_inline_completion(window, cx); provider.refresh(buffer, cursor_buffer_position, debounce, cx); Some(()) } @@ -4582,7 +4740,8 @@ impl Editor { fn cycle_inline_completion( &mut self, direction: Direction, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option<()> { let provider = self.inline_completion_provider()?; let cursor = self.selections.newest_anchor().head(); @@ -4595,28 +4754,38 @@ impl Editor { } provider.cycle(buffer, cursor_buffer_position, direction, cx); - self.update_visible_inline_completion(cx); + self.update_visible_inline_completion(window, cx); Some(()) } - pub fn show_inline_completion(&mut self, _: &ShowInlineCompletion, cx: &mut ViewContext) { + pub fn show_inline_completion( + &mut self, + _: &ShowInlineCompletion, + window: &mut Window, + cx: &mut Context, + ) { if !self.has_active_inline_completion() { - self.refresh_inline_completion(false, true, cx); + self.refresh_inline_completion(false, true, window, cx); return; } - self.update_visible_inline_completion(cx); + self.update_visible_inline_completion(window, cx); } - pub fn display_cursor_names(&mut self, _: &DisplayCursorNames, cx: &mut ViewContext) { - self.show_cursor_names(cx); + pub fn display_cursor_names( + &mut self, + _: &DisplayCursorNames, + window: &mut Window, + cx: &mut Context, + ) { + self.show_cursor_names(window, cx); } - fn show_cursor_names(&mut self, cx: &mut ViewContext) { + fn show_cursor_names(&mut self, window: &mut Window, cx: &mut Context) { self.show_cursor_names = true; cx.notify(); - cx.spawn(|this, mut cx| async move { + cx.spawn_in(window, |this, mut cx| async move { cx.background_executor().timer(CURSORS_VISIBLE_FOR).await; this.update(&mut cx, |this, cx| { this.show_cursor_names = false; @@ -4627,11 +4796,18 @@ impl Editor { .detach(); } - pub fn next_inline_completion(&mut self, _: &NextInlineCompletion, cx: &mut ViewContext) { + pub fn next_inline_completion( + &mut self, + _: &NextInlineCompletion, + window: &mut Window, + cx: &mut Context, + ) { if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Next, cx); + self.cycle_inline_completion(Direction::Next, window, cx); } else { - let is_copilot_disabled = self.refresh_inline_completion(false, true, cx).is_none(); + let is_copilot_disabled = self + .refresh_inline_completion(false, true, window, cx) + .is_none(); if is_copilot_disabled { cx.propagate(); } @@ -4641,12 +4817,15 @@ impl Editor { pub fn previous_inline_completion( &mut self, _: &PreviousInlineCompletion, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Prev, cx); + self.cycle_inline_completion(Direction::Prev, window, cx); } else { - let is_copilot_disabled = self.refresh_inline_completion(false, true, cx).is_none(); + let is_copilot_disabled = self + .refresh_inline_completion(false, true, window, cx) + .is_none(); if is_copilot_disabled { cx.propagate(); } @@ -4656,7 +4835,8 @@ impl Editor { pub fn accept_inline_completion( &mut self, _: &AcceptInlineCompletion, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -4670,13 +4850,13 @@ impl Editor { && cursor.column <= current_indent.len && current_indent.len <= suggested_indent.len { - self.tab(&Default::default(), cx); + self.tab(&Default::default(), window, cx); return; } } if self.show_inline_completions_in_menu(cx) { - self.hide_context_menu(cx); + self.hide_context_menu(window, cx); } let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { @@ -4688,11 +4868,11 @@ impl Editor { match &active_inline_completion.completion { InlineCompletion::Move(position) => { let position = *position; - self.change_selections(Some(Autoscroll::newest()), cx, |selections| { + self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { selections.select_anchor_ranges([position..position]); }); } - InlineCompletion::Edit(edits) => { + InlineCompletion::Edit { edits, .. } => { if let Some(provider) = self.inline_completion_provider() { provider.accept(cx); } @@ -4704,13 +4884,13 @@ impl Editor { buffer.edit(edits.iter().cloned(), None, cx) }); - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { s.select_anchor_ranges([last_edit_end..last_edit_end]) }); - self.update_visible_inline_completion(cx); + self.update_visible_inline_completion(window, cx); if self.active_inline_completion.is_none() { - self.refresh_inline_completion(true, true, cx); + self.refresh_inline_completion(true, true, window, cx); } cx.notify(); @@ -4721,7 +4901,8 @@ impl Editor { pub fn accept_partial_inline_completion( &mut self, _: &AcceptPartialInlineCompletion, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { return; @@ -4735,11 +4916,11 @@ impl Editor { match &active_inline_completion.completion { InlineCompletion::Move(position) => { let position = *position; - self.change_selections(Some(Autoscroll::newest()), cx, |selections| { + self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { selections.select_anchor_ranges([position..position]); }); } - InlineCompletion::Edit(edits) => { + InlineCompletion::Edit { edits, .. } => { // Find an insertion that starts at the cursor position. let snapshot = self.buffer.read(cx).snapshot(cx); let cursor_offset = self.selections.newest::(cx).head(); @@ -4771,12 +4952,12 @@ impl Editor { text: partial_completion.clone().into(), }); - self.insert_with_autoindent_mode(&partial_completion, None, cx); + self.insert_with_autoindent_mode(&partial_completion, None, window, cx); - self.refresh_inline_completion(true, true, cx); + self.refresh_inline_completion(true, true, window, cx); cx.notify(); } else { - self.accept_inline_completion(&Default::default(), cx); + self.accept_inline_completion(&Default::default(), window, cx); } } } @@ -4785,7 +4966,7 @@ impl Editor { fn discard_inline_completion( &mut self, should_report_inline_completion_event: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool { if should_report_inline_completion_event { self.report_inline_completion_event(false, cx); @@ -4798,7 +4979,7 @@ impl Editor { self.take_active_inline_completion(cx).is_some() } - fn report_inline_completion_event(&self, accepted: bool, cx: &AppContext) { + fn report_inline_completion_event(&self, accepted: bool, cx: &App) { let Some(provider) = self.inline_completion_provider() else { return; }; @@ -4834,7 +5015,7 @@ impl Editor { fn take_active_inline_completion( &mut self, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option { let active_inline_completion = self.active_inline_completion.take()?; self.splice_inlays(active_inline_completion.inlay_ids, Default::default(), cx); @@ -4842,7 +5023,11 @@ impl Editor { Some(active_inline_completion.completion) } - fn update_visible_inline_completion(&mut self, cx: &mut ViewContext) -> Option<()> { + fn update_visible_inline_completion( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option<()> { let selection = self.selections.newest_anchor(); let cursor = selection.head(); let multibuffer = self.buffer.read(cx).snapshot(cx); @@ -4874,8 +5059,8 @@ impl Editor { let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - let completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; - let edits = completion + let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = inline_completion .edits .into_iter() .flat_map(|(range, new_text)| { @@ -4889,28 +5074,23 @@ impl Editor { } let first_edit_start = edits.first().unwrap().0.start; - let edit_start_row = first_edit_start - .to_point(&multibuffer) - .row - .saturating_sub(2); + let first_edit_start_point = first_edit_start.to_point(&multibuffer); + let edit_start_row = first_edit_start_point.row.saturating_sub(2); let last_edit_end = edits.last().unwrap().0.end; - let edit_end_row = cmp::min( - multibuffer.max_point().row, - last_edit_end.to_point(&multibuffer).row + 2, - ); + let last_edit_end_point = last_edit_end.to_point(&multibuffer); + let edit_end_row = cmp::min(multibuffer.max_point().row, last_edit_end_point.row + 2); let cursor_row = cursor.to_point(&multibuffer).row; let mut inlay_ids = Vec::new(); let invalidation_row_range; - let completion; - if cursor_row < edit_start_row { + let completion = if cursor_row < edit_start_row { invalidation_row_range = cursor_row..edit_end_row; - completion = InlineCompletion::Move(first_edit_start); + InlineCompletion::Move(first_edit_start) } else if cursor_row > edit_end_row { invalidation_row_range = edit_start_row..cursor_row; - completion = InlineCompletion::Move(first_edit_start); + InlineCompletion::Move(first_edit_start) } else { if edits .iter() @@ -4941,7 +5121,28 @@ impl Editor { } invalidation_row_range = edit_start_row..edit_end_row; - completion = InlineCompletion::Edit(edits); + + let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) { + if provider.show_tab_accept_marker() + && first_edit_start_point.row == last_edit_end_point.row + && !edits.iter().any(|(_, edit)| edit.contains('\n')) + { + EditDisplayMode::TabAccept + } else { + EditDisplayMode::Inline + } + } else { + EditDisplayMode::DiffPopover + }; + + let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?; + + InlineCompletion::Edit { + edits, + edit_preview: inline_completion.edit_preview, + display_mode, + snapshot, + } }; let invalidation_range = multibuffer @@ -4958,7 +5159,7 @@ impl Editor { }); if self.show_inline_completions_in_menu(cx) && self.has_active_completions_menu() { - if let Some(hint) = self.inline_completion_menu_hint(cx) { + if let Some(hint) = self.inline_completion_menu_hint(window, cx) { match self.context_menu.borrow_mut().as_mut() { Some(CodeContextMenu::Completions(menu)) => { menu.show_inline_completion_hint(hint); @@ -4974,30 +5175,41 @@ impl Editor { } fn inline_completion_menu_hint( - &mut self, - cx: &mut ViewContext, + &self, + window: &mut Window, + cx: &mut Context, ) -> Option { let provider = self.inline_completion_provider()?; if self.has_active_inline_completion() { - let editor_snapshot = self.snapshot(cx); + let editor_snapshot = self.snapshot(window, cx); let text = match &self.active_inline_completion.as_ref()?.completion { - InlineCompletion::Edit(edits) => { - inline_completion_edit_text(&editor_snapshot, edits, true, cx) - } + InlineCompletion::Edit { + edits, + edit_preview, + display_mode: _, + snapshot, + } => edit_preview + .as_ref() + .and_then(|edit_preview| { + inline_completion_edit_text(&snapshot, &edits, edit_preview, true, cx) + }) + .map(InlineCompletionText::Edit), InlineCompletion::Move(target) => { let target_point = target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot); let target_line = target_point.row + 1; - InlineCompletionText::Move( + Some(InlineCompletionText::Move( format!("Jump to edit in line {}", target_line).into(), - ) + )) } }; - Some(InlineCompletionMenuHint::Loaded { text }) + Some(InlineCompletionMenuHint::Loaded { text: text? }) } else if provider.is_refreshing(cx) { Some(InlineCompletionMenuHint::Loading) + } else if provider.needs_terms_acceptance(cx) { + Some(InlineCompletionMenuHint::PendingTermsAcceptance) } else { Some(InlineCompletionMenuHint::None) } @@ -5007,8 +5219,14 @@ impl Editor { Some(self.inline_completion_provider.as_ref()?.provider.clone()) } - fn show_inline_completions_in_menu(&self, cx: &AppContext) -> bool { - EditorSettings::get_global(cx).show_inline_completions_in_menu + fn show_inline_completions_in_menu(&self, cx: &App) -> bool { + let by_provider = matches!( + self.menu_inline_completions_policy, + MenuInlineCompletionsPolicy::ByProvider + ); + + by_provider + && EditorSettings::get_global(cx).show_inline_completions_in_menu && self .inline_completion_provider() .map_or(false, |provider| provider.show_completions_in_menu()) @@ -5020,7 +5238,7 @@ impl Editor { row: DisplayRow, is_active: bool, breakpoint: Option<&Breakpoint>, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option { let color = if breakpoint.is_some() { Color::Debugger @@ -5044,32 +5262,35 @@ impl Editor { .toggle_state(is_active) .tooltip({ let focus_handle = self.focus_handle.clone(); - move |cx| { + move |window, cx| { Tooltip::for_action_in( "Toggle Code Actions", &ToggleCodeActions { deployed_from_indicator: None, }, &focus_handle, + window, cx, ) } }) - .on_click(cx.listener(move |editor, _e, cx| { - editor.focus(cx); + .on_click(cx.listener(move |editor, _e, window, cx| { + window.focus(&editor.focus_handle(cx)); editor.toggle_code_actions( &ToggleCodeActions { deployed_from_indicator: Some(row), }, + window, cx, ); })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { editor.set_breakpoint_context_menu( row, position, bp_kind.clone(), event.down.position, + window, cx, ); })), @@ -5097,7 +5318,8 @@ impl Editor { /// TODO debugger: Use this function to color toggle symbols that house nested breakpoints fn active_breakpoint_points( &mut self, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> HashMap { let mut breakpoint_display_points = HashMap::default(); @@ -5105,7 +5327,7 @@ impl Editor { return breakpoint_display_points; }; - let snapshot = self.snapshot(cx); + let snapshot = self.snapshot(window, cx); let breakpoints = dap_store.read(cx).breakpoints(); @@ -5137,9 +5359,7 @@ impl Editor { let info = excerpt_boundary.next.as_ref(); if let Some(info) = info { - let Some(excerpt_ranges) = - multi_buffer_snapshot.range_for_excerpt::(info.id) - else { + let Some(excerpt_ranges) = multi_buffer_snapshot.range_for_excerpt(info.id) else { continue; }; @@ -5189,10 +5409,11 @@ impl Editor { anchor: text::Anchor, kind: Arc, row: DisplayRow, - cx: &mut ViewContext, - ) -> View { - let editor_weak = cx.view().downgrade(); - let editor_weak2 = editor_weak.clone(); + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let weak_editor = cx.weak_entity(); + let weak_editor2 = weak_editor.clone(); let focus_handle = self.focus_handle(cx); let second_entry_msg = if kind.log_message().is_some() { @@ -5201,12 +5422,12 @@ impl Editor { "Add Log Breakpoint" }; - ui::ContextMenu::build(cx, |menu, _cx| { + ui::ContextMenu::build(window, cx, |menu, _, _cx| { menu.on_blur_subscription(Subscription::new(|| {})) .context(focus_handle) - .entry("Toggle Breakpoint", None, move |cx| { - if let Some(editor) = editor_weak.upgrade() { - editor.update(cx, |this, cx| { + .entry("Toggle Breakpoint", None, move |_window, cx| { + weak_editor + .update(cx, |this, cx| { this.edit_breakpoint_at_anchor( anchor, BreakpointKind::Standard, @@ -5214,14 +5435,14 @@ impl Editor { cx, ); }) - } + .log_err(); }) - .entry(second_entry_msg, None, move |cx| { - if let Some(editor) = editor_weak2.clone().upgrade() { - editor.update(cx, |this, cx| { - this.add_edit_breakpoint_block(row, anchor, kind.as_ref(), cx); - }); - } + .entry(second_entry_msg, None, move |window, cx| { + weak_editor2 + .update(cx, |this, cx| { + this.add_edit_breakpoint_block(row, anchor, kind.as_ref(), window, cx); + }) + .log_err(); }) }) } @@ -5231,7 +5452,7 @@ impl Editor { position: text::Anchor, row: DisplayRow, kind: &BreakpointKind, - cx: &mut ViewContext, + cx: &mut Context, ) -> IconButton { let color = if self .gutter_breakpoint_indicator @@ -5254,8 +5475,8 @@ impl Editor { .size(ui::ButtonSize::None) .icon_color(color) .style(ButtonStyle::Transparent) - .on_click(cx.listener(move |editor, _e, cx| { - editor.focus(cx); + .on_click(cx.listener(move |editor, _e, window, cx| { + window.focus(&editor.focus_handle(cx)); editor.edit_breakpoint_at_anchor( position, arc_kind.as_ref().clone(), @@ -5263,23 +5484,24 @@ impl Editor { cx, ); })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { editor.set_breakpoint_context_menu( row, Some(position), arc_kind2.clone(), event.down.position, + window, cx, ); })) } fn build_tasks_context( - project: &Model, - buffer: &Model, + project: &Entity, + buffer: &Entity, buffer_row: u32, tasks: &Arc, - cx: &mut ViewContext, + cx: &mut Context, ) -> Task> { let position = Point::new(buffer_row, tasks.column); let range_start = buffer.read(cx).anchor_at(position, Bias::Right); @@ -5302,7 +5524,12 @@ impl Editor { }) } - pub fn spawn_nearest_task(&mut self, action: &SpawnNearestTask, cx: &mut ViewContext) { + pub fn spawn_nearest_task( + &mut self, + action: &SpawnNearestTask, + window: &mut Window, + cx: &mut Context, + ) { let Some((workspace, _)) = self.workspace.clone() else { return; }; @@ -5322,7 +5549,7 @@ impl Editor { let reveal_strategy = action.reveal; let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { let context = task_context.await?; let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?; @@ -5346,8 +5573,8 @@ impl Editor { fn find_closest_task( &mut self, - cx: &mut ViewContext, - ) -> Option<(Model, u32, Arc)> { + cx: &mut Context, + ) -> Option<(Entity, u32, Arc)> { let cursor_row = self.selections.newest_adjusted(cx).head().row; let ((buffer_id, row), tasks) = self @@ -5362,8 +5589,8 @@ impl Editor { fn find_enclosing_node_task( &mut self, - cx: &mut ViewContext, - ) -> Option<(Model, u32, Arc)> { + cx: &mut Context, + ) -> Option<(Entity, u32, Arc)> { let snapshot = self.buffer.read(cx).snapshot(cx); let offset = self.selections.newest::(cx).head(); let excerpt = snapshot.excerpt_containing(offset..offset)?; @@ -5406,7 +5633,7 @@ impl Editor { is_active: bool, row: DisplayRow, breakpoint: Option, - cx: &mut ViewContext, + cx: &mut Context, ) -> IconButton { let color = if breakpoint.is_some() { Color::Debugger @@ -5426,27 +5653,29 @@ impl Editor { .icon_size(IconSize::XSmall) .icon_color(color) .toggle_state(is_active) - .on_click(cx.listener(move |editor, _e, cx| { - editor.focus(cx); + .on_click(cx.listener(move |editor, _e, window, cx| { + window.focus(&editor.focus_handle(cx)); editor.toggle_code_actions( &ToggleCodeActions { deployed_from_indicator: Some(row), }, + window, cx, ); })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, cx| { + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { editor.set_breakpoint_context_menu( row, position, bp_kind.clone(), event.down.position, + window, cx, ); })) } - #[cfg(any(feature = "test-support", test))] + #[cfg(any(test, feature = "test-support"))] pub fn context_menu_visible(&self) -> bool { self.context_menu .borrow() @@ -5480,11 +5709,13 @@ impl Editor { &self, style: &EditorStyle, max_height_in_lines: u32, - cx: &mut ViewContext, + y_flipped: bool, + window: &mut Window, + cx: &mut Context, ) -> Option { self.context_menu.borrow().as_ref().and_then(|menu| { if menu.visible() { - Some(menu.render(style, max_height_in_lines, cx)) + Some(menu.render(style, max_height_in_lines, y_flipped, window, cx)) } else { None } @@ -5495,7 +5726,7 @@ impl Editor { &self, style: &EditorStyle, max_size: Size, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option { self.context_menu.borrow().as_ref().and_then(|menu| { if menu.visible() { @@ -5511,12 +5742,16 @@ impl Editor { }) } - fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { + fn hide_context_menu( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option { cx.notify(); self.completion_tasks.clear(); let context_menu = self.context_menu.borrow_mut().take(); if context_menu.is_some() && !self.show_inline_completions_in_menu(cx) { - self.update_visible_inline_completion(cx); + self.update_visible_inline_completion(window, cx); } context_menu } @@ -5525,7 +5760,7 @@ impl Editor { &mut self, choices: &Vec, selection: Range, - cx: &mut ViewContext, + cx: &mut Context, ) { if selection.start.buffer_id.is_none() { return; @@ -5545,7 +5780,8 @@ impl Editor { &mut self, insertion_ranges: &[Range], snippet: Snippet, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Result<()> { struct Tabstop { is_end_tabstop: bool, @@ -5602,7 +5838,7 @@ impl Editor { .collect::>() }); if let Some(tabstop) = tabstops.first() { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_ranges(tabstop.ranges.iter().cloned()); }); @@ -5671,15 +5907,28 @@ impl Editor { Ok(()) } - pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { - self.move_to_snippet_tabstop(Bias::Right, cx) + pub fn move_to_next_snippet_tabstop( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.move_to_snippet_tabstop(Bias::Right, window, cx) } - pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { - self.move_to_snippet_tabstop(Bias::Left, cx) + pub fn move_to_prev_snippet_tabstop( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.move_to_snippet_tabstop(Bias::Left, window, cx) } - pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { + pub fn move_to_snippet_tabstop( + &mut self, + bias: Bias, + window: &mut Window, + cx: &mut Context, + ) -> bool { if let Some(mut snippet) = self.snippet_stack.pop() { match bias { Bias::Left => { @@ -5700,7 +5949,7 @@ impl Editor { } } if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_anchor_ranges(current_ranges.iter().cloned()) }); @@ -5721,16 +5970,16 @@ impl Editor { false } - pub fn clear(&mut self, cx: &mut ViewContext) { - self.transact(cx, |this, cx| { - this.select_all(&SelectAll, cx); - this.insert("", cx); + pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { + self.transact(window, cx, |this, window, cx| { + this.select_all(&SelectAll, window, cx); + this.insert("", window, cx); }); } - pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { - self.transact(cx, |this, cx| { - this.select_autoclose_pair(cx); + pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); let mut linked_ranges = HashMap::<_, Vec<_>>::default(); if !this.linked_edit_ranges.is_empty() { let selections = this.selections.all::(cx); @@ -5791,8 +6040,10 @@ impl Editor { } this.signature_help_state.set_backspace_pressed(true); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); - this.insert("", cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + this.insert("", window, cx); let empty_str: Arc = Arc::from(""); for (buffer, edits) in linked_ranges { let snapshot = buffer.read(cx).snapshot(); @@ -5819,14 +6070,14 @@ impl Editor { this.edit(edits, None, cx); }) } - this.refresh_inline_completion(true, false, cx); - linked_editing_ranges::refresh_linked_ranges(this, cx); + this.refresh_inline_completion(true, false, window, cx); + linked_editing_ranges::refresh_linked_ranges(this, window, cx); }); } - pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { - self.transact(cx, |this, cx| { - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if selection.is_empty() && !line_mode { @@ -5837,21 +6088,21 @@ impl Editor { } }) }); - this.insert("", cx); - this.refresh_inline_completion(true, false, cx); + this.insert("", window, cx); + this.refresh_inline_completion(true, false, window, cx); }); } - pub fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext) { - if self.move_to_prev_snippet_tabstop(cx) { + pub fn tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context) { + if self.move_to_prev_snippet_tabstop(window, cx) { return; } - self.outdent(&Outdent, cx); + self.outdent(&Outdent, window, cx); } - pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { - if self.move_to_next_snippet_tabstop(cx) || self.read_only(cx) { + pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { + if self.move_to_next_snippet_tabstop(window, cx) || self.read_only(cx) { return; } @@ -5922,14 +6173,16 @@ impl Editor { row_delta += tab_size.len; } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); - this.refresh_inline_completion(true, false, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + this.refresh_inline_completion(true, false, window, cx); }); } - pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext) { + pub fn indent(&mut self, _: &Indent, window: &mut Window, cx: &mut Context) { if self.read_only(cx) { return; } @@ -5949,9 +6202,11 @@ impl Editor { Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); }); } @@ -5961,7 +6216,7 @@ impl Editor { selection: &mut Selection, edits: &mut Vec<(Range, String)>, delta_for_start_row: u32, - cx: &AppContext, + cx: &App, ) -> u32 { let settings = buffer.settings_at(selection.start, cx); let tab_size = settings.tab_size.get(); @@ -6031,7 +6286,7 @@ impl Editor { } } - pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { + pub fn outdent(&mut self, _: &Outdent, window: &mut Window, cx: &mut Context) { if self.read_only(cx) { return; } @@ -6086,7 +6341,7 @@ impl Editor { } } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |buffer, cx| { let empty_str: Arc = Arc::default(); buffer.edit( @@ -6098,11 +6353,13 @@ impl Editor { ); }); let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); }); } - pub fn autoindent(&mut self, _: &AutoIndent, cx: &mut ViewContext) { + pub fn autoindent(&mut self, _: &AutoIndent, window: &mut Window, cx: &mut Context) { if self.read_only(cx) { return; } @@ -6112,16 +6369,18 @@ impl Editor { .into_iter() .map(|s| s.range()); - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |buffer, cx| { buffer.autoindent_ranges(selections, cx); }); let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); }); } - pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { + pub fn delete_line(&mut self, _: &DeleteLine, window: &mut Window, cx: &mut Context) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); @@ -6171,7 +6430,7 @@ impl Editor { edit_ranges.push(edit_start..edit_end); } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { let buffer = this.buffer.update(cx, |buffer, cx| { let empty_str: Arc = Arc::default(); buffer.edit( @@ -6197,13 +6456,18 @@ impl Editor { }) .collect(); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(new_selections); }); }); } - pub fn join_lines_impl(&mut self, insert_whitespace: bool, cx: &mut ViewContext) { + pub fn join_lines_impl( + &mut self, + insert_whitespace: bool, + window: &mut Window, + cx: &mut Context, + ) { if self.read_only(cx) { return; } @@ -6237,7 +6501,7 @@ impl Editor { cursor_positions.push(anchor..anchor); } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { for row_range in row_ranges.into_iter().rev() { for row in row_range.iter_rows().rev() { let end_of_line = Point::new(row.0, snapshot.line_len(row)); @@ -6258,38 +6522,43 @@ impl Editor { } } - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_anchor_ranges(cursor_positions) }); }); } - pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext) { - self.join_lines_impl(true, cx); + pub fn join_lines(&mut self, _: &JoinLines, window: &mut Window, cx: &mut Context) { + self.join_lines_impl(true, window, cx); } pub fn sort_lines_case_sensitive( &mut self, _: &SortLinesCaseSensitive, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.manipulate_lines(cx, |lines| lines.sort()) + self.manipulate_lines(window, cx, |lines| lines.sort()) } pub fn sort_lines_case_insensitive( &mut self, _: &SortLinesCaseInsensitive, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase())) + self.manipulate_lines(window, cx, |lines| { + lines.sort_by_key(|line| line.to_lowercase()) + }) } pub fn unique_lines_case_insensitive( &mut self, _: &UniqueLinesCaseInsensitive, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.manipulate_lines(cx, |lines| { + self.manipulate_lines(window, cx, |lines| { let mut seen = HashSet::default(); lines.retain(|line| seen.insert(line.to_lowercase())); }) @@ -6298,59 +6567,72 @@ impl Editor { pub fn unique_lines_case_sensitive( &mut self, _: &UniqueLinesCaseSensitive, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.manipulate_lines(cx, |lines| { + self.manipulate_lines(window, cx, |lines| { let mut seen = HashSet::default(); lines.retain(|line| seen.insert(*line)); }) } - pub fn revert_file(&mut self, _: &RevertFile, cx: &mut ViewContext) { + pub fn revert_file(&mut self, _: &RevertFile, window: &mut Window, cx: &mut Context) { let mut revert_changes = HashMap::default(); - let snapshot = self.snapshot(cx); - for hunk in hunks_for_ranges( - Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter(), - &snapshot, - ) { + let snapshot = self.snapshot(window, cx); + for hunk in snapshot + .hunks_for_ranges(Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter()) + { self.prepare_revert_change(&mut revert_changes, &hunk, cx); } if !revert_changes.is_empty() { - self.transact(cx, |editor, cx| { - editor.revert(revert_changes, cx); + self.transact(window, cx, |editor, window, cx| { + editor.revert(revert_changes, window, cx); }); } } - pub fn reload_file(&mut self, _: &ReloadFile, cx: &mut ViewContext) { + pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context) { let Some(project) = self.project.clone() else { return; }; - self.reload(project, cx).detach_and_notify_err(cx); + self.reload(project, window, cx) + .detach_and_notify_err(window, cx); } - pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext) { - let revert_changes = self.gather_revert_changes(&self.selections.all(cx), cx); - if !revert_changes.is_empty() { - self.transact(cx, |editor, cx| { - editor.revert(revert_changes, cx); - }); - } + pub fn revert_selected_hunks( + &mut self, + _: &RevertSelectedHunks, + window: &mut Window, + cx: &mut Context, + ) { + let selections = self.selections.all(cx).into_iter().map(|s| s.range()); + self.revert_hunks_in_ranges(selections, window, cx); } - fn revert_hunk(&mut self, hunk: HoveredHunk, cx: &mut ViewContext) { - let snapshot = self.buffer.read(cx).read(cx); - if let Some(hunk) = crate::hunk_diff::to_diff_hunk(&hunk, &snapshot) { - drop(snapshot); - let mut revert_changes = HashMap::default(); + fn revert_hunks_in_ranges( + &mut self, + ranges: impl Iterator>, + window: &mut Window, + cx: &mut Context, + ) { + let mut revert_changes = HashMap::default(); + let snapshot = self.snapshot(window, cx); + for hunk in &snapshot.hunks_for_ranges(ranges) { self.prepare_revert_change(&mut revert_changes, &hunk, cx); - if !revert_changes.is_empty() { - self.revert(revert_changes, cx) - } + } + if !revert_changes.is_empty() { + self.transact(window, cx, |editor, window, cx| { + editor.revert(revert_changes, window, cx); + }); } } - pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + pub fn open_active_item_in_terminal( + &mut self, + _: &OpenInTerminal, + window: &mut Window, + cx: &mut Context, + ) { if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; let project = self.project.as_ref()?.read(cx); @@ -6363,7 +6645,7 @@ impl Editor { .to_path_buf(); Some(parent) }) { - cx.dispatch_action(OpenTerminal { working_directory }.boxed_clone()); + window.dispatch_action(OpenTerminal { working_directory }.boxed_clone(), cx); } } @@ -6373,7 +6655,8 @@ impl Editor { position: Option, kind: Arc, clicked_point: gpui::Point, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let source = self .buffer @@ -6381,11 +6664,22 @@ impl Editor { .snapshot(cx) .breakpoint_anchor(Point::new(row.0, 0u32)); - let context_menu = - self.breakpoint_context_menu(position.unwrap_or(source.text_anchor), kind, row, cx); + let context_menu = self.breakpoint_context_menu( + position.unwrap_or(source.text_anchor), + kind, + row, + window, + cx, + ); - self.mouse_context_menu = - MouseContextMenu::pinned_to_editor(self, source, clicked_point, context_menu, cx); + self.mouse_context_menu = MouseContextMenu::pinned_to_editor( + self, + source, + clicked_point, + context_menu, + window, + cx, + ); } fn add_edit_breakpoint_block( @@ -6393,15 +6687,16 @@ impl Editor { row: DisplayRow, anchor: text::Anchor, kind: &BreakpointKind, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let position = self - .snapshot(cx) + .snapshot(window, cx) .display_point_to_anchor(DisplayPoint::new(row, 0), Bias::Right); - let weak_editor = cx.view().downgrade(); + let weak_editor = cx.weak_entity(); let bp_prompt = - cx.new_view(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), cx)); + cx.new(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), window, cx)); let height = bp_prompt.update(cx, |this, cx| { this.prompt @@ -6420,7 +6715,7 @@ impl Editor { }]; let focus_handle = bp_prompt.focus_handle(cx); - cx.focus(&focus_handle); + window.focus(&focus_handle); let block_ids = self.insert_blocks(blocks, None, cx); bp_prompt.update(cx, |prompt, _| { @@ -6430,7 +6725,8 @@ impl Editor { pub(crate) fn breakpoint_at_cursor_head( &mut self, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option<(text::Anchor, BreakpointKind)> { let cursor_position: Point = self.selections.newest(cx).head(); @@ -6439,7 +6735,7 @@ impl Editor { // breakpoint. Otherwise, toggling a breakpoint through an action wouldn't // untoggle a breakpoint that was added through clicking on the gutter let breakpoint_position = self - .snapshot(cx) + .snapshot(window, cx) .display_snapshot .buffer_snapshot .breakpoint_anchor(Point::new(cursor_position.row, 0)) @@ -6465,21 +6761,28 @@ impl Editor { Some((bp.active_position?, bp.kind)) } - pub fn edit_log_breakpoint(&mut self, _: &EditLogBreakpoint, cx: &mut ViewContext) { - let (anchor, kind) = self.breakpoint_at_cursor_head(cx).unwrap_or_else(|| { - let cursor_position: Point = self.selections.newest(cx).head(); - - let breakpoint_position = self - .snapshot(cx) - .display_snapshot - .buffer_snapshot - .breakpoint_anchor(Point::new(cursor_position.row, 0)) - .text_anchor; + pub fn edit_log_breakpoint( + &mut self, + _: &EditLogBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + let (anchor, kind) = self + .breakpoint_at_cursor_head(window, cx) + .unwrap_or_else(|| { + let cursor_position: Point = self.selections.newest(cx).head(); + + let breakpoint_position = self + .snapshot(window, cx) + .display_snapshot + .buffer_snapshot + .breakpoint_anchor(Point::new(cursor_position.row, 0)) + .text_anchor; - let kind = BreakpointKind::Standard; + let kind = BreakpointKind::Standard; - (breakpoint_position, kind) - }); + (breakpoint_position, kind) + }); if let Some(buffer) = self .buffer() @@ -6489,23 +6792,28 @@ impl Editor { { let row = buffer .summary_for_anchor::(&anchor) - .to_display_point(&self.snapshot(cx)) + .to_display_point(&self.snapshot(window, cx)) .row(); - self.add_edit_breakpoint_block(row, anchor, &kind, cx); + self.add_edit_breakpoint_block(row, anchor, &kind, window, cx); } } - pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext) { + pub fn toggle_breakpoint( + &mut self, + _: &ToggleBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { let edit_action = BreakpointEditAction::Toggle; - if let Some((anchor, kind)) = self.breakpoint_at_cursor_head(cx) { + if let Some((anchor, kind)) = self.breakpoint_at_cursor_head(window, cx) { self.edit_breakpoint_at_anchor(anchor, kind, edit_action, cx); } else { let cursor_position: Point = self.selections.newest(cx).head(); let breakpoint_position = self - .snapshot(cx) + .snapshot(window, cx) .display_snapshot .buffer_snapshot .breakpoint_anchor(Point::new(cursor_position.row, 0)) @@ -6525,7 +6833,7 @@ impl Editor { breakpoint_position: text::Anchor, kind: BreakpointKind, edit_action: BreakpointEditAction, - cx: &mut ViewContext, + cx: &mut Context, ) { let Some(project) = &self.project else { return; @@ -6566,33 +6874,20 @@ impl Editor { cx.notify(); } - fn gather_revert_changes( - &mut self, - selections: &[Selection], - cx: &mut ViewContext, - ) -> HashMap, Rope)>> { - let mut revert_changes = HashMap::default(); - let snapshot = self.snapshot(cx); - for hunk in hunks_for_selections(&snapshot, selections) { - self.prepare_revert_change(&mut revert_changes, &hunk, cx); - } - revert_changes - } - pub fn prepare_revert_change( - &mut self, + &self, revert_changes: &mut HashMap, Rope)>>, hunk: &MultiBufferDiffHunk, - cx: &AppContext, + cx: &mut App, ) -> Option<()> { - let buffer = self.buffer.read(cx).buffer(hunk.buffer_id)?; + let buffer = self.buffer.read(cx); + let change_set = buffer.change_set_for(hunk.buffer_id)?; + let buffer = buffer.buffer(hunk.buffer_id)?; let buffer = buffer.read(cx); - let change_set = &self.diff_map.diff_bases.get(&hunk.buffer_id)?.change_set; let original_text = change_set .read(cx) .base_text .as_ref()? - .read(cx) .as_rope() .slice(hunk.diff_base_byte_range.clone()); let buffer_snapshot = buffer.snapshot(); @@ -6611,16 +6906,20 @@ impl Editor { } } - pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext) { - self.manipulate_lines(cx, |lines| lines.reverse()) + pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context) { + self.manipulate_lines(window, cx, |lines| lines.reverse()) } - pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { - self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng())) + pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context) { + self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) } - fn manipulate_lines(&mut self, cx: &mut ViewContext, mut callback: Fn) - where + fn manipulate_lines( + &mut self, + window: &mut Window, + cx: &mut Context, + mut callback: Fn, + ) where Fn: FnMut(&mut Vec<&str>), { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -6679,7 +6978,7 @@ impl Editor { } } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { let buffer = this.buffer.update(cx, |buffer, cx| { buffer.edit(edits, None, cx); buffer.snapshot(cx) @@ -6701,7 +7000,7 @@ impl Editor { }) .collect(); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(new_selections); }); @@ -6709,36 +7008,62 @@ impl Editor { }); } - pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext) { - self.manipulate_text(cx, |text| text.to_uppercase()) + pub fn convert_to_upper_case( + &mut self, + _: &ConvertToUpperCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_uppercase()) } - pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext) { - self.manipulate_text(cx, |text| text.to_lowercase()) + pub fn convert_to_lower_case( + &mut self, + _: &ConvertToLowerCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_lowercase()) } - pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext) { - self.manipulate_text(cx, |text| { + pub fn convert_to_title_case( + &mut self, + _: &ConvertToTitleCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { text.split('\n') .map(|line| line.to_case(Case::Title)) .join("\n") }) } - pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext) { - self.manipulate_text(cx, |text| text.to_case(Case::Snake)) + pub fn convert_to_snake_case( + &mut self, + _: &ConvertToSnakeCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Snake)) } - pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext) { - self.manipulate_text(cx, |text| text.to_case(Case::Kebab)) + pub fn convert_to_kebab_case( + &mut self, + _: &ConvertToKebabCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Kebab)) } pub fn convert_to_upper_camel_case( &mut self, _: &ConvertToUpperCamelCase, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.manipulate_text(cx, |text| { + self.manipulate_text(window, cx, |text| { text.split('\n') .map(|line| line.to_case(Case::UpperCamel)) .join("\n") @@ -6748,17 +7073,19 @@ impl Editor { pub fn convert_to_lower_camel_case( &mut self, _: &ConvertToLowerCamelCase, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.manipulate_text(cx, |text| text.to_case(Case::Camel)) + self.manipulate_text(window, cx, |text| text.to_case(Case::Camel)) } pub fn convert_to_opposite_case( &mut self, _: &ConvertToOppositeCase, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.manipulate_text(cx, |text| { + self.manipulate_text(window, cx, |text| { text.chars() .fold(String::with_capacity(text.len()), |mut t, c| { if c.is_uppercase() { @@ -6771,7 +7098,7 @@ impl Editor { }) } - fn manipulate_text(&mut self, cx: &mut ViewContext, mut callback: Fn) + fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, mut callback: Fn) where Fn: FnMut(&str) -> String, { @@ -6813,12 +7140,12 @@ impl Editor { edits.push((start..end, text)); } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |buffer, cx| { buffer.edit(edits, None, cx); }); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(new_selections); }); @@ -6826,7 +7153,13 @@ impl Editor { }); } - pub fn duplicate(&mut self, upwards: bool, whole_lines: bool, cx: &mut ViewContext) { + pub fn duplicate( + &mut self, + upwards: bool, + whole_lines: bool, + window: &mut Window, + cx: &mut Context, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; let selections = self.selections.all::(cx); @@ -6874,7 +7207,7 @@ impl Editor { } } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, _, cx| { this.buffer.update(cx, |buffer, cx| { buffer.edit(edits, None, cx); }); @@ -6883,19 +7216,34 @@ impl Editor { }); } - pub fn duplicate_line_up(&mut self, _: &DuplicateLineUp, cx: &mut ViewContext) { - self.duplicate(true, true, cx); + pub fn duplicate_line_up( + &mut self, + _: &DuplicateLineUp, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(true, true, window, cx); } - pub fn duplicate_line_down(&mut self, _: &DuplicateLineDown, cx: &mut ViewContext) { - self.duplicate(false, true, cx); + pub fn duplicate_line_down( + &mut self, + _: &DuplicateLineDown, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(false, true, window, cx); } - pub fn duplicate_selection(&mut self, _: &DuplicateSelection, cx: &mut ViewContext) { - self.duplicate(false, false, cx); + pub fn duplicate_selection( + &mut self, + _: &DuplicateSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(false, false, window, cx); } - pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { + pub fn move_line_up(&mut self, _: &MoveLineUp, window: &mut Window, cx: &mut Context) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -6933,12 +7281,8 @@ impl Editor { // Don't move lines across excerpts if buffer - .excerpt_boundaries_in_range(( - Bound::Excluded(insertion_point), - Bound::Included(range_to_move.end), - )) - .next() - .is_none() + .excerpt_containing(insertion_point..range_to_move.end) + .is_some() { let text = buffer .text_for_range(range_to_move.clone()) @@ -6985,21 +7329,26 @@ impl Editor { new_selections.append(&mut contiguous_row_selections); } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { this.unfold_ranges(&unfold_ranges, true, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { buffer.edit([(range, text)], None, cx); } }); - this.fold_creases(refold_creases, true, cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + this.fold_creases(refold_creases, true, window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(new_selections); }) }); } - pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { + pub fn move_line_down( + &mut self, + _: &MoveLineDown, + window: &mut Window, + cx: &mut Context, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -7031,12 +7380,8 @@ impl Editor { // Don't move lines across excerpt boundaries if buffer - .excerpt_boundaries_in_range(( - Bound::Excluded(range_to_move.start), - Bound::Included(insertion_point), - )) - .next() - .is_none() + .excerpt_containing(range_to_move.start..insertion_point) + .is_some() { let mut text = String::from("\n"); text.extend(buffer.text_for_range(range_to_move.clone())); @@ -7079,22 +7424,24 @@ impl Editor { new_selections.append(&mut contiguous_row_selections); } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { this.unfold_ranges(&unfold_ranges, true, true, cx); this.buffer.update(cx, |buffer, cx| { for (range, text) in edits { buffer.edit([(range, text)], None, cx); } }); - this.fold_creases(refold_creases, true, cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); + this.fold_creases(refold_creases, true, window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections) + }); }); } - pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext) { - let text_layout_details = &self.text_layout_details(cx); - self.transact(cx, |this, cx| { - let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn transpose(&mut self, _: &Transpose, window: &mut Window, cx: &mut Context) { + let text_layout_details = &self.text_layout_details(window); + self.transact(window, cx, |this, window, cx| { + let edits = this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let mut edits: Vec<(Range, String)> = Default::default(); let line_mode = s.line_mode; s.move_with(|display_map, selection| { @@ -7143,17 +7490,17 @@ impl Editor { this.buffer .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(selections); }); }); } - pub fn rewrap(&mut self, _: &Rewrap, cx: &mut ViewContext) { + pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context) { self.rewrap_impl(IsVimMode::No, cx) } - pub fn rewrap_impl(&mut self, is_vim_mode: IsVimMode, cx: &mut ViewContext) { + pub fn rewrap_impl(&mut self, is_vim_mode: IsVimMode, cx: &mut Context) { let buffer = self.buffer.read(cx).snapshot(cx); let selections = self.selections.all::(cx); let mut selections = selections.iter().peekable(); @@ -7335,7 +7682,7 @@ impl Editor { .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } - pub fn cut_common(&mut self, cx: &mut ViewContext) -> ClipboardItem { + pub fn cut_common(&mut self, window: &mut Window, cx: &mut Context) -> ClipboardItem { let mut text = String::new(); let buffer = self.buffer.read(cx).snapshot(cx); let mut selections = self.selections.all::(cx); @@ -7374,33 +7721,38 @@ impl Editor { } } - self.transact(cx, |this, cx| { - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(selections); }); - this.insert("", cx); + this.insert("", window, cx); }); ClipboardItem::new_string_with_json_metadata(text, clipboard_selections) } - pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { - let item = self.cut_common(cx); + pub fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { + let item = self.cut_common(window, cx); cx.write_to_clipboard(item); } - pub fn kill_ring_cut(&mut self, _: &KillRingCut, cx: &mut ViewContext) { - self.change_selections(None, cx, |s| { + pub fn kill_ring_cut(&mut self, _: &KillRingCut, window: &mut Window, cx: &mut Context) { + self.change_selections(None, window, cx, |s| { s.move_with(|snapshot, sel| { if sel.is_empty() { sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) } }); }); - let item = self.cut_common(cx); + let item = self.cut_common(window, cx); cx.set_global(KillRing(item)) } - pub fn kill_ring_yank(&mut self, _: &KillRingYank, cx: &mut ViewContext) { + pub fn kill_ring_yank( + &mut self, + _: &KillRingYank, + window: &mut Window, + cx: &mut Context, + ) { let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { (kill_ring.text().to_string(), kill_ring.metadata_json()) @@ -7410,10 +7762,10 @@ impl Editor { } else { return; }; - self.do_paste(&text, metadata, false, cx); + self.do_paste(&text, metadata, false, window, cx); } - pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + pub fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { let selections = self.selections.all::(cx); let buffer = self.buffer.read(cx).read(cx); let mut text = String::new(); @@ -7459,7 +7811,8 @@ impl Editor { text: &String, clipboard_selections: Option>, handle_entire_lines: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if self.read_only(cx) { return; @@ -7467,7 +7820,7 @@ impl Editor { let clipboard_text = Cow::Borrowed(text); - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { if let Some(mut clipboard_selections) = clipboard_selections { let old_selections = this.selections.all::(cx); let all_selections_were_entire_line = @@ -7535,14 +7888,16 @@ impl Editor { }); let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); } else { - this.insert(&clipboard_text, cx); + this.insert(&clipboard_text, window, cx); } }); } - pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { if let Some(item) = cx.read_from_clipboard() { let entries = item.entries(); @@ -7554,14 +7909,15 @@ impl Editor { clipboard_string.text(), clipboard_string.metadata_json::>(), true, + window, cx, ), - _ => self.do_paste(&item.text().unwrap_or_default(), None, true, cx), + _ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx), } } } - pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { + pub fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context) { if self.read_only(cx) { return; } @@ -7570,19 +7926,19 @@ impl Editor { if let Some((selections, _)) = self.selection_history.transaction(transaction_id).cloned() { - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { s.select_anchors(selections.to_vec()); }); } self.request_autoscroll(Autoscroll::fit(), cx); - self.unmark_text(cx); - self.refresh_inline_completion(true, false, cx); + self.unmark_text(window, cx); + self.refresh_inline_completion(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); cx.emit(EditorEvent::TransactionUndone { transaction_id }); } } - pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { + pub fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context) { if self.read_only(cx) { return; } @@ -7591,29 +7947,29 @@ impl Editor { if let Some((_, Some(selections))) = self.selection_history.transaction(transaction_id).cloned() { - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { s.select_anchors(selections.to_vec()); }); } self.request_autoscroll(Autoscroll::fit(), cx); - self.unmark_text(cx); - self.refresh_inline_completion(true, false, cx); + self.unmark_text(window, cx); + self.refresh_inline_completion(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); } } - pub fn finalize_last_transaction(&mut self, cx: &mut ViewContext) { + pub fn finalize_last_transaction(&mut self, cx: &mut Context) { self.buffer .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); } - pub fn group_until_transaction(&mut self, tx_id: TransactionId, cx: &mut ViewContext) { + pub fn group_until_transaction(&mut self, tx_id: TransactionId, cx: &mut Context) { self.buffer .update(cx, |buffer, cx| buffer.group_until_transaction(tx_id, cx)); } - pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { let cursor = if selection.is_empty() && !line_mode { @@ -7626,14 +7982,14 @@ impl Editor { }) } - pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None)); }) } - pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { let cursor = if selection.is_empty() && !line_mode { @@ -7646,14 +8002,14 @@ impl Editor { }) } - pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None)); }) } - pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { + pub fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { + if self.take_rename(true, window, cx).is_some() { return; } @@ -7662,11 +8018,11 @@ impl Editor { return; } - let text_layout_details = &self.text_layout_details(cx); + let text_layout_details = &self.text_layout_details(window); let selection_count = self.selections.count(); let first_selection = self.selections.first_anchor(); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { @@ -7689,8 +8045,13 @@ impl Editor { } } - pub fn move_up_by_lines(&mut self, action: &MoveUpByLines, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { + pub fn move_up_by_lines( + &mut self, + action: &MoveUpByLines, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { return; } @@ -7699,9 +8060,9 @@ impl Editor { return; } - let text_layout_details = &self.text_layout_details(cx); + let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { @@ -7720,8 +8081,13 @@ impl Editor { }) } - pub fn move_down_by_lines(&mut self, action: &MoveDownByLines, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { + pub fn move_down_by_lines( + &mut self, + action: &MoveDownByLines, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { return; } @@ -7730,9 +8096,9 @@ impl Editor { return; } - let text_layout_details = &self.text_layout_details(cx); + let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { @@ -7751,40 +8117,60 @@ impl Editor { }) } - pub fn select_down_by_lines(&mut self, action: &SelectDownByLines, cx: &mut ViewContext) { - let text_layout_details = &self.text_layout_details(cx); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn select_down_by_lines( + &mut self, + action: &SelectDownByLines, + window: &mut Window, + cx: &mut Context, + ) { + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details) }) }) } - pub fn select_up_by_lines(&mut self, action: &SelectUpByLines, cx: &mut ViewContext) { - let text_layout_details = &self.text_layout_details(cx); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn select_up_by_lines( + &mut self, + action: &SelectUpByLines, + window: &mut Window, + cx: &mut Context, + ) { + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details) }) }) } - pub fn select_page_up(&mut self, _: &SelectPageUp, cx: &mut ViewContext) { + pub fn select_page_up( + &mut self, + _: &SelectPageUp, + window: &mut Window, + cx: &mut Context, + ) { let Some(row_count) = self.visible_row_count() else { return; }; - let text_layout_details = &self.text_layout_details(cx); + let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::up_by_rows(map, head, row_count, goal, false, text_layout_details) }) }) } - pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { + pub fn move_page_up( + &mut self, + action: &MovePageUp, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { return; } @@ -7813,9 +8199,9 @@ impl Editor { Autoscroll::fit() }; - let text_layout_details = &self.text_layout_details(cx); + let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(autoscroll), cx, |s| { + self.change_selections(Some(autoscroll), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { @@ -7834,28 +8220,28 @@ impl Editor { }); } - pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { - let text_layout_details = &self.text_layout_details(cx); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::up(map, head, goal, false, text_layout_details) }) }) } - pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { - self.take_rename(true, cx); + pub fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { + self.take_rename(true, window, cx); if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } - let text_layout_details = &self.text_layout_details(cx); + let text_layout_details = &self.text_layout_details(window); let selection_count = self.selections.count(); let first_selection = self.selections.first_anchor(); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { @@ -7878,22 +8264,32 @@ impl Editor { } } - pub fn select_page_down(&mut self, _: &SelectPageDown, cx: &mut ViewContext) { + pub fn select_page_down( + &mut self, + _: &SelectPageDown, + window: &mut Window, + cx: &mut Context, + ) { let Some(row_count) = self.visible_row_count() else { return; }; - let text_layout_details = &self.text_layout_details(cx); + let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::down_by_rows(map, head, row_count, goal, false, text_layout_details) }) }) } - pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext) { - if self.take_rename(true, cx).is_some() { + pub fn move_page_down( + &mut self, + action: &MovePageDown, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { return; } @@ -7922,8 +8318,8 @@ impl Editor { Autoscroll::fit() }; - let text_layout_details = &self.text_layout_details(cx); - self.change_selections(Some(autoscroll), cx, |s| { + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(autoscroll), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { @@ -7942,34 +8338,54 @@ impl Editor { }); } - pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { - let text_layout_details = &self.text_layout_details(cx); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::down(map, head, goal, false, text_layout_details) }) }); } - pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { + pub fn context_menu_first( + &mut self, + _: &ContextMenuFirst, + _window: &mut Window, + cx: &mut Context, + ) { if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { context_menu.select_first(self.completion_provider.as_deref(), cx); } } - pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { + pub fn context_menu_prev( + &mut self, + _: &ContextMenuPrev, + _window: &mut Window, + cx: &mut Context, + ) { if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { context_menu.select_prev(self.completion_provider.as_deref(), cx); } } - pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { + pub fn context_menu_next( + &mut self, + _: &ContextMenuNext, + _window: &mut Window, + cx: &mut Context, + ) { if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { context_menu.select_next(self.completion_provider.as_deref(), cx); } } - pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { + pub fn context_menu_last( + &mut self, + _: &ContextMenuLast, + _window: &mut Window, + cx: &mut Context, + ) { if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { context_menu.select_last(self.completion_provider.as_deref(), cx); } @@ -7978,9 +8394,10 @@ impl Editor { pub fn move_to_previous_word_start( &mut self, _: &MoveToPreviousWordStart, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_cursors_with(|map, head, _| { ( movement::previous_word_start(map, head), @@ -7993,9 +8410,10 @@ impl Editor { pub fn move_to_previous_subword_start( &mut self, _: &MoveToPreviousSubwordStart, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_cursors_with(|map, head, _| { ( movement::previous_subword_start(map, head), @@ -8008,9 +8426,10 @@ impl Editor { pub fn select_to_previous_word_start( &mut self, _: &SelectToPreviousWordStart, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::previous_word_start(map, head), @@ -8023,9 +8442,10 @@ impl Editor { pub fn select_to_previous_subword_start( &mut self, _: &SelectToPreviousSubwordStart, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::previous_subword_start(map, head), @@ -8038,11 +8458,12 @@ impl Editor { pub fn delete_to_previous_word_start( &mut self, action: &DeleteToPreviousWordStart, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.transact(cx, |this, cx| { - this.select_autoclose_pair(cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if selection.is_empty() && !line_mode { @@ -8055,18 +8476,19 @@ impl Editor { } }); }); - this.insert("", cx); + this.insert("", window, cx); }); } pub fn delete_to_previous_subword_start( &mut self, _: &DeleteToPreviousSubwordStart, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.transact(cx, |this, cx| { - this.select_autoclose_pair(cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if selection.is_empty() && !line_mode { @@ -8075,12 +8497,17 @@ impl Editor { } }); }); - this.insert("", cx); + this.insert("", window, cx); }); } - pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn move_to_next_word_end( + &mut self, + _: &MoveToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_cursors_with(|map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) }); @@ -8090,18 +8517,24 @@ impl Editor { pub fn move_to_next_subword_end( &mut self, _: &MoveToNextSubwordEnd, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_cursors_with(|map, head, _| { (movement::next_subword_end(map, head), SelectionGoal::None) }); }) } - pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_heads_with(|map, head, _| { + pub fn select_to_next_word_end( + &mut self, + _: &SelectToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) }); }) @@ -8110,9 +8543,10 @@ impl Editor { pub fn select_to_next_subword_end( &mut self, _: &SelectToNextSubwordEnd, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| { (movement::next_subword_end(map, head), SelectionGoal::None) }); @@ -8122,10 +8556,11 @@ impl Editor { pub fn delete_to_next_word_end( &mut self, action: &DeleteToNextWordEnd, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.transact(cx, |this, cx| { - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if selection.is_empty() && !line_mode { @@ -8138,17 +8573,18 @@ impl Editor { } }); }); - this.insert("", cx); + this.insert("", window, cx); }); } pub fn delete_to_next_subword_end( &mut self, _: &DeleteToNextSubwordEnd, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.transact(cx, |this, cx| { - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { let cursor = movement::next_subword_end(map, selection.head()); @@ -8156,16 +8592,17 @@ impl Editor { } }); }); - this.insert("", cx); + this.insert("", window, cx); }); } pub fn move_to_beginning_of_line( &mut self, action: &MoveToBeginningOfLine, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_cursors_with(|map, head, _| { ( movement::indented_line_beginning(map, head, action.stop_at_soft_wraps), @@ -8178,9 +8615,10 @@ impl Editor { pub fn select_to_beginning_of_line( &mut self, action: &SelectToBeginningOfLine, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::indented_line_beginning(map, head, action.stop_at_soft_wraps), @@ -8193,10 +8631,11 @@ impl Editor { pub fn delete_to_beginning_of_line( &mut self, _: &DeleteToBeginningOfLine, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.transact(cx, |this, cx| { - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_with(|_, selection| { selection.reversed = true; }); @@ -8206,14 +8645,20 @@ impl Editor { &SelectToBeginningOfLine { stop_at_soft_wraps: false, }, + window, cx, ); - this.backspace(&Backspace, cx); + this.backspace(&Backspace, window, cx); }); } - pub fn move_to_end_of_line(&mut self, action: &MoveToEndOfLine, cx: &mut ViewContext) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + pub fn move_to_end_of_line( + &mut self, + action: &MoveToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_cursors_with(|map, head, _| { ( movement::line_end(map, head, action.stop_at_soft_wraps), @@ -8226,9 +8671,10 @@ impl Editor { pub fn select_to_end_of_line( &mut self, action: &SelectToEndOfLine, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::line_end(map, head, action.stop_at_soft_wraps), @@ -8238,41 +8684,54 @@ impl Editor { }) } - pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { - self.transact(cx, |this, cx| { + pub fn delete_to_end_of_line( + &mut self, + _: &DeleteToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.transact(window, cx, |this, window, cx| { this.select_to_end_of_line( &SelectToEndOfLine { stop_at_soft_wraps: false, }, + window, cx, ); - this.delete(&Delete, cx); + this.delete(&Delete, window, cx); }); } - pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { - self.transact(cx, |this, cx| { + pub fn cut_to_end_of_line( + &mut self, + _: &CutToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.transact(window, cx, |this, window, cx| { this.select_to_end_of_line( &SelectToEndOfLine { stop_at_soft_wraps: false, }, + window, cx, ); - this.cut(&Cut, cx); + this.cut(&Cut, window, cx); }); } pub fn move_to_start_of_paragraph( &mut self, _: &MoveToStartOfParagraph, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::start_of_paragraph(map, selection.head(), 1), @@ -8285,14 +8744,15 @@ impl Editor { pub fn move_to_end_of_paragraph( &mut self, _: &MoveToEndOfParagraph, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::end_of_paragraph(map, selection.head(), 1), @@ -8305,14 +8765,15 @@ impl Editor { pub fn select_to_start_of_paragraph( &mut self, _: &SelectToStartOfParagraph, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::start_of_paragraph(map, head, 1), @@ -8325,14 +8786,15 @@ impl Editor { pub fn select_to_end_of_paragraph( &mut self, _: &SelectToEndOfParagraph, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::end_of_paragraph(map, head, 1), @@ -8342,34 +8804,44 @@ impl Editor { }) } - pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { + pub fn move_to_beginning( + &mut self, + _: &MoveToBeginning, + window: &mut Window, + cx: &mut Context, + ) { if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_ranges(vec![0..0]); }); } - pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { + pub fn select_to_beginning( + &mut self, + _: &SelectToBeginning, + window: &mut Window, + cx: &mut Context, + ) { let mut selection = self.selections.last::(cx); selection.set_head(Point::zero(), SelectionGoal::None); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(vec![selection]); }); } - pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { + pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context) { if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } let cursor = self.buffer.read(cx).read(cx).len(); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_ranges(vec![cursor..cursor]) }); } @@ -8386,7 +8858,7 @@ impl Editor { &mut self, cursor_anchor: Anchor, new_position: Option, - cx: &mut ViewContext, + cx: &mut Context, ) { if let Some(nav_history) = self.nav_history.as_mut() { let buffer = self.buffer.read(cx).read(cx); @@ -8414,23 +8886,23 @@ impl Editor { } } - pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { + pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context) { let buffer = self.buffer.read(cx).snapshot(cx); let mut selection = self.selections.first::(cx); selection.set_head(buffer.len(), SelectionGoal::None); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(vec![selection]); }); } - pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { + pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { let end = self.buffer.read(cx).read(cx).len(); - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { s.select_ranges(vec![0..end]); }); } - pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext) { + pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(cx); let max_point = display_map.buffer_snapshot.max_point(); @@ -8440,7 +8912,7 @@ impl Editor { selection.end = cmp::min(max_point, Point::new(rows.end.0, 0)); selection.reversed = false; } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(selections); }); } @@ -8448,7 +8920,8 @@ impl Editor { pub fn split_selection_into_lines( &mut self, _: &SplitSelectionIntoLines, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let mut to_unfold = Vec::new(); let mut new_selection_ranges = Vec::new(); @@ -8465,23 +8938,33 @@ impl Editor { } } self.unfold_ranges(&to_unfold, true, true, cx); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_ranges(new_selection_ranges); }); } - pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext) { - self.add_selection(true, cx); + pub fn add_selection_above( + &mut self, + _: &AddSelectionAbove, + window: &mut Window, + cx: &mut Context, + ) { + self.add_selection(true, window, cx); } - pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext) { - self.add_selection(false, cx); + pub fn add_selection_below( + &mut self, + _: &AddSelectionBelow, + window: &mut Window, + cx: &mut Context, + ) { + self.add_selection(false, window, cx); } - fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { + fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(cx); - let text_layout_details = self.text_layout_details(cx); + let text_layout_details = self.text_layout_details(window); let mut state = self.add_selections_state.take().unwrap_or_else(|| { let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); let range = oldest_selection.display_range(&display_map).sorted(); @@ -8573,7 +9056,7 @@ impl Editor { state.stack.pop(); } - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(new_selections); }); if state.stack.len() > 1 { @@ -8586,17 +9069,19 @@ impl Editor { display_map: &DisplaySnapshot, replace_newest: bool, autoscroll: Option, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Result<()> { fn select_next_match_ranges( this: &mut Editor, range: Range, replace_newest: bool, auto_scroll: Option, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { this.unfold_ranges(&[range.clone()], false, true, cx); - this.change_selections(auto_scroll, cx, |s| { + this.change_selections(auto_scroll, window, cx, |s| { if replace_newest { s.delete(s.newest_anchor().id); } @@ -8653,6 +9138,7 @@ impl Editor { next_selected_range, replace_newest, autoscroll, + window, cx, ); } else { @@ -8710,6 +9196,7 @@ impl Editor { selection.start..selection.end, replace_newest, autoscroll, + window, cx, ); } @@ -8737,7 +9224,13 @@ impl Editor { wordwise: false, done: false, }); - self.select_next_match_internal(display_map, replace_newest, autoscroll, cx)?; + self.select_next_match_internal( + display_map, + replace_newest, + autoscroll, + window, + cx, + )?; } } Ok(()) @@ -8746,12 +9239,13 @@ impl Editor { pub fn select_all_matches( &mut self, _action: &SelectAllMatches, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Result<()> { self.push_to_selection_history(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - self.select_next_match_internal(&display_map, false, None, cx)?; + self.select_next_match_internal(&display_map, false, None, window, cx)?; let Some(select_next_state) = self.select_next_state.as_mut() else { return Ok(()); }; @@ -8814,20 +9308,26 @@ impl Editor { false, cx, ); - self.change_selections(Some(Autoscroll::fit()), cx, |selections| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |selections| { selections.select(new_selections) }); Ok(()) } - pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) -> Result<()> { + pub fn select_next( + &mut self, + action: &SelectNext, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { self.push_to_selection_history(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.select_next_match_internal( &display_map, action.replace_newest, Some(Autoscroll::newest()), + window, cx, )?; Ok(()) @@ -8836,7 +9336,8 @@ impl Editor { pub fn select_previous( &mut self, action: &SelectPrevious, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Result<()> { self.push_to_selection_history(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -8879,7 +9380,7 @@ impl Editor { if let Some(next_selected_range) = next_selected_range { self.unfold_ranges(&[next_selected_range.clone()], false, true, cx); - self.change_selections(Some(Autoscroll::newest()), cx, |s| { + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { if action.replace_newest { s.delete(s.newest_anchor().id); } @@ -8960,7 +9461,7 @@ impl Editor { true, cx, ); - self.change_selections(Some(Autoscroll::newest()), cx, |s| { + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { s.select(selections); }); } else if let Some(selected_text) = selected_text { @@ -8969,18 +9470,23 @@ impl Editor { wordwise: false, done: false, }); - self.select_previous(action, cx)?; + self.select_previous(action, window, cx)?; } } Ok(()) } - pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { + pub fn toggle_comments( + &mut self, + action: &ToggleComments, + window: &mut Window, + cx: &mut Context, + ) { if self.read_only(cx) { return; } - let text_layout_details = &self.text_layout_details(cx); - self.transact(cx, |this, cx| { + let text_layout_details = &self.text_layout_details(window); + self.transact(window, cx, |this, window, cx| { let mut selections = this.selections.all::(cx); let mut edits = Vec::new(); let mut selection_edit_ranges = Vec::new(); @@ -9233,7 +9739,9 @@ impl Editor { } drop(snapshot); - this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); let selections = this.selections.all::(cx); let selections_on_single_row = selections.windows(2).all(|selections| { @@ -9252,7 +9760,7 @@ impl Editor { if advance_downwards { let snapshot = this.buffer.read(cx).snapshot(cx); - this.change_selections(Some(Autoscroll::fit()), cx, |s| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_cursors_with(|display_snapshot, display_point, _| { let mut point = display_point.to_point(display_snapshot); point.row += 1; @@ -9273,7 +9781,8 @@ impl Editor { pub fn select_enclosing_symbol( &mut self, _: &SelectEnclosingSymbol, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let buffer = self.buffer.read(cx).snapshot(cx); let old_selections = self.selections.all::(cx).into_boxed_slice(); @@ -9316,7 +9825,7 @@ impl Editor { .collect::>(); if selected_larger_symbol { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(new_selections); }); } @@ -9325,7 +9834,8 @@ impl Editor { pub fn select_larger_syntax_node( &mut self, _: &SelectLargerSyntaxNode, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -9374,7 +9884,7 @@ impl Editor { if selected_larger_node { stack.push(old_selections); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(new_selections); }); } @@ -9384,24 +9894,25 @@ impl Editor { pub fn select_smaller_syntax_node( &mut self, _: &SelectSmallerSyntaxNode, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); if let Some(selections) = stack.pop() { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(selections.to_vec()); }); } self.select_larger_syntax_node_stack = stack; } - fn refresh_runnables(&mut self, cx: &mut ViewContext) -> Task<()> { + fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context) -> Task<()> { if !EditorSettings::get_global(cx).gutter.runnables { self.clear_tasks(); return Task::ready(()); } - let project = self.project.as_ref().map(Model::downgrade); - cx.spawn(|this, mut cx| async move { + let project = self.project.as_ref().map(Entity::downgrade); + cx.spawn_in(window, |this, mut cx| async move { cx.background_executor().timer(UPDATE_DEBOUNCE).await; let Some(project) = project.and_then(|p| p.upgrade()) else { return; @@ -9430,8 +9941,8 @@ impl Editor { } }) .await; - let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone()); + let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone()); this.update(&mut cx, |this, _| { this.clear_tasks(); for (key, value) in rows { @@ -9449,7 +9960,7 @@ impl Editor { } fn runnable_rows( - project: Model, + project: Entity, snapshot: DisplaySnapshot, runnable_ranges: Vec, mut cx: AsyncWindowContext, @@ -9458,7 +9969,7 @@ impl Editor { .into_iter() .filter_map(|mut runnable| { let tasks = cx - .update(|cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) + .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) .ok()?; if tasks.is_empty() { return None; @@ -9490,9 +10001,9 @@ impl Editor { } fn templates_with_tags( - project: &Model, + project: &Entity, runnable: &mut Runnable, - cx: &WindowContext, + cx: &mut App, ) -> Vec<(TaskSourceKind, TaskTemplate)> { let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { let (worktree_id, file) = project @@ -9548,9 +10059,10 @@ impl Editor { pub fn move_to_enclosing_bracket( &mut self, _: &MoveToEnclosingBracket, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_offsets_with(|snapshot, selection| { let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) @@ -9604,11 +10116,18 @@ impl Editor { }); } - pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext) { - self.end_selection(cx); + pub fn undo_selection( + &mut self, + _: &UndoSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.end_selection(window, cx); self.selection_history.mode = SelectionHistoryMode::Undoing; if let Some(entry) = self.selection_history.undo_stack.pop_back() { - self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); + self.change_selections(None, window, cx, |s| { + s.select_anchors(entry.selections.to_vec()) + }); self.select_next_state = entry.select_next_state; self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; @@ -9617,11 +10136,18 @@ impl Editor { self.selection_history.mode = SelectionHistoryMode::Normal; } - pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext) { - self.end_selection(cx); + pub fn redo_selection( + &mut self, + _: &RedoSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.end_selection(window, cx); self.selection_history.mode = SelectionHistoryMode::Redoing; if let Some(entry) = self.selection_history.redo_stack.pop_back() { - self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); + self.change_selections(None, window, cx, |s| { + s.select_anchors(entry.selections.to_vec()) + }); self.select_next_state = entry.select_next_state; self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; @@ -9630,19 +10156,30 @@ impl Editor { self.selection_history.mode = SelectionHistoryMode::Normal; } - pub fn expand_excerpts(&mut self, action: &ExpandExcerpts, cx: &mut ViewContext) { + pub fn expand_excerpts( + &mut self, + action: &ExpandExcerpts, + _: &mut Window, + cx: &mut Context, + ) { self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::UpAndDown, cx) } pub fn expand_excerpts_down( &mut self, action: &ExpandExcerptsDown, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) { self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Down, cx) } - pub fn expand_excerpts_up(&mut self, action: &ExpandExcerptsUp, cx: &mut ViewContext) { + pub fn expand_excerpts_up( + &mut self, + action: &ExpandExcerptsUp, + _: &mut Window, + cx: &mut Context, + ) { self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Up, cx) } @@ -9650,7 +10187,8 @@ impl Editor { &mut self, lines: u32, direction: ExpandExcerptDirection, - cx: &mut ViewContext, + + cx: &mut Context, ) { let selections = self.selections.disjoint_anchors(); @@ -9664,11 +10202,7 @@ impl Editor { let snapshot = buffer.snapshot(cx); let mut excerpt_ids = selections .iter() - .flat_map(|selection| { - snapshot - .excerpts_for_range(selection.range()) - .map(|excerpt| excerpt.id()) - }) + .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range())) .collect::>(); excerpt_ids.sort(); excerpt_ids.dedup(); @@ -9680,7 +10214,7 @@ impl Editor { &mut self, excerpt: ExcerptId, direction: ExpandExcerptDirection, - cx: &mut ViewContext, + cx: &mut Context, ) { let lines = EditorSettings::get_global(cx).expand_excerpt_lines; self.buffer.update(cx, |buffer, cx| { @@ -9688,30 +10222,83 @@ impl Editor { }) } - fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { - self.go_to_diagnostic_impl(Direction::Next, cx) + pub fn go_to_singleton_buffer_point( + &mut self, + point: Point, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_singleton_buffer_range(point..point, window, cx); + } + + pub fn go_to_singleton_buffer_range( + &mut self, + range: Range, + window: &mut Window, + cx: &mut Context, + ) { + let multibuffer = self.buffer().read(cx); + let Some(buffer) = multibuffer.as_singleton() else { + return; + }; + let Some(start) = multibuffer.buffer_point_to_anchor(&buffer, range.start, cx) else { + return; + }; + let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else { + return; + }; + self.change_selections(Some(Autoscroll::center()), window, cx, |s| { + s.select_anchor_ranges([start..end]) + }); + } + + fn go_to_diagnostic( + &mut self, + _: &GoToDiagnostic, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_diagnostic_impl(Direction::Next, window, cx) } - fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext) { - self.go_to_diagnostic_impl(Direction::Prev, cx) + fn go_to_prev_diagnostic( + &mut self, + _: &GoToPrevDiagnostic, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_diagnostic_impl(Direction::Prev, window, cx) } - pub fn go_to_diagnostic_impl(&mut self, direction: Direction, cx: &mut ViewContext) { + pub fn go_to_diagnostic_impl( + &mut self, + direction: Direction, + window: &mut Window, + cx: &mut Context, + ) { let buffer = self.buffer.read(cx).snapshot(cx); let selection = self.selections.newest::(cx); // If there is an active Diagnostic Popover jump to its diagnostic instead. if direction == Direction::Next { if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() { - self.activate_diagnostics(popover.group_id(), cx); + let Some(buffer_id) = popover.local_diagnostic.range.start.buffer_id else { + return; + }; + self.activate_diagnostics( + buffer_id, + popover.local_diagnostic.diagnostic.group_id, + window, + cx, + ); if let Some(active_diagnostics) = self.active_diagnostics.as_ref() { let primary_range_start = active_diagnostics.primary_range.start; - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { let mut new_selection = s.newest_anchor().clone(); new_selection.collapse_to(primary_range_start, SelectionGoal::None); s.select_anchors(vec![new_selection.clone()]); }); - self.refresh_inline_completion(false, true, cx); + self.refresh_inline_completion(false, true, window, cx); } return; } @@ -9732,27 +10319,29 @@ impl Editor { } else { selection.head() }; - let snapshot = self.snapshot(cx); + let snapshot = self.snapshot(window, cx); loop { - let diagnostics = if direction == Direction::Prev { - buffer.diagnostics_in_range(0..search_start, true) + let mut diagnostics; + if direction == Direction::Prev { + diagnostics = buffer + .diagnostics_in_range::<_, usize>(0..search_start) + .collect::>(); + diagnostics.reverse(); } else { - buffer.diagnostics_in_range(search_start..buffer.len(), false) - } - .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start)); - let search_start_anchor = buffer.anchor_after(search_start); + diagnostics = buffer + .diagnostics_in_range::<_, usize>(search_start..buffer.len()) + .collect::>(); + }; let group = diagnostics + .into_iter() + .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start)) // relies on diagnostics_in_range to return diagnostics with the same starting range to // be sorted in a stable way // skip until we are at current active diagnostic, if it exists .skip_while(|entry| { let is_in_range = match direction { - Direction::Prev => { - entry.range.start.cmp(&search_start_anchor, &buffer).is_ge() - } - Direction::Next => { - entry.range.start.cmp(&search_start_anchor, &buffer).is_le() - } + Direction::Prev => entry.range.end > search_start, + Direction::Next => entry.range.start < search_start, }; is_in_range && self @@ -9763,7 +10352,7 @@ impl Editor { .find_map(|entry| { if entry.diagnostic.is_primary && entry.diagnostic.severity <= DiagnosticSeverity::WARNING - && !(entry.range.start == entry.range.end) + && entry.range.start != entry.range.end // if we match with the active diagnostic, skip it && Some(entry.diagnostic.group_id) != self.active_diagnostics.as_ref().map(|d| d.group_id) @@ -9775,10 +10364,12 @@ impl Editor { }); if let Some((primary_range, group_id)) = group { - self.activate_diagnostics(group_id, cx); - let primary_range = primary_range.to_offset(&buffer); + let Some(buffer_id) = buffer.anchor_after(primary_range.start).buffer_id else { + return; + }; + self.activate_diagnostics(buffer_id, group_id, window, cx); if self.active_diagnostics.is_some() { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select(vec![Selection { id: selection.id, start: primary_range.start, @@ -9787,7 +10378,7 @@ impl Editor { goal: SelectionGoal::None, }]); }); - self.refresh_inline_completion(false, true, cx); + self.refresh_inline_completion(false, true, window, cx); } break; } else { @@ -9809,102 +10400,76 @@ impl Editor { } } - fn go_to_next_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx); + fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { + let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::(cx); - self.go_to_hunk_after_position(&snapshot, selection.head(), cx); + self.go_to_hunk_after_position(&snapshot, selection.head(), window, cx); } fn go_to_hunk_after_position( &mut self, snapshot: &EditorSnapshot, position: Point, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option { - for (ix, position) in [position, Point::zero()].into_iter().enumerate() { - if let Some(hunk) = self.go_to_next_hunk_in_direction( - snapshot, - position, - ix > 0, - snapshot.diff_map.diff_hunks_in_range( - position + Point::new(1, 0)..snapshot.buffer_snapshot.max_point(), - &snapshot.buffer_snapshot, - ), - cx, - ) { - return Some(hunk); - } + let mut hunk = snapshot + .buffer_snapshot + .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) + .find(|hunk| hunk.row_range.start.0 > position.row); + if hunk.is_none() { + hunk = snapshot + .buffer_snapshot + .diff_hunks_in_range(Point::zero()..position) + .find(|hunk| hunk.row_range.end.0 < position.row) + } + if let Some(hunk) = &hunk { + let destination = Point::new(hunk.row_range.start.0, 0); + self.unfold_ranges(&[destination..destination], false, false, cx); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![destination..destination]); + }); } - None + + hunk } - fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext) { - let snapshot = self.snapshot(cx); + fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, window: &mut Window, cx: &mut Context) { + let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::(cx); - self.go_to_hunk_before_position(&snapshot, selection.head(), cx); + self.go_to_hunk_before_position(&snapshot, selection.head(), window, cx); } fn go_to_hunk_before_position( &mut self, snapshot: &EditorSnapshot, position: Point, - cx: &mut ViewContext, - ) -> Option { - for (ix, position) in [position, snapshot.buffer_snapshot.max_point()] - .into_iter() - .enumerate() - { - if let Some(hunk) = self.go_to_next_hunk_in_direction( - snapshot, - position, - ix > 0, - snapshot - .diff_map - .diff_hunks_in_range_rev(Point::zero()..position, &snapshot.buffer_snapshot), - cx, - ) { - return Some(hunk); - } - } - None - } - - fn go_to_next_hunk_in_direction( - &mut self, - snapshot: &DisplaySnapshot, - initial_point: Point, - is_wrapped: bool, - hunks: impl Iterator, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option { - let display_point = initial_point.to_display_point(snapshot); - let mut hunks = hunks - .map(|hunk| (diff_hunk_to_display(&hunk, snapshot), hunk)) - .filter(|(display_hunk, _)| { - is_wrapped || !display_hunk.contains_display_row(display_point.row()) - }) - .dedup(); - - if let Some((display_hunk, hunk)) = hunks.next() { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - let row = display_hunk.start_display_row(); - let point = DisplayPoint::new(row, 0); - s.select_display_ranges([point..point]); + let mut hunk = snapshot.buffer_snapshot.diff_hunk_before(position); + if hunk.is_none() { + hunk = snapshot.buffer_snapshot.diff_hunk_before(Point::MAX); + } + if let Some(hunk) = &hunk { + let destination = Point::new(hunk.row_range.start.0, 0); + self.unfold_ranges(&[destination..destination], false, false, cx); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![destination..destination]); }); - - Some(hunk) - } else { - None } + + hunk } pub fn go_to_line( &mut self, row: u32, highlight_color: Option, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - let snapshot = self.snapshot(cx).display_snapshot; + let snapshot = self.snapshot(window, cx).display_snapshot; let start = snapshot .buffer_snapshot .clip_point(Point::new(row, 0), Bias::Left); @@ -9926,15 +10491,17 @@ impl Editor { pub fn go_to_definition( &mut self, _: &GoToDefinition, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { - let definition = self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx); - cx.spawn(|editor, mut cx| async move { + let definition = + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, window, cx); + cx.spawn_in(window, |editor, mut cx| async move { if definition.await? == Navigated::Yes { return Ok(Navigated::Yes); } - match editor.update(&mut cx, |editor, cx| { - editor.find_all_references(&FindAllReferences, cx) + match editor.update_in(&mut cx, |editor, window, cx| { + editor.find_all_references(&FindAllReferences, window, cx) })? { Some(references) => references.await, None => Ok(Navigated::No), @@ -9945,64 +10512,72 @@ impl Editor { pub fn go_to_declaration( &mut self, _: &GoToDeclaration, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, cx) + self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, window, cx) } pub fn go_to_declaration_split( &mut self, _: &GoToDeclaration, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, cx) + self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, window, cx) } pub fn go_to_implementation( &mut self, _: &GoToImplementation, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, cx) + self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, window, cx) } pub fn go_to_implementation_split( &mut self, _: &GoToImplementationSplit, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, cx) + self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, window, cx) } pub fn go_to_type_definition( &mut self, _: &GoToTypeDefinition, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx) + self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, window, cx) } pub fn go_to_definition_split( &mut self, _: &GoToDefinitionSplit, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, cx) + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, window, cx) } pub fn go_to_type_definition_split( &mut self, _: &GoToTypeDefinitionSplit, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, cx) + self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, window, cx) } fn go_to_definition_of_kind( &mut self, kind: GotoDefinitionKind, split: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { let Some(provider) = self.semantics_provider.clone() else { return Task::ready(Ok(Navigated::No)); @@ -10019,10 +10594,10 @@ impl Editor { return Task::ready(Ok(Navigated::No)); }; - cx.spawn(|editor, mut cx| async move { + cx.spawn_in(window, |editor, mut cx| async move { let definitions = definitions.await?; let navigated = editor - .update(&mut cx, |editor, cx| { + .update_in(&mut cx, |editor, window, cx| { editor.navigate_to_hover_links( Some(kind), definitions @@ -10033,6 +10608,7 @@ impl Editor { .map(HoverLink::Text) .collect::>(), split, + window, cx, ) })? @@ -10041,7 +10617,7 @@ impl Editor { }) } - pub fn open_url(&mut self, _: &OpenUrl, cx: &mut ViewContext) { + pub fn open_url(&mut self, _: &OpenUrl, window: &mut Window, cx: &mut Context) { let selection = self.selections.newest_anchor(); let head = selection.head(); let tail = selection.tail(); @@ -10061,7 +10637,7 @@ impl Editor { None }; - let url_finder = cx.spawn(|editor, mut cx| async move { + let url_finder = cx.spawn_in(window, |editor, mut cx| async move { let url = if let Some(end_pos) = end_position { find_url_from_range(&buffer, start_position..end_pos, cx.clone()) } else { @@ -10080,7 +10656,12 @@ impl Editor { url_finder.detach(); } - pub fn open_selected_filename(&mut self, _: &OpenSelectedFilename, cx: &mut ViewContext) { + pub fn open_selected_filename( + &mut self, + _: &OpenSelectedFilename, + window: &mut Window, + cx: &mut Context, + ) { let Some(workspace) = self.workspace() else { return; }; @@ -10095,13 +10676,13 @@ impl Editor { let project = self.project.clone(); - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { let result = find_file(&buffer, project, buffer_position, &mut cx).await; if let Some((_, path)) = result { workspace - .update(&mut cx, |workspace, cx| { - workspace.open_resolved_path(path, cx) + .update_in(&mut cx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) })? .await?; } @@ -10115,7 +10696,8 @@ impl Editor { kind: Option, mut definitions: Vec, split: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { // If there is one definition, just open it directly if definitions.len() == 1 { @@ -10131,7 +10713,8 @@ impl Editor { Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target)))) } HoverLink::InlayHint(lsp_location, server_id) => { - let computation = self.compute_target_location(lsp_location, server_id, cx); + let computation = + self.compute_target_location(lsp_location, server_id, window, cx); cx.background_executor().spawn(async move { let location = computation.await?; Ok(TargetTaskResult::Location(location)) @@ -10143,10 +10726,10 @@ impl Editor { } HoverLink::File(path) => { if let Some(workspace) = self.workspace() { - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { workspace - .update(&mut cx, |workspace, cx| { - workspace.open_resolved_path(path, cx) + .update_in(&mut cx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) })? .await .map(|_| TargetTaskResult::AlreadyNavigated) @@ -10156,34 +10739,31 @@ impl Editor { } } }; - cx.spawn(|editor, mut cx| async move { + cx.spawn_in(window, |editor, mut cx| async move { let target = match target_task.await.context("target resolution task")? { TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes), TargetTaskResult::Location(None) => return Ok(Navigated::No), TargetTaskResult::Location(Some(target)) => target, }; - editor.update(&mut cx, |editor, cx| { + editor.update_in(&mut cx, |editor, window, cx| { let Some(workspace) = editor.workspace() else { return Navigated::No; }; let pane = workspace.read(cx).active_pane().clone(); - let range = target.range.to_offset(target.buffer.read(cx)); + let range = target.range.to_point(target.buffer.read(cx)); let range = editor.range_for_match(&range); + let range = collapse_multiline_range(range); if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { - let buffer = target.buffer.read(cx); - let range = check_multiline_range(buffer, range); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); + editor.go_to_singleton_buffer_range(range.clone(), window, cx); } else { - cx.window_context().defer(move |cx| { - let target_editor: View = + window.defer(cx, move |window, cx| { + let target_editor: Entity = workspace.update(cx, |workspace, cx| { let pane = if split { - workspace.adjacent_pane(cx) + workspace.adjacent_pane(window, cx) } else { workspace.active_pane().clone() }; @@ -10193,6 +10773,7 @@ impl Editor { target.buffer.clone(), true, true, + window, cx, ) }); @@ -10200,15 +10781,7 @@ impl Editor { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. pane.update(cx, |pane, _| pane.disable_history()); - let buffer = target.buffer.read(cx); - let range = check_multiline_range(buffer, range); - target_editor.change_selections( - Some(Autoscroll::focused()), - cx, - |s| { - s.select_ranges([range]); - }, - ); + target_editor.go_to_singleton_buffer_range(range, window, cx); pane.update(cx, |pane, _| pane.enable_history()); }); }); @@ -10217,9 +10790,9 @@ impl Editor { }) }) } else if !definitions.is_empty() { - cx.spawn(|editor, mut cx| async move { + cx.spawn_in(window, |editor, mut cx| async move { let (title, location_tasks, workspace) = editor - .update(&mut cx, |editor, cx| { + .update_in(&mut cx, |editor, window, cx| { let tab_kind = match kind { Some(GotoDefinitionKind::Implementation) => "Implementations", _ => "Definitions", @@ -10246,9 +10819,8 @@ impl Editor { .into_iter() .map(|definition| match definition { HoverLink::Text(link) => Task::ready(Ok(Some(link.target))), - HoverLink::InlayHint(lsp_location, server_id) => { - editor.compute_target_location(lsp_location, server_id, cx) - } + HoverLink::InlayHint(lsp_location, server_id) => editor + .compute_target_location(lsp_location, server_id, window, cx), HoverLink::Url(_) => Task::ready(Ok(None)), HoverLink::File(_) => Task::ready(Ok(None)), }) @@ -10268,8 +10840,16 @@ impl Editor { return Ok(Navigated::No); }; let opened = workspace - .update(&mut cx, |workspace, cx| { - Self::open_locations_in_multibuffer(workspace, locations, title, split, cx) + .update_in(&mut cx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + split, + MultibufferSelectionMode::First, + window, + cx, + ) }) .ok(); @@ -10284,13 +10864,14 @@ impl Editor { &self, lsp_location: lsp::Location, server_id: LanguageServerId, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task>> { let Some(project) = self.project.clone() else { return Task::ready(Ok(None)); }; - cx.spawn(move |editor, mut cx| async move { + cx.spawn_in(window, move |editor, mut cx| async move { let location_task = editor.update(&mut cx, |_, cx| { project.update(cx, |project, cx| { let language_server_name = project @@ -10332,7 +10913,8 @@ impl Editor { pub fn find_all_references( &mut self, _: &FindAllReferences, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option>> { let selection = self.selections.newest::(cx); let multi_buffer = self.buffer.read(cx); @@ -10367,7 +10949,7 @@ impl Editor { let workspace = self.workspace()?; let project = workspace.read(cx).project().clone(); let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); - Some(cx.spawn(|editor, mut cx| async move { + Some(cx.spawn_in(window, |editor, mut cx| async move { let _cleanup = defer({ let mut cx = cx.clone(); move || { @@ -10390,7 +10972,7 @@ impl Editor { return anyhow::Ok(Navigated::No); } - workspace.update(&mut cx, |workspace, cx| { + workspace.update_in(&mut cx, |workspace, window, cx| { let title = locations .first() .as_ref() @@ -10404,7 +10986,15 @@ impl Editor { ) }) .unwrap(); - Self::open_locations_in_multibuffer(workspace, locations, title, false, cx); + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + false, + MultibufferSelectionMode::First, + window, + cx, + ); Navigated::Yes }) })) @@ -10416,15 +11006,17 @@ impl Editor { mut locations: Vec, title: String, split: bool, - cx: &mut ViewContext, + multibuffer_selection_mode: MultibufferSelectionMode, + window: &mut Window, + cx: &mut Context, ) { // If there are multiple definitions, open them in a multibuffer locations.sort_by_key(|location| location.buffer.read(cx).remote_id()); let mut locations = locations.into_iter().peekable(); - let mut ranges_to_highlight = Vec::new(); + let mut ranges = Vec::new(); let capability = workspace.project().read(cx).capability(); - let excerpt_buffer = cx.new_model(|cx| { + let excerpt_buffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(capability); while let Some(location) = locations.next() { let buffer = location.buffer.read(cx); @@ -10442,7 +11034,7 @@ impl Editor { } ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); - ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines( + ranges.extend(multibuffer.push_excerpts_with_context_lines( location.buffer.clone(), ranges_for_buffer, DEFAULT_MULTIBUFFER_CONTEXT, @@ -10453,21 +11045,37 @@ impl Editor { multibuffer.with_title(title) }); - let editor = cx.new_view(|cx| { - Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), true, cx) + let editor = cx.new(|cx| { + Editor::for_multibuffer( + excerpt_buffer, + Some(workspace.project().clone()), + true, + window, + cx, + ) }); editor.update(cx, |editor, cx| { - if let Some(first_range) = ranges_to_highlight.first() { - editor.change_selections(None, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(std::iter::once(first_range.clone())); - }); + match multibuffer_selection_mode { + MultibufferSelectionMode::First => { + if let Some(first_range) = ranges.first() { + editor.change_selections(None, window, cx, |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(std::iter::once(first_range.clone())); + }); + } + editor.highlight_background::( + &ranges, + |theme| theme.editor_highlighted_line_background, + cx, + ); + } + MultibufferSelectionMode::All => { + editor.change_selections(None, window, cx, |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(ranges); + }); + } } - editor.highlight_background::( - &ranges_to_highlight, - |theme| theme.editor_highlighted_line_background, - cx, - ); editor.register_buffers_with_language_servers(cx); }); @@ -10475,23 +11083,28 @@ impl Editor { let item_id = item.item_id(); if split { - workspace.split_item(SplitDirection::Right, item.clone(), cx); + workspace.split_item(SplitDirection::Right, item.clone(), window, cx); } else { let destination_index = workspace.active_pane().update(cx, |pane, cx| { if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - pane.close_current_preview_item(cx) + pane.close_current_preview_item(window, cx) } else { None } }); - workspace.add_item_to_active_pane(item.clone(), destination_index, true, cx); + workspace.add_item_to_active_pane(item.clone(), destination_index, true, window, cx); } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); }); } - pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + pub fn rename( + &mut self, + _: &Rename, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { use language::ToOffset as _; let provider = self.semantics_provider.clone()?; @@ -10516,7 +11129,7 @@ impl Editor { .unwrap_or_else(|| Task::ready(Ok(None))); drop(snapshot); - Some(cx.spawn(|this, mut cx| async move { + Some(cx.spawn_in(window, |this, mut cx| async move { let rename_range = if let Some(range) = prepare_rename.await? { Some(range) } else { @@ -10534,7 +11147,7 @@ impl Editor { })? }; if let Some(rename_range) = rename_range { - this.update(&mut cx, |this, cx| { + this.update_in(&mut cx, |this, window, cx| { let snapshot = cursor_buffer.read(cx).snapshot(); let rename_buffer_range = rename_range.to_offset(&snapshot); let cursor_offset_in_rename_range = @@ -10542,7 +11155,7 @@ impl Editor { let cursor_offset_in_rename_range_end = cursor_buffer_offset_end.saturating_sub(rename_buffer_range.start); - this.take_rename(false, cx); + this.take_rename(false, window, cx); let buffer = this.buffer.read(cx).read(cx); let cursor_offset = selection.head().to_offset(&buffer); let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); @@ -10564,8 +11177,8 @@ impl Editor { // Position the selection in the rename editor so that it matches the current selection. this.show_local_selections = false; - let rename_editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); + let rename_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); editor.buffer.update(cx, |buffer, cx| { buffer.edit([(0..0, old_name.clone())], None, cx) }); @@ -10573,7 +11186,7 @@ impl Editor { .cmp(&cursor_offset_in_rename_range_end) { Ordering::Equal => { - editor.select_all(&SelectAll, cx); + editor.select_all(&SelectAll, window, cx); return editor; } Ordering::Less => { @@ -10584,9 +11197,9 @@ impl Editor { } }; if rename_selection_range.end > old_name.len() { - editor.select_all(&SelectAll, cx); + editor.select_all(&SelectAll, window, cx); } else { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.select_ranges([rename_selection_range]); }); } @@ -10619,7 +11232,7 @@ impl Editor { cx, ); let rename_focus_handle = rename_editor.focus_handle(cx); - cx.focus(&rename_focus_handle); + window.focus(&rename_focus_handle); let block_id = this.insert_blocks( [BlockProperties { style: BlockStyle::Flex, @@ -10648,10 +11261,10 @@ impl Editor { status: cx.editor_style.status.clone(), inlay_hints_style: HighlightStyle { font_weight: Some(FontWeight::BOLD), - ..make_inlay_hints_style(cx) + ..make_inlay_hints_style(cx.app) }, inline_completion_styles: make_suggestion_styles( - cx, + cx.app, ), ..EditorStyle::default() }, @@ -10680,9 +11293,10 @@ impl Editor { pub fn confirm_rename( &mut self, _: &ConfirmRename, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option>> { - let rename = self.take_rename(false, cx)?; + let rename = self.take_rename(false, window, cx)?; let workspace = self.workspace()?.downgrade(); let (buffer, start) = self .buffer @@ -10706,7 +11320,7 @@ impl Editor { cx, )?; - Some(cx.spawn(|editor, mut cx| async move { + Some(cx.spawn_in(window, |editor, mut cx| async move { let project_transaction = rename.await?; Self::open_project_transaction( &editor, @@ -10727,11 +11341,12 @@ impl Editor { fn take_rename( &mut self, moving_cursor: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option { let rename = self.pending_rename.take()?; - if rename.editor.focus_handle(cx).is_focused(cx) { - cx.focus(&self.focus_handle); + if rename.editor.focus_handle(cx).is_focused(window) { + window.focus(&self.focus_handle); } self.remove_blocks( @@ -10756,7 +11371,7 @@ impl Editor { .min(rename_range.end); drop(snapshot); - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { s.select_ranges(vec![cursor_in_editor..cursor_in_editor]) }); } else { @@ -10770,19 +11385,31 @@ impl Editor { self.pending_rename.as_ref() } - fn format(&mut self, _: &Format, cx: &mut ViewContext) -> Option>> { + fn format( + &mut self, + _: &Format, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { let project = match &self.project { Some(project) => project.clone(), None => return None, }; - Some(self.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffers, cx)) + Some(self.perform_format( + project, + FormatTrigger::Manual, + FormatTarget::Buffers, + window, + cx, + )) } fn format_selections( &mut self, _: &FormatSelections, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option>> { let project = match &self.project { Some(project) => project.clone(), @@ -10800,16 +11427,18 @@ impl Editor { project, FormatTrigger::Manual, FormatTarget::Ranges(ranges), + window, cx, )) } fn perform_format( &mut self, - project: Model, + project: Entity, trigger: FormatTrigger, target: FormatTarget, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task> { let buffer = self.buffer.clone(); let (buffers, target) = match target { @@ -10827,11 +11456,12 @@ impl Editor { let mut buffer_id_to_ranges: BTreeMap>> = BTreeMap::new(); for selection_range in selection_ranges { - for (excerpt, buffer_range) in snapshot.range_to_buffer_ranges(selection_range) + for (buffer, buffer_range, _) in + snapshot.range_to_buffer_ranges(selection_range) { - let buffer_id = excerpt.buffer_id(); - let start = excerpt.buffer().anchor_before(buffer_range.start); - let end = excerpt.buffer().anchor_after(buffer_range.end); + let buffer_id = buffer.remote_id(); + let start = buffer.anchor_before(buffer_range.start); + let end = buffer.anchor_after(buffer_range.end); buffers.insert(multi_buffer.buffer(buffer_id).unwrap()); buffer_id_to_ranges .entry(buffer_id) @@ -10848,7 +11478,7 @@ impl Editor { project.format(buffers, target, true, trigger, cx) }); - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { let transaction = futures::select_biased! { () = timeout => { log::warn!("timed out waiting for formatting"); @@ -10873,7 +11503,12 @@ impl Editor { }) } - fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext) { + fn restart_language_server( + &mut self, + _: &RestartLanguageServer, + _: &mut Window, + cx: &mut Context, + ) { if let Some(project) = self.project.clone() { self.buffer.update(cx, |multi_buffer, cx| { project.update(cx, |project, cx| { @@ -10886,7 +11521,8 @@ impl Editor { fn cancel_language_server_work( &mut self, _: &actions::CancelLanguageServerWork, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) { if let Some(project) = self.project.clone() { self.buffer.update(cx, |multi_buffer, cx| { @@ -10897,21 +11533,25 @@ impl Editor { } } - fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { - cx.show_character_palette(); + fn show_character_palette( + &mut self, + _: &ShowCharacterPalette, + window: &mut Window, + _: &mut Context, + ) { + window.show_character_palette(); } - fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { + fn refresh_active_diagnostics(&mut self, cx: &mut Context) { if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { let buffer = self.buffer.read(cx).snapshot(cx); let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); let is_valid = buffer - .diagnostics_in_range(active_diagnostics.primary_range.clone(), false) + .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone()) .any(|entry| { - let range = entry.range.to_offset(&buffer); entry.diagnostic.is_primary - && !range.is_empty() - && range.start == primary_range_start + && !entry.range.is_empty() + && entry.range.start == primary_range_start && entry.diagnostic.message == active_diagnostics.primary_message }); @@ -10931,29 +11571,31 @@ impl Editor { } } - fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) { + fn activate_diagnostics( + &mut self, + buffer_id: BufferId, + group_id: usize, + window: &mut Window, + cx: &mut Context, + ) { self.dismiss_diagnostics(cx); - let snapshot = self.snapshot(cx); + let snapshot = self.snapshot(window, cx); self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { let buffer = self.buffer.read(cx).snapshot(cx); let mut primary_range = None; let mut primary_message = None; - let mut group_end = Point::zero(); let diagnostic_group = buffer - .diagnostic_group(group_id) + .diagnostic_group(buffer_id, group_id) .filter_map(|entry| { - let start = entry.range.start.to_point(&buffer); - let end = entry.range.end.to_point(&buffer); + let start = entry.range.start; + let end = entry.range.end; if snapshot.is_line_folded(MultiBufferRow(start.row)) && (start.row == end.row || snapshot.is_line_folded(MultiBufferRow(end.row))) { return None; } - if end > group_end { - group_end = end; - } if entry.diagnostic.is_primary { primary_range = Some(entry.range.clone()); primary_message = Some(entry.diagnostic.message.clone()); @@ -10986,7 +11628,8 @@ impl Editor { .collect(); Some(ActiveDiagnosticGroup { - primary_range, + primary_range: buffer.anchor_before(primary_range.start) + ..buffer.anchor_after(primary_range.end), primary_message, group_id, blocks, @@ -10995,7 +11638,7 @@ impl Editor { }); } - fn dismiss_diagnostics(&mut self, cx: &mut ViewContext) { + fn dismiss_diagnostics(&mut self, cx: &mut Context) { if let Some(active_diagnostic_group) = self.active_diagnostics.take() { self.display_map.update(cx, |display_map, cx| { display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx); @@ -11008,7 +11651,8 @@ impl Editor { &mut self, selections: Vec>, pending_selection: Option>, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let old_cursor_position = self.selections.newest_anchor().head(); self.selections.change_with(cx, |s| { @@ -11019,7 +11663,7 @@ impl Editor { s.clear_pending(); } }); - self.selections_did_change(false, &old_cursor_position, true, cx); + self.selections_did_change(false, &old_cursor_position, true, window, cx); } fn push_to_selection_history(&mut self) { @@ -11033,16 +11677,22 @@ impl Editor { pub fn transact( &mut self, - cx: &mut ViewContext, - update: impl FnOnce(&mut Self, &mut ViewContext), + window: &mut Window, + cx: &mut Context, + update: impl FnOnce(&mut Self, &mut Window, &mut Context), ) -> Option { - self.start_transaction_at(Instant::now(), cx); - update(self, cx); + self.start_transaction_at(Instant::now(), window, cx); + update(self, window, cx); self.end_transaction_at(Instant::now(), cx) } - pub fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { - self.end_selection(cx); + pub fn start_transaction_at( + &mut self, + now: Instant, + window: &mut Window, + cx: &mut Context, + ) { + self.end_selection(window, cx); if let Some(tx_id) = self .buffer .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) @@ -11058,7 +11708,7 @@ impl Editor { pub fn end_transaction_at( &mut self, now: Instant, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option { if let Some(transaction_id) = self .buffer @@ -11079,7 +11729,41 @@ impl Editor { } } - pub fn toggle_fold(&mut self, _: &actions::ToggleFold, cx: &mut ViewContext) { + pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { + if self.selection_mark_mode { + self.change_selections(None, window, cx, |s| { + s.move_with(|_, sel| { + sel.collapse_to(sel.head(), SelectionGoal::None); + }); + }) + } + self.selection_mark_mode = true; + cx.notify(); + } + + pub fn swap_selection_ends( + &mut self, + _: &actions::SwapSelectionEnds, + window: &mut Window, + cx: &mut Context, + ) { + self.change_selections(None, window, cx, |s| { + s.move_with(|_, sel| { + if sel.start != sel.end { + sel.reversed = !sel.reversed + } + }); + }); + self.request_autoscroll(Autoscroll::newest(), cx); + cx.notify(); + } + + pub fn toggle_fold( + &mut self, + _: &actions::ToggleFold, + window: &mut Window, + cx: &mut Context, + ) { if self.is_singleton(cx) { let selection = self.selections.newest::(cx); @@ -11094,23 +11778,22 @@ impl Editor { selection.range() }; if display_map.folds_in_range(range).next().is_some() { - self.unfold_lines(&Default::default(), cx) + self.unfold_lines(&Default::default(), window, cx) } else { - self.fold(&Default::default(), cx) + self.fold(&Default::default(), window, cx) } } else { let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let mut toggled_buffers = HashSet::default(); - for (_, buffer_snapshot, _) in - multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges()) - { - let buffer_id = buffer_snapshot.remote_id(); - if toggled_buffers.insert(buffer_id) { - if self.buffer_folded(buffer_id, cx) { - self.unfold_buffer(buffer_id, cx); - } else { - self.fold_buffer(buffer_id, cx); - } + let buffer_ids: HashSet<_> = multi_buffer_snapshot + .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges()) + .map(|(snapshot, _, _)| snapshot.remote_id()) + .collect(); + + for buffer_id in buffer_ids { + if self.is_buffer_folded(buffer_id, cx) { + self.unfold_buffer(buffer_id, cx); + } else { + self.fold_buffer(buffer_id, cx); } } } @@ -11119,7 +11802,8 @@ impl Editor { pub fn toggle_fold_recursive( &mut self, _: &actions::ToggleFoldRecursive, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let selection = self.selections.newest::(cx); @@ -11134,13 +11818,13 @@ impl Editor { selection.range() }; if display_map.folds_in_range(range).next().is_some() { - self.unfold_recursive(&Default::default(), cx) + self.unfold_recursive(&Default::default(), window, cx) } else { - self.fold_recursive(&Default::default(), cx) + self.fold_recursive(&Default::default(), window, cx) } } - pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext) { + pub fn fold(&mut self, _: &actions::Fold, window: &mut Window, cx: &mut Context) { if self.is_singleton(cx) { let mut to_fold = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -11180,22 +11864,26 @@ impl Editor { } } - self.fold_creases(to_fold, true, cx); + self.fold_creases(to_fold, true, window, cx); } else { let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let mut folded_buffers = HashSet::default(); - for (_, buffer_snapshot, _) in - multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges()) - { - let buffer_id = buffer_snapshot.remote_id(); - if folded_buffers.insert(buffer_id) { - self.fold_buffer(buffer_id, cx); - } + + let buffer_ids: HashSet<_> = multi_buffer_snapshot + .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges()) + .map(|(snapshot, _, _)| snapshot.remote_id()) + .collect(); + for buffer_id in buffer_ids { + self.fold_buffer(buffer_id, cx); } } } - fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext) { + fn fold_at_level( + &mut self, + fold_at: &FoldAtLevel, + window: &mut Window, + cx: &mut Context, + ) { if !self.buffer.read(cx).is_singleton() { return; } @@ -11208,7 +11896,7 @@ impl Editor { while let Some((mut start_row, end_row, current_level)) = stack.pop() { while start_row < end_row { match self - .snapshot(cx) + .snapshot(window, cx) .crease_for_buffer_row(MultiBufferRow(start_row)) { Some(crease) => { @@ -11228,27 +11916,28 @@ impl Editor { } } - self.fold_creases(to_fold, true, cx); + self.fold_creases(to_fold, true, window, cx); } - pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext) { + pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context) { if self.buffer.read(cx).is_singleton() { let mut fold_ranges = Vec::new(); let snapshot = self.buffer.read(cx).snapshot(cx); for row in 0..snapshot.max_row().0 { - if let Some(foldable_range) = - self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row)) + if let Some(foldable_range) = self + .snapshot(window, cx) + .crease_for_buffer_row(MultiBufferRow(row)) { fold_ranges.push(foldable_range); } } - self.fold_creases(fold_ranges, true, cx); + self.fold_creases(fold_ranges, true, window, cx); } else { - self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move { + self.toggle_fold_multiple_buffers = cx.spawn_in(window, |editor, mut cx| async move { editor - .update(&mut cx, |editor, cx| { + .update_in(&mut cx, |editor, _, cx| { for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { editor.fold_buffer(buffer_id, cx); } @@ -11261,21 +11950,30 @@ impl Editor { pub fn fold_function_bodies( &mut self, _: &actions::FoldFunctionBodies, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let snapshot = self.buffer.read(cx).snapshot(cx); - let Some((_, _, buffer)) = snapshot.as_singleton() else { - return; - }; - let creases = buffer - .function_body_fold_ranges(0..buffer.len()) + + let ranges = snapshot + .text_object_ranges(0..snapshot.len(), TreeSitterOptions::default()) + .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range)) + .collect::>(); + + let creases = ranges + .into_iter() .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone())) .collect(); - self.fold_creases(creases, true, cx); + self.fold_creases(creases, true, window, cx); } - pub fn fold_recursive(&mut self, _: &actions::FoldRecursive, cx: &mut ViewContext) { + pub fn fold_recursive( + &mut self, + _: &actions::FoldRecursive, + window: &mut Window, + cx: &mut Context, + ) { let mut to_fold = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all_adjusted(cx); @@ -11308,10 +12006,10 @@ impl Editor { } } - self.fold_creases(to_fold, true, cx); + self.fold_creases(to_fold, true, window, cx); } - pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext) { + pub fn fold_at(&mut self, fold_at: &FoldAt, window: &mut Window, cx: &mut Context) { let buffer_row = fold_at.buffer_row; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -11322,11 +12020,11 @@ impl Editor { .iter() .any(|selection| crease.range().overlaps(&selection.range())); - self.fold_creases(vec![crease], autoscroll, cx); + self.fold_creases(vec![crease], autoscroll, window, cx); } } - pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext) { + pub fn unfold_lines(&mut self, _: &UnfoldLines, _window: &mut Window, cx: &mut Context) { if self.is_singleton(cx) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -11346,19 +12044,22 @@ impl Editor { self.unfold_ranges(&ranges, true, true, cx); } else { let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let mut unfolded_buffers = HashSet::default(); - for (_, buffer_snapshot, _) in - multi_buffer_snapshot.excerpts_in_ranges(self.selections.disjoint_anchor_ranges()) - { - let buffer_id = buffer_snapshot.remote_id(); - if unfolded_buffers.insert(buffer_id) { - self.unfold_buffer(buffer_id, cx); - } + let buffer_ids: HashSet<_> = multi_buffer_snapshot + .ranges_to_buffer_ranges(self.selections.disjoint_anchor_ranges()) + .map(|(snapshot, _, _)| snapshot.remote_id()) + .collect(); + for buffer_id in buffer_ids { + self.unfold_buffer(buffer_id, cx); } } } - pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext) { + pub fn unfold_recursive( + &mut self, + _: &UnfoldRecursive, + _window: &mut Window, + cx: &mut Context, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); let ranges = selections @@ -11376,7 +12077,12 @@ impl Editor { self.unfold_ranges(&ranges, true, true, cx); } - pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext) { + pub fn unfold_at( + &mut self, + unfold_at: &UnfoldAt, + _window: &mut Window, + cx: &mut Context, + ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let intersection_range = Point::new(unfold_at.buffer_row.0, 0) @@ -11391,10 +12097,15 @@ impl Editor { .iter() .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range)); - self.unfold_ranges(&[intersection_range], true, autoscroll, cx) + self.unfold_ranges(&[intersection_range], true, autoscroll, cx); } - pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext) { + pub fn unfold_all( + &mut self, + _: &actions::UnfoldAll, + _window: &mut Window, + cx: &mut Context, + ) { if self.buffer.read(cx).is_singleton() { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx); @@ -11411,7 +12122,12 @@ impl Editor { } } - pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { + pub fn fold_selected_ranges( + &mut self, + _: &FoldSelectedRanges, + window: &mut Window, + cx: &mut Context, + ) { let selections = self.selections.all::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let line_mode = self.selections.line_mode; @@ -11432,28 +12148,30 @@ impl Editor { } }) .collect::>(); - self.fold_creases(ranges, true, cx); + self.fold_creases(ranges, true, window, cx); } pub fn fold_ranges( &mut self, ranges: Vec>, auto_scroll: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let ranges = ranges .into_iter() .map(|r| Crease::simple(r, display_map.fold_placeholder.clone())) .collect::>(); - self.fold_creases(ranges, auto_scroll, cx); + self.fold_creases(ranges, auto_scroll, window, cx); } pub fn fold_creases( &mut self, creases: Vec>, auto_scroll: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if creases.is_empty() { return; @@ -11475,15 +12193,11 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); } - for buffer_id in buffers_affected { - Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx); - } - cx.notify(); if let Some(active_diagnostics) = self.active_diagnostics.take() { // Clear diagnostics block when folding a range that contains it. - let snapshot = self.snapshot(cx); + let snapshot = self.snapshot(window, cx); if snapshot.intersects_fold(active_diagnostics.primary_range.start) { drop(snapshot); self.active_diagnostics = Some(active_diagnostics); @@ -11502,15 +12216,15 @@ impl Editor { ranges: &[Range], inclusive: bool, auto_scroll: bool, - cx: &mut ViewContext, + cx: &mut Context, ) { self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { map.unfold_intersecting(ranges.iter().cloned(), inclusive, cx) }); } - pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext) { - if self.buffer().read(cx).is_singleton() || self.buffer_folded(buffer_id, cx) { + pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) { return; } let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { @@ -11526,8 +12240,8 @@ impl Editor { cx.notify(); } - pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext) { - if self.buffer().read(cx).is_singleton() || !self.buffer_folded(buffer_id, cx) { + pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) { return; } let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { @@ -11544,8 +12258,12 @@ impl Editor { cx.notify(); } - pub fn buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool { - self.display_map.read(cx).buffer_folded(buffer) + pub fn is_buffer_folded(&self, buffer: BufferId, cx: &App) -> bool { + self.display_map.read(cx).is_buffer_folded(buffer) + } + + pub fn folded_buffers<'a>(&self, cx: &'a App) -> &'a HashSet { + self.display_map.read(cx).folded_buffers() } /// Removes any folds with the given ranges. @@ -11554,7 +12272,7 @@ impl Editor { ranges: &[Range], type_id: TypeId, auto_scroll: bool, - cx: &mut ViewContext, + cx: &mut Context, ) { self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { map.remove_folds_with_type(ranges.iter().cloned(), type_id, cx) @@ -11565,8 +12283,8 @@ impl Editor { &mut self, ranges: &[Range], auto_scroll: bool, - cx: &mut ViewContext, - update: impl FnOnce(&mut DisplayMap, &mut ModelContext), + cx: &mut Context, + update: impl FnOnce(&mut DisplayMap, &mut Context), ) { if ranges.is_empty() { return; @@ -11586,20 +12304,126 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); } - for buffer_id in buffers_affected { - Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx); - } - cx.notify(); self.scrollbar_marker_state.dirty = true; self.active_indent_guides_state.dirty = true; } - pub fn default_fold_placeholder(&self, cx: &AppContext) -> FoldPlaceholder { + pub fn default_fold_placeholder(&self, cx: &App) -> FoldPlaceholder { self.display_map.read(cx).fold_placeholder.clone() } - pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext) { + pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { + self.buffer.update(cx, |buffer, cx| { + buffer.set_all_diff_hunks_expanded(cx); + }); + } + + pub fn expand_all_diff_hunks( + &mut self, + _: &ExpandAllHunkDiffs, + _window: &mut Window, + cx: &mut Context, + ) { + self.buffer.update(cx, |buffer, cx| { + buffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + } + + pub fn toggle_selected_diff_hunks( + &mut self, + _: &ToggleSelectedDiffHunks, + _window: &mut Window, + cx: &mut Context, + ) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.toggle_diff_hunks_in_ranges(ranges, cx); + } + + pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.buffer + .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx)) + } + + pub fn clear_expanded_diff_hunks(&mut self, cx: &mut Context) -> bool { + self.buffer.update(cx, |buffer, cx| { + let ranges = vec![Anchor::min()..Anchor::max()]; + if !buffer.all_diff_hunks_expanded() + && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) + { + buffer.collapse_diff_hunks(ranges, cx); + true + } else { + false + } + }) + } + + fn toggle_diff_hunks_in_ranges( + &mut self, + ranges: Vec>, + cx: &mut Context<'_, Editor>, + ) { + self.buffer.update(cx, |buffer, cx| { + if buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) { + buffer.collapse_diff_hunks(ranges, cx) + } else { + buffer.expand_diff_hunks(ranges, cx) + } + }) + } + + pub(crate) fn apply_all_diff_hunks( + &mut self, + _: &ApplyAllDiffHunks, + window: &mut Window, + cx: &mut Context, + ) { + let buffers = self.buffer.read(cx).all_buffers(); + for branch_buffer in buffers { + branch_buffer.update(cx, |branch_buffer, cx| { + branch_buffer.merge_into_base(Vec::new(), cx); + }); + } + + if let Some(project) = self.project.clone() { + self.save(true, project, window, cx).detach_and_log_err(cx); + } + } + + pub(crate) fn apply_selected_diff_hunks( + &mut self, + _: &ApplyDiffHunk, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.snapshot(window, cx); + let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx).into_iter()); + let mut ranges_by_buffer = HashMap::default(); + self.transact(window, cx, |editor, _window, cx| { + for hunk in hunks { + if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { + ranges_by_buffer + .entry(buffer.clone()) + .or_insert_with(Vec::new) + .push(hunk.buffer_range.to_offset(buffer.read(cx))); + } + } + + for (buffer, ranges) in ranges_by_buffer { + buffer.update(cx, |buffer, cx| { + buffer.merge_into_base(ranges, cx); + }); + } + }); + + if let Some(project) = self.project.clone() { + self.save(true, project, window, cx).detach_and_log_err(cx); + } + } + + pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut Context) { if hovered != self.gutter_hovered { self.gutter_hovered = hovered; cx.notify(); @@ -11610,7 +12434,7 @@ impl Editor { &mut self, blocks: impl IntoIterator>, autoscroll: Option, - cx: &mut ViewContext, + cx: &mut Context, ) -> Vec { let blocks = self .display_map @@ -11626,7 +12450,7 @@ impl Editor { &mut self, heights: HashMap, autoscroll: Option, - cx: &mut ViewContext, + cx: &mut Context, ) { self.display_map .update(cx, |display_map, cx| display_map.resize_blocks(heights, cx)); @@ -11640,7 +12464,7 @@ impl Editor { &mut self, renderers: HashMap, autoscroll: Option, - cx: &mut ViewContext, + cx: &mut Context, ) { self.display_map .update(cx, |display_map, _cx| display_map.replace_blocks(renderers)); @@ -11654,7 +12478,7 @@ impl Editor { &mut self, block_ids: HashSet, autoscroll: Option, - cx: &mut ViewContext, + cx: &mut Context, ) { self.display_map.update(cx, |display_map, cx| { display_map.remove_blocks(block_ids, cx) @@ -11668,7 +12492,7 @@ impl Editor { pub fn row_for_block( &self, block_id: CustomBlockId, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option { self.display_map .update(cx, |map, cx| map.row_for_block(block_id, cx)) @@ -11685,7 +12509,7 @@ impl Editor { pub fn insert_creases( &mut self, creases: impl IntoIterator>, - cx: &mut ViewContext, + cx: &mut Context, ) -> Vec { self.display_map .update(cx, |map, cx| map.insert_creases(creases, cx)) @@ -11694,29 +12518,29 @@ impl Editor { pub fn remove_creases( &mut self, ids: impl IntoIterator, - cx: &mut ViewContext, + cx: &mut Context, ) { self.display_map .update(cx, |map, cx| map.remove_creases(ids, cx)); } - pub fn longest_row(&self, cx: &mut AppContext) -> DisplayRow { + pub fn longest_row(&self, cx: &mut App) -> DisplayRow { self.display_map .update(cx, |map, cx| map.snapshot(cx)) .longest_row() } - pub fn max_point(&self, cx: &mut AppContext) -> DisplayPoint { + pub fn max_point(&self, cx: &mut App) -> DisplayPoint { self.display_map .update(cx, |map, cx| map.snapshot(cx)) .max_point() } - pub fn text(&self, cx: &AppContext) -> String { + pub fn text(&self, cx: &App) -> String { self.buffer.read(cx).read(cx).text() } - pub fn text_option(&self, cx: &AppContext) -> Option { + pub fn text_option(&self, cx: &App) -> Option { let text = self.text(cx); let text = text.trim(); @@ -11727,8 +12551,13 @@ impl Editor { Some(text.to_string()) } - pub fn set_text(&mut self, text: impl Into>, cx: &mut ViewContext) { - self.transact(cx, |this, cx| { + pub fn set_text( + &mut self, + text: impl Into>, + window: &mut Window, + cx: &mut Context, + ) { + self.transact(window, cx, |this, _, cx| { this.buffer .read(cx) .as_singleton() @@ -11737,13 +12566,13 @@ impl Editor { }); } - pub fn display_text(&self, cx: &mut AppContext) -> String { + pub fn display_text(&self, cx: &mut App) -> String { self.display_map .update(cx, |map, cx| map.snapshot(cx)) .text() } - pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { + pub fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> { let mut wrap_guides = smallvec::smallvec![]; if self.show_wrap_guides == Some(false) { @@ -11763,7 +12592,7 @@ impl Editor { wrap_guides } - pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { + pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap { let settings = self.buffer.read(cx).settings_at(0, cx); let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap); match mode { @@ -11783,7 +12612,8 @@ impl Editor { pub fn set_soft_wrap_mode( &mut self, mode: language_settings::SoftWrap, - cx: &mut ViewContext, + + cx: &mut Context, ) { self.soft_wrap_mode_override = Some(mode); cx.notify(); @@ -11794,8 +12624,13 @@ impl Editor { } /// called by the Element so we know what style we were most recently rendered with. - pub(crate) fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext) { - let rem_size = cx.rem_size(); + pub(crate) fn set_style( + &mut self, + style: EditorStyle, + window: &mut Window, + cx: &mut Context, + ) { + let rem_size = window.rem_size(); self.display_map.update(cx, |map, cx| { map.set_font( style.text.font(), @@ -11812,12 +12647,12 @@ impl Editor { // Called by the element. This method is not designed to be called outside of the editor // element's layout code because it does not notify when rewrapping is computed synchronously. - pub(crate) fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { + pub(crate) fn set_wrap_width(&self, width: Option, cx: &mut App) -> bool { self.display_map .update(cx, |map, cx| map.set_wrap_width(width, cx)) } - pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext) { + pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, _: &mut Window, cx: &mut Context) { if self.soft_wrap_mode_override.is_some() { self.soft_wrap_mode_override.take(); } else { @@ -11833,7 +12668,7 @@ impl Editor { cx.notify(); } - pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, cx: &mut ViewContext) { + pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, _: &mut Window, cx: &mut Context) { let Some(workspace) = self.workspace() else { return; }; @@ -11844,7 +12679,12 @@ impl Editor { }); } - pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext) { + pub fn toggle_indent_guides( + &mut self, + _: &ToggleIndentGuides, + _: &mut Window, + cx: &mut Context, + ) { let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| { self.buffer .read(cx) @@ -11860,13 +12700,18 @@ impl Editor { self.show_indent_guides } - pub fn toggle_line_numbers(&mut self, _: &ToggleLineNumbers, cx: &mut ViewContext) { + pub fn toggle_line_numbers( + &mut self, + _: &ToggleLineNumbers, + _: &mut Window, + cx: &mut Context, + ) { let mut editor_settings = EditorSettings::get_global(cx).clone(); editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers; EditorSettings::override_global(editor_settings, cx); } - pub fn should_use_relative_line_numbers(&self, cx: &WindowContext) -> bool { + pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool { self.use_relative_line_numbers .unwrap_or(EditorSettings::get_global(cx).relative_line_numbers) } @@ -11874,73 +12719,66 @@ impl Editor { pub fn toggle_relative_line_numbers( &mut self, _: &ToggleRelativeLineNumbers, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) { let is_relative = self.should_use_relative_line_numbers(cx); self.set_relative_line_number(Some(!is_relative), cx) } - pub fn set_relative_line_number( - &mut self, - is_relative: Option, - cx: &mut ViewContext, - ) { + pub fn set_relative_line_number(&mut self, is_relative: Option, cx: &mut Context) { self.use_relative_line_numbers = is_relative; cx.notify(); } - pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext) { + pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut Context) { self.show_gutter = show_gutter; cx.notify(); } - pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut ViewContext) { + pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut Context) { self.show_scrollbars = show_scrollbars; cx.notify(); } - pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext) { + pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context) { self.show_line_numbers = Some(show_line_numbers); cx.notify(); } - pub fn set_show_git_diff_gutter( - &mut self, - show_git_diff_gutter: bool, - cx: &mut ViewContext, - ) { + pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context) { self.show_git_diff_gutter = Some(show_git_diff_gutter); cx.notify(); } - pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut ViewContext) { + pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut Context) { self.show_code_actions = Some(show_code_actions); cx.notify(); } - pub fn set_show_runnables(&mut self, show_runnables: bool, cx: &mut ViewContext) { + pub fn set_show_runnables(&mut self, show_runnables: bool, cx: &mut Context) { self.show_runnables = Some(show_runnables); cx.notify(); } - pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext) { + pub fn set_masked(&mut self, masked: bool, cx: &mut Context) { if self.display_map.read(cx).masked != masked { self.display_map.update(cx, |map, _| map.masked = masked); } cx.notify() } - pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext) { + pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut Context) { self.show_wrap_guides = Some(show_wrap_guides); cx.notify(); } - pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut ViewContext) { + pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut Context) { self.show_indent_guides = Some(show_indent_guides); cx.notify(); } - pub fn working_directory(&self, cx: &WindowContext) -> Option { + pub fn working_directory(&self, cx: &App) -> Option { if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { if let Some(dir) = file.abs_path(cx).parent() { @@ -11956,7 +12794,7 @@ impl Editor { None } - fn target_file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn language::LocalFile> { + fn target_file<'a>(&self, cx: &'a App) -> Option<&'a dyn language::LocalFile> { self.active_excerpt(cx)? .1 .read(cx) @@ -11964,7 +12802,7 @@ impl Editor { .and_then(|f| f.as_local()) } - fn target_file_abs_path(&self, cx: &mut ViewContext) -> Option { + fn target_file_abs_path(&self, cx: &mut Context) -> Option { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; let project = self.project.as_ref()?.read(cx); @@ -11972,7 +12810,7 @@ impl Editor { }) } - fn target_file_path(&self, cx: &mut ViewContext) -> Option { + fn target_file_path(&self, cx: &mut Context) -> Option { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; let project = self.project.as_ref()?.read(cx); @@ -11982,13 +12820,18 @@ impl Editor { }) } - pub fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext) { + pub fn reveal_in_finder( + &mut self, + _: &RevealInFileManager, + _window: &mut Window, + cx: &mut Context, + ) { if let Some(target) = self.target_file(cx) { cx.reveal_path(&target.abs_path(cx)); } } - pub fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { + pub fn copy_path(&mut self, _: &CopyPath, _window: &mut Window, cx: &mut Context) { if let Some(path) = self.target_file_abs_path(cx) { if let Some(path) = path.to_str() { cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); @@ -11996,7 +12839,12 @@ impl Editor { } } - pub fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { + pub fn copy_relative_path( + &mut self, + _: &CopyRelativePath, + _window: &mut Window, + cx: &mut Context, + ) { if let Some(path) = self.target_file_path(cx) { if let Some(path) = path.to_str() { cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); @@ -12004,7 +12852,7 @@ impl Editor { } } - pub fn project_path(&self, cx: &mut ViewContext) -> Option { + pub fn project_path(&self, cx: &mut Context) -> Option { if let Some(buffer) = self.buffer.read(cx).as_singleton() { buffer.read_with(cx, |buffer, cx| buffer.project_path(cx)) } else { @@ -12012,7 +12860,7 @@ impl Editor { } } - pub fn go_to_active_debug_line(&mut self, cx: &mut ViewContext) { + pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context) { let Some(dap_store) = self.dap_store.as_ref() else { return; }; @@ -12026,6 +12874,7 @@ impl Editor { self.go_to_line::( position, Some(cx.theme().colors().editor_debugger_active_line_background), + window, cx, ); @@ -12037,11 +12886,16 @@ impl Editor { cx.notify(); } - pub fn toggle_git_blame(&mut self, _: &ToggleGitBlame, cx: &mut ViewContext) { + pub fn toggle_git_blame( + &mut self, + _: &ToggleGitBlame, + window: &mut Window, + cx: &mut Context, + ) { self.show_git_blame_gutter = !self.show_git_blame_gutter; if self.show_git_blame_gutter && !self.has_blame_entries(cx) { - self.start_git_blame(true, cx); + self.start_git_blame(true, window, cx); } cx.notify(); @@ -12050,9 +12904,10 @@ impl Editor { pub fn toggle_git_blame_inline( &mut self, _: &ToggleGitBlameInline, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { - self.toggle_git_blame_inline_internal(true, cx); + self.toggle_git_blame_inline_internal(true, window, cx); cx.notify(); } @@ -12060,7 +12915,12 @@ impl Editor { self.git_blame_inline_enabled } - pub fn toggle_selection_menu(&mut self, _: &ToggleSelectionMenu, cx: &mut ViewContext) { + pub fn toggle_selection_menu( + &mut self, + _: &ToggleSelectionMenu, + _: &mut Window, + cx: &mut Context, + ) { self.show_selection_menu = self .show_selection_menu .map(|show_selections_menu| !show_selections_menu) @@ -12069,12 +12929,17 @@ impl Editor { cx.notify(); } - pub fn selection_menu_enabled(&self, cx: &AppContext) -> bool { + pub fn selection_menu_enabled(&self, cx: &App) -> bool { self.show_selection_menu .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu) } - fn start_git_blame(&mut self, user_triggered: bool, cx: &mut ViewContext) { + fn start_git_blame( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { if let Some(project) = self.project.as_ref() { let Some(buffer) = self.buffer().read(cx).as_singleton() else { return; @@ -12084,12 +12949,12 @@ impl Editor { return; } - let focused = self.focus_handle(cx).contains_focused(cx); + let focused = self.focus_handle(cx).contains_focused(window, cx); let project = project.clone(); - let blame = - cx.new_model(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx)); - self.blame_subscription = Some(cx.observe(&blame, |_, _, cx| cx.notify())); + let blame = cx.new(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx)); + self.blame_subscription = + Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify())); self.blame = Some(blame); } } @@ -12097,7 +12962,8 @@ impl Editor { fn toggle_git_blame_inline_internal( &mut self, user_triggered: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if self.git_blame_inline_enabled { self.git_blame_inline_enabled = false; @@ -12105,27 +12971,32 @@ impl Editor { self.show_git_blame_inline_delay_task.take(); } else { self.git_blame_inline_enabled = true; - self.start_git_blame_inline(user_triggered, cx); + self.start_git_blame_inline(user_triggered, window, cx); } cx.notify(); } - fn start_git_blame_inline(&mut self, user_triggered: bool, cx: &mut ViewContext) { - self.start_git_blame(user_triggered, cx); + fn start_git_blame_inline( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.start_git_blame(user_triggered, window, cx); if ProjectSettings::get_global(cx) .git .inline_blame_delay() .is_some() { - self.start_inline_blame_timer(cx); + self.start_inline_blame_timer(window, cx); } else { self.show_git_blame_inline = true } } - pub fn blame(&self) -> Option<&Model> { + pub fn blame(&self) -> Option<&Entity> { self.blame.as_ref() } @@ -12133,23 +13004,23 @@ impl Editor { self.show_git_blame_gutter } - pub fn render_git_blame_gutter(&mut self, cx: &mut WindowContext) -> bool { + pub fn render_git_blame_gutter(&self, cx: &App) -> bool { self.show_git_blame_gutter && self.has_blame_entries(cx) } - pub fn render_git_blame_inline(&mut self, cx: &mut WindowContext) -> bool { + pub fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool { self.show_git_blame_inline - && self.focus_handle.is_focused(cx) + && self.focus_handle.is_focused(window) && !self.newest_selection_head_on_empty_line(cx) && self.has_blame_entries(cx) } - fn has_blame_entries(&self, cx: &mut WindowContext) -> bool { + fn has_blame_entries(&self, cx: &App) -> bool { self.blame() .map_or(false, |blame| blame.read(cx).has_generated_entries()) } - fn newest_selection_head_on_empty_line(&mut self, cx: &mut WindowContext) -> bool { + fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { let cursor_anchor = self.selections.newest_anchor().head(); let snapshot = self.buffer.read(cx).snapshot(cx); @@ -12158,34 +13029,27 @@ impl Editor { snapshot.line_len(buffer_row) == 0 } - fn get_permalink_to_line(&mut self, cx: &mut ViewContext) -> Task> { + fn get_permalink_to_line(&self, cx: &mut Context) -> Task> { let buffer_and_selection = maybe!({ let selection = self.selections.newest::(cx); let selection_range = selection.range(); - let (buffer, selection) = if let Some(buffer) = self.buffer().read(cx).as_singleton() { - (buffer, selection_range.start.row..selection_range.end.row) - } else { - let multi_buffer = self.buffer().read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range); - - let (excerpt, range) = if selection.reversed { - buffer_ranges.first() - } else { - buffer_ranges.last() - }?; + let multi_buffer = self.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range); - let snapshot = excerpt.buffer(); - let selection = text::ToPoint::to_point(&range.start, &snapshot).row - ..text::ToPoint::to_point(&range.end, &snapshot).row; - ( - multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone(), - selection, - ) - }; + let (buffer, range, _) = if selection.reversed { + buffer_ranges.first() + } else { + buffer_ranges.last() + }?; - Some((buffer, selection)) + let selection = text::ToPoint::to_point(&range.start, &buffer).row + ..text::ToPoint::to_point(&range.end, &buffer).row; + Some(( + multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), + selection, + )) }); let Some((buffer, selection)) = buffer_and_selection else { @@ -12201,14 +13065,19 @@ impl Editor { }) } - pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext) { + pub fn copy_permalink_to_line( + &mut self, + _: &CopyPermalinkToLine, + window: &mut Window, + cx: &mut Context, + ) { let permalink_task = self.get_permalink_to_line(cx); let workspace = self.workspace(); - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { match permalink_task.await { Ok(permalink) => { - cx.update(|cx| { + cx.update(|_, cx| { cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); }) .ok(); @@ -12220,7 +13089,7 @@ impl Editor { if let Some(workspace) = workspace { workspace - .update(&mut cx, |workspace, cx| { + .update_in(&mut cx, |workspace, _, cx| { struct CopyPermalinkToLine; workspace.show_toast( @@ -12239,7 +13108,12 @@ impl Editor { .detach(); } - pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext) { + pub fn copy_file_location( + &mut self, + _: &CopyFileLocation, + _: &mut Window, + cx: &mut Context, + ) { let selection = self.selections.newest::(cx).start.row + 1; if let Some(file) = self.target_file(cx) { if let Some(path) = file.path().to_str() { @@ -12248,14 +13122,19 @@ impl Editor { } } - pub fn open_permalink_to_line(&mut self, _: &OpenPermalinkToLine, cx: &mut ViewContext) { + pub fn open_permalink_to_line( + &mut self, + _: &OpenPermalinkToLine, + window: &mut Window, + cx: &mut Context, + ) { let permalink_task = self.get_permalink_to_line(cx); let workspace = self.workspace(); - cx.spawn(|_, mut cx| async move { + cx.spawn_in(window, |_, mut cx| async move { match permalink_task.await { Ok(permalink) => { - cx.update(|cx| { + cx.update(|_, cx| { cx.open_url(permalink.as_ref()); }) .ok(); @@ -12286,16 +13165,26 @@ impl Editor { .detach(); } - pub fn insert_uuid_v4(&mut self, _: &InsertUuidV4, cx: &mut ViewContext) { - self.insert_uuid(UuidVersion::V4, cx); + pub fn insert_uuid_v4( + &mut self, + _: &InsertUuidV4, + window: &mut Window, + cx: &mut Context, + ) { + self.insert_uuid(UuidVersion::V4, window, cx); } - pub fn insert_uuid_v7(&mut self, _: &InsertUuidV7, cx: &mut ViewContext) { - self.insert_uuid(UuidVersion::V7, cx); + pub fn insert_uuid_v7( + &mut self, + _: &InsertUuidV7, + window: &mut Window, + cx: &mut Context, + ) { + self.insert_uuid(UuidVersion::V7, window, cx); } - fn insert_uuid(&mut self, version: UuidVersion, cx: &mut ViewContext) { - self.transact(cx, |this, cx| { + fn insert_uuid(&mut self, version: UuidVersion, window: &mut Window, cx: &mut Context) { + self.transact(window, cx, |this, window, cx| { let edits = this .selections .all::(cx) @@ -12309,10 +13198,54 @@ impl Editor { (selection.range(), uuid.to_string()) }); this.edit(edits, cx); - this.refresh_inline_completion(true, false, cx); + this.refresh_inline_completion(true, false, window, cx); }); } + pub fn open_selections_in_multibuffer( + &mut self, + _: &OpenSelectionsInMultibuffer, + window: &mut Window, + cx: &mut Context, + ) { + let multibuffer = self.buffer.read(cx); + + let Some(buffer) = multibuffer.as_singleton() else { + return; + }; + + let Some(workspace) = self.workspace() else { + return; + }; + + let locations = self + .selections + .disjoint_anchors() + .iter() + .map(|range| Location { + buffer: buffer.clone(), + range: range.start.text_anchor..range.end.text_anchor, + }) + .collect::>(); + + let title = multibuffer.title(cx).to_string(); + + cx.spawn_in(window, |_, mut cx| async move { + workspace.update_in(&mut cx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + format!("Selections for '{title}'"), + false, + MultibufferSelectionMode::All, + window, + cx, + ); + }) + }) + .detach(); + } + /// Adds a row highlight for the given range. If a row has multiple highlights, the /// last highlight added will be used. /// @@ -12322,7 +13255,7 @@ impl Editor { range: Range, color: Hsla, should_autoscroll: bool, - cx: &mut ViewContext, + cx: &mut Context, ) { let snapshot = self.buffer().read(cx).snapshot(cx); let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); @@ -12399,7 +13332,7 @@ impl Editor { pub fn remove_highlighted_rows( &mut self, ranges_to_remove: Vec>, - cx: &mut ViewContext, + cx: &mut Context, ) { let snapshot = self.buffer().read(cx).snapshot(cx); let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); @@ -12443,10 +13376,11 @@ impl Editor { /// Returns a map of display rows that are highlighted and their corresponding highlight color. /// Allows to ignore certain kinds of highlights. pub fn highlighted_display_rows( - &mut self, - cx: &mut WindowContext, + &self, + window: &mut Window, + cx: &mut App, ) -> BTreeMap { - let snapshot = self.snapshot(cx); + let snapshot = self.snapshot(window, cx); let mut used_highlight_orders = HashMap::default(); self.highlighted_rows .iter() @@ -12494,11 +13428,7 @@ impl Editor { .min() } - pub fn set_search_within_ranges( - &mut self, - ranges: &[Range], - cx: &mut ViewContext, - ) { + pub fn set_search_within_ranges(&mut self, ranges: &[Range], cx: &mut Context) { self.highlight_background::( ranges, |colors| colors.editor_document_highlight_read_background, @@ -12510,7 +13440,7 @@ impl Editor { self.breadcrumb_header = Some(new_header); } - pub fn clear_search_within_ranges(&mut self, cx: &mut ViewContext) { + pub fn clear_search_within_ranges(&mut self, cx: &mut Context) { self.clear_background_highlights::(cx); } @@ -12518,7 +13448,7 @@ impl Editor { &mut self, ranges: &[Range], color_fetcher: fn(&ThemeColors) -> Hsla, - cx: &mut ViewContext, + cx: &mut Context, ) { self.background_highlights .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); @@ -12528,7 +13458,7 @@ impl Editor { pub fn clear_background_highlights( &mut self, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option { let text_highlights = self.background_highlights.remove(&TypeId::of::())?; if !text_highlights.1.is_empty() { @@ -12541,8 +13471,8 @@ impl Editor { pub fn highlight_gutter( &mut self, ranges: &[Range], - color_fetcher: fn(&AppContext) -> Hsla, - cx: &mut ViewContext, + color_fetcher: fn(&App) -> Hsla, + cx: &mut Context, ) { self.gutter_highlights .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); @@ -12551,7 +13481,7 @@ impl Editor { pub fn clear_gutter_highlights( &mut self, - cx: &mut ViewContext, + cx: &mut Context, ) -> Option { cx.notify(); self.gutter_highlights.remove(&TypeId::of::()) @@ -12559,10 +13489,11 @@ impl Editor { #[cfg(feature = "test-support")] pub fn all_text_background_highlights( - &mut self, - cx: &mut ViewContext, + &self, + window: &mut Window, + cx: &mut Context, ) -> Vec<(Range, Hsla)> { - let snapshot = self.snapshot(cx); + let snapshot = self.snapshot(window, cx); let buffer = &snapshot.buffer_snapshot; let start = buffer.anchor_before(0); let end = buffer.anchor_after(buffer.len()); @@ -12571,10 +13502,7 @@ impl Editor { } #[cfg(feature = "test-support")] - pub fn search_background_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Vec> { + pub fn search_background_highlights(&mut self, cx: &mut Context) -> Vec> { let snapshot = self.buffer().read(cx).snapshot(cx); let highlights = self @@ -12750,7 +13678,7 @@ impl Editor { &self, search_range: Range, display_snapshot: &DisplaySnapshot, - cx: &AppContext, + cx: &App, ) -> Vec<(Range, Hsla)> { let mut results = Vec::new(); for (color_fetcher, ranges) in self.gutter_highlights.values() { @@ -12789,7 +13717,7 @@ impl Editor { &self, search_range: Range, display_snapshot: &DisplaySnapshot, - cx: &WindowContext, + cx: &App, ) -> Vec> { display_snapshot .buffer_snapshot @@ -12819,7 +13747,7 @@ impl Editor { &mut self, ranges: Vec>, style: HighlightStyle, - cx: &mut ViewContext, + cx: &mut Context, ) { self.display_map.update(cx, |map, _| { map.highlight_text(TypeId::of::(), ranges, style) @@ -12831,7 +13759,7 @@ impl Editor { &mut self, highlights: Vec, style: HighlightStyle, - cx: &mut ViewContext, + cx: &mut Context, ) { self.display_map.update(cx, |map, _| { map.highlight_inlays(TypeId::of::(), highlights, style) @@ -12841,12 +13769,12 @@ impl Editor { pub fn text_highlights<'a, T: 'static>( &'a self, - cx: &'a AppContext, + cx: &'a App, ) -> Option<(HighlightStyle, &'a [Range])> { self.display_map.read(cx).text_highlights(TypeId::of::()) } - pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + pub fn clear_highlights(&mut self, cx: &mut Context) { let cleared = self .display_map .update(cx, |map, _| map.clear_highlights(TypeId::of::())); @@ -12855,31 +13783,32 @@ impl Editor { } } - pub fn show_local_cursors(&self, cx: &WindowContext) -> bool { + pub fn show_local_cursors(&self, window: &mut Window, cx: &mut App) -> bool { (self.read_only(cx) || self.blink_manager.read(cx).visible()) - && self.focus_handle.is_focused(cx) + && self.focus_handle.is_focused(window) } - pub fn set_show_cursor_when_unfocused(&mut self, is_enabled: bool, cx: &mut ViewContext) { + pub fn set_show_cursor_when_unfocused(&mut self, is_enabled: bool, cx: &mut Context) { self.show_cursor_when_unfocused = is_enabled; cx.notify(); } - pub fn lsp_store(&self, cx: &AppContext) -> Option> { + pub fn lsp_store(&self, cx: &App) -> Option> { self.project .as_ref() .map(|project| project.read(cx).lsp_store()) } - fn on_buffer_changed(&mut self, _: Model, cx: &mut ViewContext) { + fn on_buffer_changed(&mut self, _: Entity, cx: &mut Context) { cx.notify(); } fn on_buffer_event( &mut self, - multibuffer: Model, + multibuffer: &Entity, event: &multi_buffer::Event, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { match event { multi_buffer::Event::Edited { @@ -12889,9 +13818,9 @@ impl Editor { self.scrollbar_marker_state.dirty = true; self.active_indent_guides_state.dirty = true; self.refresh_active_diagnostics(cx); - self.refresh_code_actions(cx); + self.refresh_code_actions(window, cx); if self.has_active_inline_completion() { - self.update_visible_inline_completion(cx); + self.update_visible_inline_completion(window, cx); } if let Some(buffer) = buffer_edited { let buffer_id = buffer.read(cx).remote_id(); @@ -12948,7 +13877,7 @@ impl Editor { let is_via_ssh = project.is_via_ssh(); (telemetry, is_via_ssh) }; - refresh_linked_ranges(self, cx); + refresh_linked_ranges(self, window, cx); telemetry.log_edit_event("editor", is_via_ssh); } multi_buffer::Event::ExcerptsAdded { @@ -12956,11 +13885,16 @@ impl Editor { predecessor, excerpts, } => { - self.tasks_update_task = Some(self.refresh_runnables(cx)); + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); let buffer_id = buffer.read(cx).remote_id(); - if !self.diff_map.diff_bases.contains_key(&buffer_id) { + if self.buffer.read(cx).change_set_for(buffer_id).is_none() { if let Some(project) = &self.project { - get_unstaged_changes_for_buffers(project, [buffer.clone()], cx); + get_unstaged_changes_for_buffers( + project, + [buffer.clone()], + self.buffer.clone(), + cx, + ); } } cx.emit(EditorEvent::ExcerptsAdded { @@ -12985,12 +13919,15 @@ impl Editor { cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() }) } multi_buffer::Event::Reparsed(buffer_id) => { - self.tasks_update_task = Some(self.refresh_runnables(cx)); + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); cx.emit(EditorEvent::Reparsed(*buffer_id)); } + multi_buffer::Event::DiffHunksToggled => { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + } multi_buffer::Event::LanguageChanged(buffer_id) => { - linked_editing_ranges::refresh_linked_ranges(self, cx); + linked_editing_ranges::refresh_linked_ranges(self, window, cx); cx.emit(EditorEvent::Reparsed(*buffer_id)); cx.notify(); } @@ -13026,13 +13963,18 @@ impl Editor { }; } - fn on_display_map_changed(&mut self, _: Model, cx: &mut ViewContext) { + fn on_display_map_changed( + &mut self, + _: Entity, + _: &mut Window, + cx: &mut Context, + ) { cx.notify(); } - fn settings_changed(&mut self, cx: &mut ViewContext) { - self.tasks_update_task = Some(self.refresh_runnables(cx)); - self.refresh_inline_completion(true, false, cx); + fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + self.refresh_inline_completion(true, false, window, cx); self.refresh_inlay_hints( InlayHintRefreshReason::SettingsChange(inlay_hint_settings( self.selections.newest_anchor().head(), @@ -13061,7 +14003,7 @@ impl Editor { if self.mode == EditorMode::Full { let inline_blame_enabled = project_settings.git.inline_blame_enabled(); if self.git_blame_inline_enabled != inline_blame_enabled { - self.toggle_git_blame_inline_internal(false, cx); + self.toggle_git_blame_inline_internal(false, window, cx); } } @@ -13079,7 +14021,8 @@ impl Editor { fn open_proposed_changes_editor( &mut self, _: &OpenProposedChangesEditor, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let Some(workspace) = self.workspace() else { cx.propagate(); @@ -13091,14 +14034,14 @@ impl Editor { let multi_buffer_snapshot = multi_buffer.snapshot(cx); let mut new_selections_by_buffer = HashMap::default(); for selection in selections { - for (excerpt, range) in + for (buffer, range, _) in multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end) { - let mut range = range.to_point(excerpt.buffer()); + let mut range = range.to_point(buffer); range.start.column = 0; - range.end.column = excerpt.buffer().line_len(range.end.row); + range.end.column = buffer.line_len(range.end.row); new_selections_by_buffer - .entry(multi_buffer.buffer(excerpt.buffer_id()).unwrap()) + .entry(multi_buffer.buffer(buffer.remote_id()).unwrap()) .or_insert(Vec::new()) .push(range) } @@ -13108,37 +14051,51 @@ impl Editor { .into_iter() .map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges }) .collect::>(); - let proposed_changes_editor = cx.new_view(|cx| { + let proposed_changes_editor = cx.new(|cx| { ProposedChangesEditor::new( "Proposed changes", proposed_changes_buffers, self.project.clone(), + window, cx, ) }); - cx.window_context().defer(move |cx| { + window.defer(cx, move |window, cx| { workspace.update(cx, |workspace, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(proposed_changes_editor), true, true, None, cx); + pane.add_item( + Box::new(proposed_changes_editor), + true, + true, + None, + window, + cx, + ); }); }); }); } - pub fn open_excerpts_in_split(&mut self, _: &OpenExcerptsSplit, cx: &mut ViewContext) { - self.open_excerpts_common(None, true, cx) + pub fn open_excerpts_in_split( + &mut self, + _: &OpenExcerptsSplit, + window: &mut Window, + cx: &mut Context, + ) { + self.open_excerpts_common(None, true, window, cx) } - pub fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { - self.open_excerpts_common(None, false, cx) + pub fn open_excerpts(&mut self, _: &OpenExcerpts, window: &mut Window, cx: &mut Context) { + self.open_excerpts_common(None, false, window, cx) } fn open_excerpts_common( &mut self, jump_data: Option, split: bool, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { let Some(workspace) = self.workspace() else { cx.propagate(); @@ -13199,13 +14156,13 @@ impl Editor { let selections = self.selections.all::(cx); let multi_buffer = self.buffer.read(cx); for selection in selections { - for (excerpt, mut range) in multi_buffer + for (buffer, mut range, _) in multi_buffer .snapshot(cx) .range_to_buffer_ranges(selection.range()) { // When editing branch buffers, jump to the corresponding location // in their base buffer. - let mut buffer_handle = multi_buffer.buffer(excerpt.buffer_id()).unwrap(); + let mut buffer_handle = multi_buffer.buffer(buffer.remote_id()).unwrap(); let buffer = buffer_handle.read(cx); if let Some(base_buffer) = buffer.base_buffer() { range = buffer.range_to_version(range, &base_buffer.read(cx).version()); @@ -13232,10 +14189,10 @@ impl Editor { // We defer the pane interaction because we ourselves are a workspace item // and activating a new item causes the pane to call a method on us reentrantly, // which panics if we're on the stack. - cx.window_context().defer(move |cx| { + window.defer(cx, move |window, cx| { workspace.update(cx, |workspace, cx| { let pane = if split { - workspace.adjacent_pane(cx) + workspace.adjacent_pane(window, cx) } else { workspace.active_pane().clone() }; @@ -13261,7 +14218,7 @@ impl Editor { } })?; pane.update(cx, |pane, cx| { - pane.activate_item(pane_item_index, true, true, cx) + pane.activate_item(pane_item_index, true, true, window, cx) }); Some(editor) }) @@ -13272,6 +14229,7 @@ impl Editor { buffer, true, true, + window, cx, ) }); @@ -13282,7 +14240,7 @@ impl Editor { None => Autoscroll::newest(), }; let nav_history = editor.nav_history.take(); - editor.change_selections(Some(autoscroll), cx, |s| { + editor.change_selections(Some(autoscroll), window, cx, |s| { s.select_ranges(ranges); }); editor.nav_history = nav_history; @@ -13292,7 +14250,7 @@ impl Editor { }); } - fn marked_text_ranges(&self, cx: &AppContext) -> Option>> { + fn marked_text_ranges(&self, cx: &App) -> Option>> { let snapshot = self.buffer.read(cx).read(cx); let (_, ranges) = self.text_highlights::(cx)?; Some( @@ -13308,7 +14266,7 @@ impl Editor { fn selection_replacement_ranges( &self, range: Range, - cx: &mut AppContext, + cx: &mut App, ) -> Vec> { let selections = self.selections.all::(cx); let newest_selection = selections @@ -13334,7 +14292,7 @@ impl Editor { &self, event_type: &'static str, file_extension: Option, - cx: &AppContext, + cx: &App, ) { if cfg!(any(test, feature = "test-support")) { return; @@ -13381,20 +14339,21 @@ impl Editor { /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, /// with each line being an array of {text, highlight} objects. - fn copy_highlight_json(&mut self, _: &CopyHighlightJson, cx: &mut ViewContext) { - let Some(buffer) = self.buffer.read(cx).as_singleton() else { - return; - }; - + fn copy_highlight_json( + &mut self, + _: &CopyHighlightJson, + window: &mut Window, + cx: &mut Context, + ) { #[derive(Serialize)] struct Chunk<'a> { text: String, highlight: Option<&'a str>, } - let snapshot = buffer.read(cx).snapshot(); + let snapshot = self.buffer.read(cx).snapshot(cx); let range = self - .selected_text_range(false, cx) + .selected_text_range(false, window, cx) .and_then(|selection| { if selection.range.is_empty() { None @@ -13452,10 +14411,15 @@ impl Editor { cx.write_to_clipboard(ClipboardItem::new_string(lines)); } - pub fn open_context_menu(&mut self, _: &OpenContextMenu, cx: &mut ViewContext) { + pub fn open_context_menu( + &mut self, + _: &OpenContextMenu, + window: &mut Window, + cx: &mut Context, + ) { self.request_autoscroll(Autoscroll::newest(), cx); let position = self.selections.newest_display(cx).start; - mouse_context_menu::deploy_context_menu(self, None, position, cx); + mouse_context_menu::deploy_context_menu(self, None, position, window, cx); } pub fn inlay_hint_cache(&self) -> &InlayHintCache { @@ -13466,7 +14430,8 @@ impl Editor { &mut self, text: &str, relative_utf16_range: Option>, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if !self.input_enabled { cx.emit(EditorEvent::InputIgnored { text: text.into() }); @@ -13474,7 +14439,7 @@ impl Editor { } if let Some(relative_utf16_range) = relative_utf16_range { let selections = self.selections.all::(cx); - self.change_selections(None, cx, |s| { + self.change_selections(None, window, cx, |s| { let new_ranges = selections.into_iter().map(|range| { let start = OffsetUtf16( range @@ -13494,10 +14459,10 @@ impl Editor { }); } - self.handle_input(text, cx); + self.handle_input(text, window, cx); } - pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool { + pub fn supports_inlay_hints(&self, cx: &App) -> bool { let Some(provider) = self.semantics_provider.as_ref() else { return false; }; @@ -13508,16 +14473,11 @@ impl Editor { }); supports } - - pub fn focus(&self, cx: &mut WindowContext) { - cx.focus(&self.focus_handle) + pub fn is_focused(&self, window: &mut Window) -> bool { + self.focus_handle.is_focused(window) } - pub fn is_focused(&self, cx: &WindowContext) -> bool { - self.focus_handle.is_focused(cx) - } - - fn handle_focus(&mut self, cx: &mut ViewContext) { + fn handle_focus(&mut self, window: &mut Window, cx: &mut Context) { cx.emit(EditorEvent::Focused); if let Some(descendant) = self @@ -13525,14 +14485,14 @@ impl Editor { .take() .and_then(|descendant| descendant.upgrade()) { - cx.focus(&descendant); + window.focus(&descendant); } else { if let Some(blame) = self.blame.as_ref() { blame.update(cx, GitBlame::focus) } self.blink_manager.update(cx, BlinkManager::enable); - self.show_cursor_names(cx); + self.show_cursor_names(window, cx); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); if self.leader_peer_id.is_none() { @@ -13547,17 +14507,22 @@ impl Editor { } } - fn handle_focus_in(&mut self, cx: &mut ViewContext) { + fn handle_focus_in(&mut self, _: &mut Window, cx: &mut Context) { cx.emit(EditorEvent::FocusedIn) } - fn handle_focus_out(&mut self, event: FocusOutEvent, _cx: &mut ViewContext) { + fn handle_focus_out( + &mut self, + event: FocusOutEvent, + _window: &mut Window, + _cx: &mut Context, + ) { if event.blurred != self.focus_handle { self.last_focused_descendant = Some(event.blurred); } } - pub fn handle_blur(&mut self, cx: &mut ViewContext) { + pub fn handle_blur(&mut self, window: &mut Window, cx: &mut Context) { self.blink_manager.update(cx, BlinkManager::disable); self.buffer .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); @@ -13565,30 +14530,29 @@ impl Editor { if let Some(blame) = self.blame.as_ref() { blame.update(cx, GitBlame::blur) } - if !self.hover_state.focused(cx) { + if !self.hover_state.focused(window, cx) { hide_hover(self, cx); } - self.hide_context_menu(cx); + self.hide_context_menu(window, cx); cx.emit(EditorEvent::Blurred); cx.notify(); } pub fn register_action( &mut self, - listener: impl Fn(&A, &mut WindowContext) + 'static, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, ) -> Subscription { let id = self.next_editor_action_id.post_inc(); let listener = Arc::new(listener); self.editor_actions.borrow_mut().insert( id, - Box::new(move |cx| { - let cx = cx.window_context(); + Box::new(move |window, _| { let listener = listener.clone(); - cx.on_action(TypeId::of::(), move |action, phase, cx| { + window.on_action(TypeId::of::(), move |action, phase, window, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Bubble { - listener(action, cx) + listener(action, window, cx) } }) }), @@ -13607,7 +14571,8 @@ impl Editor { pub fn revert( &mut self, revert_changes: HashMap, Rope)>>, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { self.buffer().update(cx, |multi_buffer, cx| { for (buffer_id, changes) in revert_changes { @@ -13624,27 +14589,27 @@ impl Editor { } } }); - self.change_selections(None, cx, |selections| selections.refresh()); + self.change_selections(None, window, cx, |selections| selections.refresh()); } pub fn to_pixel_point( - &mut self, + &self, source: multi_buffer::Anchor, editor_snapshot: &EditorSnapshot, - cx: &mut ViewContext, + window: &mut Window, ) -> Option> { let source_point = source.to_display_point(editor_snapshot); - self.display_to_pixel_point(source_point, editor_snapshot, cx) + self.display_to_pixel_point(source_point, editor_snapshot, window) } pub fn display_to_pixel_point( &self, source: DisplayPoint, editor_snapshot: &EditorSnapshot, - cx: &WindowContext, + window: &mut Window, ) -> Option> { - let line_height = self.style()?.text.line_height_in_pixels(cx.rem_size()); - let text_layout_details = self.text_layout_details(cx); + let line_height = self.style()?.text.line_height_in_pixels(window.rem_size()); + let text_layout_details = self.text_layout_details(window); let scroll_top = text_layout_details .scroll_anchor .scroll_position(editor_snapshot) @@ -13680,22 +14645,14 @@ impl Editor { .and_then(|item| item.to_any().downcast_ref::()) } - pub fn add_change_set( - &mut self, - change_set: Model, - cx: &mut ViewContext, - ) { - self.diff_map.add_change_set(change_set, cx); - } - - fn character_size(&self, cx: &mut ViewContext) -> gpui::Point { - let text_layout_details = self.text_layout_details(cx); + fn character_size(&self, window: &mut Window) -> gpui::Point { + let text_layout_details = self.text_layout_details(window); let style = &text_layout_details.editor_style; - let font_id = cx.text_system().resolve_font(&style.text.font()); - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let font_id = window.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(window.rem_size()); + let line_height = style.text.line_height_in_pixels(window.rem_size()); - let em_width = cx + let em_width = window .text_system() .typographic_bounds(font_id, font_size, 'm') .unwrap() @@ -13707,9 +14664,10 @@ impl Editor { } fn get_unstaged_changes_for_buffers( - project: &Model, - buffers: impl IntoIterator>, - cx: &mut ViewContext, + project: &Entity, + buffers: impl IntoIterator>, + buffer: Entity, + cx: &mut App, ) { let mut tasks = Vec::new(); project.update(cx, |project, cx| { @@ -13717,16 +14675,17 @@ fn get_unstaged_changes_for_buffers( tasks.push(project.open_unstaged_changes(buffer.clone(), cx)) } }); - cx.spawn(|this, mut cx| async move { + cx.spawn(|mut cx| async move { let change_sets = futures::future::join_all(tasks).await; - this.update(&mut cx, |this, cx| { - for change_set in change_sets { - if let Some(change_set) = change_set.log_err() { - this.diff_map.add_change_set(change_set, cx); + buffer + .update(&mut cx, |buffer, cx| { + for change_set in change_sets { + if let Some(change_set) = change_set.log_err() { + buffer.add_change_set(change_set, cx); + } } - } - }) - .ok(); + }) + .ok(); }) .detach(); } @@ -14014,78 +14973,22 @@ fn test_wrap_with_prefix() { ); } -fn hunks_for_selections( - snapshot: &EditorSnapshot, - selections: &[Selection], -) -> Vec { - hunks_for_ranges( - selections.iter().map(|selection| selection.range()), - snapshot, - ) -} - -pub fn hunks_for_ranges( - ranges: impl Iterator>, - snapshot: &EditorSnapshot, -) -> Vec { - let mut hunks = Vec::new(); - let mut processed_buffer_rows: HashMap>> = - HashMap::default(); - for query_range in ranges { - let query_rows = - MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); - for hunk in snapshot.diff_map.diff_hunks_in_range( - Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), - &snapshot.buffer_snapshot, - ) { - // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it - // when the caret is just above or just below the deleted hunk. - let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed; - let related_to_selection = if allow_adjacent { - hunk.row_range.overlaps(&query_rows) - || hunk.row_range.start == query_rows.end - || hunk.row_range.end == query_rows.start - } else { - hunk.row_range.overlaps(&query_rows) - }; - if related_to_selection { - if !processed_buffer_rows - .entry(hunk.buffer_id) - .or_default() - .insert(hunk.buffer_range.start..hunk.buffer_range.end) - { - continue; - } - hunks.push(hunk); - } - } - } - - hunks -} - pub trait CollaborationHub { - fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; - fn user_participant_indices<'a>( - &self, - cx: &'a AppContext, - ) -> &'a HashMap; - fn user_names(&self, cx: &AppContext) -> HashMap; + fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap; + fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap; + fn user_names(&self, cx: &App) -> HashMap; } -impl CollaborationHub for Model { - fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { +impl CollaborationHub for Entity { + fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap { self.read(cx).collaborators() } - fn user_participant_indices<'a>( - &self, - cx: &'a AppContext, - ) -> &'a HashMap { + fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap { self.read(cx).user_store().read(cx).participant_indices() } - fn user_names(&self, cx: &AppContext) -> HashMap { + fn user_names(&self, cx: &App) -> HashMap { let this = self.read(cx); let user_ids = this.collaborators().values().map(|c| c.user_id); this.user_store().read_with(cx, |user_store, cx| { @@ -14097,94 +15000,95 @@ impl CollaborationHub for Model { pub trait SemanticsProvider { fn hover( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>; fn inlay_hints( &self, - buffer_handle: Model, + buffer_handle: Entity, range: Range, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>>; fn resolve_inlay_hint( &self, hint: InlayHint, - buffer_handle: Model, + buffer_handle: Entity, server_id: LanguageServerId, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>; - fn supports_inlay_hints(&self, buffer: &Model, cx: &AppContext) -> bool; + fn supports_inlay_hints(&self, buffer: &Entity, cx: &App) -> bool; fn document_highlights( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>>; fn definitions( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, kind: GotoDefinitionKind, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>>; fn range_for_rename( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>>>; fn perform_rename( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, new_name: String, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>; } pub trait CompletionProvider { fn completions( &self, - buffer: &Model, + buffer: &Entity, buffer_position: text::Anchor, trigger: CompletionContext, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Task>>; fn resolve_completions( &self, - buffer: Model, + buffer: Entity, completion_indices: Vec, completions: Rc>>, - cx: &mut ViewContext, + cx: &mut Context, ) -> Task>; fn apply_additional_edits_for_completion( &self, - _buffer: Model, + _buffer: Entity, _completions: Rc>>, _completion_index: usize, _push_to_history: bool, - _cx: &mut ViewContext, + _cx: &mut Context, ) -> Task>> { Task::ready(Ok(None)) } fn is_completion_trigger( &self, - buffer: &Model, + buffer: &Entity, position: language::Anchor, text: &str, trigger_in_words: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool; fn sort_completions(&self) -> bool { @@ -14197,31 +15101,34 @@ pub trait CodeActionProvider { fn code_actions( &self, - buffer: &Model, + buffer: &Entity, range: Range, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task>>; fn apply_code_action( &self, - buffer_handle: Model, + buffer_handle: Entity, action: CodeAction, excerpt_id: ExcerptId, push_to_history: bool, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Task>; } -impl CodeActionProvider for Model { +impl CodeActionProvider for Entity { fn id(&self) -> Arc { "project".into() } fn code_actions( &self, - buffer: &Model, + buffer: &Entity, range: Range, - cx: &mut WindowContext, + _window: &mut Window, + cx: &mut App, ) -> Task>> { self.update(cx, |project, cx| { project.code_actions(buffer, range, None, cx) @@ -14230,11 +15137,12 @@ impl CodeActionProvider for Model { fn apply_code_action( &self, - buffer_handle: Model, + buffer_handle: Entity, action: CodeAction, _excerpt_id: ExcerptId, push_to_history: bool, - cx: &mut WindowContext, + _window: &mut Window, + cx: &mut App, ) -> Task> { self.update(cx, |project, cx| { project.apply_code_action(buffer_handle, action, push_to_history, cx) @@ -14244,9 +15152,9 @@ impl CodeActionProvider for Model { fn snippet_completions( project: &Project, - buffer: &Model, + buffer: &Entity, buffer_position: text::Anchor, - cx: &mut AppContext, + cx: &mut App, ) -> Task>> { let language = buffer.read(cx).language_at(buffer_position); let language_name = language.as_ref().map(|language| language.lsp_id()); @@ -14379,13 +15287,14 @@ fn snippet_completions( }) } -impl CompletionProvider for Model { +impl CompletionProvider for Entity { fn completions( &self, - buffer: &Model, + buffer: &Entity, buffer_position: text::Anchor, options: CompletionContext, - cx: &mut ViewContext, + _window: &mut Window, + cx: &mut Context, ) -> Task>> { self.update(cx, |project, cx| { let snippets = snippet_completions(project, buffer, buffer_position, cx); @@ -14401,10 +15310,10 @@ impl CompletionProvider for Model { fn resolve_completions( &self, - buffer: Model, + buffer: Entity, completion_indices: Vec, completions: Rc>>, - cx: &mut ViewContext, + cx: &mut Context, ) -> Task> { self.update(cx, |project, cx| { project.lsp_store().update(cx, |lsp_store, cx| { @@ -14415,11 +15324,11 @@ impl CompletionProvider for Model { fn apply_additional_edits_for_completion( &self, - buffer: Model, + buffer: Entity, completions: Rc>>, completion_index: usize, push_to_history: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> Task>> { self.update(cx, |project, cx| { project.lsp_store().update(cx, |lsp_store, cx| { @@ -14436,11 +15345,11 @@ impl CompletionProvider for Model { fn is_completion_trigger( &self, - buffer: &Model, + buffer: &Entity, position: language::Anchor, text: &str, trigger_in_words: bool, - cx: &mut ViewContext, + cx: &mut Context, ) -> bool { let mut chars = text.chars(); let char = if let Some(char) = chars.next() { @@ -14466,21 +15375,21 @@ impl CompletionProvider for Model { } } -impl SemanticsProvider for Model { +impl SemanticsProvider for Entity { fn hover( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, - cx: &mut AppContext, + cx: &mut App, ) -> Option>> { Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) } fn document_highlights( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>> { Some(self.update(cx, |project, cx| { project.document_highlights(buffer, position, cx) @@ -14489,10 +15398,10 @@ impl SemanticsProvider for Model { fn definitions( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, kind: GotoDefinitionKind, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>> { Some(self.update(cx, |project, cx| match kind { GotoDefinitionKind::Symbol => project.definition(&buffer, position, cx), @@ -14502,7 +15411,7 @@ impl SemanticsProvider for Model { })) } - fn supports_inlay_hints(&self, buffer: &Model, cx: &AppContext) -> bool { + fn supports_inlay_hints(&self, buffer: &Entity, cx: &App) -> bool { // TODO: make this work for remote projects self.read(cx) .language_servers_for_local_buffer(buffer.read(cx), cx) @@ -14517,9 +15426,9 @@ impl SemanticsProvider for Model { fn inlay_hints( &self, - buffer_handle: Model, + buffer_handle: Entity, range: Range, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>> { Some(self.update(cx, |project, cx| { project.inlay_hints(buffer_handle, range, cx) @@ -14529,9 +15438,9 @@ impl SemanticsProvider for Model { fn resolve_inlay_hint( &self, hint: InlayHint, - buffer_handle: Model, + buffer_handle: Entity, server_id: LanguageServerId, - cx: &mut AppContext, + cx: &mut App, ) -> Option>> { Some(self.update(cx, |project, cx| { project.resolve_inlay_hint(hint, buffer_handle, server_id, cx) @@ -14540,9 +15449,9 @@ impl SemanticsProvider for Model { fn range_for_rename( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, - cx: &mut AppContext, + cx: &mut App, ) -> Option>>>> { Some(self.update(cx, |project, cx| { let buffer = buffer.clone(); @@ -14572,10 +15481,10 @@ impl SemanticsProvider for Model { fn perform_rename( &self, - buffer: &Model, + buffer: &Entity, position: text::Anchor, new_name: String, - cx: &mut AppContext, + cx: &mut App, ) -> Option>> { Some(self.update(cx, |project, cx| { project.perform_rename(buffer.clone(), position, new_name, cx) @@ -14586,7 +15495,7 @@ impl SemanticsProvider for Model { fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, - cx: &mut ViewContext, + cx: &mut Context, ) -> InlayHintSettings { let file = snapshot.file_at(location); let language = snapshot.language_at(location).map(|l| l.name()); @@ -14627,7 +15536,7 @@ impl EditorSnapshot { &'a self, range: &'a Range, collaboration_hub: &dyn CollaborationHub, - cx: &'a AppContext, + cx: &'a App, ) -> impl 'a + Iterator { let participant_names = collaboration_hub.user_names(cx); let participant_indices = collaboration_hub.user_participant_indices(cx); @@ -14654,6 +15563,45 @@ impl EditorSnapshot { }) } + pub fn hunks_for_ranges( + &self, + ranges: impl Iterator>, + ) -> Vec { + let mut hunks = Vec::new(); + let mut processed_buffer_rows: HashMap>> = + HashMap::default(); + for query_range in ranges { + let query_rows = + MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); + for hunk in self.buffer_snapshot.diff_hunks_in_range( + Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), + ) { + // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it + // when the caret is just above or just below the deleted hunk. + let allow_adjacent = hunk.status() == DiffHunkStatus::Removed; + let related_to_selection = if allow_adjacent { + hunk.row_range.overlaps(&query_rows) + || hunk.row_range.start == query_rows.end + || hunk.row_range.end == query_rows.start + } else { + hunk.row_range.overlaps(&query_rows) + }; + if related_to_selection { + if !processed_buffer_rows + .entry(hunk.buffer_id) + .or_default() + .insert(hunk.buffer_range.start..hunk.buffer_range.end) + { + continue; + } + hunks.push(hunk); + } + } + } + + hunks + } + pub fn language_at(&self, position: T) -> Option<&Arc> { self.display_snapshot.buffer_snapshot.language_at(position) } @@ -14677,7 +15625,7 @@ impl EditorSnapshot { em_width: Pixels, em_advance: Pixels, max_line_number_width: Pixels, - cx: &AppContext, + cx: &App, ) -> GutterDimensions { if !self.show_gutter { return GutterDimensions::default(); @@ -14756,8 +15704,9 @@ impl EditorSnapshot { &self, buffer_row: MultiBufferRow, row_contains_cursor: bool, - editor: View, - cx: &mut WindowContext, + editor: Entity, + window: &mut Window, + cx: &mut App, ) -> Option { let folded = self.is_line_folded(buffer_row); let mut is_foldable = false; @@ -14770,18 +15719,29 @@ impl EditorSnapshot { match crease { Crease::Inline { render_toggle, .. } | Crease::Block { render_toggle, .. } => { if let Some(render_toggle) = render_toggle { - let toggle_callback = Arc::new(move |folded, cx: &mut WindowContext| { - if folded { - editor.update(cx, |editor, cx| { - editor.fold_at(&crate::FoldAt { buffer_row }, cx) - }); - } else { - editor.update(cx, |editor, cx| { - editor.unfold_at(&crate::UnfoldAt { buffer_row }, cx) - }); - } - }); - return Some((render_toggle)(buffer_row, folded, toggle_callback, cx)); + let toggle_callback = + Arc::new(move |folded, window: &mut Window, cx: &mut App| { + if folded { + editor.update(cx, |editor, cx| { + editor.fold_at(&crate::FoldAt { buffer_row }, window, cx) + }); + } else { + editor.update(cx, |editor, cx| { + editor.unfold_at( + &crate::UnfoldAt { buffer_row }, + window, + cx, + ) + }); + } + }); + return Some((render_toggle)( + buffer_row, + folded, + toggle_callback, + window, + cx, + )); } } } @@ -14793,11 +15753,11 @@ impl EditorSnapshot { Some( Disclosure::new(("gutter_crease", buffer_row.0), !folded) .toggle_state(folded) - .on_click(cx.listener_for(&editor, move |this, _e, cx| { + .on_click(window.listener_for(&editor, move |this, _e, window, cx| { if folded { - this.unfold_at(&UnfoldAt { buffer_row }, cx); + this.unfold_at(&UnfoldAt { buffer_row }, window, cx); } else { - this.fold_at(&FoldAt { buffer_row }, cx); + this.fold_at(&FoldAt { buffer_row }, window, cx); } })) .into_any_element(), @@ -14810,7 +15770,8 @@ impl EditorSnapshot { pub fn render_crease_trailer( &self, buffer_row: MultiBufferRow, - cx: &mut WindowContext, + window: &mut Window, + cx: &mut App, ) -> Option { let folded = self.is_line_folded(buffer_row); if let Crease::Inline { render_trailer, .. } = self @@ -14818,7 +15779,7 @@ impl EditorSnapshot { .query_row(buffer_row, &self.buffer_snapshot)? { let render_trailer = render_trailer.as_ref()?; - Some(render_trailer(buffer_row, folded, cx)) + Some(render_trailer(buffer_row, folded, window, cx)) } else { None } @@ -14843,7 +15804,7 @@ pub enum EditorEvent { text: Arc, }, ExcerptsAdded { - buffer: Model, + buffer: Entity, predecessor: ExcerptId, excerpts: Vec<(ExcerptId, ExcerptRange)>, }, @@ -14892,14 +15853,14 @@ pub enum EditorEvent { impl EventEmitter for Editor {} -impl FocusableView for Editor { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { +impl Focusable for Editor { + fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for Editor { - fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement { + fn render<'a>(&mut self, _: &mut Window, cx: &mut Context<'a, Self>) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let mut text_style = match self.mode { @@ -14935,7 +15896,7 @@ impl Render for Editor { }; EditorElement::new( - cx.view(), + &cx.entity(), EditorStyle { background, local_player: cx.theme().players().local(), @@ -14951,12 +15912,13 @@ impl Render for Editor { } } -impl ViewInputHandler for Editor { +impl EntityInputHandler for Editor { fn text_for_range( &mut self, range_utf16: Range, adjusted_range: &mut Option>, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) -> Option { let snapshot = self.buffer.read(cx).read(cx); let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left); @@ -14970,7 +15932,8 @@ impl ViewInputHandler for Editor { fn selected_text_range( &mut self, ignore_disabled_input: bool, - cx: &mut ViewContext, + _: &mut Window, + cx: &mut Context, ) -> Option { // Prevent the IME menu from appearing when holding down an alphabetic key // while input is disabled. @@ -14987,13 +15950,13 @@ impl ViewInputHandler for Editor { }) } - fn marked_text_range(&self, cx: &mut ViewContext) -> Option> { + fn marked_text_range(&self, _: &mut Window, cx: &mut Context) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.first()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } - fn unmark_text(&mut self, cx: &mut ViewContext) { + fn unmark_text(&mut self, _: &mut Window, cx: &mut Context) { self.clear_highlights::(cx); self.ime_transaction.take(); } @@ -15002,14 +15965,15 @@ impl ViewInputHandler for Editor { &mut self, range_utf16: Option>, text: &str, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if !self.input_enabled { cx.emit(EditorEvent::InputIgnored { text: text.into() }); return; } - self.transact(cx, |this, cx| { + self.transact(window, cx, |this, window, cx| { let new_selected_ranges = if let Some(range_utf16) = range_utf16 { let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); Some(this.selection_replacement_ranges(range_utf16, cx)) @@ -15041,13 +16005,13 @@ impl ViewInputHandler for Editor { }); if let Some(new_selected_ranges) = new_selected_ranges { - this.change_selections(None, cx, |selections| { + this.change_selections(None, window, cx, |selections| { selections.select_ranges(new_selected_ranges) }); - this.backspace(&Default::default(), cx); + this.backspace(&Default::default(), window, cx); } - this.handle_input(text, cx); + this.handle_input(text, window, cx); }); if let Some(transaction) = self.ime_transaction { @@ -15056,7 +16020,7 @@ impl ViewInputHandler for Editor { }); } - self.unmark_text(cx); + self.unmark_text(window, cx); } fn replace_and_mark_text_in_range( @@ -15064,13 +16028,14 @@ impl ViewInputHandler for Editor { range_utf16: Option>, text: &str, new_selected_range_utf16: Option>, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) { if !self.input_enabled { return; } - let transaction = self.transact(cx, |this, cx| { + let transaction = self.transact(window, cx, |this, window, cx| { let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) { let snapshot = this.buffer.read(cx).read(cx); if let Some(relative_range_utf16) = range_utf16.as_ref() { @@ -15115,7 +16080,7 @@ impl ViewInputHandler for Editor { }); if let Some(ranges) = ranges_to_replace { - this.change_selections(None, cx, |s| s.select_ranges(ranges)); + this.change_selections(None, window, cx, |s| s.select_ranges(ranges)); } let marked_ranges = { @@ -15130,7 +16095,7 @@ impl ViewInputHandler for Editor { }; if text.is_empty() { - this.unmark_text(cx); + this.unmark_text(window, cx); } else { this.highlight_text::( marked_ranges.clone(), @@ -15151,7 +16116,7 @@ impl ViewInputHandler for Editor { let use_auto_surround = this.use_auto_surround; this.set_use_autoclose(false); this.set_use_auto_surround(false); - this.handle_input(text, cx); + this.handle_input(text, window, cx); this.set_use_autoclose(use_autoclose); this.set_use_auto_surround(use_auto_surround); @@ -15169,7 +16134,7 @@ impl ViewInputHandler for Editor { .collect::>(); drop(snapshot); - this.change_selections(None, cx, |selections| { + this.change_selections(None, window, cx, |selections| { selections.select_ranges(new_selected_ranges) }); } @@ -15191,15 +16156,16 @@ impl ViewInputHandler for Editor { &mut self, range_utf16: Range, element_bounds: gpui::Bounds, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Option> { - let text_layout_details = self.text_layout_details(cx); + let text_layout_details = self.text_layout_details(window); let gpui::Point { x: em_width, y: line_height, - } = self.character_size(cx); + } = self.character_size(window); - let snapshot = self.snapshot(cx); + let snapshot = self.snapshot(window, cx); let scroll_position = snapshot.scroll_position(); let scroll_left = scroll_position.x * em_width; @@ -15325,7 +16291,7 @@ pub fn diagnostic_block_renderer( Arc::new(move |cx: &mut BlockContext| { let group_id: SharedString = cx.block_id.to_string().into(); - let mut text_style = cx.text_style().clone(); + let mut text_style = cx.window.text_style().clone(); text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status()); let theme_settings = ThemeSettings::get_global(cx); text_style.font_family = theme_settings.buffer_font.family.clone(); @@ -15348,8 +16314,12 @@ pub fn diagnostic_block_renderer( .size(ButtonSize::Compact) .style(ButtonStyle::Transparent) .visible_on_hover(group_id.clone()) - .on_click(move |_click, cx| cx.dispatch_action(Box::new(Cancel))) - .tooltip(|cx| Tooltip::for_action("Close Diagnostics", &Cancel, cx)) + .on_click(move |_click, window, cx| { + window.dispatch_action(Box::new(Cancel), cx) + }) + .tooltip(|window, cx| { + Tooltip::for_action("Close Diagnostics", &Cancel, window, cx) + }) })) }) .child( @@ -15360,17 +16330,19 @@ pub fn diagnostic_block_renderer( .visible_on_hover(group_id.clone()) .on_click({ let message = diagnostic.message.clone(); - move |_click, cx| { + move |_click, _, cx| { cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) } }) - .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)), + .tooltip(Tooltip::text("Copy diagnostic message")), ) }; - let icon_size = buttons(&diagnostic) - .into_any_element() - .layout_as_root(AvailableSpace::min_size(), cx); + let icon_size = buttons(&diagnostic).into_any_element().layout_as_root( + AvailableSpace::min_size(), + cx.window, + cx.app, + ); h_flex() .id(cx.block_id) @@ -15406,74 +16378,23 @@ pub fn diagnostic_block_renderer( } fn inline_completion_edit_text( - editor_snapshot: &EditorSnapshot, - edits: &Vec<(Range, String)>, + current_snapshot: &BufferSnapshot, + edits: &[(Range, String)], + edit_preview: &EditPreview, include_deletions: bool, - cx: &WindowContext, -) -> InlineCompletionText { - let edit_start = edits - .first() - .unwrap() - .0 - .start - .to_display_point(editor_snapshot); - - let mut text = String::new(); - let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left); - let mut highlights = Vec::new(); - for (old_range, new_text) in edits { - let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot); - text.extend( - editor_snapshot - .buffer_snapshot - .chunks(offset..old_offset_range.start, false) - .map(|chunk| chunk.text), - ); - offset = old_offset_range.end; - - let start = text.len(); - let color = if include_deletions && new_text.is_empty() { - text.extend( - editor_snapshot - .buffer_snapshot - .chunks(old_offset_range.start..offset, false) - .map(|chunk| chunk.text), - ); - cx.theme().status().deleted_background - } else { - text.push_str(new_text); - cx.theme().status().created_background - }; - let end = text.len(); - - highlights.push(( - start..end, - HighlightStyle { - background_color: Some(color), - ..Default::default() - }, - )); - } - - let edit_end = edits - .last() - .unwrap() - .0 - .end - .to_display_point(editor_snapshot); - let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row())) - .to_offset(editor_snapshot, Bias::Right); - text.extend( - editor_snapshot - .buffer_snapshot - .chunks(offset..end_of_line, false) - .map(|chunk| chunk.text), - ); + cx: &App, +) -> Option { + let edits = edits + .iter() + .map(|(anchor, text)| { + ( + anchor.start.text_anchor..anchor.end.text_anchor, + text.clone(), + ) + }) + .collect::>(); - InlineCompletionText::Edit { - text: text.into(), - highlights, - } + Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)) } pub fn highlight_diagnostic_message( @@ -15717,34 +16638,23 @@ impl RowRangeExt for Range { } } -fn hunk_status(hunk: &MultiBufferDiffHunk) -> DiffHunkStatus { - if hunk.diff_base_byte_range.is_empty() { - DiffHunkStatus::Added - } else if hunk.row_range.is_empty() { - DiffHunkStatus::Removed - } else { - DiffHunkStatus::Modified - } -} - /// If select range has more than one line, we /// just point the cursor to range.start. -fn check_multiline_range(buffer: &Buffer, range: Range) -> Range { - if buffer.offset_to_point(range.start).row == buffer.offset_to_point(range.end).row { +fn collapse_multiline_range(range: Range) -> Range { + if range.start.row == range.end.row { range } else { range.start..range.start } } - pub struct KillRing(ClipboardItem); impl Global for KillRing {} const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); struct BreakpointPromptEditor { - pub(crate) prompt: View, - editor: WeakView, + pub(crate) prompt: Entity, + editor: WeakEntity, breakpoint_anchor: text::Anchor, kind: BreakpointKind, block_ids: HashSet, @@ -15756,12 +16666,13 @@ impl BreakpointPromptEditor { const MAX_LINES: u8 = 4; fn new( - editor: WeakView, + editor: WeakEntity, breakpoint_anchor: text::Anchor, kind: BreakpointKind, - cx: &mut ViewContext, + window: &mut Window, + cx: &mut Context, ) -> Self { - let buffer = cx.new_model(|cx| { + let buffer = cx.new(|cx| { Buffer::local( kind.log_message() .map(|msg| msg.to_string()) @@ -15769,9 +16680,9 @@ impl BreakpointPromptEditor { cx, ) }); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - let prompt = cx.new_view(|cx| { + let prompt = cx.new(|cx| { let mut prompt = Editor::new( EditorMode::AutoHeight { max_lines: Self::MAX_LINES as usize, @@ -15779,6 +16690,7 @@ impl BreakpointPromptEditor { buffer, None, false, + window, cx, ); prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); @@ -15806,7 +16718,7 @@ impl BreakpointPromptEditor { self.block_ids.extend(block_ids) } - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { if let Some(editor) = self.editor.upgrade() { let log_message = self .prompt @@ -15828,21 +16740,21 @@ impl BreakpointPromptEditor { ); editor.remove_blocks(self.block_ids.clone(), None, cx); - editor.focus(cx); + cx.focus_self(window); }); } } - fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - if let Some(editor) = self.editor.upgrade() { - editor.update(cx, |editor, cx| { + fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { + self.editor + .update(cx, |editor, cx| { editor.remove_blocks(self.block_ids.clone(), None, cx); - editor.focus(cx); - }); - } + window.focus(&editor.focus_handle); + }) + .log_err(); } - fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + fn render_prompt_editor(&self, cx: &mut Context) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { color: if self.prompt.read(cx).read_only(cx) { @@ -15870,7 +16782,7 @@ impl BreakpointPromptEditor { } impl Render for BreakpointPromptEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let gutter_dimensions = *self.gutter_dimensions.lock(); h_flex() .key_context("Editor") @@ -15878,7 +16790,7 @@ impl Render for BreakpointPromptEditor { .border_y_1() .border_color(cx.theme().status().info_border) .size_full() - .py(cx.line_height() / 2.5) + .py(window.line_height() / 2.5) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))) @@ -15886,8 +16798,36 @@ impl Render for BreakpointPromptEditor { } } -impl FocusableView for BreakpointPromptEditor { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { +impl Focusable for BreakpointPromptEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { self.prompt.focus_handle(cx) } } + +fn all_edits_insertions_or_deletions( + edits: &Vec<(Range, String)>, + snapshot: &MultiBufferSnapshot, +) -> bool { + let mut all_insertions = true; + let mut all_deletions = true; + + for (range, new_text) in edits.iter() { + let range_is_empty = range.to_offset(&snapshot).is_empty(); + let text_is_empty = new_text.is_empty(); + + if range_is_empty != text_is_empty { + if range_is_empty { + all_deletions = false; + } else { + all_insertions = false; + } + } else { + return false; + } + + if !all_insertions && !all_deletions { + return false; + } + } + all_insertions || all_deletions +} diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 52b69833ec71dd..a69988b8e11360 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -1,4 +1,4 @@ -use gpui::AppContext; +use gpui::App; use language::CursorShape; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -463,7 +463,7 @@ pub struct GutterContent { } impl EditorSettings { - pub fn jupyter_enabled(cx: &AppContext) -> bool { + pub fn jupyter_enabled(cx: &App) -> bool { EditorSettings::get_global(cx).jupyter.enabled } } @@ -473,10 +473,7 @@ impl Settings for EditorSettings { type FileContent = EditorSettingsContent; - fn load( - sources: SettingsSources, - _: &mut AppContext, - ) -> anyhow::Result { + fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { sources.json_merge() } } diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 50392ab2c9dfec..6275ec97a18ba0 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use gpui::{AppContext, FontFeatures, FontWeight}; +use gpui::{App, FontFeatures, FontWeight}; use project::project_settings::{InlineBlameSettings, ProjectSettings}; use settings::{EditableSettingControl, Settings}; use theme::{FontFamilyCache, ThemeSettings}; @@ -27,7 +27,7 @@ impl EditorSettingsControls { } impl RenderOnce for EditorSettingsControls { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { SettingsContainer::new() .child( SettingsGroup::new("Font") @@ -65,7 +65,7 @@ impl EditableSettingControl for BufferFontFamilyControl { "Buffer Font Family".into() } - fn read(cx: &AppContext) -> Self::Value { + fn read(cx: &App) -> Self::Value { let settings = ThemeSettings::get_global(cx); settings.buffer_font.family.clone() } @@ -73,14 +73,14 @@ impl EditableSettingControl for BufferFontFamilyControl { fn apply( settings: &mut ::FileContent, value: Self::Value, - _cx: &AppContext, + _cx: &App, ) { settings.buffer_font_family = Some(value.to_string()); } } impl RenderOnce for BufferFontFamilyControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let value = Self::read(cx); h_flex() @@ -89,18 +89,18 @@ impl RenderOnce for BufferFontFamilyControl { .child(DropdownMenu::new( "buffer-font-family", value.clone(), - ContextMenu::build(cx, |mut menu, cx| { + ContextMenu::build(window, cx, |mut menu, _, cx| { let font_family_cache = FontFamilyCache::global(cx); for font_name in font_family_cache.list_font_families(cx) { menu = menu.custom_entry( { let font_name = font_name.clone(); - move |_cx| Label::new(font_name.clone()).into_any_element() + move |_window, _cx| Label::new(font_name.clone()).into_any_element() }, { let font_name = font_name.clone(); - move |cx| { + move |_window, cx| { Self::write(font_name.clone(), cx); } }, @@ -124,7 +124,7 @@ impl EditableSettingControl for BufferFontSizeControl { "Buffer Font Size".into() } - fn read(cx: &AppContext) -> Self::Value { + fn read(cx: &App) -> Self::Value { let settings = ThemeSettings::get_global(cx); settings.buffer_font_size } @@ -132,14 +132,14 @@ impl EditableSettingControl for BufferFontSizeControl { fn apply( settings: &mut ::FileContent, value: Self::Value, - _cx: &AppContext, + _cx: &App, ) { settings.buffer_font_size = Some(value.into()); } } impl RenderOnce for BufferFontSizeControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let value = Self::read(cx); h_flex() @@ -148,10 +148,10 @@ impl RenderOnce for BufferFontSizeControl { .child(NumericStepper::new( "buffer-font-size", value.to_string(), - move |_, cx| { + move |_, _, cx| { Self::write(value - px(1.), cx); }, - move |_, cx| { + move |_, _, cx| { Self::write(value + px(1.), cx); }, )) @@ -169,7 +169,7 @@ impl EditableSettingControl for BufferFontWeightControl { "Buffer Font Weight".into() } - fn read(cx: &AppContext) -> Self::Value { + fn read(cx: &App) -> Self::Value { let settings = ThemeSettings::get_global(cx); settings.buffer_font.weight } @@ -177,14 +177,14 @@ impl EditableSettingControl for BufferFontWeightControl { fn apply( settings: &mut ::FileContent, value: Self::Value, - _cx: &AppContext, + _cx: &App, ) { settings.buffer_font_weight = Some(value.0); } } impl RenderOnce for BufferFontWeightControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let value = Self::read(cx); h_flex() @@ -193,12 +193,12 @@ impl RenderOnce for BufferFontWeightControl { .child(DropdownMenu::new( "buffer-font-weight", value.0.to_string(), - ContextMenu::build(cx, |mut menu, _cx| { + ContextMenu::build(window, cx, |mut menu, _window, _cx| { for weight in FontWeight::ALL { menu = menu.custom_entry( - move |_cx| Label::new(weight.0.to_string()).into_any_element(), + move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(), { - move |cx| { + move |_, cx| { Self::write(weight, cx); } }, @@ -222,7 +222,7 @@ impl EditableSettingControl for BufferFontLigaturesControl { "Buffer Font Ligatures".into() } - fn read(cx: &AppContext) -> Self::Value { + fn read(cx: &App) -> Self::Value { let settings = ThemeSettings::get_global(cx); settings .buffer_font @@ -234,7 +234,7 @@ impl EditableSettingControl for BufferFontLigaturesControl { fn apply( settings: &mut ::FileContent, value: Self::Value, - _cx: &AppContext, + _cx: &App, ) { let value = if value { 1 } else { 0 }; @@ -255,14 +255,14 @@ impl EditableSettingControl for BufferFontLigaturesControl { } impl RenderOnce for BufferFontLigaturesControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let value = Self::read(cx); CheckboxWithLabel::new( "buffer-font-ligatures", Label::new(self.name()), value.into(), - |selection, cx| { + |selection, _, cx| { Self::write( match selection { ToggleState::Selected => true, @@ -286,7 +286,7 @@ impl EditableSettingControl for InlineGitBlameControl { "Inline Git Blame".into() } - fn read(cx: &AppContext) -> Self::Value { + fn read(cx: &App) -> Self::Value { let settings = ProjectSettings::get_global(cx); settings.git.inline_blame_enabled() } @@ -294,7 +294,7 @@ impl EditableSettingControl for InlineGitBlameControl { fn apply( settings: &mut ::FileContent, value: Self::Value, - _cx: &AppContext, + _cx: &App, ) { if let Some(inline_blame) = settings.git.inline_blame.as_mut() { inline_blame.enabled = value; @@ -308,14 +308,14 @@ impl EditableSettingControl for InlineGitBlameControl { } impl RenderOnce for InlineGitBlameControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let value = Self::read(cx); CheckboxWithLabel::new( "inline-git-blame", Label::new(self.name()), value.into(), - |selection, cx| { + |selection, _, cx| { Self::write( match selection { ToggleState::Selected => true, @@ -339,7 +339,7 @@ impl EditableSettingControl for LineNumbersControl { "Line Numbers".into() } - fn read(cx: &AppContext) -> Self::Value { + fn read(cx: &App) -> Self::Value { let settings = EditorSettings::get_global(cx); settings.gutter.line_numbers } @@ -347,7 +347,7 @@ impl EditableSettingControl for LineNumbersControl { fn apply( settings: &mut ::FileContent, value: Self::Value, - _cx: &AppContext, + _cx: &App, ) { if let Some(gutter) = settings.gutter.as_mut() { gutter.line_numbers = Some(value); @@ -361,14 +361,14 @@ impl EditableSettingControl for LineNumbersControl { } impl RenderOnce for LineNumbersControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let value = Self::read(cx); CheckboxWithLabel::new( "line-numbers", Label::new(self.name()), value.into(), - |selection, cx| { + |selection, _, cx| { Self::write( match selection { ToggleState::Selected => true, @@ -392,7 +392,7 @@ impl EditableSettingControl for RelativeLineNumbersControl { "Relative Line Numbers".into() } - fn read(cx: &AppContext) -> Self::Value { + fn read(cx: &App) -> Self::Value { let settings = EditorSettings::get_global(cx); settings.relative_line_numbers } @@ -400,27 +400,27 @@ impl EditableSettingControl for RelativeLineNumbersControl { fn apply( settings: &mut ::FileContent, value: Self::Value, - _cx: &AppContext, + _cx: &App, ) { settings.relative_line_numbers = Some(value); } } impl RenderOnce for RelativeLineNumbersControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let value = Self::read(cx); DropdownMenu::new( "relative-line-numbers", if value { "Relative" } else { "Ascending" }, - ContextMenu::build(cx, |menu, _cx| { + ContextMenu::build(window, cx, |menu, _window, _cx| { menu.custom_entry( - |_cx| Label::new("Ascending").into_any_element(), - move |cx| Self::write(false, cx), + |_window, _cx| Label::new("Ascending").into_any_element(), + move |_, cx| Self::write(false, cx), ) .custom_entry( - |_cx| Label::new("Relative").into_any_element(), - move |cx| Self::write(true, cx), + |_window, _cx| Label::new("Relative").into_any_element(), + move |_, cx| Self::write(true, cx), ) }), ) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1aa1c2564401dc..f653951a868948 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -19,11 +19,11 @@ use language::{ }, BracketPairConfig, Capability::ReadWrite, - FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, - LanguageName, Override, ParsedMarkdown, Point, + FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, + Override, ParsedMarkdown, Point, }; use language_settings::{Formatter, FormatterList, IndentGuideSettings}; -use multi_buffer::MultiBufferIndentGuide; +use multi_buffer::IndentGuide; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; use project::{buffer_store::BufferChangeSet, FakeFs}; @@ -53,7 +53,7 @@ use workspace::{ fn test_edit_events(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.new_model(|cx| { + let buffer = cx.new(|cx| { let mut buffer = language::Buffer::local("123456", cx); buffer.set_group_interval(Duration::from_secs(1)); buffer @@ -62,24 +62,31 @@ fn test_edit_events(cx: &mut TestAppContext) { let events = Rc::new(RefCell::new(Vec::new())); let editor1 = cx.add_window({ let events = events.clone(); - |cx| { - let view = cx.view().clone(); - cx.subscribe(&view, move |_, _, event: &EditorEvent, _| match event { - EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")), - EditorEvent::BufferEdited => events.borrow_mut().push(("editor1", "buffer edited")), - _ => {} - }) + |window, cx| { + let model = cx.entity().clone(); + cx.subscribe_in( + &model, + window, + move |_, _, event: &EditorEvent, _, _| match event { + EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")), + EditorEvent::BufferEdited => { + events.borrow_mut().push(("editor1", "buffer edited")) + } + _ => {} + }, + ) .detach(); - Editor::for_buffer(buffer.clone(), None, cx) + Editor::for_buffer(buffer.clone(), None, window, cx) } }); let editor2 = cx.add_window({ let events = events.clone(); - |cx| { - cx.subscribe( - &cx.view().clone(), - move |_, _, event: &EditorEvent, _| match event { + |window, cx| { + cx.subscribe_in( + &cx.entity().clone(), + window, + move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")), EditorEvent::BufferEdited => { events.borrow_mut().push(("editor2", "buffer edited")) @@ -88,14 +95,14 @@ fn test_edit_events(cx: &mut TestAppContext) { }, ) .detach(); - Editor::for_buffer(buffer.clone(), None, cx) + Editor::for_buffer(buffer.clone(), None, window, cx) } }); assert_eq!(mem::take(&mut *events.borrow_mut()), []); // Mutating editor 1 will emit an `Edited` event only for that editor. - _ = editor1.update(cx, |editor, cx| editor.insert("X", cx)); + _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ @@ -106,7 +113,7 @@ fn test_edit_events(cx: &mut TestAppContext) { ); // Mutating editor 2 will emit an `Edited` event only for that editor. - _ = editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); + _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ @@ -117,7 +124,7 @@ fn test_edit_events(cx: &mut TestAppContext) { ); // Undoing on editor 1 will emit an `Edited` event only for that editor. - _ = editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); + _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ @@ -128,7 +135,7 @@ fn test_edit_events(cx: &mut TestAppContext) { ); // Redoing on editor 1 will emit an `Edited` event only for that editor. - _ = editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); + _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ @@ -139,7 +146,7 @@ fn test_edit_events(cx: &mut TestAppContext) { ); // Undoing on editor 2 will emit an `Edited` event only for that editor. - _ = editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); + _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ @@ -150,7 +157,7 @@ fn test_edit_events(cx: &mut TestAppContext) { ); // Redoing on editor 2 will emit an `Edited` event only for that editor. - _ = editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); + _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx)); assert_eq!( mem::take(&mut *events.borrow_mut()), [ @@ -161,10 +168,10 @@ fn test_edit_events(cx: &mut TestAppContext) { ); // No event is emitted when the mutation is a no-op. - _ = editor2.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([0..0])); + _ = editor2.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([0..0])); - editor.backspace(&Backspace, cx); + editor.backspace(&Backspace, window, cx); }); assert_eq!(mem::take(&mut *events.borrow_mut()), []); } @@ -175,32 +182,32 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { let mut now = Instant::now(); let group_interval = Duration::from_millis(1); - let buffer = cx.new_model(|cx| { + let buffer = cx.new(|cx| { let mut buf = language::Buffer::local("123456", cx); buf.set_group_interval(group_interval); buf }); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx)); - _ = editor.update(cx, |editor, cx| { - editor.start_transaction_at(now, cx); - editor.change_selections(None, cx, |s| s.select_ranges([2..4])); + _ = editor.update(cx, |editor, window, cx| { + editor.start_transaction_at(now, window, cx); + editor.change_selections(None, window, cx, |s| s.select_ranges([2..4])); - editor.insert("cd", cx); + editor.insert("cd", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cd56"); assert_eq!(editor.selections.ranges(cx), vec![4..4]); - editor.start_transaction_at(now, cx); - editor.change_selections(None, cx, |s| s.select_ranges([4..5])); - editor.insert("e", cx); + editor.start_transaction_at(now, window, cx); + editor.change_selections(None, window, cx, |s| s.select_ranges([4..5])); + editor.insert("e", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!(editor.selections.ranges(cx), vec![5..5]); now += group_interval + Duration::from_millis(1); - editor.change_selections(None, cx, |s| s.select_ranges([2..2])); + editor.change_selections(None, window, cx, |s| s.select_ranges([2..2])); // Simulate an edit in another editor buffer.update(cx, |buffer, cx| { @@ -215,31 +222,31 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { // Last transaction happened past the group interval in a different editor. // Undo it individually and don't restore selections. - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!(editor.selections.ranges(cx), vec![2..2]); // First two transactions happened within the group interval in this editor. // Undo them together and restore selections. - editor.undo(&Undo, cx); - editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. + editor.undo(&Undo, window, cx); + editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op. assert_eq!(editor.text(cx), "123456"); assert_eq!(editor.selections.ranges(cx), vec![0..0]); // Redo the first two transactions together. - editor.redo(&Redo, cx); + editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!(editor.selections.ranges(cx), vec![5..5]); // Redo the last transaction on its own. - editor.redo(&Redo, cx); + editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "ab2cde6"); assert_eq!(editor.selections.ranges(cx), vec![6..6]); // Test empty transactions. - editor.start_transaction_at(now, cx); + editor.start_transaction_at(now, window, cx); editor.end_transaction_at(now, cx); - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "12cde6"); }); } @@ -248,21 +255,21 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { fn test_ime_composition(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.new_model(|cx| { + let buffer = cx.new(|cx| { let mut buffer = language::Buffer::local("abcde", cx); // Ensure automatic grouping doesn't occur. buffer.set_group_interval(Duration::ZERO); buffer }); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - cx.add_window(|cx| { - let mut editor = build_editor(buffer.clone(), cx); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + cx.add_window(|window, cx| { + let mut editor = build_editor(buffer.clone(), window, cx); // Start a new IME composition. - editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); - editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); - editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx); assert_eq!(editor.text(cx), "äbcde"); assert_eq!( editor.marked_text_ranges(cx), @@ -270,32 +277,32 @@ fn test_ime_composition(cx: &mut TestAppContext) { ); // Finalize IME composition. - editor.replace_text_in_range(None, "ā", cx); + editor.replace_text_in_range(None, "ā", window, cx); assert_eq!(editor.text(cx), "ābcde"); assert_eq!(editor.marked_text_ranges(cx), None); // IME composition edits are grouped and are undone/redone at once. - editor.undo(&Default::default(), cx); + editor.undo(&Default::default(), window, cx); assert_eq!(editor.text(cx), "abcde"); assert_eq!(editor.marked_text_ranges(cx), None); - editor.redo(&Default::default(), cx); + editor.redo(&Default::default(), window, cx); assert_eq!(editor.text(cx), "ābcde"); assert_eq!(editor.marked_text_ranges(cx), None); // Start a new IME composition. - editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); + editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx); assert_eq!( editor.marked_text_ranges(cx), Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) ); // Undoing during an IME composition cancels it. - editor.undo(&Default::default(), cx); + editor.undo(&Default::default(), window, cx); assert_eq!(editor.text(cx), "ābcde"); assert_eq!(editor.marked_text_ranges(cx), None); // Start a new IME composition with an invalid marked range, ensuring it gets clipped. - editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); + editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx); assert_eq!(editor.text(cx), "ābcdè"); assert_eq!( editor.marked_text_ranges(cx), @@ -303,19 +310,19 @@ fn test_ime_composition(cx: &mut TestAppContext) { ); // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. - editor.replace_text_in_range(Some(4..999), "ę", cx); + editor.replace_text_in_range(Some(4..999), "ę", window, cx); assert_eq!(editor.text(cx), "ābcdę"); assert_eq!(editor.marked_text_ranges(cx), None); // Start a new IME composition with multiple cursors. - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([ OffsetUtf16(1)..OffsetUtf16(1), OffsetUtf16(3)..OffsetUtf16(3), OffsetUtf16(5)..OffsetUtf16(5), ]) }); - editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); + editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx); assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); assert_eq!( editor.marked_text_ranges(cx), @@ -327,7 +334,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { ); // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. - editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); + editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx); assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); assert_eq!( editor.marked_text_ranges(cx), @@ -339,7 +346,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { ); // Finalize IME composition with multiple cursors. - editor.replace_text_in_range(Some(9..10), "2", cx); + editor.replace_text_in_range(Some(9..10), "2", window, cx); assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); assert_eq!(editor.marked_text_ranges(cx), None); @@ -351,83 +358,87 @@ fn test_ime_composition(cx: &mut TestAppContext) { fn test_selection_with_mouse(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx); }); assert_eq!( editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.display_ranges(cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)] ); - _ = editor.update(cx, |view, cx| { - view.update_selection( + _ = editor.update(cx, |editor, window, cx| { + editor.update_selection( DisplayPoint::new(DisplayRow(3), 3), 0, gpui::Point::::default(), + window, cx, ); }); assert_eq!( editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.display_ranges(cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)] ); - _ = editor.update(cx, |view, cx| { - view.update_selection( + _ = editor.update(cx, |editor, window, cx| { + editor.update_selection( DisplayPoint::new(DisplayRow(1), 1), 0, gpui::Point::::default(), + window, cx, ); }); assert_eq!( editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.display_ranges(cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)] ); - _ = editor.update(cx, |view, cx| { - view.end_selection(cx); - view.update_selection( + _ = editor.update(cx, |editor, window, cx| { + editor.end_selection(window, cx); + editor.update_selection( DisplayPoint::new(DisplayRow(3), 3), 0, gpui::Point::::default(), + window, cx, ); }); assert_eq!( editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.display_ranges(cx)) .unwrap(), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)] ); - _ = editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, cx); - view.update_selection( + _ = editor.update(cx, |editor, window, cx| { + editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx); + editor.update_selection( DisplayPoint::new(DisplayRow(0), 0), 0, gpui::Point::::default(), + window, cx, ); }); assert_eq!( editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.display_ranges(cx)) .unwrap(), [ DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1), @@ -435,13 +446,13 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { ] ); - _ = editor.update(cx, |view, cx| { - view.end_selection(cx); + _ = editor.update(cx, |editor, window, cx| { + editor.end_selection(window, cx); }); assert_eq!( editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.display_ranges(cx)) .unwrap(), [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)] ); @@ -451,30 +462,30 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { fn test_multiple_cursor_removal(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx); }); - _ = editor.update(cx, |view, cx| { - view.end_selection(cx); + _ = editor.update(cx, |editor, window, cx| { + editor.end_selection(window, cx); }); - _ = editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx); }); - _ = editor.update(cx, |view, cx| { - view.end_selection(cx); + _ = editor.update(cx, |editor, window, cx| { + editor.end_selection(window, cx); }); assert_eq!( editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.display_ranges(cx)) .unwrap(), [ DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1), @@ -482,17 +493,17 @@ fn test_multiple_cursor_removal(cx: &mut TestAppContext) { ] ); - _ = editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx); }); - _ = editor.update(cx, |view, cx| { - view.end_selection(cx); + _ = editor.update(cx, |editor, window, cx| { + editor.end_selection(window, cx); }); assert_eq!( editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.display_ranges(cx)) .unwrap(), [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)] ); @@ -502,42 +513,44 @@ fn test_multiple_cursor_removal(cx: &mut TestAppContext) { fn test_canceling_pending_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)] ); }); - _ = view.update(cx, |view, cx| { - view.update_selection( + _ = editor.update(cx, |editor, window, cx| { + editor.update_selection( DisplayPoint::new(DisplayRow(3), 3), 0, gpui::Point::::default(), + window, cx, ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)] ); }); - _ = view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - view.update_selection( + _ = editor.update(cx, |editor, window, cx| { + editor.cancel(&Cancel, window, cx); + editor.update_selection( DisplayPoint::new(DisplayRow(1), 1), 0, gpui::Point::::default(), + window, cx, ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)] ); }); @@ -547,33 +560,33 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)] ); - view.move_down(&Default::default(), cx); + editor.move_down(&Default::default(), window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)] ); - view.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, cx); + editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)] ); - view.move_up(&Default::default(), cx); + editor.move_up(&Default::default(), window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)] ); }); @@ -594,38 +607,47 @@ fn test_clone(cx: &mut TestAppContext) { true, ); - let editor = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&text, cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_ranges(selection_ranges.clone()) + }); editor.fold_creases( vec![ Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()), Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()), ], true, + window, cx, ); }); let cloned_editor = editor - .update(cx, |editor, cx| { - cx.open_window(Default::default(), |cx| cx.new_view(|cx| editor.clone(cx))) + .update(cx, |editor, _, cx| { + cx.open_window(Default::default(), |window, cx| { + cx.new(|cx| editor.clone(window, cx)) + }) }) .unwrap() .unwrap(); - let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); - let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); + let snapshot = editor + .update(cx, |e, window, cx| e.snapshot(window, cx)) + .unwrap(); + let cloned_snapshot = cloned_editor + .update(cx, |e, window, cx| e.snapshot(window, cx)) + .unwrap(); assert_eq!( cloned_editor - .update(cx, |e, cx| e.display_text(cx)) + .update(cx, |e, _, cx| e.display_text(cx)) .unwrap(), - editor.update(cx, |e, cx| e.display_text(cx)).unwrap() + editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap() ); assert_eq!( cloned_snapshot @@ -635,18 +657,18 @@ fn test_clone(cx: &mut TestAppContext) { ); assert_set_eq!( cloned_editor - .update(cx, |editor, cx| editor.selections.ranges::(cx)) + .update(cx, |editor, _, cx| editor.selections.ranges::(cx)) .unwrap(), editor - .update(cx, |editor, cx| editor.selections.ranges(cx)) + .update(cx, |editor, _, cx| editor.selections.ranges(cx)) .unwrap() ); assert_set_eq!( cloned_editor - .update(cx, |e, cx| e.selections.display_ranges(cx)) + .update(cx, |e, _window, cx| e.selections.display_ranges(cx)) .unwrap(), editor - .update(cx, |e, cx| e.selections.display_ranges(cx)) + .update(cx, |e, _, cx| e.selections.display_ranges(cx)) .unwrap() ); } @@ -659,30 +681,30 @@ async fn test_navigation_history(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); let pane = workspace - .update(cx, |workspace, _| workspace.active_pane().clone()) + .update(cx, |workspace, _, _| workspace.active_pane().clone()) .unwrap(); - _ = workspace.update(cx, |_v, cx| { - cx.new_view(|cx| { + _ = workspace.update(cx, |_v, window, cx| { + cx.new(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), cx); - let handle = cx.view(); - editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(handle))); + let mut editor = build_editor(buffer.clone(), window, cx); + let handle = cx.entity(); + editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); - fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { + fn pop_history(editor: &mut Editor, cx: &mut App) -> Option { editor.nav_history.as_mut().unwrap().pop_backward(cx) } // Move the cursor a small distance. // Nothing is added to the navigation history. - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0) ]) }); - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0) ]) @@ -691,13 +713,13 @@ async fn test_navigation_history(cx: &mut TestAppContext) { // Move the cursor a large distance. // The history can jump back to the previous position. - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3) ]) }); let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); + editor.navigate(nav_entry.data.unwrap(), window, cx); assert_eq!(nav_entry.item.id(), cx.entity_id()); assert_eq!( editor.selections.display_ranges(cx), @@ -707,8 +729,8 @@ async fn test_navigation_history(cx: &mut TestAppContext) { // Move the cursor a small distance via the mouse. // Nothing is added to the navigation history. - editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, cx); - editor.end_selection(cx); + editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx); + editor.end_selection(window, cx); assert_eq!( editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)] @@ -717,14 +739,14 @@ async fn test_navigation_history(cx: &mut TestAppContext) { // Move the cursor a large distance via the mouse. // The history can jump back to the previous position. - editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, cx); - editor.end_selection(cx); + editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx); + editor.end_selection(window, cx); assert_eq!( editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)] ); let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); + editor.navigate(nav_entry.data.unwrap(), window, cx); assert_eq!(nav_entry.item.id(), cx.entity_id()); assert_eq!( editor.selections.display_ranges(cx), @@ -733,16 +755,16 @@ async fn test_navigation_history(cx: &mut TestAppContext) { assert!(pop_history(&mut editor, cx).is_none()); // Set scroll position to check later - editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), cx); + editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), window, cx); let original_scroll_position = editor.scroll_manager.anchor(); // Jump to the end of the document and adjust scroll - editor.move_to_end(&MoveToEnd, cx); - editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), cx); + editor.move_to_end(&MoveToEnd, window, cx); + editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), window, cx); assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); + editor.navigate(nav_entry.data.unwrap(), window, cx); assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); // Ensure we don't panic when navigation data contains invalid anchors *and* points. @@ -759,6 +781,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { }, scroll_top_row: invalid_point.row, }), + window, cx, ); assert_eq!( @@ -779,31 +802,33 @@ async fn test_navigation_history(cx: &mut TestAppContext) { fn test_cancel(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, cx); - view.update_selection( + _ = editor.update(cx, |editor, window, cx| { + editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx); + editor.update_selection( DisplayPoint::new(DisplayRow(1), 1), 0, gpui::Point::::default(), + window, cx, ); - view.end_selection(cx); + editor.end_selection(window, cx); - view.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, cx); - view.update_selection( + editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx); + editor.update_selection( DisplayPoint::new(DisplayRow(0), 3), 0, gpui::Point::::default(), + window, cx, ); - view.end_selection(cx); + editor.end_selection(window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3), DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1), @@ -811,18 +836,18 @@ fn test_cancel(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.cancel(&Cancel, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)] ); }); - _ = view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.cancel(&Cancel, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)] ); }); @@ -832,7 +857,7 @@ fn test_cancel(cx: &mut TestAppContext) { fn test_fold_action(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple( &" impl Foo { @@ -854,18 +879,18 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0) ]); }); - view.fold(&Fold, cx); + editor.fold(&Fold, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " impl Foo { // Hello! @@ -884,9 +909,9 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), ); - view.fold(&Fold, cx); + editor.fold(&Fold, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " impl Foo {⋯ } @@ -894,9 +919,9 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), ); - view.unfold_lines(&UnfoldLines, cx); + editor.unfold_lines(&UnfoldLines, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " impl Foo { // Hello! @@ -915,8 +940,11 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), ); - view.unfold_lines(&UnfoldLines, cx); - assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); + editor.unfold_lines(&UnfoldLines, window, cx); + assert_eq!( + editor.display_text(cx), + editor.buffer.read(cx).read(cx).text() + ); }); } @@ -924,7 +952,7 @@ fn test_fold_action(cx: &mut TestAppContext) { fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple( &" class Foo: @@ -942,18 +970,18 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0) ]); }); - view.fold(&Fold, cx); + editor.fold(&Fold, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo: # Hello! @@ -968,18 +996,18 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { .unindent(), ); - view.fold(&Fold, cx); + editor.fold(&Fold, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo:⋯ " .unindent(), ); - view.unfold_lines(&UnfoldLines, cx); + editor.unfold_lines(&UnfoldLines, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo: # Hello! @@ -994,8 +1022,11 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { .unindent(), ); - view.unfold_lines(&UnfoldLines, cx); - assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); + editor.unfold_lines(&UnfoldLines, window, cx); + assert_eq!( + editor.display_text(cx), + editor.buffer.read(cx).read(cx).text() + ); }); } @@ -1003,7 +1034,7 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple( &" class Foo: @@ -1024,18 +1055,18 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0) ]); }); - view.fold(&Fold, cx); + editor.fold(&Fold, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo: # Hello! @@ -1053,9 +1084,9 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), ); - view.fold(&Fold, cx); + editor.fold(&Fold, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo:⋯ @@ -1064,9 +1095,9 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), ); - view.unfold_lines(&UnfoldLines, cx); + editor.unfold_lines(&UnfoldLines, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo: # Hello! @@ -1084,8 +1115,11 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), ); - view.unfold_lines(&UnfoldLines, cx); - assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); + editor.unfold_lines(&UnfoldLines, window, cx); + assert_eq!( + editor.display_text(cx), + editor.buffer.read(cx).read(cx).text() + ); }); } @@ -1093,7 +1127,7 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { fn test_fold_at_level(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple( &" class Foo: @@ -1120,13 +1154,13 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); - _ = view.update(cx, |view, cx| { - view.fold_at_level(&FoldAtLevel { level: 2 }, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.fold_at_level(&FoldAtLevel { level: 2 }, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo: # Hello! @@ -1148,9 +1182,9 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), ); - view.fold_at_level(&FoldAtLevel { level: 1 }, cx); + editor.fold_at_level(&FoldAtLevel { level: 1 }, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo:⋯ @@ -1162,10 +1196,10 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), ); - view.unfold_all(&UnfoldAll, cx); - view.fold_at_level(&FoldAtLevel { level: 0 }, cx); + editor.unfold_all(&UnfoldAll, window, cx); + editor.fold_at_level(&FoldAtLevel { level: 0 }, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), " class Foo: # Hello! @@ -1191,7 +1225,10 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), ); - assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); + assert_eq!( + editor.display_text(cx), + editor.buffer.read(cx).read(cx).text() + ); }); } @@ -1200,7 +1237,7 @@ fn test_move_cursor(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); - let view = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx)); buffer.update(cx, |buffer, cx| { buffer.edit( @@ -1212,62 +1249,62 @@ fn test_move_cursor(cx: &mut TestAppContext) { cx, ); }); - _ = view.update(cx, |view, cx| { + _ = editor.update(cx, |editor, window, cx| { assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)] ); - view.move_right(&MoveRight, cx); + editor.move_right(&MoveRight, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)] ); - view.move_left(&MoveLeft, cx); + editor.move_left(&MoveLeft, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)] ); - view.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] ); - view.move_to_end(&MoveToEnd, cx); + editor.move_to_end(&MoveToEnd, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)] ); - view.move_to_beginning(&MoveToBeginning, cx); + editor.move_to_beginning(&MoveToBeginning, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] ); - view.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2) ]); }); - view.select_to_beginning(&SelectToBeginning, cx); + editor.select_to_beginning(&SelectToBeginning, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)] ); - view.select_to_end(&SelectToEnd, cx); + editor.select_to_end(&SelectToEnd, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)] ); }); @@ -1279,113 +1316,114 @@ fn test_move_cursor(cx: &mut TestAppContext) { fn test_move_cursor_multibyte(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); - build_editor(buffer.clone(), cx) + let editor = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx); + build_editor(buffer.clone(), window, cx) }); - assert_eq!('ⓐ'.len_utf8(), 3); + assert_eq!('🟥'.len_utf8(), 4); assert_eq!('α'.len_utf8(), 2); - _ = view.update(cx, |view, cx| { - view.fold_creases( + _ = editor.update(cx, |editor, window, cx| { + editor.fold_creases( vec![ - Crease::simple(Point::new(0, 6)..Point::new(0, 12), FoldPlaceholder::test()), + Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()), Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()), Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()), ], true, + window, cx, ); - assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); + assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε"); - view.move_right(&MoveRight, cx); + editor.move_right(&MoveRight, window, cx); assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐ".len())] + editor.selections.display_ranges(cx), + &[empty_range(0, "🟥".len())] ); - view.move_right(&MoveRight, cx); + editor.move_right(&MoveRight, window, cx); assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] + editor.selections.display_ranges(cx), + &[empty_range(0, "🟥🟧".len())] ); - view.move_right(&MoveRight, cx); + editor.move_right(&MoveRight, window, cx); assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ⋯".len())] + editor.selections.display_ranges(cx), + &[empty_range(0, "🟥🟧⋯".len())] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(1, "ab⋯e".len())] ); - view.move_left(&MoveLeft, cx); + editor.move_left(&MoveLeft, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(1, "ab⋯".len())] ); - view.move_left(&MoveLeft, cx); + editor.move_left(&MoveLeft, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(1, "ab".len())] ); - view.move_left(&MoveLeft, cx); + editor.move_left(&MoveLeft, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(1, "a".len())] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(2, "α".len())] ); - view.move_right(&MoveRight, cx); + editor.move_right(&MoveRight, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(2, "αβ".len())] ); - view.move_right(&MoveRight, cx); + editor.move_right(&MoveRight, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(2, "αβ⋯".len())] ); - view.move_right(&MoveRight, cx); + editor.move_right(&MoveRight, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(2, "αβ⋯ε".len())] ); - view.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(1, "ab⋯e".len())] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(2, "αβ⋯ε".len())] ); - view.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(1, "ab⋯e".len())] ); - view.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, window, cx); assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] + editor.selections.display_ranges(cx), + &[empty_range(0, "🟥🟧".len())] ); - view.move_left(&MoveLeft, cx); + editor.move_left(&MoveLeft, window, cx); assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐ".len())] + editor.selections.display_ranges(cx), + &[empty_range(0, "🟥".len())] ); - view.move_left(&MoveLeft, cx); + editor.move_left(&MoveLeft, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(0, "".len())] ); }); @@ -1395,75 +1433,75 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); }); // moving above start of document should move selection to start of document, // but the next move down should still be at the original goal_x - view.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(0, "".len())] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(1, "abcd".len())] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(2, "αβγ".len())] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(3, "abcd".len())] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] ); // moving past end of document should not change goal_x - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(5, "".len())] ); - view.move_down(&MoveDown, cx); + editor.move_down(&MoveDown, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(5, "".len())] ); - view.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] ); - view.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(3, "abcd".len())] ); - view.move_up(&MoveUp, cx); + editor.move_up(&MoveUp, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[empty_range(2, "αβγ".len())] ); }); @@ -1480,12 +1518,12 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { stop_at_soft_wraps: true, }; - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\n def", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4), @@ -1493,10 +1531,10 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { }); }); - _ = view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&move_to_beg, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.move_to_beginning_of_line(&move_to_beg, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), @@ -1504,10 +1542,10 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&move_to_beg, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.move_to_beginning_of_line(&move_to_beg, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0), @@ -1515,10 +1553,10 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&move_to_beg, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.move_to_beginning_of_line(&move_to_beg, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), @@ -1526,10 +1564,10 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.move_to_end_of_line(&move_to_end, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.move_to_end_of_line(&move_to_end, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3), DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5), @@ -1538,10 +1576,10 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { }); // Moving to the end of line again is a no-op. - _ = view.update(cx, |view, cx| { - view.move_to_end_of_line(&move_to_end, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.move_to_end_of_line(&move_to_end, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3), DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5), @@ -1549,16 +1587,17 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.move_left(&MoveLeft, cx); - view.select_to_beginning_of_line( + _ = editor.update(cx, |editor, window, cx| { + editor.move_left(&MoveLeft, window, cx); + editor.select_to_beginning_of_line( &SelectToBeginningOfLine { stop_at_soft_wraps: true, }, + window, cx, ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0), DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2), @@ -1566,15 +1605,16 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.select_to_beginning_of_line( + _ = editor.update(cx, |editor, window, cx| { + editor.select_to_beginning_of_line( &SelectToBeginningOfLine { stop_at_soft_wraps: true, }, + window, cx, ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0), DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0), @@ -1582,15 +1622,16 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.select_to_beginning_of_line( + _ = editor.update(cx, |editor, window, cx| { + editor.select_to_beginning_of_line( &SelectToBeginningOfLine { stop_at_soft_wraps: true, }, + window, cx, ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0), DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2), @@ -1598,15 +1639,16 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.select_to_end_of_line( + _ = editor.update(cx, |editor, window, cx| { + editor.select_to_end_of_line( &SelectToEndOfLine { stop_at_soft_wraps: true, }, + window, cx, ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3), DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5), @@ -1614,11 +1656,11 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.delete_to_end_of_line(&DeleteToEndOfLine, cx); - assert_eq!(view.display_text(cx), "ab\n de"); + _ = editor.update(cx, |editor, window, cx| { + editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx); + assert_eq!(editor.display_text(cx), "ab\n de"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4), @@ -1626,11 +1668,11 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); - assert_eq!(view.display_text(cx), "\n"); + _ = editor.update(cx, |editor, window, cx| { + editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx); + assert_eq!(editor.display_text(cx), "\n"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0), @@ -1650,13 +1692,13 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) { stop_at_soft_wraps: false, }; - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.0.into()), cx); + _ = editor.update(cx, |editor, window, cx| { + editor.set_wrap_width(Some(140.0.into()), cx); // We expect the following lines after wrapping // ``` @@ -1667,34 +1709,34 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) { // The final `gs` was soft-wrapped onto a new line. assert_eq!( "thequickbrownfox\njumpedoverthelaz\nydogs", - view.display_text(cx), + editor.display_text(cx), ); // First, let's assert behavior on the first line, that was not soft-wrapped. // Start the cursor at the `k` on the first line - view.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7) ]); }); // Moving to the beginning of the line should put us at the beginning of the line. - view.move_to_beginning_of_line(&move_to_beg, cx); + editor.move_to_beginning_of_line(&move_to_beg, window, cx); assert_eq!( vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),], - view.selections.display_ranges(cx) + editor.selections.display_ranges(cx) ); // Moving to the end of the line should put us at the end of the line. - view.move_to_end_of_line(&move_to_end, cx); + editor.move_to_end_of_line(&move_to_end, window, cx); assert_eq!( vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),], - view.selections.display_ranges(cx) + editor.selections.display_ranges(cx) ); // Now, let's assert behavior on the second line, that ended up being soft-wrapped. // Start the cursor at the last line (`y` that was wrapped to a new line) - view.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0) ]); @@ -1702,32 +1744,32 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) { // Moving to the beginning of the line should put us at the start of the second line of // display text, i.e., the `j`. - view.move_to_beginning_of_line(&move_to_beg, cx); + editor.move_to_beginning_of_line(&move_to_beg, window, cx); assert_eq!( vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),], - view.selections.display_ranges(cx) + editor.selections.display_ranges(cx) ); // Moving to the beginning of the line again should be a no-op. - view.move_to_beginning_of_line(&move_to_beg, cx); + editor.move_to_beginning_of_line(&move_to_beg, window, cx); assert_eq!( vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),], - view.selections.display_ranges(cx) + editor.selections.display_ranges(cx) ); // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the // next display line. - view.move_to_end_of_line(&move_to_end, cx); + editor.move_to_end_of_line(&move_to_end, window, cx); assert_eq!( vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),], - view.selections.display_ranges(cx) + editor.selections.display_ranges(cx) ); // Moving to the end of the line again should be a no-op. - view.move_to_end_of_line(&move_to_end, cx); + editor.move_to_end_of_line(&move_to_end, window, cx); assert_eq!( vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),], - view.selections.display_ranges(cx) + editor.selections.display_ranges(cx) ); }); } @@ -1736,51 +1778,63 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) { fn test_prev_next_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11), DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4), ]) }); - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx); - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); + assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx); - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); + assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); + assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx); - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); + assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx); - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); - view.move_right(&MoveRight, cx); - view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); - assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + editor.move_right(&MoveRight, window, cx); + editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); + assert_selection_ranges( + "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", + editor, + cx, + ); - view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); - assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); + editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); + assert_selection_ranges( + "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", + editor, + cx, + ); - view.select_to_next_word_end(&SelectToNextWordEnd, cx); - assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); + editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx); + assert_selection_ranges( + "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", + editor, + cx, + ); }); } @@ -1788,57 +1842,57 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.0.into()), cx); + _ = editor.update(cx, |editor, window, cx| { + editor.set_wrap_width(Some(140.0.into()), cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), "use one::{\n two::three::\n four::five\n};" ); - view.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7) ]); }); - view.move_to_next_word_end(&MoveToNextWordEnd, cx); + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)] ); - view.move_to_next_word_end(&MoveToNextWordEnd, cx); + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)] ); - view.move_to_next_word_end(&MoveToNextWordEnd, cx); + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)] ); - view.move_to_next_word_end(&MoveToNextWordEnd, cx); + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)] ); - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)] ); - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)] ); }); @@ -1849,12 +1903,12 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let line_height = cx.editor(|editor, cx| { + let line_height = cx.editor(|editor, window, _| { editor .style() .unwrap() .text - .line_height_in_pixels(cx.rem_size()) + .line_height_in_pixels(window.rem_size()) }); cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height)); @@ -1870,7 +1924,9 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon .unindent(), ); - cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.update_editor(|editor, window, cx| { + editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx) + }); cx.assert_editor_state( &r#"one two @@ -1883,7 +1939,9 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon .unindent(), ); - cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.update_editor(|editor, window, cx| { + editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx) + }); cx.assert_editor_state( &r#"one two @@ -1896,7 +1954,9 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon .unindent(), ); - cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); + cx.update_editor(|editor, window, cx| { + editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx) + }); cx.assert_editor_state( &r#"one two @@ -1909,7 +1969,9 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon .unindent(), ); - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.update_editor(|editor, window, cx| { + editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx) + }); cx.assert_editor_state( &r#"one two @@ -1922,7 +1984,9 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon .unindent(), ); - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.update_editor(|editor, window, cx| { + editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx) + }); cx.assert_editor_state( &r#"one two @@ -1935,7 +1999,9 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon .unindent(), ); - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); + cx.update_editor(|editor, window, cx| { + editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx) + }); cx.assert_editor_state( &r#"ˇone two @@ -1953,12 +2019,12 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let line_height = cx.editor(|editor, cx| { + let line_height = cx.editor(|editor, window, _| { editor .style() .unwrap() .text - .line_height_in_pixels(cx.rem_size()) + .line_height_in_pixels(window.rem_size()) }); let window = cx.window; cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5))); @@ -1977,35 +2043,35 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { "#, ); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 0.) ); - editor.scroll_screen(&ScrollAmount::Page(1.), cx); + editor.scroll_screen(&ScrollAmount::Page(1.), window, cx); assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 3.) ); - editor.scroll_screen(&ScrollAmount::Page(1.), cx); + editor.scroll_screen(&ScrollAmount::Page(1.), window, cx); assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 6.) ); - editor.scroll_screen(&ScrollAmount::Page(-1.), cx); + editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx); assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 3.) ); - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); + editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx); assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 1.) ); - editor.scroll_screen(&ScrollAmount::Page(0.5), cx); + editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx); assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 3.) ); }); @@ -2016,13 +2082,13 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let line_height = cx.update_editor(|editor, cx| { + let line_height = cx.update_editor(|editor, window, cx| { editor.set_vertical_scroll_margin(2, cx); editor .style() .unwrap() .text - .line_height_in_pixels(cx.rem_size()) + .line_height_in_pixels(window.rem_size()) }); let window = cx.window; cx.simulate_window_resize(window, size(px(1000.), 6. * line_height)); @@ -2040,9 +2106,9 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { ten "#, ); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 0.0) ); }); @@ -2050,45 +2116,45 @@ async fn test_autoscroll(cx: &mut gpui::TestAppContext) { // Add a cursor below the visible area. Since both cursors cannot fit // on screen, the editor autoscrolls to reveal the newest cursor, and // allows the vertical scroll margin below that cursor. - cx.update_editor(|editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + cx.update_editor(|editor, window, cx| { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| { selections.select_ranges([ Point::new(0, 0)..Point::new(0, 0), Point::new(6, 0)..Point::new(6, 0), ]); }) }); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 3.0) ); }); // Move down. The editor cursor scrolls down to track the newest cursor. - cx.update_editor(|editor, cx| { - editor.move_down(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.move_down(&Default::default(), window, cx); }); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 4.0) ); }); // Add a cursor above the visible area. Since both cursors fit on screen, // the editor scrolls to show both. - cx.update_editor(|editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + cx.update_editor(|editor, window, cx| { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| { selections.select_ranges([ Point::new(1, 0)..Point::new(1, 0), Point::new(6, 0)..Point::new(6, 0), ]); }) }); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert_eq!( - editor.snapshot(cx).scroll_position(), + editor.snapshot(window, cx).scroll_position(), gpui::Point::new(0., 1.0) ); }); @@ -2099,12 +2165,12 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let line_height = cx.editor(|editor, cx| { + let line_height = cx.editor(|editor, window, _cx| { editor .style() .unwrap() .text - .line_height_in_pixels(cx.rem_size()) + .line_height_in_pixels(window.rem_size()) }); let window = cx.window; cx.simulate_window_resize(window, size(px(100.), 4. * line_height)); @@ -2124,7 +2190,9 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { .unindent(), ); - cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); + cx.update_editor(|editor, window, cx| { + editor.move_page_down(&MovePageDown::default(), window, cx) + }); cx.assert_editor_state( &r#" one @@ -2141,7 +2209,9 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { .unindent(), ); - cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); + cx.update_editor(|editor, window, cx| { + editor.move_page_down(&MovePageDown::default(), window, cx) + }); cx.assert_editor_state( &r#" one @@ -2158,7 +2228,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { .unindent(), ); - cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); + cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx)); cx.assert_editor_state( &r#" one @@ -2175,7 +2245,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { .unindent(), ); - cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); + cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx)); cx.assert_editor_state( &r#" ˇone @@ -2193,10 +2263,10 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { ); // Test select collapsing - cx.update_editor(|editor, cx| { - editor.move_page_down(&MovePageDown::default(), cx); - editor.move_page_down(&MovePageDown::default(), cx); - editor.move_page_down(&MovePageDown::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.move_page_down(&MovePageDown::default(), window, cx); + editor.move_page_down(&MovePageDown::default(), window, cx); + editor.move_page_down(&MovePageDown::default(), window, cx); }); cx.assert_editor_state( &r#" @@ -2220,8 +2290,8 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; cx.set_state("one «two threeˇ» four"); - cx.update_editor(|editor, cx| { - editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); + cx.update_editor(|editor, window, cx| { + editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx); assert_eq!(editor.text(cx), " four"); }); } @@ -2230,13 +2300,13 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { fn test_delete_to_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ // an empty selection - the preceding word fragment is deleted DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -2244,17 +2314,18 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12), ]) }); - view.delete_to_previous_word_start( + editor.delete_to_previous_word_start( &DeleteToPreviousWordStart { ignore_newlines: false, }, + window, cx, ); - assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four"); }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ // an empty selection - the following word fragment is deleted DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3), @@ -2262,13 +2333,14 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10), ]) }); - view.delete_to_next_word_end( + editor.delete_to_next_word_end( &DeleteToNextWordEnd { ignore_newlines: false, }, + window, cx, ); - assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our"); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our"); }); } @@ -2276,9 +2348,9 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); let del_to_prev_word_start = DeleteToPreviousWordStart { ignore_newlines: false, @@ -2287,24 +2359,24 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { ignore_newlines: true, }; - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1) ]) }); - view.delete_to_previous_word_start(&del_to_prev_word_start, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "one\n2\nthree\n"); - view.delete_to_previous_word_start(&del_to_prev_word_start, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "one\n2\nthree"); - view.delete_to_previous_word_start(&del_to_prev_word_start, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "one\n2\n"); - view.delete_to_previous_word_start(&del_to_prev_word_start, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "one\n2"); - view.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "one\n"); - view.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), ""); + editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n"); + editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree"); + editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n"); + editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2"); + editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n"); + editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), ""); }); } @@ -2312,9 +2384,9 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); let del_to_next_word_end = DeleteToNextWordEnd { ignore_newlines: false, @@ -2323,30 +2395,33 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { ignore_newlines: true, }; - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0) ]) }); - view.delete_to_next_word_end(&del_to_next_word_end, cx); + editor.delete_to_next_word_end(&del_to_next_word_end, window, cx); assert_eq!( - view.buffer.read(cx).read(cx).text(), + editor.buffer.read(cx).read(cx).text(), "one\n two\nthree\n four" ); - view.delete_to_next_word_end(&del_to_next_word_end, cx); + editor.delete_to_next_word_end(&del_to_next_word_end, window, cx); assert_eq!( - view.buffer.read(cx).read(cx).text(), + editor.buffer.read(cx).read(cx).text(), "\n two\nthree\n four" ); - view.delete_to_next_word_end(&del_to_next_word_end, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "two\nthree\n four"); - view.delete_to_next_word_end(&del_to_next_word_end, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "\nthree\n four"); - view.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "\n four"); - view.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), ""); + editor.delete_to_next_word_end(&del_to_next_word_end, window, cx); + assert_eq!( + editor.buffer.read(cx).read(cx).text(), + "two\nthree\n four" + ); + editor.delete_to_next_word_end(&del_to_next_word_end, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four"); + editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four"); + editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx); + assert_eq!(editor.buffer.read(cx).read(cx).text(), ""); }); } @@ -2354,13 +2429,13 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { fn test_newline(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer.clone(), cx) + build_editor(buffer.clone(), window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), @@ -2368,8 +2443,8 @@ fn test_newline(cx: &mut TestAppContext) { ]) }); - view.newline(&Newline, cx); - assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); + editor.newline(&Newline, window, cx); + assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n"); }); } @@ -2377,7 +2452,7 @@ fn test_newline(cx: &mut TestAppContext) { fn test_newline_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple( " a @@ -2392,8 +2467,8 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { .as_str(), cx, ); - let mut editor = build_editor(buffer.clone(), cx); - editor.change_selections(None, cx, |s| { + let mut editor = build_editor(buffer.clone(), window, cx); + editor.change_selections(None, window, cx, |s| { s.select_ranges([ Point::new(2, 4)..Point::new(2, 5), Point::new(5, 4)..Point::new(5, 5), @@ -2402,7 +2477,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { editor }); - _ = editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, window, cx| { // Edit the buffer directly, deleting ranges surrounding the editor's selections editor.buffer.update(cx, |buffer, cx| { buffer.edit( @@ -2431,7 +2506,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { ], ); - editor.newline(&Newline, cx); + editor.newline(&Newline, window, cx); assert_eq!( editor.text(cx), " @@ -2481,7 +2556,7 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) { ˇ);ˇ "}); - cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); + cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx)); cx.assert_editor_state(indoc! {" ˇ const a: A = ( @@ -2529,7 +2604,7 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) { ˇ);ˇ "}); - cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); + cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx)); cx.assert_editor_state(indoc! {" const a: A = ( ˇ @@ -2571,7 +2646,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) { // Fooˇ "}); - cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); cx.assert_editor_state(indoc! {" // Foo //ˇ @@ -2580,7 +2655,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) { cx.set_state(indoc! {" ˇ// Foo "}); - cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); cx.assert_editor_state(indoc! {" ˇ// Foo @@ -2594,7 +2669,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) { cx.set_state(indoc! {" // Fooˇ "}); - cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); cx.assert_editor_state(indoc! {" // Foo ˇ @@ -2605,14 +2680,16 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) { fn test_insert_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - let mut editor = build_editor(buffer.clone(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); + let mut editor = build_editor(buffer.clone(), window, cx); + editor.change_selections(None, window, cx, |s| { + s.select_ranges([3..4, 11..12, 19..20]) + }); editor }); - _ = editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, window, cx| { // Edit the buffer directly, deleting ranges surrounding the editor's selections editor.buffer.update(cx, |buffer, cx| { buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); @@ -2620,7 +2697,7 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) { }); assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); - editor.insert("Z", cx); + editor.insert("Z", window, cx); assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); // The selections are moved after the inserted characters @@ -2640,7 +2717,7 @@ async fn test_tab(cx: &mut gpui::TestAppContext) { ˇ🏀ˇ🏀ˇefg dˇ "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" ˇab ˇc ˇ🏀 ˇ🏀 ˇefg @@ -2651,7 +2728,7 @@ async fn test_tab(cx: &mut gpui::TestAppContext) { a «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" a «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» @@ -2687,7 +2764,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAp ˇ ) ); "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" ˇ const a: B = ( @@ -2708,7 +2785,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAp ˇ ) ); "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c( @@ -2743,7 +2820,7 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" fn a() { if b { @@ -2766,14 +2843,14 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { three four "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" «oneˇ» «twoˇ» three four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" «oneˇ» «twoˇ» three @@ -2786,14 +2863,14 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { t«hree ˇ» four "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" one two t«hree ˇ» four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" one two t«hree @@ -2806,7 +2883,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { ˇthree four "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" one two ˇthree @@ -2818,7 +2895,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { ˇ three four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" one two ˇthree @@ -2840,25 +2917,25 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { three four "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" \t«oneˇ» «twoˇ» three four "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" \t\t«oneˇ» «twoˇ» three four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" \t«oneˇ» «twoˇ» three four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" «oneˇ» «twoˇ» three @@ -2871,25 +2948,25 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { t«hree ˇ»four "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" one two \tt«hree ˇ»four "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" one two \t\tt«hree ˇ»four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" one two \tt«hree ˇ»four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" one two t«hree @@ -2902,19 +2979,19 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { ˇthree four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" one two ˇthree four "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); cx.assert_editor_state(indoc! {" one two \tˇthree four "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.update_editor(|e, window, cx| e.tab_prev(&TabPrev, window, cx)); cx.assert_editor_state(indoc! {" one two ˇthree @@ -2959,11 +3036,10 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { )); let toml_buffer = - cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx)); - let rust_buffer = cx.new_model(|cx| { - Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx) - }); - let multibuffer = cx.new_model(|cx| { + cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx)); + let rust_buffer = + cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx)); + let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(ReadWrite); multibuffer.push_excerpts( toml_buffer.clone(), @@ -2984,8 +3060,8 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { multibuffer }); - cx.add_window(|cx| { - let mut editor = build_editor(multibuffer, cx); + cx.add_window(|window, cx| { + let mut editor = build_editor(multibuffer, window, cx); assert_eq!( editor.text(cx), @@ -3005,10 +3081,11 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { «const c:ˇ» usize = 3; "}, + window, cx, ); - editor.tab(&Tab, cx); + editor.tab(&Tab, window, cx); assert_text_with_selections( &mut editor, indoc! {" @@ -3019,7 +3096,7 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { "}, cx, ); - editor.tab_prev(&TabPrev, cx); + editor.tab_prev(&TabPrev, window, cx); assert_text_with_selections( &mut editor, indoc! {" @@ -3048,7 +3125,7 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) { seven «ˇeight nine »ten "}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx)); cx.assert_editor_state(indoc! {" oˇe two three fouˇ five six @@ -3063,7 +3140,7 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) { ˇ ˇ ˇ three ˇ ˇ four "}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx)); cx.assert_editor_state(indoc! {" zero ˇone @@ -3072,13 +3149,13 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) { "}); // Test backspace with line_mode set to true - cx.update_editor(|e, _| e.selections.line_mode = true); + cx.update_editor(|e, _, _| e.selections.line_mode = true); cx.set_state(indoc! {" The ˇquick ˇbrown fox jumps over the lazy dog ˇThe qu«ick bˇ»rown"}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx)); cx.assert_editor_state(indoc! {" ˇfox jumps over the lazy dogˇ"}); @@ -3095,7 +3172,7 @@ async fn test_delete(cx: &mut gpui::TestAppContext) { seven «ˇeight nine »ten "}); - cx.update_editor(|e, cx| e.delete(&Delete, cx)); + cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx)); cx.assert_editor_state(indoc! {" onˇ two three fouˇ five six @@ -3103,13 +3180,13 @@ async fn test_delete(cx: &mut gpui::TestAppContext) { "}); // Test backspace with line_mode set to true - cx.update_editor(|e, _| e.selections.line_mode = true); + cx.update_editor(|e, _, _| e.selections.line_mode = true); cx.set_state(indoc! {" The ˇquick ˇbrown fox «ˇjum»ps over the lazy dog ˇThe qu«ick bˇ»rown"}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); + cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx)); cx.assert_editor_state("ˇthe lazy dogˇ"); } @@ -3117,22 +3194,22 @@ async fn test_delete(cx: &mut gpui::TestAppContext) { fn test_delete_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0), ]) }); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi"); + editor.delete_line(&DeleteLine, window, cx); + assert_eq!(editor.display_text(cx), "ghi"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1) @@ -3140,20 +3217,20 @@ fn test_delete_line(cx: &mut TestAppContext) { ); }); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1) ]) }); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi\n"); + editor.delete_line(&DeleteLine, window, cx); + assert_eq!(editor.display_text(cx), "ghi\n"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)] ); }); @@ -3163,9 +3240,9 @@ fn test_delete_line(cx: &mut TestAppContext) { fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); - cx.add_window(|cx| { + cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); - let mut editor = build_editor(buffer.clone(), cx); + let mut editor = build_editor(buffer.clone(), window, cx); let buffer = buffer.read(cx).as_singleton().unwrap(); assert_eq!( @@ -3174,7 +3251,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { ); // When on single line, replace newline at end by space - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); assert_eq!( editor.selections.ranges::(cx), @@ -3182,10 +3259,10 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { ); // When multiple lines are selected, remove newlines that are spanned by the selection - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) }); - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); assert_eq!( editor.selections.ranges::(cx), @@ -3193,7 +3270,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { ); // Undo should be transactional - editor.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); assert_eq!( editor.selections.ranges::(cx), @@ -3201,10 +3278,10 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { ); // When joining an empty line don't insert a space - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) }); - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); assert_eq!( editor.selections.ranges::(cx), @@ -3212,7 +3289,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { ); // We can remove trailing newlines - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); assert_eq!( editor.selections.ranges::(cx), @@ -3220,7 +3297,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { ); // We don't blow up on the last line - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); assert_eq!( editor.selections.ranges::(cx), @@ -3241,18 +3318,18 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { // We remove any leading spaces assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) }); - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); // We don't insert a space for a line containing only spaces - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); // We ignore any leading tabs - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); editor @@ -3263,12 +3340,12 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); - cx.add_window(|cx| { + cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); - let mut editor = build_editor(buffer.clone(), cx); + let mut editor = build_editor(buffer.clone(), window, cx); let buffer = buffer.read(cx).as_singleton().unwrap(); - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([ Point::new(0, 2)..Point::new(1, 1), Point::new(1, 2)..Point::new(1, 2), @@ -3276,7 +3353,7 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { ]) }); - editor.join_lines(&JoinLines, cx); + editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); assert_eq!( @@ -3321,8 +3398,8 @@ async fn test_join_lines_with_git_diff_base( executor.run_until_parked(); // Join lines - cx.update_editor(|editor, cx| { - editor.join_lines(&JoinLines, cx); + cx.update_editor(|editor, window, cx| { + editor.join_lines(&JoinLines, window, cx); }); executor.run_until_parked(); @@ -3335,8 +3412,8 @@ async fn test_join_lines_with_git_diff_base( .unindent(), ); // Join again - cx.update_editor(|editor, cx| { - editor.join_lines(&JoinLines, cx); + cx.update_editor(|editor, window, cx| { + editor.join_lines(&JoinLines, window, cx); }); executor.run_until_parked(); @@ -3360,12 +3437,12 @@ async fn test_custom_newlines_cause_no_false_positive_diffs( cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3"); executor.run_until_parked(); - cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); + cx.update_editor(|editor, window, cx| { + let snapshot = editor.snapshot(window, cx); assert_eq!( snapshot - .diff_map - .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot) + .buffer_snapshot + .diff_hunks_in_range(0..snapshot.buffer_snapshot.len()) .collect::>(), Vec::new(), "Should not have any diffs for files with custom newlines" @@ -3388,7 +3465,9 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { Y Xˇ» "}); - cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); + cx.update_editor(|e, window, cx| { + e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «x X @@ -3406,7 +3485,7 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { 2 1ˇ» "}); - cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); + cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx)); cx.assert_editor_state(indoc! {" «1 2 @@ -3427,7 +3506,9 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { bb a "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.update_editor(|e, window, cx| { + e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «ddddˇ» ccc @@ -3444,7 +3525,9 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { bb aaaaaˇ» "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.update_editor(|e, window, cx| { + e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «aaaaa bb @@ -3462,7 +3545,9 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { ˇ» "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.update_editor(|e, window, cx| { + e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx) + }); cx.assert_editor_state(indoc! {" « @@ -3478,7 +3563,9 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { aa«a bbˇ»b "}); - cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added_line"))); + cx.update_editor(|e, window, cx| { + e.manipulate_lines(window, cx, |lines| lines.push("added_line")) + }); cx.assert_editor_state(indoc! {" «aaa bbb @@ -3490,8 +3577,8 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { aa«a bbbˇ» "}); - cx.update_editor(|e, cx| { - e.manipulate_lines(cx, |lines| { + cx.update_editor(|e, window, cx| { + e.manipulate_lines(window, cx, |lines| { lines.pop(); }) }); @@ -3504,8 +3591,8 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { aa«a bbbˇ» "}); - cx.update_editor(|e, cx| { - e.manipulate_lines(cx, |lines| { + cx.update_editor(|e, window, cx| { + e.manipulate_lines(window, cx, |lines| { lines.drain(..); }) }); @@ -3527,7 +3614,9 @@ async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) { bb aaaˇ»aa "}); - cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx)); + cx.update_editor(|e, window, cx| { + e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «Aaaaa ccc @@ -3541,7 +3630,9 @@ async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) { bb aaaˇ»aa "}); - cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx)); + cx.update_editor(|e, window, cx| { + e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «Aaaaa ccc @@ -3557,7 +3648,9 @@ async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) { aaa«aaˇ» "}); - cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx)); + cx.update_editor(|e, window, cx| { + e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «aaaaa bbˇ» @@ -3577,7 +3670,9 @@ async fn test_unique_lines_single_selection(cx: &mut TestAppContext) { aAa Aaaˇ» "}); - cx.update_editor(|e, cx| e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, cx)); + cx.update_editor(|e, window, cx| { + e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «Aaa aAaˇ» @@ -3588,7 +3683,9 @@ async fn test_unique_lines_single_selection(cx: &mut TestAppContext) { aAa aaAˇ» "}); - cx.update_editor(|e, cx| e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, cx)); + cx.update_editor(|e, window, cx| { + e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «Aaaˇ» "}); @@ -3607,7 +3704,9 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { bb aaaˇ»aa "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.update_editor(|e, window, cx| { + e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «aaaaa bb @@ -3628,7 +3727,9 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { bb aaaˇ»aa "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.update_editor(|e, window, cx| { + e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx) + }); cx.assert_editor_state(indoc! {" «1 2 @@ -3650,7 +3751,9 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { bb«bb aaaˇ»aa "}); - cx.update_editor(|e, cx| e.manipulate_lines(cx, |lines| lines.push("added line"))); + cx.update_editor(|e, window, cx| { + e.manipulate_lines(window, cx, |lines| lines.push("added line")) + }); cx.assert_editor_state(indoc! {" «2 1 @@ -3669,8 +3772,8 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { bb«bb aaaˇ»aa "}); - cx.update_editor(|e, cx| { - e.manipulate_lines(cx, |lines| { + cx.update_editor(|e, window, cx| { + e.manipulate_lines(window, cx, |lines| { lines.pop(); }) }); @@ -3691,7 +3794,7 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.set_state(indoc! {" «hello worldˇ» "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx)); cx.assert_editor_state(indoc! {" «HELLO WORLDˇ» "}); @@ -3700,7 +3803,7 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.set_state(indoc! {" «HELLO WORLDˇ» "}); - cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx)); + cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx)); cx.assert_editor_state(indoc! {" «hello worldˇ» "}); @@ -3711,7 +3814,7 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { fox jumps over the lazy dogˇ» "}); - cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx)); + cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx)); cx.assert_editor_state(indoc! {" «The Quick Brown Fox Jumps Over @@ -3724,7 +3827,9 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { fox jumps over the lazy dogˇ» "}); - cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx)); + cx.update_editor(|e, window, cx| { + e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx) + }); cx.assert_editor_state(indoc! {" «TheQuickBrown FoxJumpsOver @@ -3738,7 +3843,7 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.set_state(indoc! {" ˇhello big beauˇtiful worldˇ "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx)); cx.assert_editor_state(indoc! {" «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ» "}); @@ -3749,7 +3854,7 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { foxˇ» jumps «overˇ» the «lazyˇ» dog "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx)); cx.assert_editor_state(indoc! {" «THEˇ» quick «BROWN FOXˇ» jumps «OVERˇ» @@ -3760,7 +3865,7 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.set_state(indoc! {" «tschüߡ» "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx)); cx.assert_editor_state(indoc! {" «TSCHÜSSˇ» "}); @@ -3769,7 +3874,9 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.set_state(indoc! {" aaa_bbbˇ "}); - cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); + cx.update_editor(|e, window, cx| { + e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx) + }); cx.assert_editor_state(indoc! {" «aaaBbbˇ» "}); @@ -3779,7 +3886,9 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.set_state(indoc! {" aaa_bˇbb bbˇb_ccc ˇccc_ddd "}); - cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); + cx.update_editor(|e, window, cx| { + e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx) + }); cx.assert_editor_state(indoc! {" «aaaBbbˇ» «bbbCccˇ» «cccDddˇ» "}); @@ -3787,7 +3896,9 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.set_state(indoc! {" «hElLo, WoRld!ˇ» "}); - cx.update_editor(|e, cx| e.convert_to_opposite_case(&ConvertToOppositeCase, cx)); + cx.update_editor(|e, window, cx| { + e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx) + }); cx.assert_editor_state(indoc! {" «HeLlO, wOrLD!ˇ» "}); @@ -3797,12 +3908,12 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { fn test_duplicate_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -3810,10 +3921,10 @@ fn test_duplicate_line(cx: &mut TestAppContext) { DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0), ]) }); - view.duplicate_line_down(&DuplicateLineDown, cx); - assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); + editor.duplicate_line_down(&DuplicateLineDown, window, cx); + assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), @@ -3823,21 +3934,21 @@ fn test_duplicate_line(cx: &mut TestAppContext) { ); }); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1), ]) }); - view.duplicate_line_down(&DuplicateLineDown, cx); - assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); + editor.duplicate_line_down(&DuplicateLineDown, window, cx); + assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1), DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1), @@ -3847,12 +3958,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) { // With `move_upwards` the selections stay in place, except for // the lines inserted above them - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -3860,10 +3971,10 @@ fn test_duplicate_line(cx: &mut TestAppContext) { DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0), ]) }); - view.duplicate_line_up(&DuplicateLineUp, cx); - assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); + editor.duplicate_line_up(&DuplicateLineUp, window, cx); + assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -3873,21 +3984,21 @@ fn test_duplicate_line(cx: &mut TestAppContext) { ); }); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1), ]) }); - view.duplicate_line_up(&DuplicateLineUp, cx); - assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); + editor.duplicate_line_up(&DuplicateLineUp, window, cx); + assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1), @@ -3895,21 +4006,21 @@ fn test_duplicate_line(cx: &mut TestAppContext) { ); }); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1), ]) }); - view.duplicate_selection(&DuplicateSelection, cx); - assert_eq!(view.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n"); + editor.duplicate_selection(&DuplicateSelection, window, cx); + assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n"); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1), @@ -3922,21 +4033,22 @@ fn test_duplicate_line(cx: &mut TestAppContext) { fn test_move_line_up_down(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.fold_creases( + _ = editor.update(cx, |editor, window, cx| { + editor.fold_creases( vec![ Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()), Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()), Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()), ], true, + window, cx, ); - view.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1), @@ -3945,17 +4057,17 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { ]) }); assert_eq!( - view.display_text(cx), + editor.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj" ); - view.move_line_up(&MoveLineUp, cx); + editor.move_line_up(&MoveLineUp, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff" ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1), @@ -3965,14 +4077,14 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.move_line_down(&MoveLineDown, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.move_line_down(&MoveLineDown, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj" ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1), @@ -3982,14 +4094,14 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.move_line_down(&MoveLineDown, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.move_line_down(&MoveLineDown, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj" ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1), DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1), @@ -3999,14 +4111,14 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.move_line_up(&MoveLineUp, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.move_line_up(&MoveLineUp, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff" ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1), @@ -4021,11 +4133,11 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = editor.update(cx, |editor, cx| { + _ = editor.update(cx, |editor, window, cx| { let snapshot = editor.buffer.read(cx).snapshot(cx); editor.insert_blocks( [BlockProperties { @@ -4038,10 +4150,10 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { Some(Autoscroll::fit()), cx, ); - editor.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) }); - editor.move_line_down(&MoveLineDown, cx); + editor.move_line_down(&MoveLineDown, window, cx); }); } @@ -4063,8 +4175,8 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) { ); // Create a four-line block that replaces three lines of text. - cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); + cx.update_editor(|editor, window, cx| { + let snapshot = editor.snapshot(window, cx); let snapshot = &snapshot.buffer_snapshot; let placement = BlockPlacement::Replace( snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)), @@ -4083,8 +4195,8 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) { }); // Move down so that the cursor touches the block. - cx.update_editor(|editor, cx| { - editor.move_down(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.move_down(&Default::default(), window, cx); }); cx.assert_editor_state( &" @@ -4099,8 +4211,8 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) { ); // Move down past the block. - cx.update_editor(|editor, cx| { - editor.move_down(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.move_down(&Default::default(), window, cx); }); cx.assert_editor_state( &" @@ -4119,89 +4231,89 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) { fn test_transpose(cx: &mut TestAppContext) { init_test(cx, |_| {}); - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); - editor.set_style(EditorStyle::default(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([1..1])); - editor.transpose(&Default::default(), cx); + _ = cx.add_window(|window, cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx); + editor.set_style(EditorStyle::default(), window, cx); + editor.change_selections(None, window, cx, |s| s.select_ranges([1..1])); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bac"); assert_eq!(editor.selections.ranges(cx), [2..2]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bca"); assert_eq!(editor.selections.ranges(cx), [3..3]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bac"); assert_eq!(editor.selections.ranges(cx), [3..3]); editor }); - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - editor.set_style(EditorStyle::default(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([3..3])); - editor.transpose(&Default::default(), cx); + _ = cx.add_window(|window, cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx); + editor.set_style(EditorStyle::default(), window, cx); + editor.change_selections(None, window, cx, |s| s.select_ranges([3..3])); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acb\nde"); assert_eq!(editor.selections.ranges(cx), [3..3]); - editor.change_selections(None, cx, |s| s.select_ranges([4..4])); - editor.transpose(&Default::default(), cx); + editor.change_selections(None, window, cx, |s| s.select_ranges([4..4])); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acbd\ne"); assert_eq!(editor.selections.ranges(cx), [5..5]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acbde\n"); assert_eq!(editor.selections.ranges(cx), [6..6]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acbd\ne"); assert_eq!(editor.selections.ranges(cx), [6..6]); editor }); - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - editor.set_style(EditorStyle::default(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); - editor.transpose(&Default::default(), cx); + _ = cx.add_window(|window, cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx); + editor.set_style(EditorStyle::default(), window, cx); + editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bacd\ne"); assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bcade\n"); assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bcda\ne"); assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bcade\n"); assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bcaed\n"); assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); editor }); - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); - editor.set_style(EditorStyle::default(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([4..4])); - editor.transpose(&Default::default(), cx); + _ = cx.add_window(|window, cx| { + let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx); + editor.set_style(EditorStyle::default(), window, cx); + editor.change_selections(None, window, cx, |s| s.select_ranges([4..4])); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "🏀🍐✋"); assert_eq!(editor.selections.ranges(cx), [8..8]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "🏀✋🍐"); assert_eq!(editor.selections.ranges(cx), [11..11]); - editor.transpose(&Default::default(), cx); + editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "🏀🍐✋"); assert_eq!(editor.selections.ranges(cx), [11..11]); @@ -4491,7 +4603,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { ) { cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); cx.set_state(unwrapped_text); - cx.update_editor(|e, cx| e.rewrap(&Rewrap, cx)); + cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx)); cx.assert_editor_state(wrapped_text); } } @@ -4503,22 +4615,22 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); - cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx)); cx.assert_editor_state("ˇtwo ˇfour ˇsix "); // Paste with three cursors. Each cursor pastes one slice of the clipboard text. cx.set_state("two ˇfour ˇsix ˇ"); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx)); cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ"); // Paste again but with only two cursors. Since the number of cursors doesn't // match the number of slices in the clipboard, the entire clipboard text // is pasted at each cursor. cx.set_state("ˇtwo one✅ four three six five ˇ"); - cx.update_editor(|e, cx| { - e.handle_input("( ", cx); - e.paste(&Paste, cx); - e.handle_input(") ", cx); + cx.update_editor(|e, window, cx| { + e.handle_input("( ", window, cx); + e.paste(&Paste, window, cx); + e.handle_input(") ", window, cx); }); cx.assert_editor_state( &([ @@ -4536,7 +4648,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { 1«2ˇ»3 4ˇ567 «8ˇ»9"}); - cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx)); cx.assert_editor_state(indoc! {" 1ˇ3 ˇ9"}); @@ -4547,7 +4659,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { 1ˇ3 9ˇ «oˇ»ne"}); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx)); cx.assert_editor_state(indoc! {" 12ˇ3 4567 @@ -4559,7 +4671,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { The quick brown fox juˇmps over the lazy dog"}); - cx.update_editor(|e, cx| e.copy(&Copy, cx)); + cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx)); assert_eq!( cx.read_from_clipboard() .and_then(|item| item.text().as_deref().map(str::to_string)), @@ -4572,7 +4684,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { Tˇhe quick brown «foˇ»x jumps over tˇhe lazy dog"}); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx)); cx.assert_editor_state(indoc! {" fox jumps over Tˇhe quick brown @@ -4603,7 +4715,7 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { )ˇ» ); "}); - cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c(), @@ -4612,7 +4724,7 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { "}); // Paste it at the same position. - cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c(), @@ -4630,7 +4742,7 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { c(), ); "}); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx)); cx.assert_editor_state(indoc! {" d( e, @@ -4651,7 +4763,7 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { ) ˇ»); "}); - cx.update_editor(|e, cx| e.cut(&Cut, cx)); + cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c(), @@ -4659,7 +4771,7 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { "}); // Paste it at the same position. - cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c(), @@ -4680,7 +4792,7 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { ) ); "}); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); + cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx)); cx.assert_editor_state(indoc! {" const a: B = ( c(), @@ -4700,14 +4812,14 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { fn test_select_all(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.select_all(&SelectAll, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.select_all(&SelectAll, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)] ); }); @@ -4717,12 +4829,12 @@ fn test_select_all(cx: &mut TestAppContext) { fn test_select_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -4730,9 +4842,9 @@ fn test_select_line(cx: &mut TestAppContext) { DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2), ]) }); - view.select_line(&SelectLine, cx); + editor.select_line(&SelectLine, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0), DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0), @@ -4740,10 +4852,10 @@ fn test_select_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.select_line(&SelectLine, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.select_line(&SelectLine, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0), DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5), @@ -4751,10 +4863,10 @@ fn test_select_line(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.select_line(&SelectLine, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.select_line(&SelectLine, window, cx); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)] ); }); @@ -4764,21 +4876,22 @@ fn test_select_line(cx: &mut TestAppContext) { fn test_split_selection_into_lines(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let view = cx.add_window(|cx| { + let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); - build_editor(buffer, cx) + build_editor(buffer, window, cx) }); - _ = view.update(cx, |view, cx| { - view.fold_creases( + _ = editor.update(cx, |editor, window, cx| { + editor.fold_creases( vec![ Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()), Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()), Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()), ], true, + window, cx, ); - view.change_selections(None, cx, |s| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -4786,17 +4899,20 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4), ]) }); - assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"); + assert_eq!( + editor.display_text(cx), + "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i" + ); }); - _ = view.update(cx, |view, cx| { - view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + _ = editor.update(cx, |editor, window, cx| { + editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -4806,19 +4922,19 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { ); }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1) ]) }); - view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); assert_eq!( - view.display_text(cx), + editor.display_text(cx), "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [ DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5), DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5), @@ -4849,8 +4965,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|editor, cx| { - editor.add_selection_above(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4862,8 +4978,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|editor, cx| { - editor.add_selection_above(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4875,8 +4991,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4888,8 +5004,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.undo_selection(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4901,8 +5017,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.redo_selection(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.redo_selection(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4914,8 +5030,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4927,8 +5043,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4950,8 +5066,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4963,8 +5079,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4976,8 +5092,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_above(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -4989,8 +5105,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_above(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -5012,8 +5128,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -5025,8 +5141,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( r#"a«bcˇ» @@ -5036,8 +5152,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { n«lmoˇ» "# )); - cx.update_editor(|view, cx| { - view.add_selection_above(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -5059,8 +5175,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_above(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -5072,8 +5188,8 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { "# )); - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); }); cx.assert_editor_state(indoc!( @@ -5093,25 +5209,25 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx)) .unwrap(); cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx)) .unwrap(); cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx)); cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx)); cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx)) .unwrap(); cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); } @@ -5123,7 +5239,7 @@ async fn test_select_all_matches(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches, cx)) + cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); } @@ -5141,7 +5257,7 @@ let foo = 2; let foo = ˇ2;"#, ); - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx)) .unwrap(); cx.assert_editor_state( r#"let foo = 2; @@ -5152,7 +5268,7 @@ let foo = «2ˇ»;"#, ); // noop for multiple selections with different contents - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) + cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx)) .unwrap(); cx.assert_editor_state( r#"let foo = 2; @@ -5202,29 +5318,29 @@ async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx)); cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx)); cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); } @@ -5242,7 +5358,7 @@ let foo = 2; let foo = ˇ2;"#, ); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state( r#"let foo = 2; @@ -5253,7 +5369,7 @@ let foo = «2ˇ»;"#, ); // noop for multiple selections with different contents - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state( r#"let foo = 2; @@ -5271,25 +5387,25 @@ async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContex let mut cx = EditorTestContext::new(cx).await; cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx)); cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx)); cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) + cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx)) .unwrap(); cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); } @@ -5312,23 +5428,23 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); editor - .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; - editor.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25), DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12), DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18), ]); }); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx); }); editor.update(cx, |editor, cx| { assert_text_with_selections( @@ -5344,8 +5460,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ); }); - editor.update(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + editor.update_in(cx, |editor, window, cx| { + editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx); }); editor.update(cx, |editor, cx| { assert_text_with_selections( @@ -5361,25 +5477,25 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ); }); - editor.update(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + editor.update_in(cx, |editor, window, cx| { + editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)] ); // Trying to expand the selected syntax node one more time has no effect. - editor.update(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + editor.update_in(cx, |editor, window, cx| { + editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx); }); assert_eq!( - editor.update(cx, |view, cx| view.selections.display_ranges(cx)), + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)] ); - editor.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + editor.update_in(cx, |editor, window, cx| { + editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx); }); editor.update(cx, |editor, cx| { assert_text_with_selections( @@ -5395,8 +5511,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ); }); - editor.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + editor.update_in(cx, |editor, window, cx| { + editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx); }); editor.update(cx, |editor, cx| { assert_text_with_selections( @@ -5412,8 +5528,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ); }); - editor.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + editor.update_in(cx, |editor, window, cx| { + editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx); }); editor.update(cx, |editor, cx| { assert_text_with_selections( @@ -5430,10 +5546,10 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { }); // Trying to shrink the selected syntax node one more time has no effect. - editor.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + editor.update_in(cx, |editor, window, cx| { + editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx); }); - editor.update(cx, |editor, cx| { + editor.update_in(cx, |editor, _, cx| { assert_text_with_selections( editor, indoc! {r#" @@ -5449,8 +5565,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { // Ensure that we keep expanding the selection if the larger selection starts or ends within // a fold. - editor.update(cx, |view, cx| { - view.fold_creases( + editor.update_in(cx, |editor, window, cx| { + editor.fold_creases( vec![ Crease::simple( Point::new(0, 21)..Point::new(0, 24), @@ -5462,9 +5578,10 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ), ], true, + window, cx, ); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx); }); editor.update(cx, |editor, cx| { assert_text_with_selections( @@ -5481,6 +5598,109 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_fold_function_bodies(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let base_text = r#" + impl A { + // this is an unstaged comment + + fn b() { + c(); + } + + // this is another unstaged comment + + fn d() { + // e + // f + } + } + + fn g() { + // h + } + "# + .unindent(); + + let text = r#" + ˇimpl A { + + fn b() { + c(); + } + + fn d() { + // e + // f + } + } + + fn g() { + // h + } + "# + .unindent(); + + let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await; + cx.set_state(&text); + cx.set_diff_base(&base_text); + cx.update_editor(|editor, window, cx| { + editor.expand_all_diff_hunks(&Default::default(), window, cx); + }); + + cx.assert_state_with_diff( + " + ˇimpl A { + - // this is an unstaged comment + + fn b() { + c(); + } + + - // this is another unstaged comment + - + fn d() { + // e + // f + } + } + + fn g() { + // h + } + " + .unindent(), + ); + + let expected_display_text = " + impl A { + // this is an unstaged comment + + fn b() { + ⋯ + } + + // this is another unstaged comment + + fn d() { + ⋯ + } + } + + fn g() { + ⋯ + } + " + .unindent(); + + cx.update_editor(|editor, window, cx| { + editor.fold_function_bodies(&FoldFunctionBodies, window, cx); + assert_eq!(editor.display_text(cx), expected_display_text); + }); +} + #[gpui::test] async fn test_autoindent(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -5522,16 +5742,16 @@ async fn test_autoindent(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); editor .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); - editor.newline(&Newline, cx); + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); + editor.newline(&Newline, window, cx); assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); assert_eq!( editor.selections.ranges(cx), @@ -5561,8 +5781,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|editor, cx| { - editor.autoindent(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.autoindent(&Default::default(), window, cx); }); cx.assert_editor_state(indoc! {" @@ -5593,7 +5813,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { "}], ); - let buffer = cx.update_editor(|editor, cx| { + let buffer = cx.update_editor(|editor, _, cx| { let buffer = editor.buffer().update(cx, |buffer, _| { buffer.all_buffers().iter().next().unwrap().clone() }); @@ -5602,13 +5822,13 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { }); cx.run_until_parked(); - cx.update_editor(|editor, cx| { - editor.select_all(&Default::default(), cx); - editor.autoindent(&Default::default(), cx) + cx.update_editor(|editor, window, cx| { + editor.select_all(&Default::default(), window, cx); + editor.autoindent(&Default::default(), window, cx) }); cx.run_until_parked(); - cx.update(|cx| { + cx.update(|_, cx| { pretty_assertions::assert_eq!( buffer.read(cx).text(), indoc! { " @@ -5703,10 +5923,10 @@ async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) { ); // autoclose multiple nested brackets at multiple cursors - cx.update_editor(|view, cx| { - view.handle_input("{", cx); - view.handle_input("{", cx); - view.handle_input("{", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("{", window, cx); + editor.handle_input("{", window, cx); + editor.handle_input("{", window, cx); }); cx.assert_editor_state( &" @@ -5718,8 +5938,8 @@ async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) { ); // insert a different closing bracket - cx.update_editor(|view, cx| { - view.handle_input(")", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input(")", window, cx); }); cx.assert_editor_state( &" @@ -5731,11 +5951,11 @@ async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) { ); // skip over the auto-closed brackets when typing a closing bracket - cx.update_editor(|view, cx| { - view.move_right(&MoveRight, cx); - view.handle_input("}", cx); - view.handle_input("}", cx); - view.handle_input("}", cx); + cx.update_editor(|editor, window, cx| { + editor.move_right(&MoveRight, window, cx); + editor.handle_input("}", window, cx); + editor.handle_input("}", window, cx); + editor.handle_input("}", window, cx); }); cx.assert_editor_state( &" @@ -5754,9 +5974,9 @@ async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) { " .unindent(), ); - cx.update_editor(|view, cx| { - view.handle_input("/", cx); - view.handle_input("*", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("/", window, cx); + editor.handle_input("*", window, cx); }); cx.assert_editor_state( &" @@ -5775,7 +5995,7 @@ async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) { " .unindent(), ); - cx.update_editor(|view, cx| view.handle_input("*", cx)); + cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx)); cx.assert_editor_state( &" /*ˇ */ @@ -5787,34 +6007,34 @@ async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) { // Don't autoclose if the next character isn't whitespace and isn't // listed in the language's "autoclose_before" section. cx.set_state("ˇa b"); - cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx)); cx.assert_editor_state("{ˇa b"); // Don't autoclose if `close` is false for the bracket pair cx.set_state("ˇ"); - cx.update_editor(|view, cx| view.handle_input("[", cx)); + cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx)); cx.assert_editor_state("[ˇ"); // Surround with brackets if text is selected cx.set_state("«aˇ» b"); - cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx)); cx.assert_editor_state("{«aˇ»} b"); // Autclose pair where the start and end characters are the same cx.set_state("aˇ"); - cx.update_editor(|view, cx| view.handle_input("\"", cx)); + cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx)); cx.assert_editor_state("a\"ˇ\""); - cx.update_editor(|view, cx| view.handle_input("\"", cx)); + cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx)); cx.assert_editor_state("a\"\"ˇ"); // Don't autoclose pair if autoclose is disabled cx.set_state("ˇ"); - cx.update_editor(|view, cx| view.handle_input("<", cx)); + cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx)); cx.assert_editor_state("<ˇ"); // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled cx.set_state("«aˇ» b"); - cx.update_editor(|view, cx| view.handle_input("<", cx)); + cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx)); cx.assert_editor_state("<«aˇ»> b"); } @@ -5875,11 +6095,11 @@ async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestA ); // ensure only matching closing brackets are skipped over - cx.update_editor(|view, cx| { - view.handle_input("}", cx); - view.move_left(&MoveLeft, cx); - view.handle_input(")", cx); - view.move_left(&MoveLeft, cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("}", window, cx); + editor.move_left(&MoveLeft, window, cx); + editor.handle_input(")", window, cx); + editor.move_left(&MoveLeft, window, cx); }); cx.assert_editor_state( &" @@ -5891,9 +6111,9 @@ async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestA ); // skip-over closing brackets at multiple cursors - cx.update_editor(|view, cx| { - view.handle_input(")", cx); - view.handle_input("}", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input(")", window, cx); + editor.handle_input("}", window, cx); }); cx.assert_editor_state( &" @@ -5905,10 +6125,10 @@ async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut gpui::TestA ); // ignore non-close brackets - cx.update_editor(|view, cx| { - view.handle_input("]", cx); - view.move_left(&MoveLeft, cx); - view.handle_input("]", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("]", window, cx); + editor.move_left(&MoveLeft, window, cx); + editor.handle_input("]", window, cx); }); cx.assert_editor_state( &" @@ -6019,8 +6239,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { ); // Precondition: different languages are active at different locations. - cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); + cx.update_editor(|editor, window, cx| { + let snapshot = editor.snapshot(window, cx); let cursors = editor.selections.ranges::(cx); let languages = cursors .iter() @@ -6033,9 +6253,9 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { }); // Angle brackets autoclose in HTML, but not JavaScript. - cx.update_editor(|editor, cx| { - editor.handle_input("<", cx); - editor.handle_input("a", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("<", window, cx); + editor.handle_input("a", window, cx); }); cx.assert_editor_state( &r#" @@ -6049,11 +6269,11 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { ); // Curly braces and parens autoclose in both HTML and JavaScript. - cx.update_editor(|editor, cx| { - editor.handle_input(" b=", cx); - editor.handle_input("{", cx); - editor.handle_input("c", cx); - editor.handle_input("(", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input(" b=", window, cx); + editor.handle_input("{", window, cx); + editor.handle_input("c", window, cx); + editor.handle_input("(", window, cx); }); cx.assert_editor_state( &r#" @@ -6067,10 +6287,10 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { ); // Brackets that were already autoclosed are skipped. - cx.update_editor(|editor, cx| { - editor.handle_input(")", cx); - editor.handle_input("d", cx); - editor.handle_input("}", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input(")", window, cx); + editor.handle_input("d", window, cx); + editor.handle_input("}", window, cx); }); cx.assert_editor_state( &r#" @@ -6082,8 +6302,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { "# .unindent(), ); - cx.update_editor(|editor, cx| { - editor.handle_input(">", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input(">", window, cx); }); cx.assert_editor_state( &r#" @@ -6108,8 +6328,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { .unindent(), ); - cx.update_editor(|editor, cx| { - editor.handle_input("<", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("<", window, cx); }); cx.assert_editor_state( &r#" @@ -6123,8 +6343,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { ); // When backspacing, the closing angle brackets are removed. - cx.update_editor(|editor, cx| { - editor.backspace(&Backspace, cx); + cx.update_editor(|editor, window, cx| { + editor.backspace(&Backspace, window, cx); }); cx.assert_editor_state( &r#" @@ -6138,9 +6358,9 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { ); // Block comments autoclose in JavaScript, but not HTML. - cx.update_editor(|editor, cx| { - editor.handle_input("/", cx); - editor.handle_input("*", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("/", window, cx); + editor.handle_input("*", window, cx); }); cx.assert_editor_state( &r#" @@ -6191,8 +6411,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { ); // Inserting a quotation mark. A closing quotation mark is automatically inserted. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("\"", window, cx); }); cx.assert_editor_state( &r#" @@ -6203,8 +6423,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { // Inserting another quotation mark. The cursor moves across the existing // automatically-inserted quotation mark. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("\"", window, cx); }); cx.assert_editor_state( &r#" @@ -6222,12 +6442,12 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { ); // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); - editor.handle_input(" ", cx); - editor.move_left(&Default::default(), cx); - editor.handle_input("\\", cx); - editor.handle_input("\"", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("\"", window, cx); + editor.handle_input(" ", window, cx); + editor.move_left(&Default::default(), window, cx); + editor.handle_input("\\", window, cx); + editor.handle_input("\"", window, cx); }); cx.assert_editor_state( &r#" @@ -6238,9 +6458,9 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { // Inserting a closing quotation mark at the position of an automatically-inserted quotation // mark. Nothing is inserted. - cx.update_editor(|editor, cx| { - editor.move_right(&Default::default(), cx); - editor.handle_input("\"", cx); + cx.update_editor(|editor, window, cx| { + editor.move_right(&Default::default(), window, cx); + editor.handle_input("\"", window, cx); }); cx.assert_editor_state( &r#" @@ -6287,14 +6507,15 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1), @@ -6302,11 +6523,11 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { ]) }); - view.handle_input("{", cx); - view.handle_input("{", cx); - view.handle_input("{", cx); + editor.handle_input("{", window, cx); + editor.handle_input("{", window, cx); + editor.handle_input("{", window, cx); assert_eq!( - view.text(cx), + editor.text(cx), " {{{a}}} {{{b}}} @@ -6315,7 +6536,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { .unindent() ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [ DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4), DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4), @@ -6323,11 +6544,11 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { ] ); - view.undo(&Undo, cx); - view.undo(&Undo, cx); - view.undo(&Undo, cx); + editor.undo(&Undo, window, cx); + editor.undo(&Undo, window, cx); + editor.undo(&Undo, window, cx); assert_eq!( - view.text(cx), + editor.text(cx), " a b @@ -6336,7 +6557,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { .unindent() ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1), @@ -6346,9 +6567,9 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { // Ensure inserting the first character of a multi-byte bracket pair // doesn't surround the selections with the bracket. - view.handle_input("/", cx); + editor.handle_input("/", window, cx); assert_eq!( - view.text(cx), + editor.text(cx), " / / @@ -6357,7 +6578,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { .unindent() ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1), @@ -6365,9 +6586,9 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { ] ); - view.undo(&Undo, cx); + editor.undo(&Undo, window, cx); assert_eq!( - view.text(cx), + editor.text(cx), " a b @@ -6376,7 +6597,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { .unindent() ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1), @@ -6386,9 +6607,9 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { // Ensure inserting the last character of a multi-byte bracket pair // doesn't surround the selections with the bracket. - view.handle_input("*", cx); + editor.handle_input("*", window, cx); assert_eq!( - view.text(cx), + editor.text(cx), " * * @@ -6397,7 +6618,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { .unindent() ); assert_eq!( - view.selections.display_ranges(cx), + editor.selections.display_ranges(cx), [ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1), @@ -6436,15 +6657,15 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); editor - .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_ranges([ Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), @@ -6452,9 +6673,9 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { ]) }); - editor.handle_input("{", cx); - editor.handle_input("{", cx); - editor.handle_input("_", cx); + editor.handle_input("{", window, cx); + editor.handle_input("{", window, cx); + editor.handle_input("_", window, cx); assert_eq!( editor.text(cx), " @@ -6473,8 +6694,8 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { ] ); - editor.backspace(&Default::default(), cx); - editor.backspace(&Default::default(), cx); + editor.backspace(&Default::default(), window, cx); + editor.backspace(&Default::default(), window, cx); assert_eq!( editor.text(cx), " @@ -6493,7 +6714,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { ] ); - editor.delete_to_previous_word_start(&Default::default(), cx); + editor.delete_to_previous_word_start(&Default::default(), window, cx); assert_eq!( editor.text(cx), " @@ -6570,9 +6791,9 @@ async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppC .unindent(), ); - cx.update_editor(|view, cx| { - view.backspace(&Default::default(), cx); - view.backspace(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.backspace(&Default::default(), window, cx); + editor.backspace(&Default::default(), window, cx); }); cx.assert_editor_state( @@ -6584,14 +6805,14 @@ async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppC .unindent(), ); - cx.update_editor(|view, cx| { - view.handle_input("{", cx); - view.handle_input("{", cx); - view.move_right(&MoveRight, cx); - view.move_right(&MoveRight, cx); - view.move_left(&MoveLeft, cx); - view.move_left(&MoveLeft, cx); - view.backspace(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("{", window, cx); + editor.handle_input("{", window, cx); + editor.move_right(&MoveRight, window, cx); + editor.move_right(&MoveRight, window, cx); + editor.move_left(&MoveLeft, window, cx); + editor.move_left(&MoveLeft, window, cx); + editor.backspace(&Default::default(), window, cx); }); cx.assert_editor_state( @@ -6603,8 +6824,8 @@ async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut gpui::TestAppC .unindent(), ); - cx.update_editor(|view, cx| { - view.backspace(&Default::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.backspace(&Default::default(), window, cx); }); cx.assert_editor_state( @@ -6626,59 +6847,59 @@ async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) { Some(tree_sitter_rust::LANGUAGE.into()), )); - let buffer = cx.new_model(|cx| Buffer::local("", cx).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); editor - .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; - editor.update(cx, |editor, cx| { + editor.update_in(cx, |editor, window, cx| { editor.set_auto_replace_emoji_shortcode(true); - editor.handle_input("Hello ", cx); - editor.handle_input(":wave", cx); + editor.handle_input("Hello ", window, cx); + editor.handle_input(":wave", window, cx); assert_eq!(editor.text(cx), "Hello :wave".unindent()); - editor.handle_input(":", cx); + editor.handle_input(":", window, cx); assert_eq!(editor.text(cx), "Hello 👋".unindent()); - editor.handle_input(" :smile", cx); + editor.handle_input(" :smile", window, cx); assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent()); - editor.handle_input(":", cx); + editor.handle_input(":", window, cx); assert_eq!(editor.text(cx), "Hello 👋 😄".unindent()); // Ensure shortcode gets replaced when it is part of a word that only consists of emojis - editor.handle_input(":wave", cx); + editor.handle_input(":wave", window, cx); assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent()); - editor.handle_input(":", cx); + editor.handle_input(":", window, cx); assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent()); - editor.handle_input(":1", cx); + editor.handle_input(":1", window, cx); assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent()); - editor.handle_input(":", cx); + editor.handle_input(":", window, cx); assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent()); // Ensure shortcode does not get replaced when it is part of a word - editor.handle_input(" Test:wave", cx); + editor.handle_input(" Test:wave", window, cx); assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent()); - editor.handle_input(":", cx); + editor.handle_input(":", window, cx); assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent()); editor.set_auto_replace_emoji_shortcode(false); // Ensure shortcode does not get replaced when auto replace is off - editor.handle_input(" :wave", cx); + editor.handle_input(" :wave", window, cx); assert_eq!( editor.text(cx), "Hello 👋 😄👋:1: Test:wave: :wave".unindent() ); - editor.handle_input(":", cx); + editor.handle_input(":", window, cx); assert_eq!( editor.text(cx), "Hello 👋 😄👋:1: Test:wave: :wave:".unindent() @@ -6698,16 +6919,16 @@ async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) { ); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); - _ = editor.update(cx, |editor, cx| { + _ = editor.update_in(cx, |editor, window, cx| { let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap(); editor - .insert_snippet(&insertion_ranges, snippet, cx) + .insert_snippet(&insertion_ranges, snippet, window, cx) .unwrap(); - fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { + fn assert(editor: &mut Editor, cx: &mut Context, marked_text: &str) { let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); assert_eq!(editor.text(cx), expected_text); assert_eq!(editor.selections.ranges::(cx), selection_ranges); @@ -6739,16 +6960,16 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { ); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); - editor.update(cx, |editor, cx| { + editor.update_in(cx, |editor, window, cx| { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); editor - .insert_snippet(&insertion_ranges, snippet, cx) + .insert_snippet(&insertion_ranges, snippet, window, cx) .unwrap(); - fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { + fn assert(editor: &mut Editor, cx: &mut Context, marked_text: &str) { let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); assert_eq!(editor.text(cx), expected_text); assert_eq!(editor.selections.ranges::(cx), selection_ranges); @@ -6765,7 +6986,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { ); // Can't move earlier than the first tab stop - assert!(!editor.move_to_prev_snippet_tabstop(cx)); + assert!(!editor.move_to_prev_snippet_tabstop(window, cx)); assert( editor, cx, @@ -6776,7 +6997,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { "}, ); - assert!(editor.move_to_next_snippet_tabstop(cx)); + assert!(editor.move_to_next_snippet_tabstop(window, cx)); assert( editor, cx, @@ -6787,7 +7008,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { "}, ); - editor.move_to_prev_snippet_tabstop(cx); + editor.move_to_prev_snippet_tabstop(window, cx); assert( editor, cx, @@ -6798,7 +7019,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { "}, ); - assert!(editor.move_to_next_snippet_tabstop(cx)); + assert!(editor.move_to_next_snippet_tabstop(window, cx)); assert( editor, cx, @@ -6808,7 +7029,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { a.f(one, «two», three) b "}, ); - assert!(editor.move_to_next_snippet_tabstop(cx)); + assert!(editor.move_to_next_snippet_tabstop(window, cx)); assert( editor, cx, @@ -6820,7 +7041,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { ); // As soon as the last tab stop is reached, snippet state is gone - editor.move_to_prev_snippet_tabstop(cx); + editor.move_to_prev_snippet_tabstop(window, cx); assert( editor, cx, @@ -6860,17 +7081,22 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = - cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx)); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), buffer, window, cx) + }); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("one\ntwo\nthree\n", window, cx) + }); assert!(cx.read(|cx| editor.is_dirty(cx))); cx.executor().start_waiting(); let fake_server = fake_servers.next().await.unwrap(); let save = editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); fake_server .handle_request::(move |params, _| async move { @@ -6895,7 +7121,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { ); assert!(!cx.read(|cx| editor.is_dirty(cx))); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("one\ntwo\nthree\n", window, cx) + }); assert!(cx.read(|cx| editor.is_dirty(cx))); // Ensure we can still save even if formatting hangs. @@ -6908,7 +7136,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { unreachable!() }); let save = editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); cx.executor().start_waiting(); @@ -6921,7 +7151,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { // For non-dirty buffer, no formatting request should be sent let save = editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); let _pending_format_request = fake_server .handle_request::(move |_, _| async move { @@ -6942,10 +7174,14 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { ); }); - editor.update(cx, |editor, cx| editor.set_text("somehting_new\n", cx)); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("somehting_new\n", window, cx) + }); assert!(cx.read(|cx| editor.is_dirty(cx))); let save = editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); fake_server .handle_request::(move |params, _| async move { @@ -6996,7 +7232,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/a".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); @@ -7038,7 +7274,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let multi_buffer = cx.new_model(|cx| { + let multi_buffer = cx.new(|cx| { let mut multi_buffer = MultiBuffer::new(ReadWrite); multi_buffer.push_excerpts( buffer_1.clone(), @@ -7096,26 +7332,29 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) { ); multi_buffer }); - let multi_buffer_editor = cx.new_view(|cx| { + let multi_buffer_editor = cx.new_window_entity(|window, cx| { Editor::new( EditorMode::Full, multi_buffer, Some(project.clone()), true, + window, cx, ) }); - multi_buffer_editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::Next), cx, |s| s.select_ranges(Some(1..2))); - editor.insert("|one|two|three|", cx); + multi_buffer_editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { + s.select_ranges(Some(1..2)) + }); + editor.insert("|one|two|three|", window, cx); }); assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx))); - multi_buffer_editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::Next), cx, |s| { + multi_buffer_editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { s.select_ranges(Some(60..70)) }); - editor.insert("|four|five|six|", cx); + editor.insert("|four|five|six|", window, cx); }); assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx))); @@ -7146,7 +7385,9 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) { cx.executor().start_waiting(); let save = multi_buffer_editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); let fake_server = fake_servers.next().await.unwrap(); @@ -7214,17 +7455,22 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = - cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx)); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), buffer, window, cx) + }); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("one\ntwo\nthree\n", window, cx) + }); assert!(cx.read(|cx| editor.is_dirty(cx))); cx.executor().start_waiting(); let fake_server = fake_servers.next().await.unwrap(); let save = editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); fake_server .handle_request::(move |params, _| async move { @@ -7248,7 +7494,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { ); assert!(!cx.read(|cx| editor.is_dirty(cx))); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("one\ntwo\nthree\n", window, cx) + }); assert!(cx.read(|cx| editor.is_dirty(cx))); // Ensure we can still save even if formatting hangs. @@ -7263,7 +7511,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { }, ); let save = editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); cx.executor().start_waiting(); @@ -7276,7 +7526,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { // For non-dirty buffer, no formatting request should be sent let save = editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); let _pending_format_request = fake_server .handle_request::(move |_, _| async move { @@ -7297,10 +7549,14 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { ); }); - editor.update(cx, |editor, cx| editor.set_text("somehting_new\n", cx)); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("somehting_new\n", window, cx) + }); assert!(cx.read(|cx| editor.is_dirty(cx))); let save = editor - .update(cx, |editor, cx| editor.save(true, project.clone(), cx)) + .update_in(cx, |editor, window, cx| { + editor.save(true, project.clone(), window, cx) + }) .unwrap(); fake_server .handle_request::(move |params, _| async move { @@ -7366,20 +7622,24 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = - cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx)); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), buffer, window, cx) + }); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("one\ntwo\nthree\n", window, cx) + }); cx.executor().start_waiting(); let fake_server = fake_servers.next().await.unwrap(); let format = editor - .update(cx, |editor, cx| { + .update_in(cx, |editor, window, cx| { editor.perform_format( project.clone(), FormatTrigger::Manual, FormatTarget::Buffers, + window, cx, ) }) @@ -7405,7 +7665,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { "one, two\nthree\n" ); - editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("one\ntwo\nthree\n", window, cx) + }); // Ensure we don't lock if formatting hangs. fake_server.handle_request::(move |params, _| async move { assert_eq!( @@ -7416,8 +7678,14 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { unreachable!() }); let format = editor - .update(cx, |editor, cx| { - editor.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffers, cx) + .update_in(cx, |editor, window, cx| { + editor.perform_format( + project, + FormatTrigger::Manual, + FormatTarget::Buffers, + window, + cx, + ) }) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); @@ -7462,13 +7730,13 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { // Submit a format request. let format_1 = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) + .update_editor(|editor, window, cx| editor.format(&Format, window, cx)) .unwrap(); cx.executor().run_until_parked(); // Submit a second format request. let format_2 = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) + .update_editor(|editor, window, cx| editor.format(&Format, window, cx)) .unwrap(); cx.executor().run_until_parked(); @@ -7514,7 +7782,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) // Submit a format request. let format = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) + .update_editor(|editor, window, cx| editor.format(&Format, window, cx)) .unwrap(); // Record which buffer changes have been sent to the language server @@ -7699,8 +7967,8 @@ async fn test_handle_input_for_show_signature_help_auto_signature_help_true( .unindent(), ); - cx.update_editor(|view, cx| { - view.handle_input("(", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("(", window, cx); }); cx.assert_editor_state( &" @@ -7735,7 +8003,7 @@ async fn test_handle_input_for_show_signature_help_auto_signature_help_true( cx.condition(|editor, _| editor.signature_help_state.is_shown()) .await; - cx.editor(|editor, _| { + cx.editor(|editor, _, _| { let signature_help_state = editor.signature_help_state.popover().cloned(); assert!(signature_help_state.is_some()); let ParsedMarkdown { @@ -7841,8 +8109,8 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui: "# .unindent(), ); - cx.update_editor(|view, cx| { - view.handle_input("(", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("(", window, cx); }); cx.assert_editor_state( &" @@ -7852,7 +8120,7 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui: " .unindent(), ); - cx.editor(|editor, _| { + cx.editor(|editor, _, _| { assert!(editor.signature_help_state.task().is_none()); }); @@ -7877,7 +8145,7 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui: }; // Ensure that signature_help is called when enabled afte edits - cx.update(|cx| { + cx.update(|_, cx| { cx.update_global::(|settings, cx| { settings.update_user_settings::(cx, |settings| { settings.auto_signature_help = Some(false); @@ -7893,8 +8161,8 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui: "# .unindent(), ); - cx.update_editor(|view, cx| { - view.handle_input("(", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("(", window, cx); }); cx.assert_editor_state( &" @@ -7907,7 +8175,7 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui: handle_signature_help_request(&mut cx, mocked_response.clone()).await; cx.condition(|editor, _| editor.signature_help_state.is_shown()) .await; - cx.update_editor(|editor, _| { + cx.update_editor(|editor, _, _| { let signature_help_state = editor.signature_help_state.popover().cloned(); assert!(signature_help_state.is_some()); let ParsedMarkdown { @@ -7919,7 +8187,7 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui: }); // Ensure that signature_help is called when auto signature help override is enabled - cx.update(|cx| { + cx.update(|_, cx| { cx.update_global::(|settings, cx| { settings.update_user_settings::(cx, |settings| { settings.auto_signature_help = Some(true); @@ -7935,8 +8203,8 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui: "# .unindent(), ); - cx.update_editor(|view, cx| { - view.handle_input("(", cx); + cx.update_editor(|editor, window, cx| { + editor.handle_input("(", window, cx); }); cx.assert_editor_state( &" @@ -7949,7 +8217,7 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui: handle_signature_help_request(&mut cx, mocked_response).await; cx.condition(|editor, _| editor.signature_help_state.is_shown()) .await; - cx.editor(|editor, _| { + cx.editor(|editor, _, _| { let signature_help_state = editor.signature_help_state.popover().cloned(); assert!(signature_help_state.is_some()); let ParsedMarkdown { @@ -7983,8 +8251,8 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { .await; // A test that directly calls `show_signature_help` - cx.update_editor(|editor, cx| { - editor.show_signature_help(&ShowSignatureHelp, cx); + cx.update_editor(|editor, window, cx| { + editor.show_signature_help(&ShowSignatureHelp, window, cx); }); let mocked_response = lsp::SignatureHelp { @@ -8011,7 +8279,7 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { cx.condition(|editor, _| editor.signature_help_state.is_shown()) .await; - cx.editor(|editor, _| { + cx.editor(|editor, _, _| { let signature_help_state = editor.signature_help_state.popover().cloned(); assert!(signature_help_state.is_some()); let ParsedMarkdown { @@ -8030,8 +8298,8 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { fn sample(param1: u8, param2: u8) {} "}); - cx.update_editor(|editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([0..0])); + cx.update_editor(|editor, window, cx| { + editor.change_selections(None, window, cx, |s| s.select_ranges([0..0])); }); let mocked_response = lsp::SignatureHelp { @@ -8044,7 +8312,7 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { cx.condition(|editor, _| !editor.signature_help_state.is_shown()) .await; - cx.editor(|editor, _| { + cx.editor(|editor, _, _| { assert!(!editor.signature_help_state.is_shown()); }); @@ -8079,7 +8347,7 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { handle_signature_help_request(&mut cx, mocked_response.clone()).await; cx.condition(|editor, _| editor.signature_help_state.is_shown()) .await; - cx.editor(|editor, _| { + cx.editor(|editor, _, _| { assert!(editor.signature_help_state.is_shown()); }); @@ -8117,8 +8385,8 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { // When selecting a range, the popover is gone. // Avoid using `cx.set_state` to not actually edit the document, just change its selections. - cx.update_editor(|editor, cx| { - editor.change_selections(None, cx, |s| { + cx.update_editor(|editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19))); }) }); @@ -8129,13 +8397,13 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { fn sample(param1: u8, param2: u8) {} "}); - cx.editor(|editor, _| { + cx.editor(|editor, _, _| { assert!(!editor.signature_help_state.is_shown()); }); // When unselecting again, the popover is back if within the brackets. - cx.update_editor(|editor, cx| { - editor.change_selections(None, cx, |s| { + cx.update_editor(|editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); }) }); @@ -8149,13 +8417,13 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { handle_signature_help_request(&mut cx, mocked_response).await; cx.condition(|editor, _| editor.signature_help_state.is_shown()) .await; - cx.editor(|editor, _| { + cx.editor(|editor, _, _| { assert!(editor.signature_help_state.is_shown()); }); // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape. - cx.update_editor(|editor, cx| { - editor.change_selections(None, cx, |s| { + cx.update_editor(|editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0))); s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); }) @@ -8190,13 +8458,13 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { handle_signature_help_request(&mut cx, mocked_response.clone()).await; cx.condition(|editor, _| editor.signature_help_state.is_shown()) .await; - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, _, cx| { editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape); }); cx.condition(|editor, _| !editor.signature_help_state.is_shown()) .await; - cx.update_editor(|editor, cx| { - editor.change_selections(None, cx, |s| { + cx.update_editor(|editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19))); }) }); @@ -8207,8 +8475,8 @@ async fn test_signature_help(cx: &mut gpui::TestAppContext) { fn sample(param1: u8, param2: u8) {} "}); - cx.update_editor(|editor, cx| { - editor.change_selections(None, cx, |s| { + cx.update_editor(|editor, window, cx| { + editor.change_selections(None, window, cx, |s| { s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); }) }); @@ -8279,25 +8547,25 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { active_parameter: None, }, ); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { assert!( !editor.signature_help_state.is_shown(), "No signature help was called for" ); - editor.show_signature_help(&ShowSignatureHelp, cx); + editor.show_signature_help(&ShowSignatureHelp, window, cx); }); cx.run_until_parked(); - cx.update_editor(|editor, _| { + cx.update_editor(|editor, _, _| { assert!( !editor.signature_help_state.is_shown(), "No signature help should be shown when completions menu is open" ); }); - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor.context_menu_next(&Default::default(), cx); + let apply_additional_edits = cx.update_editor(|editor, window, cx| { + editor.context_menu_next(&Default::default(), window, cx); editor - .confirm_completion(&ConfirmCompletion::default(), cx) + .confirm_completion(&ConfirmCompletion::default(), window, cx) .unwrap() }); cx.assert_editor_state(indoc! {" @@ -8345,9 +8613,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { additional edit "}); cx.simulate_keystroke(" "); - assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none())); + assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none())); cx.simulate_keystroke("s"); - assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none())); + assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none())); cx.assert_editor_state(indoc! {" one.second_completion @@ -8389,9 +8657,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { .await; assert_eq!(counter.load(atomic::Ordering::Acquire), 3); - let apply_additional_edits = cx.update_editor(|editor, cx| { + let apply_additional_edits = cx.update_editor(|editor, window, cx| { editor - .confirm_completion(&ConfirmCompletion::default(), cx) + .confirm_completion(&ConfirmCompletion::default(), window, cx) .unwrap() }); cx.assert_editor_state(indoc! {" @@ -8408,14 +8676,14 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { }); cx.set_state("editorˇ"); cx.simulate_keystroke("."); - assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none())); + assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none())); cx.simulate_keystroke("c"); cx.simulate_keystroke("l"); cx.simulate_keystroke("o"); cx.assert_editor_state("editor.cloˇ"); - assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none())); - cx.update_editor(|editor, cx| { - editor.show_completions(&ShowCompletions { trigger: None }, cx); + assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none())); + cx.update_editor(|editor, window, cx| { + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); handle_completion_request( &mut cx, @@ -8428,9 +8696,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { .await; assert_eq!(counter.load(atomic::Ordering::Acquire), 4); - let apply_additional_edits = cx.update_editor(|editor, cx| { + let apply_additional_edits = cx.update_editor(|editor, window, cx| { editor - .confirm_completion(&ConfirmCompletion::default(), cx) + .confirm_completion(&ConfirmCompletion::default(), window, cx) .unwrap() }); cx.assert_editor_state("editor.closeˇ"); @@ -8439,37 +8707,278 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) { +async fn test_multiline_completion(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/a", + json!({ + "main.ts": "a", + }), ) .await; - cx.lsp - .handle_request::(move |_, _| async move { + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let typescript_language = Arc::new(Language::new( + LanguageConfig { + name: "TypeScript".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..LanguageMatcher::default() + }, + line_comments: vec!["// ".into()], + ..LanguageConfig::default() + }, + Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), + )); + language_registry.add(typescript_language.clone()); + let mut fake_servers = language_registry.register_fake_lsp( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..lsp::CompletionOptions::default() + }), + signature_help_provider: Some(lsp::SignatureHelpOptions::default()), + ..lsp::ServerCapabilities::default() + }, + // Emulate vtsls label generation + label_for_completion: Some(Box::new(|item, _| { + let text = if let Some(description) = item + .label_details + .as_ref() + .and_then(|label_details| label_details.description.as_ref()) + { + format!("{} {}", item.label, description) + } else if let Some(detail) = &item.detail { + format!("{} {}", item.label, detail) + } else { + item.label.clone() + }; + let len = text.len(); + Some(language::CodeLabel { + text, + runs: Vec::new(), + filter_range: 0..len, + }) + })), + ..FakeLspAdapter::default() + }, + ); + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let worktree_id = workspace + .update(cx, |workspace, _window, cx| { + workspace.project().update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }) + .unwrap(); + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp("/a/main.ts", cx) + }) + .await + .unwrap(); + let editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_path((worktree_id, "main.ts"), None, true, window, cx) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + let fake_server = fake_servers.next().await.unwrap(); + + let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,"; + let multiline_label_2 = "a\nb\nc\n"; + let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}"; + let multiline_description = "d\ne\nf\n"; + let multiline_detail_2 = "g\nh\ni\n"; + + let mut completion_handle = + fake_server.handle_request::(move |params, _| async move { Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { - label: "first".into(), - ..Default::default() + label: multiline_label.to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + end: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + }, + new_text: "new_text_1".to_string(), + })), + ..lsp::CompletionItem::default() }, lsp::CompletionItem { - label: "last".into(), - ..Default::default() + label: "single line label 1".to_string(), + detail: Some(multiline_detail.to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + end: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + }, + new_text: "new_text_2".to_string(), + })), + ..lsp::CompletionItem::default() + }, + lsp::CompletionItem { + label: "single line label 2".to_string(), + label_details: Some(lsp::CompletionItemLabelDetails { + description: Some(multiline_description.to_string()), + detail: None, + }), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + end: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + }, + new_text: "new_text_2".to_string(), + })), + ..lsp::CompletionItem::default() + }, + lsp::CompletionItem { + label: multiline_label_2.to_string(), + detail: Some(multiline_detail_2.to_string()), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + end: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + }, + new_text: "new_text_3".to_string(), + })), + ..lsp::CompletionItem::default() + }, + lsp::CompletionItem { + label: "Label with many spaces and \t but without newlines".to_string(), + detail: Some( + "Details with many spaces and \t but without newlines".to_string(), + ), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + end: lsp::Position { + line: params.text_document_position.position.line, + character: params.text_document_position.position.character, + }, + }, + new_text: "new_text_4".to_string(), + })), + ..lsp::CompletionItem::default() }, ]))) }); - cx.set_state("variableˇ"); - cx.simulate_keystroke("."); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _| { + editor.update_in(cx, |editor, window, cx| { + cx.focus_self(window); + editor.move_to_end(&MoveToEnd, window, cx); + editor.handle_input(".", window, cx); + }); + cx.run_until_parked(); + completion_handle.next().await.unwrap(); + + editor.update(cx, |editor, _| { + assert!(editor.context_menu_visible()); + if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() + { + let completion_labels = menu + .completions + .borrow() + .iter() + .map(|c| c.label.text.clone()) + .collect::>(); + assert_eq!( + completion_labels, + &[ + "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,", + "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}", + "single line label 2 d e f ", + "a b c g h i ", + "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines", + ], + "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.", + ); + + for completion in menu + .completions + .borrow() + .iter() { + assert_eq!( + completion.label.filter_range, + 0..completion.label.text.len(), + "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}" + ); + } + + } else { + panic!("expected completion menu to be open"); + } + }); +} + +#[gpui::test] +async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + cx.lsp + .handle_request::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "first".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "last".into(), + ..Default::default() + }, + ]))) + }); + cx.set_state("variableˇ"); + cx.simulate_keystroke("."); + cx.executor().run_until_parked(); + + cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!(completion_menu_entries(&menu), &["first", "last"]); @@ -8478,8 +8987,8 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) { } }); - cx.update_editor(|editor, cx| { - editor.move_page_down(&MovePageDown::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.move_page_down(&MovePageDown::default(), window, cx); if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert!( @@ -8491,8 +9000,8 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) { } }); - cx.update_editor(|editor, cx| { - editor.move_page_up(&MovePageUp::default(), cx); + cx.update_editor(|editor, window, cx| { + editor.move_page_up(&MovePageUp::default(), window, cx); if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert!( @@ -8551,17 +9060,18 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) { }); cx.set_state("rˇ"); cx.executor().run_until_parked(); - cx.update_editor(|editor, cx| { + cx.update_editor(|editor, window, cx| { editor.show_completions( &ShowCompletions { trigger: Some("r".into()), }, + window, cx, ); }); cx.executor().run_until_parked(); - cx.update_editor(|editor, _| { + cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( @@ -8693,7 +9203,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8705,7 +9215,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { // The comment prefix is inserted at the same column for every line in a // selection. - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8724,7 +9234,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8743,7 +9253,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8762,7 +9272,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8781,7 +9291,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8819,7 +9329,7 @@ async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8830,7 +9340,7 @@ async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) { "}); // The comment prefix is inserted at the beginning of each line - cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8849,7 +9359,7 @@ async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8868,7 +9378,7 @@ async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8887,7 +9397,7 @@ async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8906,7 +9416,7 @@ async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) { } "}); - cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx)); cx.assert_editor_state(indoc! {" fn a() { @@ -8949,8 +9459,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) cat(); }" )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(toggle_comments, window, cx); }); cx.assert_editor_state(indoc!( "fn a() { @@ -8966,8 +9476,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) cat(); }" )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(toggle_comments, window, cx); }); cx.assert_editor_state(indoc!( "fn a() { @@ -8983,8 +9493,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) cat(); }" )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(toggle_comments, window, cx); }); cx.assert_editor_state(indoc!( "fn a() { @@ -9000,8 +9510,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) cat(); }" )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(toggle_comments, window, cx); }); cx.assert_editor_state(indoc!( "fn a() { @@ -9019,8 +9529,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) cat(); }" )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(toggle_comments, window, cx); }); cx.assert_editor_state(indoc!( "fn a() { @@ -9038,8 +9548,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) cat(); }" )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(toggle_comments, window, cx); }); cx.assert_editor_state(indoc!( "fn a() { @@ -9098,7 +9608,9 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { "# .unindent(), ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(&ToggleComments::default(), window, cx) + }); cx.assert_editor_state( &r#" @@ -9107,7 +9619,9 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { "# .unindent(), ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(&ToggleComments::default(), window, cx) + }); cx.assert_editor_state( &r#"

A

ˇ @@ -9129,7 +9643,9 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { .unindent(), ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); + cx.update_editor(|editor, window, cx| { + editor.toggle_comments(&ToggleComments::default(), window, cx) + }); cx.assert_editor_state( &r#" - `elm-format`, `elm-review` and `elm` need to be installed and made available in the environment or configured in the settings. See the [full list of server settings here](https://github.com/elm-tooling/elm-language-server?tab=readme-ov-file#server-settings). + +## Known Issues + +There is an [upstream issue](https://github.com/elm-tooling/elm-language-server/issues/1311) with `elm-language-server` incorrectly supporting `linked_edits`. It is recommend you disable that feature in your Zed settings.json with: + +``` + "languages": { + "Elm": { + "linked_edits": false + } + } +``` diff --git a/docs/src/languages/yaml.md b/docs/src/languages/yaml.md index 7b840d08252ca4..971f4742145da5 100644 --- a/docs/src/languages/yaml.md +++ b/docs/src/languages/yaml.md @@ -30,6 +30,39 @@ You can configure various [yaml-language-server settings](https://github.com/red Note, settings keys must be nested, so `yaml.keyOrdering` becomes `{"yaml": { "keyOrdering": true }}`. +## Formatting + +By default Zed will use prettier for formatting YAML files. + +### Prettier Formatting + +You can customize the formatting behavior of Prettier. For example to use single-quotes in yaml files add the following to a `.prettierrc`: + +```json +{ + "overrides": [ + { + "files": ["*.yaml", "*.yml"] + "options": { + "singleQuote": false + } + } + ] +} +``` + +### yaml-language-server Formatting + +To use `yaml-language-server` instead of Prettier for YAML formatting, add the following to your Zed settings.json: + +```json + "languages": { + "YAML": { + "formatter": "language_server" + } + } +``` + ## Schemas By default yaml-language-server will attempt to determine the correct schema for a given yaml file and retrieve the appropriate JSON Schema from [Json Schema Store](https://schemastore.org/). diff --git a/docs/src/linux.md b/docs/src/linux.md index 6e62300ec82e60..98f432f5093b5d 100644 --- a/docs/src/linux.md +++ b/docs/src/linux.md @@ -115,7 +115,7 @@ If you are using Mesa, and want more control over which GPU is selected you can If you are using `amdvlk` you may find that zed only opens when run with `sudo $(which zed)`. To fix this, remove the `amdvlk` and `lib32-amdvlk` packages and install mesa/vulkan instead. ([#14141](https://github.com/zed-industries/zed/issues/14141). -If you have a discrete GPU and you are using [PRIME](https://wiki.archlinux.org/title/PRIME) you may be able to configure Zed to work by setting `/etc/prime-discrete` to 'on'. +If you have a discrete GPU and you are using [PRIME](https://wiki.archlinux.org/title/PRIME) (e.g. Pop_OS 24.04, ArchLinux, etc) you may be able to configure Zed to work by switching `/etc/prime-discrete` from 'off' to 'on' (or the reverse). For more information, the [Arch guide to Vulkan](https://wiki.archlinux.org/title/Vulkan) has some good steps that translate well to most distributions. diff --git a/extensions/csharp/Cargo.toml b/extensions/csharp/Cargo.toml index da4fde5a6fffdb..a1f7bb383bb221 100644 --- a/extensions/csharp/Cargo.toml +++ b/extensions/csharp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_csharp" -version = "0.1.0" +version = "0.1.1" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/csharp/extension.toml b/extensions/csharp/extension.toml index 8813ad06060ee2..9e16ca51529b93 100644 --- a/extensions/csharp/extension.toml +++ b/extensions/csharp/extension.toml @@ -1,7 +1,7 @@ id = "csharp" name = "C#" description = "C# support." -version = "0.1.0" +version = "0.1.1" schema_version = 1 authors = ["fminkowski "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/elixir/Cargo.toml b/extensions/elixir/Cargo.toml index 0651916f62edf1..79b3a30ad25904 100644 --- a/extensions/elixir/Cargo.toml +++ b/extensions/elixir/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_elixir" -version = "0.1.2" +version = "0.1.4" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/elixir/extension.toml b/extensions/elixir/extension.toml index 05ab75fbefc574..01dd6055fafb44 100644 --- a/extensions/elixir/extension.toml +++ b/extensions/elixir/extension.toml @@ -1,7 +1,7 @@ id = "elixir" name = "Elixir" description = "Elixir support." -version = "0.1.3" +version = "0.1.4" schema_version = 1 authors = ["Marshall Bowers "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/haskell/Cargo.toml b/extensions/haskell/Cargo.toml index 4db581e3e75683..e32801cd9a3c6c 100644 --- a/extensions/haskell/Cargo.toml +++ b/extensions/haskell/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_haskell" -version = "0.1.2" +version = "0.1.3" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/haskell/extension.toml b/extensions/haskell/extension.toml index 003687136ea0c8..b8079bb96dc68a 100644 --- a/extensions/haskell/extension.toml +++ b/extensions/haskell/extension.toml @@ -1,7 +1,7 @@ id = "haskell" name = "Haskell" description = "Haskell support." -version = "0.1.2" +version = "0.1.3" schema_version = 1 authors = [ "Pocæus ", diff --git a/extensions/html/Cargo.toml b/extensions/html/Cargo.toml index 2b1deb1777ecc2..d295de7e2acca1 100644 --- a/extensions/html/Cargo.toml +++ b/extensions/html/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_html" -version = "0.1.4" +version = "0.1.5" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/html/extension.toml b/extensions/html/extension.toml index cf0508f3468dde..345b25089f9eec 100644 --- a/extensions/html/extension.toml +++ b/extensions/html/extension.toml @@ -1,7 +1,7 @@ id = "html" name = "HTML" description = "HTML support." -version = "0.1.4" +version = "0.1.5" schema_version = 1 authors = ["Isaac Clayton "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/php/Cargo.toml b/extensions/php/Cargo.toml index c069e35e82974e..b3f0ad663dd9d8 100644 --- a/extensions/php/Cargo.toml +++ b/extensions/php/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_php" -version = "0.2.3" +version = "0.2.4" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/php/extension.toml b/extensions/php/extension.toml index a2bc1d921ed8ed..0f89e3f885c1a0 100644 --- a/extensions/php/extension.toml +++ b/extensions/php/extension.toml @@ -1,7 +1,7 @@ id = "php" name = "PHP" description = "PHP support." -version = "0.2.3" +version = "0.2.4" schema_version = 1 authors = ["Piotr Osiewicz "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/php/languages/php/injections.scm b/extensions/php/languages/php/injections.scm index fdf5559130d473..657145c13f62de 100644 --- a/extensions/php/languages/php/injections.scm +++ b/extensions/php/languages/php/injections.scm @@ -7,3 +7,5 @@ (#set! injection.language "phpdoc")) ((heredoc_body) (heredoc_end) @injection.language) @injection.content + +((nowdoc_body) (heredoc_end) @injection.language) @injection.content diff --git a/extensions/prisma/extension.toml b/extensions/prisma/extension.toml index 22b2bd9f2b2183..fc7ea267ebaa1e 100644 --- a/extensions/prisma/extension.toml +++ b/extensions/prisma/extension.toml @@ -3,7 +3,7 @@ name = "Prisma" description = "Prisma support." version = "0.0.4" schema_version = 1 -authors = ["Matthew Gramigna "] +authors = ["Matthew Gramigna ", "Victor Quiroz "] repository = "https://github.com/zed-industries/zed" [language_servers.prisma-language-server] @@ -12,4 +12,4 @@ language = "Prisma" [grammars.prisma] repository = "https://github.com/victorhqc/tree-sitter-prisma" -commit = "eca2596a355b1a9952b4f80f8f9caed300a272b5" +commit = "beeac50812da9765e29cbb2af1324783033cdca3" diff --git a/extensions/prisma/languages/prisma/highlights.scm b/extensions/prisma/languages/prisma/highlights.scm index 5a7d7a81241314..c43df8b3b0319d 100644 --- a/extensions/prisma/languages/prisma/highlights.scm +++ b/extensions/prisma/languages/prisma/highlights.scm @@ -3,18 +3,28 @@ "enum" "generator" "model" + "view" ] @keyword (comment) @comment (developer_comment) @comment +(number) @number +(string) @string +(false) @boolean +(true) @boolean (arguments) @property -(attribute) @function -(call_expression) @function -(column_type) @type +(maybe) @punctuation +(call_expression (identifier) @function) (enumeral) @constant (identifier) @variable -(string) @string +(column_declaration (identifier) (column_type (identifier) @type)) +(attribute (identifier) @label) +(attribute (call_expression (identifier) @label)) +(attribute (call_expression (member_expression (identifier) @label))) +(block_attribute_declaration (identifier) @label) +(block_attribute_declaration (call_expression (identifier) @label)) +(type_expression (identifier) @property) "(" @punctuation.bracket ")" @punctuation.bracket @@ -24,3 +34,4 @@ "}" @punctuation.bracket "=" @operator "@" @operator +"@@" @operator diff --git a/extensions/purescript/Cargo.toml b/extensions/purescript/Cargo.toml index 189e2f87c59e42..8dab67317460d1 100644 --- a/extensions/purescript/Cargo.toml +++ b/extensions/purescript/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_purescript" -version = "0.0.1" +version = "0.1.0" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/purescript/extension.toml b/extensions/purescript/extension.toml index d957d31b870498..26ca90fc96c132 100644 --- a/extensions/purescript/extension.toml +++ b/extensions/purescript/extension.toml @@ -1,7 +1,7 @@ id = "purescript" name = "PureScript" description = "PureScript support." -version = "0.0.1" +version = "0.1.0" schema_version = 1 authors = ["Iván Molina Rebolledo "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/scheme/extension.toml b/extensions/scheme/extension.toml index f7916ff78362b0..d991c803c4f710 100644 --- a/extensions/scheme/extension.toml +++ b/extensions/scheme/extension.toml @@ -1,7 +1,7 @@ id = "scheme" name = "Scheme" description = "Scheme support." -version = "0.0.1" +version = "0.0.2" schema_version = 1 authors = ["Mikayla Maki "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/terraform/Cargo.toml b/extensions/terraform/Cargo.toml index 7ac68b0d1cae76..ecf1b86fa98eb3 100644 --- a/extensions/terraform/Cargo.toml +++ b/extensions/terraform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_terraform" -version = "0.1.1" +version = "0.1.2" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/terraform/extension.toml b/extensions/terraform/extension.toml index fc96f773e9b238..38e412715a2b2b 100644 --- a/extensions/terraform/extension.toml +++ b/extensions/terraform/extension.toml @@ -1,7 +1,7 @@ id = "terraform" name = "Terraform" description = "Terraform support." -version = "0.1.1" +version = "0.1.2" schema_version = 1 authors = ["Caius Durling ", "Daniel Banck "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/zig/Cargo.toml b/extensions/zig/Cargo.toml index 236866d0b0a5df..2e3d0ffa6654e3 100644 --- a/extensions/zig/Cargo.toml +++ b/extensions/zig/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_zig" -version = "0.3.2" +version = "0.3.3" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/zig/extension.toml b/extensions/zig/extension.toml index 380300683b7257..265b0b1eef28f9 100644 --- a/extensions/zig/extension.toml +++ b/extensions/zig/extension.toml @@ -1,7 +1,7 @@ id = "zig" name = "Zig" description = "Zig support." -version = "0.3.2" +version = "0.3.3" schema_version = 1 authors = ["Allan Calix "] repository = "https://github.com/zed-industries/zed" diff --git a/flake.lock b/flake.lock index c0cf3f726c8553..3acb60b7ed6930 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1734324364, - "narHash": "sha256-omYTR59TdH0AumP1cfh49fBnWZ52HjfdNfaLzCMZBx0=", + "lastModified": 1736898272, + "narHash": "sha256-D10wlrU/HCpSRcb3a7yk+bU3ggpMD1kGbseKtO+7teo=", "owner": "ipetkov", "repo": "crane", - "rev": "60d7623f1320470bf2fdb92fd2dca1e9a27b98ce", + "rev": "6a589f034202a7c6e10bce6c5d1d392d7bc0f340", "type": "github" }, "original": { @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1734119587, - "narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=", + "lastModified": 1737062831, + "narHash": "sha256-Tbk1MZbtV2s5aG+iM99U8FqwxU/YNArMcWAv6clcsBc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5", + "rev": "5df43628fdf08d642be8ba5b3625a6c70731c19c", "type": "github" }, "original": { @@ -61,11 +61,11 @@ ] }, "locked": { - "lastModified": 1734316514, - "narHash": "sha256-0aLx44yMblcOGpfFXKCzp2GhU5JaE6OTvdU+JYrXiUc=", + "lastModified": 1737166965, + "narHash": "sha256-vlDROBAgq+7PEVM0vaS2zboY6DXs3oKK0qW/1dVuFs4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "83ee8ff74d6294a7657320f16814754c4594127b", + "rev": "fc839c9d5d1ebc789b4657c43c4d54838c7c01de", "type": "github" }, "original": { diff --git a/nix/shell.nix b/nix/shell.nix index 69472d01fa660c..edfa2f442c3bd2 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -29,6 +29,7 @@ pkgs.mkShell rec { pkgs.libgit2 pkgs.openssl pkgs.sqlite + pkgs.stdenv.cc.cc pkgs.zlib pkgs.zstd pkgs.rustToolchain @@ -42,17 +43,14 @@ pkgs.mkShell rec { ] ++ lib.optional pkgs.stdenv.hostPlatform.isDarwin pkgs.apple-sdk_15; - LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib"; + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; + + PROTOC="${pkgs.protobuf}/bin/protoc"; # We set SDKROOT and DEVELOPER_DIR to the Xcode ones instead of the nixpkgs ones, # because we need Swift 6.0 and nixpkgs doesn't have it. # Xcode is required for development anyways - shellHook = - '' - export LD_LIBRARY_PATH="${lib.makeLibraryPath buildInputs}:$LD_LIBRARY_PATH" - export PROTOC="${pkgs.protobuf}/bin/protoc" - '' - + lib.optionalString pkgs.stdenv.hostPlatform.isDarwin '' + shellHook = lib.optionalString pkgs.stdenv.hostPlatform.isDarwin '' export SDKROOT="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"; export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"; ''; diff --git a/script/label_data.json b/script/label_data.json deleted file mode 100644 index d2adc05647ca5a..00000000000000 --- a/script/label_data.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "core_labels": [ - "bug", - "design", - "discussion", - "documentation", - "duplicate", - "enhancement", - "panic / crash", - "support" - ], - "additional_labels": ["ai", "linux", "vim", "windows"], - "ignored_label": "ignore top-ranking issues", - "new_issue_labels": ["triage", "admin read"] -} diff --git a/script/update_top_ranking_issues/.python-version b/script/update_top_ranking_issues/.python-version deleted file mode 100644 index 24ee5b1be9961e..00000000000000 --- a/script/update_top_ranking_issues/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/script/update_top_ranking_issues/main.py b/script/update_top_ranking_issues/main.py deleted file mode 100644 index b7983b1f376183..00000000000000 --- a/script/update_top_ranking_issues/main.py +++ /dev/null @@ -1,311 +0,0 @@ -import json -import os -import pathlib -from collections import defaultdict -from datetime import datetime, timedelta -from typing import Optional - -import typer -from github import Github -from github.Issue import Issue -from github.Repository import Repository -from pytz import timezone -from typer import Typer - -app: Typer = typer.Typer() - -DATETIME_FORMAT: str = "%m/%d/%Y %I:%M %p" -LABEL_DATA_FILE_PATH = pathlib.Path(__file__).parent.parent / "label_data.json" -ISSUES_PER_LABEL: int = 20 -MISSING_LABEL_ERROR_MESSAGE: str = "missing core label" - -with open(LABEL_DATA_FILE_PATH, "r") as label_data_file: - label_data = json.load(label_data_file) - CORE_LABELS: set[str] = set(label_data["core_labels"]) - # A set of labels for adding in labels that we want present in the final - # report, but that we don't want being defined as a core label, since issues - # with without core labels are flagged as errors. - ADDITIONAL_LABELS: set[str] = set(label_data["additional_labels"]) - NEW_ISSUE_LABELS: set[str] = set(label_data["new_issue_labels"]) - IGNORED_LABEL: str = label_data["ignored_label"] - - -class IssueData: - def __init__(self, issue: Issue) -> None: - self.title = issue.title - self.url: str = issue.html_url - self.like_count: int = issue._rawData["reactions"]["+1"] # type: ignore [attr-defined] - self.creation_datetime: str = issue.created_at.strftime(DATETIME_FORMAT) - # TODO: Change script to support storing labels here, rather than directly in the script - self.labels: set[str] = {label["name"] for label in issue._rawData["labels"]} # type: ignore [attr-defined] - self._issue = issue - - -@app.command() -def main( - github_token: Optional[str] = None, - issue_reference_number: Optional[int] = None, - query_day_interval: Optional[int] = None, -) -> None: - start_time: datetime = datetime.now() - - start_date: datetime | None = None - - if query_day_interval: - tz = timezone("america/new_york") - current_time = datetime.now(tz).replace( - hour=0, minute=0, second=0, microsecond=0 - ) - start_date = current_time - timedelta(days=query_day_interval) - - # GitHub Workflow will pass in the token as an environment variable, - # but we can place it in our env when running the script locally, for convenience - github_token = github_token or os.getenv("GITHUB_ACCESS_TOKEN") - github = Github(github_token) - - remaining_requests_before: int = github.rate_limiting[0] - print(f"Remaining requests before: {remaining_requests_before}") - - repo_name: str = "zed-industries/zed" - repository: Repository = github.get_repo(repo_name) - - # There has to be a nice way of adding types to tuple unpacking - label_to_issue_data: dict[str, list[IssueData]] - error_message_to_erroneous_issue_data: dict[str, list[IssueData]] - ( - label_to_issue_data, - error_message_to_erroneous_issue_data, - ) = get_issue_maps(github, repository, start_date) - - issue_text: str = get_issue_text( - label_to_issue_data, - error_message_to_erroneous_issue_data, - ) - - if issue_reference_number: - top_ranking_issues_issue: Issue = repository.get_issue(issue_reference_number) - top_ranking_issues_issue.edit(body=issue_text) - else: - print(issue_text) - - for error_message, issue_data in error_message_to_erroneous_issue_data.items(): - if error_message == MISSING_LABEL_ERROR_MESSAGE: - for issue in issue_data: - # Used as a dry-run flag - if issue_reference_number: - issue._issue.add_to_labels(*NEW_ISSUE_LABELS) - - remaining_requests_after: int = github.rate_limiting[0] - print(f"Remaining requests after: {remaining_requests_after}") - print(f"Requests used: {remaining_requests_before - remaining_requests_after}") - - run_duration: timedelta = datetime.now() - start_time - print(run_duration) - - -def get_issue_maps( - github: Github, - repository: Repository, - start_date: datetime | None = None, -) -> tuple[dict[str, list[IssueData]], dict[str, list[IssueData]]]: - label_to_issues: defaultdict[str, list[Issue]] = get_label_to_issues( - github, - repository, - start_date, - ) - label_to_issue_data: dict[str, list[IssueData]] = get_label_to_issue_data( - label_to_issues - ) - - error_message_to_erroneous_issues: defaultdict[str, list[Issue]] = ( - get_error_message_to_erroneous_issues(github, repository) - ) - error_message_to_erroneous_issue_data: dict[str, list[IssueData]] = ( - get_error_message_to_erroneous_issue_data(error_message_to_erroneous_issues) - ) - - # Create a new dictionary with labels ordered by the summation the of likes on the associated issues - labels = list(label_to_issue_data.keys()) - - labels.sort( - key=lambda label: sum( - issue_data.like_count for issue_data in label_to_issue_data[label] - ), - reverse=True, - ) - - label_to_issue_data = {label: label_to_issue_data[label] for label in labels} - - return ( - label_to_issue_data, - error_message_to_erroneous_issue_data, - ) - - -def get_label_to_issues( - github: Github, - repository: Repository, - start_date: datetime | None = None, -) -> defaultdict[str, list[Issue]]: - label_to_issues: defaultdict[str, list[Issue]] = defaultdict(list) - - labels: set[str] = CORE_LABELS | ADDITIONAL_LABELS - - date_query: str = ( - f"created:>={start_date.strftime('%Y-%m-%d')}" if start_date else "" - ) - - for label in labels: - query: str = f'repo:{repository.full_name} is:open is:issue {date_query} label:"{label}" -label:"{IGNORED_LABEL}" sort:reactions-+1-desc' - - issues = github.search_issues(query) - - if issues.totalCount > 0: - for issue in issues[0:ISSUES_PER_LABEL]: - label_to_issues[label].append(issue) - - return label_to_issues - - -def get_label_to_issue_data( - label_to_issues: defaultdict[str, list[Issue]], -) -> dict[str, list[IssueData]]: - label_to_issue_data: dict[str, list[IssueData]] = {} - - for label in label_to_issues: - issues: list[Issue] = label_to_issues[label] - issue_data: list[IssueData] = [IssueData(issue) for issue in issues] - issue_data.sort( - key=lambda issue_data: ( - -issue_data.like_count, - issue_data.creation_datetime, - ) - ) - - if issue_data: - label_to_issue_data[label] = issue_data - - return label_to_issue_data - - -def get_error_message_to_erroneous_issues( - github: Github, repository: Repository -) -> defaultdict[str, list[Issue]]: - error_message_to_erroneous_issues: defaultdict[str, list[Issue]] = defaultdict(list) - - # Query for all open issues that don't have either a core or the ignored label and mark those as erroneous - filter_labels: set[str] = CORE_LABELS | {IGNORED_LABEL} - filter_labels_text: str = " ".join([f'-label:"{label}"' for label in filter_labels]) - query: str = f"repo:{repository.full_name} is:open is:issue {filter_labels_text}" - - for issue in github.search_issues(query): - error_message_to_erroneous_issues[MISSING_LABEL_ERROR_MESSAGE].append(issue) - - return error_message_to_erroneous_issues - - -def get_error_message_to_erroneous_issue_data( - error_message_to_erroneous_issues: defaultdict[str, list[Issue]], -) -> dict[str, list[IssueData]]: - error_message_to_erroneous_issue_data: dict[str, list[IssueData]] = {} - - for label in error_message_to_erroneous_issues: - issues: list[Issue] = error_message_to_erroneous_issues[label] - issue_data: list[IssueData] = [IssueData(issue) for issue in issues] - error_message_to_erroneous_issue_data[label] = issue_data - - return error_message_to_erroneous_issue_data - - -def get_issue_text( - label_to_issue_data: dict[str, list[IssueData]], - error_message_to_erroneous_issue_data: dict[str, list[IssueData]], -) -> str: - tz = timezone("america/new_york") - current_datetime: str = datetime.now(tz).strftime(f"{DATETIME_FORMAT} (%Z)") - - highest_ranking_issues_lines: list[str] = get_highest_ranking_issues_lines( - label_to_issue_data - ) - - issue_text_lines: list[str] = [ - f"*Updated on {current_datetime}*", - *highest_ranking_issues_lines, - "", - "---\n", - ] - - erroneous_issues_lines: list[str] = get_erroneous_issues_lines( - error_message_to_erroneous_issue_data - ) - - if erroneous_issues_lines: - core_labels_text: str = ", ".join( - f'"{core_label}"' for core_label in CORE_LABELS - ) - - issue_text_lines.extend( - [ - "## errors with issues (this section only shows when there are errors with issues)\n", - f"This script expects every issue to have at least one of the following core labels: {core_labels_text}", - f"This script currently ignores issues that have the following label: {IGNORED_LABEL}\n", - "### what to do?\n", - "- Adjust the core labels on an issue to put it into a correct state or add a currently-ignored label to the issue", - "- Adjust the core and ignored labels registered in this script", - *erroneous_issues_lines, - "", - "---\n", - ] - ) - - issue_text_lines.extend( - [ - "*For details on how this issue is generated, [see the script](https://github.com/zed-industries/zed/blob/main/script/update_top_ranking_issues/main.py)*", - ] - ) - - return "\n".join(issue_text_lines) - - -def get_highest_ranking_issues_lines( - label_to_issue_data: dict[str, list[IssueData]], -) -> list[str]: - highest_ranking_issues_lines: list[str] = [] - - if label_to_issue_data: - for label, issue_data in label_to_issue_data.items(): - highest_ranking_issues_lines.append(f"\n## {label}\n") - - for i, issue_data in enumerate(issue_data): - markdown_bullet_point: str = ( - f"{issue_data.url} ({issue_data.like_count} :thumbsup:)" - ) - - markdown_bullet_point = f"{i + 1}. {markdown_bullet_point}" - highest_ranking_issues_lines.append(markdown_bullet_point) - - return highest_ranking_issues_lines - - -def get_erroneous_issues_lines( - error_message_to_erroneous_issue_data, -) -> list[str]: - erroneous_issues_lines: list[str] = [] - - if error_message_to_erroneous_issue_data: - for ( - error_message, - erroneous_issue_data, - ) in error_message_to_erroneous_issue_data.items(): - erroneous_issues_lines.append(f"\n#### {error_message}\n") - - for erroneous_issue_data in erroneous_issue_data: - erroneous_issues_lines.append(f"- {erroneous_issue_data.url}") - - return erroneous_issues_lines - - -if __name__ == "__main__": - app() - -# TODO: Sort label output into core and non core sections diff --git a/script/update_top_ranking_issues/pyproject.toml b/script/update_top_ranking_issues/pyproject.toml deleted file mode 100644 index 88619d4d408877..00000000000000 --- a/script/update_top_ranking_issues/pyproject.toml +++ /dev/null @@ -1,13 +0,0 @@ -[project] -name = "update-top-ranking-issues" -version = "0.1.0" -readme = "README.md" -requires-python = ">=3.13" -dependencies = [ - "mypy>=1.11.2", - "pygithub>=2.4.0", - "pytz>=2024.2", - "ruff>=0.6.9", - "typer>=0.12.5", - "types-pytz>=2024.2.0.20241003", -] diff --git a/script/update_top_ranking_issues/pyrightconfig.json b/script/update_top_ranking_issues/pyrightconfig.json deleted file mode 100644 index 8fd86437bcb1a1..00000000000000 --- a/script/update_top_ranking_issues/pyrightconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "venvPath": ".", - "venv": ".venv" -} diff --git a/script/update_top_ranking_issues/uv.lock b/script/update_top_ranking_issues/uv.lock deleted file mode 100644 index f7bd526d2a7886..00000000000000 --- a/script/update_top_ranking_issues/uv.lock +++ /dev/null @@ -1,385 +0,0 @@ -version = 1 -requires-python = ">=3.13" - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, - { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, - { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, - { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, - { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, - { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, - { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, - { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, - { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, - { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, - { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, - { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, - { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, - { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - -[[package]] -name = "click" -version = "8.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "cryptography" -version = "43.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/ba/0664727028b37e249e73879348cc46d45c5c1a2a2e81e8166462953c5755/cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", size = 686927 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/28/b92c98a04ba762f8cdeb54eba5c4c84e63cac037a7c5e70117d337b15ad6/cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", size = 6223222 }, - { url = "https://files.pythonhosted.org/packages/33/13/1193774705783ba364121aa2a60132fa31a668b8ababd5edfa1662354ccd/cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", size = 3794751 }, - { url = "https://files.pythonhosted.org/packages/5e/4b/39bb3c4c8cfb3e94e736b8d8859ce5c81536e91a1033b1d26770c4249000/cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", size = 3981827 }, - { url = "https://files.pythonhosted.org/packages/ce/dc/1471d4d56608e1013237af334b8a4c35d53895694fbb73882d1c4fd3f55e/cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", size = 3780034 }, - { url = "https://files.pythonhosted.org/packages/ad/43/7a9920135b0d5437cc2f8f529fa757431eb6a7736ddfadfdee1cc5890800/cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", size = 3993407 }, - { url = "https://files.pythonhosted.org/packages/cc/42/9ab8467af6c0b76f3d9b8f01d1cf25b9c9f3f2151f4acfab888d21c55a72/cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", size = 3886457 }, - { url = "https://files.pythonhosted.org/packages/a4/65/430509e31700286ec02868a2457d2111d03ccefc20349d24e58d171ae0a7/cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", size = 4081499 }, - { url = "https://files.pythonhosted.org/packages/bb/18/a04b6467e6e09df8c73b91dcee8878f4a438a43a3603dc3cd6f8003b92d8/cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", size = 2616504 }, - { url = "https://files.pythonhosted.org/packages/cc/73/0eacbdc437202edcbdc07f3576ed8fb8b0ab79d27bf2c5d822d758a72faa/cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", size = 3067456 }, - { url = "https://files.pythonhosted.org/packages/8a/b6/bc54b371f02cffd35ff8dc6baba88304d7cf8e83632566b4b42e00383e03/cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", size = 6225263 }, - { url = "https://files.pythonhosted.org/packages/00/0e/8217e348a1fa417ec4c78cd3cdf24154f5e76fd7597343a35bd403650dfd/cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", size = 3794368 }, - { url = "https://files.pythonhosted.org/packages/3d/ed/38b6be7254d8f7251fde8054af597ee8afa14f911da67a9410a45f602fc3/cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", size = 3981750 }, - { url = "https://files.pythonhosted.org/packages/64/f3/b7946c3887cf7436f002f4cbb1e6aec77b8d299b86be48eeadfefb937c4b/cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", size = 3778925 }, - { url = "https://files.pythonhosted.org/packages/ac/7e/ebda4dd4ae098a0990753efbb4b50954f1d03003846b943ea85070782da7/cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", size = 3993152 }, - { url = "https://files.pythonhosted.org/packages/43/f6/feebbd78a3e341e3913846a3bb2c29d0b09b1b3af1573c6baabc2533e147/cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", size = 3886392 }, - { url = "https://files.pythonhosted.org/packages/bd/4c/ab0b9407d5247576290b4fd8abd06b7f51bd414f04eef0f2800675512d61/cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", size = 4082606 }, - { url = "https://files.pythonhosted.org/packages/05/36/e532a671998d6fcfdb9122da16434347a58a6bae9465e527e450e0bc60a5/cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", size = 2617948 }, - { url = "https://files.pythonhosted.org/packages/b3/c6/c09cee6968add5ff868525c3815e5dccc0e3c6e89eec58dc9135d3c40e88/cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", size = 3070445 }, -] - -[[package]] -name = "deprecated" -version = "1.2.14" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/14/1e41f504a246fc224d2ac264c227975427a85caf37c3979979edb9b1b232/Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3", size = 2974416 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/8d/778b7d51b981a96554f29136cd59ca7880bf58094338085bcf2a979a0e6a/Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", size = 9561 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "mypy" -version = "1.11.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - -[[package]] -name = "pygithub" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "pynacl" }, - { name = "requests" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/a0/1e8b8ca88df9857836f5bf8e3ee15dfb810d19814ef700b12f99ce11f691/pygithub-2.4.0.tar.gz", hash = "sha256:6601e22627e87bac192f1e2e39c6e6f69a43152cfb8f307cee575879320b3051", size = 3476673 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/f3/e185613c411757c0c18b904ea2db173f2872397eddf444a3fe8cdde47077/PyGithub-2.4.0-py3-none-any.whl", hash = "sha256:81935aa4bdc939fba98fee1cb47422c09157c56a27966476ff92775602b9ee24", size = 362599 }, -] - -[[package]] -name = "pygments" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, -] - -[[package]] -name = "pyjwt" -version = "2.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344 }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pynacl" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920 }, - { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722 }, - { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087 }, - { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678 }, - { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660 }, - { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824 }, - { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912 }, - { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624 }, - { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141 }, -] - -[[package]] -name = "pytz" -version = "2024.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "rich" -version = "13.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/9e/1784d15b057b0075e5136445aaea92d23955aad2c93eaede673718a40d95/rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c", size = 222843 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/91/5474b84e505a6ccc295b2d322d90ff6aa0746745717839ee0c5fb4fdcceb/rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1", size = 242117 }, -] - -[[package]] -name = "ruff" -version = "0.6.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/0d/6148a48dab5662ca1d5a93b7c0d13c03abd3cc7e2f35db08410e47cef15d/ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2", size = 3095355 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/8f/f7a0a0ef1818662efb32ed6df16078c95da7a0a3248d64c2410c1e27799f/ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd", size = 10440526 }, - { url = "https://files.pythonhosted.org/packages/8b/69/b179a5faf936a9e2ab45bb412a668e4661eded964ccfa19d533f29463ef6/ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec", size = 10034612 }, - { url = "https://files.pythonhosted.org/packages/c7/ef/fd1b4be979c579d191eeac37b5cfc0ec906de72c8bcd8595e2c81bb700c1/ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c", size = 9706197 }, - { url = "https://files.pythonhosted.org/packages/29/61/b376d775deb5851cb48d893c568b511a6d3625ef2c129ad5698b64fb523c/ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e", size = 10751855 }, - { url = "https://files.pythonhosted.org/packages/13/d7/def9e5f446d75b9a9c19b24231a3a658c075d79163b08582e56fa5dcfa38/ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577", size = 10200889 }, - { url = "https://files.pythonhosted.org/packages/6c/d6/7f34160818bcb6e84ce293a5966cba368d9112ff0289b273fbb689046047/ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829", size = 11038678 }, - { url = "https://files.pythonhosted.org/packages/13/34/a40ff8ae62fb1b26fb8e6fa7e64bc0e0a834b47317880de22edd6bfb54fb/ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5", size = 11808682 }, - { url = "https://files.pythonhosted.org/packages/2e/6d/25a4386ae4009fc798bd10ba48c942d1b0b3e459b5403028f1214b6dd161/ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7", size = 11330446 }, - { url = "https://files.pythonhosted.org/packages/f7/f6/bdf891a9200d692c94ebcd06ae5a2fa5894e522f2c66c2a12dd5d8cb2654/ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f", size = 12483048 }, - { url = "https://files.pythonhosted.org/packages/a7/86/96f4252f41840e325b3fa6c48297e661abb9f564bd7dcc0572398c8daa42/ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa", size = 10936855 }, - { url = "https://files.pythonhosted.org/packages/45/87/801a52d26c8dbf73424238e9908b9ceac430d903c8ef35eab1b44fcfa2bd/ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb", size = 10713007 }, - { url = "https://files.pythonhosted.org/packages/be/27/6f7161d90320a389695e32b6ebdbfbedde28ccbf52451e4b723d7ce744ad/ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0", size = 10274594 }, - { url = "https://files.pythonhosted.org/packages/00/52/dc311775e7b5f5b19831563cb1572ecce63e62681bccc609867711fae317/ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625", size = 10608024 }, - { url = "https://files.pythonhosted.org/packages/98/b6/be0a1ddcbac65a30c985cf7224c4fce786ba2c51e7efeb5178fe410ed3cf/ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039", size = 10982085 }, - { url = "https://files.pythonhosted.org/packages/bb/a4/c84bc13d0b573cf7bb7d17b16d6d29f84267c92d79b2f478d4ce322e8e72/ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d", size = 8522088 }, - { url = "https://files.pythonhosted.org/packages/74/be/fc352bd8ca40daae8740b54c1c3e905a7efe470d420a268cd62150248c91/ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117", size = 9359275 }, - { url = "https://files.pythonhosted.org/packages/3e/14/fd026bc74ded05e2351681545a5f626e78ef831f8edce064d61acd2e6ec7/ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", size = 8679879 }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - -[[package]] -name = "typer" -version = "0.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, -] - -[[package]] -name = "types-pytz" -version = "2024.2.0.20241003" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/d0/73aa3063a9ef9881bd7103cb4ae379bfd8fafda0e86b01b694d676313a4b/types-pytz-2024.2.0.20241003.tar.gz", hash = "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44", size = 5474 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/60/2a2977ce0f91255bbb668350b127a801a06ad37c326a2e5bfd52f03e0784/types_pytz-2024.2.0.20241003-py3-none-any.whl", hash = "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7", size = 5245 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "update-top-ranking-issues" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "mypy" }, - { name = "pygithub" }, - { name = "pytz" }, - { name = "ruff" }, - { name = "typer" }, - { name = "types-pytz" }, -] - -[package.metadata] -requires-dist = [ - { name = "mypy", specifier = ">=1.11.2" }, - { name = "pygithub", specifier = ">=2.4.0" }, - { name = "pytz", specifier = ">=2024.2" }, - { name = "ruff", specifier = ">=0.6.9" }, - { name = "typer", specifier = ">=0.12.5" }, - { name = "types-pytz", specifier = ">=2024.2.0.20241003" }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] - -[[package]] -name = "wrapt" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, -] diff --git a/tooling/xtask/src/tasks/package_conformity.rs b/tooling/xtask/src/tasks/package_conformity.rs index 8a17e7be43a895..de2db3cdd14453 100644 --- a/tooling/xtask/src/tasks/package_conformity.rs +++ b/tooling/xtask/src/tasks/package_conformity.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::fs; use std::path::Path; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use cargo_toml::{Dependency, Manifest}; use clap::Parser; diff --git a/tooling/xtask/src/workspace.rs b/tooling/xtask/src/workspace.rs index 4c4ece6cffd3c6..fd71aa6bbd7be6 100644 --- a/tooling/xtask/src/workspace.rs +++ b/tooling/xtask/src/workspace.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use cargo_metadata::{Metadata, MetadataCommand}; /// Returns the Cargo workspace. From dfe978b06adacf45e26ae70a57136c05d51925e1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 29 Jan 2025 20:55:57 +0100 Subject: [PATCH 490/650] Add toolchain support for python debug adapter (#90) * WIP add toolchain for python Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> * Integrate toolchain store with dap_store & use it for debugpy * Wip require worktree to start debug session * Move start debug session to project for determing the worktree * Make all tests pass again * Make collab tests pass * Use notify instead of manual notification * Use reference instead of clone * Revert "Use reference instead of clone" This reverts commit 61469bb1679bc35d5d3bf0b93e5b7cfc94357c80. * Revert "Use notify instead of manual notification" This reverts commit a0b9bf52a1d948dfb244c4b7040576a34ec6f465. * Revert debugger branch merge * Revert "Revert debugger branch merge" This reverts commit 56c883d4dba4877826ea2185a8177fddefa0d054. * Clean up * Make node runtime required * Pass worktree id into get_environment * Fix use the resolved debug adapter config * Add fallback if toolchain could not be found to common binary names --------- Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Co-authored-by: Anthony Eid --- Cargo.lock | 3 + crates/collab/src/tests/debug_panel_tests.rs | 226 ++++++----- crates/dap/Cargo.toml | 1 + crates/dap/src/adapters.rs | 40 +- crates/dap_adapters/Cargo.toml | 8 +- crates/dap_adapters/src/custom.rs | 3 + crates/dap_adapters/src/gdb.rs | 3 + crates/dap_adapters/src/go.rs | 10 +- crates/dap_adapters/src/javascript.rs | 14 +- crates/dap_adapters/src/lldb.rs | 3 + crates/dap_adapters/src/php.rs | 14 +- crates/dap_adapters/src/python.rs | 48 ++- crates/debugger_ui/src/tests.rs | 9 +- crates/debugger_ui/src/tests/attach_modal.rs | 106 +++--- crates/debugger_ui/src/tests/console.rs | 92 +++-- .../debugger_ui/src/tests/debugger_panel.rs | 350 ++++++++++-------- .../debugger_ui/src/tests/stack_frame_list.rs | 72 ++-- crates/debugger_ui/src/tests/variable_list.rs | 72 ++-- crates/project/src/dap_store.rs | 107 +++--- crates/project/src/project.rs | 64 ++-- crates/remote_server/src/headless_project.rs | 19 +- crates/task/src/lib.rs | 2 +- crates/tasks_ui/src/modal.rs | 9 +- 23 files changed, 682 insertions(+), 593 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a6b22e4a6fca6..1b97423f95166b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3634,6 +3634,7 @@ dependencies = [ "futures 0.3.31", "gpui", "http_client", + "language", "log", "node_runtime", "parking_lot", @@ -3666,6 +3667,8 @@ dependencies = [ "anyhow", "async-trait", "dap", + "gpui", + "language", "paths", "regex", "serde", diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 5bf62d9ad1cebe..7f19c8cfbed752 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -67,7 +67,7 @@ async fn test_debug_panel_item_opens_on_remote( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); - let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -92,19 +92,17 @@ async fn test_debug_panel_item_opens_on_remote( cx_b.run_until_parked(); let task = project_a.update(cx_a, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -188,7 +186,7 @@ async fn test_active_debug_panel_item_set_on_join_project( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); - let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -206,19 +204,17 @@ async fn test_active_debug_panel_item_set_on_join_project( cx_a.run_until_parked(); let task = project_a.update(cx_a, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -329,7 +325,7 @@ async fn test_debug_panel_remote_button_presses( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); - let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -352,19 +348,17 @@ async fn test_debug_panel_remote_button_presses( add_debugger_panel(&workspace_b, cx_b).await; let task = project_a.update(cx_a, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (_, client) = task.await.unwrap(); @@ -692,7 +686,7 @@ async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppC let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); - let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -715,19 +709,17 @@ async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppC add_debugger_panel(&workspace_b, cx_b).await; let task = project_a.update(cx_a, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -856,7 +848,7 @@ async fn test_updated_breakpoints_send_to_dap( client_a .fs() .insert_tree( - "/a", + "/project", json!({ "test.txt": "one\ntwo\nthree\nfour\nfive", }), @@ -872,7 +864,7 @@ async fn test_updated_breakpoints_send_to_dap( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -900,19 +892,17 @@ async fn test_updated_breakpoints_send_to_dap( add_debugger_panel(&workspace_b, cx_b).await; let task = project_a.update(cx_a, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -941,7 +931,7 @@ async fn test_updated_breakpoints_send_to_dap( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!("/project/test.txt", args.source.path.unwrap()); assert_eq!( vec![SourceBreakpoint { line: 3, @@ -1015,7 +1005,7 @@ async fn test_updated_breakpoints_send_to_dap( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!("/project/test.txt", args.source.path.unwrap()); assert!(args.breakpoints.unwrap().is_empty()); assert!(!args.source_modified.unwrap()); @@ -1048,7 +1038,7 @@ async fn test_updated_breakpoints_send_to_dap( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!("/project/test.txt", args.source.path.unwrap()); let mut breakpoints = args.breakpoints.unwrap(); breakpoints.sort_by_key(|b| b.line); assert_eq!( @@ -1130,7 +1120,7 @@ async fn test_module_list( let active_call_b = cx_b.read(ActiveCall::global); let active_call_c = cx_c.read(ActiveCall::global); - let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -1153,19 +1143,17 @@ async fn test_module_list( add_debugger_panel(&workspace_b, cx_b).await; let task = project_a.update(cx_a, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -1403,7 +1391,7 @@ async fn test_variable_list( let active_call_b = cx_b.read(ActiveCall::global); let active_call_c = cx_c.read(ActiveCall::global); - let (project_a, _worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -1426,19 +1414,17 @@ async fn test_variable_list( add_debugger_panel(&workspace_b, cx_b).await; let task = project_a.update(cx_a, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -1881,7 +1867,7 @@ async fn test_ignore_breakpoints( client_a .fs() .insert_tree( - "/a", + "/project", json!({ "test.txt": "one\ntwo\nthree\nfour\nfive", }), @@ -1899,7 +1885,7 @@ async fn test_ignore_breakpoints( let active_call_b = cx_b.read(ActiveCall::global); let active_call_c = cx_c.read(ActiveCall::global); - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await; active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) .await @@ -1947,19 +1933,17 @@ async fn test_ignore_breakpoints( cx_b.run_until_parked(); let task = project_a.update(cx_a, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -1979,7 +1963,7 @@ async fn test_ignore_breakpoints( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!("/project/test.txt", args.source.path.unwrap()); let mut actual_breakpoints = args.breakpoints.unwrap(); actual_breakpoints.sort_by_key(|b| b.line); @@ -2095,7 +2079,7 @@ async fn test_ignore_breakpoints( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!("/project/test.txt", args.source.path.unwrap()); assert_eq!(args.breakpoints, Some(vec![])); called_set_breakpoints.store(true, Ordering::SeqCst); @@ -2190,7 +2174,7 @@ async fn test_ignore_breakpoints( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!("/project/test.txt", args.source.path.unwrap()); let mut actual_breakpoints = args.breakpoints.unwrap(); actual_breakpoints.sort_by_key(|b| b.line); @@ -2281,7 +2265,7 @@ async fn test_ignore_breakpoints( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!("/project/test.txt", args.source.path.unwrap()); assert_eq!(args.breakpoints, Some(vec![])); called_set_breakpoints.store(true, Ordering::SeqCst); diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index b4c03759718a06..f4b90a7a0d6b8d 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -30,6 +30,7 @@ fs.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true +language.workspace = true log.workspace = true node_runtime.workspace = true parking_lot.workspace = true diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index e7eee938b3ff03..4dcf37693ba97d 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -7,11 +7,13 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use futures::io::BufReader; -use gpui::SharedString; +use gpui::{AsyncApp, SharedString}; pub use http_client::{github::latest_github_release, HttpClient}; +use language::LanguageToolchainStore; use node_runtime::NodeRuntime; use serde::{Deserialize, Serialize}; use serde_json::Value; +use settings::WorktreeId; use smol::{self, fs::File, lock::Mutex}; use std::{ collections::{HashMap, HashSet}, @@ -35,8 +37,10 @@ pub enum DapStatus { #[async_trait(?Send)] pub trait DapDelegate { - fn http_client(&self) -> Option>; - fn node_runtime(&self) -> Option; + fn worktree_id(&self) -> WorktreeId; + fn http_client(&self) -> Arc; + fn node_runtime(&self) -> NodeRuntime; + fn toolchain_store(&self) -> Arc; fn fs(&self) -> Arc; fn updated_adapters(&self) -> Arc>>; fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus); @@ -135,10 +139,8 @@ pub async fn download_adapter_from_github( &github_version.url, ); - let http_client = delegate + let mut response = delegate .http_client() - .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; - let mut response = http_client .get(&github_version.url, Default::default(), true) .await .context("Error downloading release")?; @@ -191,15 +193,11 @@ pub async fn fetch_latest_adapter_version_from_github( github_repo: GithubRepo, delegate: &dyn DapDelegate, ) -> Result { - let http_client = delegate - .http_client() - .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; - let release = latest_github_release( &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name), false, false, - http_client, + delegate.http_client(), ) .await?; @@ -218,6 +216,7 @@ pub trait DebugAdapter: 'static + Send + Sync { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, + cx: &mut AsyncApp, ) -> Result { if delegate .updated_adapters() @@ -228,7 +227,7 @@ pub trait DebugAdapter: 'static + Send + Sync { log::info!("Using cached debug adapter binary {}", self.name()); if let Some(binary) = self - .get_installed_binary(delegate, &config, user_installed_path.clone()) + .get_installed_binary(delegate, &config, user_installed_path.clone(), cx) .await .log_err() { @@ -258,7 +257,7 @@ pub trait DebugAdapter: 'static + Send + Sync { .insert(self.name()); } - self.get_installed_binary(delegate, &config, user_installed_path) + self.get_installed_binary(delegate, &config, user_installed_path, cx) .await } @@ -283,6 +282,7 @@ pub trait DebugAdapter: 'static + Send + Sync { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, + cx: &mut AsyncApp, ) -> Result; /// Should return base configuration to make the debug adapter work @@ -328,9 +328,10 @@ impl DebugAdapter for FakeAdapter { async fn get_binary( &self, - _delegate: &dyn DapDelegate, - _config: &DebugAdapterConfig, - _user_installed_path: Option, + _: &dyn DapDelegate, + _: &DebugAdapterConfig, + _: Option, + _: &mut AsyncApp, ) -> Result { Ok(DebugAdapterBinary { command: "command".into(), @@ -357,9 +358,10 @@ impl DebugAdapter for FakeAdapter { async fn get_installed_binary( &self, - _delegate: &dyn DapDelegate, - _config: &DebugAdapterConfig, - _user_installed_path: Option, + _: &dyn DapDelegate, + _: &DebugAdapterConfig, + _: Option, + _: &mut AsyncApp, ) -> Result { unimplemented!("get installed binary"); } diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 1672050ed720ad..885f377d5a3149 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -7,9 +7,10 @@ license = "GPL-3.0-or-later" [features] test-support = [ - "util/test-support", - "task/test-support", "dap/test-support", + "gpui/test-support", + "task/test-support", + "util/test-support", ] [lints] @@ -23,6 +24,8 @@ doctest = false anyhow.workspace = true async-trait.workspace = true dap.workspace = true +gpui.workspace = true +language.workspace = true paths.workspace = true regex.workspace = true serde.workspace = true @@ -33,5 +36,6 @@ util.workspace = true [dev-dependencies] dap = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } task = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index ed54a828f882f6..6abec9407f328a 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,6 +1,7 @@ use std::{ffi::OsString, path::PathBuf, sync::Arc}; use dap::transport::{StdioTransport, TcpTransport, Transport}; +use gpui::AsyncApp; use serde_json::Value; use task::DebugAdapterConfig; @@ -44,6 +45,7 @@ impl DebugAdapter for CustomDebugAdapter { _: &dyn DapDelegate, config: &DebugAdapterConfig, _: Option, + _: &mut AsyncApp, ) -> Result { Ok(DebugAdapterBinary { command: self.custom_args.command.clone(), @@ -70,6 +72,7 @@ impl DebugAdapter for CustomDebugAdapter { _: &dyn DapDelegate, _: &DebugAdapterConfig, _: Option, + _: &mut AsyncApp, ) -> Result { bail!("Custom debug adapters cannot be installed") } diff --git a/crates/dap_adapters/src/gdb.rs b/crates/dap_adapters/src/gdb.rs index ed97bcec505da1..84977cb8a777c9 100644 --- a/crates/dap_adapters/src/gdb.rs +++ b/crates/dap_adapters/src/gdb.rs @@ -3,6 +3,7 @@ use std::ffi::OsStr; use anyhow::Result; use async_trait::async_trait; use dap::transport::{StdioTransport, Transport}; +use gpui::AsyncApp; use task::DebugAdapterConfig; use crate::*; @@ -32,6 +33,7 @@ impl DebugAdapter for GdbDebugAdapter { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, + _: &mut AsyncApp, ) -> Result { let user_setting_path = user_installed_path .filter(|p| p.exists()) @@ -74,6 +76,7 @@ impl DebugAdapter for GdbDebugAdapter { _: &dyn DapDelegate, _: &DebugAdapterConfig, _: Option, + _: &mut AsyncApp, ) -> Result { unimplemented!("GDB cannot be installed by Zed (yet)") } diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index b1e8b34c526dc7..d19a3f2290a9e9 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -1,4 +1,5 @@ use dap::transport::{TcpTransport, Transport}; +use gpui::AsyncApp; use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf, sync::Arc}; use crate::*; @@ -10,8 +11,7 @@ pub(crate) struct GoDebugAdapter { } impl GoDebugAdapter { - const _ADAPTER_NAME: &'static str = "delve"; - // const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; + const ADAPTER_NAME: &'static str = "delve"; pub(crate) async fn new(host: &TCPHost) -> Result { Ok(GoDebugAdapter { @@ -25,7 +25,7 @@ impl GoDebugAdapter { #[async_trait(?Send)] impl DebugAdapter for GoDebugAdapter { fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::_ADAPTER_NAME.into()) + DebugAdapterName(Self::ADAPTER_NAME.into()) } fn transport(&self) -> Arc { @@ -37,8 +37,9 @@ impl DebugAdapter for GoDebugAdapter { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, + cx: &mut AsyncApp, ) -> Result { - self.get_installed_binary(delegate, config, user_installed_path) + self.get_installed_binary(delegate, config, user_installed_path, cx) .await } @@ -69,6 +70,7 @@ impl DebugAdapter for GoDebugAdapter { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, _: Option, + _: &mut AsyncApp, ) -> Result { let delve_path = delegate .which(OsStr::new("dlv")) diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index da425ba70553d1..a68afc28f15445 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,5 +1,6 @@ use adapters::latest_github_release; use dap::transport::{TcpTransport, Transport}; +use gpui::AsyncApp; use regex::Regex; use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf, sync::Arc}; use sysinfo::{Pid, Process}; @@ -40,14 +41,11 @@ impl DebugAdapter for JsDebugAdapter { &self, delegate: &dyn DapDelegate, ) -> Result { - let http_client = delegate - .http_client() - .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; let release = latest_github_release( &format!("{}/{}", "microsoft", Self::ADAPTER_NAME), true, false, - http_client, + delegate.http_client(), ) .await?; @@ -70,6 +68,7 @@ impl DebugAdapter for JsDebugAdapter { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, + _: &mut AsyncApp, ) -> Result { let adapter_path = if let Some(user_installed_path) = user_installed_path { user_installed_path @@ -85,12 +84,9 @@ impl DebugAdapter for JsDebugAdapter { .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))? }; - let node_runtime = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))?; - Ok(DebugAdapterBinary { - command: node_runtime + command: delegate + .node_runtime() .binary_path() .await? .to_string_lossy() diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 9206375b88ba83..ae506fbae2a017 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -3,6 +3,7 @@ use std::{ffi::OsStr, path::PathBuf, sync::Arc}; use anyhow::Result; use async_trait::async_trait; use dap::transport::{StdioTransport, Transport}; +use gpui::AsyncApp; use task::DebugAdapterConfig; use crate::*; @@ -32,6 +33,7 @@ impl DebugAdapter for LldbDebugAdapter { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, + _: &mut AsyncApp, ) -> Result { let lldb_dap_path = if let Some(user_installed_path) = user_installed_path { user_installed_path.to_string_lossy().into() @@ -76,6 +78,7 @@ impl DebugAdapter for LldbDebugAdapter { _: &dyn DapDelegate, _: &DebugAdapterConfig, _: Option, + _: &mut AsyncApp, ) -> Result { unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)") } diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 1cba2b4ce2fa38..23aa2c30ef8a6d 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,5 +1,6 @@ use adapters::latest_github_release; use dap::transport::{TcpTransport, Transport}; +use gpui::AsyncApp; use std::{net::Ipv4Addr, path::PathBuf, sync::Arc}; use crate::*; @@ -37,14 +38,11 @@ impl DebugAdapter for PhpDebugAdapter { &self, delegate: &dyn DapDelegate, ) -> Result { - let http_client = delegate - .http_client() - .ok_or_else(|| anyhow!("Failed to download adapter: couldn't connect to GitHub"))?; let release = latest_github_release( &format!("{}/{}", "xdebug", Self::ADAPTER_NAME), true, false, - http_client, + delegate.http_client(), ) .await?; @@ -67,6 +65,7 @@ impl DebugAdapter for PhpDebugAdapter { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, + _: &mut AsyncApp, ) -> Result { let adapter_path = if let Some(user_installed_path) = user_installed_path { user_installed_path @@ -82,12 +81,9 @@ impl DebugAdapter for PhpDebugAdapter { .ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))? }; - let node_runtime = delegate - .node_runtime() - .ok_or(anyhow!("Couldn't get npm runtime"))?; - Ok(DebugAdapterBinary { - command: node_runtime + command: delegate + .node_runtime() .binary_path() .await? .to_string_lossy() diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index d930383c1531f9..e44cadd22ef1e5 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,8 +1,8 @@ +use crate::*; use dap::transport::{TcpTransport, Transport}; +use gpui::AsyncApp; use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf, sync::Arc}; -use crate::*; - pub(crate) struct PythonDebugAdapter { port: u16, host: Ipv4Addr, @@ -12,6 +12,7 @@ pub(crate) struct PythonDebugAdapter { impl PythonDebugAdapter { const ADAPTER_NAME: &'static str = "debugpy"; const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; + const LANGUAGE_NAME: &'static str = "Python"; pub(crate) async fn new(host: &TCPHost) -> Result { Ok(PythonDebugAdapter { @@ -78,7 +79,10 @@ impl DebugAdapter for PythonDebugAdapter { delegate: &dyn DapDelegate, config: &DebugAdapterConfig, user_installed_path: Option, + cx: &mut AsyncApp, ) -> Result { + const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"]; + let debugpy_dir = if let Some(user_installed_path) = user_installed_path { user_installed_path } else { @@ -92,26 +96,30 @@ impl DebugAdapter for PythonDebugAdapter { .ok_or_else(|| anyhow!("Debugpy directory not found"))? }; - let python_cmds = [ - OsStr::new("python3"), - OsStr::new("python"), - OsStr::new("py"), - ]; - let python_path = python_cmds - .iter() - .filter_map(|cmd| { - delegate - .which(cmd) - .and_then(|path| path.to_str().map(|str| str.to_string())) - }) - .find(|_| true); - - let python_path = python_path.ok_or(anyhow!( - "Failed to start debugger because python couldn't be found in PATH" - ))?; + let toolchain = delegate + .toolchain_store() + .active_toolchain( + delegate.worktree_id(), + language::LanguageName::new(Self::LANGUAGE_NAME), + cx, + ) + .await; + + let python_path = if let Some(toolchain) = toolchain { + Some(toolchain.path.to_string()) + } else { + BINARY_NAMES + .iter() + .filter_map(|cmd| { + delegate + .which(OsStr::new(cmd)) + .map(|path| path.to_string_lossy().to_string()) + }) + .find(|_| true) + }; Ok(DebugAdapterBinary { - command: python_path, + command: python_path.ok_or(anyhow!("failed to find binary path for python"))?, arguments: Some(vec![ debugpy_dir.join(Self::ADAPTER_PATH).into(), format!("--port={}", self.port).into(), diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 6cc3265f100d0f..76682fc46cda26 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -1,5 +1,5 @@ use gpui::{Entity, TestAppContext, WindowHandle}; -use project::Project; +use project::{Project, Worktree}; use settings::SettingsStore; use terminal_view::terminal_panel::TerminalPanel; use workspace::Workspace; @@ -72,3 +72,10 @@ pub fn active_debug_panel_item( }) .unwrap() } + +pub fn worktree_from_project( + project: &Entity, + cx: &mut TestAppContext, +) -> Entity { + project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()) +} diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 8cbc84e185e6d3..c5161c76a8fa20 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -20,26 +20,32 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Attach(AttachConfig { - process_id: Some(10), - }), - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Attach(AttachConfig { + process_id: Some(10), + }), + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -102,24 +108,30 @@ async fn test_show_attach_modal_and_select_process( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Attach(AttachConfig { process_id: None }), - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Attach(AttachConfig { process_id: None }), + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -207,24 +219,30 @@ async fn test_shutdown_session_when_modal_is_dismissed( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Attach(AttachConfig { process_id: None }), - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Attach(AttachConfig { process_id: None }), + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 14caf9667cb6dd..1f653e8af08288 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -20,24 +20,30 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -181,24 +187,30 @@ async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppConte let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -489,19 +501,17 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 4aaccf08f104c4..07a08e9ecf3e78 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -27,7 +27,7 @@ use std::{ }, }; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; -use tests::{active_debug_panel_item, init_test, init_test_workspace}; +use tests::{active_debug_panel_item, init_test, init_test_workspace, worktree_from_project}; use workspace::{dock::Panel, Item}; #[gpui::test] @@ -36,24 +36,30 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -153,24 +159,30 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -301,24 +313,30 @@ async fn test_client_can_open_multiple_thread_panels( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -451,24 +469,30 @@ async fn test_handle_successful_run_in_terminal_reverse_request( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -557,24 +581,30 @@ async fn test_handle_error_run_in_terminal_reverse_request( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -654,24 +684,30 @@ async fn test_handle_start_debugging_reverse_request( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -783,24 +819,30 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -979,38 +1021,33 @@ async fn test_send_breakpoints_when_editor_has_been_saved( let fs = FakeFs::new(executor.clone()); fs.insert_tree( - "/a", + "/project", json!({ "main.rs": "First line\nSecond line\nThird line\nFourth line", }), ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); + let worktree = worktree_from_project(&project, cx); let worktree_id = workspace - .update(cx, |workspace, _window, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }) + .update(cx, |_, _, cx| worktree.read(cx).id()) .unwrap(); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -1072,7 +1109,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/main.rs", args.source.path.unwrap()); + assert_eq!("/project/main.rs", args.source.path.unwrap()); assert_eq!( vec![SourceBreakpoint { line: 2, @@ -1112,7 +1149,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/main.rs", args.source.path.unwrap()); + assert_eq!("/project/main.rs", args.source.path.unwrap()); assert_eq!( vec![SourceBreakpoint { line: 3, @@ -1173,38 +1210,33 @@ async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( let fs = FakeFs::new(executor.clone()); fs.insert_tree( - "/a", + "/project", json!({ "main.rs": "First line\nSecond line\nThird line\nFourth line", }), ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); + let worktree = worktree_from_project(&project, cx); let worktree_id = workspace - .update(cx, |workspace, _window, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }) + .update(cx, |_, _, cx| worktree.read(cx).id()) .unwrap(); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -1248,7 +1280,7 @@ async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( .on_request::({ let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { - assert_eq!("/a/main.rs", args.source.path.unwrap()); + assert_eq!("/project/main.rs", args.source.path.unwrap()); assert_eq!( vec![SourceBreakpoint { line: 2, @@ -1315,24 +1347,30 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails( let fs = FakeFs::new(executor.clone()); - let project = Project::test(fs, [], cx).await; + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 87c9e675588476..31c922c0423c2d 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -51,19 +51,17 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -215,19 +213,17 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -462,19 +458,17 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 2d593b08f82be3..60682cdb0675d5 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -46,19 +46,17 @@ async fn test_basic_fetch_initial_scope_and_variables( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -270,19 +268,17 @@ async fn test_fetch_variables_for_multiple_scopes( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); @@ -539,19 +535,17 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }) + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) }); let (session, client) = task.await.unwrap(); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 084be5779cfd9b..cfb64ebee41841 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -35,12 +35,11 @@ use dap::{ use dap_adapters::build_adapter; use fs::Fs; use futures::future::Shared; -use futures::FutureExt; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; use http_client::HttpClient; use language::{ proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, - Buffer, BufferSnapshot, LanguageRegistry, LanguageServerBinaryStatus, + Buffer, BufferSnapshot, LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, }; use lsp::LanguageServerName; use node_runtime::NodeRuntime; @@ -65,6 +64,7 @@ use std::{ use task::{AttachConfig, DebugAdapterConfig, DebugRequestType}; use text::Point; use util::{merge_json_value_into, ResultExt as _}; +use worktree::Worktree; pub enum DapStoreEvent { DebugClientStarted((DebugSessionId, DebugAdapterClientId)), @@ -92,10 +92,14 @@ pub enum DapStoreMode { } pub struct LocalDapStore { + fs: Arc, + node_runtime: NodeRuntime, next_client_id: AtomicUsize, next_session_id: AtomicUsize, - delegate: DapAdapterDelegate, + http_client: Arc, environment: Entity, + language_registry: Arc, + toolchain_store: Arc, sessions: HashMap>, client_by_session: HashMap, } @@ -182,25 +186,24 @@ impl DapStore { http_client: Arc, node_runtime: NodeRuntime, fs: Arc, - languages: Arc, + language_registry: Arc, environment: Entity, + toolchain_store: Arc, cx: &mut Context, ) -> Self { cx.on_app_quit(Self::shutdown_sessions).detach(); Self { mode: DapStoreMode::Local(LocalDapStore { + fs, environment, + http_client, + node_runtime, + toolchain_store, + language_registry, sessions: HashMap::default(), next_client_id: Default::default(), next_session_id: Default::default(), - delegate: DapAdapterDelegate::new( - Some(http_client.clone()), - Some(node_runtime.clone()), - fs.clone(), - languages.clone(), - Task::ready(None).shared(), - ), client_by_session: Default::default(), }), downstream_client: None, @@ -210,11 +213,7 @@ impl DapStore { } } - pub fn new_remote( - project_id: u64, - upstream_client: AnyProtoClient, - _: &mut Context, - ) -> Self { + pub fn new_remote(project_id: u64, upstream_client: AnyProtoClient) -> Self { Self { mode: DapStoreMode::Remote(RemoteDapStore { upstream_client: Some(upstream_client), @@ -668,6 +667,7 @@ impl DapStore { fn start_client_internal( &mut self, session_id: DebugSessionId, + delegate: Arc, config: DebugAdapterConfig, cx: &mut Context, ) -> Task>> { @@ -675,14 +675,7 @@ impl DapStore { return Task::ready(Err(anyhow!("cannot start client on remote side"))); }; - let mut adapter_delegate = local_store.delegate.clone(); - let worktree_abs_path = config.cwd.as_ref().map(|p| Arc::from(p.as_path())); - adapter_delegate.refresh_shell_env_task(local_store.environment.update(cx, |env, cx| { - env.get_environment(None, worktree_abs_path, cx) - })); - let adapter_delegate = Arc::new(adapter_delegate); - - let client_id = self.as_local().unwrap().next_client_id(); + let client_id = local_store.next_client_id(); cx.spawn(|this, mut cx| async move { let adapter = build_adapter(&config.kind).await?; @@ -701,11 +694,11 @@ impl DapStore { })?; let (adapter, binary) = match adapter - .get_binary(adapter_delegate.as_ref(), &config, binary) + .get_binary(delegate.as_ref(), &config, binary, &mut cx) .await { Err(error) => { - adapter_delegate.update_status( + delegate.update_status( adapter.name(), DapStatus::Failed { error: error.to_string(), @@ -715,9 +708,9 @@ impl DapStore { return Err(error); } Ok(mut binary) => { - adapter_delegate.update_status(adapter.name(), DapStatus::None); + delegate.update_status(adapter.name(), DapStatus::None); - let shell_env = adapter_delegate.shell_env().await; + let shell_env = delegate.shell_env().await; let mut envs = binary.envs.unwrap_or_default(); envs.extend(shell_env); binary.envs = Some(envs); @@ -755,14 +748,29 @@ impl DapStore { pub fn start_debug_session( &mut self, config: DebugAdapterConfig, + worktree: &Entity, cx: &mut Context, ) -> Task, Arc)>> { let Some(local_store) = self.as_local() else { return Task::ready(Err(anyhow!("cannot start session on remote side"))); }; + let delegate = Arc::new(DapAdapterDelegate::new( + local_store.fs.clone(), + worktree.read(cx).id(), + local_store.node_runtime.clone(), + local_store.http_client.clone(), + local_store.language_registry.clone(), + local_store.toolchain_store.clone(), + local_store.environment.update(cx, |env, cx| { + let worktree = worktree.read(cx); + env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx) + }), + )); + let session_id = local_store.next_session_id(); - let start_client_task = self.start_client_internal(session_id, config.clone(), cx); + let start_client_task = + self.start_client_internal(session_id, delegate, config.clone(), cx); cx.spawn(|this, mut cx| async move { let session = cx.new(|_| DebugSession::new_local(session_id, config))?; @@ -2363,46 +2371,49 @@ impl SerializedBreakpoint { #[derive(Clone)] pub struct DapAdapterDelegate { fs: Arc, - http_client: Option>, - node_runtime: Option, + worktree_id: WorktreeId, + node_runtime: NodeRuntime, + http_client: Arc, + language_registry: Arc, + toolchain_store: Arc, updated_adapters: Arc>>, - languages: Arc, load_shell_env_task: Shared>>>, } impl DapAdapterDelegate { pub fn new( - http_client: Option>, - node_runtime: Option, fs: Arc, - languages: Arc, + worktree_id: WorktreeId, + node_runtime: NodeRuntime, + http_client: Arc, + language_registry: Arc, + toolchain_store: Arc, load_shell_env_task: Shared>>>, ) -> Self { Self { fs, - languages, + worktree_id, http_client, node_runtime, + toolchain_store, + language_registry, load_shell_env_task, updated_adapters: Default::default(), } } - - pub(crate) fn refresh_shell_env_task( - &mut self, - load_shell_env_task: Shared>>>, - ) { - self.load_shell_env_task = load_shell_env_task; - } } #[async_trait(?Send)] impl dap::adapters::DapDelegate for DapAdapterDelegate { - fn http_client(&self) -> Option> { + fn worktree_id(&self) -> WorktreeId { + self.worktree_id.clone() + } + + fn http_client(&self) -> Arc { self.http_client.clone() } - fn node_runtime(&self) -> Option { + fn node_runtime(&self) -> NodeRuntime { self.node_runtime.clone() } @@ -2423,7 +2434,7 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { DapStatus::CheckingForUpdate => LanguageServerBinaryStatus::CheckingForUpdate, }; - self.languages + self.language_registry .update_dap_status(LanguageServerName(name), status); } @@ -2435,4 +2446,8 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { let task = self.load_shell_env_task.clone(); task.await.unwrap_or_default() } + + fn toolchain_store(&self) -> Arc { + self.toolchain_store.clone() + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 837048a56a6709..d09ba0c0dc7df4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -38,7 +38,8 @@ use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, debugger_settings::DebuggerSettings, messages::Message, - session::DebugSessionId, + session::{DebugSession, DebugSessionId}, + DebugAdapterConfig, }; use collections::{BTreeSet, HashMap, HashSet}; @@ -663,6 +664,15 @@ impl Project { .detach(); let environment = ProjectEnvironment::new(&worktree_store, env, cx); + let toolchain_store = cx.new(|cx| { + ToolchainStore::local( + languages.clone(), + worktree_store.clone(), + environment.clone(), + cx, + ) + }); + let dap_store = cx.new(|cx| { DapStore::new_local( client.http_client(), @@ -670,6 +680,7 @@ impl Project { fs.clone(), languages.clone(), environment.clone(), + toolchain_store.read(cx).as_language_toolchain_store(), cx, ) }); @@ -694,14 +705,6 @@ impl Project { ) }); - let toolchain_store = cx.new(|cx| { - ToolchainStore::local( - languages.clone(), - worktree_store.clone(), - environment.clone(), - cx, - ) - }); let task_store = cx.new(|cx| { TaskStore::local( fs.clone(), @@ -861,8 +864,7 @@ impl Project { }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - let dap_store = - cx.new(|cx| DapStore::new_remote(SSH_PROJECT_ID, client.clone().into(), cx)); + let dap_store = cx.new(|_| DapStore::new_remote(SSH_PROJECT_ID, client.clone().into())); let git_state = Some(cx.new(|cx| GitState::new(&worktree_store, languages.clone(), cx))); @@ -1031,8 +1033,9 @@ impl Project { })?; let environment = cx.update(|cx| ProjectEnvironment::new(&worktree_store, None, cx))?; + let dap_store = cx.new(|cx| { - let mut dap_store = DapStore::new_remote(remote_id, client.clone().into(), cx); + let mut dap_store = DapStore::new_remote(remote_id, client.clone().into()); dap_store.set_breakpoints_from_proto(response.payload.breakpoints, cx); dap_store.set_debug_sessions_from_proto(response.payload.debug_sessions, cx); @@ -1306,30 +1309,29 @@ impl Project { }) } - pub fn start_debug_adapter_client_from_task( + pub fn start_debug_session( &mut self, - debug_task: task::ResolvedTask, + config: DebugAdapterConfig, cx: &mut Context, - ) { - if let Some(config) = debug_task.debug_adapter_config() { - self.dap_store.update(cx, |store, cx| { - store.start_debug_session(config, cx).detach_and_log_err(cx); - }); - } + ) -> Task, Arc)>> { + let worktree = maybe!({ + if let Some(cwd) = &config.cwd { + Some(self.find_worktree(cwd.as_path(), cx)?.0) + } else { + self.worktrees(cx).next() + } + }); + + let Some(worktree) = &worktree else { + return Task::ready(Err(anyhow!("Failed to find a worktree"))); + }; + + self.dap_store.update(cx, |dap_store, cx| { + dap_store.start_debug_session(config, worktree, cx) + }) } /// Get all serialized breakpoints that belong to a buffer - /// - /// # Parameters - /// `buffer_id`: The buffer id to get serialized breakpoints of - /// `cx`: The context of the editor - /// - /// # Return - /// `None`: If the buffer associated with buffer id doesn't exist or this editor - /// doesn't belong to a project - /// - /// `(Path, Vec Option { + pub fn resolved_debug_adapter_config(&self) -> Option { match self.original_task.task_type.clone() { TaskType::Script => None, TaskType::Debug(mut adapter_config) => { diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 609422c0c13687..eff7a9e92018c4 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -333,9 +333,10 @@ impl PickerDelegate for TasksModalDelegate { omit_history_entry, cx, ), - // This would allow users to access to debug history and other issues TaskType::Debug(_) => workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client_from_task(task, cx) + project + .start_debug_session(task.resolved_debug_adapter_config().unwrap(), cx) + .detach_and_log_err(cx); }), }; }) @@ -502,7 +503,9 @@ impl PickerDelegate for TasksModalDelegate { // TODO: Should create a schedule_resolved_debug_task function // This would allow users to access to debug history and other issues TaskType::Debug(_) => workspace.project().update(cx, |project, cx| { - project.start_debug_adapter_client_from_task(task, cx) + project + .start_debug_session(task.resolved_debug_adapter_config().unwrap(), cx) + .detach_and_log_err(cx); }), }; }) From 197166a8c1514210d9388bbc2837e4cf44b8cd82 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 31 Jan 2025 04:33:35 -0500 Subject: [PATCH 491/650] Fix clippy errors --- crates/project/src/dap_store.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index cfb64ebee41841..b06caf0e8d3ecc 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -667,7 +667,7 @@ impl DapStore { fn start_client_internal( &mut self, session_id: DebugSessionId, - delegate: Arc, + delegate: DapAdapterDelegate, config: DebugAdapterConfig, cx: &mut Context, ) -> Task>> { @@ -694,7 +694,7 @@ impl DapStore { })?; let (adapter, binary) = match adapter - .get_binary(delegate.as_ref(), &config, binary, &mut cx) + .get_binary(&delegate, &config, binary, &mut cx) .await { Err(error) => { @@ -755,7 +755,7 @@ impl DapStore { return Task::ready(Err(anyhow!("cannot start session on remote side"))); }; - let delegate = Arc::new(DapAdapterDelegate::new( + let delegate = DapAdapterDelegate::new( local_store.fs.clone(), worktree.read(cx).id(), local_store.node_runtime.clone(), @@ -766,7 +766,7 @@ impl DapStore { let worktree = worktree.read(cx); env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx) }), - )); + ); let session_id = local_store.next_session_id(); let start_client_task = @@ -2406,7 +2406,7 @@ impl DapAdapterDelegate { #[async_trait(?Send)] impl dap::adapters::DapDelegate for DapAdapterDelegate { fn worktree_id(&self) -> WorktreeId { - self.worktree_id.clone() + self.worktree_id } fn http_client(&self) -> Arc { From ef3a6deb05f99ea29abc8353276cc7e61b299b2c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 1 Feb 2025 16:52:48 +0100 Subject: [PATCH 492/650] Improve the visibility of process entries (#105) Before this change it was impossible to see the command arguments, as the executable was to long. This changes that so we use the process name as executable name VSCode as seems to do this. So you could still see the arguments of the program. I also added a tooltip to see the correct executable + arguments. --- crates/debugger_ui/src/attach_modal.rs | 50 +++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index feabb90d41dd64..bbbcd61b112c80 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -7,7 +7,7 @@ use picker::{Picker, PickerDelegate}; use project::dap_store::DapStore; use std::sync::Arc; use sysinfo::System; -use ui::{prelude::*, Context}; +use ui::{prelude::*, Context, Tooltip}; use ui::{ListItem, ListItemSpacing}; use workspace::ModalView; @@ -15,7 +15,7 @@ use workspace::ModalView; struct Candidate { pid: u32, name: String, - command: String, + command: Vec, } pub(crate) struct AttachModalDelegate { @@ -152,9 +152,8 @@ impl PickerDelegate for AttachModalDelegate { command: process .cmd() .iter() - .map(|s| s.to_string_lossy()) - .collect::>() - .join(" "), + .map(|s| s.to_string_lossy().to_string()) + .collect::>(), }) .collect::>(); @@ -175,8 +174,13 @@ impl PickerDelegate for AttachModalDelegate { .map(|(id, candidate)| { StringMatchCandidate::new( id, - format!("{} {} {}", candidate.command, candidate.pid, candidate.name) - .as_str(), + format!( + "{} {} {}", + candidate.command.join(" "), + candidate.pid, + candidate.name + ) + .as_str(), ) }) .collect::>(), @@ -249,18 +253,40 @@ impl PickerDelegate for AttachModalDelegate { let candidate = &candidates.get(hit.candidate_id)?; Some( - ListItem::new(SharedString::from(format!("attach-modal-{ix}"))) + ListItem::new(SharedString::from(format!("process-entry-{ix}"))) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .child( v_flex() .items_start() - .child(Label::new(candidate.command.clone())) + .child(Label::new(format!("{} {}", candidate.name, candidate.pid))) .child( - Label::new(format!("Pid: {}, name: {}", candidate.pid, candidate.name)) - .size(LabelSize::Small) - .color(Color::Muted), + div() + .id(SharedString::from(format!("process-entry-{ix}-command"))) + .tooltip(Tooltip::text( + candidate + .command + .clone() + .into_iter() + .collect::>() + .join(" "), + )) + .child( + Label::new(format!( + "{} {}", + candidate.name, + candidate + .command + .clone() + .into_iter() + .skip(1) + .collect::>() + .join(" ") + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), ), ), ) From 889949ca761c6c83c303beec38306eb182332d19 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 2 Feb 2025 13:39:12 -0500 Subject: [PATCH 493/650] Add lldb attach support --- crates/dap_adapters/src/lldb.rs | 29 +++++++++++++++++++++++++++-- docs/src/debugger.md | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index ae506fbae2a017..496a4ecb975b37 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -1,10 +1,11 @@ -use std::{ffi::OsStr, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::Arc}; use anyhow::Result; use async_trait::async_trait; use dap::transport::{StdioTransport, Transport}; use gpui::AsyncApp; -use task::DebugAdapterConfig; +use sysinfo::{Pid, Process}; +use task::{DebugAdapterConfig, DebugRequestType}; use crate::*; @@ -84,9 +85,33 @@ impl DebugAdapter for LldbDebugAdapter { } fn request_args(&self, config: &DebugAdapterConfig) -> Value { + let pid = if let DebugRequestType::Attach(attach_config) = &config.request { + attach_config.process_id + } else { + None + }; + json!({ "program": config.program, + "request": match config.request { + DebugRequestType::Launch => "launch", + DebugRequestType::Attach(_) => "attach", + }, + "pid": pid, "cwd": config.cwd, }) } + + fn supports_attach(&self) -> bool { + true + } + + fn attach_processes<'a>( + &self, + processes: &'a HashMap, + ) -> Option> { + // let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap(); + + Some(processes.iter().collect::>()) + } } diff --git a/docs/src/debugger.md b/docs/src/debugger.md index b06f2f263c6686..0e8809492698ff 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -62,7 +62,7 @@ To create a custom debug configuration you have to create a `.zed/debug.json` fi ### Using Attach [WIP] -Only javascript supports starting a debug session using attach. +Only javascript and lldb supports starting a debug session using attach. When using the attach request with a process ID the syntax is as follows: From b6b7ad38b5480695d83f5d94c7988aa94e349531 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 3 Feb 2025 10:11:35 +0100 Subject: [PATCH 494/650] Remove commented code in lldb attach code --- crates/dap_adapters/src/lldb.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 496a4ecb975b37..ec274bd05d0ae0 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -110,8 +110,6 @@ impl DebugAdapter for LldbDebugAdapter { &self, processes: &'a HashMap, ) -> Option> { - // let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap(); - Some(processes.iter().collect::>()) } } From c9940753275e4a71bd3197d9565fd6d3cf23badd Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 3 Feb 2025 08:22:06 -0500 Subject: [PATCH 495/650] Clean up Debugger collab tests (#104) The debugger collab tests are difficult to understand and read due to the sheer size of each test and the setup involved to start a collab debugger session. This PR aims to mitigate these problems by creating a struct called Zed Instance that manages setting up tests and joining/rejoining collab debug sessions. * Create util functions to set up debugger collab tests * WIP converting debugger collab tests to use ZedInstance * Clean up collab test utility functions to work with 3 member calls * Update item set on join project test to use new api * Update test update breakpoints send to dap * Update test_ignore_breakpoints * Update last collab tests * Don't setup file tree for tests that don't need it --- crates/collab/src/tests/debug_panel_tests.rs | 853 ++++++++----------- 1 file changed, 369 insertions(+), 484 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 7f19c8cfbed752..0f9627c8f21a44 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -8,7 +8,7 @@ use dap::{Scope, Variable}; use debugger_ui::{debugger_panel::DebugPanel, variable_list::VariableContainer}; use editor::Editor; use gpui::{Entity, TestAppContext, VisualTestContext}; -use project::ProjectPath; +use project::{Project, ProjectPath, WorktreeId}; use serde_json::json; use std::sync::Arc; use std::{ @@ -17,7 +17,7 @@ use std::{ }; use workspace::{dock::Panel, Workspace}; -use super::TestServer; +use super::{TestClient, TestServer}; pub fn init_test(cx: &mut gpui::TestAppContext) { if std::env::var("RUST_LOG").is_ok() { @@ -48,50 +48,151 @@ async fn add_debugger_panel(workspace: &Entity, cx: &mut VisualTestCo }); } -#[gpui::test] -async fn test_debug_panel_item_opens_on_remote( - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let executor = cx_a.executor(); - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; +struct ZedInstance<'a> { + client: TestClient, + project: Option>, + active_call: Entity, + cx: &'a mut TestAppContext, +} + +impl<'a> ZedInstance<'a> { + fn new(client: TestClient, cx: &'a mut TestAppContext) -> Self { + ZedInstance { + project: None, + client, + active_call: cx.read(ActiveCall::global), + cx, + } + } + + async fn host_project( + &mut self, + project_files: Option, + ) -> (u64, WorktreeId) { + let (project, worktree_id) = self.client.build_local_project("/project", self.cx).await; + self.active_call + .update(self.cx, |call, cx| call.set_location(Some(&project), cx)) + .await + .unwrap(); + + if let Some(tree) = project_files { + self.client.fs().insert_tree("/project", tree).await; + } + + self.project = Some(project.clone()); + + let project_id = self + .active_call + .update(self.cx, |call, cx| call.share_project(project, cx)) + .await + .unwrap(); + + (project_id, worktree_id) + } + + async fn join_project(&mut self, project_id: u64) { + let remote_project = self.client.join_remote_project(project_id, self.cx).await; + self.project = Some(remote_project); + + self.active_call + .update(self.cx, |call, cx| { + call.set_location(self.project.as_ref(), cx) + }) + .await + .unwrap(); + } - init_test(cx_a); - init_test(cx_b); + async fn expand( + &'a mut self, + ) -> ( + &'a TestClient, + Entity, + Entity, + &'a mut VisualTestContext, + ) { + let (workspace, cx) = self.client.build_workspace( + self.project + .as_ref() + .expect("Project should be hosted or built before expanding"), + self.cx, + ); + add_debugger_panel(&workspace, cx).await; + (&self.client, workspace, self.project.clone().unwrap(), cx) + } +} + +async fn setup_three_member_test<'a, 'b, 'c>( + server: &mut TestServer, + host_cx: &'a mut TestAppContext, + first_remote_cx: &'b mut TestAppContext, + second_remote_cx: &'c mut TestAppContext, +) -> (ZedInstance<'a>, ZedInstance<'b>, ZedInstance<'c>) { + let host_client = server.create_client(host_cx, "user_host").await; + let first_remote_client = server.create_client(first_remote_cx, "user_remote_1").await; + let second_remote_client = server + .create_client(second_remote_cx, "user_remote_2") + .await; + + init_test(host_cx); + init_test(first_remote_cx); + init_test(second_remote_cx); server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .create_room(&mut [ + (&host_client, host_cx), + (&first_remote_client, first_remote_cx), + (&second_remote_client, second_remote_cx), + ]) .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); + let host_zed = ZedInstance::new(host_client, host_cx); + let first_remote_zed = ZedInstance::new(first_remote_client, first_remote_cx); + let second_remote_zed = ZedInstance::new(second_remote_client, second_remote_cx); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.join_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); + (host_zed, first_remote_zed, second_remote_zed) +} + +async fn setup_two_member_test<'a, 'b>( + server: &mut TestServer, + host_cx: &'a mut TestAppContext, + remote_cx: &'b mut TestAppContext, +) -> (ZedInstance<'a>, ZedInstance<'b>) { + let host_client = server.create_client(host_cx, "user_host").await; + let remote_client = server.create_client(remote_cx, "user_remote").await; + + init_test(host_cx); + init_test(remote_cx); + + server + .create_room(&mut [(&host_client, host_cx), (&remote_client, remote_cx)]) + .await; + + let host_zed = ZedInstance::new(host_client, host_cx); + let remote_zed = ZedInstance::new(remote_client, remote_cx); + + (host_zed, remote_zed) +} + +#[gpui::test] +async fn test_debug_panel_item_opens_on_remote( + host_cx: &mut TestAppContext, + remote_cx: &mut TestAppContext, +) { + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; - let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let (mut host_zed, mut remote_zed) = + setup_two_member_test(&mut server, host_cx, remote_cx).await; - add_debugger_panel(&workspace_a, cx_a).await; - add_debugger_panel(&workspace_b, cx_b).await; + let (host_project_id, _) = host_zed.host_project(None).await; + remote_zed.join_project(host_project_id).await; - cx_b.run_until_parked(); + let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await; + let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; - let task = project_a.update(cx_a, |project, cx| { + remote_cx.run_until_parked(); + + let task = host_project.update(host_cx, |project, cx| { project.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), @@ -141,10 +242,10 @@ async fn test_debug_panel_item_opens_on_remote( })) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - workspace_b.update(cx_b, |workspace, cx| { + remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -158,7 +259,7 @@ async fn test_debug_panel_item_opens_on_remote( assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); }); - let shutdown_client = project_a.update(cx_a, |project, cx| { + let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) @@ -169,41 +270,22 @@ async fn test_debug_panel_item_opens_on_remote( #[gpui::test] async fn test_active_debug_panel_item_set_on_join_project( - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, + host_cx: &mut TestAppContext, + remote_cx: &mut TestAppContext, ) { - let executor = cx_a.executor(); - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - init_test(cx_a); - init_test(cx_b); - - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; - let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); + let (mut host_zed, mut remote_zed) = + setup_two_member_test(&mut server, host_cx, remote_cx).await; - let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (host_project_id, _) = host_zed.host_project(None).await; - add_debugger_panel(&workspace_a, cx_a).await; + let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await; - cx_a.run_until_parked(); + host_cx.run_until_parked(); - let task = project_a.update(cx_a, |project, cx| { + let task = host_project.update(host_cx, |project, cx| { project.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), @@ -253,25 +335,16 @@ async fn test_active_debug_panel_item_set_on_join_project( })) .await; - // Give client_a time to send a debug panel item to collab server - cx_a.run_until_parked(); - - let project_b = client_b.join_remote_project(project_id, cx_b).await; - - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - add_debugger_panel(&workspace_b, cx_b).await; + // Give host_client time to send a debug panel item to collab server + host_cx.run_until_parked(); - cx_b.run_until_parked(); + remote_zed.join_project(host_project_id).await; + let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - cx_a.run_until_parked(); - cx_b.run_until_parked(); - - workspace_b.update(cx_b, |workspace, cx| { + remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -285,7 +358,7 @@ async fn test_active_debug_panel_item_set_on_join_project( assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); }); - let shutdown_client = project_a.update(cx_a, |project, cx| { + let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) @@ -293,10 +366,10 @@ async fn test_active_debug_panel_item_set_on_join_project( shutdown_client.await.unwrap(); - cx_b.run_until_parked(); + remote_cx.run_until_parked(); // assert we don't have a debug panel item anymore because the client shutdown - workspace_b.update(cx_b, |workspace, cx| { + remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |this, cx| { @@ -308,46 +381,22 @@ async fn test_active_debug_panel_item_set_on_join_project( #[gpui::test] async fn test_debug_panel_remote_button_presses( - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, + host_cx: &mut TestAppContext, + remote_cx: &mut TestAppContext, ) { - let executor = cx_a.executor(); - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - init_test(cx_a); - init_test(cx_b); - - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; - let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); + let (mut host_zed, mut remote_zed) = + setup_two_member_test(&mut server, host_cx, remote_cx).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.join_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); + let (host_project_id, _) = host_zed.host_project(None).await; + remote_zed.join_project(host_project_id).await; - let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; + let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; - add_debugger_panel(&workspace_a, cx_a).await; - add_debugger_panel(&workspace_b, cx_b).await; - - let task = project_a.update(cx_a, |project, cx| { + let task = host_project.update(host_cx, |project, cx| { project.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), @@ -405,10 +454,10 @@ async fn test_debug_panel_remote_button_presses( }) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - let remote_debug_item = workspace_b.update(cx_b, |workspace, cx| { + let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -423,7 +472,7 @@ async fn test_debug_panel_remote_button_presses( active_debug_panel_item }); - let local_debug_item = workspace_a.update(cx_a, |workspace, cx| { + let local_debug_item = host_workspace.update(host_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -438,21 +487,21 @@ async fn test_debug_panel_remote_button_presses( active_debug_panel_item }); - remote_debug_item.update(cx_b, |this, cx| { + remote_debug_item.update(remote_cx, |this, cx| { this.continue_thread(cx); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - local_debug_item.update(cx_a, |debug_panel_item, cx| { + local_debug_item.update(host_cx, |debug_panel_item, cx| { assert_eq!( debugger_ui::debugger_panel::ThreadStatus::Running, debug_panel_item.thread_state().read(cx).status, ); }); - remote_debug_item.update(cx_b, |debug_panel_item, cx| { + remote_debug_item.update(remote_cx, |debug_panel_item, cx| { assert_eq!( debugger_ui::debugger_panel::ThreadStatus::Running, debug_panel_item.thread_state().read(cx).status, @@ -480,17 +529,17 @@ async fn test_debug_panel_remote_button_presses( }) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - local_debug_item.update(cx_a, |debug_panel_item, cx| { + local_debug_item.update(host_cx, |debug_panel_item, cx| { assert_eq!( debugger_ui::debugger_panel::ThreadStatus::Stopped, debug_panel_item.thread_state().read(cx).status, ); }); - remote_debug_item.update(cx_b, |debug_panel_item, cx| { + remote_debug_item.update(remote_cx, |debug_panel_item, cx| { assert_eq!( debugger_ui::debugger_panel::ThreadStatus::Stopped, debug_panel_item.thread_state().read(cx).status, @@ -505,21 +554,21 @@ async fn test_debug_panel_remote_button_presses( }) .await; - local_debug_item.update(cx_a, |this, cx| { + local_debug_item.update(host_cx, |this, cx| { this.continue_thread(cx); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - local_debug_item.update(cx_a, |debug_panel_item, cx| { + local_debug_item.update(host_cx, |debug_panel_item, cx| { assert_eq!( debugger_ui::debugger_panel::ThreadStatus::Running, debug_panel_item.thread_state().read(cx).status, ); }); - remote_debug_item.update(cx_b, |debug_panel_item, cx| { + remote_debug_item.update(remote_cx, |debug_panel_item, cx| { assert_eq!( debugger_ui::debugger_panel::ThreadStatus::Running, debug_panel_item.thread_state().read(cx).status, @@ -551,18 +600,18 @@ async fn test_debug_panel_remote_button_presses( })) .await; - remote_debug_item.update(cx_b, |this, cx| { + remote_debug_item.update(remote_cx, |this, cx| { this.pause_thread(cx); }); - cx_b.run_until_parked(); - cx_a.run_until_parked(); + remote_cx.run_until_parked(); + host_cx.run_until_parked(); client .on_request::(move |_, _| Ok(())) .await; - remote_debug_item.update(cx_b, |this, cx| { + remote_debug_item.update(remote_cx, |this, cx| { this.step_out(cx); }); @@ -578,14 +627,14 @@ async fn test_debug_panel_remote_button_presses( })) .await; - cx_b.run_until_parked(); - cx_a.run_until_parked(); + remote_cx.run_until_parked(); + host_cx.run_until_parked(); client .on_request::(move |_, _| Ok(())) .await; - remote_debug_item.update(cx_b, |this, cx| { + remote_debug_item.update(remote_cx, |this, cx| { this.step_over(cx); }); @@ -601,14 +650,14 @@ async fn test_debug_panel_remote_button_presses( })) .await; - cx_b.run_until_parked(); - cx_a.run_until_parked(); + remote_cx.run_until_parked(); + host_cx.run_until_parked(); client .on_request::(move |_, _| Ok(())) .await; - remote_debug_item.update(cx_b, |this, cx| { + remote_debug_item.update(remote_cx, |this, cx| { this.step_in(cx); }); @@ -624,14 +673,14 @@ async fn test_debug_panel_remote_button_presses( })) .await; - cx_b.run_until_parked(); - cx_a.run_until_parked(); + remote_cx.run_until_parked(); + host_cx.run_until_parked(); client .on_request::(move |_, _| Ok(())) .await; - remote_debug_item.update(cx_b, |this, cx| { + remote_debug_item.update(remote_cx, |this, cx| { this.step_back(cx); }); @@ -647,18 +696,18 @@ async fn test_debug_panel_remote_button_presses( })) .await; - cx_b.run_until_parked(); - cx_a.run_until_parked(); + remote_cx.run_until_parked(); + host_cx.run_until_parked(); - remote_debug_item.update(cx_b, |this, cx| { + remote_debug_item.update(remote_cx, |this, cx| { this.stop_thread(cx); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); // assert we don't have a debug panel item anymore because the client shutdown - workspace_b.update(cx_b, |workspace, cx| { + remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel.update(cx, |this, cx| { @@ -669,46 +718,22 @@ async fn test_debug_panel_remote_button_presses( } #[gpui::test] -async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { - let executor = cx_a.executor(); - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - init_test(cx_a); - init_test(cx_b); +async fn test_restart_stack_frame(host_cx: &mut TestAppContext, remote_cx: &mut TestAppContext) { + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; - let called_restart_frame = Arc::new(AtomicBool::new(false)); + let (mut host_zed, mut remote_zed) = + setup_two_member_test(&mut server, host_cx, remote_cx).await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.join_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); + let (host_project_id, _) = host_zed.host_project(None).await; + remote_zed.join_project(host_project_id).await; - let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await; + let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; - add_debugger_panel(&workspace_a, cx_a).await; - add_debugger_panel(&workspace_b, cx_b).await; + let called_restart_frame = Arc::new(AtomicBool::new(false)); - let task = project_a.update(cx_a, |project, cx| { + let task = host_project.update(host_cx, |project, cx| { project.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), @@ -799,11 +824,11 @@ async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppC })) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); // try to restart stack frame 1 from the guest side - workspace_b.update(cx_b, |workspace, cx| { + remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -818,15 +843,15 @@ async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppC }); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); assert!( called_restart_frame.load(std::sync::atomic::Ordering::SeqCst), "Restart stack frame was not called" ); - let shutdown_client = project_a.update(cx_a, |project, cx| { + let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) @@ -837,61 +862,30 @@ async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppC #[gpui::test] async fn test_updated_breakpoints_send_to_dap( - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, + host_cx: &mut TestAppContext, + remote_cx: &mut TestAppContext, ) { - let executor = cx_a.executor(); - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - - client_a - .fs() - .insert_tree( - "/project", - json!({ - "test.txt": "one\ntwo\nthree\nfour\nfive", - }), - ) - .await; + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; - init_test(cx_a); - init_test(cx_b); + let (mut host_zed, mut remote_zed) = + setup_two_member_test(&mut server, host_cx, remote_cx).await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + let (host_project_id, worktree_id) = host_zed + .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"}))) .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); + remote_zed.join_project(host_project_id).await; + + let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; + let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; let project_path = ProjectPath { worktree_id, path: Arc::from(Path::new(&"test.txt")), }; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.join_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - - add_debugger_panel(&workspace_a, cx_a).await; - add_debugger_panel(&workspace_b, cx_b).await; - - let task = project_a.update(cx_a, |project, cx| { + let task = host_project.update(host_cx, |project, cx| { project.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), @@ -968,12 +962,12 @@ async fn test_updated_breakpoints_send_to_dap( })) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); // Client B opens an editor. - let editor_b = workspace_b - .update_in(cx_b, |workspace, window, cx| { + let editor_b = remote_workspace + .update_in(remote_cx, |workspace, window, cx| { workspace.open_path(project_path.clone(), None, true, window, cx) }) .await @@ -981,15 +975,15 @@ async fn test_updated_breakpoints_send_to_dap( .downcast::() .unwrap(); - editor_b.update_in(cx_b, |editor, window, cx| { + editor_b.update_in(remote_cx, |editor, window, cx| { editor.move_down(&editor::actions::MoveDown, window, cx); editor.move_down(&editor::actions::MoveDown, window, cx); editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); // Client A opens an editor. - let editor_a = workspace_a - .update_in(cx_a, |workspace, window, cx| { + let editor_a = host_workspace + .update_in(host_cx, |workspace, window, cx| { workspace.open_path(project_path.clone(), None, true, window, cx) }) .await @@ -997,8 +991,8 @@ async fn test_updated_breakpoints_send_to_dap( .downcast::() .unwrap(); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); let called_set_breakpoints = Arc::new(AtomicBool::new(false)); client @@ -1019,14 +1013,14 @@ async fn test_updated_breakpoints_send_to_dap( .await; // remove the breakpoint that client B added - editor_a.update_in(cx_a, |editor, window, cx| { + editor_a.update_in(host_cx, |editor, window, cx| { editor.move_down(&editor::actions::MoveDown, window, cx); editor.move_down(&editor::actions::MoveDown, window, cx); editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); assert!( called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), @@ -1074,21 +1068,21 @@ async fn test_updated_breakpoints_send_to_dap( .await; // Add our own breakpoint now - editor_a.update_in(cx_a, |editor, window, cx| { + editor_a.update_in(host_cx, |editor, window, cx| { editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); editor.move_up(&editor::actions::MoveUp, window, cx); editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); assert!( called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), "SetBreakpoint request must be called" ); - let shutdown_client = project_a.update(cx_a, |project, cx| { + let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) @@ -1099,50 +1093,24 @@ async fn test_updated_breakpoints_send_to_dap( #[gpui::test] async fn test_module_list( - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, + host_cx: &mut TestAppContext, + remote_cx: &mut TestAppContext, + late_join_cx: &mut TestAppContext, ) { - let executor = cx_a.executor(); - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - - init_test(cx_a); - init_test(cx_b); - init_test(cx_c); + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - let active_call_c = cx_c.read(ActiveCall::global); - - let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); + let (mut host_zed, mut remote_zed, mut late_join_zed) = + setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.join_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); + let (host_project_id, _worktree_id) = host_zed.host_project(None).await; - let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + remote_zed.join_project(host_project_id).await; - add_debugger_panel(&workspace_a, cx_a).await; - add_debugger_panel(&workspace_b, cx_b).await; + let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; + let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; - let task = project_a.update(cx_a, |project, cx| { + let task = host_project.update(host_cx, |project, cx| { project.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), @@ -1233,8 +1201,8 @@ async fn test_module_list( }) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); assert!( called_initialize.load(std::sync::atomic::Ordering::SeqCst), @@ -1253,15 +1221,15 @@ async fn test_module_list( })) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); assert!( called_modules.load(std::sync::atomic::Ordering::SeqCst), "Request Modules must be called" ); - workspace_a.update(cx_a, |workspace, cx| { + host_workspace.update(host_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -1289,7 +1257,7 @@ async fn test_module_list( }) }); - workspace_b.update(cx_b, |workspace, cx| { + remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -1316,21 +1284,11 @@ async fn test_module_list( }) }); - let project_c = client_c.join_remote_project(project_id, cx_c).await; - active_call_c - .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) - .await - .unwrap(); - - let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); - - add_debugger_panel(&workspace_c, cx_c).await; - - cx_c.run_until_parked(); + late_join_zed.join_project(host_project_id).await; + let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) = + late_join_zed.expand().await; - cx_c.run_until_parked(); - - workspace_c.update(cx_c, |workspace, cx| { + late_join_workspace.update(late_join_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -1359,7 +1317,7 @@ async fn test_module_list( client.on_request::(move |_, _| Ok(())).await; - let shutdown_client = project_a.update(cx_a, |project, cx| { + let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) @@ -1370,50 +1328,26 @@ async fn test_module_list( #[gpui::test] async fn test_variable_list( - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, + host_cx: &mut TestAppContext, + remote_cx: &mut TestAppContext, + late_join_cx: &mut TestAppContext, ) { - let executor = cx_a.executor(); - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; - init_test(cx_a); - init_test(cx_b); - init_test(cx_c); + let (mut host_zed, mut remote_zed, mut late_join_zed) = + setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + let (host_project_id, _worktree_id) = host_zed + .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"}))) .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - let active_call_c = cx_c.read(ActiveCall::global); - - let (project_a, _worktree_id) = client_a.build_local_project("/project", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.join_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + remote_zed.join_project(host_project_id).await; - add_debugger_panel(&workspace_a, cx_a).await; - add_debugger_panel(&workspace_b, cx_b).await; + let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; + let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; - let task = project_a.update(cx_a, |project, cx| { + let task = host_project.update(host_cx, |project, cx| { project.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), @@ -1578,11 +1512,10 @@ async fn test_variable_list( })) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); - cx_c.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - let local_debug_item = workspace_a.update(cx_a, |workspace, cx| { + let local_debug_item = host_workspace.update(host_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -1597,7 +1530,7 @@ async fn test_variable_list( active_debug_panel_item }); - let remote_debug_item = workspace_b.update(cx_b, |workspace, cx| { + let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -1627,8 +1560,8 @@ async fn test_variable_list( ]; local_debug_item - .update(cx_a, |this, _| this.variable_list().clone()) - .update(cx_a, |variable_list, cx| { + .update(host_cx, |this, _| this.variable_list().clone()) + .update(host_cx, |variable_list, cx| { assert_eq!(1, variable_list.scopes().len()); assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); assert_eq!(&first_variable_containers, &variable_list.variables()); @@ -1650,8 +1583,8 @@ async fn test_variable_list( .await; remote_debug_item - .update(cx_b, |this, _| this.variable_list().clone()) - .update(cx_b, |variable_list, cx| { + .update(remote_cx, |this, _| this.variable_list().clone()) + .update(remote_cx, |variable_list, cx| { assert_eq!(1, variable_list.scopes().len()); assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); assert_eq!(&first_variable_containers, &variable_list.variables()); @@ -1666,9 +1599,8 @@ async fn test_variable_list( ); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); - cx_c.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); let second_req_variable_list = vec![ VariableContainer { @@ -1689,8 +1621,8 @@ async fn test_variable_list( ]; remote_debug_item - .update(cx_b, |this, _| this.variable_list().clone()) - .update(cx_b, |variable_list, cx| { + .update(remote_cx, |this, _| this.variable_list().clone()) + .update(remote_cx, |variable_list, cx| { assert_eq!(1, variable_list.scopes().len()); assert_eq!(3, variable_list.variables().len()); assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); @@ -1721,8 +1653,8 @@ async fn test_variable_list( .await; local_debug_item - .update(cx_a, |this, _| this.variable_list().clone()) - .update(cx_a, |variable_list, cx| { + .update(host_cx, |this, _| this.variable_list().clone()) + .update(host_cx, |variable_list, cx| { assert_eq!(1, variable_list.scopes().len()); assert_eq!(3, variable_list.variables().len()); assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); @@ -1738,9 +1670,8 @@ async fn test_variable_list( ); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); - cx_c.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); let final_variable_containers: Vec = vec![ VariableContainer { @@ -1766,8 +1697,8 @@ async fn test_variable_list( ]; remote_debug_item - .update(cx_b, |this, _| this.variable_list().clone()) - .update(cx_b, |variable_list, cx| { + .update(remote_cx, |this, _| this.variable_list().clone()) + .update(remote_cx, |variable_list, cx| { assert_eq!(1, variable_list.scopes().len()); assert_eq!(4, variable_list.variables().len()); assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); @@ -1785,8 +1716,8 @@ async fn test_variable_list( }); local_debug_item - .update(cx_a, |this, _| this.variable_list().clone()) - .update(cx_a, |variable_list, cx| { + .update(host_cx, |this, _| this.variable_list().clone()) + .update(host_cx, |variable_list, cx| { assert_eq!(1, variable_list.scopes().len()); assert_eq!(4, variable_list.variables().len()); assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); @@ -1803,19 +1734,13 @@ async fn test_variable_list( ); }); - let project_c = client_c.join_remote_project(project_id, cx_c).await; - active_call_c - .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) - .await - .unwrap(); - - let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); - add_debugger_panel(&workspace_c, cx_c).await; + late_join_zed.join_project(host_project_id).await; + let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) = + late_join_zed.expand().await; - cx_c.run_until_parked(); - cx_c.run_until_parked(); + late_join_cx.run_until_parked(); - let last_join_remote_item = workspace_c.update(cx_c, |workspace, cx| { + let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -1831,8 +1756,8 @@ async fn test_variable_list( }); last_join_remote_item - .update(cx_c, |this, _| this.variable_list().clone()) - .update(cx_c, |variable_list, cx| { + .update(late_join_cx, |this, _| this.variable_list().clone()) + .update(late_join_cx, |variable_list, cx| { assert_eq!(1, variable_list.scopes().len()); assert_eq!(4, variable_list.variables().len()); assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); @@ -1843,7 +1768,7 @@ async fn test_variable_list( client.on_request::(move |_, _| Ok(())).await; - let shutdown_client = project_a.update(cx_a, |project, cx| { + let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) @@ -1854,66 +1779,32 @@ async fn test_variable_list( #[gpui::test] async fn test_ignore_breakpoints( - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, + host_cx: &mut TestAppContext, + remote_cx: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - let executor = cx_a.executor(); - let mut server = TestServer::start(executor.clone()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - - client_a - .fs() - .insert_tree( - "/project", - json!({ - "test.txt": "one\ntwo\nthree\nfour\nfive", - }), - ) - .await; + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; - init_test(cx_a); - init_test(cx_b); - init_test(cx_c); + let (mut host_zed, mut remote_zed, mut late_join_zed) = + setup_three_member_test(&mut server, host_cx, remote_cx, cx_c).await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + let (host_project_id, worktree_id) = host_zed + .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"}))) .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - let active_call_c = cx_c.read(ActiveCall::global); - let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); + remote_zed.join_project(host_project_id).await; + + let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; + let (_client_remote, remote_workspace, remote_project, remote_cx) = remote_zed.expand().await; let project_path = ProjectPath { worktree_id, path: Arc::from(Path::new(&"test.txt")), }; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.join_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - - add_debugger_panel(&workspace_a, cx_a).await; - add_debugger_panel(&workspace_b, cx_b).await; - - let local_editor = workspace_a - .update_in(cx_a, |workspace, window, cx| { + let local_editor = host_workspace + .update_in(host_cx, |workspace, window, cx| { workspace.open_path(project_path.clone(), None, true, window, cx) }) .await @@ -1921,7 +1812,7 @@ async fn test_ignore_breakpoints( .downcast::() .unwrap(); - local_editor.update_in(cx_a, |editor, window, cx| { + local_editor.update_in(host_cx, |editor, window, cx| { editor.move_down(&editor::actions::MoveDown, window, cx); editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); // Line 2 editor.move_down(&editor::actions::MoveDown, window, cx); @@ -1929,10 +1820,10 @@ async fn test_ignore_breakpoints( // Line 3 }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - let task = project_a.update(cx_a, |project, cx| { + let task = host_project.update(host_cx, |project, cx| { project.start_debug_session( dap::DebugAdapterConfig { label: "test config".into(), @@ -2019,8 +1910,8 @@ async fn test_ignore_breakpoints( ))) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); assert!( called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), @@ -2039,10 +1930,10 @@ async fn test_ignore_breakpoints( })) .await; - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - let remote_debug_item = workspace_b.update(cx_b, |workspace, cx| { + let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -2091,7 +1982,7 @@ async fn test_ignore_breakpoints( }) .await; - let local_debug_item = workspace_a.update(cx_a, |workspace, cx| { + let local_debug_item = host_workspace.update(host_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -2112,13 +2003,13 @@ async fn test_ignore_breakpoints( active_debug_panel_item }); - local_debug_item.update(cx_a, |item, cx| { + local_debug_item.update(host_cx, |item, cx| { item.toggle_ignore_breakpoints(cx); // Set to true assert_eq!(true, item.are_breakpoints_ignored(cx)); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); assert!( called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), @@ -2138,8 +2029,8 @@ async fn test_ignore_breakpoints( }) .await; - let remote_editor = workspace_b - .update_in(cx_b, |workspace, window, cx| { + let remote_editor = remote_workspace + .update_in(remote_cx, |workspace, window, cx| { workspace.open_path(project_path.clone(), None, true, window, cx) }) .await @@ -2149,20 +2040,20 @@ async fn test_ignore_breakpoints( called_set_breakpoints.store(false, std::sync::atomic::Ordering::SeqCst); - remote_editor.update_in(cx_b, |editor, window, cx| { + remote_editor.update_in(remote_cx, |editor, window, cx| { // Line 1 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); assert!( called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), "SetBreakpoint request be called whenever breakpoints are toggled but with not breakpoints" ); - remote_debug_item.update(cx_b, |debug_panel, cx| { + remote_debug_item.update(remote_cx, |debug_panel, cx| { let breakpoints_ignored = debug_panel.are_breakpoints_ignored(cx); assert_eq!(true, breakpoints_ignored); @@ -2217,19 +2108,13 @@ async fn test_ignore_breakpoints( }) .await; - let project_c = client_c.join_remote_project(project_id, cx_c).await; - active_call_c - .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) - .await - .unwrap(); - - let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); - - add_debugger_panel(&workspace_c, cx_c).await; + late_join_zed.join_project(host_project_id).await; + let (_late_join_client, late_join_workspace, late_join_project, late_join_cx) = + late_join_zed.expand().await; - cx_c.run_until_parked(); + late_join_cx.run_until_parked(); - let last_join_remote_item = workspace_c.update(cx_c, |workspace, cx| { + let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| { let debug_panel = workspace.panel::(cx).unwrap(); let active_debug_panel_item = debug_panel .update(cx, |this, cx| this.active_debug_panel_item(cx)) @@ -2248,13 +2133,13 @@ async fn test_ignore_breakpoints( active_debug_panel_item }); - remote_debug_item.update(cx_b, |item, cx| { + remote_debug_item.update(remote_cx, |item, cx| { item.toggle_ignore_breakpoints(cx); }); - cx_a.run_until_parked(); - cx_b.run_until_parked(); - cx_c.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); + late_join_cx.run_until_parked(); assert!( called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), @@ -2277,7 +2162,7 @@ async fn test_ignore_breakpoints( }) .await; - local_debug_item.update(cx_a, |debug_panel_item, cx| { + local_debug_item.update(host_cx, |debug_panel_item, cx| { assert_eq!( false, debug_panel_item.are_breakpoints_ignored(cx), @@ -2285,7 +2170,7 @@ async fn test_ignore_breakpoints( ); }); - remote_debug_item.update(cx_b, |debug_panel_item, cx| { + remote_debug_item.update(remote_cx, |debug_panel_item, cx| { assert_eq!( false, debug_panel_item.are_breakpoints_ignored(cx), @@ -2293,7 +2178,7 @@ async fn test_ignore_breakpoints( ); }); - last_join_remote_item.update(cx_c, |debug_panel_item, cx| { + last_join_remote_item.update(late_join_cx, |debug_panel_item, cx| { assert_eq!( false, debug_panel_item.are_breakpoints_ignored(cx), @@ -2301,7 +2186,7 @@ async fn test_ignore_breakpoints( ); }); - let shutdown_client = project_a.update(cx_a, |project, cx| { + let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { dap_store.shutdown_session(&session.read(cx).id(), cx) }) @@ -2309,10 +2194,10 @@ async fn test_ignore_breakpoints( shutdown_client.await.unwrap(); - cx_a.run_until_parked(); - cx_b.run_until_parked(); + host_cx.run_until_parked(); + remote_cx.run_until_parked(); - project_b.update(cx_b, |project, cx| { + remote_project.update(remote_cx, |project, cx| { project.dap_store().update(cx, |dap_store, _cx| { let sessions = dap_store.sessions().collect::>(); @@ -2329,7 +2214,7 @@ async fn test_ignore_breakpoints( }) }); - project_c.update(cx_c, |project, cx| { + late_join_project.update(late_join_cx, |project, cx| { project.dap_store().update(cx, |dap_store, _cx| { let sessions = dap_store.sessions().collect::>(); From 47b3f55a17a35be468442bd1bc3986490f4b0798 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 3 Feb 2025 08:35:51 -0500 Subject: [PATCH 496/650] Minor cleanup of breakpoint context menu code --- crates/editor/src/editor.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d8af5e2b37b666..37d9ddbc7193c9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5434,7 +5434,6 @@ impl Editor { cx: &mut Context, ) -> Entity { let weak_editor = cx.weak_entity(); - let weak_editor2 = weak_editor.clone(); let focus_handle = self.focus_handle(cx); let second_entry_msg = if kind.log_message().is_some() { @@ -5446,20 +5445,23 @@ impl Editor { ui::ContextMenu::build(window, cx, |menu, _, _cx| { menu.on_blur_subscription(Subscription::new(|| {})) .context(focus_handle) - .entry("Toggle Breakpoint", None, move |_window, cx| { - weak_editor - .update(cx, |this, cx| { - this.edit_breakpoint_at_anchor( - anchor, - BreakpointKind::Standard, - BreakpointEditAction::Toggle, - cx, - ); - }) - .log_err(); + .entry("Toggle Breakpoint", None, { + let weak_editor = weak_editor.clone(); + move |_window, cx| { + weak_editor + .update(cx, |this, cx| { + this.edit_breakpoint_at_anchor( + anchor, + BreakpointKind::Standard, + BreakpointEditAction::Toggle, + cx, + ); + }) + .log_err(); + } }) .entry(second_entry_msg, None, move |window, cx| { - weak_editor2 + weak_editor .update(cx, |this, cx| { this.add_edit_breakpoint_block(row, anchor, kind.as_ref(), window, cx); }) From 3bf583313560fb896824eaa19d35e1b4aad3ea6d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:48:44 +0100 Subject: [PATCH 497/650] Make Pane take non-optional double click action again Co-authored-by: Anthony --- crates/assistant/src/assistant_panel.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/workspace/src/pane.rs | 11 ++++++----- crates/workspace/src/workspace.rs | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 71c2405976aa30..b3b11fa9c737ef 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -137,7 +137,7 @@ impl AssistantPanel { workspace.project().clone(), Default::default(), None, - Some(NewContext.boxed_clone()), + NewContext.boxed_clone(), window, cx, ); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 73a66aa6a6fb35..79cda0bb326506 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -135,7 +135,7 @@ impl DebugPanel { workspace.project().clone(), Default::default(), None, - None, + gpui::NoAction.boxed_clone(), window, cx, ); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 68b93d13dfda6b..48ff807fc37568 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -910,7 +910,7 @@ pub fn new_terminal_pane( project.clone(), Default::default(), None, - Some(NewTerminal.boxed_clone()), + NewTerminal.boxed_clone(), window, cx, ); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 0efdf79afabe96..7be40a43c1ff07 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -310,7 +310,7 @@ pub struct Pane { /// Is None if navigation buttons are permanently turned off (and should not react to setting changes). /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed. display_nav_history_buttons: Option, - double_click_dispatch_action: Option>, + double_click_dispatch_action: Box, save_modals_spawned: HashSet, close_pane_if_empty: bool, pub new_item_context_menu_handle: PopoverMenuHandle, @@ -384,7 +384,7 @@ impl Pane { project: Entity, next_timestamp: Arc, can_drop_predicate: Option bool + 'static>>, - double_click_dispatch_action: Option>, + double_click_dispatch_action: Box, window: &mut Window, cx: &mut Context, ) -> Self { @@ -2658,9 +2658,10 @@ impl Pane { })) .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| { if event.up.click_count == 2 { - if let Some(action) = &this.double_click_dispatch_action { - window.dispatch_action(action.boxed_clone(), cx); - } + window.dispatch_action( + this.double_click_dispatch_action.boxed_clone(), + cx, + ); } })), ), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9fd2bd62a94f2c..160c7b90a82daa 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -969,7 +969,7 @@ impl Workspace { project.clone(), pane_history_timestamp.clone(), None, - Some(NewFile.boxed_clone()), + NewFile.boxed_clone(), window, cx, ); @@ -2661,7 +2661,7 @@ impl Workspace { self.project.clone(), self.pane_history_timestamp.clone(), None, - Some(NewFile.boxed_clone()), + NewFile.boxed_clone(), window, cx, ); From 945e3226d81945a72cdc2c6f25107520341c349d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 3 Feb 2025 15:05:17 +0100 Subject: [PATCH 498/650] Lazy load stack frame information (scopes & variables) (#106) * Add tests for incremental fetching scopes & variables for stack frames * Fetch scopes and variables when you select a stack frame * Send proto update message when you select a stack frame --- crates/debugger_ui/src/console.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/stack_frame_list.rs | 10 +- crates/debugger_ui/src/tests/variable_list.rs | 584 ++++++++++++++++++ crates/debugger_ui/src/variable_list.rs | 106 ++-- 5 files changed, 660 insertions(+), 44 deletions(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 91f08db1832931..d81834f9f788c6 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -108,7 +108,7 @@ impl Console { cx: &mut Context, ) { match event { - StackFrameListEvent::SelectedStackFrameChanged => cx.notify(), + StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(), StackFrameListEvent::StackFramesUpdated => {} } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index cb34644d692034..d94155c8bf1008 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -174,7 +174,7 @@ impl DebugPanelItem { cx.subscribe( &stack_frame_list, move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { - StackFrameListEvent::SelectedStackFrameChanged + StackFrameListEvent::SelectedStackFrameChanged(_) | StackFrameListEvent::StackFramesUpdated => this.clear_highlights(cx), }, ), diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index b4dd9c834c9ab3..24c3fdbd832254 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -19,9 +19,11 @@ use workspace::Workspace; use crate::debugger_panel_item::DebugPanelItemEvent::Stopped; use crate::debugger_panel_item::{self, DebugPanelItem}; +pub type StackFrameId = u64; + #[derive(Debug)] pub enum StackFrameListEvent { - SelectedStackFrameChanged, + SelectedStackFrameChanged(StackFrameId), StackFramesUpdated, } @@ -31,12 +33,12 @@ pub struct StackFrameList { focus_handle: FocusHandle, session_id: DebugSessionId, dap_store: Entity, - current_stack_frame_id: u64, stack_frames: Vec, entries: Vec, workspace: WeakEntity, client_id: DebugAdapterClientId, _subscriptions: Vec, + current_stack_frame_id: StackFrameId, fetch_stack_frames_task: Option>>, } @@ -244,7 +246,9 @@ impl StackFrameList { ) -> Task> { self.current_stack_frame_id = stack_frame.id; - cx.emit(StackFrameListEvent::SelectedStackFrameChanged); + cx.emit(StackFrameListEvent::SelectedStackFrameChanged( + stack_frame.id, + )); cx.notify(); if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 60682cdb0675d5..941350b7b4fa22 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -1117,3 +1117,587 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_it_only_fetches_scopes_and_variables_for_the_first_stack_frame( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + import { SOME_VALUE } './module.js'; + + console.log(SOME_VALUE); + "# + .unindent(); + + let module_file_content = r#" + export SOME_VALUE = 'some value'; + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + "module.js": module_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![ + StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + StackFrame { + id: 2, + name: "Stack Frame 2".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + ]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + let frame_1_scopes = vec![Scope { + name: "Frame 1 Scope 1".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }]; + + client + .on_request::({ + let frame_1_scopes = Arc::new(frame_1_scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*frame_1_scopes).clone(), + }) + } + }) + .await; + + let frame_1_variables = vec![ + Variable { + name: "variable1".into(), + value: "value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "variable2".into(), + value: "value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + client + .on_request::({ + let frame_1_variables = Arc::new(frame_1_variables.clone()); + move |_, args| { + assert_eq!(2, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*frame_1_variables).clone(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let variable_list = debug_panel_item.variable_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + assert_eq!( + frame_1_variables + .clone() + .into_iter() + .map(|variable| VariableContainer { + container_reference: 2, + variable, + depth: 1 + }) + .collect::>(), + variable_list.variables_by_stack_frame_id(1) + ); + assert!(variable_list.variables_by_stack_frame_id(2).is_empty()); + }); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} + +#[gpui::test] +async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + import { SOME_VALUE } './module.js'; + + console.log(SOME_VALUE); + "# + .unindent(); + + let module_file_content = r#" + export SOME_VALUE = 'some value'; + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + "module.js": module_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![ + StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + StackFrame { + id: 2, + name: "Stack Frame 2".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + ]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + let frame_1_scopes = vec![Scope { + name: "Frame 1 Scope 1".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }]; + + client + .on_request::({ + let frame_1_scopes = Arc::new(frame_1_scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*frame_1_scopes).clone(), + }) + } + }) + .await; + + let frame_1_variables = vec![ + Variable { + name: "variable1".into(), + value: "value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "variable2".into(), + value: "value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + client + .on_request::({ + let frame_1_variables = Arc::new(frame_1_variables.clone()); + move |_, args| { + assert_eq!(2, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*frame_1_variables).clone(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let variable_list = debug_panel_item.variable_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + assert_eq!( + frame_1_variables + .clone() + .into_iter() + .map(|variable| VariableContainer { + container_reference: 2, + variable, + depth: 1 + }) + .collect::>(), + variable_list.variables_by_stack_frame_id(1) + ); + assert!(variable_list.variables_by_stack_frame_id(2).is_empty()); + }); + + // add handlers for fetching the second stack frame's scopes and variables + // after the user clicked the stack frame + + let frame_2_scopes = vec![Scope { + name: "Frame 2 Scope 1".into(), + presentation_hint: None, + variables_reference: 3, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }]; + + client + .on_request::({ + let frame_2_scopes = Arc::new(frame_2_scopes.clone()); + move |_, args| { + assert_eq!(2, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*frame_2_scopes).clone(), + }) + } + }) + .await; + + let frame_2_variables = vec![ + Variable { + name: "variable3".into(), + value: "old value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "variable4".into(), + value: "old value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + client + .on_request::({ + let frame_2_variables = Arc::new(frame_2_variables.clone()); + move |_, args| { + assert_eq!(3, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*frame_2_variables).clone(), + }) + } + }) + .await; + + active_debug_panel_item(workspace, cx) + .update_in(cx, |debug_panel_item, window, cx| { + debug_panel_item + .stack_frame_list() + .update(cx, |stack_frame_list, cx| { + stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx) + }) + }) + .await + .unwrap(); + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let variable_list = debug_panel_item.variable_list().read(cx); + + assert_eq!(2, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + assert_eq!( + frame_1_variables + .into_iter() + .map(|variable| VariableContainer { + container_reference: 2, + variable, + depth: 1 + }) + .collect::>(), + variable_list.variables_by_stack_frame_id(1) + ); + assert_eq!( + frame_2_variables + .into_iter() + .map(|variable| VariableContainer { + container_reference: 3, + variable, + depth: 1 + }) + .collect::>(), + variable_list.variables_by_stack_frame_id(2) + ); + }); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index d33fe62c9dfd6a..61dd782a386bf7 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,4 +1,4 @@ -use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; +use crate::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, @@ -324,7 +324,6 @@ impl ScopeVariableIndex { } } -type StackFrameId = u64; type ScopeId = u64; pub struct VariableList { @@ -511,8 +510,8 @@ impl VariableList { cx: &mut Context, ) { match event { - StackFrameListEvent::SelectedStackFrameChanged => { - self.build_entries(true, true, cx); + StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => { + self.handle_selected_stack_frame_changed(*stack_frame_id, cx); } StackFrameListEvent::StackFramesUpdated => { self.fetch_variables(cx); @@ -520,6 +519,40 @@ impl VariableList { } } + fn handle_selected_stack_frame_changed( + &mut self, + stack_frame_id: StackFrameId, + cx: &mut Context, + ) { + if self.scopes.contains_key(&stack_frame_id) { + return self.build_entries(true, true, cx); + } + + self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { + let task = this.update(&mut cx, |variable_list, cx| { + variable_list.fetch_variables_for_stack_frame(stack_frame_id, &Vec::default(), cx) + })?; + + let (scopes, variables) = task.await?; + + this.update(&mut cx, |variable_list, cx| { + variable_list.scopes.insert(stack_frame_id, scopes); + + for (scope_id, variables) in variables.into_iter() { + let mut variable_index = ScopeVariableIndex::new(); + variable_index.add_variables(scope_id, variables); + + variable_list + .variables + .insert((stack_frame_id, scope_id), variable_index); + } + + variable_list.build_entries(true, true, cx); + variable_list.send_update_proto_message(cx); + }) + })); + } + #[cfg(any(test, feature = "test-support"))] pub fn scopes(&self) -> &HashMap> { &self.scopes @@ -930,8 +963,6 @@ impl VariableList { let stack_frames = self.stack_frame_list.read(cx).stack_frames().clone(); self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { - let mut tasks = Vec::with_capacity(stack_frames.len()); - let open_entries = this.update(&mut cx, |this, _| { this.open_entries .iter() @@ -940,34 +971,27 @@ impl VariableList { .collect::>() })?; - for stack_frame in stack_frames.clone().into_iter() { - let task = this.update(&mut cx, |this, cx| { - this.fetch_variables_for_stack_frame(stack_frame.id, &open_entries, cx) - }); - - tasks.push( - cx.background_executor() - .spawn(async move { anyhow::Ok((stack_frame.id, task?.await?)) }), - ); - } + let first_stack_frame = stack_frames + .first() + .ok_or(anyhow!("Expected to find a stackframe"))?; - let results = futures::future::join_all(tasks).await; + let (scopes, variables) = this + .update(&mut cx, |this, cx| { + this.fetch_variables_for_stack_frame(first_stack_frame.id, &open_entries, cx) + })? + .await?; this.update(&mut cx, |this, cx| { let mut new_variables = BTreeMap::new(); let mut new_scopes = HashMap::new(); - for (stack_frame_id, (scopes, variables)) in - results.into_iter().filter_map(|result| result.ok()) - { - new_scopes.insert(stack_frame_id, scopes); + new_scopes.insert(first_stack_frame.id, scopes); - for (scope_id, variables) in variables.into_iter() { - let mut variable_index = ScopeVariableIndex::new(); - variable_index.add_variables(scope_id, variables); + for (scope_id, variables) in variables.into_iter() { + let mut variable_index = ScopeVariableIndex::new(); + variable_index.add_variables(scope_id, variables); - new_variables.insert((stack_frame_id, scope_id), variable_index); - } + new_variables.insert((first_stack_frame.id, scope_id), variable_index); } std::mem::swap(&mut this.variables, &mut new_variables); @@ -976,25 +1000,29 @@ impl VariableList { this.entries.clear(); this.build_entries(true, true, cx); - if let Some((client, project_id)) = this.dap_store.read(cx).downstream_client() { - let request = UpdateDebugAdapter { - client_id: this.client_id.to_proto(), - session_id: this.session_id.to_proto(), - thread_id: Some(this.stack_frame_list.read(cx).thread_id()), - project_id: *project_id, - variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( - this.to_proto(), - )), - }; - - client.send(request).log_err(); - }; + this.send_update_proto_message(cx); this.fetch_variables_task.take(); }) })); } + fn send_update_proto_message(&self, cx: &mut Context) { + if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { + let request = UpdateDebugAdapter { + client_id: self.client_id.to_proto(), + session_id: self.session_id.to_proto(), + thread_id: Some(self.stack_frame_list.read(cx).thread_id()), + project_id: *project_id, + variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( + self.to_proto(), + )), + }; + + client.send(request).log_err(); + }; + } + fn deploy_variable_context_menu( &mut self, parent_variables_reference: u64, From 26f14fd036aef740b06b28e57cb7256d3ceb7974 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 3 Feb 2025 15:09:02 +0100 Subject: [PATCH 499/650] Remove futures from debugger ui crate --- Cargo.lock | 1 - crates/debugger_ui/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc0e085ba73db4..84e666f83d805b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3776,7 +3776,6 @@ dependencies = [ "dap", "editor", "env_logger 0.11.6", - "futures 0.3.31", "fuzzy", "gpui", "language", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index d3ad8d5827fbd2..d6a463433face0 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -25,7 +25,6 @@ collections.workspace = true command_palette_hooks.workspace = true dap.workspace = true editor.workspace = true -futures.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true From c45b6e92715cdf8fb10f813697240f2d824a2c81 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:51:48 +0100 Subject: [PATCH 500/650] Clean up naming in activity_indicator.rs Co-authored-by: Anthony --- crates/activity_indicator/src/activity_indicator.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 21ee6dc2ed7aa0..f468c9483107ca 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -101,21 +101,18 @@ impl ActivityIndicator { }); cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event { - Event::ShowError { - server_name: lsp_name, - error, - } => { + Event::ShowError { server_name, error } => { let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx)); let project = project.clone(); let error = error.clone(); - let lsp_name = lsp_name.clone(); + let server_name = server_name.clone(); cx.spawn_in(window, |workspace, mut cx| async move { let buffer = create_buffer.await?; buffer.update(&mut cx, |buffer, cx| { buffer.edit( [( 0..0, - format!("Language server error: {}\n\n{}", lsp_name, error), + format!("Language server error: {}\n\n{}", server_name, error), )], None, cx, From 69548f5f34d0c41549b307c464460638eaeecfdb Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:24:37 +0100 Subject: [PATCH 501/650] Clean up naming around activity indicator and binary statuses Co-authored-by: Anthony --- .../src/activity_indicator.rs | 27 ++++----- crates/extension/src/extension_host_proxy.rs | 6 +- .../src/extension_store_test.rs | 8 +-- .../src/wasm_host/wit/since_v0_0_1.rs | 10 ++-- .../src/wasm_host/wit/since_v0_1_0.rs | 10 ++-- .../src/wasm_host/wit/since_v0_2_0.rs | 10 ++-- crates/language/src/language.rs | 14 ++--- crates/language/src/language_registry.rs | 58 +++++-------------- .../src/extension_lsp_adapter.rs | 4 +- crates/project/src/dap_store.rs | 10 ++-- crates/project/src/lsp_store.rs | 8 +-- 11 files changed, 66 insertions(+), 99 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index f468c9483107ca..834e83f9887055 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -7,8 +7,7 @@ use gpui::{ EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Transformation, Window, }; -use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId}; -use lsp::LanguageServerName; +use language::{BinaryStatus, LanguageRegistry, LanguageServerId}; use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId}; use smallvec::SmallVec; use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration}; @@ -20,7 +19,7 @@ actions!(activity_indicator, [ShowErrorMessage]); pub enum Event { ShowError { - server_name: LanguageServerName, + server_name: SharedString, error: String, }, } @@ -33,8 +32,8 @@ pub struct ActivityIndicator { } struct ServerStatus { - name: LanguageServerName, - status: LanguageServerBinaryStatus, + name: SharedString, + status: BinaryStatus, } struct PendingWork<'a> { @@ -142,7 +141,7 @@ impl ActivityIndicator { fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context) { self.statuses.retain(|status| { - if let LanguageServerBinaryStatus::Failed { error } = &status.status { + if let BinaryStatus::Failed { error } = &status.status { cx.emit(Event::ShowError { server_name: status.name.clone(), error: error.clone(), @@ -273,12 +272,10 @@ impl ActivityIndicator { let mut failed = SmallVec::<[_; 3]>::new(); for status in &self.statuses { match status.status { - LanguageServerBinaryStatus::CheckingForUpdate => { - checking_for_update.push(status.name.clone()) - } - LanguageServerBinaryStatus::Downloading => downloading.push(status.name.clone()), - LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.clone()), - LanguageServerBinaryStatus::None => {} + BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()), + BinaryStatus::Downloading => downloading.push(status.name.clone()), + BinaryStatus::Failed { .. } => failed.push(status.name.clone()), + BinaryStatus::None => {} } } @@ -291,7 +288,7 @@ impl ActivityIndicator { ), message: format!( "Downloading {}...", - downloading.iter().map(|name| name.0.as_ref()).fold( + downloading.iter().map(|name| name.as_ref()).fold( String::new(), |mut acc, s| { if !acc.is_empty() { @@ -319,7 +316,7 @@ impl ActivityIndicator { ), message: format!( "Checking for updates to {}...", - checking_for_update.iter().map(|name| name.0.as_ref()).fold( + checking_for_update.iter().map(|name| name.as_ref()).fold( String::new(), |mut acc, s| { if !acc.is_empty() { @@ -349,7 +346,7 @@ impl ActivityIndicator { "Failed to run {}. Click to show error.", failed .iter() - .map(|name| name.0.as_ref()) + .map(|name| name.as_ref()) .fold(String::new(), |mut acc, s| { if !acc.is_empty() { acc.push_str(", "); diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 25d4a4e539cc1d..03dbb263db1707 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::Result; use fs::Fs; use gpui::{App, Global, ReadGlobal, SharedString, Task}; -use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage}; +use language::{LanguageMatcher, LanguageName, BinaryStatus, LoadedLanguage}; use lsp::LanguageServerName; use parking_lot::RwLock; @@ -264,7 +264,7 @@ pub trait ExtensionLanguageServerProxy: Send + Sync + 'static { fn update_language_server_status( &self, language_server_id: LanguageServerName, - status: LanguageServerBinaryStatus, + status: BinaryStatus, ); } @@ -297,7 +297,7 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy { fn update_language_server_status( &self, language_server_id: LanguageServerName, - status: LanguageServerBinaryStatus, + status: BinaryStatus, ) { let Some(proxy) = self.language_server_proxy.read().clone() else { return; diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index e65678bcc8b5ff..20877b0e74b7d9 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -10,7 +10,7 @@ use fs::{FakeFs, Fs, RealFs}; use futures::{io::BufReader, AsyncReadExt, StreamExt}; use gpui::{AppContext as _, SemanticVersion, TestAppContext}; use http_client::{FakeHttpClient, Response}; -use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus}; +use language::{LanguageMatcher, LanguageRegistry, BinaryStatus}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; use parking_lot::Mutex; @@ -655,15 +655,15 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { [ ( LanguageServerName("gleam".into()), - LanguageServerBinaryStatus::CheckingForUpdate + BinaryStatus::CheckingForUpdate ), ( LanguageServerName("gleam".into()), - LanguageServerBinaryStatus::Downloading + BinaryStatus::Downloading ), ( LanguageServerName("gleam".into()), - LanguageServerBinaryStatus::None + BinaryStatus::None ) ] ); diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs index 1f0891b4105f9c..71b04817b0f81f 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs @@ -4,7 +4,7 @@ use crate::wasm_host::WasmState; use anyhow::Result; use async_trait::async_trait; use extension::{ExtensionLanguageServerProxy, WorktreeDelegate}; -use language::LanguageServerBinaryStatus; +use language::BinaryStatus; use semantic_version::SemanticVersion; use std::sync::{Arc, OnceLock}; use wasmtime::component::{Linker, Resource}; @@ -136,15 +136,15 @@ impl ExtensionImports for WasmState { ) -> wasmtime::Result<()> { let status = match status { LanguageServerInstallationStatus::CheckingForUpdate => { - LanguageServerBinaryStatus::CheckingForUpdate + BinaryStatus::CheckingForUpdate } LanguageServerInstallationStatus::Downloading => { - LanguageServerBinaryStatus::Downloading + BinaryStatus::Downloading } LanguageServerInstallationStatus::Cached - | LanguageServerInstallationStatus::Downloaded => LanguageServerBinaryStatus::None, + | LanguageServerInstallationStatus::Downloaded => BinaryStatus::None, LanguageServerInstallationStatus::Failed(error) => { - LanguageServerBinaryStatus::Failed { error } + BinaryStatus::Failed { error } } }; diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index c1c07a2b09be5f..0d502e033b2ffe 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -9,7 +9,7 @@ use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDel use futures::{io::BufReader, FutureExt as _}; use futures::{lock::Mutex, AsyncReadExt}; use language::LanguageName; -use language::{language_settings::AllLanguageSettings, LanguageServerBinaryStatus}; +use language::{language_settings::AllLanguageSettings, BinaryStatus}; use project::project_settings::ProjectSettings; use semantic_version::SemanticVersion; use std::{ @@ -483,14 +483,14 @@ impl ExtensionImports for WasmState { ) -> wasmtime::Result<()> { let status = match status { LanguageServerInstallationStatus::CheckingForUpdate => { - LanguageServerBinaryStatus::CheckingForUpdate + BinaryStatus::CheckingForUpdate } LanguageServerInstallationStatus::Downloading => { - LanguageServerBinaryStatus::Downloading + BinaryStatus::Downloading } - LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None, + LanguageServerInstallationStatus::None => BinaryStatus::None, LanguageServerInstallationStatus::Failed(error) => { - LanguageServerBinaryStatus::Failed { error } + BinaryStatus::Failed { error } } }; diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index b722d7b2352188..57dcce7cde8e48 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -13,7 +13,7 @@ use extension::{ }; use futures::{io::BufReader, FutureExt as _}; use futures::{lock::Mutex, AsyncReadExt}; -use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus}; +use language::{language_settings::AllLanguageSettings, LanguageName, BinaryStatus}; use project::project_settings::ProjectSettings; use semantic_version::SemanticVersion; use std::{ @@ -672,14 +672,14 @@ impl ExtensionImports for WasmState { ) -> wasmtime::Result<()> { let status = match status { LanguageServerInstallationStatus::CheckingForUpdate => { - LanguageServerBinaryStatus::CheckingForUpdate + BinaryStatus::CheckingForUpdate } LanguageServerInstallationStatus::Downloading => { - LanguageServerBinaryStatus::Downloading + BinaryStatus::Downloading } - LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None, + LanguageServerInstallationStatus::None => BinaryStatus::None, LanguageServerInstallationStatus::Failed(error) => { - LanguageServerBinaryStatus::Failed { error } + BinaryStatus::Failed { error } } }; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index beef29355809a0..58daf906e5dcf8 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -75,7 +75,7 @@ pub use buffer::*; pub use diagnostic_set::DiagnosticEntry; pub use language_registry::{ AvailableLanguage, LanguageNotFound, LanguageQueries, LanguageRegistry, - LanguageServerBinaryStatus, QUERY_FILENAME_PREFIXES, + BinaryStatus, QUERY_FILENAME_PREFIXES, }; pub use lsp::LanguageServerId; pub use outline::*; @@ -270,7 +270,7 @@ pub trait LspAdapterDelegate: Send + Sync { fn http_client(&self) -> Arc; fn worktree_id(&self) -> WorktreeId; fn worktree_root_path(&self) -> &Path; - fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus); + fn update_status(&self, language: LanguageServerName, status: BinaryStatus); async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option>; async fn npm_package_installed_version( @@ -348,7 +348,7 @@ pub trait LspAdapter: 'static + Send + Sync { } else { delegate.update_status( self.name(), - LanguageServerBinaryStatus::Failed { + BinaryStatus::Failed { error: format!("{error:?}"), }, ); @@ -522,7 +522,7 @@ async fn try_fetch_server_binary let name = adapter.name(); log::info!("fetching latest version of language server {:?}", name.0); - delegate.update_status(name.clone(), LanguageServerBinaryStatus::CheckingForUpdate); + delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate); let latest_version = adapter .fetch_latest_server_version(delegate.as_ref()) @@ -533,16 +533,16 @@ async fn try_fetch_server_binary .await { log::info!("language server {:?} is already installed", name.0); - delegate.update_status(name.clone(), LanguageServerBinaryStatus::None); + delegate.update_status(name.clone(), BinaryStatus::None); Ok(binary) } else { log::info!("downloading language server {:?}", name.0); - delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading); + delegate.update_status(adapter.name(), BinaryStatus::Downloading); let binary = adapter .fetch_server_binary(latest_version, container_dir, delegate.as_ref()) .await; - delegate.update_status(name.clone(), LanguageServerBinaryStatus::None); + delegate.update_status(name.clone(), BinaryStatus::None); binary } } diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 8e07cf147cd3d2..46893c70aa03ef 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -98,8 +98,8 @@ pub struct LanguageRegistry { state: RwLock, language_server_download_dir: Option>, executor: BackgroundExecutor, - lsp_binary_status_tx: LspBinaryStatusSender, - dap_binary_status_tx: DapBinaryStatusSender, + lsp_binary_status_tx: BinaryStatusSender, + dap_binary_status_tx: BinaryStatusSender, } struct LanguageRegistryState { @@ -130,7 +130,7 @@ pub struct FakeLanguageServerEntry { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum LanguageServerBinaryStatus { +pub enum BinaryStatus { None, CheckingForUpdate, Downloading, @@ -213,8 +213,8 @@ pub struct LanguageQueries { } #[derive(Clone, Default)] -struct LspBinaryStatusSender { - txs: Arc>>>, +struct BinaryStatusSender { + txs: Arc>>>, } pub struct LoadedLanguage { @@ -894,20 +894,12 @@ impl LanguageRegistry { .unwrap_or_default() } - pub fn update_lsp_status( - &self, - server_name: LanguageServerName, - status: LanguageServerBinaryStatus, - ) { - self.lsp_binary_status_tx.send(server_name, status); + pub fn update_lsp_status(&self, server_name: LanguageServerName, status: BinaryStatus) { + self.lsp_binary_status_tx.send(server_name.0, status); } - pub fn update_dap_status( - &self, - server_name: LanguageServerName, - status: LanguageServerBinaryStatus, - ) { - self.dap_binary_status_tx.send(server_name, status); + pub fn update_dap_status(&self, server_name: LanguageServerName, status: BinaryStatus) { + self.dap_binary_status_tx.send(server_name.0, status); } pub fn next_language_server_id(&self) -> LanguageServerId { @@ -961,13 +953,13 @@ impl LanguageRegistry { pub fn language_server_binary_statuses( &self, - ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { + ) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> { self.lsp_binary_status_tx.subscribe() } pub fn dap_server_binary_statuses( &self, - ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { + ) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> { self.dap_binary_status_tx.subscribe() } @@ -1081,36 +1073,14 @@ impl LanguageRegistryState { } } -#[derive(Clone, Default)] -struct DapBinaryStatusSender { - txs: Arc>>>, -} - -impl DapBinaryStatusSender { - fn subscribe( - &self, - ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { - let (tx, rx) = mpsc::unbounded(); - self.txs.lock().push(tx); - rx - } - - fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) { - let mut txs = self.txs.lock(); - txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok()); - } -} - -impl LspBinaryStatusSender { - fn subscribe( - &self, - ) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> { +impl BinaryStatusSender { + fn subscribe(&self) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> { let (tx, rx) = mpsc::unbounded(); self.txs.lock().push(tx); rx } - fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) { + fn send(&self, name: SharedString, status: BinaryStatus) { let mut txs = self.txs.lock(); txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok()); } diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index 26b53a11c1a930..9047ff3438b155 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -12,7 +12,7 @@ use fs::Fs; use futures::{Future, FutureExt}; use gpui::AsyncApp; use language::{ - CodeLabel, HighlightId, Language, LanguageName, LanguageServerBinaryStatus, + CodeLabel, HighlightId, Language, LanguageName, BinaryStatus, LanguageToolchainStore, LspAdapter, LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName}; @@ -80,7 +80,7 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy { fn update_language_server_status( &self, language_server_id: LanguageServerName, - status: LanguageServerBinaryStatus, + status: BinaryStatus, ) { self.language_registry .update_lsp_status(language_server_id, status); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index b06caf0e8d3ecc..9aadd39ed367f6 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -39,7 +39,7 @@ use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedStrin use http_client::HttpClient; use language::{ proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, - Buffer, BufferSnapshot, LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, + Buffer, BufferSnapshot, LanguageRegistry, BinaryStatus, LanguageToolchainStore, }; use lsp::LanguageServerName; use node_runtime::NodeRuntime; @@ -2428,10 +2428,10 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { fn update_status(&self, dap_name: DebugAdapterName, status: dap::adapters::DapStatus) { let name = SharedString::from(dap_name.to_string()); let status = match status { - DapStatus::None => LanguageServerBinaryStatus::None, - DapStatus::Downloading => LanguageServerBinaryStatus::Downloading, - DapStatus::Failed { error } => LanguageServerBinaryStatus::Failed { error }, - DapStatus::CheckingForUpdate => LanguageServerBinaryStatus::CheckingForUpdate, + DapStatus::None => BinaryStatus::None, + DapStatus::Downloading => BinaryStatus::Downloading, + DapStatus::Failed { error } => BinaryStatus::Failed { error }, + DapStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, }; self.language_registry diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 1d9cd423241938..ca4d0e033138bc 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -38,7 +38,7 @@ use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, CompletionDocumentation, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, - LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, + LanguageName, LanguageRegistry, BinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, }; @@ -350,7 +350,7 @@ impl LocalLspStore { let log = stderr_capture.lock().take().unwrap_or_default(); delegate.update_status( adapter.name(), - LanguageServerBinaryStatus::Failed { + BinaryStatus::Failed { error: format!("{err}\n-- stderr--\n{}", log), }, ); @@ -490,7 +490,7 @@ impl LocalLspStore { ) .await; - delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None); + delegate.update_status(adapter.name.clone(), BinaryStatus::None); let mut binary = binary_result?; if let Some(arguments) = settings.and_then(|b| b.arguments) { @@ -8625,7 +8625,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { fn update_status( &self, server_name: LanguageServerName, - status: language::LanguageServerBinaryStatus, + status: language::BinaryStatus, ) { self.language_registry .update_lsp_status(server_name, status); From 44e9444c8cdc7be52afee936946a09a8b3db3a1e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 3 Feb 2025 16:14:39 +0100 Subject: [PATCH 502/650] Fix choose debugger action didn't work anymore --- crates/debugger_ui/src/debugger_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 79cda0bb326506..1e98f33e139907 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1216,8 +1216,8 @@ impl Render for DebugPanel { "Choose a debugger", ) .label_size(LabelSize::Small) - .on_click(move |_, _window, cx| { - cx.dispatch_action(&Start); + .on_click(move |_, window, cx| { + window.dispatch_action(Box::new(Start), cx); }) ), ), From 4c930652af38d901e0675a6b62580d0bd7034bfb Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:52:53 +0100 Subject: [PATCH 503/650] Use workspace values for edition and publish in new crates --- Cargo.toml | 2 +- crates/dap/Cargo.toml | 4 ++-- crates/dap_adapters/Cargo.toml | 4 ++-- crates/debugger_tools/Cargo.toml | 4 ++-- crates/debugger_ui/Cargo.toml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46ee24b00a191b..a72bacf8a56897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "crates/copilot", "crates/dap", "crates/dap_adapters", + "crates/debugger_tools", "crates/debugger_ui", "crates/db", "crates/deepseek", @@ -189,7 +190,6 @@ members = [ # "tooling/xtask", - "crates/debugger_tools", ] default-members = ["crates/zed"] diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index f4b90a7a0d6b8d..b604c269100711 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "dap" version = "0.1.0" -edition = "2021" -publish = false +edition.workspace = true +publish.workspace = true license = "GPL-3.0-or-later" [lints] diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 885f377d5a3149..0fe055738a1e87 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "dap_adapters" version = "0.1.0" -edition = "2021" -publish = false +edition.workspace = true +publish.workspace = true license = "GPL-3.0-or-later" [features] diff --git a/crates/debugger_tools/Cargo.toml b/crates/debugger_tools/Cargo.toml index a062b749f281a8..a7a96587e275c0 100644 --- a/crates/debugger_tools/Cargo.toml +++ b/crates/debugger_tools/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "debugger_tools" version = "0.1.0" -edition = "2021" -publish = false +edition.workspace = true +publish.workspace = true license = "GPL-3.0-or-later" [lints] diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index d6a463433face0..076af6e56485c2 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "debugger_ui" version = "0.1.0" -edition = "2021" -publish = false +edition.workspace = true +publish.workspace = true license = "GPL-3.0-or-later" [lints] From d60089888f98ddb65125d508a9e57344b11bebfe Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:31:06 -0500 Subject: [PATCH 504/650] Show console output on remote debug clients (#107) This PR shows console output & removes the query bar for remote clients. We don't show the query bar because it's currently impossible for remote clients to send evaluations requests from their console. We didn't implement that feature yet because of security consoles and will do so in the future with permissions. Co-authored-by: Remco Smits \ * Add basic collab debug console test Co-authored-by: Remco Smits * Show output events on remote client debug consoles Co-authored-by: Remco Smits * Don't show debug console query bar on remote clients Co-authored-by: Remco Smits --------- Co-authored-by: Remco Smits --- .../collab/src/db/tables/debug_panel_items.rs | 1 + crates/collab/src/tests/debug_panel_tests.rs | 301 ++++++++++++++++++ crates/dap/src/proto_conversions.rs | 91 +++++- crates/debugger_ui/src/console.rs | 38 ++- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 15 +- crates/proto/proto/zed.proto | 28 +- 7 files changed, 464 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/db/tables/debug_panel_items.rs b/crates/collab/src/db/tables/debug_panel_items.rs index fa5a124242a4b4..152328cf965939 100644 --- a/crates/collab/src/db/tables/debug_panel_items.rs +++ b/crates/collab/src/db/tables/debug_panel_items.rs @@ -129,6 +129,7 @@ impl Model { let encoded = module_list.encode_to_vec(); self.module_list = encoded; } + proto::update_debug_adapter::Variant::OutputEvent(_) => {} } Ok(()) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 0f9627c8f21a44..e90b42a178c543 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -5,6 +5,7 @@ use dap::{ SourceBreakpoint, StackFrame, }; use dap::{Scope, Variable}; +use debugger_ui::debugger_panel_item::DebugPanelItem; use debugger_ui::{debugger_panel::DebugPanel, variable_list::VariableContainer}; use editor::Editor; use gpui::{Entity, TestAppContext, VisualTestContext}; @@ -15,6 +16,7 @@ use std::{ path::Path, sync::atomic::{AtomicBool, Ordering}, }; +use unindent::Unindent as _; use workspace::{dock::Panel, Workspace}; use super::{TestClient, TestServer}; @@ -48,6 +50,18 @@ async fn add_debugger_panel(workspace: &Entity, cx: &mut VisualTestCo }); } +pub fn active_debug_panel_item( + workspace: Entity, + cx: &mut VisualTestContext, +) -> Entity { + workspace.update_in(cx, |workspace, _window, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap() + }) +} + struct ZedInstance<'a> { client: TestClient, project: Option>, @@ -2231,3 +2245,290 @@ async fn test_ignore_breakpoints( }) }); } + +#[gpui::test] +async fn test_debug_panel_console(host_cx: &mut TestAppContext, remote_cx: &mut TestAppContext) { + let executor = host_cx.executor(); + let mut server = TestServer::start(executor).await; + + let (mut host_zed, mut remote_zed) = + setup_two_member_test(&mut server, host_cx, remote_cx).await; + + let (host_project_id, _) = host_zed.host_project(None).await; + remote_zed.join_project(host_project_id).await; + + let (_client_host, _host_workspace, host_project, host_cx) = host_zed.expand().await; + let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; + + remote_cx.run_until_parked(); + + let task = host_project.update(host_cx, |project, cx| { + project.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: None, + output: "First line".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First group".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::Start), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First item in group 1".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second item in group 1".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second group".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::Start), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First item in group 2".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second item in group 2".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "End group 2".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::End), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Third group".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::StartCollapsed), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "First item in group 3".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second item in group 3".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "End group 3".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::End), + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Third item in group 1".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: None, + })) + .await; + + client + .fake_event(dap::messages::Events::Output(dap::OutputEvent { + category: Some(dap::OutputEventCategory::Stdout), + output: "Second item".to_string(), + data: None, + variables_reference: None, + source: None, + line: None, + column: None, + group: Some(dap::OutputEventGroup::End), + })) + .await; + + host_cx.run_until_parked(); + remote_cx.run_until_parked(); + + active_debug_panel_item(remote_workspace, remote_cx).update( + remote_cx, + |debug_panel_item, cx| { + debug_panel_item.console().update(cx, |console, cx| { + console.editor().update(cx, |editor, cx| { + pretty_assertions::assert_eq!( + " + First line + First group + First item in group 1 + Second item in group 1 + Second group + First item in group 2 + Second item in group 2 + End group 2 + ⋯ End group 3 + Third item in group 1 + Second item + " + .unindent(), + editor.display_text(cx) + ); + }) + }); + }, + ); + + let shutdown_client = host_project.update(host_cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index b543c01f6c2595..440cc182d2d48c 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -3,7 +3,9 @@ use client::proto::{ self, DapChecksum, DapChecksumAlgorithm, DapModule, DapScope, DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable, SetDebugClientCapabilities, }; -use dap_types::{Capabilities, ScopePresentationHint, Source}; +use dap_types::{ + Capabilities, OutputEventCategory, OutputEventGroup, ScopePresentationHint, Source, +}; pub trait ProtoConversion { type ProtoType; @@ -401,3 +403,90 @@ impl ProtoConversion for dap_types::SteppingGranularity { } } } + +impl ProtoConversion for dap_types::OutputEventCategory { + type ProtoType = proto::DapOutputCategory; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + match self { + Self::Console => proto::DapOutputCategory::ConsoleOutput, + Self::Important => proto::DapOutputCategory::Important, + Self::Stdout => proto::DapOutputCategory::Stdout, + Self::Stderr => proto::DapOutputCategory::Stderr, + _ => proto::DapOutputCategory::Unknown, + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + match payload { + proto::DapOutputCategory::ConsoleOutput => Self::Console, + proto::DapOutputCategory::Important => Self::Important, + proto::DapOutputCategory::Stdout => Self::Stdout, + proto::DapOutputCategory::Stderr => Self::Stderr, + proto::DapOutputCategory::Unknown => Self::Unknown, + } + } +} + +impl ProtoConversion for dap_types::OutputEvent { + type ProtoType = proto::DapOutputEvent; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + proto::DapOutputEvent { + category: self + .category + .as_ref() + .map(|category| category.to_proto().into()), + output: self.output.clone(), + variables_reference: self.variables_reference, + source: self.source.as_ref().map(|source| source.to_proto()), + line: self.line.map(|line| line as u32), + column: self.column.map(|column| column as u32), + group: self.group.map(|group| group.to_proto().into()), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + dap_types::OutputEvent { + category: payload + .category + .and_then(proto::DapOutputCategory::from_i32) + .map(OutputEventCategory::from_proto), + output: payload.output.clone(), + variables_reference: payload.variables_reference, + source: payload.source.map(Source::from_proto), + line: payload.line.map(|line| line as u64), + column: payload.column.map(|column| column as u64), + group: payload + .group + .and_then(proto::DapOutputEventGroup::from_i32) + .map(OutputEventGroup::from_proto), + data: None, + } + } +} + +impl ProtoConversion for dap_types::OutputEventGroup { + type ProtoType = proto::DapOutputEventGroup; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + match self { + dap_types::OutputEventGroup::Start => proto::DapOutputEventGroup::Start, + dap_types::OutputEventGroup::StartCollapsed => { + proto::DapOutputEventGroup::StartCollapsed + } + dap_types::OutputEventGroup::End => proto::DapOutputEventGroup::End, + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + match payload { + proto::DapOutputEventGroup::Start => Self::Start, + proto::DapOutputEventGroup::StartCollapsed => Self::StartCollapsed, + proto::DapOutputEventGroup::End => Self::End, + } + } +} diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index d81834f9f788c6..817bfecfa8dd56 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -2,7 +2,10 @@ use crate::{ stack_frame_list::{StackFrameList, StackFrameListEvent}, variable_list::VariableList, }; -use dap::{client::DebugAdapterClientId, OutputEvent, OutputEventGroup}; +use dap::{ + client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, + OutputEvent, OutputEventGroup, +}; use editor::{ display_map::{Crease, CreaseId}, Anchor, CompletionProvider, Editor, EditorElement, EditorStyle, FoldPlaceholder, @@ -12,10 +15,12 @@ use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16}; use menu::Confirm; use project::{dap_store::DapStore, Completion}; +use rpc::proto; use settings::Settings; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, Disclosure, ElevationIndex}; +use util::ResultExt; pub struct OutputGroup { pub start: Anchor, @@ -29,6 +34,7 @@ pub struct Console { groups: Vec, console: Entity, query_bar: Entity, + session_id: DebugSessionId, dap_store: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, @@ -39,6 +45,7 @@ pub struct Console { impl Console { pub fn new( stack_frame_list: &Entity, + session_id: &DebugSessionId, client_id: &DebugAdapterClientId, variable_list: Entity, dap_store: Entity, @@ -87,6 +94,7 @@ impl Console { _subscriptions, client_id: *client_id, groups: Vec::default(), + session_id: *session_id, stack_frame_list: stack_frame_list.clone(), } } @@ -101,6 +109,10 @@ impl Console { &self.query_bar } + fn is_local(&self, cx: &Context) -> bool { + self.dap_store.read(cx).as_local().is_some() + } + fn handle_stack_frame_list_events( &mut self, _: Entity, @@ -114,6 +126,21 @@ impl Console { } pub fn add_message(&mut self, event: OutputEvent, window: &mut Window, cx: &mut Context) { + if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { + client + .send(proto::UpdateDebugAdapter { + project_id: *project_id, + client_id: self.client_id.to_proto(), + thread_id: None, + session_id: self.session_id.to_proto(), + variant: Some(proto::update_debug_adapter::Variant::OutputEvent( + event.to_proto(), + )), + }) + .log_err(); + return; + } + self.console.update(cx, |console, cx| { let output = event.output.trim_end().to_string(); @@ -329,11 +356,10 @@ impl Render for Console { .on_action(cx.listener(Self::evaluate)) .size_full() .child(self.render_console(cx)) - .child( - div() - .child(self.render_query_bar(cx)) - .pt(DynamicSpacing::Base04.rems(cx)), - ) + .when(self.is_local(cx), |this| { + this.child(self.render_query_bar(cx)) + .pt(DynamicSpacing::Base04.rems(cx)) + }) .border_2() } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1e98f33e139907..02a8318a7d4322 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1009,7 +1009,7 @@ impl DebugPanel { if let Some((debug_panel_item, is_active_item)) = search { debug_panel_item.update(cx, |this, cx| { - this.update_adapter(update, cx); + this.update_adapter(update, window, cx); if is_active_item { this.go_to_current_stack_frame(window, cx); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index d94155c8bf1008..51f1fca4b43957 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -5,7 +5,7 @@ use crate::module_list::ModuleList; use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; -use dap::proto_conversions; +use dap::proto_conversions::{self, ProtoConversion}; use dap::session::DebugSessionId; use dap::{ client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, @@ -124,6 +124,7 @@ impl DebugPanelItem { let console = cx.new(|cx| { Console::new( &stack_frame_list, + session_id, client_id, variable_list.clone(), dap_store.clone(), @@ -441,7 +442,12 @@ impl DebugPanelItem { } } - pub(crate) fn update_adapter(&mut self, update: &UpdateDebugAdapter, cx: &mut Context) { + pub(crate) fn update_adapter( + &mut self, + update: &UpdateDebugAdapter, + window: &mut Window, + cx: &mut Context, + ) { if let Some(update_variant) = update.variant.as_ref() { match update_variant { proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => { @@ -465,6 +471,11 @@ impl DebugPanelItem { this.set_from_proto(module_list, cx); }) } + proto::update_debug_adapter::Variant::OutputEvent(output_event) => { + self.console.update(cx, |this, cx| { + this.add_message(OutputEvent::from_proto(output_event.clone()), window, cx); + }) + } } } } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 9501b3ed943762..fd9102132f17e4 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2809,8 +2809,7 @@ message DebuggerLoadedSourceList { } message DebuggerConsole { - // Editor console = 1; - // Editor query_bar = 2; + // repeated DapOutputEvent events = 1; } message DebuggerModuleList { @@ -2844,6 +2843,7 @@ message UpdateDebugAdapter { DebuggerVariableList variable_list = 7; AddToVariableList add_to_variable_list = 8; DebuggerModuleList modules = 9; + DapOutputEvent output_event = 10; } } @@ -2892,6 +2892,30 @@ message DapSource { repeated DapChecksum checksums = 8; } +enum DapOutputCategory { + ConsoleOutput = 0; + Important = 1; + Stdout = 2; + Stderr = 3; + Unknown = 4; +} + +enum DapOutputEventGroup { + Start = 0; + StartCollapsed = 1; + End = 2; +} + +message DapOutputEvent { + string output = 1; + optional DapOutputCategory category = 2; + optional uint64 variables_reference = 3; + optional DapOutputEventGroup group = 4; + optional DapSource source = 5; + optional uint32 line = 6; + optional uint32 column = 7; +} + enum DapChecksumAlgorithm { CHECKSUM_ALGORITHM_UNSPECIFIED = 0; MD5 = 1; From 5cd93ca15884e1258f940570895b3c0e68bf3e26 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Mon, 3 Feb 2025 16:24:23 -0500 Subject: [PATCH 505/650] Fix test_extension_store_with_test_extension --- .../src/extension_store_test.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 20877b0e74b7d9..21f65ef97002fa 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -8,9 +8,9 @@ use collections::BTreeMap; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs, RealFs}; use futures::{io::BufReader, AsyncReadExt, StreamExt}; -use gpui::{AppContext as _, SemanticVersion, TestAppContext}; +use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext}; use http_client::{FakeHttpClient, Response}; -use language::{LanguageMatcher, LanguageRegistry, BinaryStatus}; +use language::{BinaryStatus, LanguageMatcher, LanguageRegistry}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; use parking_lot::Mutex; @@ -653,18 +653,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { status_updates.next().await.unwrap(), ], [ - ( - LanguageServerName("gleam".into()), - BinaryStatus::CheckingForUpdate - ), - ( - LanguageServerName("gleam".into()), - BinaryStatus::Downloading - ), - ( - LanguageServerName("gleam".into()), - BinaryStatus::None - ) + (SharedString::new("gleam"), BinaryStatus::CheckingForUpdate), + (SharedString::new("gleam"), BinaryStatus::Downloading), + (SharedString::new("gleam"), BinaryStatus::None) ] ); From dfeddaae8aa0fd86ddedc0537a7c17394d271221 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 3 Feb 2025 23:27:57 +0100 Subject: [PATCH 506/650] Remove unused dep from activity indicator --- Cargo.lock | 1 - Cargo.toml | 2 +- crates/activity_indicator/Cargo.toml | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84e666f83d805b..048584aeb5511b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,7 +13,6 @@ dependencies = [ "futures 0.3.31", "gpui", "language", - "lsp", "project", "smallvec", "ui", diff --git a/Cargo.toml b/Cargo.toml index a72bacf8a56897..3191d068a19b79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -473,7 +473,7 @@ runtimelib = { version = "0.25.0", default-features = false, features = [ "async-dispatcher-runtime", ] } rustc-demangle = "0.1.23" -rust-embed = { version = "8.5", features = ["include-exclude"] } +rust-embed = { version = "8.4", features = ["include-exclude"] } rustc-hash = "2.1.0" rustls = "0.21.12" rustls-native-certs = "0.8.0" diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index e28c30ca886720..a17846690a3922 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -20,7 +20,6 @@ extension_host.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true -lsp.workspace = true project.workspace = true smallvec.workspace = true ui.workspace = true From 080d0c46add5fb6d7ad5248c39656c0d2283d446 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 4 Feb 2025 00:03:36 +0100 Subject: [PATCH 507/650] Remove sqlez dependency on project --- Cargo.lock | 1 - crates/sqlez/Cargo.toml | 1 - crates/sqlez/src/bindable.rs | 35 --------------- crates/workspace/src/persistence.rs | 67 ++++++++++++++++++++++++++--- 4 files changed, 61 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 048584aeb5511b..8220b798ad30e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12407,7 +12407,6 @@ dependencies = [ "indoc", "libsqlite3-sys", "parking_lot", - "project", "smol", "sqlformat", "thread_local", diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index dff26632d05409..98dd9a330a6ebd 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -15,7 +15,6 @@ futures.workspace = true indoc.workspace = true libsqlite3-sys.workspace = true parking_lot.workspace = true -project.workspace = true smol.workspace = true sqlformat.workspace = true thread_local = "1.1.4" diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index 1246a211b61443..19bd1e2fa4737b 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -1,4 +1,3 @@ -use project::dap_store::BreakpointKind; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -382,40 +381,6 @@ impl Column for () { } } -impl StaticColumnCount for BreakpointKind { - fn column_count() -> usize { - 1 - } -} - -impl Bind for BreakpointKind { - fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { - let next_index = statement.bind(&self.to_int(), start_index)?; - - match self { - BreakpointKind::Standard => { - statement.bind_null(next_index)?; - Ok(next_index + 1) - } - BreakpointKind::Log(message) => statement.bind(message, next_index), - } - } -} - -impl Column for BreakpointKind { - fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - let kind = statement.column_int(start_index)?; - match kind { - 0 => Ok((BreakpointKind::Standard, start_index + 2)), - 1 => { - let message = statement.column_text(start_index + 1)?.to_string(); - Ok((BreakpointKind::Log(message.into()), start_index + 1)) - } - _ => Err(anyhow::anyhow!("Invalid BreakpointKind discriminant")), - } - } -} - macro_rules! impl_tuple_row_traits { ( $($local:ident: $type:ident),+ ) => { impl<$($type: StaticColumnCount),+> StaticColumnCount for ($($type,)+) { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 0ac3e90f81dbfe..1278ddd0982a4b 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1,6 +1,7 @@ pub mod model; use std::{ + borrow::Cow, path::{Path, PathBuf}, str::FromStr, sync::Arc, @@ -148,6 +149,48 @@ pub struct Breakpoint { pub kind: BreakpointKind, } +/// Wrapper for DB type of a breakpoint +struct BreakpointKindWrapper<'a>(Cow<'a, BreakpointKind>); + +impl From for BreakpointKindWrapper<'static> { + fn from(kind: BreakpointKind) -> Self { + BreakpointKindWrapper(Cow::Owned(kind)) + } +} +impl StaticColumnCount for BreakpointKindWrapper<'_> { + fn column_count() -> usize { + 1 + } +} + +impl Bind for BreakpointKindWrapper<'_> { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + let next_index = statement.bind(&self.0.to_int(), start_index)?; + + match self.0.as_ref() { + BreakpointKind::Standard => { + statement.bind_null(next_index)?; + Ok(next_index + 1) + } + BreakpointKind::Log(message) => statement.bind(&message.as_ref(), next_index), + } + } +} + +impl Column for BreakpointKindWrapper<'_> { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + let kind = statement.column_int(start_index)?; + match kind { + 0 => Ok((BreakpointKind::Standard.into(), start_index + 2)), + 1 => { + let message = statement.column_text(start_index + 1)?.to_string(); + Ok((BreakpointKind::Log(message.into()).into(), start_index + 1)) + } + _ => Err(anyhow::anyhow!("Invalid BreakpointKind discriminant")), + } + } +} + /// This struct is used to implement traits on Vec #[derive(Debug)] #[allow(dead_code)] @@ -155,7 +198,7 @@ struct Breakpoints(Vec); impl sqlez::bindable::StaticColumnCount for Breakpoint { fn column_count() -> usize { - 1 + BreakpointKind::column_count() + 1 + BreakpointKindWrapper::column_count() } } @@ -166,7 +209,10 @@ impl sqlez::bindable::Bind for Breakpoint { start_index: i32, ) -> anyhow::Result { let next_index = statement.bind(&self.position, start_index)?; - statement.bind(&self.kind, next_index) + statement.bind( + &BreakpointKindWrapper(Cow::Borrowed(&self.kind)), + next_index, + ) } } @@ -177,9 +223,15 @@ impl Column for Breakpoint { .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? as u32; - let (kind, next_index) = BreakpointKind::column(statement, start_index + 1)?; + let (kind, next_index) = BreakpointKindWrapper::column(statement, start_index + 1)?; - Ok((Breakpoint { position, kind }, next_index)) + Ok(( + Breakpoint { + position, + kind: kind.0.into_owned(), + }, + next_index, + )) } } @@ -197,9 +249,12 @@ impl Column for Breakpoints { .with_context(|| format!("Failed to read BreakPoint at index {index}"))? as u32; - let (kind, next_index) = BreakpointKind::column(statement, index + 1)?; + let (kind, next_index) = BreakpointKindWrapper::column(statement, index + 1)?; - breakpoints.push(Breakpoint { position, kind }); + breakpoints.push(Breakpoint { + position, + kind: kind.0.into_owned(), + }); index = next_index; } } From ad98bf444d58170cb51aa060eccb9dea972b0e08 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 4 Feb 2025 00:10:19 +0100 Subject: [PATCH 508/650] Trigger CLA check From 52350245e6f44f395b9a835bce3efa6c2e2fcd89 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 4 Feb 2025 00:22:37 +0100 Subject: [PATCH 509/650] cargo fmt --- crates/extension/src/extension_host_proxy.rs | 2 +- .../src/wasm_host/wit/since_v0_0_1.rs | 12 +++--------- .../src/wasm_host/wit/since_v0_1_0.rs | 12 +++--------- .../src/wasm_host/wit/since_v0_2_0.rs | 14 ++++---------- crates/language/src/language.rs | 4 ++-- .../src/extension_lsp_adapter.rs | 4 ++-- crates/project/src/dap_store.rs | 2 +- crates/project/src/lsp_store.rs | 12 ++++-------- 8 files changed, 20 insertions(+), 42 deletions(-) diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 03dbb263db1707..dfb93e27cb1bbf 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use anyhow::Result; use fs::Fs; use gpui::{App, Global, ReadGlobal, SharedString, Task}; -use language::{LanguageMatcher, LanguageName, BinaryStatus, LoadedLanguage}; +use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage}; use lsp::LanguageServerName; use parking_lot::RwLock; diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs index 71b04817b0f81f..6871cab739bbe2 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs @@ -135,17 +135,11 @@ impl ExtensionImports for WasmState { status: LanguageServerInstallationStatus, ) -> wasmtime::Result<()> { let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => { - BinaryStatus::CheckingForUpdate - } - LanguageServerInstallationStatus::Downloading => { - BinaryStatus::Downloading - } + LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, + LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, LanguageServerInstallationStatus::Cached | LanguageServerInstallationStatus::Downloaded => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => { - BinaryStatus::Failed { error } - } + LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, }; self.host diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index 0d502e033b2ffe..87ab35917c42e4 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -482,16 +482,10 @@ impl ExtensionImports for WasmState { status: LanguageServerInstallationStatus, ) -> wasmtime::Result<()> { let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => { - BinaryStatus::CheckingForUpdate - } - LanguageServerInstallationStatus::Downloading => { - BinaryStatus::Downloading - } + LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, + LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, LanguageServerInstallationStatus::None => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => { - BinaryStatus::Failed { error } - } + LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, }; self.host diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index 57dcce7cde8e48..25b4190c7b1631 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -13,7 +13,7 @@ use extension::{ }; use futures::{io::BufReader, FutureExt as _}; use futures::{lock::Mutex, AsyncReadExt}; -use language::{language_settings::AllLanguageSettings, LanguageName, BinaryStatus}; +use language::{language_settings::AllLanguageSettings, BinaryStatus, LanguageName}; use project::project_settings::ProjectSettings; use semantic_version::SemanticVersion; use std::{ @@ -671,16 +671,10 @@ impl ExtensionImports for WasmState { status: LanguageServerInstallationStatus, ) -> wasmtime::Result<()> { let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => { - BinaryStatus::CheckingForUpdate - } - LanguageServerInstallationStatus::Downloading => { - BinaryStatus::Downloading - } + LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, + LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, LanguageServerInstallationStatus::None => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => { - BinaryStatus::Failed { error } - } + LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, }; self.host diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 58daf906e5dcf8..ef6ac777433c88 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -74,8 +74,8 @@ pub use buffer::Operation; pub use buffer::*; pub use diagnostic_set::DiagnosticEntry; pub use language_registry::{ - AvailableLanguage, LanguageNotFound, LanguageQueries, LanguageRegistry, - BinaryStatus, QUERY_FILENAME_PREFIXES, + AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry, + QUERY_FILENAME_PREFIXES, }; pub use lsp::LanguageServerId; pub use outline::*; diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index 9047ff3438b155..ed8b0b1d49cd0e 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -12,8 +12,8 @@ use fs::Fs; use futures::{Future, FutureExt}; use gpui::AsyncApp; use language::{ - CodeLabel, HighlightId, Language, LanguageName, BinaryStatus, - LanguageToolchainStore, LspAdapter, LspAdapterDelegate, + BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, + LspAdapter, LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName}; use serde::Serialize; diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 9aadd39ed367f6..440998c9156523 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -39,7 +39,7 @@ use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedStrin use http_client::HttpClient; use language::{ proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, - Buffer, BufferSnapshot, LanguageRegistry, BinaryStatus, LanguageToolchainStore, + BinaryStatus, Buffer, BufferSnapshot, LanguageRegistry, LanguageToolchainStore, }; use lsp::LanguageServerName; use node_runtime::NodeRuntime; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index ca4d0e033138bc..e91ae53ea55999 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -36,9 +36,9 @@ use language::{ }, markdown, point_to_lsp, prepare_completion_documentation, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, - CompletionDocumentation, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, - LanguageName, LanguageRegistry, BinaryStatus, LanguageToolchainStore, LocalFile, + range_from_lsp, range_to_lsp, Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, + CodeLabel, CompletionDocumentation, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, + File as _, Language, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, }; @@ -8622,11 +8622,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { )) } - fn update_status( - &self, - server_name: LanguageServerName, - status: language::BinaryStatus, - ) { + fn update_status(&self, server_name: LanguageServerName, status: language::BinaryStatus) { self.language_registry .update_lsp_status(server_name, status); } From 28874b60cfcffb43639cb7c21979450af9a3ca12 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 4 Feb 2025 02:34:55 +0100 Subject: [PATCH 510/650] Fix up symlinks for license files --- crates/dap/LICENSE-GPL | 2 +- crates/dap_adapters/LICENSE-GPL | 2 +- crates/debugger_tools/LICENSE-GPL | 2 +- crates/debugger_ui/LICENSE-GPL | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/dap/LICENSE-GPL b/crates/dap/LICENSE-GPL index e0f9dbd5d63fef..89e542f750cd38 120000 --- a/crates/dap/LICENSE-GPL +++ b/crates/dap/LICENSE-GPL @@ -1 +1 @@ -LICENSE-GPL \ No newline at end of file +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/dap_adapters/LICENSE-GPL b/crates/dap_adapters/LICENSE-GPL index e0f9dbd5d63fef..89e542f750cd38 120000 --- a/crates/dap_adapters/LICENSE-GPL +++ b/crates/dap_adapters/LICENSE-GPL @@ -1 +1 @@ -LICENSE-GPL \ No newline at end of file +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/debugger_tools/LICENSE-GPL b/crates/debugger_tools/LICENSE-GPL index e0f9dbd5d63fef..89e542f750cd38 120000 --- a/crates/debugger_tools/LICENSE-GPL +++ b/crates/debugger_tools/LICENSE-GPL @@ -1 +1 @@ -LICENSE-GPL \ No newline at end of file +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/debugger_ui/LICENSE-GPL b/crates/debugger_ui/LICENSE-GPL index e0f9dbd5d63fef..89e542f750cd38 120000 --- a/crates/debugger_ui/LICENSE-GPL +++ b/crates/debugger_ui/LICENSE-GPL @@ -1 +1 @@ -LICENSE-GPL \ No newline at end of file +../../LICENSE-GPL \ No newline at end of file From 27b60436b840393f64d4ffab42c9cdbac5c5ac55 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 4 Feb 2025 07:47:42 -0500 Subject: [PATCH 511/650] Revert "Lazy load stack frame information (scopes & variables) (#106)" I'm reverting this because it introduced a regression with collab's test variable list & didn't supoort fetching stack frames initiated by a remote user. This reverts commit 945e3226d81945a72cdc2c6f25107520341c349d. --- crates/debugger_ui/src/console.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/stack_frame_list.rs | 10 +- crates/debugger_ui/src/tests/variable_list.rs | 584 ------------------ crates/debugger_ui/src/variable_list.rs | 106 ++-- 5 files changed, 44 insertions(+), 660 deletions(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 817bfecfa8dd56..e1d18574098e44 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -120,7 +120,7 @@ impl Console { cx: &mut Context, ) { match event { - StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(), + StackFrameListEvent::SelectedStackFrameChanged => cx.notify(), StackFrameListEvent::StackFramesUpdated => {} } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 51f1fca4b43957..ba71f1bc9d87a8 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -175,7 +175,7 @@ impl DebugPanelItem { cx.subscribe( &stack_frame_list, move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { - StackFrameListEvent::SelectedStackFrameChanged(_) + StackFrameListEvent::SelectedStackFrameChanged | StackFrameListEvent::StackFramesUpdated => this.clear_highlights(cx), }, ), diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 24c3fdbd832254..b4dd9c834c9ab3 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -19,11 +19,9 @@ use workspace::Workspace; use crate::debugger_panel_item::DebugPanelItemEvent::Stopped; use crate::debugger_panel_item::{self, DebugPanelItem}; -pub type StackFrameId = u64; - #[derive(Debug)] pub enum StackFrameListEvent { - SelectedStackFrameChanged(StackFrameId), + SelectedStackFrameChanged, StackFramesUpdated, } @@ -33,12 +31,12 @@ pub struct StackFrameList { focus_handle: FocusHandle, session_id: DebugSessionId, dap_store: Entity, + current_stack_frame_id: u64, stack_frames: Vec, entries: Vec, workspace: WeakEntity, client_id: DebugAdapterClientId, _subscriptions: Vec, - current_stack_frame_id: StackFrameId, fetch_stack_frames_task: Option>>, } @@ -246,9 +244,7 @@ impl StackFrameList { ) -> Task> { self.current_stack_frame_id = stack_frame.id; - cx.emit(StackFrameListEvent::SelectedStackFrameChanged( - stack_frame.id, - )); + cx.emit(StackFrameListEvent::SelectedStackFrameChanged); cx.notify(); if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 941350b7b4fa22..60682cdb0675d5 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -1117,587 +1117,3 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp shutdown_session.await.unwrap(); } - -#[gpui::test] -async fn test_it_only_fetches_scopes_and_variables_for_the_first_stack_frame( - executor: BackgroundExecutor, - cx: &mut TestAppContext, -) { - init_test(cx); - - let fs = FakeFs::new(executor.clone()); - - let test_file_content = r#" - import { SOME_VALUE } './module.js'; - - console.log(SOME_VALUE); - "# - .unindent(); - - let module_file_content = r#" - export SOME_VALUE = 'some value'; - "# - .unindent(); - - fs.insert_tree( - "/project", - json!({ - "src": { - "test.js": test_file_content, - "module.js": module_file_content, - } - }), - ) - .await; - - let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = init_test_workspace(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }); - - let (session, client) = task.await.unwrap(); - - client - .on_request::(move |_, _| { - Ok(dap::Capabilities { - supports_step_back: Some(false), - ..Default::default() - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - let stack_frames = vec![ - StackFrame { - id: 1, - name: "Stack Frame 1".into(), - source: Some(dap::Source { - name: Some("test.js".into()), - path: Some("/project/src/test.js".into()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - }), - line: 3, - column: 1, - end_line: None, - end_column: None, - can_restart: None, - instruction_pointer_reference: None, - module_id: None, - presentation_hint: None, - }, - StackFrame { - id: 2, - name: "Stack Frame 2".into(), - source: Some(dap::Source { - name: Some("module.js".into()), - path: Some("/project/src/module.js".into()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - }), - line: 1, - column: 1, - end_line: None, - end_column: None, - can_restart: None, - instruction_pointer_reference: None, - module_id: None, - presentation_hint: None, - }, - ]; - - client - .on_request::({ - let stack_frames = Arc::new(stack_frames.clone()); - move |_, args| { - assert_eq!(1, args.thread_id); - - Ok(dap::StackTraceResponse { - stack_frames: (*stack_frames).clone(), - total_frames: None, - }) - } - }) - .await; - - let frame_1_scopes = vec![Scope { - name: "Frame 1 Scope 1".into(), - presentation_hint: None, - variables_reference: 2, - named_variables: None, - indexed_variables: None, - expensive: false, - source: None, - line: None, - column: None, - end_line: None, - end_column: None, - }]; - - client - .on_request::({ - let frame_1_scopes = Arc::new(frame_1_scopes.clone()); - move |_, args| { - assert_eq!(1, args.frame_id); - - Ok(dap::ScopesResponse { - scopes: (*frame_1_scopes).clone(), - }) - } - }) - .await; - - let frame_1_variables = vec![ - Variable { - name: "variable1".into(), - value: "value 1".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 0, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }, - Variable { - name: "variable2".into(), - value: "value 2".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 0, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }, - ]; - - client - .on_request::({ - let frame_1_variables = Arc::new(frame_1_variables.clone()); - move |_, args| { - assert_eq!(2, args.variables_reference); - - Ok(dap::VariablesResponse { - variables: (*frame_1_variables).clone(), - }) - } - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - client - .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { - reason: dap::StoppedEventReason::Pause, - description: None, - thread_id: Some(1), - preserve_focus_hint: None, - text: None, - all_threads_stopped: None, - hit_breakpoint_ids: None, - })) - .await; - - cx.run_until_parked(); - - active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - let variable_list = debug_panel_item.variable_list().read(cx); - - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); - - assert_eq!( - frame_1_variables - .clone() - .into_iter() - .map(|variable| VariableContainer { - container_reference: 2, - variable, - depth: 1 - }) - .collect::>(), - variable_list.variables_by_stack_frame_id(1) - ); - assert!(variable_list.variables_by_stack_frame_id(2).is_empty()); - }); - - let shutdown_session = project.update(cx, |project, cx| { - project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) - }) - }); - - shutdown_session.await.unwrap(); -} - -#[gpui::test] -async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( - executor: BackgroundExecutor, - cx: &mut TestAppContext, -) { - init_test(cx); - - let fs = FakeFs::new(executor.clone()); - - let test_file_content = r#" - import { SOME_VALUE } './module.js'; - - console.log(SOME_VALUE); - "# - .unindent(); - - let module_file_content = r#" - export SOME_VALUE = 'some value'; - "# - .unindent(); - - fs.insert_tree( - "/project", - json!({ - "src": { - "test.js": test_file_content, - "module.js": module_file_content, - } - }), - ) - .await; - - let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = init_test_workspace(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }); - - let (session, client) = task.await.unwrap(); - - client - .on_request::(move |_, _| { - Ok(dap::Capabilities { - supports_step_back: Some(false), - ..Default::default() - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - let stack_frames = vec![ - StackFrame { - id: 1, - name: "Stack Frame 1".into(), - source: Some(dap::Source { - name: Some("test.js".into()), - path: Some("/project/src/test.js".into()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - }), - line: 3, - column: 1, - end_line: None, - end_column: None, - can_restart: None, - instruction_pointer_reference: None, - module_id: None, - presentation_hint: None, - }, - StackFrame { - id: 2, - name: "Stack Frame 2".into(), - source: Some(dap::Source { - name: Some("module.js".into()), - path: Some("/project/src/module.js".into()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - }), - line: 1, - column: 1, - end_line: None, - end_column: None, - can_restart: None, - instruction_pointer_reference: None, - module_id: None, - presentation_hint: None, - }, - ]; - - client - .on_request::({ - let stack_frames = Arc::new(stack_frames.clone()); - move |_, args| { - assert_eq!(1, args.thread_id); - - Ok(dap::StackTraceResponse { - stack_frames: (*stack_frames).clone(), - total_frames: None, - }) - } - }) - .await; - - let frame_1_scopes = vec![Scope { - name: "Frame 1 Scope 1".into(), - presentation_hint: None, - variables_reference: 2, - named_variables: None, - indexed_variables: None, - expensive: false, - source: None, - line: None, - column: None, - end_line: None, - end_column: None, - }]; - - client - .on_request::({ - let frame_1_scopes = Arc::new(frame_1_scopes.clone()); - move |_, args| { - assert_eq!(1, args.frame_id); - - Ok(dap::ScopesResponse { - scopes: (*frame_1_scopes).clone(), - }) - } - }) - .await; - - let frame_1_variables = vec![ - Variable { - name: "variable1".into(), - value: "value 1".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 0, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }, - Variable { - name: "variable2".into(), - value: "value 2".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 0, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }, - ]; - - client - .on_request::({ - let frame_1_variables = Arc::new(frame_1_variables.clone()); - move |_, args| { - assert_eq!(2, args.variables_reference); - - Ok(dap::VariablesResponse { - variables: (*frame_1_variables).clone(), - }) - } - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - client - .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { - reason: dap::StoppedEventReason::Pause, - description: None, - thread_id: Some(1), - preserve_focus_hint: None, - text: None, - all_threads_stopped: None, - hit_breakpoint_ids: None, - })) - .await; - - cx.run_until_parked(); - - active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - let variable_list = debug_panel_item.variable_list().read(cx); - - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); - - assert_eq!( - frame_1_variables - .clone() - .into_iter() - .map(|variable| VariableContainer { - container_reference: 2, - variable, - depth: 1 - }) - .collect::>(), - variable_list.variables_by_stack_frame_id(1) - ); - assert!(variable_list.variables_by_stack_frame_id(2).is_empty()); - }); - - // add handlers for fetching the second stack frame's scopes and variables - // after the user clicked the stack frame - - let frame_2_scopes = vec![Scope { - name: "Frame 2 Scope 1".into(), - presentation_hint: None, - variables_reference: 3, - named_variables: None, - indexed_variables: None, - expensive: false, - source: None, - line: None, - column: None, - end_line: None, - end_column: None, - }]; - - client - .on_request::({ - let frame_2_scopes = Arc::new(frame_2_scopes.clone()); - move |_, args| { - assert_eq!(2, args.frame_id); - - Ok(dap::ScopesResponse { - scopes: (*frame_2_scopes).clone(), - }) - } - }) - .await; - - let frame_2_variables = vec![ - Variable { - name: "variable3".into(), - value: "old value 1".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 0, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }, - Variable { - name: "variable4".into(), - value: "old value 2".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 0, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }, - ]; - - client - .on_request::({ - let frame_2_variables = Arc::new(frame_2_variables.clone()); - move |_, args| { - assert_eq!(3, args.variables_reference); - - Ok(dap::VariablesResponse { - variables: (*frame_2_variables).clone(), - }) - } - }) - .await; - - active_debug_panel_item(workspace, cx) - .update_in(cx, |debug_panel_item, window, cx| { - debug_panel_item - .stack_frame_list() - .update(cx, |stack_frame_list, cx| { - stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx) - }) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - let variable_list = debug_panel_item.variable_list().read(cx); - - assert_eq!(2, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); - - assert_eq!( - frame_1_variables - .into_iter() - .map(|variable| VariableContainer { - container_reference: 2, - variable, - depth: 1 - }) - .collect::>(), - variable_list.variables_by_stack_frame_id(1) - ); - assert_eq!( - frame_2_variables - .into_iter() - .map(|variable| VariableContainer { - container_reference: 3, - variable, - depth: 1 - }) - .collect::>(), - variable_list.variables_by_stack_frame_id(2) - ); - }); - - let shutdown_session = project.update(cx, |project, cx| { - project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) - }) - }); - - shutdown_session.await.unwrap(); -} diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 61dd782a386bf7..d33fe62c9dfd6a 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,4 +1,4 @@ -use crate::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; +use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, @@ -324,6 +324,7 @@ impl ScopeVariableIndex { } } +type StackFrameId = u64; type ScopeId = u64; pub struct VariableList { @@ -510,8 +511,8 @@ impl VariableList { cx: &mut Context, ) { match event { - StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => { - self.handle_selected_stack_frame_changed(*stack_frame_id, cx); + StackFrameListEvent::SelectedStackFrameChanged => { + self.build_entries(true, true, cx); } StackFrameListEvent::StackFramesUpdated => { self.fetch_variables(cx); @@ -519,40 +520,6 @@ impl VariableList { } } - fn handle_selected_stack_frame_changed( - &mut self, - stack_frame_id: StackFrameId, - cx: &mut Context, - ) { - if self.scopes.contains_key(&stack_frame_id) { - return self.build_entries(true, true, cx); - } - - self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { - let task = this.update(&mut cx, |variable_list, cx| { - variable_list.fetch_variables_for_stack_frame(stack_frame_id, &Vec::default(), cx) - })?; - - let (scopes, variables) = task.await?; - - this.update(&mut cx, |variable_list, cx| { - variable_list.scopes.insert(stack_frame_id, scopes); - - for (scope_id, variables) in variables.into_iter() { - let mut variable_index = ScopeVariableIndex::new(); - variable_index.add_variables(scope_id, variables); - - variable_list - .variables - .insert((stack_frame_id, scope_id), variable_index); - } - - variable_list.build_entries(true, true, cx); - variable_list.send_update_proto_message(cx); - }) - })); - } - #[cfg(any(test, feature = "test-support"))] pub fn scopes(&self) -> &HashMap> { &self.scopes @@ -963,6 +930,8 @@ impl VariableList { let stack_frames = self.stack_frame_list.read(cx).stack_frames().clone(); self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { + let mut tasks = Vec::with_capacity(stack_frames.len()); + let open_entries = this.update(&mut cx, |this, _| { this.open_entries .iter() @@ -971,27 +940,34 @@ impl VariableList { .collect::>() })?; - let first_stack_frame = stack_frames - .first() - .ok_or(anyhow!("Expected to find a stackframe"))?; + for stack_frame in stack_frames.clone().into_iter() { + let task = this.update(&mut cx, |this, cx| { + this.fetch_variables_for_stack_frame(stack_frame.id, &open_entries, cx) + }); + + tasks.push( + cx.background_executor() + .spawn(async move { anyhow::Ok((stack_frame.id, task?.await?)) }), + ); + } - let (scopes, variables) = this - .update(&mut cx, |this, cx| { - this.fetch_variables_for_stack_frame(first_stack_frame.id, &open_entries, cx) - })? - .await?; + let results = futures::future::join_all(tasks).await; this.update(&mut cx, |this, cx| { let mut new_variables = BTreeMap::new(); let mut new_scopes = HashMap::new(); - new_scopes.insert(first_stack_frame.id, scopes); + for (stack_frame_id, (scopes, variables)) in + results.into_iter().filter_map(|result| result.ok()) + { + new_scopes.insert(stack_frame_id, scopes); - for (scope_id, variables) in variables.into_iter() { - let mut variable_index = ScopeVariableIndex::new(); - variable_index.add_variables(scope_id, variables); + for (scope_id, variables) in variables.into_iter() { + let mut variable_index = ScopeVariableIndex::new(); + variable_index.add_variables(scope_id, variables); - new_variables.insert((first_stack_frame.id, scope_id), variable_index); + new_variables.insert((stack_frame_id, scope_id), variable_index); + } } std::mem::swap(&mut this.variables, &mut new_variables); @@ -1000,29 +976,25 @@ impl VariableList { this.entries.clear(); this.build_entries(true, true, cx); - this.send_update_proto_message(cx); + if let Some((client, project_id)) = this.dap_store.read(cx).downstream_client() { + let request = UpdateDebugAdapter { + client_id: this.client_id.to_proto(), + session_id: this.session_id.to_proto(), + thread_id: Some(this.stack_frame_list.read(cx).thread_id()), + project_id: *project_id, + variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( + this.to_proto(), + )), + }; + + client.send(request).log_err(); + }; this.fetch_variables_task.take(); }) })); } - fn send_update_proto_message(&self, cx: &mut Context) { - if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { - let request = UpdateDebugAdapter { - client_id: self.client_id.to_proto(), - session_id: self.session_id.to_proto(), - thread_id: Some(self.stack_frame_list.read(cx).thread_id()), - project_id: *project_id, - variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( - self.to_proto(), - )), - }; - - client.send(request).log_err(); - }; - } - fn deploy_variable_context_menu( &mut self, parent_variables_reference: u64, From 3cf96588e2c48251fe8a8fa93b796ae5eae4146d Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 4 Feb 2025 07:50:43 -0500 Subject: [PATCH 512/650] Revert "Remove futures from debugger ui crate" Futures are once again needed by variable list dued to the lazy stack frame fetching PR revert This reverts commit 26f14fd036aef740b06b28e57cb7256d3ceb7974. --- Cargo.lock | 1 + crates/debugger_ui/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 756c974ffbf9d3..5308cfee5c4a90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3815,6 +3815,7 @@ dependencies = [ "dap", "editor", "env_logger 0.11.6", + "futures 0.3.31", "fuzzy", "gpui", "language", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 076af6e56485c2..e66605473c3f77 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -25,6 +25,7 @@ collections.workspace = true command_palette_hooks.workspace = true dap.workspace = true editor.workspace = true +futures.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true From c76144e9186a6629def9a4c55636950b66a261f3 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 5 Feb 2025 17:45:03 +0100 Subject: [PATCH 513/650] Re-Apply Lazy load stack frame information (#108) * Reapply "Lazy load stack frame information (scopes & variables) (#106)" This reverts commit 27b60436b840393f64d4ffab42c9cdbac5c5ac55. * Reapply "Remove futures from debugger ui crate" This reverts commit 3cf96588e2c48251fe8a8fa93b796ae5eae4146d. * Don't fetch initial variables twice This was introduced by my original PR, because I added the fetch on stack frame select but when the stack frames where updated we would already fetch the initial variables. And when the selectedStackFrameUpdated event was received we would refetch it because it was not done yet. * Remove duplicated method just for testing * Make keep open entries work again The issue was the we used the scope_id, which changes after each debug step. So using the name of the scope instead should be more reliable. * Correctly invalidate variable list information * Comment out collab variable list for now Also commenting out an event that triggers the infinite loop. We want to revisited the collab stuff anyway so we decided to do it this way. --- Cargo.lock | 1 - crates/collab/src/tests/debug_panel_tests.rs | 895 +++++++++--------- crates/debugger_ui/Cargo.toml | 1 - crates/debugger_ui/src/console.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 2 +- crates/debugger_ui/src/stack_frame_list.rs | 10 +- crates/debugger_ui/src/tests/console.rs | 2 +- crates/debugger_ui/src/tests/variable_list.rs | 584 ++++++++++++ crates/debugger_ui/src/variable_list.rs | 238 ++--- crates/project/src/dap_command.rs | 4 +- crates/proto/proto/zed.proto | 2 +- 11 files changed, 1143 insertions(+), 598 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cc9f988617bd1..579d4aa9ad4248 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3759,7 +3759,6 @@ dependencies = [ "dap", "editor", "env_logger 0.11.6", - "futures 0.3.31", "fuzzy", "gpui", "language", diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index e90b42a178c543..c4aea60da88b9b 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1,12 +1,11 @@ use call::ActiveCall; -use dap::requests::{Disconnect, Initialize, Launch, Scopes, StackTrace, Variables}; +use dap::requests::{Disconnect, Initialize, Launch, StackTrace}; use dap::{ requests::{RestartFrame, SetBreakpoints}, SourceBreakpoint, StackFrame, }; -use dap::{Scope, Variable}; +use debugger_ui::debugger_panel::DebugPanel; use debugger_ui::debugger_panel_item::DebugPanelItem; -use debugger_ui::{debugger_panel::DebugPanel, variable_list::VariableContainer}; use editor::Editor; use gpui::{Entity, TestAppContext, VisualTestContext}; use project::{Project, ProjectPath, WorktreeId}; @@ -1340,456 +1339,446 @@ async fn test_module_list( shutdown_client.await.unwrap(); } -#[gpui::test] -async fn test_variable_list( - host_cx: &mut TestAppContext, - remote_cx: &mut TestAppContext, - late_join_cx: &mut TestAppContext, -) { - let executor = host_cx.executor(); - let mut server = TestServer::start(executor).await; - - let (mut host_zed, mut remote_zed, mut late_join_zed) = - setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await; - - let (host_project_id, _worktree_id) = host_zed - .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"}))) - .await; - - remote_zed.join_project(host_project_id).await; - - let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; - let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; - - let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) - }); - - let (session, client) = task.await.unwrap(); - - client - .on_request::(move |_, _| { - Ok(dap::Capabilities { - supports_step_back: Some(true), - ..Default::default() - }) - }) - .await; - - client.on_request::(move |_, _| Ok(())).await; - - let stack_frames = vec![dap::StackFrame { - id: 1, - name: "Stack Frame 1".into(), - source: Some(dap::Source { - name: Some("test.js".into()), - path: Some("/project/src/test.js".into()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - }), - line: 1, - column: 1, - end_line: None, - end_column: None, - can_restart: None, - instruction_pointer_reference: None, - module_id: None, - presentation_hint: None, - }]; - - let scopes = vec![Scope { - name: "Scope 1".into(), - presentation_hint: None, - variables_reference: 1, - named_variables: None, - indexed_variables: None, - expensive: false, - source: None, - line: None, - column: None, - end_line: None, - end_column: None, - }]; - - let variable_1 = Variable { - name: "variable 1".into(), - value: "1".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 2, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }; - - let variable_2 = Variable { - name: "variable 2".into(), - value: "2".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 3, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }; - - let variable_3 = Variable { - name: "variable 3".into(), - value: "hello world".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 4, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }; - - let variable_4 = Variable { - name: "variable 4".into(), - value: "hello world this is the final variable".into(), - type_: None, - presentation_hint: None, - evaluate_name: None, - variables_reference: 0, - named_variables: None, - indexed_variables: None, - memory_reference: None, - }; - - client - .on_request::({ - let stack_frames = std::sync::Arc::new(stack_frames.clone()); - move |_, args| { - assert_eq!(1, args.thread_id); - - Ok(dap::StackTraceResponse { - stack_frames: (*stack_frames).clone(), - total_frames: None, - }) - } - }) - .await; - - client - .on_request::({ - let scopes = Arc::new(scopes.clone()); - move |_, args| { - assert_eq!(1, args.frame_id); - - Ok(dap::ScopesResponse { - scopes: (*scopes).clone(), - }) - } - }) - .await; - - let first_variable_request = vec![variable_1.clone(), variable_2.clone()]; - - client - .on_request::({ - move |_, args| { - assert_eq!(1, args.variables_reference); - - Ok(dap::VariablesResponse { - variables: first_variable_request.clone(), - }) - } - }) - .await; - - client - .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { - reason: dap::StoppedEventReason::Pause, - description: None, - thread_id: Some(1), - preserve_focus_hint: None, - text: None, - all_threads_stopped: None, - hit_breakpoint_ids: None, - })) - .await; - - host_cx.run_until_parked(); - remote_cx.run_until_parked(); - - let local_debug_item = host_workspace.update(host_cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - assert_eq!( - 1, - debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) - ); - assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); - active_debug_panel_item - }); - - let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - assert_eq!( - 1, - debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) - ); - assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); - active_debug_panel_item - }); - - let first_visual_entries = vec!["v Scope 1", " > variable 1", " > variable 2"]; - let first_variable_containers = vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variable_1.clone(), - depth: 1, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variable_2.clone(), - depth: 1, - }, - ]; - - local_debug_item - .update(host_cx, |this, _| this.variable_list().clone()) - .update(host_cx, |variable_list, cx| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - assert_eq!(&first_variable_containers, &variable_list.variables()); - - variable_list.assert_visual_entries(first_visual_entries.clone(), cx); - }); - - client - .on_request::({ - let variables = Arc::new(vec![variable_3.clone()]); - move |_, args| { - assert_eq!(2, args.variables_reference); - - Ok(dap::VariablesResponse { - variables: (*variables).clone(), - }) - } - }) - .await; - - remote_debug_item - .update(remote_cx, |this, _| this.variable_list().clone()) - .update(remote_cx, |variable_list, cx| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - assert_eq!(&first_variable_containers, &variable_list.variables()); - - variable_list.assert_visual_entries(first_visual_entries.clone(), cx); - - variable_list.toggle_variable_in_test( - scopes[0].variables_reference, - &variable_1, - 1, - cx, - ); - }); - - host_cx.run_until_parked(); - remote_cx.run_until_parked(); - - let second_req_variable_list = vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variable_1.clone(), - depth: 1, - }, - VariableContainer { - container_reference: variable_1.variables_reference, - variable: variable_3.clone(), - depth: 2, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variable_2.clone(), - depth: 1, - }, - ]; - - remote_debug_item - .update(remote_cx, |this, _| this.variable_list().clone()) - .update(remote_cx, |variable_list, cx| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(3, variable_list.variables().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - assert_eq!(&second_req_variable_list, &variable_list.variables()); - - variable_list.assert_visual_entries( - vec![ - "v Scope 1", - " v variable 1", - " > variable 3", - " > variable 2", - ], - cx, - ); - }); - - client - .on_request::({ - let variables = Arc::new(vec![variable_4.clone()]); - move |_, args| { - assert_eq!(3, args.variables_reference); - - Ok(dap::VariablesResponse { - variables: (*variables).clone(), - }) - } - }) - .await; - - local_debug_item - .update(host_cx, |this, _| this.variable_list().clone()) - .update(host_cx, |variable_list, cx| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(3, variable_list.variables().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - assert_eq!(&second_req_variable_list, &variable_list.variables()); - - variable_list.assert_visual_entries(first_visual_entries.clone(), cx); - - variable_list.toggle_variable_in_test( - scopes[0].variables_reference, - &variable_2.clone(), - 1, - cx, - ); - }); - - host_cx.run_until_parked(); - remote_cx.run_until_parked(); - - let final_variable_containers: Vec = vec![ - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variable_1.clone(), - depth: 1, - }, - VariableContainer { - container_reference: variable_1.variables_reference, - variable: variable_3.clone(), - depth: 2, - }, - VariableContainer { - container_reference: scopes[0].variables_reference, - variable: variable_2.clone(), - depth: 1, - }, - VariableContainer { - container_reference: variable_2.variables_reference, - variable: variable_4.clone(), - depth: 2, - }, - ]; - - remote_debug_item - .update(remote_cx, |this, _| this.variable_list().clone()) - .update(remote_cx, |variable_list, cx| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(4, variable_list.variables().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - assert_eq!(&final_variable_containers, &variable_list.variables()); - - variable_list.assert_visual_entries( - vec![ - "v Scope 1", - " v variable 1", - " > variable 3", - " > variable 2", - ], - cx, - ); - }); - - local_debug_item - .update(host_cx, |this, _| this.variable_list().clone()) - .update(host_cx, |variable_list, cx| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(4, variable_list.variables().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - assert_eq!(&final_variable_containers, &variable_list.variables()); - - variable_list.assert_visual_entries( - vec![ - "v Scope 1", - " > variable 1", - " v variable 2", - " > variable 4", - ], - cx, - ); - }); - - late_join_zed.join_project(host_project_id).await; - let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) = - late_join_zed.expand().await; - - late_join_cx.run_until_parked(); - - let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - let active_debug_panel_item = debug_panel - .update(cx, |this, cx| this.active_debug_panel_item(cx)) - .unwrap(); - - assert_eq!( - 1, - debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) - ); - assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); - active_debug_panel_item - }); - - last_join_remote_item - .update(late_join_cx, |this, _| this.variable_list().clone()) - .update(late_join_cx, |variable_list, cx| { - assert_eq!(1, variable_list.scopes().len()); - assert_eq!(4, variable_list.variables().len()); - assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); - assert_eq!(final_variable_containers, variable_list.variables()); - - variable_list.assert_visual_entries(first_visual_entries, cx); - }); - - client.on_request::(move |_, _| Ok(())).await; - - let shutdown_client = host_project.update(host_cx, |project, cx| { - project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) - }) - }); - - shutdown_client.await.unwrap(); -} +// #[gpui::test] +// async fn test_variable_list( +// host_cx: &mut TestAppContext, +// remote_cx: &mut TestAppContext, +// late_join_cx: &mut TestAppContext, +// ) { +// let executor = host_cx.executor(); +// let mut server = TestServer::start(executor).await; + +// let (mut host_zed, mut remote_zed, mut late_join_zed) = +// setup_three_member_test(&mut server, host_cx, remote_cx, late_join_cx).await; + +// let (host_project_id, _worktree_id) = host_zed +// .host_project(Some(json!({"test.txt": "one\ntwo\nthree\nfour\nfive"}))) +// .await; + +// remote_zed.join_project(host_project_id).await; + +// let (_client_host, host_workspace, host_project, host_cx) = host_zed.expand().await; +// let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; + +// let task = host_project.update(host_cx, |project, cx| { +// project.start_debug_session( +// dap::DebugAdapterConfig { +// label: "test config".into(), +// kind: dap::DebugAdapterKind::Fake, +// request: dap::DebugRequestType::Launch, +// program: None, +// cwd: None, +// initialize_args: None, +// }, +// cx, +// ) +// }); + +// let (session, client) = task.await.unwrap(); + +// client +// .on_request::(move |_, _| { +// Ok(dap::Capabilities { +// supports_step_back: Some(true), +// ..Default::default() +// }) +// }) +// .await; + +// client.on_request::(move |_, _| Ok(())).await; + +// let stack_frames = vec![dap::StackFrame { +// id: 1, +// name: "Stack Frame 1".into(), +// source: Some(dap::Source { +// name: Some("test.js".into()), +// path: Some("/project/src/test.js".into()), +// source_reference: None, +// presentation_hint: None, +// origin: None, +// sources: None, +// adapter_data: None, +// checksums: None, +// }), +// line: 1, +// column: 1, +// end_line: None, +// end_column: None, +// can_restart: None, +// instruction_pointer_reference: None, +// module_id: None, +// presentation_hint: None, +// }]; + +// let scopes = vec![Scope { +// name: "Scope 1".into(), +// presentation_hint: None, +// variables_reference: 1, +// named_variables: None, +// indexed_variables: None, +// expensive: false, +// source: None, +// line: None, +// column: None, +// end_line: None, +// end_column: None, +// }]; + +// let variable_1 = Variable { +// name: "variable 1".into(), +// value: "1".into(), +// type_: None, +// presentation_hint: None, +// evaluate_name: None, +// variables_reference: 2, +// named_variables: None, +// indexed_variables: None, +// memory_reference: None, +// }; + +// let variable_2 = Variable { +// name: "variable 2".into(), +// value: "2".into(), +// type_: None, +// presentation_hint: None, +// evaluate_name: None, +// variables_reference: 3, +// named_variables: None, +// indexed_variables: None, +// memory_reference: None, +// }; + +// let variable_3 = Variable { +// name: "variable 3".into(), +// value: "hello world".into(), +// type_: None, +// presentation_hint: None, +// evaluate_name: None, +// variables_reference: 4, +// named_variables: None, +// indexed_variables: None, +// memory_reference: None, +// }; + +// let variable_4 = Variable { +// name: "variable 4".into(), +// value: "hello world this is the final variable".into(), +// type_: None, +// presentation_hint: None, +// evaluate_name: None, +// variables_reference: 0, +// named_variables: None, +// indexed_variables: None, +// memory_reference: None, +// }; + +// client +// .on_request::({ +// let stack_frames = std::sync::Arc::new(stack_frames.clone()); +// move |_, args| { +// assert_eq!(1, args.thread_id); + +// Ok(dap::StackTraceResponse { +// stack_frames: (*stack_frames).clone(), +// total_frames: None, +// }) +// } +// }) +// .await; + +// client +// .on_request::({ +// let scopes = Arc::new(scopes.clone()); +// move |_, args| { +// assert_eq!(1, args.frame_id); + +// Ok(dap::ScopesResponse { +// scopes: (*scopes).clone(), +// }) +// } +// }) +// .await; + +// let first_variable_request = vec![variable_1.clone(), variable_2.clone()]; + +// client +// .on_request::({ +// move |_, args| { +// assert_eq!(1, args.variables_reference); + +// Ok(dap::VariablesResponse { +// variables: first_variable_request.clone(), +// }) +// } +// }) +// .await; + +// client +// .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { +// reason: dap::StoppedEventReason::Pause, +// description: None, +// thread_id: Some(1), +// preserve_focus_hint: None, +// text: None, +// all_threads_stopped: None, +// hit_breakpoint_ids: None, +// })) +// .await; + +// host_cx.run_until_parked(); +// remote_cx.run_until_parked(); + +// let local_debug_item = host_workspace.update(host_cx, |workspace, cx| { +// let debug_panel = workspace.panel::(cx).unwrap(); +// let active_debug_panel_item = debug_panel +// .update(cx, |this, cx| this.active_debug_panel_item(cx)) +// .unwrap(); + +// assert_eq!( +// 1, +// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) +// ); +// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); +// assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); +// active_debug_panel_item +// }); + +// let remote_debug_item = remote_workspace.update(remote_cx, |workspace, cx| { +// let debug_panel = workspace.panel::(cx).unwrap(); +// let active_debug_panel_item = debug_panel +// .update(cx, |this, cx| this.active_debug_panel_item(cx)) +// .unwrap(); + +// assert_eq!( +// 1, +// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) +// ); +// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); +// assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); +// active_debug_panel_item +// }); + +// let first_visual_entries = vec!["v Scope 1", " > variable 1", " > variable 2"]; +// let first_variable_containers = vec![ +// VariableContainer { +// container_reference: scopes[0].variables_reference, +// variable: variable_1.clone(), +// depth: 1, +// }, +// VariableContainer { +// container_reference: scopes[0].variables_reference, +// variable: variable_2.clone(), +// depth: 1, +// }, +// ]; + +// local_debug_item +// .update(host_cx, |this, _| this.variable_list().clone()) +// .update(host_cx, |variable_list, cx| { +// assert_eq!(1, variable_list.scopes().len()); +// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); +// assert_eq!(&first_variable_containers, &variable_list.variables()); + +// variable_list.assert_visual_entries(first_visual_entries.clone(), cx); +// }); + +// client +// .on_request::({ +// let variables = Arc::new(vec![variable_3.clone()]); +// move |_, args| { +// assert_eq!(2, args.variables_reference); + +// Ok(dap::VariablesResponse { +// variables: (*variables).clone(), +// }) +// } +// }) +// .await; + +// remote_debug_item +// .update(remote_cx, |this, _| this.variable_list().clone()) +// .update(remote_cx, |variable_list, cx| { +// assert_eq!(1, variable_list.scopes().len()); +// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); +// assert_eq!(&first_variable_containers, &variable_list.variables()); + +// variable_list.assert_visual_entries(first_visual_entries.clone(), cx); + +// variable_list.toggle_variable(&scopes[0], &variable_1, 1, cx); +// }); + +// host_cx.run_until_parked(); +// remote_cx.run_until_parked(); + +// let second_req_variable_list = vec![ +// VariableContainer { +// container_reference: scopes[0].variables_reference, +// variable: variable_1.clone(), +// depth: 1, +// }, +// VariableContainer { +// container_reference: variable_1.variables_reference, +// variable: variable_3.clone(), +// depth: 2, +// }, +// VariableContainer { +// container_reference: scopes[0].variables_reference, +// variable: variable_2.clone(), +// depth: 1, +// }, +// ]; + +// remote_debug_item +// .update(remote_cx, |this, _| this.variable_list().clone()) +// .update(remote_cx, |variable_list, cx| { +// assert_eq!(1, variable_list.scopes().len()); +// assert_eq!(3, variable_list.variables().len()); +// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); +// assert_eq!(&second_req_variable_list, &variable_list.variables()); + +// variable_list.assert_visual_entries( +// vec![ +// "v Scope 1", +// " v variable 1", +// " > variable 3", +// " > variable 2", +// ], +// cx, +// ); +// }); + +// client +// .on_request::({ +// let variables = Arc::new(vec![variable_4.clone()]); +// move |_, args| { +// assert_eq!(3, args.variables_reference); + +// Ok(dap::VariablesResponse { +// variables: (*variables).clone(), +// }) +// } +// }) +// .await; + +// local_debug_item +// .update(host_cx, |this, _| this.variable_list().clone()) +// .update(host_cx, |variable_list, cx| { +// assert_eq!(1, variable_list.scopes().len()); +// assert_eq!(3, variable_list.variables().len()); +// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); +// assert_eq!(&second_req_variable_list, &variable_list.variables()); + +// variable_list.assert_visual_entries(first_visual_entries.clone(), cx); + +// variable_list.toggle_variable(&scopes[0], &variable_2.clone(), 1, cx); +// }); + +// host_cx.run_until_parked(); +// remote_cx.run_until_parked(); + +// let final_variable_containers: Vec = vec![ +// VariableContainer { +// container_reference: scopes[0].variables_reference, +// variable: variable_1.clone(), +// depth: 1, +// }, +// VariableContainer { +// container_reference: variable_1.variables_reference, +// variable: variable_3.clone(), +// depth: 2, +// }, +// VariableContainer { +// container_reference: scopes[0].variables_reference, +// variable: variable_2.clone(), +// depth: 1, +// }, +// VariableContainer { +// container_reference: variable_2.variables_reference, +// variable: variable_4.clone(), +// depth: 2, +// }, +// ]; + +// remote_debug_item +// .update(remote_cx, |this, _| this.variable_list().clone()) +// .update(remote_cx, |variable_list, cx| { +// assert_eq!(1, variable_list.scopes().len()); +// assert_eq!(4, variable_list.variables().len()); +// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); +// assert_eq!(&final_variable_containers, &variable_list.variables()); + +// variable_list.assert_visual_entries( +// vec![ +// "v Scope 1", +// " v variable 1", +// " > variable 3", +// " > variable 2", +// ], +// cx, +// ); +// }); + +// local_debug_item +// .update(host_cx, |this, _| this.variable_list().clone()) +// .update(host_cx, |variable_list, cx| { +// assert_eq!(1, variable_list.scopes().len()); +// assert_eq!(4, variable_list.variables().len()); +// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); +// assert_eq!(&final_variable_containers, &variable_list.variables()); + +// variable_list.assert_visual_entries( +// vec![ +// "v Scope 1", +// " > variable 1", +// " v variable 2", +// " > variable 4", +// ], +// cx, +// ); +// }); + +// late_join_zed.join_project(host_project_id).await; +// let (_late_join_client, late_join_workspace, _late_join_project, late_join_cx) = +// late_join_zed.expand().await; + +// late_join_cx.run_until_parked(); + +// let last_join_remote_item = late_join_workspace.update(late_join_cx, |workspace, cx| { +// let debug_panel = workspace.panel::(cx).unwrap(); +// let active_debug_panel_item = debug_panel +// .update(cx, |this, cx| this.active_debug_panel_item(cx)) +// .unwrap(); + +// assert_eq!( +// 1, +// debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) +// ); +// assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); +// assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); +// active_debug_panel_item +// }); + +// last_join_remote_item +// .update(late_join_cx, |this, _| this.variable_list().clone()) +// .update(late_join_cx, |variable_list, cx| { +// assert_eq!(1, variable_list.scopes().len()); +// assert_eq!(4, variable_list.variables().len()); +// assert_eq!(scopes, variable_list.scopes().get(&1).unwrap().clone()); +// assert_eq!(final_variable_containers, variable_list.variables()); + +// variable_list.assert_visual_entries(first_visual_entries, cx); +// }); + +// client.on_request::(move |_, _| Ok(())).await; + +// let shutdown_client = host_project.update(host_cx, |project, cx| { +// project.dap_store().update(cx, |dap_store, cx| { +// dap_store.shutdown_session(&session.read(cx).id(), cx) +// }) +// }); + +// shutdown_client.await.unwrap(); +// } #[gpui::test] async fn test_ignore_breakpoints( diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index e66605473c3f77..076af6e56485c2 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -25,7 +25,6 @@ collections.workspace = true command_palette_hooks.workspace = true dap.workspace = true editor.workspace = true -futures.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index e1d18574098e44..817bfecfa8dd56 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -120,7 +120,7 @@ impl Console { cx: &mut Context, ) { match event { - StackFrameListEvent::SelectedStackFrameChanged => cx.notify(), + StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(), StackFrameListEvent::StackFramesUpdated => {} } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index ba71f1bc9d87a8..51f1fca4b43957 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -175,7 +175,7 @@ impl DebugPanelItem { cx.subscribe( &stack_frame_list, move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { - StackFrameListEvent::SelectedStackFrameChanged + StackFrameListEvent::SelectedStackFrameChanged(_) | StackFrameListEvent::StackFramesUpdated => this.clear_highlights(cx), }, ), diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index b4dd9c834c9ab3..24c3fdbd832254 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -19,9 +19,11 @@ use workspace::Workspace; use crate::debugger_panel_item::DebugPanelItemEvent::Stopped; use crate::debugger_panel_item::{self, DebugPanelItem}; +pub type StackFrameId = u64; + #[derive(Debug)] pub enum StackFrameListEvent { - SelectedStackFrameChanged, + SelectedStackFrameChanged(StackFrameId), StackFramesUpdated, } @@ -31,12 +33,12 @@ pub struct StackFrameList { focus_handle: FocusHandle, session_id: DebugSessionId, dap_store: Entity, - current_stack_frame_id: u64, stack_frames: Vec, entries: Vec, workspace: WeakEntity, client_id: DebugAdapterClientId, _subscriptions: Vec, + current_stack_frame_id: StackFrameId, fetch_stack_frames_task: Option>>, } @@ -244,7 +246,9 @@ impl StackFrameList { ) -> Task> { self.current_stack_frame_id = stack_frame.id; - cx.emit(StackFrameListEvent::SelectedStackFrameChanged); + cx.emit(StackFrameListEvent::SelectedStackFrameChanged( + stack_frame.id, + )); cx.notify(); if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 1f653e8af08288..85db5619dbb0a7 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -739,7 +739,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp .update(cx, |variable_list, cx| { variable_list.toggle_entry( &variable_list::OpenEntry::Variable { - scope_id: scopes[0].variables_reference, + scope_name: scopes[0].name.clone(), name: scope1_variables[0].name.clone(), depth: 1, }, diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 60682cdb0675d5..941350b7b4fa22 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -1117,3 +1117,587 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_it_only_fetches_scopes_and_variables_for_the_first_stack_frame( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + import { SOME_VALUE } './module.js'; + + console.log(SOME_VALUE); + "# + .unindent(); + + let module_file_content = r#" + export SOME_VALUE = 'some value'; + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + "module.js": module_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![ + StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + StackFrame { + id: 2, + name: "Stack Frame 2".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + ]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + let frame_1_scopes = vec![Scope { + name: "Frame 1 Scope 1".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }]; + + client + .on_request::({ + let frame_1_scopes = Arc::new(frame_1_scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*frame_1_scopes).clone(), + }) + } + }) + .await; + + let frame_1_variables = vec![ + Variable { + name: "variable1".into(), + value: "value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "variable2".into(), + value: "value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + client + .on_request::({ + let frame_1_variables = Arc::new(frame_1_variables.clone()); + move |_, args| { + assert_eq!(2, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*frame_1_variables).clone(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let variable_list = debug_panel_item.variable_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + assert_eq!( + frame_1_variables + .clone() + .into_iter() + .map(|variable| VariableContainer { + container_reference: 2, + variable, + depth: 1 + }) + .collect::>(), + variable_list.variables_by_stack_frame_id(1) + ); + assert!(variable_list.variables_by_stack_frame_id(2).is_empty()); + }); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} + +#[gpui::test] +async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let test_file_content = r#" + import { SOME_VALUE } './module.js'; + + console.log(SOME_VALUE); + "# + .unindent(); + + let module_file_content = r#" + export SOME_VALUE = 'some value'; + "# + .unindent(); + + fs.insert_tree( + "/project", + json!({ + "src": { + "test.js": test_file_content, + "module.js": module_file_content, + } + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_step_back: Some(false), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + let stack_frames = vec![ + StackFrame { + id: 1, + name: "Stack Frame 1".into(), + source: Some(dap::Source { + name: Some("test.js".into()), + path: Some("/project/src/test.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 3, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + StackFrame { + id: 2, + name: "Stack Frame 2".into(), + source: Some(dap::Source { + name: Some("module.js".into()), + path: Some("/project/src/module.js".into()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }), + line: 1, + column: 1, + end_line: None, + end_column: None, + can_restart: None, + instruction_pointer_reference: None, + module_id: None, + presentation_hint: None, + }, + ]; + + client + .on_request::({ + let stack_frames = Arc::new(stack_frames.clone()); + move |_, args| { + assert_eq!(1, args.thread_id); + + Ok(dap::StackTraceResponse { + stack_frames: (*stack_frames).clone(), + total_frames: None, + }) + } + }) + .await; + + let frame_1_scopes = vec![Scope { + name: "Frame 1 Scope 1".into(), + presentation_hint: None, + variables_reference: 2, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }]; + + client + .on_request::({ + let frame_1_scopes = Arc::new(frame_1_scopes.clone()); + move |_, args| { + assert_eq!(1, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*frame_1_scopes).clone(), + }) + } + }) + .await; + + let frame_1_variables = vec![ + Variable { + name: "variable1".into(), + value: "value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "variable2".into(), + value: "value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + client + .on_request::({ + let frame_1_variables = Arc::new(frame_1_variables.clone()); + move |_, args| { + assert_eq!(2, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*frame_1_variables).clone(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let variable_list = debug_panel_item.variable_list().read(cx); + + assert_eq!(1, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + assert_eq!( + frame_1_variables + .clone() + .into_iter() + .map(|variable| VariableContainer { + container_reference: 2, + variable, + depth: 1 + }) + .collect::>(), + variable_list.variables_by_stack_frame_id(1) + ); + assert!(variable_list.variables_by_stack_frame_id(2).is_empty()); + }); + + // add handlers for fetching the second stack frame's scopes and variables + // after the user clicked the stack frame + + let frame_2_scopes = vec![Scope { + name: "Frame 2 Scope 1".into(), + presentation_hint: None, + variables_reference: 3, + named_variables: None, + indexed_variables: None, + expensive: false, + source: None, + line: None, + column: None, + end_line: None, + end_column: None, + }]; + + client + .on_request::({ + let frame_2_scopes = Arc::new(frame_2_scopes.clone()); + move |_, args| { + assert_eq!(2, args.frame_id); + + Ok(dap::ScopesResponse { + scopes: (*frame_2_scopes).clone(), + }) + } + }) + .await; + + let frame_2_variables = vec![ + Variable { + name: "variable3".into(), + value: "old value 1".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + Variable { + name: "variable4".into(), + value: "old value 2".into(), + type_: None, + presentation_hint: None, + evaluate_name: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + ]; + + client + .on_request::({ + let frame_2_variables = Arc::new(frame_2_variables.clone()); + move |_, args| { + assert_eq!(3, args.variables_reference); + + Ok(dap::VariablesResponse { + variables: (*frame_2_variables).clone(), + }) + } + }) + .await; + + active_debug_panel_item(workspace, cx) + .update_in(cx, |debug_panel_item, window, cx| { + debug_panel_item + .stack_frame_list() + .update(cx, |stack_frame_list, cx| { + stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx) + }) + }) + .await + .unwrap(); + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { + let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let variable_list = debug_panel_item.variable_list().read(cx); + + assert_eq!(2, stack_frame_list.current_stack_frame_id()); + assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + + assert_eq!( + frame_1_variables + .into_iter() + .map(|variable| VariableContainer { + container_reference: 2, + variable, + depth: 1 + }) + .collect::>(), + variable_list.variables_by_stack_frame_id(1) + ); + assert_eq!( + frame_2_variables + .into_iter() + .map(|variable| VariableContainer { + container_reference: 3, + variable, + depth: 1 + }) + .collect::>(), + variable_list.variables_by_stack_frame_id(2) + ); + }); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index d33fe62c9dfd6a..1798be2729c26f 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,4 +1,4 @@ -use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; +use crate::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, @@ -131,7 +131,7 @@ pub enum OpenEntry { name: String, }, Variable { - scope_id: u64, + scope_name: String, name: String, depth: usize, }, @@ -146,7 +146,7 @@ impl OpenEntry { proto::variable_list_open_entry::Entry::Variable(state) => Some(Self::Variable { name: state.name.clone(), depth: state.depth as usize, - scope_id: state.scope_id, + scope_name: state.scope_name.clone(), }), } } @@ -161,12 +161,12 @@ impl OpenEntry { OpenEntry::Variable { name, depth, - scope_id, + scope_name, } => { proto::variable_list_open_entry::Entry::Variable(proto::DebuggerOpenEntryVariable { name: name.clone(), depth: *depth as u64, - scope_id: *scope_id, + scope_name: scope_name.clone(), }) } }; @@ -324,7 +324,6 @@ impl ScopeVariableIndex { } } -type StackFrameId = u64; type ScopeId = u64; pub struct VariableList { @@ -470,7 +469,7 @@ impl VariableList { self.add_variables(variables.clone()); } - self.build_entries(true, true, cx); + self.build_entries(true, cx); cx.notify(); } @@ -511,15 +510,53 @@ impl VariableList { cx: &mut Context, ) { match event { - StackFrameListEvent::SelectedStackFrameChanged => { - self.build_entries(true, true, cx); + StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => { + self.handle_selected_stack_frame_changed(*stack_frame_id, cx); } StackFrameListEvent::StackFramesUpdated => { - self.fetch_variables(cx); + self.entries.clear(); + self.variables.clear(); + self.scopes.clear(); } } } + fn handle_selected_stack_frame_changed( + &mut self, + stack_frame_id: StackFrameId, + cx: &mut Context, + ) { + if self.scopes.contains_key(&stack_frame_id) { + return self.build_entries(true, cx); + } + + self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { + let task = this.update(&mut cx, |variable_list, cx| { + variable_list.fetch_variables_for_stack_frame(stack_frame_id, cx) + })?; + + let (scopes, variables) = task.await?; + + this.update(&mut cx, |variable_list, cx| { + variable_list.scopes.insert(stack_frame_id, scopes); + + for (scope_id, variables) in variables.into_iter() { + let mut variable_index = ScopeVariableIndex::new(); + variable_index.add_variables(scope_id, variables); + + variable_list + .variables + .insert((stack_frame_id, scope_id), variable_index); + } + + variable_list.build_entries(true, cx); + variable_list.send_update_proto_message(cx); + + variable_list.fetch_variables_task.take(); + }) + })); + } + #[cfg(any(test, feature = "test-support"))] pub fn scopes(&self) -> &HashMap> { &self.scopes @@ -599,14 +636,15 @@ impl VariableList { } } - fn toggle_variable( + pub fn toggle_variable( &mut self, - scope_id: u64, + scope: &Scope, variable: &Variable, depth: usize, cx: &mut Context, ) { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + let scope_id = scope.variables_reference; let Some(variable_index) = self.variables_by_scope(stack_frame_id, scope_id) else { return; @@ -614,8 +652,8 @@ impl VariableList { let entry_id = OpenEntry::Variable { depth, - scope_id, name: variable.name.clone(), + scope_name: scope.name.clone(), }; let has_children = variable.variables_reference > 0; @@ -678,25 +716,16 @@ impl VariableList { } }; - self.build_entries(false, true, cx); + self.build_entries(false, cx); } - pub fn build_entries( - &mut self, - open_first_scope: bool, - keep_open_entries: bool, - cx: &mut Context, - ) { + pub fn build_entries(&mut self, open_first_scope: bool, cx: &mut Context) { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); let Some(scopes) = self.scopes.get(&stack_frame_id) else { return; }; - if !keep_open_entries { - self.open_entries.clear(); - } - let mut entries: Vec = Vec::default(); for scope in scopes { let Some(index) = self @@ -753,7 +782,7 @@ impl VariableList { .binary_search(&OpenEntry::Variable { depth, name: variable.name.clone(), - scope_id: scope.variables_reference, + scope_name: scope.name.clone(), }) .is_err() { @@ -832,6 +861,7 @@ impl VariableList { fn fetch_nested_variables( &self, + scope: &Scope, container_reference: u64, depth: usize, open_entries: &Vec, @@ -840,14 +870,13 @@ impl VariableList { let stack_frame_list = self.stack_frame_list.read(cx); let thread_id = stack_frame_list.thread_id(); let stack_frame_id = stack_frame_list.current_stack_frame_id(); - let scope_id = container_reference; let variables_task = self.dap_store.update(cx, |store, cx| { store.variables( &self.client_id, thread_id, stack_frame_id, - scope_id, + scope.variables_reference, self.session_id, container_reference, cx, @@ -855,6 +884,7 @@ impl VariableList { }); cx.spawn({ + let scope = scope.clone(); let open_entries = open_entries.clone(); |this, mut cx| async move { let mut variables = Vec::new(); @@ -869,13 +899,14 @@ impl VariableList { if open_entries .binary_search(&OpenEntry::Variable { depth, - scope_id: container_reference, - name: variable.name.clone(), + name: variable.name, + scope_name: scope.name.clone(), }) .is_ok() { let task = this.update(&mut cx, |this, cx| { this.fetch_nested_variables( + &scope, variable.variables_reference, depth + 1, &open_entries, @@ -895,7 +926,6 @@ impl VariableList { fn fetch_variables_for_stack_frame( &self, stack_frame_id: u64, - open_entries: &Vec, cx: &mut Context, ) -> Task, HashMap>)>> { let scopes_task = self.dap_store.update(cx, |store, cx| { @@ -903,15 +933,29 @@ impl VariableList { }); cx.spawn({ - let open_entries = open_entries.clone(); |this, mut cx| async move { let mut variables = HashMap::new(); let scopes = scopes_task.await?; + let open_entries = this.read_with(&cx, |variable_list, _| { + variable_list + .open_entries + .iter() + .filter(|entry| matches!(entry, OpenEntry::Variable { .. })) + .cloned() + .collect::>() + })?; + for scope in scopes.iter() { let variables_task = this.update(&mut cx, |this, cx| { - this.fetch_nested_variables(scope.variables_reference, 1, &open_entries, cx) + this.fetch_nested_variables( + scope, + scope.variables_reference, + 1, + &open_entries, + cx, + ) })?; variables.insert(scope.variables_reference, variables_task.await?); @@ -922,77 +966,20 @@ impl VariableList { }) } - fn fetch_variables(&mut self, cx: &mut Context) { - if self.dap_store.read(cx).upstream_client().is_some() { - return; - } - - let stack_frames = self.stack_frame_list.read(cx).stack_frames().clone(); - - self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { - let mut tasks = Vec::with_capacity(stack_frames.len()); - - let open_entries = this.update(&mut cx, |this, _| { - this.open_entries - .iter() - .filter(|e| matches!(e, OpenEntry::Variable { .. })) - .cloned() - .collect::>() - })?; - - for stack_frame in stack_frames.clone().into_iter() { - let task = this.update(&mut cx, |this, cx| { - this.fetch_variables_for_stack_frame(stack_frame.id, &open_entries, cx) - }); - - tasks.push( - cx.background_executor() - .spawn(async move { anyhow::Ok((stack_frame.id, task?.await?)) }), - ); - } - - let results = futures::future::join_all(tasks).await; - - this.update(&mut cx, |this, cx| { - let mut new_variables = BTreeMap::new(); - let mut new_scopes = HashMap::new(); - - for (stack_frame_id, (scopes, variables)) in - results.into_iter().filter_map(|result| result.ok()) - { - new_scopes.insert(stack_frame_id, scopes); - - for (scope_id, variables) in variables.into_iter() { - let mut variable_index = ScopeVariableIndex::new(); - variable_index.add_variables(scope_id, variables); - - new_variables.insert((stack_frame_id, scope_id), variable_index); - } - } - - std::mem::swap(&mut this.variables, &mut new_variables); - std::mem::swap(&mut this.scopes, &mut new_scopes); - - this.entries.clear(); - this.build_entries(true, true, cx); - - if let Some((client, project_id)) = this.dap_store.read(cx).downstream_client() { - let request = UpdateDebugAdapter { - client_id: this.client_id.to_proto(), - session_id: this.session_id.to_proto(), - thread_id: Some(this.stack_frame_list.read(cx).thread_id()), - project_id: *project_id, - variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( - this.to_proto(), - )), - }; - - client.send(request).log_err(); - }; + fn send_update_proto_message(&self, cx: &mut Context) { + if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { + let request = UpdateDebugAdapter { + client_id: self.client_id.to_proto(), + session_id: self.session_id.to_proto(), + thread_id: Some(self.stack_frame_list.read(cx).thread_id()), + project_id: *project_id, + variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( + self.to_proto(), + )), + }; - this.fetch_variables_task.take(); - }) - })); + client.send(request).log_err(); + }; } fn deploy_variable_context_menu( @@ -1095,7 +1082,7 @@ impl VariableList { window.focus(&editor.focus_handle(cx)) }); - this.build_entries(false, true, cx); + this.build_entries(false, cx); }), ) }) @@ -1124,7 +1111,7 @@ impl VariableList { return; }; - self.build_entries(false, true, cx); + self.build_entries(false, cx); } fn set_variable_value(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { @@ -1162,7 +1149,7 @@ impl VariableList { set_value_task.await?; this.update_in(&mut cx, |this, window, cx| { - this.build_entries(false, true, cx); + this.build_entries(false, cx); this.invalidate(window, cx); }) }) @@ -1170,6 +1157,10 @@ impl VariableList { } pub fn invalidate(&mut self, window: &mut Window, cx: &mut Context) { + self.variables.clear(); + self.scopes.clear(); + self.entries.clear(); + self.stack_frame_list.update(cx, |stack_frame_list, cx| { stack_frame_list.invalidate(window, cx); }); @@ -1247,18 +1238,13 @@ impl VariableList { let entry_id = &OpenEntry::Variable { depth: *depth, name: variable.name.clone(), - scope_id: scope.variables_reference, + scope_name: scope.name.clone(), }; if self.open_entries.binary_search(entry_id).is_err() { self.select_prev(&SelectPrev, window, cx); } else { - self.toggle_variable( - scope.variables_reference, - &variable.clone(), - *depth, - cx, - ); + self.toggle_variable(&scope.clone(), &variable.clone(), *depth, cx); } } VariableListEntry::SetVariableEditor { .. } => {} @@ -1294,18 +1280,13 @@ impl VariableList { let entry_id = &OpenEntry::Variable { depth: *depth, name: variable.name.clone(), - scope_id: scope.variables_reference, + scope_name: scope.name.clone(), }; if self.open_entries.binary_search(entry_id).is_ok() { self.select_next(&SelectNext, window, cx); } else { - self.toggle_variable( - scope.variables_reference, - &variable.clone(), - *depth, - cx, - ); + self.toggle_variable(&scope.clone(), &variable.clone(), *depth, cx); } } VariableListEntry::SetVariableEditor { .. } => {} @@ -1332,17 +1313,6 @@ impl VariableList { .into_any_element() } - #[cfg(any(test, feature = "test-support"))] - pub fn toggle_variable_in_test( - &mut self, - scope_id: u64, - variable: &Variable, - depth: usize, - cx: &mut Context, - ) { - self.toggle_variable(scope_id, variable, depth, cx); - } - #[track_caller] #[cfg(any(test, feature = "test-support"))] pub fn assert_visual_entries(&self, expected: Vec<&str>, cx: &Context) { @@ -1390,7 +1360,7 @@ impl VariableList { .binary_search(&OpenEntry::Variable { depth: *depth, name: variable.name.clone(), - scope_id: scope.variables_reference, + scope_name: scope.name.clone(), }) .is_ok(); @@ -1419,11 +1389,10 @@ impl VariableList { is_selected: bool, cx: &mut Context, ) -> AnyElement { - let scope_id = scope.variables_reference; let entry_id = OpenEntry::Variable { depth, - scope_id, name: variable.name.clone(), + scope_name: scope.name.clone(), }; let disclosed = has_children.then(|| self.open_entries.binary_search(&entry_id).is_ok()); @@ -1477,9 +1446,10 @@ impl VariableList { .toggle(disclosed) .when(has_children, |list_item| { list_item.on_toggle(cx.listener({ + let scope = scope.clone(); let variable = variable.clone(); move |this, _, _window, cx| { - this.toggle_variable(scope_id, &variable, depth, cx) + this.toggle_variable(&scope, &variable, depth, cx) } })) }) diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs index 8cd22873c4ee02..3ee50cc2ea3816 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/dap_command.rs @@ -828,7 +828,7 @@ impl DapCommand for VariablesCommand { ) -> Result { let variables = response?; - dap_store.update(cx, |this, cx| { + dap_store.update(cx, |this, _| { if let Some((downstream_clients, project_id)) = this.downstream_client() { let update = proto::UpdateDebugAdapter { project_id: *project_id, @@ -846,7 +846,7 @@ impl DapCommand for VariablesCommand { }; downstream_clients.send(update.clone()).log_err(); - cx.emit(crate::dap_store::DapStoreEvent::UpdateDebugAdapter(update)); + // cx.emit(crate::dap_store::DapStoreEvent::UpdateDebugAdapter(update)); } })?; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index ad348b12223918..dcaff99a5ded98 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2617,7 +2617,7 @@ message DebuggerOpenEntryScope { } message DebuggerOpenEntryVariable { - uint64 scope_id = 1; + string scope_name = 1; string name = 2; uint64 depth = 3; } From 1be2836f60176fbdde27d0b26eeb2b0c19f7a17c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 5 Feb 2025 19:19:23 +0100 Subject: [PATCH 514/650] Refactor session id to use entity instead (#109) * Move common session fields to struct * Use session entity instead of session id inside UI This is to prepare to move the state to the --- crates/collab/src/tests/debug_panel_tests.rs | 5 +- crates/dap/src/session.rs | 62 +++++------- crates/debugger_ui/src/console.rs | 18 ++-- crates/debugger_ui/src/debugger_panel.rs | 41 ++++---- crates/debugger_ui/src/debugger_panel_item.rs | 97 +++++++++---------- crates/debugger_ui/src/module_list.rs | 10 +- crates/debugger_ui/src/stack_frame_list.rs | 18 ++-- crates/debugger_ui/src/variable_list.rs | 26 ++--- crates/project/src/dap_store.rs | 7 +- 9 files changed, 138 insertions(+), 146 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index c4aea60da88b9b..8ab704be4cdc1a 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1960,7 +1960,10 @@ async fn test_ignore_breakpoints( let breakpoints_ignored = active_debug_panel_item.read(cx).are_breakpoints_ignored(cx); - assert_eq!(session_id, active_debug_panel_item.read(cx).session_id()); + assert_eq!( + session_id, + active_debug_panel_item.read(cx).session().read(cx).id() + ); assert_eq!(false, breakpoints_ignored); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs index ce7f60a1fc8960..eda6129cb86a35 100644 --- a/crates/dap/src/session.rs +++ b/crates/dap/src/session.rs @@ -19,14 +19,18 @@ impl DebugSessionId { } } -pub enum DebugSession { +pub struct DebugSession { + id: DebugSessionId, + mode: DebugSessionMode, + ignore_breakpoints: bool, +} + +pub enum DebugSessionMode { Local(LocalDebugSession), Remote(RemoteDebugSession), } pub struct LocalDebugSession { - id: DebugSessionId, - ignore_breakpoints: bool, configuration: DebugAdapterConfig, clients: HashMap>, } @@ -80,77 +84,63 @@ impl LocalDebugSession { pub fn client_ids(&self) -> impl Iterator + '_ { self.clients.keys().cloned() } - - pub fn id(&self) -> DebugSessionId { - self.id - } } pub struct RemoteDebugSession { - id: DebugSessionId, - ignore_breakpoints: bool, label: String, } impl DebugSession { pub fn new_local(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { - Self::Local(LocalDebugSession { + Self { id, ignore_breakpoints: false, - configuration, - clients: HashMap::default(), - }) + mode: DebugSessionMode::Local(LocalDebugSession { + configuration, + clients: HashMap::default(), + }), + } } pub fn as_local(&self) -> Option<&LocalDebugSession> { - match self { - DebugSession::Local(local) => Some(local), + match &self.mode { + DebugSessionMode::Local(local) => Some(local), _ => None, } } pub fn as_local_mut(&mut self) -> Option<&mut LocalDebugSession> { - match self { - DebugSession::Local(local) => Some(local), + match &mut self.mode { + DebugSessionMode::Local(local) => Some(local), _ => None, } } pub fn new_remote(id: DebugSessionId, label: String, ignore_breakpoints: bool) -> Self { - Self::Remote(RemoteDebugSession { + Self { id, - label: label.clone(), ignore_breakpoints, - }) + mode: DebugSessionMode::Remote(RemoteDebugSession { label }), + } } pub fn id(&self) -> DebugSessionId { - match self { - DebugSession::Local(local) => local.id, - DebugSession::Remote(remote) => remote.id, - } + self.id } pub fn name(&self) -> String { - match self { - DebugSession::Local(local) => local.configuration.label.clone(), - DebugSession::Remote(remote) => remote.label.clone(), + match &self.mode { + DebugSessionMode::Local(local) => local.configuration.label.clone(), + DebugSessionMode::Remote(remote) => remote.label.clone(), } } pub fn ignore_breakpoints(&self) -> bool { - match self { - DebugSession::Local(local) => local.ignore_breakpoints, - DebugSession::Remote(remote) => remote.ignore_breakpoints, - } + self.ignore_breakpoints } pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut Context) { - match self { - DebugSession::Local(local) => local.ignore_breakpoints = ignore, - DebugSession::Remote(remote) => remote.ignore_breakpoints = ignore, - } - + self.ignore_breakpoints = ignore; cx.notify(); } } diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 817bfecfa8dd56..1eb05867c51cd4 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -3,7 +3,7 @@ use crate::{ variable_list::VariableList, }; use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, + client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSession, OutputEvent, OutputEventGroup, }; use editor::{ @@ -34,8 +34,8 @@ pub struct Console { groups: Vec, console: Entity, query_bar: Entity, - session_id: DebugSessionId, dap_store: Entity, + session: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, variable_list: Entity, @@ -44,11 +44,11 @@ pub struct Console { impl Console { pub fn new( - stack_frame_list: &Entity, - session_id: &DebugSessionId, + session: Entity, client_id: &DebugAdapterClientId, - variable_list: Entity, dap_store: Entity, + stack_frame_list: Entity, + variable_list: Entity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -84,18 +84,18 @@ impl Console { }); let _subscriptions = - vec![cx.subscribe(stack_frame_list, Self::handle_stack_frame_list_events)]; + vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)]; Self { + session, console, dap_store, query_bar, variable_list, _subscriptions, + stack_frame_list, client_id: *client_id, groups: Vec::default(), - session_id: *session_id, - stack_frame_list: stack_frame_list.clone(), } } @@ -132,7 +132,7 @@ impl Console { project_id: *project_id, client_id: self.client_id.to_proto(), thread_id: None, - session_id: self.session_id.to_proto(), + session_id: self.session.read(cx).id().to_proto(), variant: Some(proto::update_debug_adapter::Variant::OutputEvent( event.to_proto(), )), diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 02a8318a7d4322..e937635d452984 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -346,7 +346,7 @@ impl DebugPanel { let thread_panel = item.downcast::().unwrap(); let thread_id = thread_panel.read(cx).thread_id(); - let session_id = thread_panel.read(cx).session_id(); + let session_id = thread_panel.read(cx).session().read(cx).id(); let client_id = thread_panel.read(cx).client_id(); self.thread_states.remove(&(client_id, thread_id)); @@ -758,20 +758,17 @@ impl DebugPanel { return; }; - let Some(session_name) = self + let Some(session) = self .dap_store .read(cx) .session_by_id(session_id) - .map(|session| session.read(cx).name()) + .map(|session| session) else { - return; // this can never happen + return; // this can/should never happen }; - let session_id = *session_id; let client_id = *client_id; - let session_name = SharedString::from(session_name); - cx.spawn_in(window, { let event = event.clone(); |this, mut cx| async move { @@ -789,18 +786,17 @@ impl DebugPanel { let existing_item = this.debug_panel_item_by_client(&client_id, thread_id, cx); if existing_item.is_none() { - let debug_panel = cx.entity().clone(); + let debug_panel = cx.entity(); this.pane.update(cx, |pane, cx| { let tab = cx.new(|cx| { DebugPanelItem::new( - debug_panel, - this.workspace.clone(), - this.dap_store.clone(), - thread_state.clone(), - &session_id, + session, &client_id, - session_name, thread_id, + thread_state, + this.dap_store.clone(), + &debug_panel, + this.workspace.clone(), window, cx, ) @@ -1025,6 +1021,10 @@ impl DebugPanel { cx: &mut Context, ) { let session_id = DebugSessionId::from_proto(payload.session_id); + let Some(session) = self.dap_store.read(cx).session_by_id(&session_id) else { + return; + }; + let client_id = DebugAdapterClientId::from_proto(payload.client_id); let thread_id = payload.thread_id; let thread_state = payload.thread_state.clone().unwrap(); @@ -1045,18 +1045,17 @@ impl DebugPanel { self.thread_states .insert((client_id, thread_id), thread_state.clone()); - let debug_panel = cx.entity().clone(); + let debug_panel = cx.entity(); let debug_panel_item = self.pane.update(cx, |pane, cx| { let debug_panel_item = cx.new(|cx| { DebugPanelItem::new( - debug_panel, - self.workspace.clone(), - self.dap_store.clone(), - thread_state, - &session_id, + session, &client_id, - payload.session_name.clone().into(), thread_id, + thread_state, + self.dap_store.clone(), + &debug_panel, + self.workspace.clone(), window, cx, ) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 51f1fca4b43957..e25fd62fe1d353 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -6,7 +6,7 @@ use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; use dap::proto_conversions::{self, ProtoConversion}; -use dap::session::DebugSessionId; +use dap::session::DebugSession; use dap::{ client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, @@ -65,9 +65,8 @@ pub struct DebugPanelItem { console: Entity, focus_handle: FocusHandle, remote_id: Option, - session_name: SharedString, dap_store: Entity, - session_id: DebugSessionId, + session: Entity, show_console_indicator: bool, module_list: Entity, active_thread_item: ThreadItem, @@ -83,14 +82,13 @@ pub struct DebugPanelItem { impl DebugPanelItem { #[allow(clippy::too_many_arguments)] pub fn new( - debug_panel: Entity, - workspace: WeakEntity, - dap_store: Entity, - thread_state: Entity, - session_id: &DebugSessionId, + session: Entity, client_id: &DebugAdapterClientId, - session_name: SharedString, thread_id: u64, + thread_state: Entity, + dap_store: Entity, + debug_panel: &Entity, + workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -100,34 +98,41 @@ impl DebugPanelItem { let stack_frame_list = cx.new(|cx| { StackFrameList::new( - &workspace, &this, &dap_store, client_id, session_id, thread_id, window, cx, + workspace.clone(), + &this, + dap_store.clone(), + session.clone(), + client_id, + thread_id, + window, + cx, ) }); let variable_list = cx.new(|cx| { VariableList::new( - &stack_frame_list, + session.clone(), + client_id, dap_store.clone(), - &client_id, - session_id, + stack_frame_list.clone(), window, cx, ) }); let module_list = - cx.new(|cx| ModuleList::new(dap_store.clone(), &client_id, &session_id, cx)); + cx.new(|cx| ModuleList::new(dap_store.clone(), session.clone(), &client_id, cx)); let loaded_source_list = cx.new(|cx| LoadedSourceList::new(&this, dap_store.clone(), &client_id, cx)); let console = cx.new(|cx| { Console::new( - &stack_frame_list, - session_id, + session.clone(), client_id, - variable_list.clone(), dap_store.clone(), + stack_frame_list.clone(), + variable_list.clone(), window, cx, ) @@ -136,7 +141,7 @@ impl DebugPanelItem { cx.observe(&module_list, |_, _, cx| cx.notify()).detach(); let _subscriptions = vec![ - cx.subscribe_in(&debug_panel, window, { + cx.subscribe_in(debug_panel, window, { move |this: &mut Self, _, event: &DebugPanelEvent, window, cx| { match event { DebugPanelEvent::Stopped { @@ -182,11 +187,11 @@ impl DebugPanelItem { ]; Self { + session, console, thread_id, dap_store, workspace, - session_name, module_list, thread_state, focus_handle, @@ -196,7 +201,6 @@ impl DebugPanelItem { stack_frame_list, loaded_source_list, client_id: *client_id, - session_id: *session_id, show_console_indicator: false, active_thread_item: ThreadItem::Variables, } @@ -209,7 +213,7 @@ impl DebugPanelItem { SetDebuggerPanelItem { project_id, - session_id: self.session_id.to_proto(), + session_id: self.session.read(cx).id().to_proto(), client_id: self.client_id.to_proto(), thread_id: self.thread_id, console: None, @@ -219,7 +223,7 @@ impl DebugPanelItem { variable_list, stack_frame_list, loaded_source_list: None, - session_name: self.session_name.to_string(), + session_name: self.session.read(cx).name(), } } @@ -434,7 +438,7 @@ impl DebugPanelItem { let message = proto_conversions::capabilities_to_proto( &self.dap_store.read(cx).capabilities_by_id(client_id), *project_id, - self.session_id.to_proto(), + self.session.read(cx).id().to_proto(), self.client_id.to_proto(), ); @@ -480,8 +484,8 @@ impl DebugPanelItem { } } - pub fn session_id(&self) -> DebugSessionId { - self.session_id + pub fn session(&self) -> &Entity { + &self.session } pub fn client_id(&self) -> DebugAdapterClientId { @@ -519,8 +523,7 @@ impl DebugPanelItem { #[cfg(any(test, feature = "test-support"))] pub fn are_breakpoints_ignored(&self, cx: &App) -> bool { - self.dap_store - .read_with(cx, |dap, cx| dap.ignore_breakpoints(&self.session_id, cx)) + self.session.read(cx).ignore_breakpoints() } pub fn capabilities(&self, cx: &mut Context) -> Capabilities { @@ -711,7 +714,7 @@ impl DebugPanelItem { self.dap_store.update(cx, |store, cx| { store .terminate_threads( - &self.session_id, + &self.session.read(cx).id(), &self.client_id, Some(vec![self.thread_id; 1]), cx, @@ -729,15 +732,9 @@ impl DebugPanelItem { } pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context) { - self.workspace - .update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project - .toggle_ignore_breakpoints(&self.session_id, &self.client_id, cx) - .detach_and_log_err(cx); - }) - }) - .ok(); + self.session.update(cx, |session, cx| { + session.set_ignore_breakpoints(!session.ignore_breakpoints(), cx); + }); } } @@ -756,21 +753,25 @@ impl Item for DebugPanelItem { &self, params: workspace::item::TabContentParams, _window: &Window, - _: &App, + cx: &App, ) -> AnyElement { - Label::new(format!("{} - Thread {}", self.session_name, self.thread_id)) - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() + Label::new(format!( + "{} - Thread {}", + self.session.read(cx).name(), + self.thread_id + )) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } fn tab_tooltip_text(&self, cx: &App) -> Option { Some(SharedString::from(format!( "{} Thread {} - {:?}", - self.session_name, + self.session.read(cx).name(), self.thread_id, self.thread_state.read(cx).status, ))) @@ -992,9 +993,7 @@ impl Render for DebugPanelItem { .child( IconButton::new( "debug-ignore-breakpoints", - if self.dap_store.update(cx, |store, cx| { - store.ignore_breakpoints(&self.session_id, cx) - }) { + if self.session.read(cx).ignore_breakpoints() { IconName::DebugIgnoreBreakpoints } else { IconName::DebugBreakpoint diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index eb165e5211f266..13d21189cf0182 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,6 +1,6 @@ use anyhow::Result; use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, + client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSession, Module, ModuleEvent, }; use gpui::{list, AnyElement, Entity, FocusHandle, Focusable, ListState, Task}; @@ -14,15 +14,15 @@ pub struct ModuleList { modules: Vec, focus_handle: FocusHandle, dap_store: Entity, + session: Entity, client_id: DebugAdapterClientId, - session_id: DebugSessionId, } impl ModuleList { pub fn new( dap_store: Entity, + session: Entity, client_id: &DebugAdapterClientId, - session_id: &DebugSessionId, cx: &mut Context, ) -> Self { let weak_entity = cx.weak_entity(); @@ -42,10 +42,10 @@ impl ModuleList { let this = Self { list, + session, dap_store, focus_handle, client_id: *client_id, - session_id: *session_id, modules: Vec::default(), }; @@ -128,7 +128,7 @@ impl ModuleList { fn propagate_updates(&self, cx: &Context) { if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { let request = UpdateDebugAdapter { - session_id: self.session_id.to_proto(), + session_id: self.session.read(cx).id().to_proto(), client_id: self.client_id.to_proto(), project_id: *id, thread_id: None, diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 24c3fdbd832254..07949e3af64254 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -3,7 +3,7 @@ use std::path::Path; use anyhow::{anyhow, Result}; use dap::client::DebugAdapterClientId; use dap::proto_conversions::ProtoConversion; -use dap::session::DebugSessionId; +use dap::session::DebugSession; use dap::StackFrame; use gpui::{ list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, @@ -31,8 +31,8 @@ pub struct StackFrameList { thread_id: u64, list: ListState, focus_handle: FocusHandle, - session_id: DebugSessionId, dap_store: Entity, + session: Entity, stack_frames: Vec, entries: Vec, workspace: WeakEntity, @@ -51,11 +51,11 @@ pub enum StackFrameEntry { impl StackFrameList { #[allow(clippy::too_many_arguments)] pub fn new( - workspace: &WeakEntity, + workspace: WeakEntity, debug_panel_item: &Entity, - dap_store: &Entity, + dap_store: Entity, + session: Entity, client_id: &DebugAdapterClientId, - session_id: &DebugSessionId, thread_id: u64, window: &Window, cx: &mut Context, @@ -85,14 +85,14 @@ impl StackFrameList { Self { list, + session, + workspace, + dap_store, thread_id, focus_handle, _subscriptions, client_id: *client_id, - session_id: *session_id, entries: Default::default(), - workspace: workspace.clone(), - dap_store: dap_store.clone(), fetch_stack_frames_task: None, stack_frames: Default::default(), current_stack_frame_id: Default::default(), @@ -254,7 +254,7 @@ impl StackFrameList { if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { let request = UpdateDebugAdapter { client_id: self.client_id.to_proto(), - session_id: self.session_id.to_proto(), + session_id: self.session.read(cx).id().to_proto(), project_id: *id, thread_id: Some(self.thread_id), variant: Some(rpc::proto::update_debug_adapter::Variant::StackFrameList( diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 1798be2729c26f..5171fae947902d 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,8 +1,8 @@ use crate::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSessionId, - Scope, ScopePresentationHint, Variable, + client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSession, Scope, + ScopePresentationHint, Variable, }; use editor::{actions::SelectAll, Editor, EditorEvent}; use gpui::{ @@ -330,11 +330,11 @@ pub struct VariableList { list: ListState, focus_handle: FocusHandle, dap_store: Entity, - session_id: DebugSessionId, open_entries: Vec, + session: Entity, client_id: DebugAdapterClientId, - set_variable_editor: Entity, _subscriptions: Vec, + set_variable_editor: Entity, selection: Option, stack_frame_list: Entity, scopes: HashMap>, @@ -347,10 +347,10 @@ pub struct VariableList { impl VariableList { pub fn new( - stack_frame_list: &Entity, - dap_store: Entity, + session: Entity, client_id: &DebugAdapterClientId, - session_id: &DebugSessionId, + dap_store: Entity, + stack_frame_list: Entity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -382,17 +382,18 @@ impl VariableList { .detach(); let _subscriptions = - vec![cx.subscribe(stack_frame_list, Self::handle_stack_frame_list_events)]; + vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)]; Self { list, + session, dap_store, focus_handle, _subscriptions, selection: None, + stack_frame_list, set_variable_editor, client_id: *client_id, - session_id: *session_id, open_context_menu: None, set_variable_state: None, fetch_variables_task: None, @@ -400,7 +401,6 @@ impl VariableList { entries: Default::default(), variables: Default::default(), open_entries: Default::default(), - stack_frame_list: stack_frame_list.clone(), } } @@ -672,7 +672,7 @@ impl VariableList { thread_id, stack_frame_id, scope_id, - self.session_id, + self.session.read(cx).id(), variable.variables_reference, cx, ) @@ -877,7 +877,7 @@ impl VariableList { thread_id, stack_frame_id, scope.variables_reference, - self.session_id, + self.session.read(cx).id(), container_reference, cx, ) @@ -970,7 +970,7 @@ impl VariableList { if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { let request = UpdateDebugAdapter { client_id: self.client_id.to_proto(), - session_id: self.session_id.to_proto(), + session_id: self.session.read(cx).id().to_proto(), thread_id: Some(self.stack_frame_list.read(cx).thread_id()), project_id: *project_id, variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 04c1c2ac3bf9c2..517fe8cea3efd4 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -2090,10 +2090,11 @@ impl DapStore { for session in local_store .sessions .values() - .filter_map(|session| session.read(cx).as_local()) + .filter(|session| session.read(cx).as_local().is_some()) { - let ignore_breakpoints = self.ignore_breakpoints(&session.id(), cx); - for client in session.clients().collect::>() { + let session = session.read(cx); + let ignore_breakpoints = session.ignore_breakpoints(); + for client in session.as_local().unwrap().clients().collect::>() { tasks.push(self.send_breakpoints( &client.id(), Arc::from(absolute_path.clone()), From 4fcb10dd2693ff2ebd895581a21df910f2f8481e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:27:57 +0100 Subject: [PATCH 515/650] lsp: Add support for default rename behavior in prepareRename request (#24246) Fixes #24184 Release Notes: - Fixed renaming not working with some language servers (e.g. hls) --- crates/lsp/src/lsp.rs | 3 +++ crates/project/src/lsp_command.rs | 37 +++++++++++++++---------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index baff4c343c13f6..edbe564b795bea 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -724,6 +724,9 @@ impl LanguageServer { }), rename: Some(RenameClientCapabilities { prepare_support: Some(true), + prepare_support_default_behavior: Some( + PrepareSupportDefaultBehavior::IDENTIFIER, + ), ..Default::default() }), hover: Some(HoverClientCapabilities { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c632f9aca16847..68f1522af52ea8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -299,28 +299,27 @@ impl LspCommand for PrepareRename { _: LanguageServerId, mut cx: AsyncApp, ) -> Result { - buffer.update(&mut cx, |buffer, _| { - match message { - Some(lsp::PrepareRenameResponse::Range(range)) - | Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }) => { - let Range { start, end } = range_from_lsp(range); - if buffer.clip_point_utf16(start, Bias::Left) == start.0 - && buffer.clip_point_utf16(end, Bias::Left) == end.0 - { - Ok(PrepareRenameResponse::Success( - buffer.anchor_after(start)..buffer.anchor_before(end), - )) - } else { - Ok(PrepareRenameResponse::InvalidPosition) - } - } - Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => { - Err(anyhow!("Invalid for language server to send a `defaultBehavior` response to `prepareRename`")) - } - None => { + buffer.update(&mut cx, |buffer, _| match message { + Some(lsp::PrepareRenameResponse::Range(range)) + | Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }) => { + let Range { start, end } = range_from_lsp(range); + if buffer.clip_point_utf16(start, Bias::Left) == start.0 + && buffer.clip_point_utf16(end, Bias::Left) == end.0 + { + Ok(PrepareRenameResponse::Success( + buffer.anchor_after(start)..buffer.anchor_before(end), + )) + } else { Ok(PrepareRenameResponse::InvalidPosition) } } + Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => { + let snapshot = buffer.snapshot(); + let (range, _) = snapshot.surrounding_word(self.position); + let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); + Ok(PrepareRenameResponse::Success(range)) + } + None => Ok(PrepareRenameResponse::InvalidPosition), })? } From 180ce5e9ba16d176b0eb93c5e0420a71fe42c365 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 5 Feb 2025 10:39:20 -0300 Subject: [PATCH 516/650] edit prediction: Allow enabling OSS data collection with no project open (#24265) This was an leftover from when we were persisting a per-project setting. Release Notes: - N/A --- crates/zeta/src/onboarding_modal.rs | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/crates/zeta/src/onboarding_modal.rs b/crates/zeta/src/onboarding_modal.rs index d2eec7c0b09c9f..b9e214508cd122 100644 --- a/crates/zeta/src/onboarding_modal.rs +++ b/crates/zeta/src/onboarding_modal.rs @@ -1,6 +1,6 @@ use std::{sync::Arc, time::Duration}; -use crate::{Zeta, ZED_PREDICT_DATA_COLLECTION_CHOICE}; +use crate::ZED_PREDICT_DATA_COLLECTION_CHOICE; use client::{Client, UserStore}; use db::kvp::KEY_VALUE_STORE; use feature_flags::FeatureFlagAppExt as _; @@ -11,10 +11,9 @@ use gpui::{ }; use language::language_settings::{AllLanguageSettings, InlineCompletionProvider}; use settings::{update_settings_file, Settings}; -use ui::{prelude::*, Checkbox, TintColor, Tooltip}; +use ui::{prelude::*, Checkbox, TintColor}; use util::ResultExt; use workspace::{notifications::NotifyTaskExt, ModalView, Workspace}; -use worktree::Worktree; /// Introduces user to Zed's Edit Prediction feature and terms of service pub struct ZedPredictModal { @@ -26,7 +25,6 @@ pub struct ZedPredictModal { terms_of_service: bool, data_collection_expanded: bool, data_collection_opted_in: bool, - worktrees: Vec>, } #[derive(PartialEq, Eq)] @@ -48,8 +46,6 @@ impl ZedPredictModal { window: &mut Window, cx: &mut Context, ) { - let worktrees = workspace.visible_worktrees(cx).collect(); - workspace.toggle_modal(window, cx, |_window, cx| Self { user_store, client, @@ -59,7 +55,6 @@ impl ZedPredictModal { terms_of_service: false, data_collection_expanded: false, data_collection_opted_in: false, - worktrees, }); } @@ -107,13 +102,6 @@ impl ZedPredictModal { .inline_completion_provider = Some(InlineCompletionProvider::Zed); }); - if this.worktrees.is_empty() { - cx.emit(DismissEvent); - return; - } - - Zeta::register(None, this.client.clone(), this.user_store.clone(), cx); - cx.emit(DismissEvent); }) }) @@ -336,17 +324,6 @@ impl Render for ZedPredictModal { ) .label("Optionally share training data (OSS-only).") .fill() - .when(self.worktrees.is_empty(), |element| { - element.disabled(true).tooltip(move |window, cx| { - Tooltip::with_meta( - "No Project Open", - None, - "Open a project to enable this option.", - window, - cx, - ) - }) - }) .on_click(cx.listener( move |this, state, _window, cx| { this.data_collection_opted_in = @@ -355,7 +332,6 @@ impl Render for ZedPredictModal { }, )), ) - // TODO: show each worktree if more than 1 .child( Button::new("learn-more", "Learn More") .icon(accordion_icons.0) From 6ec4adffe7933def6359b64140e8fab76a8f6103 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 5 Feb 2025 11:06:12 -0300 Subject: [PATCH 517/650] Accept edit predictions with `alt-tab` in addition to `tab` (#24272) When you have an edit prediction available, you can now also accept it with `alt-tab` (or `alt-enter` on Linux) even if you don't have an LSP completions menu open. This is meant to lower the mental load when going from one mode to another. Release Notes: - N/A --- assets/keymaps/default-linux.json | 12 ++++++------ assets/keymaps/default-macos.json | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 83c716bf2badd7..ac4d604feea50d 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -503,17 +503,17 @@ } }, { - "context": "Editor && inline_completion && !showing_completions", - "use_key_equivalents": true, + "context": "Editor && inline_completion", "bindings": { - "tab": "editor::AcceptInlineCompletion" + // Changing the modifier currently breaks accepting while you also an LSP completions menu open + "alt-enter": "editor::AcceptInlineCompletion" } }, { - "context": "Editor && inline_completion && showing_completions", + "context": "Editor && inline_completion && !showing_completions", + "use_key_equivalents": true, "bindings": { - // Currently, changing this binding breaks the preview behavior - "alt-enter": "editor::AcceptInlineCompletion" + "tab": "editor::AcceptInlineCompletion" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 1e649aa6b42e6c..f52625ac329c4a 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -580,17 +580,17 @@ } }, { - "context": "Editor && inline_completion && !showing_completions", - "use_key_equivalents": true, + "context": "Editor && inline_completion", "bindings": { - "tab": "editor::AcceptInlineCompletion" + // Changing the modifier currently breaks accepting while you also an LSP completions menu open + "alt-tab": "editor::AcceptInlineCompletion" } }, { - "context": "Editor && inline_completion && showing_completions", + "context": "Editor && inline_completion && !showing_completions", + "use_key_equivalents": true, "bindings": { - // Currently, changing this binding breaks the preview behavior - "alt-tab": "editor::AcceptInlineCompletion" + "tab": "editor::AcceptInlineCompletion" } }, { From b7f0a1f5c8588f85210cd2c2e1f2b2b4e42a8105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Wed, 5 Feb 2025 22:30:09 +0800 Subject: [PATCH 518/650] windows: Fix tests on Windows (#22616) Release Notes: - N/A --------- Co-authored-by: Mikayla --- .github/actions/run_tests_windows/action.yml | 26 + .github/workflows/ci.yml | 10 +- Cargo.lock | 9 + Cargo.toml | 13 +- .../src/file_command.rs | 73 ++- crates/copilot/src/copilot.rs | 5 +- .../src/copilot_completion_provider.rs | 13 +- crates/editor/src/display_map.rs | 8 +- crates/editor/src/display_map/block_map.rs | 12 +- crates/editor/src/display_map/wrap_map.rs | 6 +- crates/editor/src/editor_tests.rs | 85 +-- crates/editor/src/git/blame.rs | 20 +- crates/editor/src/hover_links.rs | 103 ++- crates/editor/src/inlay_hint_cache.rs | 71 +- crates/editor/src/items.rs | 19 +- crates/editor/src/test.rs | 21 +- .../src/extension_store_test.rs | 7 + crates/file_finder/src/file_finder_tests.rs | 123 ++-- crates/fuzzy/src/matcher.rs | 67 +- crates/git/src/blame.rs | 2 +- .../gpui/src/platform/windows/direct_write.rs | 29 +- crates/gpui/src/platform/windows/platform.rs | 17 +- crates/gpui_macros/Cargo.toml | 6 +- crates/language_tools/src/lsp_log_tests.rs | 7 +- crates/languages/src/rust.rs | 3 +- crates/prettier/src/prettier.rs | 2 +- crates/project/src/project_tests.rs | 618 ++++++++++-------- crates/project/src/task_inventory.rs | 8 +- crates/project_panel/src/project_panel.rs | 326 +++++---- crates/project_symbols/src/project_symbols.rs | 14 +- crates/recent_projects/src/recent_projects.rs | 5 +- .../refineable/derive_refineable/Cargo.toml | 6 +- .../remote_server/src/remote_editing_tests.rs | 68 +- crates/search/src/project_search.rs | 13 +- crates/semantic_index/Cargo.toml | 4 +- crates/semantic_index/src/semantic_index.rs | 6 +- crates/settings/src/settings_file.rs | 16 + crates/sqlez_macros/Cargo.toml | 2 +- crates/tab_switcher/src/tab_switcher_tests.rs | 21 +- crates/tasks_ui/src/modal.rs | 62 +- crates/tasks_ui/src/tasks_ui.rs | 34 +- crates/ui_macros/Cargo.toml | 6 +- crates/util/Cargo.toml | 4 +- crates/util/src/paths.rs | 34 +- crates/util/src/util.rs | 46 ++ crates/util_macros/Cargo.toml | 18 + crates/util_macros/LICENSE-APACHE | 1 + crates/util_macros/src/util_macros.rs | 56 ++ crates/vim/src/command.rs | 32 +- crates/vim/src/normal/paste.rs | 8 + crates/worktree/src/worktree.rs | 9 +- crates/worktree/src/worktree_tests.rs | 16 +- crates/zed/src/zed.rs | 154 +++-- crates/zed/src/zed/open_listener.rs | 27 +- script/exit-ci-if-dev-drive-is-full.ps1 | 22 + script/setup-dev-driver.ps1 | 3 +- 56 files changed, 1540 insertions(+), 856 deletions(-) create mode 100644 .github/actions/run_tests_windows/action.yml create mode 100644 crates/util_macros/Cargo.toml create mode 120000 crates/util_macros/LICENSE-APACHE create mode 100644 crates/util_macros/src/util_macros.rs create mode 100644 script/exit-ci-if-dev-drive-is-full.ps1 diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml new file mode 100644 index 00000000000000..c4be7f6d6db0a5 --- /dev/null +++ b/.github/actions/run_tests_windows/action.yml @@ -0,0 +1,26 @@ +name: "Run tests on Windows" +description: "Runs the tests on Windows" + +inputs: + working-directory: + description: "The working directory" + required: true + default: "." + +runs: + using: "composite" + steps: + - name: Install Rust + shell: pwsh + working-directory: ${{ inputs.working-directory }} + run: cargo install cargo-nextest --locked + + - name: Install Node + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 + with: + node-version: "18" + + - name: Run tests + shell: pwsh + working-directory: ${{ inputs.working-directory }} + run: cargo nextest run --workspace --no-fail-fast diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c22474d4590dc..f20495baeb423f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,7 +228,6 @@ jobs: if: always() run: rm -rf ./../.cargo - # todo(windows): Actually run the tests windows_tests: timeout-minutes: 60 name: (Windows) Run Clippy and tests @@ -269,10 +268,19 @@ jobs: # Windows can't run shell scripts, so we need to use `cargo xtask`. run: cargo xtask clippy + - name: Run tests + uses: ./.github/actions/run_tests_windows + with: + working-directory: ${{ env.ZED_WORKSPACE }} + - name: Build Zed working-directory: ${{ env.ZED_WORKSPACE }} run: cargo build + - name: Check dev drive space + working-directory: ${{ env.ZED_WORKSPACE }} + run: ./script/exit-ci-if-dev-drive-is-full.ps1 55 + # Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug. - name: Clean CI config file if: always() diff --git a/Cargo.lock b/Cargo.lock index 579d4aa9ad4248..909649188166df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14523,6 +14523,15 @@ dependencies = [ "tempfile", "tendril", "unicase", + "util_macros", +] + +[[package]] +name = "util_macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn 1.0.109", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fe455637264f85..40bb2f9f4fb21d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,6 +148,7 @@ members = [ "crates/ui_input", "crates/ui_macros", "crates/util", + "crates/util_macros", "crates/vcs_menu", "crates/vim", "crates/vim_mode_setting", @@ -347,6 +348,7 @@ ui = { path = "crates/ui" } ui_input = { path = "crates/ui_input" } ui_macros = { path = "crates/ui_macros" } util = { path = "crates/util" } +util_macros = { path = "crates/util_macros" } vcs_menu = { path = "crates/vcs_menu" } vim = { path = "crates/vim" } vim_mode_setting = { path = "crates/vim_mode_setting" } @@ -367,7 +369,7 @@ alacritty_terminal = { git = "https://github.com/alacritty/alacritty.git", rev = any_vec = "0.14" anyhow = "1.0.86" arrayvec = { version = "0.7.4", features = ["serde"] } -ashpd = { version = "0.10", default-features = false, features = ["async-std"]} +ashpd = { version = "0.10", default-features = false, features = ["async-std"] } async-compat = "0.2.1" async-compression = { version = "0.4", features = ["gzip", "futures-io"] } async-dispatcher = "0.1" @@ -429,7 +431,11 @@ jupyter-websocket-client = { version = "0.9.0" } libc = "0.2" libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" -livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="811ceae29fabee455f110c56cd66b3f49a7e5003", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false } +livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [ + "dispatcher", + "services-dispatcher", + "rustls-tls-native-roots", +], default-features = false } log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" @@ -449,11 +455,13 @@ pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git" pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1abe5cec5ebfbe97ca71746a4cfc7fe89bddf8e0" } postage = { version = "0.5", features = ["futures-traits"] } pretty_assertions = { version = "1.3.0", features = ["unstable"] } +proc-macro2 = "1.0.93" profiling = "1" prost = "0.9" prost-build = "0.9" prost-types = "0.9" pulldown-cmark = { version = "0.12.0", default-features = false } +quote = "1.0.9" rand = "0.8.5" rayon = "1.8" regex = "1.5" @@ -497,6 +505,7 @@ sqlformat = "0.2" strsim = "0.11" strum = { version = "0.26.0", features = ["derive"] } subtle = "2.5.0" +syn = { version = "1.0.72", features = ["full", "extra-traits"] } sys-locale = "0.3.1" sysinfo = "0.31.0" take-until = "0.2.0" diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index d898d82bc3f538..71a73768459865 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -323,7 +323,14 @@ fn collect_files( )))?; directory_stack.push(entry.path.clone()); } else { - let entry_name = format!("{}/{}", prefix_paths, &filename); + // todo(windows) + // Potential bug: this assumes that the path separator is always `\` on Windows + let entry_name = format!( + "{}{}{}", + prefix_paths, + std::path::MAIN_SEPARATOR_STR, + &filename + ); events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection { icon: IconName::Folder, label: entry_name.clone().into(), @@ -455,6 +462,7 @@ mod custom_path_matcher { use std::{fmt::Debug as _, path::Path}; use globset::{Glob, GlobSet, GlobSetBuilder}; + use util::paths::SanitizedPath; #[derive(Clone, Debug, Default)] pub struct PathMatcher { @@ -481,7 +489,7 @@ mod custom_path_matcher { pub fn new(globs: &[String]) -> Result { let globs = globs .into_iter() - .map(|glob| Glob::new(&glob)) + .map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string())) .collect::, _>>()?; let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect(); let sources_with_trailing_slash = globs @@ -507,7 +515,9 @@ mod custom_path_matcher { .zip(self.sources_with_trailing_slash.iter()) .any(|(source, with_slash)| { let as_bytes = other_path.as_os_str().as_encoded_bytes(); - let with_slash = if source.ends_with("/") { + // todo(windows) + // Potential bug: this assumes that the path separator is always `\` on Windows + let with_slash = if source.ends_with(std::path::MAIN_SEPARATOR_STR) { source.as_bytes() } else { with_slash.as_bytes() @@ -569,6 +579,7 @@ mod test { use serde_json::json; use settings::SettingsStore; use smol::stream::StreamExt; + use util::{path, separator}; use super::collect_files; @@ -592,7 +603,7 @@ mod test { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/root", + path!("/root"), json!({ "dir": { "subdir": { @@ -607,7 +618,7 @@ mod test { ) .await; - let project = Project::test(fs, ["/root".as_ref()], cx).await; + let project = Project::test(fs, [path!("/root").as_ref()], cx).await; let result_1 = cx.update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx)); @@ -615,7 +626,7 @@ mod test { .await .unwrap(); - assert!(result_1.text.starts_with("root/dir")); + assert!(result_1.text.starts_with(separator!("root/dir"))); // 4 files + 2 directories assert_eq!(result_1.sections.len(), 6); @@ -631,7 +642,7 @@ mod test { cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed()); let result = SlashCommandOutput::from_event_stream(result).await.unwrap(); - assert!(result.text.starts_with("root/dir")); + assert!(result.text.starts_with(separator!("root/dir"))); // 5 files + 2 directories assert_eq!(result.sections.len(), 7); @@ -645,7 +656,7 @@ mod test { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/zed", + path!("/zed"), json!({ "assets": { "dir1": { @@ -670,7 +681,7 @@ mod test { ) .await; - let project = Project::test(fs, ["/zed".as_ref()], cx).await; + let project = Project::test(fs, [path!("/zed").as_ref()], cx).await; let result = cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx)); @@ -679,27 +690,36 @@ mod test { .unwrap(); // Sanity check - assert!(result.text.starts_with("zed/assets/themes\n")); + assert!(result.text.starts_with(separator!("zed/assets/themes\n"))); assert_eq!(result.sections.len(), 7); // Ensure that full file paths are included in the real output - assert!(result.text.contains("zed/assets/themes/andromeda/LICENSE")); - assert!(result.text.contains("zed/assets/themes/ayu/LICENSE")); - assert!(result.text.contains("zed/assets/themes/summercamp/LICENSE")); + assert!(result + .text + .contains(separator!("zed/assets/themes/andromeda/LICENSE"))); + assert!(result + .text + .contains(separator!("zed/assets/themes/ayu/LICENSE"))); + assert!(result + .text + .contains(separator!("zed/assets/themes/summercamp/LICENSE"))); assert_eq!(result.sections[5].label, "summercamp"); // Ensure that things are in descending order, with properly relativized paths assert_eq!( result.sections[0].label, - "zed/assets/themes/andromeda/LICENSE" + separator!("zed/assets/themes/andromeda/LICENSE") ); assert_eq!(result.sections[1].label, "andromeda"); - assert_eq!(result.sections[2].label, "zed/assets/themes/ayu/LICENSE"); + assert_eq!( + result.sections[2].label, + separator!("zed/assets/themes/ayu/LICENSE") + ); assert_eq!(result.sections[3].label, "ayu"); assert_eq!( result.sections[4].label, - "zed/assets/themes/summercamp/LICENSE" + separator!("zed/assets/themes/summercamp/LICENSE") ); // Ensure that the project lasts until after the last await @@ -712,7 +732,7 @@ mod test { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/zed", + path!("/zed"), json!({ "assets": { "themes": { @@ -732,7 +752,7 @@ mod test { ) .await; - let project = Project::test(fs, ["/zed".as_ref()], cx).await; + let project = Project::test(fs, [path!("/zed").as_ref()], cx).await; let result = cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx)); @@ -740,26 +760,29 @@ mod test { .await .unwrap(); - assert!(result.text.starts_with("zed/assets/themes\n")); - assert_eq!(result.sections[0].label, "zed/assets/themes/LICENSE"); + assert!(result.text.starts_with(separator!("zed/assets/themes\n"))); + assert_eq!( + result.sections[0].label, + separator!("zed/assets/themes/LICENSE") + ); assert_eq!( result.sections[1].label, - "zed/assets/themes/summercamp/LICENSE" + separator!("zed/assets/themes/summercamp/LICENSE") ); assert_eq!( result.sections[2].label, - "zed/assets/themes/summercamp/subdir/LICENSE" + separator!("zed/assets/themes/summercamp/subdir/LICENSE") ); assert_eq!( result.sections[3].label, - "zed/assets/themes/summercamp/subdir/subsubdir/LICENSE" + separator!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE") ); assert_eq!(result.sections[4].label, "subsubdir"); assert_eq!(result.sections[5].label, "subdir"); assert_eq!(result.sections[6].label, "summercamp"); - assert_eq!(result.sections[7].label, "zed/assets/themes"); + assert_eq!(result.sections[7].label, separator!("zed/assets/themes")); - assert_eq!(result.text, "zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"); + assert_eq!(result.text, separator!("zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n")); // Ensure that the project lasts until after the last await drop(project); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index a95b2efeb03c7a..6b65b8057c0091 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1061,6 +1061,7 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { mod tests { use super::*; use gpui::TestAppContext; + use util::path; #[gpui::test(iterations = 10)] async fn test_buffer_management(cx: &mut TestAppContext) { @@ -1123,7 +1124,7 @@ mod tests { buffer_1.update(cx, |buffer, cx| { buffer.file_updated( Arc::new(File { - abs_path: "/root/child/buffer-1".into(), + abs_path: path!("/root/child/buffer-1").into(), path: Path::new("child/buffer-1").into(), }), cx, @@ -1136,7 +1137,7 @@ mod tests { text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri), } ); - let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap(); + let buffer_1_uri = lsp::Url::from_file_path(path!("/root/child/buffer-1")).unwrap(); assert_eq!( lsp.receive_notification::() .await, diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index dd1bf335cac141..9c25e295aa91f7 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -290,7 +290,10 @@ mod tests { use serde_json::json; use settings::SettingsStore; use std::future::Future; - use util::test::{marked_text_ranges_by, TextRangeMarker}; + use util::{ + path, + test::{marked_text_ranges_by, TextRangeMarker}, + }; #[gpui::test(iterations = 10)] async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) { @@ -949,24 +952,24 @@ mod tests { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/test", + path!("/test"), json!({ ".env": "SECRET=something\n", "README.md": "hello\nworld\nhow\nare\nyou\ntoday" }), ) .await; - let project = Project::test(fs, ["/test".as_ref()], cx).await; + let project = Project::test(fs, [path!("/test").as_ref()], cx).await; let private_buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/test/.env", cx) + project.open_local_buffer(path!("/test/.env"), cx) }) .await .unwrap(); let public_buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/test/README.md", cx) + project.open_local_buffer(path!("/test/README.md"), cx) }) .await .unwrap(); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 973c7736e5035f..a4ee103232ce56 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1444,7 +1444,10 @@ impl ToDisplayPoint for Anchor { #[cfg(test)] pub mod tests { use super::*; - use crate::{movement, test::marked_display_snapshot}; + use crate::{ + movement, + test::{marked_display_snapshot, test_font}, + }; use block_map::BlockPlacement; use gpui::{ div, font, observe, px, App, AppContext as _, BorrowAppContext, Element, Hsla, Rgba, @@ -1503,10 +1506,11 @@ pub mod tests { } }); + let font = test_font(); let map = cx.new(|cx| { DisplayMap::new( buffer.clone(), - font("Helvetica"), + font, font_size, wrap_width, true, diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 7715471a1f3f91..b4bf81846ede77 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1992,8 +1992,9 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) { #[cfg(test)] mod tests { use super::*; - use crate::display_map::{ - fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap, + use crate::{ + display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap}, + test::test_font, }; use gpui::{div, font, px, App, AppContext as _, Element}; use itertools::Itertools; @@ -2227,7 +2228,7 @@ mod tests { multi_buffer }); - let font = font("Helvetica"); + let font = test_font(); let font_size = px(14.); let font_id = cx.text_system().resolve_font(&font); let mut wrap_width = px(0.); @@ -3069,8 +3070,9 @@ mod tests { let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); - let (wrap_map, wraps_snapshot) = cx - .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx)); + let font = test_font(); + let (wrap_map, wraps_snapshot) = + cx.update(|cx| WrapMap::new(tab_snapshot, font, font_size, wrap_width, cx)); let mut block_map = BlockMap::new( wraps_snapshot, true, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 6b00ab7db0f796..77eab5881cb965 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1169,9 +1169,10 @@ mod tests { use super::*; use crate::{ display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, + test::test_font, MultiBuffer, }; - use gpui::{font, px, test::observe}; + use gpui::{px, test::observe}; use rand::prelude::*; use settings::SettingsStore; use smol::stream::StreamExt; @@ -1196,7 +1197,8 @@ mod tests { Some(px(rng.gen_range(0.0..=1000.0))) }; let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); - let font = font("Helvetica"); + + let font = test_font(); let _font_id = text_system.font_id(&font); let font_size = px(14.0); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b17ef2a1dbf5dd..a28c5479ac2039 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -41,8 +41,9 @@ use std::{ use test::{build_editor_with_project, editor_lsp_test_context::rust_lang}; use unindent::Unindent; use util::{ - assert_set_eq, + assert_set_eq, path, test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, + uri, }; use workspace::{ item::{FollowEvent, FollowableItem, Item, ItemHandle}, @@ -7075,9 +7076,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; + fs.insert_file(path!("/file.rs"), Default::default()).await; - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -7093,7 +7094,9 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { ); let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/file.rs"), cx) + }) .await .unwrap(); @@ -7118,7 +7121,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() + lsp::Url::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( @@ -7146,7 +7149,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { fake_server.handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() + lsp::Url::from_file_path(path!("/file.rs")).unwrap() ); futures::future::pending::<()>().await; unreachable!() @@ -7203,7 +7206,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() + lsp::Url::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 8); Ok(Some(vec![])) @@ -7238,7 +7241,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": sample_text_1, "other.rs": sample_text_2, @@ -7247,7 +7250,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); @@ -7422,20 +7425,20 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) { assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx))); assert_eq!( multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)), - "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}", + uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"), ); buffer_1.update(cx, |buffer, _| { assert!(!buffer.is_dirty()); assert_eq!( buffer.text(), - "a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n", + uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"), ) }); buffer_2.update(cx, |buffer, _| { assert!(!buffer.is_dirty()); assert_eq!( buffer.text(), - "lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n", + uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"), ) }); buffer_3.update(cx, |buffer, _| { @@ -7449,9 +7452,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; + fs.insert_file(path!("/file.rs"), Default::default()).await; - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs, [path!("/").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -7467,7 +7470,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { ); let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/file.rs"), cx) + }) .await .unwrap(); @@ -7492,7 +7497,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() + lsp::Url::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( @@ -7520,7 +7525,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() + lsp::Url::from_file_path(path!("/file.rs")).unwrap() ); futures::future::pending::<()>().await; unreachable!() @@ -7578,7 +7583,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() + lsp::Url::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 8); Ok(Some(vec![])) @@ -7598,9 +7603,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { }); let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; + fs.insert_file(path!("/file.rs"), Default::default()).await; - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs, [path!("/").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(Arc::new(Language::new( @@ -7634,7 +7639,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { ); let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/file.rs"), cx) + }) .await .unwrap(); @@ -7664,7 +7671,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { .handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() + lsp::Url::from_file_path(path!("/file.rs")).unwrap() ); assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( @@ -7688,7 +7695,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { fake_server.handle_request::(move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() + lsp::Url::from_file_path(path!("/file.rs")).unwrap() ); futures::future::pending::<()>().await; unreachable!() @@ -8728,14 +8735,14 @@ async fn test_multiline_completion(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.ts": "a", }), ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let typescript_language = Arc::new(Language::new( LanguageConfig { @@ -8795,7 +8802,7 @@ async fn test_multiline_completion(cx: &mut gpui::TestAppContext) { .unwrap(); let _buffer = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/a/main.ts", cx) + project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx) }) .await .unwrap(); @@ -10571,7 +10578,7 @@ async fn go_to_prev_overlapping_diagnostic( .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/root/file").unwrap(), + uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), version: None, diagnostics: vec![ lsp::Diagnostic { @@ -10664,7 +10671,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) { lsp_store.update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/root/file").unwrap(), + uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)), @@ -10925,14 +10932,14 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": "fn main() { let a = 5; }", "other.rs": "// Test file", }), ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(Arc::new(Language::new( @@ -10984,7 +10991,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { let buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) + project.open_local_buffer(path!("/a/main.rs"), cx) }) .await .unwrap(); @@ -11004,7 +11011,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { fake_server.handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), + lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), ); assert_eq!( params.text_document_position.position, @@ -11042,7 +11049,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": "fn main() { let a = 5; }", "other.rs": "// Test file", @@ -11050,7 +11057,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let server_restarts = Arc::new(AtomicUsize::new(0)); let closure_restarts = Arc::clone(&server_restarts); @@ -11090,7 +11097,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let _buffer = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/a/main.rs", cx) + project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx) }) .await .unwrap(); @@ -11863,9 +11870,9 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { }); let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.ts", Default::default()).await; + fs.insert_file(path!("/file.ts"), Default::default()).await; - let project = Project::test(fs, ["/file.ts".as_ref()], cx).await; + let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(Arc::new(Language::new( @@ -11897,7 +11904,9 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.ts", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/file.ts"), cx) + }) .await .unwrap(); diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index d9c4926d33cc85..767c1eabb9e0e0 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -560,7 +560,7 @@ mod tests { use settings::SettingsStore; use std::{cmp, env, ops::Range, path::Path}; use unindent::Unindent as _; - use util::RandomCharIter; + use util::{path, RandomCharIter}; // macro_rules! assert_blame_rows { // ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => { @@ -793,7 +793,7 @@ mod tests { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/my-repo", + path!("/my-repo"), json!({ ".git": {}, "file.txt": r#" @@ -807,7 +807,7 @@ mod tests { .await; fs.set_blame_for_repo( - Path::new("/my-repo/.git"), + Path::new(path!("/my-repo/.git")), vec![( "file.txt".into(), Blame { @@ -817,10 +817,10 @@ mod tests { )], ); - let project = Project::test(fs, ["/my-repo".as_ref()], cx).await; + let project = Project::test(fs, [path!("/my-repo").as_ref()], cx).await; let buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/my-repo/file.txt", cx) + project.open_local_buffer(path!("/my-repo/file.txt"), cx) }) .await .unwrap(); @@ -945,7 +945,7 @@ mod tests { log::info!("initial buffer text: {:?}", buffer_initial_text); fs.insert_tree( - "/my-repo", + path!("/my-repo"), json!({ ".git": {}, "file.txt": buffer_initial_text.to_string() @@ -956,7 +956,7 @@ mod tests { let blame_entries = gen_blame_entries(buffer_initial_text.max_point().row, &mut rng); log::info!("initial blame entries: {:?}", blame_entries); fs.set_blame_for_repo( - Path::new("/my-repo/.git"), + Path::new(path!("/my-repo/.git")), vec![( "file.txt".into(), Blame { @@ -966,10 +966,10 @@ mod tests { )], ); - let project = Project::test(fs.clone(), ["/my-repo".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/my-repo").as_ref()], cx).await; let buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/my-repo/file.txt", cx) + project.open_local_buffer(path!("/my-repo/file.txt"), cx) }) .await .unwrap(); @@ -998,7 +998,7 @@ mod tests { log::info!("regenerating blame entries: {:?}", blame_entries); fs.set_blame_for_repo( - Path::new("/my-repo/.git"), + Path::new(path!("/my-repo/.git")), vec![( "file.txt".into(), Blame { diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 0442669e5eecaa..b0e4abcb32f4a4 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -921,7 +921,7 @@ mod tests { use indoc::indoc; use language::language_settings::InlayHintSettings; use lsp::request::{GotoDefinition, GotoTypeDefinition}; - use util::assert_set_eq; + use util::{assert_set_eq, path}; use workspace::item::Item; #[gpui::test] @@ -1574,18 +1574,31 @@ mod tests { // Insert a new file let fs = cx.update_workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); fs.as_fake() - .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec()) + .insert_file( + path!("/root/dir/file2.rs"), + "This is file2.rs".as_bytes().to_vec(), + ) .await; + #[cfg(not(target_os = "windows"))] cx.set_state(indoc! {" You can't go to a file that does_not_exist.txt. Go to file2.rs if you want. Or go to ../dir/file2.rs if you want. Or go to /root/dir/file2.rs if project is local. Or go to /root/dir/file2 if this is a Rust file.ˇ + "}); + #[cfg(target_os = "windows")] + cx.set_state(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to file2.rs if you want. + Or go to ../dir/file2.rs if you want. + Or go to C:/root/dir/file2.rs if project is local. + Or go to C:/root/dir/file2 if this is a Rust file.ˇ "}); // File does not exist + #[cfg(not(target_os = "windows"))] let screen_coord = cx.pixel_position(indoc! {" You can't go to a file that dˇoes_not_exist.txt. Go to file2.rs if you want. @@ -1593,6 +1606,14 @@ mod tests { Or go to /root/dir/file2.rs if project is local. Or go to /root/dir/file2 if this is a Rust file. "}); + #[cfg(target_os = "windows")] + let screen_coord = cx.pixel_position(indoc! {" + You can't go to a file that dˇoes_not_exist.txt. + Go to file2.rs if you want. + Or go to ../dir/file2.rs if you want. + Or go to C:/root/dir/file2.rs if project is local. + Or go to C:/root/dir/file2 if this is a Rust file. + "}); cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key()); // No highlight cx.update_editor(|editor, window, cx| { @@ -1605,6 +1626,7 @@ mod tests { }); // Moving the mouse over a file that does exist should highlight it. + #[cfg(not(target_os = "windows"))] let screen_coord = cx.pixel_position(indoc! {" You can't go to a file that does_not_exist.txt. Go to fˇile2.rs if you want. @@ -1612,8 +1634,17 @@ mod tests { Or go to /root/dir/file2.rs if project is local. Or go to /root/dir/file2 if this is a Rust file. "}); + #[cfg(target_os = "windows")] + let screen_coord = cx.pixel_position(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to fˇile2.rs if you want. + Or go to ../dir/file2.rs if you want. + Or go to C:/root/dir/file2.rs if project is local. + Or go to C:/root/dir/file2 if this is a Rust file. + "}); cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key()); + #[cfg(not(target_os = "windows"))] cx.assert_editor_text_highlights::(indoc! {" You can't go to a file that does_not_exist.txt. Go to «file2.rsˇ» if you want. @@ -1621,8 +1652,17 @@ mod tests { Or go to /root/dir/file2.rs if project is local. Or go to /root/dir/file2 if this is a Rust file. "}); + #[cfg(target_os = "windows")] + cx.assert_editor_text_highlights::(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to «file2.rsˇ» if you want. + Or go to ../dir/file2.rs if you want. + Or go to C:/root/dir/file2.rs if project is local. + Or go to C:/root/dir/file2 if this is a Rust file. + "}); // Moving the mouse over a relative path that does exist should highlight it + #[cfg(not(target_os = "windows"))] let screen_coord = cx.pixel_position(indoc! {" You can't go to a file that does_not_exist.txt. Go to file2.rs if you want. @@ -1630,8 +1670,17 @@ mod tests { Or go to /root/dir/file2.rs if project is local. Or go to /root/dir/file2 if this is a Rust file. "}); + #[cfg(target_os = "windows")] + let screen_coord = cx.pixel_position(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to file2.rs if you want. + Or go to ../dir/fˇile2.rs if you want. + Or go to C:/root/dir/file2.rs if project is local. + Or go to C:/root/dir/file2 if this is a Rust file. + "}); cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key()); + #[cfg(not(target_os = "windows"))] cx.assert_editor_text_highlights::(indoc! {" You can't go to a file that does_not_exist.txt. Go to file2.rs if you want. @@ -1639,8 +1688,17 @@ mod tests { Or go to /root/dir/file2.rs if project is local. Or go to /root/dir/file2 if this is a Rust file. "}); + #[cfg(target_os = "windows")] + cx.assert_editor_text_highlights::(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to file2.rs if you want. + Or go to «../dir/file2.rsˇ» if you want. + Or go to C:/root/dir/file2.rs if project is local. + Or go to C:/root/dir/file2 if this is a Rust file. + "}); // Moving the mouse over an absolute path that does exist should highlight it + #[cfg(not(target_os = "windows"))] let screen_coord = cx.pixel_position(indoc! {" You can't go to a file that does_not_exist.txt. Go to file2.rs if you want. @@ -1649,7 +1707,17 @@ mod tests { Or go to /root/dir/file2 if this is a Rust file. "}); + #[cfg(target_os = "windows")] + let screen_coord = cx.pixel_position(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to file2.rs if you want. + Or go to ../dir/file2.rs if you want. + Or go to C:/root/diˇr/file2.rs if project is local. + Or go to C:/root/dir/file2 if this is a Rust file. + "}); + cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key()); + #[cfg(not(target_os = "windows"))] cx.assert_editor_text_highlights::(indoc! {" You can't go to a file that does_not_exist.txt. Go to file2.rs if you want. @@ -1657,8 +1725,17 @@ mod tests { Or go to «/root/dir/file2.rsˇ» if project is local. Or go to /root/dir/file2 if this is a Rust file. "}); + #[cfg(target_os = "windows")] + cx.assert_editor_text_highlights::(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to file2.rs if you want. + Or go to ../dir/file2.rs if you want. + Or go to «C:/root/dir/file2.rsˇ» if project is local. + Or go to C:/root/dir/file2 if this is a Rust file. + "}); // Moving the mouse over a path that exists, if we add the language-specific suffix, it should highlight it + #[cfg(not(target_os = "windows"))] let screen_coord = cx.pixel_position(indoc! {" You can't go to a file that does_not_exist.txt. Go to file2.rs if you want. @@ -1666,8 +1743,17 @@ mod tests { Or go to /root/dir/file2.rs if project is local. Or go to /root/diˇr/file2 if this is a Rust file. "}); + #[cfg(target_os = "windows")] + let screen_coord = cx.pixel_position(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to file2.rs if you want. + Or go to ../dir/file2.rs if you want. + Or go to C:/root/dir/file2.rs if project is local. + Or go to C:/root/diˇr/file2 if this is a Rust file. + "}); cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key()); + #[cfg(not(target_os = "windows"))] cx.assert_editor_text_highlights::(indoc! {" You can't go to a file that does_not_exist.txt. Go to file2.rs if you want. @@ -1675,6 +1761,14 @@ mod tests { Or go to /root/dir/file2.rs if project is local. Or go to «/root/dir/file2ˇ» if this is a Rust file. "}); + #[cfg(target_os = "windows")] + cx.assert_editor_text_highlights::(indoc! {" + You can't go to a file that does_not_exist.txt. + Go to file2.rs if you want. + Or go to ../dir/file2.rs if you want. + Or go to C:/root/dir/file2.rs if project is local. + Or go to «C:/root/dir/file2ˇ» if this is a Rust file. + "}); cx.simulate_click(screen_coord, Modifiers::secondary_key()); @@ -1692,7 +1786,10 @@ mod tests { let file = buffer.read(cx).file().unwrap(); let file_path = file.as_local().unwrap().abs_path(cx); - assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs"); + assert_eq!( + file_path, + std::path::PathBuf::from(path!("/root/dir/file2.rs")) + ); }); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 3b325a58025b76..e6789c3a3abf85 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1274,6 +1274,7 @@ pub mod tests { use settings::SettingsStore; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use text::Point; + use util::path; use super::*; @@ -1499,7 +1500,7 @@ pub mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", "other.md": "Test md file with some text", @@ -1507,7 +1508,7 @@ pub mod tests { ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let mut rs_fake_servers = None; @@ -1542,14 +1543,16 @@ pub mod tests { "Rust" => { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), + lsp::Url::from_file_path(path!("/a/main.rs")) + .unwrap(), ); rs_lsp_request_count.fetch_add(1, Ordering::Release) + 1 } "Markdown" => { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/a/other.md").unwrap(), + lsp::Url::from_file_path(path!("/a/other.md")) + .unwrap(), ); md_lsp_request_count.fetch_add(1, Ordering::Release) + 1 } @@ -1585,7 +1588,7 @@ pub mod tests { let rs_buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) + project.open_local_buffer(path!("/a/main.rs"), cx) }) .await .unwrap(); @@ -1611,7 +1614,7 @@ pub mod tests { cx.executor().run_until_parked(); let md_buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/a/other.md", cx) + project.open_local_buffer(path!("/a/other.md"), cx) }) .await .unwrap(); @@ -2173,7 +2176,7 @@ pub mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), "other.rs": "// Test file", @@ -2181,7 +2184,7 @@ pub mod tests { ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -2209,7 +2212,7 @@ pub mod tests { async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), + lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), ); task_lsp_request_ranges.lock().push(params.range); @@ -2237,7 +2240,7 @@ pub mod tests { let buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) + project.open_local_buffer(path!("/a/main.rs"), cx) }) .await .unwrap(); @@ -2471,7 +2474,7 @@ pub mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), @@ -2479,7 +2482,7 @@ pub mod tests { ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let language = rust_lang(); @@ -2497,13 +2500,13 @@ pub mod tests { let (buffer_1, _handle1) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/a/main.rs", cx) + project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx) }) .await .unwrap(); let (buffer_2, _handle2) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/a/other.rs", cx) + project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx) }) .await .unwrap(); @@ -2585,11 +2588,11 @@ pub mod tests { let task_editor_edited = Arc::clone(&closure_editor_edited); async move { let hint_text = if params.text_document.uri - == lsp::Url::from_file_path("/a/main.rs").unwrap() + == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap() { "main hint" } else if params.text_document.uri - == lsp::Url::from_file_path("/a/other.rs").unwrap() + == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap() { "other hint" } else { @@ -2815,7 +2818,7 @@ pub mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), @@ -2823,7 +2826,7 @@ pub mod tests { ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -2840,13 +2843,13 @@ pub mod tests { let (buffer_1, _handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/a/main.rs", cx) + project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx) }) .await .unwrap(); let (buffer_2, _handle2) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/a/other.rs", cx) + project.open_local_buffer_with_lsp(path!("/a/other.rs"), cx) }) .await .unwrap(); @@ -2886,11 +2889,11 @@ pub mod tests { let task_editor_edited = Arc::clone(&closure_editor_edited); async move { let hint_text = if params.text_document.uri - == lsp::Url::from_file_path("/a/main.rs").unwrap() + == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap() { "main hint" } else if params.text_document.uri - == lsp::Url::from_file_path("/a/other.rs").unwrap() + == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap() { "other hint" } else { @@ -3027,7 +3030,7 @@ pub mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), "other.rs": "// Test file", @@ -3035,7 +3038,7 @@ pub mod tests { ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -3054,7 +3057,7 @@ pub mod tests { async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), + lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), ); let query_start = params.range.start; Ok(Some(vec![lsp::InlayHint { @@ -3077,7 +3080,7 @@ pub mod tests { let buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) + project.open_local_buffer(path!("/a/main.rs"), cx) }) .await .unwrap(); @@ -3250,7 +3253,7 @@ pub mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": "fn main() { let x = 42; @@ -3265,7 +3268,7 @@ pub mod tests { ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -3281,7 +3284,7 @@ pub mod tests { move |params, _| async move { assert_eq!( params.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), + lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), ); Ok(Some( serde_json::from_value(json!([ @@ -3351,7 +3354,7 @@ pub mod tests { let buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) + project.open_local_buffer(path!("/a/main.rs"), cx) }) .await .unwrap(); @@ -3408,7 +3411,7 @@ pub mod tests { ) -> (&'static str, WindowHandle, FakeLanguageServer) { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/a", + path!("/a"), json!({ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", "other.rs": "// Test file", @@ -3416,8 +3419,8 @@ pub mod tests { ) .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - let file_path = "/a/main.rs"; + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let file_path = path!("/a/main.rs"); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -3435,7 +3438,7 @@ pub mod tests { let buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) + project.open_local_buffer(path!("/a/main.rs"), cx) }) .await .unwrap(); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2679df6a7c306d..d53b95b007e733 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1717,6 +1717,7 @@ mod tests { use language::{LanguageMatcher, TestFile}; use project::FakeFs; use std::path::{Path, PathBuf}; + use util::path; #[gpui::test] fn test_path_for_file(cx: &mut App) { @@ -1771,24 +1772,24 @@ mod tests { init_test(cx, |_| {}); let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; + fs.insert_file(path!("/file.rs"), Default::default()).await; // Test case 1: Deserialize with path and contents { - let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let item_id = 1234 as ItemId; let mtime = fs - .metadata(Path::new("/file.rs")) + .metadata(Path::new(path!("/file.rs"))) .await .unwrap() .unwrap() .mtime; let serialized_editor = SerializedEditor { - abs_path: Some(PathBuf::from("/file.rs")), + abs_path: Some(PathBuf::from(path!("/file.rs"))), contents: Some("fn main() {}".to_string()), language: Some("Rust".to_string()), mtime: Some(mtime), @@ -1812,7 +1813,7 @@ mod tests { // Test case 2: Deserialize with only path { - let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -1820,7 +1821,7 @@ mod tests { let item_id = 5678 as ItemId; let serialized_editor = SerializedEditor { - abs_path: Some(PathBuf::from("/file.rs")), + abs_path: Some(PathBuf::from(path!("/file.rs"))), contents: None, language: None, mtime: None, @@ -1845,7 +1846,7 @@ mod tests { // Test case 3: Deserialize with no path (untitled buffer, with content and language) { - let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; // Add Rust to the language, so that we can restore the language of the buffer project.update(cx, |project, _| project.languages().add(rust_language())); @@ -1884,7 +1885,7 @@ mod tests { // Test case 4: Deserialize with path, content, and old mtime { - let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -1893,7 +1894,7 @@ mod tests { let item_id = 9345 as ItemId; let old_mtime = MTime::from_seconds_and_nanos(0, 50); let serialized_editor = SerializedEditor { - abs_path: Some(PathBuf::from("/file.rs")), + abs_path: Some(PathBuf::from(path!("/file.rs"))), contents: Some("fn main() {}".to_string()), language: Some("Rust".to_string()), mtime: Some(old_mtime), diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 6b8451cba1d0bc..35fb1b4c91f857 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,12 +1,15 @@ pub mod editor_lsp_test_context; pub mod editor_test_context; +use std::sync::LazyLock; + use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, }; use gpui::{ - AppContext as _, Context, Entity, Font, FontFeatures, FontStyle, FontWeight, Pixels, Window, + font, AppContext as _, Context, Entity, Font, FontFeatures, FontStyle, FontWeight, Pixels, + Window, }; use project::Project; use util::test::{marked_text_offsets, marked_text_ranges}; @@ -19,6 +22,22 @@ fn init_logger() { } } +pub fn test_font() -> Font { + static TEST_FONT: LazyLock = LazyLock::new(|| { + #[cfg(not(target_os = "windows"))] + { + font("Helvetica") + } + + #[cfg(target_os = "windows")] + { + font("Courier New") + } + }); + + TEST_FONT.clone() +} + // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. pub fn marked_display_snapshot( text: &str, diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 21f65ef97002fa..26c147a663323e 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -455,7 +455,12 @@ async fn test_extension_store(cx: &mut TestAppContext) { }); } +// todo(windows) +// Disable this test on Windows for now. Because this test hangs at +// `let fake_server = fake_servers.next().await.unwrap();`. +// Reenable this test when we figure out why. #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); @@ -634,6 +639,8 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { .await .unwrap(); + // todo(windows) + // This test hangs here on Windows. let fake_server = fake_servers.next().await.unwrap(); let expected_server_path = extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam")); diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index b25ed8b9c1555a..8555da775e693b 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -6,6 +6,7 @@ use gpui::{Entity, TestAppContext, VisualTestContext}; use menu::{Confirm, SelectNext, SelectPrev}; use project::{RemoveOptions, FS_WATCH_LATENCY}; use serde_json::json; +use util::path; use workspace::{AppState, ToggleFileFinder, Workspace}; #[ctor::ctor] @@ -90,7 +91,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "a": { "file1.txt": "", @@ -102,16 +103,16 @@ async fn test_absolute_paths(cx: &mut TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (picker, workspace, cx) = build_find_picker(project, cx); - let matching_abs_path = "/root/a/b/file2.txt"; + let matching_abs_path = path!("/root/a/b/file2.txt").to_string(); picker .update_in(cx, |picker, window, cx| { picker .delegate - .update_matches(matching_abs_path.to_string(), window, cx) + .update_matches(matching_abs_path, window, cx) }) .await; picker.update(cx, |picker, _| { @@ -128,12 +129,12 @@ async fn test_absolute_paths(cx: &mut TestAppContext) { assert_eq!(active_editor.read(cx).title(cx), "file2.txt"); }); - let mismatching_abs_path = "/root/a/b/file1.txt"; + let mismatching_abs_path = path!("/root/a/b/file1.txt").to_string(); picker .update_in(cx, |picker, window, cx| { picker .delegate - .update_matches(mismatching_abs_path.to_string(), window, cx) + .update_matches(mismatching_abs_path, window, cx) }) .await; picker.update(cx, |picker, _| { @@ -518,7 +519,7 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "dir1": { "a.txt": "" }, "dir2": { @@ -529,7 +530,7 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let worktree_id = cx.read(|cx| { @@ -606,7 +607,7 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { "first.rs": "// First Rust file", @@ -617,7 +618,7 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); @@ -648,7 +649,7 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) { worktree_id, path: Arc::from(Path::new("test/first.rs")), }, - Some(PathBuf::from("/src/test/first.rs")) + Some(PathBuf::from(path!("/src/test/first.rs"))) )], "Should show 1st opened item in the history when opening the 2nd item" ); @@ -663,14 +664,14 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) { worktree_id, path: Arc::from(Path::new("test/second.rs")), }, - Some(PathBuf::from("/src/test/second.rs")) + Some(PathBuf::from(path!("/src/test/second.rs"))) ), FoundPath::new( ProjectPath { worktree_id, path: Arc::from(Path::new("test/first.rs")), }, - Some(PathBuf::from("/src/test/first.rs")) + Some(PathBuf::from(path!("/src/test/first.rs"))) ), ], "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ @@ -687,21 +688,21 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) { worktree_id, path: Arc::from(Path::new("test/third.rs")), }, - Some(PathBuf::from("/src/test/third.rs")) + Some(PathBuf::from(path!("/src/test/third.rs"))) ), FoundPath::new( ProjectPath { worktree_id, path: Arc::from(Path::new("test/second.rs")), }, - Some(PathBuf::from("/src/test/second.rs")) + Some(PathBuf::from(path!("/src/test/second.rs"))) ), FoundPath::new( ProjectPath { worktree_id, path: Arc::from(Path::new("test/first.rs")), }, - Some(PathBuf::from("/src/test/first.rs")) + Some(PathBuf::from(path!("/src/test/first.rs"))) ), ], "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ @@ -718,21 +719,21 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) { worktree_id, path: Arc::from(Path::new("test/second.rs")), }, - Some(PathBuf::from("/src/test/second.rs")) + Some(PathBuf::from(path!("/src/test/second.rs"))) ), FoundPath::new( ProjectPath { worktree_id, path: Arc::from(Path::new("test/third.rs")), }, - Some(PathBuf::from("/src/test/third.rs")) + Some(PathBuf::from(path!("/src/test/third.rs"))) ), FoundPath::new( ProjectPath { worktree_id, path: Arc::from(Path::new("test/first.rs")), }, - Some(PathBuf::from("/src/test/first.rs")) + Some(PathBuf::from(path!("/src/test/first.rs"))) ), ], "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ @@ -748,7 +749,7 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { "first.rs": "// First Rust file", @@ -762,7 +763,7 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/external-src", + path!("/external-src"), json!({ "test": { "third.rs": "// Third Rust file", @@ -772,10 +773,10 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; cx.update(|cx| { project.update(cx, |project, cx| { - project.find_or_create_worktree("/external-src", false, cx) + project.find_or_create_worktree(path!("/external-src"), false, cx) }) }) .detach(); @@ -791,7 +792,7 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) { workspace .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( - PathBuf::from("/external-src/test/third.rs"), + PathBuf::from(path!("/external-src/test/third.rs")), false, window, cx, @@ -827,7 +828,7 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) { worktree_id: external_worktree_id, path: Arc::from(Path::new("")), }, - Some(PathBuf::from("/external-src/test/third.rs")) + Some(PathBuf::from(path!("/external-src/test/third.rs"))) )], "Should show external file with its full path in the history after it was open" ); @@ -842,14 +843,14 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) { worktree_id, path: Arc::from(Path::new("test/second.rs")), }, - Some(PathBuf::from("/src/test/second.rs")) + Some(PathBuf::from(path!("/src/test/second.rs"))) ), FoundPath::new( ProjectPath { worktree_id: external_worktree_id, path: Arc::from(Path::new("")), }, - Some(PathBuf::from("/external-src/test/third.rs")) + Some(PathBuf::from(path!("/external-src/test/third.rs"))) ), ], "Should keep external file with history updates", @@ -864,7 +865,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { "first.rs": "// First Rust file", @@ -875,7 +876,7 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from @@ -919,7 +920,7 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { "first.rs": "// First Rust file", @@ -931,7 +932,7 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); @@ -964,7 +965,7 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { worktree_id, path: Arc::from(Path::new("test/first.rs")), }, - Some(PathBuf::from("/src/test/first.rs")) + Some(PathBuf::from(path!("/src/test/first.rs"))) )); assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs")); @@ -1007,7 +1008,7 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { worktree_id, path: Arc::from(Path::new("test/first.rs")), }, - Some(PathBuf::from("/src/test/first.rs")) + Some(PathBuf::from(path!("/src/test/first.rs"))) )); assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs")); @@ -1022,7 +1023,7 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "test": { "1_qw": "// First file that matches the query", @@ -1037,7 +1038,7 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await; @@ -1079,7 +1080,7 @@ async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppCon .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "test": { "1_qw": "", @@ -1088,7 +1089,7 @@ async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppCon ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // Open new buffer open_queried_buffer("1", 1, "1_qw", &workspace, cx).await; @@ -1109,7 +1110,7 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one( .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { "bar.rs": "// Bar file", @@ -1122,7 +1123,7 @@ async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one( ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; @@ -1202,7 +1203,7 @@ async fn test_non_separate_history_items(cx: &mut TestAppContext) { .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { "bar.rs": "// Bar file", @@ -1215,7 +1216,7 @@ async fn test_non_separate_history_items(cx: &mut TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await; @@ -1296,7 +1297,7 @@ async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) { .fs .as_fake() .insert_tree( - "/test", + path!("/test"), json!({ "test": { "1.txt": "// One", @@ -1307,7 +1308,7 @@ async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; @@ -1354,7 +1355,7 @@ async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut .fs .as_fake() .insert_tree( - "/test", + path!("/test"), json!({ "test": { "1.txt": "// One", @@ -1365,7 +1366,7 @@ async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut ) .await; - let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await; @@ -1384,7 +1385,11 @@ async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut // Add more files to the worktree to trigger update matches for i in 0..5 { - let filename = format!("/test/{}.txt", 4 + i); + let filename = if cfg!(windows) { + format!("C:/test/{}.txt", 4 + i) + } else { + format!("/test/{}.txt", 4 + i) + }; app_state .fs .create_file(Path::new(&filename), Default::default()) @@ -1410,7 +1415,7 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "collab_ui": { "first.rs": "// First Rust file", @@ -1422,7 +1427,7 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; @@ -1456,7 +1461,7 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) .fs .as_fake() .insert_tree( - "/src", + path!("/src"), json!({ "test": { "first.rs": "// First Rust file", @@ -1467,7 +1472,7 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) ) .await; - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; @@ -1476,7 +1481,7 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) app_state .fs .remove_file( - Path::new("/src/test/nonexistent.rs"), + Path::new(path!("/src/test/nonexistent.rs")), RemoveOptions::default(), ) .await @@ -1742,14 +1747,14 @@ async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui:: .fs .as_fake() .insert_tree( - "/test", + path!("/test"), json!({ "1.txt": "// One", }), ) .await; - let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; @@ -1809,7 +1814,7 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav( .fs .as_fake() .insert_tree( - "/test", + path!("/test"), json!({ "1.txt": "// One", "2.txt": "// Two", @@ -1817,7 +1822,7 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav( ) .await; - let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; @@ -1864,7 +1869,7 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav( .fs .as_fake() .insert_tree( - "/test", + path!("/test"), json!({ "1.txt": "// One", "2.txt": "// Two", @@ -1873,7 +1878,7 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav( ) .await; - let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; @@ -1921,14 +1926,14 @@ async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::Test .fs .as_fake() .insert_tree( - "/test", + path!("/test"), json!({ "1.txt": "// One", }), ) .await; - let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); open_queried_buffer("1", 1, "1.txt", &workspace, cx).await; diff --git a/crates/fuzzy/src/matcher.rs b/crates/fuzzy/src/matcher.rs index 1b039c16f507ba..66a480d87a8151 100644 --- a/crates/fuzzy/src/matcher.rs +++ b/crates/fuzzy/src/matcher.rs @@ -9,6 +9,8 @@ const BASE_DISTANCE_PENALTY: f64 = 0.6; const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; const MIN_DISTANCE_PENALTY: f64 = 0.2; +// TODO: +// Use `Path` instead of `&str` for paths. pub struct Matcher<'a> { query: &'a [char], lowercase_query: &'a [char], @@ -173,6 +175,8 @@ impl<'a> Matcher<'a> { path_idx: usize, cur_score: f64, ) -> f64 { + use std::path::MAIN_SEPARATOR; + if query_idx == self.query.len() { return 1.0; } @@ -196,13 +200,19 @@ impl<'a> Matcher<'a> { } else { path_cased[j - prefix.len()] }; - let is_path_sep = path_char == '/' || path_char == '\\'; + let is_path_sep = path_char == MAIN_SEPARATOR; if query_idx == 0 && is_path_sep { last_slash = j; } - if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { + #[cfg(not(target_os = "windows"))] + let need_to_score = + query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\'); + // `query_char == '\\'` breaks `test_match_path_entries` on Windows, `\` is only used as a path separator on Windows. + #[cfg(target_os = "windows")] + let need_to_score = query_char == path_char || (is_path_sep && query_char == '_'); + if need_to_score { let curr = if j < prefix.len() { prefix[j] } else { @@ -217,7 +227,7 @@ impl<'a> Matcher<'a> { path[j - 1 - prefix.len()] }; - if last == '/' { + if last == MAIN_SEPARATOR { char_score = 0.9; } else if (last == '-' || last == '_' || last == ' ' || last.is_numeric()) || (last.is_lowercase() && curr.is_uppercase()) @@ -238,7 +248,7 @@ impl<'a> Matcher<'a> { // Apply a severe penalty if the case doesn't match. // This will make the exact matches have higher score than the case-insensitive and the // path insensitive matches. - if (self.smart_case || curr == '/') && self.query[query_idx] != curr { + if (self.smart_case || curr == MAIN_SEPARATOR) && self.query[query_idx] != curr { char_score *= 0.001; } @@ -322,6 +332,7 @@ mod tests { assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]); } + #[cfg(not(target_os = "windows"))] #[test] fn test_match_path_entries() { let paths = vec![ @@ -363,6 +374,54 @@ mod tests { ); } + /// todo(windows) + /// Now, on Windows, users can only use the backslash as a path separator. + /// I do want to support both the backslash and the forward slash as path separators on Windows. + #[cfg(target_os = "windows")] + #[test] + fn test_match_path_entries() { + let paths = vec![ + "", + "a", + "ab", + "abC", + "abcd", + "alphabravocharlie", + "AlphaBravoCharlie", + "thisisatestdir", + "\\\\\\\\\\ThisIsATestDir", + "\\this\\is\\a\\test\\dir", + "\\test\\tiatd", + ]; + + assert_eq!( + match_single_path_query("abc", false, &paths), + vec![ + ("abC", vec![0, 1, 2]), + ("abcd", vec![0, 1, 2]), + ("AlphaBravoCharlie", vec![0, 5, 10]), + ("alphabravocharlie", vec![4, 5, 10]), + ] + ); + assert_eq!( + match_single_path_query("t\\i\\a\\t\\d", false, &paths), + vec![( + "\\this\\is\\a\\test\\dir", + vec![1, 5, 6, 8, 9, 10, 11, 15, 16] + ),] + ); + + assert_eq!( + match_single_path_query("tiatd", false, &paths), + vec![ + ("\\test\\tiatd", vec![6, 7, 8, 9, 10]), + ("\\this\\is\\a\\test\\dir", vec![1, 6, 9, 11, 16]), + ("\\\\\\\\\\ThisIsATestDir", vec![5, 9, 11, 12, 16]), + ("thisisatestdir", vec![0, 2, 6, 7, 11]), + ] + ); + } + #[test] fn test_lowercase_longer_than_uppercase() { // This character has more chars in lower-case than in upper-case. diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 0a7a4b921145f2..e4947e5bbd6dae 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -353,7 +353,7 @@ mod tests { let want_json = std::fs::read_to_string(&path).unwrap_or_else(|_| { panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); - }); + }).replace("\r\n", "\n"); pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); } diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index fb53a833d64e66..eef52b2014d760 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -428,17 +428,24 @@ impl DirectWriteState { target_font.fallbacks.as_ref(), ) .unwrap_or_else(|| { - let family = self.system_ui_font_name.clone(); - log::error!("{} not found, use {} instead.", target_font.family, family); - self.get_font_id_from_font_collection( - family.as_ref(), - target_font.weight, - target_font.style, - &target_font.features, - target_font.fallbacks.as_ref(), - true, - ) - .unwrap() + #[cfg(any(test, feature = "test-support"))] + { + panic!("ERROR: {} font not found!", target_font.family); + } + #[cfg(not(any(test, feature = "test-support")))] + { + let family = self.system_ui_font_name.clone(); + log::error!("{} not found, use {} instead.", target_font.family, family); + self.get_font_id_from_font_collection( + family.as_ref(), + target_font.weight, + target_font.style, + &target_font.features, + target_font.fallbacks.as_ref(), + true, + ) + .unwrap() + } }) } } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index ef4fd4a778a046..5423dfcbc775e0 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -756,21 +756,20 @@ fn should_auto_hide_scrollbars() -> Result { #[cfg(test)] mod tests { - use crate::{ClipboardItem, Platform, WindowsPlatform}; + use crate::{read_from_clipboard, write_to_clipboard, ClipboardItem}; #[test] fn test_clipboard() { - let platform = WindowsPlatform::new(); - let item = ClipboardItem::new_string("你好".to_string()); - platform.write_to_clipboard(item.clone()); - assert_eq!(platform.read_from_clipboard(), Some(item)); + let item = ClipboardItem::new_string("你好,我是张小白".to_string()); + write_to_clipboard(item.clone()); + assert_eq!(read_from_clipboard(), Some(item)); let item = ClipboardItem::new_string("12345".to_string()); - platform.write_to_clipboard(item.clone()); - assert_eq!(platform.read_from_clipboard(), Some(item)); + write_to_clipboard(item.clone()); + assert_eq!(read_from_clipboard(), Some(item)); let item = ClipboardItem::new_string_with_json_metadata("abcdef".to_string(), vec![3, 4]); - platform.write_to_clipboard(item.clone()); - assert_eq!(platform.read_from_clipboard(), Some(item)); + write_to_clipboard(item.clone()); + assert_eq!(read_from_clipboard(), Some(item)); } } diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml index c8236245e69332..997b167f891dfd 100644 --- a/crates/gpui_macros/Cargo.toml +++ b/crates/gpui_macros/Cargo.toml @@ -14,9 +14,9 @@ proc-macro = true doctest = true [dependencies] -proc-macro2 = "1.0.66" -quote = "1.0.9" -syn = { version = "1.0.72", features = ["full", "extra-traits"] } +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true [dev-dependencies] gpui.workspace = true diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index 204625a05f40ac..5d318d0afadbbe 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -11,6 +11,7 @@ use lsp_log::LogKind; use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; +use util::path; #[gpui::test] async fn test_lsp_logs(cx: &mut TestAppContext) { @@ -22,7 +23,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/the-root", + path!("/the-root"), json!({ "test.rs": "", "package.json": "", @@ -30,7 +31,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { ) .await; - let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(Arc::new(Language::new( @@ -57,7 +58,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { let _rust_buffer = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/the-root/test.rs", cx) + project.open_local_buffer_with_lsp(path!("/the-root/test.rs"), cx) }) .await .unwrap(); diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index ba68cf2be19e05..61167620fca036 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -818,11 +818,12 @@ mod tests { use lsp::CompletionItemLabelDetails; use settings::SettingsStore; use theme::SyntaxTheme; + use util::path; #[gpui::test] async fn test_process_rust_diagnostics() { let mut params = lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/a").unwrap(), + uri: lsp::Url::from_file_path(path!("/a")).unwrap(), version: None, diagnostics: vec![ // no newlines diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 4411e43f1b4458..a9254ac157d56b 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -946,7 +946,7 @@ mod tests { .await { Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"), Err(e) => { - let message = e.to_string(); + let message = e.to_string().replace("\\\\", "/"); assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined"); assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents"); }, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 06c2ab55855495..0f85ed7f5c8593 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -25,10 +25,7 @@ use std::{mem, num::NonZeroU32, ops::Range, task::Poll}; use task::{ResolvedTask, TaskContext}; use unindent::Unindent as _; use util::{ - assert_set_eq, - paths::{replace_path_separator, PathMatcher}, - test::TempTree, - TryFutureExt as _, + assert_set_eq, path, paths::PathMatcher, separator, test::TempTree, uri, TryFutureExt as _, }; #[gpui::test] @@ -37,7 +34,10 @@ async fn test_block_via_channel(cx: &mut gpui::TestAppContext) { let (tx, mut rx) = futures::channel::mpsc::unbounded(); let _thread = std::thread::spawn(move || { + #[cfg(not(target_os = "windows"))] std::fs::metadata("/tmp").unwrap(); + #[cfg(target_os = "windows")] + std::fs::metadata("C:/Windows").unwrap(); std::thread::sleep(Duration::from_millis(1000)); tx.unbounded_send(1).unwrap(); }); @@ -199,7 +199,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/the-root", + path!("/dir"), json!({ ".zed": { "settings.json": r#"{ "tab_size": 8 }"#, @@ -227,7 +227,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ) .await; - let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); let task_context = TaskContext::default(); @@ -280,8 +280,12 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ( TaskSourceKind::Worktree { id: worktree_id, - directory_in_worktree: PathBuf::from("b/.zed"), - id_base: "local worktree tasks from directory \"b/.zed\"".into(), + directory_in_worktree: PathBuf::from(separator!("b/.zed")), + id_base: if cfg!(windows) { + "local worktree tasks from directory \"b\\\\.zed\"".into() + } else { + "local worktree tasks from directory \"b/.zed\"".into() + }, }, "cargo check".to_string(), vec!["check".to_string()], @@ -360,8 +364,12 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ( TaskSourceKind::Worktree { id: worktree_id, - directory_in_worktree: PathBuf::from("b/.zed"), - id_base: "local worktree tasks from directory \"b/.zed\"".into(), + directory_in_worktree: PathBuf::from(separator!("b/.zed")), + id_base: if cfg!(windows) { + "local worktree tasks from directory \"b\\\\.zed\"".into() + } else { + "local worktree tasks from directory \"b/.zed\"".into() + }, }, "cargo check".to_string(), vec!["check".to_string()], @@ -393,7 +401,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/the-root", + path!("/dir"), json!({ "test.rs": "const A: i32 = 1;", "test2.rs": "", @@ -403,7 +411,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let mut fake_rust_servers = language_registry.register_fake_lsp( @@ -450,7 +458,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { // Open a buffer without an associated language server. let (toml_buffer, _handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/the-root/Cargo.toml", cx) + project.open_local_buffer_with_lsp(path!("/dir/Cargo.toml"), cx) }) .await .unwrap(); @@ -458,7 +466,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { // Open a buffer with an associated language server before the language for it has been loaded. let (rust_buffer, _handle2) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/the-root/test.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/test.rs"), cx) }) .await .unwrap(); @@ -483,7 +491,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(), version: 0, text: "const A: i32 = 1;".to_string(), language_id: "rust".to_string(), @@ -513,7 +521,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(), 1 ) ); @@ -521,7 +529,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { // Open a third buffer with a different associated language server. let (json_buffer, _json_handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/the-root/package.json", cx) + project.open_local_buffer_with_lsp(path!("/dir/package.json"), cx) }) .await .unwrap(); @@ -534,7 +542,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(), version: 0, text: "{\"a\": 1}".to_string(), language_id: "json".to_string(), @@ -558,7 +566,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { // it is also configured based on the existing language server's capabilities. let (rust_buffer2, _handle4) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/the-root/test2.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/test2.rs"), cx) }) .await .unwrap(); @@ -584,7 +592,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/test2.rs").unwrap(), + lsp::Url::from_file_path(path!("/dir/test2.rs")).unwrap(), 1 ) ); @@ -599,20 +607,24 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .receive_notification::() .await .text_document, - lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap()) + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(path!("/dir/Cargo.toml")).unwrap() + ) ); assert_eq!( fake_json_server .receive_notification::() .await .text_document, - lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap()) + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(path!("/dir/Cargo.toml")).unwrap() + ) ); // Renames are reported only to servers matching the buffer's language. fs.rename( - Path::new("/the-root/test2.rs"), - Path::new("/the-root/test3.rs"), + Path::new(path!("/dir/test2.rs")), + Path::new(path!("/dir/test3.rs")), Default::default(), ) .await @@ -622,7 +634,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .receive_notification::() .await .text_document, - lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/test2.rs").unwrap()), + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test2.rs")).unwrap()), ); assert_eq!( fake_rust_server @@ -630,7 +642,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test3.rs").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap(), version: 0, text: rust_buffer2.update(cx, |buffer, _| buffer.text()), language_id: "rust".to_string(), @@ -661,8 +673,8 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { // When the rename changes the extension of the file, the buffer gets closed on the old // language server and gets opened on the new one. fs.rename( - Path::new("/the-root/test3.rs"), - Path::new("/the-root/test3.json"), + Path::new(path!("/dir/test3.rs")), + Path::new(path!("/dir/test3.json")), Default::default(), ) .await @@ -672,7 +684,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .receive_notification::() .await .text_document, - lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),), + lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap(),), ); assert_eq!( fake_json_server @@ -680,7 +692,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(), version: 0, text: rust_buffer2.update(cx, |buffer, _| buffer.text()), language_id: "json".to_string(), @@ -706,7 +718,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/test3.json").unwrap(), + lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(), 1 ) ); @@ -735,7 +747,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { .await .text_document, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(), version: 0, text: rust_buffer.update(cx, |buffer, _| buffer.text()), language_id: "rust".to_string(), @@ -756,13 +768,13 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { ], [ lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(), version: 0, text: json_buffer.update(cx, |buffer, _| buffer.text()), language_id: "json".to_string(), }, lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(), version: 0, text: rust_buffer2.update(cx, |buffer, _| buffer.text()), language_id: "json".to_string(), @@ -774,7 +786,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { cx.update(|_| drop(_json_handle)); let close_message = lsp::DidCloseTextDocumentParams { text_document: lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/package.json").unwrap(), + lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(), ), }; assert_eq!( @@ -787,19 +799,11 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { - fn add_root_for_windows(path: &str) -> String { - if cfg!(windows) { - format!("C:{}", path) - } else { - path.to_string() - } - } - init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( - add_root_for_windows("/the-root"), + path!("/the-root"), json!({ ".gitignore": "target\n", "src": { @@ -827,7 +831,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon ) .await; - let project = Project::test(fs.clone(), [add_root_for_windows("/the-root").as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); let mut fake_servers = language_registry.register_fake_lsp( @@ -843,7 +847,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon // Start the language server by opening a buffer with a compatible file extension. let _ = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp(add_root_for_windows("/the-root/src/a.rs"), cx) + project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx) }) .await .unwrap(); @@ -883,21 +887,21 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon lsp::DidChangeWatchedFilesRegistrationOptions { watchers: vec![ lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String(add_root_for_windows( - "/the-root/Cargo.toml", - )), + glob_pattern: lsp::GlobPattern::String( + path!("/the-root/Cargo.toml").to_string(), + ), kind: None, }, lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String(add_root_for_windows( - "/the-root/src/*.{rs,c}", - )), + glob_pattern: lsp::GlobPattern::String( + path!("/the-root/src/*.{rs,c}").to_string(), + ), kind: None, }, lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String(add_root_for_windows( - "/the-root/target/y/**/*.rs", - )), + glob_pattern: lsp::GlobPattern::String( + path!("/the-root/target/y/**/*.rs").to_string(), + ), kind: None, }, ], @@ -950,32 +954,23 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon // Perform some file system mutations, two of which match the watched patterns, // and one of which does not. + fs.create_file(path!("/the-root/src/c.rs").as_ref(), Default::default()) + .await + .unwrap(); + fs.create_file(path!("/the-root/src/d.txt").as_ref(), Default::default()) + .await + .unwrap(); + fs.remove_file(path!("/the-root/src/b.rs").as_ref(), Default::default()) + .await + .unwrap(); fs.create_file( - add_root_for_windows("/the-root/src/c.rs").as_ref(), - Default::default(), - ) - .await - .unwrap(); - fs.create_file( - add_root_for_windows("/the-root/src/d.txt").as_ref(), - Default::default(), - ) - .await - .unwrap(); - fs.remove_file( - add_root_for_windows("/the-root/src/b.rs").as_ref(), - Default::default(), - ) - .await - .unwrap(); - fs.create_file( - add_root_for_windows("/the-root/target/x/out/x2.rs").as_ref(), + path!("/the-root/target/x/out/x2.rs").as_ref(), Default::default(), ) .await .unwrap(); fs.create_file( - add_root_for_windows("/the-root/target/y/out/y2.rs").as_ref(), + path!("/the-root/target/y/out/y2.rs").as_ref(), Default::default(), ) .await @@ -987,16 +982,15 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon &*file_changes.lock(), &[ lsp::FileEvent { - uri: lsp::Url::from_file_path(add_root_for_windows("/the-root/src/b.rs")).unwrap(), + uri: lsp::Url::from_file_path(path!("/the-root/src/b.rs")).unwrap(), typ: lsp::FileChangeType::DELETED, }, lsp::FileEvent { - uri: lsp::Url::from_file_path(add_root_for_windows("/the-root/src/c.rs")).unwrap(), + uri: lsp::Url::from_file_path(path!("/the-root/src/c.rs")).unwrap(), typ: lsp::FileChangeType::CREATED, }, lsp::FileEvent { - uri: lsp::Url::from_file_path(add_root_for_windows("/the-root/target/y/out/y2.rs")) - .unwrap(), + uri: lsp::Url::from_file_path(path!("/the-root/target/y/out/y2.rs")).unwrap(), typ: lsp::FileChangeType::CREATED, }, ] @@ -1009,7 +1003,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": "let a = 1;", "b.rs": "let b = 2;" @@ -1017,15 +1011,24 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await; + let project = Project::test( + fs, + [path!("/dir/a.rs").as_ref(), path!("/dir/b.rs").as_ref()], + cx, + ) + .await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let buffer_a = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); let buffer_b = project - .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/b.rs"), cx) + }) .await .unwrap(); @@ -1034,7 +1037,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), + uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), @@ -1051,7 +1054,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .update_diagnostics( LanguageServerId(0), lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/b.rs").unwrap(), + uri: Url::from_file_path(path!("/dir/b.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), @@ -1102,7 +1105,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/root", + path!("/root"), json!({ "dir": { ".git": { @@ -1117,11 +1120,11 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs, ["/root/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/root/dir").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let (worktree, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/root/dir", true, cx) + project.find_or_create_worktree(path!("/root/dir"), true, cx) }) .await .unwrap(); @@ -1129,7 +1132,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { let (worktree, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/root/other.rs", false, cx) + project.find_or_create_worktree(path!("/root/other.rs"), false, cx) }) .await .unwrap(); @@ -1141,7 +1144,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { .update_diagnostics( server_id, lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/root/dir/b.rs").unwrap(), + uri: Url::from_file_path(path!("/root/dir/b.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), @@ -1158,7 +1161,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { .update_diagnostics( server_id, lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/root/other.rs").unwrap(), + uri: Url::from_file_path(path!("/root/other.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)), @@ -1245,7 +1248,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": "fn a() { A }", "b.rs": "const y: i32 = 1", @@ -1253,7 +1256,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -1271,7 +1274,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { // Cause worktree to start the fake language server let _ = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/b.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/b.rs"), cx) }) .await .unwrap(); @@ -1300,7 +1303,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { ); fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), + uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), @@ -1326,7 +1329,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { ); let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/a.rs"), cx)) .await .unwrap(); @@ -1352,7 +1355,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { // Ensure publishing empty diagnostics twice only results in one update event. fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), + uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: Default::default(), }); @@ -1365,7 +1368,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { ); fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), + uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: Default::default(), }); @@ -1380,9 +1383,9 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC let progress_token = "the-progress-token"; let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/dir", json!({ "a.rs": "" })).await; + fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -1400,7 +1403,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC let (buffer, _handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/a.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx) }) .await .unwrap(); @@ -1466,9 +1469,9 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp init_test(cx); let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/dir", json!({ "a.rs": "x" })).await; + fs.insert_tree(path!("/dir"), json!({ "a.rs": "x" })).await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -1476,7 +1479,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp let (buffer, _) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/a.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx) }) .await .unwrap(); @@ -1484,7 +1487,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp // Publish diagnostics let fake_server = fake_servers.next().await.unwrap(); fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), + uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), @@ -1547,9 +1550,9 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T init_test(cx); let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/dir", json!({ "a.rs": "" })).await; + fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -1557,7 +1560,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T let (buffer, _handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/a.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx) }) .await .unwrap(); @@ -1565,7 +1568,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T // Before restarting the server, report diagnostics with an unknown buffer version. let fake_server = fake_servers.next().await.unwrap(); fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: Some(10000), diagnostics: Vec::new(), }); @@ -1589,9 +1592,9 @@ async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) { let progress_token = "the-progress-token"; let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/dir", json!({ "a.rs": "" })).await; + fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -1607,7 +1610,7 @@ async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) { let (buffer, _handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/a.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx) }) .await .unwrap(); @@ -1652,10 +1655,10 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) + fs.insert_tree(path!("/dir"), json!({ "a.rs": "", "b.js": "" })) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let mut fake_rust_servers = language_registry.register_fake_lsp( @@ -1677,13 +1680,13 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { let _rs_buffer = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/a.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx) }) .await .unwrap(); let _js_buffer = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/b.js", cx) + project.open_local_buffer_with_lsp(path!("/dir/b.js"), cx) }) .await .unwrap(); @@ -1696,7 +1699,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { .text_document .uri .as_str(), - "file:///dir/a.rs" + uri!("file:///dir/a.rs") ); let mut fake_js_server = fake_js_servers.next().await.unwrap(); @@ -1707,7 +1710,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { .text_document .uri .as_str(), - "file:///dir/b.js" + uri!("file:///dir/b.js") ); // Disable Rust language server, ensuring only that server gets stopped. @@ -1758,7 +1761,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { .text_document .uri .as_str(), - "file:///dir/a.rs" + uri!("file:///dir/a.rs") ); fake_js_server .receive_notification::() @@ -1777,9 +1780,9 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { .unindent(); let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/dir", json!({ "a.rs": text })).await; + fs.insert_tree(path!("/dir"), json!({ "a.rs": text })).await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); @@ -1793,7 +1796,9 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { ); let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); @@ -1815,7 +1820,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Report some diagnostics for the initial version of the buffer fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: Some(open_notification.text_document.version), diagnostics: vec![ lsp::Diagnostic { @@ -1901,7 +1906,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Ensure overlapping diagnostics are highlighted correctly. fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: Some(open_notification.text_document.version), diagnostics: vec![ lsp::Diagnostic { @@ -1993,7 +1998,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { // Handle out-of-order diagnostics fake_server.notify::(&lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), + uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), version: Some(change_notification_2.text_document.version), diagnostics: vec![ lsp::Diagnostic { @@ -2199,14 +2204,14 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": text.clone(), }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let language_registry = project.read_with(cx, |project, _| project.languages().clone()); @@ -2215,7 +2220,7 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { let (buffer, _handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/a.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx) }) .await .unwrap(); @@ -2352,17 +2357,19 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": text.clone(), }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); @@ -2461,17 +2468,19 @@ async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": text.clone(), }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) + .update(cx, |project, cx| { + project.open_local_buffer(path!("/dir/a.rs"), cx) + }) .await .unwrap(); @@ -2572,7 +2581,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.rs": "const fn a() { A }", "b.rs": "const y: i32 = crate::a()", @@ -2580,7 +2589,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir/b.rs").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -2588,7 +2597,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { let (buffer, _handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/b.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/b.rs"), cx) }) .await .unwrap(); @@ -2598,13 +2607,13 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { let params = params.text_document_position_params; assert_eq!( params.text_document.uri.to_file_path().unwrap(), - Path::new("/dir/b.rs"), + Path::new(path!("/dir/b.rs")), ); assert_eq!(params.position, lsp::Position::new(0, 22)); Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path("/dir/a.rs").unwrap(), + lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(), lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), ), ))) @@ -2630,18 +2639,24 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { .as_local() .unwrap() .abs_path(cx), - Path::new("/dir/a.rs"), + Path::new(path!("/dir/a.rs")), ); assert_eq!(definition.target.range.to_offset(target_buffer), 9..10); assert_eq!( list_worktrees(&project, cx), - [("/dir/a.rs".as_ref(), false), ("/dir/b.rs".as_ref(), true)], + [ + (path!("/dir/a.rs").as_ref(), false), + (path!("/dir/b.rs").as_ref(), true) + ], ); drop(definition); }); cx.update(|cx| { - assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]); + assert_eq!( + list_worktrees(&project, cx), + [(path!("/dir/b.rs").as_ref(), true)] + ); }); fn list_worktrees<'a>(project: &'a Entity, cx: &'a App) -> Vec<(&'a Path, bool)> { @@ -2665,14 +2680,14 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.ts": "", }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(typescript_lang()); @@ -2691,7 +2706,9 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { ); let (buffer, _handle) = project - .update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx)) + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) .await .unwrap(); @@ -2757,14 +2774,14 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.ts": "", }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(typescript_lang()); @@ -2783,7 +2800,9 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { ); let (buffer, _handle) = project - .update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx)) + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) .await .unwrap(); @@ -2818,14 +2837,14 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.ts": "a", }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(typescript_lang()); @@ -2846,7 +2865,9 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { ); let (buffer, _handle) = project - .update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx)) + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) .await .unwrap(); @@ -2911,7 +2932,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { edit: lsp::WorkspaceEdit { changes: Some( [( - lsp::Url::from_file_path("/dir/a.ts").unwrap(), + lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(), vec![lsp::TextEdit { range: lsp::Range::new( lsp::Position::new(0, 0), @@ -2953,16 +2974,16 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "file1": "the old contents", }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx)) .await .unwrap(); buffer.update(cx, |buffer, cx| { @@ -2975,7 +2996,11 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); + let new_text = fs + .load(Path::new(path!("/dir/file1"))) + .await + .unwrap() + .replace("\r\n", "\n"); assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text())); } @@ -2985,17 +3010,17 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "file1": "the original contents", }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx)) .await .unwrap(); @@ -3006,7 +3031,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) // Change the buffer's file on disk, and then wait for the file change // to be detected by the worktree, so that the buffer starts reloading. fs.save( - "/dir/file1".as_ref(), + path!("/dir/file1").as_ref(), &"the first contents".into(), Default::default(), ) @@ -3017,7 +3042,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) // Change the buffer's file again. Depending on the random seed, the // previous file change may still be in progress. fs.save( - "/dir/file1".as_ref(), + path!("/dir/file1").as_ref(), &"the second contents".into(), Default::default(), ) @@ -3026,7 +3051,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) worktree.next_event(cx).await; cx.executor().run_until_parked(); - let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap(); + let on_disk_text = fs.load(Path::new(path!("/dir/file1"))).await.unwrap(); buffer.read_with(cx, |buffer, _| { assert_eq!(buffer.text(), on_disk_text); assert!(!buffer.is_dirty(), "buffer should not be dirty"); @@ -3040,17 +3065,17 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "file1": "the original contents", }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx)) .await .unwrap(); @@ -3061,7 +3086,7 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) { // Change the buffer's file on disk, and then wait for the file change // to be detected by the worktree, so that the buffer starts reloading. fs.save( - "/dir/file1".as_ref(), + path!("/dir/file1").as_ref(), &"the first contents".into(), Default::default(), ) @@ -3080,7 +3105,7 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) { }); cx.executor().run_until_parked(); - let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap(); + let on_disk_text = fs.load(Path::new(path!("/dir/file1"))).await.unwrap(); buffer.read_with(cx, |buffer, _| { let buffer_text = buffer.text(); if buffer_text == on_disk_text { @@ -3104,16 +3129,16 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "file1": "the old contents", }), ) .await; - let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir/file1").as_ref()], cx).await; let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx)) .await .unwrap(); buffer.update(cx, |buffer, cx| { @@ -3125,7 +3150,11 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { .await .unwrap(); - let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); + let new_text = fs + .load(Path::new(path!("/dir/file1"))) + .await + .unwrap() + .replace("\r\n", "\n"); assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text())); } @@ -3260,26 +3289,21 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); tree.flush_fs_events(cx).await; - let expected_paths = vec![ - "a", - "a/file1", - "a/file2.new", - "b", - "d", - "d/file3", - "d/file4", - ] - .into_iter() - .map(replace_path_separator) - .collect::>(); - cx.update(|app| { assert_eq!( tree.read(app) .paths() .map(|p| p.to_str().unwrap()) .collect::>(), - expected_paths + vec![ + "a", + separator!("a/file1"), + separator!("a/file2.new"), + "b", + "d", + separator!("d/file3"), + separator!("d/file4"), + ] ); }); @@ -3339,7 +3363,15 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { .paths() .map(|p| p.to_str().unwrap()) .collect::>(), - expected_paths + vec![ + "a", + separator!("a/file1"), + separator!("a/file2.new"), + "b", + "d", + separator!("d/file3"), + separator!("d/file4"), + ] ); }); } @@ -3448,7 +3480,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "file1": "abc", "file2": "def", @@ -3457,10 +3489,10 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let buffer1 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx)) .await .unwrap(); let events = Arc::new(Mutex::new(Vec::new())); @@ -3543,7 +3575,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { // When a file is deleted, the buffer is considered dirty. let events = Arc::new(Mutex::new(Vec::new())); let buffer2 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx)) .await .unwrap(); buffer2.update(cx, |_, cx| { @@ -3554,7 +3586,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { .detach(); }); - fs.remove_file("/dir/file2".as_ref(), Default::default()) + fs.remove_file(path!("/dir/file2").as_ref(), Default::default()) .await .unwrap(); cx.executor().run_until_parked(); @@ -3570,7 +3602,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { // When a file is already dirty when deleted, we don't emit a Dirtied event. let events = Arc::new(Mutex::new(Vec::new())); let buffer3 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file3"), cx)) .await .unwrap(); buffer3.update(cx, |_, cx| { @@ -3585,7 +3617,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { buffer.edit([(0..0, "x")], None, cx); }); events.lock().clear(); - fs.remove_file("/dir/file3".as_ref(), Default::default()) + fs.remove_file(path!("/dir/file3").as_ref(), Default::default()) .await .unwrap(); cx.executor().run_until_parked(); @@ -3600,15 +3632,15 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { let initial_contents = "aaa\nbbbbb\nc\n"; let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "the-file": initial_contents, }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/the-file"), cx)) .await .unwrap(); @@ -3624,7 +3656,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { }); let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; fs.save( - "/dir/the-file".as_ref(), + path!("/dir/the-file").as_ref(), &new_contents.into(), LineEnding::Unix, ) @@ -3659,7 +3691,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { // Change the file on disk again, adding blank lines to the beginning. fs.save( - "/dir/the-file".as_ref(), + path!("/dir/the-file").as_ref(), &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), LineEnding::Unix, ) @@ -3680,7 +3712,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "file1": "a\nb\nc\n", "file2": "one\r\ntwo\r\nthree\r\n", @@ -3688,13 +3720,13 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let buffer1 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx)) .await .unwrap(); let buffer2 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx)) .await .unwrap(); @@ -3710,7 +3742,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { // Change a file's line endings on disk from unix to windows. The buffer's // state updates correctly. fs.save( - "/dir/file1".as_ref(), + path!("/dir/file1").as_ref(), &"aaa\nb\nc\n".into(), LineEnding::Windows, ) @@ -3731,7 +3763,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { .await .unwrap(); assert_eq!( - fs.load("/dir/file2".as_ref()).await.unwrap(), + fs.load(path!("/dir/file2").as_ref()).await.unwrap(), "one\r\ntwo\r\nthree\r\nfour\r\n", ); } @@ -3742,7 +3774,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/the-dir", + path!("/dir"), json!({ "a.rs": " fn foo(mut v: Vec) { @@ -3756,14 +3788,14 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx)) + .update(cx, |p, cx| p.open_local_buffer(path!("/dir/a.rs"), cx)) .await .unwrap(); - let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap(); + let buffer_uri = Url::from_file_path(path!("/dir/a.rs")).unwrap(); let message = lsp::PublishDiagnosticsParams { uri: buffer_uri.clone(), diagnostics: vec![ @@ -3985,7 +4017,7 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": "const ONE: usize = 1;", "two": { @@ -3995,7 +4027,7 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -4039,7 +4071,7 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { let _ = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/one.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/one.rs"), cx) }) .await .unwrap(); @@ -4068,7 +4100,7 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { new_text: "This is not a drill".to_owned(), })], text_document: lsp::OptionalVersionedTextDocumentIdentifier { - uri: Url::from_str("file:///dir/two/two.rs").unwrap(), + uri: Url::from_str(uri!("file:///dir/two/two.rs")).unwrap(), version: Some(1337), }, }] @@ -4085,8 +4117,8 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { let expected_edit = expected_edit.clone(); async move { assert_eq!(params.files.len(), 1); - assert_eq!(params.files[0].old_uri, "file:///dir/one.rs"); - assert_eq!(params.files[0].new_uri, "file:///dir/three.rs"); + assert_eq!(params.files[0].old_uri, uri!("file:///dir/one.rs")); + assert_eq!(params.files[0].new_uri, uri!("file:///dir/three.rs")); resolved_workspace_edit.set(expected_edit.clone()).unwrap(); Ok(Some(expected_edit)) } @@ -4099,8 +4131,8 @@ async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { fake_server .handle_notification::(|params, _| { assert_eq!(params.files.len(), 1); - assert_eq!(params.files[0].old_uri, "file:///dir/one.rs"); - assert_eq!(params.files[0].new_uri, "file:///dir/three.rs"); + assert_eq!(params.files[0].old_uri, uri!("file:///dir/one.rs")); + assert_eq!(params.files[0].new_uri, uri!("file:///dir/three.rs")); }) .next() .await @@ -4115,7 +4147,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": "const ONE: usize = 1;", "two.rs": "const TWO: usize = one::ONE + one::ONE;" @@ -4123,7 +4155,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -4143,7 +4175,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { let (buffer, _handle) = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/one.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/one.rs"), cx) }) .await .unwrap(); @@ -4155,7 +4187,10 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { }); fake_server .handle_request::(|params, _| async move { - assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!( + params.text_document.uri.as_str(), + uri!("file:///dir/one.rs") + ); assert_eq!(params.position, lsp::Position::new(0, 7)); Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( lsp::Position::new(0, 6), @@ -4179,7 +4214,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { .handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), - "file:///dir/one.rs" + uri!("file:///dir/one.rs") ); assert_eq!( params.text_document_position.position, @@ -4190,14 +4225,14 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { changes: Some( [ ( - lsp::Url::from_file_path("/dir/one.rs").unwrap(), + lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), "THREE".to_string(), )], ), ( - lsp::Url::from_file_path("/dir/two.rs").unwrap(), + lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(), vec![ lsp::TextEdit::new( lsp::Range::new( @@ -4251,7 +4286,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": "const ONE: usize = 1;", "two.rs": "const TWO: usize = one::ONE + one::ONE;", @@ -4260,7 +4295,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) { }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; assert_eq!( search( &project, @@ -4279,14 +4314,14 @@ async fn test_search(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/two.rs".to_string(), vec![6..9]), - ("dir/three.rs".to_string(), vec![37..40]) + (separator!("dir/two.rs").to_string(), vec![6..9]), + (separator!("dir/three.rs").to_string(), vec![37..40]) ]) ); let buffer_4 = project .update(cx, |project, cx| { - project.open_local_buffer("/dir/four.rs", cx) + project.open_local_buffer(path!("/dir/four.rs"), cx) }) .await .unwrap(); @@ -4313,9 +4348,9 @@ async fn test_search(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/two.rs".to_string(), vec![6..9]), - ("dir/three.rs".to_string(), vec![37..40]), - ("dir/four.rs".to_string(), vec![25..28, 36..39]) + (separator!("dir/two.rs").to_string(), vec![6..9]), + (separator!("dir/three.rs").to_string(), vec![37..40]), + (separator!("dir/four.rs").to_string(), vec![25..28, 36..39]) ]) ); } @@ -4328,7 +4363,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": r#"// Rust file one"#, "one.ts": r#"// TypeScript file one"#, @@ -4337,7 +4372,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; assert!( search( @@ -4378,8 +4413,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/one.rs".to_string(), vec![8..12]), - ("dir/two.rs".to_string(), vec![8..12]), + (separator!("dir/one.rs").to_string(), vec![8..12]), + (separator!("dir/two.rs").to_string(), vec![8..12]), ]), "Rust only search should give only Rust files" ); @@ -4403,8 +4438,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/one.ts".to_string(), vec![14..18]), - ("dir/two.ts".to_string(), vec![14..18]), + (separator!("dir/one.ts").to_string(), vec![14..18]), + (separator!("dir/two.ts").to_string(), vec![14..18]), ]), "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything" ); @@ -4428,10 +4463,10 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/two.ts".to_string(), vec![14..18]), - ("dir/one.rs".to_string(), vec![8..12]), - ("dir/one.ts".to_string(), vec![14..18]), - ("dir/two.rs".to_string(), vec![8..12]), + (separator!("dir/two.ts").to_string(), vec![14..18]), + (separator!("dir/one.rs").to_string(), vec![8..12]), + (separator!("dir/one.ts").to_string(), vec![14..18]), + (separator!("dir/two.rs").to_string(), vec![8..12]), ]), "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything" ); @@ -4445,7 +4480,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": r#"// Rust file one"#, "one.ts": r#"// TypeScript file one"#, @@ -4454,7 +4489,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; assert_eq!( search( @@ -4474,10 +4509,10 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/one.rs".to_string(), vec![8..12]), - ("dir/one.ts".to_string(), vec![14..18]), - ("dir/two.rs".to_string(), vec![8..12]), - ("dir/two.ts".to_string(), vec![14..18]), + (separator!("dir/one.rs").to_string(), vec![8..12]), + (separator!("dir/one.ts").to_string(), vec![14..18]), + (separator!("dir/two.rs").to_string(), vec![8..12]), + (separator!("dir/two.ts").to_string(), vec![14..18]), ]), "If no exclusions match, all files should be returned" ); @@ -4500,8 +4535,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/one.ts".to_string(), vec![14..18]), - ("dir/two.ts".to_string(), vec![14..18]), + (separator!("dir/one.ts").to_string(), vec![14..18]), + (separator!("dir/two.ts").to_string(), vec![14..18]), ]), "Rust exclusion search should give only TypeScript files" ); @@ -4523,8 +4558,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/one.rs".to_string(), vec![8..12]), - ("dir/two.rs".to_string(), vec![8..12]), + (separator!("dir/one.rs").to_string(), vec![8..12]), + (separator!("dir/two.rs").to_string(), vec![8..12]), ]), "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything" ); @@ -4559,7 +4594,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": r#"// Rust file one"#, "one.ts": r#"// TypeScript file one"#, @@ -4568,7 +4603,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; assert!( search( @@ -4650,8 +4685,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex .await .unwrap(), HashMap::from_iter([ - ("dir/one.ts".to_string(), vec![14..18]), - ("dir/two.ts".to_string(), vec![14..18]), + (separator!("dir/one.ts").to_string(), vec![14..18]), + (separator!("dir/two.ts").to_string(), vec![14..18]), ]), "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files" ); @@ -4663,7 +4698,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/worktree-a", + path!("/worktree-a"), json!({ "haystack.rs": r#"// NEEDLE"#, "haystack.ts": r#"// NEEDLE"#, @@ -4671,7 +4706,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo ) .await; fs.insert_tree( - "/worktree-b", + path!("/worktree-b"), json!({ "haystack.rs": r#"// NEEDLE"#, "haystack.ts": r#"// NEEDLE"#, @@ -4681,7 +4716,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo let project = Project::test( fs.clone(), - ["/worktree-a".as_ref(), "/worktree-b".as_ref()], + [path!("/worktree-a").as_ref(), path!("/worktree-b").as_ref()], cx, ) .await; @@ -4703,7 +4738,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo ) .await .unwrap(), - HashMap::from_iter([("worktree-a/haystack.rs".to_string(), vec![3..9])]), + HashMap::from_iter([(separator!("worktree-a/haystack.rs").to_string(), vec![3..9])]), "should only return results from included worktree" ); assert_eq!( @@ -4723,7 +4758,7 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo ) .await .unwrap(), - HashMap::from_iter([("worktree-b/haystack.rs".to_string(), vec![3..9])]), + HashMap::from_iter([(separator!("worktree-b/haystack.rs").to_string(), vec![3..9])]), "should only return results from included worktree" ); @@ -4745,8 +4780,8 @@ async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppCo .await .unwrap(), HashMap::from_iter([ - ("worktree-a/haystack.ts".to_string(), vec![3..9]), - ("worktree-b/haystack.ts".to_string(), vec![3..9]) + (separator!("worktree-a/haystack.ts").to_string(), vec![3..9]), + (separator!("worktree-b/haystack.ts").to_string(), vec![3..9]) ]), "should return results from both worktrees" ); @@ -4758,7 +4793,7 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ ".git": {}, ".gitignore": "**/target\n/node_modules\n", @@ -4779,7 +4814,7 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let query = "key"; assert_eq!( @@ -4799,11 +4834,11 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { ) .await .unwrap(), - HashMap::from_iter([("dir/package.json".to_string(), vec![8..11])]), + HashMap::from_iter([(separator!("dir/package.json").to_string(), vec![8..11])]), "Only one non-ignored file should have the query" ); - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; assert_eq!( search( &project, @@ -4822,19 +4857,22 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([ - ("dir/package.json".to_string(), vec![8..11]), - ("dir/target/index.txt".to_string(), vec![6..9]), + (separator!("dir/package.json").to_string(), vec![8..11]), + (separator!("dir/target/index.txt").to_string(), vec![6..9]), ( - "dir/node_modules/prettier/package.json".to_string(), + separator!("dir/node_modules/prettier/package.json").to_string(), vec![9..12] ), ( - "dir/node_modules/prettier/index.ts".to_string(), + separator!("dir/node_modules/prettier/index.ts").to_string(), vec![15..18] ), - ("dir/node_modules/eslint/index.ts".to_string(), vec![13..16]), ( - "dir/node_modules/eslint/package.json".to_string(), + separator!("dir/node_modules/eslint/index.ts").to_string(), + vec![13..16] + ), + ( + separator!("dir/node_modules/eslint/package.json").to_string(), vec![8..11] ), ]), @@ -4843,7 +4881,7 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { let files_to_include = PathMatcher::new(&["node_modules/prettier/**".to_owned()]).unwrap(); let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()]).unwrap(); - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; assert_eq!( search( &project, @@ -4862,7 +4900,7 @@ async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { .await .unwrap(), HashMap::from_iter([( - "dir/node_modules/prettier/package.json".to_string(), + separator!("dir/node_modules/prettier/package.json").to_string(), vec![9..12] )]), "With search including ignored prettier directory and excluding TS files, only one file should be found" @@ -4945,14 +4983,14 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.tsx": "a", }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(tsx_lang()); @@ -5010,7 +5048,9 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) { ]; let (buffer, _handle) = project - .update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.tsx", cx)) + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx) + }) .await .unwrap(); cx.executor().run_until_parked(); @@ -5096,14 +5136,14 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.ts": "a", }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(typescript_lang()); @@ -5119,7 +5159,9 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) { ); let (buffer, _handle) = project - .update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx)) + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) .await .unwrap(); cx.executor().run_until_parked(); @@ -5166,14 +5208,14 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.ts": "a", }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(typescript_lang()); @@ -5189,7 +5231,9 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) { ); let (buffer, _handle) = project - .update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.ts", cx)) + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) .await .unwrap(); cx.executor().run_until_parked(); @@ -5244,14 +5288,14 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a.tsx": "a", }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(tsx_lang()); @@ -5310,7 +5354,9 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { ]; let (buffer, _handle) = project - .update(cx, |p, cx| p.open_local_buffer_with_lsp("/dir/a.tsx", cx)) + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx) + }) .await .unwrap(); cx.executor().run_until_parked(); diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index e7d889f0eb4452..a0c494ab35e203 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -19,7 +19,7 @@ use task::{ TaskVariables, VariableName, }; use text::{Point, ToPoint}; -use util::{post_inc, NumericPrefixWithSuffix, ResultExt as _}; +use util::{paths::PathExt as _, post_inc, NumericPrefixWithSuffix, ResultExt as _}; use worktree::WorktreeId; use crate::worktree_store::WorktreeStore; @@ -483,7 +483,7 @@ impl ContextProvider for BasicContextProvider { let current_file = buffer .file() .and_then(|file| file.as_local()) - .map(|file| file.abs_path(cx).to_string_lossy().to_string()); + .map(|file| file.abs_path(cx).to_sanitized_string()); let Point { row, column } = location.range.start.to_point(&buffer_snapshot); let row = row + 1; let column = column + 1; @@ -515,14 +515,14 @@ impl ContextProvider for BasicContextProvider { if let Some(Some(worktree_path)) = worktree_root_dir { task_variables.insert( VariableName::WorktreeRoot, - worktree_path.to_string_lossy().to_string(), + worktree_path.to_sanitized_string(), ); if let Some(full_path) = current_file.as_ref() { let relative_path = pathdiff::diff_paths(full_path, worktree_path); if let Some(relative_path) = relative_path { task_variables.insert( VariableName::RelativeFile, - relative_path.to_string_lossy().into_owned(), + relative_path.to_sanitized_string(), ); } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 369a6c5fcbadda..e244302c4da938 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1106,8 +1106,13 @@ impl ProjectPanel { let worktree_id = edit_state.worktree_id; let is_new_entry = edit_state.is_new_entry(); let filename = self.filename_editor.read(cx).text(cx); - edit_state.is_dir = edit_state.is_dir - || (edit_state.is_new_entry() && filename.ends_with(std::path::MAIN_SEPARATOR)); + #[cfg(not(target_os = "windows"))] + let filename_indicates_dir = filename.ends_with("/"); + // On Windows, path separator could be either `/` or `\`. + #[cfg(target_os = "windows")] + let filename_indicates_dir = filename.ends_with("/") || filename.ends_with("\\"); + edit_state.is_dir = + edit_state.is_dir || (edit_state.is_new_entry() && filename_indicates_dir); let is_dir = edit_state.is_dir; let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone(); @@ -4809,6 +4814,7 @@ mod tests { use serde_json::json; use settings::SettingsStore; use std::path::{Path, PathBuf}; + use util::{path, separator}; use workspace::{ item::{Item, ProjectItem}, register_project_item, AppState, @@ -4910,7 +4916,7 @@ mod tests { let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/src", + path!("/src"), json!({ "test": { "first.rs": "// First Rust file", @@ -4921,7 +4927,7 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -5082,7 +5088,7 @@ mod tests { let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/root1", + path!("/root1"), json!({ "dir_1": { "nested_dir_1": { @@ -5104,7 +5110,7 @@ mod tests { ) .await; fs.insert_tree( - "/root2", + path!("/root2"), json!({ "dir_2": { "file_1.java": "// File contents", @@ -5113,7 +5119,12 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let project = Project::test( + fs.clone(), + [path!("/root1").as_ref(), path!("/root2").as_ref()], + cx, + ) + .await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -5131,10 +5142,10 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ - "v root1", - " > dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - "v root2", - " > dir_2", + separator!("v root1"), + separator!(" > dir_1/nested_dir_1/nested_dir_2/nested_dir_3"), + separator!("v root2"), + separator!(" > dir_2"), ] ); @@ -5146,14 +5157,14 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected", - " > nested_dir_4/nested_dir_5", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " > dir_2", + separator!("v root1"), + separator!(" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected"), + separator!(" > nested_dir_4/nested_dir_5"), + separator!(" file_a.java"), + separator!(" file_b.java"), + separator!(" file_c.java"), + separator!("v root2"), + separator!(" > dir_2"), ] ); @@ -5165,31 +5176,31 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - " v nested_dir_4/nested_dir_5 <== selected", - " file_d.java", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " > dir_2", + separator!("v root1"), + separator!(" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"), + separator!(" v nested_dir_4/nested_dir_5 <== selected"), + separator!(" file_d.java"), + separator!(" file_a.java"), + separator!(" file_b.java"), + separator!(" file_c.java"), + separator!("v root2"), + separator!(" > dir_2"), ] ); toggle_expand_dir(&panel, "root2/dir_2", cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - " v nested_dir_4/nested_dir_5", - " file_d.java", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " v dir_2 <== selected", - " file_1.java", + separator!("v root1"), + separator!(" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"), + separator!(" v nested_dir_4/nested_dir_5"), + separator!(" file_d.java"), + separator!(" file_a.java"), + separator!(" file_b.java"), + separator!(" file_c.java"), + separator!("v root2"), + separator!(" v dir_2 <== selected"), + separator!(" file_1.java"), ] ); } @@ -5698,7 +5709,7 @@ mod tests { let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/root1", + path!("/root1"), json!({ ".dockerignore": "", ".git": { @@ -5708,7 +5719,7 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -5743,9 +5754,10 @@ mod tests { ); let confirm = panel.update_in(cx, |panel, window, cx| { + // If we want to create a subdirectory, there should be no prefix slash. panel .filename_editor - .update(cx, |editor, cx| editor.set_text("/new_dir/", window, cx)); + .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx)); panel.confirm_edit(window, cx).unwrap() }); @@ -5754,14 +5766,14 @@ mod tests { &[ "v root1", " > .git", - " [PROCESSING: '/new_dir/'] <== selected", + " [PROCESSING: 'new_dir/'] <== selected", " .dockerignore", ] ); confirm.await.unwrap(); assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), + visible_entries_as_strings(&panel, 0..10, cx), &[ "v root1", " > .git", @@ -5769,6 +5781,54 @@ mod tests { " .dockerignore", ] ); + + // Test filename with whitespace + select_path(&panel, "root1", cx); + panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); + let confirm = panel.update_in(cx, |panel, window, cx| { + // If we want to create a subdirectory, there should be no prefix slash. + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx)); + panel.confirm_edit(window, cx).unwrap() + }); + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " v new dir 2 <== selected", + " v new_dir", + " .dockerignore", + ] + ); + + // Test filename ends with "\" + #[cfg(target_os = "windows")] + { + select_path(&panel, "root1", cx); + panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); + let confirm = panel.update_in(cx, |panel, window, cx| { + // If we want to create a subdirectory, there should be no prefix slash. + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx)); + panel.confirm_edit(window, cx).unwrap() + }); + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " v new dir 2", + " v new_dir", + " v new_dir_3 <== selected", + " .dockerignore", + ] + ); + } } #[gpui::test] @@ -6425,7 +6485,7 @@ mod tests { let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/src", + path!("/src"), json!({ "test": { "first.rs": "// First Rust file", @@ -6436,7 +6496,7 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -8561,7 +8621,7 @@ mod tests { let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ ".gitignore": "**/ignored_dir\n**/ignored_nested", "dir1": { @@ -8589,7 +8649,7 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -8618,12 +8678,12 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1 <== selected", - " > empty1/empty2/empty3", - " > ignored_dir", - " > subdir1", - " .gitignore", + separator!("v root"), + separator!(" v dir1 <== selected"), + separator!(" > empty1/empty2/empty3"), + separator!(" > ignored_dir"), + separator!(" > subdir1"), + separator!(" .gitignore"), ], "Should show first level with auto-folded dirs and ignored dir visible" ); @@ -8640,18 +8700,18 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " > ignored_dir", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", + separator!("v root"), + separator!(" v dir1 <== selected"), + separator!(" v empty1"), + separator!(" v empty2"), + separator!(" v empty3"), + separator!(" file.txt"), + separator!(" > ignored_dir"), + separator!(" v subdir1"), + separator!(" > ignored_nested"), + separator!(" file1.txt"), + separator!(" file2.txt"), + separator!(" .gitignore"), ], "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested" ); @@ -8676,12 +8736,12 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1 <== selected", - " > empty1", - " > ignored_dir", - " > subdir1", - " .gitignore", + separator!("v root"), + separator!(" v dir1 <== selected"), + separator!(" > empty1"), + separator!(" > ignored_dir"), + separator!(" > subdir1"), + separator!(" .gitignore"), ], "With auto-fold disabled: should show all directories separately" ); @@ -8698,18 +8758,18 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " > ignored_dir", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", + separator!("v root"), + separator!(" v dir1 <== selected"), + separator!(" v empty1"), + separator!(" v empty2"), + separator!(" v empty3"), + separator!(" file.txt"), + separator!(" > ignored_dir"), + separator!(" v subdir1"), + separator!(" > ignored_nested"), + separator!(" file1.txt"), + separator!(" file2.txt"), + separator!(" .gitignore"), ], "After expand_all without auto-fold: should expand all dirs normally, \ expand ignored_dir itself but not its subdirs, and not expand ignored_nested" @@ -8728,20 +8788,20 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " v ignored_dir", - " v subdir", - " deep_file.txt", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", + separator!("v root"), + separator!(" v dir1 <== selected"), + separator!(" v empty1"), + separator!(" v empty2"), + separator!(" v empty3"), + separator!(" file.txt"), + separator!(" v ignored_dir"), + separator!(" v subdir"), + separator!(" deep_file.txt"), + separator!(" v subdir1"), + separator!(" > ignored_nested"), + separator!(" file1.txt"), + separator!(" file2.txt"), + separator!(" .gitignore"), ], "After expand_all on ignored_dir: should expand all contents of the ignored directory" ); @@ -8753,7 +8813,7 @@ mod tests { let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( - "/root", + path!("/root"), json!({ "dir1": { "subdir1": { @@ -8775,7 +8835,7 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*workspace, cx); @@ -8792,15 +8852,15 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1", - " v subdir1", - " v nested1", - " file1.txt", - " file2.txt", - " v subdir2 <== selected", - " file4.txt", - " > dir2", + separator!("v root"), + separator!(" v dir1"), + separator!(" v subdir1"), + separator!(" v nested1"), + separator!(" file1.txt"), + separator!(" file2.txt"), + separator!(" v subdir2 <== selected"), + separator!(" file4.txt"), + separator!(" > dir2"), ], "Initial state with everything expanded" ); @@ -8842,13 +8902,13 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1", - " v subdir1/nested1 <== selected", - " file1.txt", - " file2.txt", - " > subdir2", - " > dir2/single_file", + separator!("v root"), + separator!(" v dir1"), + separator!(" v subdir1/nested1 <== selected"), + separator!(" file1.txt"), + separator!(" file2.txt"), + separator!(" > subdir2"), + separator!(" > dir2/single_file"), ], "Initial state with some dirs expanded" ); @@ -8865,11 +8925,11 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1 <== selected", - " > subdir1/nested1", - " > subdir2", - " > dir2/single_file", + separator!("v root"), + separator!(" v dir1 <== selected"), + separator!(" > subdir1/nested1"), + separator!(" > subdir2"), + separator!(" > dir2/single_file"), ], "Subdirs should be collapsed and folded with auto-fold enabled" ); @@ -8897,14 +8957,14 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1", - " v subdir1", - " v nested1 <== selected", - " file1.txt", - " file2.txt", - " > subdir2", - " > dir2", + separator!("v root"), + separator!(" v dir1"), + separator!(" v subdir1"), + separator!(" v nested1 <== selected"), + separator!(" file1.txt"), + separator!(" file2.txt"), + separator!(" > subdir2"), + separator!(" > dir2"), ], "Initial state with some dirs expanded and auto-fold disabled" ); @@ -8921,11 +8981,11 @@ mod tests { assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ - "v root", - " v dir1 <== selected", - " > subdir1", - " > subdir2", - " > dir2", + separator!("v root"), + separator!(" v dir1 <== selected"), + separator!(" > subdir1"), + separator!(" > subdir2"), + separator!(" > dir2"), ], "Subdirs should be collapsed but not folded with auto-fold disabled" ); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 2b2462ef2556f4..7ae87aeff2a8fc 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -272,15 +272,17 @@ mod tests { use serde_json::json; use settings::SettingsStore; use std::{path::Path, sync::Arc}; + use util::path; #[gpui::test] async fn test_project_symbols(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/dir", json!({ "test.rs": "" })).await; + fs.insert_tree(path!("/dir"), json!({ "test.rs": "" })) + .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(Arc::new(Language::new( @@ -299,7 +301,7 @@ mod tests { let _buffer = project .update(cx, |project, cx| { - project.open_local_buffer_with_lsp("/dir/test.rs", cx) + project.open_local_buffer_with_lsp(path!("/dir/test.rs"), cx) }) .await .unwrap(); @@ -307,9 +309,9 @@ mod tests { // Set up fake language server to return fuzzy matches against // a fixed set of symbol names. let fake_symbols = [ - symbol("one", "/external"), - symbol("ton", "/dir/test.rs"), - symbol("uno", "/dir/test.rs"), + symbol("one", path!("/external")), + symbol("ton", path!("/dir/test.rs")), + symbol("uno", path!("/dir/test.rs")), ]; let fake_server = fake_servers.next().await.unwrap(); fake_server.handle_request::( diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 065dfdc4c59b7d..9ab26159a5b29b 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -596,6 +596,7 @@ mod tests { use project::{project_settings::ProjectSettings, Project}; use serde_json::json; use settings::SettingsStore; + use util::path; use workspace::{open_paths, AppState}; use super::*; @@ -616,7 +617,7 @@ mod tests { .fs .as_fake() .insert_tree( - "/dir", + path!("/dir"), json!({ "main.ts": "a" }), @@ -624,7 +625,7 @@ mod tests { .await; cx.update(|cx| { open_paths( - &[PathBuf::from("/dir/main.ts")], + &[PathBuf::from(path!("/dir/main.ts"))], app_state, workspace::OpenOptions::default(), cx, diff --git a/crates/refineable/derive_refineable/Cargo.toml b/crates/refineable/derive_refineable/Cargo.toml index 62669c610c36ed..8ec8de31fd2fb2 100644 --- a/crates/refineable/derive_refineable/Cargo.toml +++ b/crates/refineable/derive_refineable/Cargo.toml @@ -14,6 +14,6 @@ proc-macro = true doctest = false [dependencies] -syn = "1.0.72" -quote = "1.0.9" -proc-macro2 = "1.0.66" +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index be18bad293829e..b39df8edce3f8c 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1,3 +1,6 @@ +/// todo(windows) +/// The tests in this file assume that server_cx is running on Windows too. +/// We neead to find a way to test Windows-Non-Windows interactions. use crate::headless_project::HeadlessProject; use client::{Client, UserStore}; use clock::FakeSystemClock; @@ -24,12 +27,13 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use util::{path, separator}; #[gpui::test] async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -45,14 +49,14 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test ) .await; fs.set_index_for_repo( - Path::new("/code/project1/.git"), + Path::new(path!("/code/project1/.git")), &[("src/lib.rs".into(), "fn one() -> usize { 0 }".into())], ); let (project, _headless) = init_test(&fs, cx, server_cx).await; let (worktree, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) + project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); @@ -113,7 +117,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test // A new file is created in the remote filesystem. The user // sees the new file. fs.save( - "/code/project1/src/main.rs".as_ref(), + path!("/code/project1/src/main.rs").as_ref(), &"fn main() {}".into(), Default::default(), ) @@ -134,8 +138,8 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test // A file that is currently open in a buffer is renamed. fs.rename( - "/code/project1/src/lib.rs".as_ref(), - "/code/project1/src/lib2.rs".as_ref(), + path!("/code/project1/src/lib.rs").as_ref(), + path!("/code/project1/src/lib2.rs").as_ref(), Default::default(), ) .await @@ -146,7 +150,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test }); fs.set_index_for_repo( - Path::new("/code/project1/.git"), + Path::new(path!("/code/project1/.git")), &[("src/lib2.rs".into(), "fn one() -> usize { 100 }".into())], ); cx.executor().run_until_parked(); @@ -162,7 +166,7 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -179,7 +183,7 @@ async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut Tes project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) + project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); @@ -210,7 +214,7 @@ async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut Tes buffer.update(&mut cx, |buffer, cx| { assert_eq!( buffer.file().unwrap().full_path(cx).to_string_lossy(), - "project1/README.md" + separator!("project1/README.md") ) }); @@ -368,7 +372,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -384,7 +388,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext let (project, headless) = init_test(&fs, cx, server_cx).await; fs.insert_tree( - "/code/project1/.zed", + path!("/code/project1/.zed"), json!({ "settings.json": r#" { @@ -431,7 +435,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext let worktree_id = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) + project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap() @@ -512,7 +516,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext Ok(Some(lsp::WorkspaceEdit { changes: Some( [( - lsp::Url::from_file_path("/code/project1/src/lib.rs").unwrap(), + lsp::Url::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(), vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)), "two".to_string(), @@ -545,7 +549,7 @@ async fn test_remote_cancel_language_server_work( ) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -561,7 +565,7 @@ async fn test_remote_cancel_language_server_work( let (project, headless) = init_test(&fs, cx, server_cx).await; fs.insert_tree( - "/code/project1/.zed", + path!("/code/project1/.zed"), json!({ "settings.json": r#" { @@ -608,7 +612,7 @@ async fn test_remote_cancel_language_server_work( let worktree_id = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) + project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap() @@ -708,7 +712,7 @@ async fn test_remote_cancel_language_server_work( async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -724,7 +728,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont let (project, _headless) = init_test(&fs, cx, server_cx).await; let (worktree, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) + project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); @@ -739,7 +743,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont .unwrap(); fs.save( - &PathBuf::from("/code/project1/src/lib.rs"), + &PathBuf::from(path!("/code/project1/src/lib.rs")), &("bangles".to_string().into()), LineEnding::Unix, ) @@ -754,7 +758,7 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont }); fs.save( - &PathBuf::from("/code/project1/src/lib.rs"), + &PathBuf::from(path!("/code/project1/src/lib.rs")), &("bloop".to_string().into()), LineEnding::Unix, ) @@ -786,7 +790,7 @@ async fn test_remote_resolve_path_in_buffer( ) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -802,7 +806,7 @@ async fn test_remote_resolve_path_in_buffer( let (project, _headless) = init_test(&fs, cx, server_cx).await; let (worktree, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) + project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); @@ -818,14 +822,14 @@ async fn test_remote_resolve_path_in_buffer( let path = project .update(cx, |project, cx| { - project.resolve_path_in_buffer("/code/project1/README.md", &buffer, cx) + project.resolve_path_in_buffer(path!("/code/project1/README.md"), &buffer, cx) }) .await .unwrap(); assert!(path.is_file()); assert_eq!( path.abs_path().unwrap().to_string_lossy(), - "/code/project1/README.md" + path!("/code/project1/README.md") ); let path = project @@ -1013,7 +1017,7 @@ async fn test_adding_then_removing_then_adding_worktrees( async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -1035,7 +1039,9 @@ async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut Test cx.update(|cx| { assert_eq!( buffer.read(cx).text(), - initial_server_settings_content().to_string() + initial_server_settings_content() + .to_string() + .replace("\r\n", "\n") ) }) } @@ -1044,7 +1050,7 @@ async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut Test async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -1061,7 +1067,7 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) let (worktree, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) + project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); @@ -1091,7 +1097,9 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) .unwrap(); assert_eq!( - fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(), + fs.load(path!("/code/project1/src/lib.rs").as_ref()) + .await + .unwrap(), "fn one() -> usize { 100 }" ); } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 3c40eecb2f579f..3fe85f13f20ad7 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2188,6 +2188,7 @@ pub mod tests { use project::FakeFs; use serde_json::json; use settings::SettingsStore; + use util::path; use workspace::DeploySearch; #[gpui::test] @@ -3313,13 +3314,13 @@ pub mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": "const ONE: usize = 1;", }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let worktree_id = project.update(cx, |this, cx| { this.worktrees(cx).next().unwrap().read(cx).id() }); @@ -3537,13 +3538,13 @@ pub mod tests { // Setup 2 panes, both with a file open and one with a project search. let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": "const ONE: usize = 1;", }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let worktree_id = project.update(cx, |this, cx| { this.worktrees(cx).next().unwrap().read(cx).id() }); @@ -3771,13 +3772,13 @@ pub mod tests { let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "one.rs": "const ONE: usize = 1;", }), ) .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let worktree_id = project.update(cx, |this, cx| { this.worktrees(cx).next().unwrap().read(cx).id() }); diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 221e854195fe92..1f3c40507bf6d8 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -44,9 +44,9 @@ sha2.workspace = true smol.workspace = true theme.workspace = true tree-sitter.workspace = true -ui. workspace = true +ui.workspace = true unindent.workspace = true -util. workspace = true +util.workspace = true workspace.workspace = true worktree.workspace = true diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 0daf4a985ac2b2..9345965ccd7274 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -279,6 +279,7 @@ mod tests { use settings::SettingsStore; use smol::channel; use std::{future, path::Path, sync::Arc}; + use util::separator; fn init_test(cx: &mut TestAppContext) { env_logger::try_init().ok(); @@ -421,7 +422,10 @@ mod tests { // Find result that is greater than 0.5 let search_result = results.iter().find(|result| result.score > 0.9).unwrap(); - assert_eq!(search_result.path.to_string_lossy(), "fixture/needle.md"); + assert_eq!( + search_result.path.to_string_lossy(), + separator!("fixture/needle.md") + ); let content = cx .update(|cx| { diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 101695508f0ce9..622c42d006df7a 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -12,6 +12,7 @@ pub fn test_settings() -> String { crate::default_settings().as_ref(), ) .unwrap(); + #[cfg(not(target_os = "windows"))] util::merge_non_null_json_value_into( serde_json::json!({ "ui_font_family": "Courier", @@ -26,6 +27,21 @@ pub fn test_settings() -> String { }), &mut value, ); + #[cfg(target_os = "windows")] + util::merge_non_null_json_value_into( + serde_json::json!({ + "ui_font_family": "Courier New", + "ui_font_features": {}, + "ui_font_size": 14, + "ui_font_fallback": [], + "buffer_font_family": "Courier New", + "buffer_font_features": {}, + "buffer_font_size": 14, + "buffer_font_fallback": [], + "theme": EMPTY_THEME_NAME, + }), + &mut value, + ); value.as_object_mut().unwrap().remove("languages"); serde_json::to_string(&value).unwrap() } diff --git a/crates/sqlez_macros/Cargo.toml b/crates/sqlez_macros/Cargo.toml index 5959617f72c911..cff96d0b894975 100644 --- a/crates/sqlez_macros/Cargo.toml +++ b/crates/sqlez_macros/Cargo.toml @@ -16,4 +16,4 @@ doctest = false [dependencies] sqlez.workspace = true sqlformat.workspace = true -syn = "1.0" +syn.workspace = true diff --git a/crates/tab_switcher/src/tab_switcher_tests.rs b/crates/tab_switcher/src/tab_switcher_tests.rs index 045879ef069bb6..f1a5b64b10b8ae 100644 --- a/crates/tab_switcher/src/tab_switcher_tests.rs +++ b/crates/tab_switcher/src/tab_switcher_tests.rs @@ -5,6 +5,7 @@ use menu::SelectPrev; use project::{Project, ProjectPath}; use serde_json::json; use std::path::Path; +use util::path; use workspace::{AppState, Workspace}; #[ctor::ctor] @@ -24,7 +25,7 @@ async fn test_open_with_prev_tab_selected_and_cycle_on_toggle_action( .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "1.txt": "First file", "2.txt": "Second file", @@ -34,7 +35,7 @@ async fn test_open_with_prev_tab_selected_and_cycle_on_toggle_action( ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -81,7 +82,7 @@ async fn test_open_with_last_tab_selected(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "1.txt": "First file", "2.txt": "Second file", @@ -90,7 +91,7 @@ async fn test_open_with_last_tab_selected(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -172,10 +173,10 @@ async fn test_open_with_single_item(cx: &mut gpui::TestAppContext) { app_state .fs .as_fake() - .insert_tree("/root", json!({"1.txt": "Single file"})) + .insert_tree(path!("/root"), json!({"1.txt": "Single file"})) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -195,7 +196,7 @@ async fn test_close_selected_item(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "1.txt": "First file", "2.txt": "Second file", @@ -203,7 +204,7 @@ async fn test_close_selected_item(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -241,7 +242,7 @@ async fn test_close_preserves_selected_position(cx: &mut gpui::TestAppContext) { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "1.txt": "First file", "2.txt": "Second file", @@ -250,7 +251,7 @@ async fn test_close_preserves_selected_position(cx: &mut gpui::TestAppContext) { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index d3376066aab23f..3e728088fd71c2 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -649,6 +649,7 @@ mod tests { use project::{ContextProviderWithTasks, FakeFs, Project}; use serde_json::json; use task::TaskTemplates; + use util::path; use workspace::CloseInactiveTabsAndPanes; use crate::{modal::Spawn, tests::init_test}; @@ -660,7 +661,7 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ ".zed": { "tasks.json": r#"[ @@ -681,7 +682,7 @@ mod tests { ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); @@ -700,7 +701,7 @@ mod tests { let _ = workspace .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path(PathBuf::from("/dir/a.ts"), true, window, cx) + workspace.open_abs_path(PathBuf::from(path!("/dir/a.ts")), true, window, cx) }) .await .unwrap(); @@ -824,7 +825,7 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ ".zed": { "tasks.json": r#"[ @@ -846,7 +847,7 @@ mod tests { ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -865,7 +866,7 @@ mod tests { let _ = workspace .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( - PathBuf::from("/dir/file_with.odd_extension"), + PathBuf::from(path!("/dir/file_with.odd_extension")), true, window, cx, @@ -878,8 +879,8 @@ mod tests { assert_eq!( task_names(&tasks_picker, cx), vec![ - "hello from /dir/file_with.odd_extension:1:1".to_string(), - "opened now: /dir".to_string() + concat!("hello from ", path!("/dir/file_with.odd_extension:1:1")).to_string(), + concat!("opened now: ", path!("/dir")).to_string(), ], "Second opened buffer should fill the context, labels should be trimmed if long enough" ); @@ -892,7 +893,7 @@ mod tests { let second_item = workspace .update_in(cx, |workspace, window, cx| { workspace.open_abs_path( - PathBuf::from("/dir/file_without_extension"), + PathBuf::from(path!("/dir/file_without_extension")), true, window, cx, @@ -914,8 +915,8 @@ mod tests { assert_eq!( task_names(&tasks_picker, cx), vec![ - "hello from /dir/file_without_extension:2:3".to_string(), - "opened now: /dir".to_string() + concat!("hello from ", path!("/dir/file_without_extension:2:3")).to_string(), + concat!("opened now: ", path!("/dir")).to_string(), ], "Opened buffer should fill the context, labels should be trimmed if long enough" ); @@ -931,7 +932,7 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ "a1.ts": "// a1", "a2.ts": "// a2", @@ -940,7 +941,7 @@ mod tests { ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; project.read_with(cx, |project, _| { let language_registry = project.languages(); language_registry.add(Arc::new( @@ -1001,7 +1002,7 @@ mod tests { let _ts_file_1 = workspace .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path(PathBuf::from("/dir/a1.ts"), true, window, cx) + workspace.open_abs_path(PathBuf::from(path!("/dir/a1.ts")), true, window, cx) }) .await .unwrap(); @@ -1009,23 +1010,28 @@ mod tests { assert_eq!( task_names(&tasks_picker, cx), vec![ - "Another task from file /dir/a1.ts", - "TypeScript task from file /dir/a1.ts", + concat!("Another task from file ", path!("/dir/a1.ts")), + concat!("TypeScript task from file ", path!("/dir/a1.ts")), "Task without variables", ], "Should open spawn TypeScript tasks for the opened file, tasks with most template variables above, all groups sorted alphanumerically" ); + emulate_task_schedule( tasks_picker, &project, - "TypeScript task from file /dir/a1.ts", + concat!("TypeScript task from file ", path!("/dir/a1.ts")), cx, ); let tasks_picker = open_spawn_tasks(&workspace, cx); assert_eq!( task_names(&tasks_picker, cx), - vec!["TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"], + vec![ + concat!("TypeScript task from file ", path!("/dir/a1.ts")), + concat!("Another task from file ", path!("/dir/a1.ts")), + "Task without variables", + ], "After spawning the task and getting it into the history, it should be up in the sort as recently used. Tasks with the same labels and context are deduplicated." ); @@ -1037,7 +1043,7 @@ mod tests { let _ts_file_2 = workspace .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path(PathBuf::from("/dir/a2.ts"), true, window, cx) + workspace.open_abs_path(PathBuf::from(path!("/dir/a2.ts")), true, window, cx) }) .await .unwrap(); @@ -1045,10 +1051,10 @@ mod tests { assert_eq!( task_names(&tasks_picker, cx), vec![ - "TypeScript task from file /dir/a1.ts", - "Another task from file /dir/a2.ts", - "TypeScript task from file /dir/a2.ts", - "Task without variables" + concat!("TypeScript task from file ", path!("/dir/a1.ts")), + concat!("Another task from file ", path!("/dir/a2.ts")), + concat!("TypeScript task from file ", path!("/dir/a2.ts")), + "Task without variables", ], "Even when both TS files are open, should only show the history (on the top), and tasks, resolved for the current file" ); @@ -1075,7 +1081,7 @@ mod tests { emulate_task_schedule(tasks_picker, &project, "Rust task", cx); let _ts_file_2 = workspace .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path(PathBuf::from("/dir/a2.ts"), true, window, cx) + workspace.open_abs_path(PathBuf::from(path!("/dir/a2.ts")), true, window, cx) }) .await .unwrap(); @@ -1083,10 +1089,10 @@ mod tests { assert_eq!( task_names(&tasks_picker, cx), vec![ - "TypeScript task from file /dir/a1.ts", - "Another task from file /dir/a2.ts", - "TypeScript task from file /dir/a2.ts", - "Task without variables" + concat!("TypeScript task from file ", path!("/dir/a1.ts")), + concat!("Another task from file ", path!("/dir/a2.ts")), + concat!("TypeScript task from file ", path!("/dir/a2.ts")), + "Task without variables", ], "After closing all but *.rs tabs, running a Rust task and switching back to TS tasks, \ same TS spawn history should be restored" diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 956a81e586571f..781e2249dc32e2 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -269,6 +269,7 @@ mod tests { use serde_json::json; use task::{TaskContext, TaskVariables, VariableName}; use ui::VisualContext; + use util::{path, separator}; use workspace::{AppState, Workspace}; use crate::task_context; @@ -278,7 +279,7 @@ mod tests { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ ".zed": { "tasks.json": r#"[ @@ -302,7 +303,7 @@ mod tests { }), ) .await; - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; let worktree_store = project.update(cx, |project, _| project.worktree_store().clone()); let rust_language = Arc::new( Language::new( @@ -382,17 +383,18 @@ mod tests { task_context(workspace, window, cx) }) .await; + assert_eq!( first_context, TaskContext { - cwd: Some("/dir".into()), + cwd: Some(path!("/dir").into()), task_variables: TaskVariables::from_iter([ - (VariableName::File, "/dir/rust/b.rs".into()), + (VariableName::File, path!("/dir/rust/b.rs").into()), (VariableName::Filename, "b.rs".into()), - (VariableName::RelativeFile, "rust/b.rs".into()), - (VariableName::Dirname, "/dir/rust".into()), + (VariableName::RelativeFile, separator!("rust/b.rs").into()), + (VariableName::Dirname, path!("/dir/rust").into()), (VariableName::Stem, "b".into()), - (VariableName::WorktreeRoot, "/dir".into()), + (VariableName::WorktreeRoot, path!("/dir").into()), (VariableName::Row, "1".into()), (VariableName::Column, "1".into()), ]), @@ -414,14 +416,14 @@ mod tests { }) .await, TaskContext { - cwd: Some("/dir".into()), + cwd: Some(path!("/dir").into()), task_variables: TaskVariables::from_iter([ - (VariableName::File, "/dir/rust/b.rs".into()), + (VariableName::File, path!("/dir/rust/b.rs").into()), (VariableName::Filename, "b.rs".into()), - (VariableName::RelativeFile, "rust/b.rs".into()), - (VariableName::Dirname, "/dir/rust".into()), + (VariableName::RelativeFile, separator!("rust/b.rs").into()), + (VariableName::Dirname, path!("/dir/rust").into()), (VariableName::Stem, "b".into()), - (VariableName::WorktreeRoot, "/dir".into()), + (VariableName::WorktreeRoot, path!("/dir").into()), (VariableName::Row, "1".into()), (VariableName::Column, "15".into()), (VariableName::SelectedText, "is_i".into()), @@ -440,14 +442,14 @@ mod tests { }) .await, TaskContext { - cwd: Some("/dir".into()), + cwd: Some(path!("/dir").into()), task_variables: TaskVariables::from_iter([ - (VariableName::File, "/dir/a.ts".into()), + (VariableName::File, path!("/dir/a.ts").into()), (VariableName::Filename, "a.ts".into()), (VariableName::RelativeFile, "a.ts".into()), - (VariableName::Dirname, "/dir".into()), + (VariableName::Dirname, path!("/dir").into()), (VariableName::Stem, "a".into()), - (VariableName::WorktreeRoot, "/dir".into()), + (VariableName::WorktreeRoot, path!("/dir").into()), (VariableName::Row, "1".into()), (VariableName::Column, "1".into()), (VariableName::Symbol, "this_is_a_test".into()), diff --git a/crates/ui_macros/Cargo.toml b/crates/ui_macros/Cargo.toml index 7687c2b36ba66d..773c07d2383b62 100644 --- a/crates/ui_macros/Cargo.toml +++ b/crates/ui_macros/Cargo.toml @@ -13,7 +13,7 @@ path = "src/ui_macros.rs" proc-macro = true [dependencies] -proc-macro2 = "1.0.66" -quote = "1.0.9" -syn = { version = "1.0.72", features = ["full", "extra-traits"] } +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true convert_case.workspace = true diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 213e2fc0d4f38a..06c2c4d8bac786 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -13,7 +13,7 @@ path = "src/util.rs" doctest = true [features] -test-support = ["tempfile", "git2", "rand"] +test-support = ["tempfile", "git2", "rand", "util_macros"] [dependencies] anyhow.workspace = true @@ -35,6 +35,7 @@ smol.workspace = true take-until.workspace = true tempfile = { workspace = true, optional = true } unicase.workspace = true +util_macros = { workspace = true, optional = true } [target.'cfg(unix)'.dependencies] libc.workspace = true @@ -47,3 +48,4 @@ dunce = "1.0" git2.workspace = true rand.workspace = true tempfile.workspace = true +util_macros.workspace = true diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index ba78d6d06d4270..275895d228440e 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -23,6 +23,7 @@ pub trait PathExt { fn compact(&self) -> PathBuf; fn icon_stem_or_suffix(&self) -> Option<&str>; fn extension_or_hidden_file_name(&self) -> Option<&str>; + fn to_sanitized_string(&self) -> String; fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result where Self: From<&'a Path>, @@ -94,6 +95,20 @@ impl> PathExt for T { self.as_ref().file_name()?.to_str()?.split('.').last() } + + /// Returns a sanitized string representation of the path. + /// Note, on Windows, this assumes that the path is a valid UTF-8 string and + /// is not a UNC path. + fn to_sanitized_string(&self) -> String { + #[cfg(target_os = "windows")] + { + self.as_ref().to_string_lossy().replace("/", "\\") + } + #[cfg(not(target_os = "windows"))] + { + self.as_ref().to_string_lossy().to_string() + } + } } /// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath` @@ -115,6 +130,17 @@ impl SanitizedPath { self.0.to_string_lossy().to_string() } + pub fn to_glob_string(&self) -> String { + #[cfg(target_os = "windows")] + { + self.0.to_string_lossy().replace("/", "\\") + } + #[cfg(not(target_os = "windows"))] + { + self.0.to_string_lossy().to_string() + } + } + pub fn join(&self, path: &Self) -> Self { self.0.join(&path.0).into() } @@ -448,14 +474,6 @@ pub fn compare_paths( } } -#[cfg(any(test, feature = "test-support"))] -pub fn replace_path_separator(path: &str) -> String { - #[cfg(target_os = "windows")] - return path.replace("/", std::path::MAIN_SEPARATOR_STR); - #[cfg(not(target_os = "windows"))] - return path.to_string(); -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 77a788aef2cc56..9fd802a09cf165 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -28,6 +28,8 @@ use unicase::UniCase; use anyhow::{anyhow, Context as _}; pub use take_until::*; +#[cfg(any(test, feature = "test-support"))] +pub use util_macros::{separator, uri}; #[macro_export] macro_rules! debug_panic { @@ -41,6 +43,50 @@ macro_rules! debug_panic { }; } +/// A macro to add "C:" to the beginning of a path literal on Windows, and replace all +/// the separator from `/` to `\`. +/// But on non-Windows platforms, it will return the path literal as is. +/// +/// # Examples +/// ```rust +/// use util::path; +/// +/// let path = path!("/Users/user/file.txt"); +/// #[cfg(target_os = "windows")] +/// assert_eq!(path, "C:\\Users\\user\\file.txt"); +/// #[cfg(not(target_os = "windows"))] +/// assert_eq!(path, "/Users/user/file.txt"); +/// ``` +#[cfg(all(any(test, feature = "test-support"), target_os = "windows"))] +#[macro_export] +macro_rules! path { + ($path:literal) => { + concat!("C:", util::separator!($path)) + }; +} + +/// A macro to add "C:" to the beginning of a path literal on Windows, and replace all +/// the separator from `/` to `\`. +/// But on non-Windows platforms, it will return the path literal as is. +/// +/// # Examples +/// ```rust +/// use util::path; +/// +/// let path = path!("/Users/user/file.txt"); +/// #[cfg(target_os = "windows")] +/// assert_eq!(path, "C:\\Users\\user\\file.txt"); +/// #[cfg(not(target_os = "windows"))] +/// assert_eq!(path, "/Users/user/file.txt"); +/// ``` +#[cfg(all(any(test, feature = "test-support"), not(target_os = "windows")))] +#[macro_export] +macro_rules! path { + ($path:literal) => { + $path + }; +} + pub fn truncate(s: &str, max_chars: usize) -> &str { match s.char_indices().nth(max_chars) { None => s, diff --git a/crates/util_macros/Cargo.toml b/crates/util_macros/Cargo.toml new file mode 100644 index 00000000000000..59c8ee96995152 --- /dev/null +++ b/crates/util_macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "util_macros" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/util_macros.rs" +proc-macro = true +doctest = false + +[dependencies] +quote.workspace = true +syn.workspace = true diff --git a/crates/util_macros/LICENSE-APACHE b/crates/util_macros/LICENSE-APACHE new file mode 120000 index 00000000000000..1cd601d0a3affa --- /dev/null +++ b/crates/util_macros/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/util_macros/src/util_macros.rs b/crates/util_macros/src/util_macros.rs new file mode 100644 index 00000000000000..2baba2f473881e --- /dev/null +++ b/crates/util_macros/src/util_macros.rs @@ -0,0 +1,56 @@ +#![cfg_attr(not(target_os = "windows"), allow(unused))] + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, LitStr}; + +/// This macro replaces the path separator `/` with `\` for Windows. +/// But if the target OS is not Windows, the path is returned as is. +/// +/// # Example +/// ```rust +/// # use util_macros::separator; +/// let path = separator!("path/to/file"); +/// #[cfg(target_os = "windows")] +/// assert_eq!(path, "path\\to\\file"); +/// #[cfg(not(target_os = "windows"))] +/// assert_eq!(path, "path/to/file"); +/// ``` +#[proc_macro] +pub fn separator(input: TokenStream) -> TokenStream { + let path = parse_macro_input!(input as LitStr); + let path = path.value(); + + #[cfg(target_os = "windows")] + let path = path.replace("/", "\\"); + + TokenStream::from(quote! { + #path + }) +} + +/// This macro replaces the path prefix `file:///` with `file:///C:/` for Windows. +/// But if the target OS is not Windows, the URI is returned as is. +/// +/// # Example +/// ```rust +/// use util_macros::uri; +/// +/// let uri = uri!("file:///path/to/file"); +/// #[cfg(target_os = "windows")] +/// assert_eq!(uri, "file:///C:/path/to/file"); +/// #[cfg(not(target_os = "windows"))] +/// assert_eq!(uri, "file:///path/to/file"); +/// ``` +#[proc_macro] +pub fn uri(input: TokenStream) -> TokenStream { + let uri = parse_macro_input!(input as LitStr); + let uri = uri.value(); + + #[cfg(target_os = "windows")] + let uri = uri.replace("file:///", "file:///C:/"); + + TokenStream::from(quote! { + #uri + }) +} diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index e6a68a072f43f8..886de69e413e36 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1456,6 +1456,7 @@ mod test { use editor::Editor; use gpui::{Context, TestAppContext}; use indoc::indoc; + use util::path; use workspace::Workspace; #[gpui::test] @@ -1552,13 +1553,13 @@ mod test { #[gpui::test] async fn test_command_write(cx: &mut TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - let path = Path::new("/root/dir/file.rs"); + let path = Path::new(path!("/root/dir/file.rs")); let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); cx.simulate_keystrokes("i @ escape"); cx.simulate_keystrokes(": w enter"); - assert_eq!(fs.load(path).await.unwrap(), "@\n"); + assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n"); fs.as_fake().insert_file(path, b"oops\n".to_vec()).await; @@ -1568,12 +1569,12 @@ mod test { assert!(cx.has_pending_prompt()); // "Cancel" cx.simulate_prompt_answer(0); - assert_eq!(fs.load(path).await.unwrap(), "oops\n"); + assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n"); assert!(!cx.has_pending_prompt()); // force overwrite cx.simulate_keystrokes(": w ! enter"); assert!(!cx.has_pending_prompt()); - assert_eq!(fs.load(path).await.unwrap(), "@@\n"); + assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n"); } #[gpui::test] @@ -1665,7 +1666,7 @@ mod test { let file_path = file.as_local().unwrap().abs_path(cx); assert_eq!(text, expected_text); - assert_eq!(file_path.to_str().unwrap(), expected_path); + assert_eq!(file_path, Path::new(expected_path)); } #[gpui::test] @@ -1674,16 +1675,22 @@ mod test { // Assert base state, that we're in /root/dir/file.rs cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, "/root/dir/file.rs", "", cx); + assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); }); // Insert a new file let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); fs.as_fake() - .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec()) + .insert_file( + path!("/root/dir/file2.rs"), + "This is file2.rs".as_bytes().to_vec(), + ) .await; fs.as_fake() - .insert_file("/root/dir/file3.rs", "go to file3".as_bytes().to_vec()) + .insert_file( + path!("/root/dir/file3.rs"), + "go to file3".as_bytes().to_vec(), + ) .await; // Put the path to the second file into the currently open buffer @@ -1695,7 +1702,12 @@ mod test { // We now have two items cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, "/root/dir/file2.rs", "This is file2.rs", cx); + assert_active_item( + workspace, + path!("/root/dir/file2.rs"), + "This is file2.rs", + cx, + ); }); // Update editor to point to `file2.rs` @@ -1712,7 +1724,7 @@ mod test { // We now have three items cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, "/root/dir/file3.rs", "go to file3", cx); + assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx); }); } diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index eb3acde6dc3982..e47e48a6b496f4 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -696,12 +696,20 @@ mod test { // not testing nvim as it doesn't have a filename cx.simulate_keystrokes("\" % p"); + #[cfg(not(target_os = "windows"))] cx.assert_state( indoc! {" The quick brown dogdir/file.rˇs"}, Mode::Normal, ); + #[cfg(target_os = "windows")] + cx.assert_state( + indoc! {" + The quick brown + dogdir\\file.rˇs"}, + Mode::Normal, + ); } #[gpui::test] diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 955afbe20912a7..8ba52747d1f50a 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1319,14 +1319,7 @@ impl LocalWorktree { let settings = self.settings.clone(); let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); let background_scanner = cx.background_executor().spawn({ - let abs_path = &snapshot.abs_path; - #[cfg(target_os = "windows")] - let abs_path = abs_path - .as_path() - .canonicalize() - .unwrap_or_else(|_| abs_path.as_path().to_path_buf()); - #[cfg(not(target_os = "windows"))] - let abs_path = abs_path.as_path().to_path_buf(); + let abs_path = snapshot.abs_path.as_path().to_path_buf(); let background = cx.background_executor().clone(); async move { let (events, watcher) = fs.watch(&abs_path, FS_WATCH_LATENCY).await; diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 533ae7eb8724de..2cee728aec89e4 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -2156,7 +2156,13 @@ const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus { second_head: UnmergedStatusCode::Updated, }); +// NOTE: +// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename +// a directory which some program has already open. +// This is a limitation of the Windows. +// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] async fn test_rename_work_directory(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); @@ -2184,7 +2190,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { let repo = git_init(&root_path.join("projects/project1")); git_add("a", &repo); git_commit("init", &repo); - std::fs::write(root_path.join("projects/project1/a"), "aa").ok(); + std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; @@ -2209,7 +2215,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { root_path.join("projects/project1"), root_path.join("projects/project2"), ) - .ok(); + .unwrap(); tree.flush_fs_events(cx).await; cx.read(|cx| { @@ -2335,7 +2341,13 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { }); } +// NOTE: +// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename +// a directory which some program has already open. +// This is a limitation of the Windows. +// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder #[gpui::test] +#[cfg_attr(target_os = "windows", ignore)] async fn test_file_status(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7f8386661810a4..745e31a8eb0bce 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1582,6 +1582,7 @@ mod tests { time::Duration, }; use theme::{ThemeRegistry, ThemeSettings}; + use util::{path, separator}; use workspace::{ item::{Item, ItemHandle}, open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection, @@ -1750,12 +1751,15 @@ mod tests { app_state .fs .as_fake() - .insert_tree("/root", json!({"a": "hey", "b": "", "dir": {"c": "f"}})) + .insert_tree( + path!("/root"), + json!({"a": "hey", "b": "", "dir": {"c": "f"}}), + ) .await; cx.update(|cx| { open_paths( - &[PathBuf::from("/root/dir")], + &[PathBuf::from(path!("/root/dir"))], app_state.clone(), workspace::OpenOptions::default(), cx, @@ -1767,7 +1771,7 @@ mod tests { cx.update(|cx| { open_paths( - &[PathBuf::from("/root/a")], + &[PathBuf::from(path!("/root/a"))], app_state.clone(), workspace::OpenOptions { open_new_workspace: Some(false), @@ -1782,7 +1786,7 @@ mod tests { cx.update(|cx| { open_paths( - &[PathBuf::from("/root/dir/c")], + &[PathBuf::from(path!("/root/dir/c"))], app_state.clone(), workspace::OpenOptions { open_new_workspace: Some(true), @@ -1802,12 +1806,15 @@ mod tests { app_state .fs .as_fake() - .insert_tree("/root", json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}})) + .insert_tree( + path!("/root"), + json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}), + ) .await; cx.update(|cx| { open_paths( - &[PathBuf::from("/root/dir1/a")], + &[PathBuf::from(path!("/root/dir1/a"))], app_state.clone(), workspace::OpenOptions::default(), cx, @@ -1820,7 +1827,7 @@ mod tests { cx.update(|cx| { open_paths( - &[PathBuf::from("/root/dir2/c")], + &[PathBuf::from(path!("/root/dir2/c"))], app_state.clone(), workspace::OpenOptions::default(), cx, @@ -1832,7 +1839,7 @@ mod tests { cx.update(|cx| { open_paths( - &[PathBuf::from("/root/dir2")], + &[PathBuf::from(path!("/root/dir2"))], app_state.clone(), workspace::OpenOptions::default(), cx, @@ -1848,7 +1855,7 @@ mod tests { cx.update(|cx| { open_paths( - &[PathBuf::from("/root/dir2/c")], + &[PathBuf::from(path!("/root/dir2/c"))], app_state.clone(), workspace::OpenOptions::default(), cx, @@ -1877,12 +1884,12 @@ mod tests { app_state .fs .as_fake() - .insert_tree("/root", json!({"a": "hey"})) + .insert_tree(path!("/root"), json!({"a": "hey"})) .await; cx.update(|cx| { open_paths( - &[PathBuf::from("/root/a")], + &[PathBuf::from(path!("/root/a"))], app_state.clone(), workspace::OpenOptions::default(), cx, @@ -1964,7 +1971,7 @@ mod tests { // Opening the buffer again doesn't impact the window's edited state. cx.update(|cx| { open_paths( - &[PathBuf::from("/root/a")], + &[PathBuf::from(path!("/root/a"))], app_state, workspace::OpenOptions::default(), cx, @@ -2026,12 +2033,12 @@ mod tests { app_state .fs .as_fake() - .insert_tree("/root", json!({"a": "hey"})) + .insert_tree(path!("/root"), json!({"a": "hey"})) .await; cx.update(|cx| { open_paths( - &[PathBuf::from("/root/a")], + &[PathBuf::from(path!("/root/a"))], app_state.clone(), workspace::OpenOptions::default(), cx, @@ -2083,7 +2090,7 @@ mod tests { // When we now reopen the window, the edited state and the edited buffer are back cx.update(|cx| { open_paths( - &[PathBuf::from("/root/a")], + &[PathBuf::from(path!("/root/a"))], app_state.clone(), workspace::OpenOptions::default(), cx, @@ -2179,7 +2186,7 @@ mod tests { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "a": { "file1": "contents 1", @@ -2190,7 +2197,7 @@ mod tests { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| { project.languages().add(markdown_language()) }); @@ -2311,7 +2318,7 @@ mod tests { .fs .as_fake() .insert_tree( - "/", + path!("/"), json!({ "dir1": { "a.txt": "" @@ -2329,7 +2336,7 @@ mod tests { cx.update(|cx| { open_paths( - &[PathBuf::from("/dir1/")], + &[PathBuf::from(path!("/dir1/"))], app_state, workspace::OpenOptions::default(), cx, @@ -2376,7 +2383,7 @@ mod tests { window .update(cx, |workspace, window, cx| { workspace.open_paths( - vec!["/dir1/a.txt".into()], + vec![path!("/dir1/a.txt").into()], OpenVisible::All, None, window, @@ -2387,7 +2394,12 @@ mod tests { .await; cx.read(|cx| { let workspace = workspace.read(cx); - assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx); + assert_project_panel_selection( + workspace, + Path::new(path!("/dir1")), + Path::new("a.txt"), + cx, + ); assert_eq!( workspace .active_pane() @@ -2406,7 +2418,7 @@ mod tests { window .update(cx, |workspace, window, cx| { workspace.open_paths( - vec!["/dir2/b.txt".into()], + vec![path!("/dir2/b.txt").into()], OpenVisible::All, None, window, @@ -2417,14 +2429,19 @@ mod tests { .await; cx.read(|cx| { let workspace = workspace.read(cx); - assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx); + assert_project_panel_selection( + workspace, + Path::new(path!("/dir2/b.txt")), + Path::new(""), + cx, + ); let worktree_roots = workspace .worktrees(cx) .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); assert_eq!( worktree_roots, - vec!["/dir1", "/dir2/b.txt"] + vec![path!("/dir1"), path!("/dir2/b.txt")] .into_iter() .map(Path::new) .collect(), @@ -2447,7 +2464,7 @@ mod tests { window .update(cx, |workspace, window, cx| { workspace.open_paths( - vec!["/dir3".into(), "/dir3/c.txt".into()], + vec![path!("/dir3").into(), path!("/dir3/c.txt").into()], OpenVisible::All, None, window, @@ -2458,14 +2475,19 @@ mod tests { .await; cx.read(|cx| { let workspace = workspace.read(cx); - assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx); + assert_project_panel_selection( + workspace, + Path::new(path!("/dir3")), + Path::new("c.txt"), + cx, + ); let worktree_roots = workspace .worktrees(cx) .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); assert_eq!( worktree_roots, - vec!["/dir1", "/dir2/b.txt", "/dir3"] + vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")] .into_iter() .map(Path::new) .collect(), @@ -2487,23 +2509,39 @@ mod tests { // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. window .update(cx, |workspace, window, cx| { - workspace.open_paths(vec!["/d.txt".into()], OpenVisible::None, None, window, cx) + workspace.open_paths( + vec![path!("/d.txt").into()], + OpenVisible::None, + None, + window, + cx, + ) }) .unwrap() .await; cx.read(|cx| { let workspace = workspace.read(cx); - assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx); + assert_project_panel_selection( + workspace, + Path::new(path!("/d.txt")), + Path::new(""), + cx, + ); let worktree_roots = workspace .worktrees(cx) .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); assert_eq!( worktree_roots, - vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"] - .into_iter() - .map(Path::new) - .collect(), + vec![ + path!("/dir1"), + path!("/dir2/b.txt"), + path!("/dir3"), + path!("/d.txt") + ] + .into_iter() + .map(Path::new) + .collect(), ); let visible_worktree_roots = workspace @@ -2512,7 +2550,7 @@ mod tests { .collect::>(); assert_eq!( visible_worktree_roots, - vec!["/dir1", "/dir2/b.txt", "/dir3"] + vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")] .into_iter() .map(Path::new) .collect(), @@ -2548,7 +2586,7 @@ mod tests { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ ".gitignore": "ignored_dir\n", ".git": { @@ -2573,7 +2611,7 @@ mod tests { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| { project.languages().add(markdown_language()) }); @@ -2582,9 +2620,9 @@ mod tests { let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); let paths_to_open = [ - Path::new("/root/excluded_dir/file").to_path_buf(), - Path::new("/root/.git/HEAD").to_path_buf(), - Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), + PathBuf::from(path!("/root/excluded_dir/file")), + PathBuf::from(path!("/root/.git/HEAD")), + PathBuf::from(path!("/root/excluded_dir/ignored_subdir")), ]; let (opened_workspace, new_items) = cx .update(|cx| { @@ -2629,8 +2667,8 @@ mod tests { opened_paths, vec![ None, - Some(".git/HEAD".to_string()), - Some("excluded_dir/file".to_string()), + Some(separator!(".git/HEAD").to_string()), + Some(separator!("excluded_dir/file").to_string()), ], "Excluded files should get opened, excluded dir should not get opened" ); @@ -2656,7 +2694,7 @@ mod tests { opened_buffer_paths.sort(); assert_eq!( opened_buffer_paths, - vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], + vec![separator!(".git/HEAD").to_string(), separator!("excluded_dir/file").to_string()], "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" ); }); @@ -2668,10 +2706,10 @@ mod tests { app_state .fs .as_fake() - .insert_tree("/root", json!({ "a.txt": "" })) + .insert_tree(path!("/root"), json!({ "a.txt": "" })) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| { project.languages().add(markdown_language()) }); @@ -2682,7 +2720,7 @@ mod tests { window .update(cx, |workspace, window, cx| { workspace.open_paths( - vec![PathBuf::from("/root/a.txt")], + vec![PathBuf::from(path!("/root/a.txt"))], OpenVisible::All, None, window, @@ -2706,7 +2744,7 @@ mod tests { app_state .fs .as_fake() - .insert_file("/root/a.txt", b"changed".to_vec()) + .insert_file(path!("/root/a.txt"), b"changed".to_vec()) .await; cx.run_until_parked(); @@ -2734,9 +2772,13 @@ mod tests { #[gpui::test] async fn test_open_and_save_new_file(cx: &mut TestAppContext) { let app_state = init_test(cx); - app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + app_state + .fs + .create_dir(Path::new(path!("/root"))) + .await + .unwrap(); - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _| { project.languages().add(markdown_language()); project.languages().add(rust_lang()); @@ -2779,7 +2821,7 @@ mod tests { .unwrap(); cx.background_executor.run_until_parked(); cx.simulate_new_path_selection(|parent_dir| { - assert_eq!(parent_dir, Path::new("/root")); + assert_eq!(parent_dir, Path::new(path!("/root"))); Some(parent_dir.join("the-new-name.rs")) }); cx.read(|cx| { @@ -2935,7 +2977,7 @@ mod tests { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "a": { "file1": "contents 1", @@ -2946,7 +2988,7 @@ mod tests { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| { project.languages().add(markdown_language()) }); @@ -3033,7 +3075,7 @@ mod tests { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "a": { "file1": "contents 1\n".repeat(20), @@ -3044,7 +3086,7 @@ mod tests { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| { project.languages().add(markdown_language()) }); @@ -3275,7 +3317,7 @@ mod tests { .unwrap(); app_state .fs - .remove_file(Path::new("/root/a/file2"), Default::default()) + .remove_file(Path::new(path!("/root/a/file2")), Default::default()) .await .unwrap(); cx.background_executor.run_until_parked(); @@ -3416,7 +3458,7 @@ mod tests { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "a": { "file1": "", @@ -3428,7 +3470,7 @@ mod tests { ) .await; - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; project.update(cx, |project, _cx| { project.languages().add(markdown_language()) }); diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 9389ff481ec515..4fa22fed796f89 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -535,6 +535,7 @@ mod tests { use editor::Editor; use gpui::TestAppContext; use serde_json::json; + use util::path; use workspace::{AppState, Workspace}; use crate::zed::{open_listener::open_local_workspace, tests::init_test}; @@ -547,7 +548,7 @@ mod tests { .fs .as_fake() .insert_tree( - "/root", + path!("/root"), json!({ "dir1": { "file1.txt": "content1", @@ -560,7 +561,7 @@ mod tests { assert_eq!(cx.windows().len(), 0); // First open the workspace directory - open_workspace_file("/root/dir1", None, app_state.clone(), cx).await; + open_workspace_file(path!("/root/dir1"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); let workspace = cx.windows()[0].downcast::().unwrap(); @@ -571,7 +572,7 @@ mod tests { .unwrap(); // Now open a file inside that workspace - open_workspace_file("/root/dir1/file1.txt", None, app_state.clone(), cx).await; + open_workspace_file(path!("/root/dir1/file1.txt"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); workspace @@ -581,7 +582,13 @@ mod tests { .unwrap(); // Now open a file inside that workspace, but tell Zed to open a new window - open_workspace_file("/root/dir1/file1.txt", Some(true), app_state.clone(), cx).await; + open_workspace_file( + path!("/root/dir1/file1.txt"), + Some(true), + app_state.clone(), + cx, + ) + .await; assert_eq!(cx.windows().len(), 2); @@ -599,12 +606,16 @@ mod tests { async fn test_open_workspace_with_nonexistent_files(cx: &mut TestAppContext) { let app_state = init_test(cx); - app_state.fs.as_fake().insert_tree("/root", json!({})).await; + app_state + .fs + .as_fake() + .insert_tree(path!("/root"), json!({})) + .await; assert_eq!(cx.windows().len(), 0); // Test case 1: Open a single file that does not exist yet - open_workspace_file("/root/file5.txt", None, app_state.clone(), cx).await; + open_workspace_file(path!("/root/file5.txt"), None, app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); let workspace_1 = cx.windows()[0].downcast::().unwrap(); @@ -616,7 +627,7 @@ mod tests { // Test case 2: Open a single file that does not exist yet, // but tell Zed to add it to the current workspace - open_workspace_file("/root/file6.txt", Some(false), app_state.clone(), cx).await; + open_workspace_file(path!("/root/file6.txt"), Some(false), app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 1); workspace_1 @@ -628,7 +639,7 @@ mod tests { // Test case 3: Open a single file that does not exist yet, // but tell Zed to NOT add it to the current workspace - open_workspace_file("/root/file7.txt", Some(true), app_state.clone(), cx).await; + open_workspace_file(path!("/root/file7.txt"), Some(true), app_state.clone(), cx).await; assert_eq!(cx.windows().len(), 2); let workspace_2 = cx.windows()[1].downcast::().unwrap(); diff --git a/script/exit-ci-if-dev-drive-is-full.ps1 b/script/exit-ci-if-dev-drive-is-full.ps1 new file mode 100644 index 00000000000000..98684d58ee606d --- /dev/null +++ b/script/exit-ci-if-dev-drive-is-full.ps1 @@ -0,0 +1,22 @@ +param ( + [Parameter(Mandatory = $true)] + [int]$MAX_SIZE_IN_GB +) + +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true +$ProgressPreference = "SilentlyContinue" + +if (-Not (Test-Path -Path "target")) { + Write-Host "target directory does not exist yet" + exit 0 +} + +$current_size_gb = (Get-ChildItem -Recurse -Force -File -Path "target" | Measure-Object -Property Length -Sum).Sum / 1GB + +Write-Host "target directory size: ${current_size_gb}GB. max size: ${MAX_SIZE_IN_GB}GB" + +if ($current_size_gb -gt $MAX_SIZE_IN_GB) { + Write-Host "Dev drive is almost full, increase the size first!" + exit 1 +} diff --git a/script/setup-dev-driver.ps1 b/script/setup-dev-driver.ps1 index 28a9c3ed6c6168..51aa17c2678ac9 100644 --- a/script/setup-dev-driver.ps1 +++ b/script/setup-dev-driver.ps1 @@ -3,7 +3,8 @@ # The current version of the Windows runner is 10.0.20348 which does not support DevDrive option. # Ref: https://learn.microsoft.com/en-us/windows/dev-drive/ -$Volume = New-VHD -Path C:/zed_dev_drive.vhdx -SizeBytes 30GB | +# Currently, total CI requires almost 45GB of space, here we are creating a 60GB drive. +$Volume = New-VHD -Path C:/zed_dev_drive.vhdx -SizeBytes 60GB | Mount-VHD -Passthru | Initialize-Disk -Passthru | New-Partition -AssignDriveLetter -UseMaximumSize | From da66f7541022768fa0ab67f5b3b64a6e0fcdf51e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 5 Feb 2025 10:03:45 -0500 Subject: [PATCH 519/650] Revert "copilot: Correct o3-mini context length" (#24275) Reverts zed-industries/zed#24152 See comment: https://github.com/zed-industries/zed/pull/24152#issuecomment-2636808170 Manually confirmed >20k generates error. --- crates/copilot/src/copilot_chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index b0ec1cefd2f74c..b45bd6270c067b 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -89,7 +89,7 @@ impl Model { Self::Gpt4o => 64000, Self::Gpt4 => 32768, Self::Gpt3_5Turbo => 12288, - Self::O3Mini => 200_000, + Self::O3Mini => 20000, Self::O1 => 20000, Self::Claude3_5Sonnet => 200_000, } From 33e39281a6c70af75b584ec143231dcc42720ab8 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 5 Feb 2025 10:07:53 -0500 Subject: [PATCH 520/650] languages: Sort dependencies in `Cargo.toml` (#24277) This PR sorts the dependency lists in the `Cargo.toml` for the `languages` crate. Release Notes: - N/A --- crates/languages/Cargo.toml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 3d51f748c1f7c3..5665b9b53ab25d 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -13,6 +13,7 @@ test-support = [ "load-grammars" ] load-grammars = [ + "tree-sitter", "tree-sitter-bash", "tree-sitter-c", "tree-sitter-cpp", @@ -29,7 +30,6 @@ load-grammars = [ "tree-sitter-rust", "tree-sitter-typescript", "tree-sitter-yaml", - "tree-sitter", ] [dependencies] @@ -46,12 +46,12 @@ log.workspace = true lsp.workspace = true node_runtime.workspace = true paths.workspace = true -pet.workspace = true -pet-fs.workspace = true -pet-core.workspace = true pet-conda.workspace = true +pet-core.workspace = true +pet-fs.workspace = true pet-poetry.workspace = true pet-reporter.workspace = true +pet.workspace = true project.workspace = true regex.workspace = true rope.workspace = true @@ -83,15 +83,15 @@ tree-sitter-yaml = { workspace = true, optional = true } util.workspace = true [dev-dependencies] -tree-sitter.workspace = true +pretty_assertions.workspace = true text.workspace = true theme = { workspace = true, features = ["test-support"] } -unindent.workspace = true -workspace = { workspace = true, features = ["test-support"] } -tree-sitter-typescript.workspace = true -tree-sitter-python.workspace = true -tree-sitter-go.workspace = true +tree-sitter-bash.workspace = true tree-sitter-c.workspace = true tree-sitter-css.workspace = true -tree-sitter-bash.workspace = true -pretty_assertions.workspace = true +tree-sitter-go.workspace = true +tree-sitter-python.workspace = true +tree-sitter-typescript.workspace = true +tree-sitter.workspace = true +unindent.workspace = true +workspace = { workspace = true, features = ["test-support"] } From cee4e25d5187a4d4e1de62de95cd527e9aeeab26 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 5 Feb 2025 12:26:11 -0300 Subject: [PATCH 521/650] edit predictions: Onboarding funnel telemetry (#24237) Release Notes: - N/A --- Cargo.lock | 1 + crates/editor/src/editor.rs | 14 +++++----- crates/inline_completion_button/Cargo.toml | 1 + .../src/inline_completion_button.rs | 20 +++++++++++++- .../zed/src/zed/inline_completion_registry.rs | 19 ++++++++++---- crates/zeta/src/onboarding_banner.rs | 4 +++ crates/zeta/src/onboarding_modal.rs | 26 ++++++++++++++++--- crates/zeta/src/onboarding_telemetry.rs | 9 +++++++ crates/zeta/src/rate_completion_modal.rs | 2 ++ crates/zeta/src/zeta.rs | 1 + 10 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 crates/zeta/src/onboarding_telemetry.rs diff --git a/Cargo.lock b/Cargo.lock index 909649188166df..682e4ca11e5f85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6503,6 +6503,7 @@ dependencies = [ "serde_json", "settings", "supermaven", + "telemetry", "theme", "ui", "workspace", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index efd1ed15e141ba..f2b265604898b6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3970,10 +3970,6 @@ impl Editor { self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) } - fn toggle_zed_predict_onboarding(&mut self, window: &mut Window, cx: &mut Context) { - window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx); - } - fn do_completion( &mut self, item_ix: Option, @@ -5704,7 +5700,11 @@ impl Editor { .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) .on_click(cx.listener(|this, _event, window, cx| { cx.stop_propagation(); - this.toggle_zed_predict_onboarding(window, cx) + this.report_editor_event("Edit Prediction Provider ToS Clicked", None, cx); + window.dispatch_action( + zed_actions::OpenZedPredictOnboarding.boxed_clone(), + cx, + ); })) .child( h_flex() @@ -14630,7 +14630,8 @@ impl Editor { .get("vim_mode") == Some(&serde_json::Value::Bool(true)); - let copilot_enabled = all_language_settings(file, cx).inline_completions.provider + let edit_predictions_provider = all_language_settings(file, cx).inline_completions.provider; + let copilot_enabled = edit_predictions_provider == language::language_settings::InlineCompletionProvider::Copilot; let copilot_enabled_for_language = self .buffer @@ -14645,6 +14646,7 @@ impl Editor { vim_mode, copilot_enabled, copilot_enabled_for_language, + edit_predictions_provider, is_via_ssh = project.is_via_ssh(), ); } diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index b5daba3893ae24..e8c51efcaf3405 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -29,6 +29,7 @@ workspace.workspace = true zed_actions.workspace = true zeta.workspace = true client.workspace = true +telemetry.workspace = true [dev-dependencies] copilot = { workspace = true, features = ["test-support"] } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 20cad6ec0b1e06..a2b72ed1c2ebc9 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -256,6 +256,10 @@ impl Render for InlineCompletionButton { ) }) .on_click(cx.listener(move |_, _, window, cx| { + telemetry::event!( + "Pending ToS Clicked", + source = "Edit Prediction Status Button" + ); window.dispatch_action( zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx, @@ -426,6 +430,8 @@ impl InlineCompletionButton { if data_collection.is_supported() { let provider = provider.clone(); + let enabled = data_collection.is_enabled(); + menu = menu .separator() .header("Help Improve The Model") @@ -434,9 +440,21 @@ impl InlineCompletionButton { // TODO: We want to add something later that communicates whether // the current project is open-source. ContextMenuEntry::new("Share Training Data") - .toggleable(IconPosition::Start, data_collection.is_enabled()) + .toggleable(IconPosition::Start, enabled) .handler(move |_, cx| { provider.toggle_data_collection(cx); + + if !enabled { + telemetry::event!( + "Data Collection Enabled", + source = "Edit Prediction Status Menu" + ); + } else { + telemetry::event!( + "Data Collection Disabled", + source = "Edit Prediction Status Menu" + ); + } }), ); } diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index 58faf1263d0422..6e2879a6c98a15 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/inline_completion_registry.rs @@ -94,7 +94,20 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { let user_store = user_store.clone(); move |cx| { let new_provider = all_language_settings(None, cx).inline_completions.provider; + if new_provider != provider { + let tos_accepted = user_store + .read(cx) + .current_user_has_accepted_terms() + .unwrap_or(false); + + telemetry::event!( + "Edit Prediction Provider Changed", + from = provider, + to = new_provider, + zed_ai_tos_accepted = tos_accepted, + ); + provider = new_provider; assign_inline_completion_providers( &editors, @@ -104,11 +117,7 @@ pub fn init(client: Arc, user_store: Entity, cx: &mut App) { cx, ); - if !user_store - .read(cx) - .current_user_has_accepted_terms() - .unwrap_or(false) - { + if !tos_accepted { match provider { InlineCompletionProvider::Zed => { let Some(window) = cx.active_window() else { diff --git a/crates/zeta/src/onboarding_banner.rs b/crates/zeta/src/onboarding_banner.rs index 26169b2cbffc3b..54a6939d6292c5 100644 --- a/crates/zeta/src/onboarding_banner.rs +++ b/crates/zeta/src/onboarding_banner.rs @@ -6,6 +6,8 @@ use settings::SettingsStore; use ui::{prelude::*, ButtonLike, Tooltip}; use util::ResultExt; +use crate::onboarding_event; + /// Prompts the user to try Zed's Edit Prediction feature pub struct ZedPredictBanner { dismissed: bool, @@ -53,6 +55,7 @@ impl ZedPredictBanner { } fn dismiss(&mut self, cx: &mut Context) { + onboarding_event!("Banner Dismissed"); persist_dismissed(cx); self.dismissed = true; cx.notify(); @@ -107,6 +110,7 @@ impl Render for ZedPredictBanner { ), ) .on_click(|_, window, cx| { + onboarding_event!("Banner Clicked"); window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx) }), ) diff --git a/crates/zeta/src/onboarding_modal.rs b/crates/zeta/src/onboarding_modal.rs index b9e214508cd122..c17289b78f13db 100644 --- a/crates/zeta/src/onboarding_modal.rs +++ b/crates/zeta/src/onboarding_modal.rs @@ -1,6 +1,6 @@ use std::{sync::Arc, time::Duration}; -use crate::ZED_PREDICT_DATA_COLLECTION_CHOICE; +use crate::{onboarding_event, ZED_PREDICT_DATA_COLLECTION_CHOICE}; use client::{Client, UserStore}; use db::kvp::KEY_VALUE_STORE; use feature_flags::FeatureFlagAppExt as _; @@ -61,16 +61,22 @@ impl ZedPredictModal { fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { cx.open_url("https://zed.dev/terms-of-service"); cx.notify(); + + onboarding_event!("ToS Link Clicked"); } fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { cx.open_url("https://zed.dev/blog/"); // TODO Add the link when live cx.notify(); + + onboarding_event!("Blog Link clicked"); } fn inline_completions_doc(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { cx.open_url("https://zed.dev/docs/configuring-zed#inline-completions"); cx.notify(); + + onboarding_event!("Docs Link Clicked"); } fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { @@ -106,6 +112,11 @@ impl ZedPredictModal { }) }) .detach_and_notify_err(window, cx); + + onboarding_event!( + "Enable Clicked", + data_collection_opted_in = self.data_collection_opted_in, + ); } fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { @@ -122,12 +133,15 @@ impl ZedPredictModal { this.update(&mut cx, |this, cx| { this.sign_in_status = status; + onboarding_event!("Signed In"); cx.notify() })?; result }) .detach_and_notify_err(window, cx); + + onboarding_event!("Sign In Clicked"); } fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { @@ -159,6 +173,7 @@ impl Render for ZedPredictModal { .track_focus(&self.focus_handle(cx)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { + onboarding_event!("Cancelled", trigger = "Action"); cx.emit(DismissEvent); })) .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| { @@ -241,6 +256,7 @@ impl Render for ZedPredictModal { .child(h_flex().absolute().top_2().right_2().child( IconButton::new("cancel", IconName::X).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { + onboarding_event!("Cancelled", trigger = "X click"); cx.emit(DismissEvent); }, )), @@ -302,7 +318,7 @@ impl Render for ZedPredictModal { .label("Read and accept the") .on_click(cx.listener(move |this, state, _window, cx| { this.terms_of_service = *state == ToggleState::Selected; - cx.notify() + cx.notify(); })), ) .child( @@ -340,7 +356,11 @@ impl Render for ZedPredictModal { .on_click(cx.listener(|this, _, _, cx| { this.data_collection_expanded = !this.data_collection_expanded; - cx.notify() + cx.notify(); + + if this.data_collection_expanded { + onboarding_event!("Data Collection Learn More Clicked"); + } })), ), ) diff --git a/crates/zeta/src/onboarding_telemetry.rs b/crates/zeta/src/onboarding_telemetry.rs new file mode 100644 index 00000000000000..3c7d5e1442947c --- /dev/null +++ b/crates/zeta/src/onboarding_telemetry.rs @@ -0,0 +1,9 @@ +#[macro_export] +macro_rules! onboarding_event { + ($name:expr) => { + telemetry::event!($name, source = "Edit Prediction Onboarding"); + }; + ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { + telemetry::event!($name, source = "Edit Prediction Onboarding", $($key $(= $value)?),+); + }; +} diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index 073388e22c9170..dda838c21b3b92 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -52,6 +52,8 @@ impl RateCompletionModal { pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { if let Some(zeta) = Zeta::global(cx) { workspace.toggle_modal(window, cx, |_window, cx| RateCompletionModal::new(zeta, cx)); + + telemetry::event!("Rate Completion Modal Open", source = "Edit Prediction"); } } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 7aa2dc212a3f91..584e4a8bb88a7c 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -3,6 +3,7 @@ mod init; mod license_detection; mod onboarding_banner; mod onboarding_modal; +mod onboarding_telemetry; mod rate_completion_modal; pub(crate) use completion_diff_element::*; From e21ea19fc629fc5871437579a331efbea2f3de3e Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:28:44 -0300 Subject: [PATCH 522/650] edit prediction: Don't log an error if license file isn't found (#24278) Logging an error in this case isn't super necessary. Release Notes: - N/A Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- crates/zeta/src/zeta.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 584e4a8bb88a7c..0be88b3c6a93a2 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -958,8 +958,7 @@ impl LicenseDetectionWatcher { Self { is_open_source_rx, _is_open_source_task: cx.spawn(|_, _| async move { - // TODO: Don't display error if file not found - let Some(loaded_file) = loaded_file_fut.await.log_err() else { + let Ok(loaded_file) = loaded_file_fut.await else { return; }; From 9c3e85a6ff989305553b6b6611d0e05f25231f84 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 5 Feb 2025 17:36:24 +0200 Subject: [PATCH 523/650] Rework shared commit editors (#24274) Rework of https://github.com/zed-industries/zed/pull/24130 Uses https://github.com/d1y/git_firefly/tree/1033c0b57ec88a002cb68efc64c8d9bf5c212e30 `COMMIT_EDITMSG` language-related definitions (thanks @d1y ) Instead of using real `.git/COMMIT_EDITMSG` file, create a buffer without FS representation, stored in the `Repository` and shared the regular way via the `BufferStore`. Adds a knowledge of what `Git Commit` language is, and uses it in the buffers which are rendered in the git panel. Release Notes: - N/A --------- Co-authored-by: Conrad Irwin Co-authored-by: d1y Co-authored-by: Smit --- Cargo.lock | 11 +- Cargo.toml | 1 + crates/git/src/repository.rs | 18 +- crates/git_ui/Cargo.toml | 1 - crates/git_ui/src/git_panel.rs | 198 +++++++---------- crates/git_ui/src/project_diff.rs | 75 +++---- crates/git_ui/src/repository_selector.rs | 12 +- crates/languages/Cargo.toml | 2 + crates/languages/src/gitcommit/config.toml | 18 ++ crates/languages/src/gitcommit/highlights.scm | 18 ++ crates/languages/src/gitcommit/injections.scm | 5 + crates/languages/src/lib.rs | 20 ++ crates/project/src/git.rs | 204 ++++++++++++------ crates/project/src/project.rs | 126 +++++------ crates/proto/proto/zed.proto | 1 + crates/remote_server/src/headless_project.rs | 86 +++----- crates/worktree/src/worktree.rs | 2 +- 17 files changed, 427 insertions(+), 371 deletions(-) create mode 100644 crates/languages/src/gitcommit/config.toml create mode 100644 crates/languages/src/gitcommit/highlights.scm create mode 100644 crates/languages/src/gitcommit/injections.scm diff --git a/Cargo.lock b/Cargo.lock index 682e4ca11e5f85..ad4b121f3957b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5369,7 +5369,6 @@ dependencies = [ "picker", "postage", "project", - "rpc", "schemars", "serde", "serde_derive", @@ -7123,6 +7122,7 @@ dependencies = [ "tree-sitter-cpp", "tree-sitter-css", "tree-sitter-diff", + "tree-sitter-gitcommit", "tree-sitter-go", "tree-sitter-gomod", "tree-sitter-gowork", @@ -14063,6 +14063,15 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-gitcommit" +version = "0.0.1" +source = "git+https://github.com/zed-industries/tree-sitter-git-commit?rev=88309716a69dd13ab83443721ba6e0b491d37ee9#88309716a69dd13ab83443721ba6e0b491d37ee9" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-go" version = "0.23.4" diff --git a/Cargo.toml b/Cargo.toml index 40bb2f9f4fb21d..9e55f821507eae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -530,6 +530,7 @@ tree-sitter-cpp = "0.23" tree-sitter-css = "0.23" tree-sitter-elixir = "0.3" tree-sitter-embedded-template = "0.23.0" +tree-sitter-gitcommit = {git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9"} tree-sitter-go = "0.23" tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" } tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index efedb0d461d70f..50191ea6836dc3 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1,6 +1,6 @@ use crate::status::FileStatus; +use crate::GitHostingProviderRegistry; use crate::{blame::Blame, status::GitStatus}; -use crate::{GitHostingProviderRegistry, COMMIT_MESSAGE}; use anyhow::{anyhow, Context as _, Result}; use collections::{HashMap, HashSet}; use git2::BranchType; @@ -68,7 +68,7 @@ pub trait GitRepository: Send + Sync { /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index. fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>; - fn commit(&self, name_and_email: Option<(&str, &str)>) -> Result<()>; + fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>; } impl std::fmt::Debug for dyn GitRepository { @@ -298,22 +298,14 @@ impl GitRepository for RealGitRepository { Ok(()) } - fn commit(&self, name_and_email: Option<(&str, &str)>) -> Result<()> { + fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> { let working_directory = self .repository .lock() .workdir() .context("failed to read git work directory")? .to_path_buf(); - let commit_file = self.dot_git_dir().join(*COMMIT_MESSAGE); - let commit_file_path = commit_file.to_string_lossy(); - let mut args = vec![ - "commit", - "--quiet", - "-F", - commit_file_path.as_ref(), - "--cleanup=strip", - ]; + let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"]; let author = name_and_email.map(|(name, email)| format!("{name} <{email}>")); if let Some(author) = author.as_deref() { args.push("--author"); @@ -480,7 +472,7 @@ impl GitRepository for FakeGitRepository { unimplemented!() } - fn commit(&self, _name_and_email: Option<(&str, &str)>) -> Result<()> { + fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> { unimplemented!() } } diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 055410760485f2..701f9a01d7014e 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -26,7 +26,6 @@ multi_buffer.workspace = true menu.workspace = true postage.workspace = true project.workspace = true -rpc.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 1e7ce96cef8def..9cf054467d8656 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4,7 +4,7 @@ use crate::ProjectDiff; use crate::{ git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, }; -use anyhow::{Context as _, Result}; +use anyhow::Result; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::actions::MoveToEnd; @@ -12,13 +12,12 @@ use editor::scroll::ScrollbarAutoHide; use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar}; use git::repository::RepoPath; use git::status::FileStatus; -use git::{CommitAllChanges, CommitChanges, ToggleStaged, COMMIT_MESSAGE}; +use git::{CommitAllChanges, CommitChanges, ToggleStaged}; use gpui::*; -use language::{Buffer, BufferId}; +use language::Buffer; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::git::{GitEvent, GitRepo, RepositoryHandle}; -use project::{CreateOptions, Fs, Project, ProjectPath}; -use rpc::proto; +use project::git::{GitEvent, Repository}; +use project::{Fs, Project, ProjectPath}; use serde::{Deserialize, Serialize}; use settings::Settings as _; use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize}; @@ -32,7 +31,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId}; use workspace::Toast; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - Item, Workspace, + Workspace, }; actions!( @@ -144,7 +143,7 @@ pub struct GitPanel { pending_serialization: Task>, workspace: WeakEntity, project: Entity, - active_repository: Option, + active_repository: Option>, scroll_handle: UniformListScrollHandle, scrollbar_state: ScrollbarState, selected_entry: Option, @@ -162,63 +161,6 @@ pub struct GitPanel { can_commit_all: bool, } -fn commit_message_buffer( - project: &Entity, - active_repository: &RepositoryHandle, - cx: &mut App, -) -> Task>> { - match &active_repository.git_repo { - GitRepo::Local(repo) => { - let commit_message_file = repo.dot_git_dir().join(*COMMIT_MESSAGE); - let fs = project.read(cx).fs().clone(); - let project = project.downgrade(); - cx.spawn(|mut cx| async move { - fs.create_file( - &commit_message_file, - CreateOptions { - overwrite: false, - ignore_if_exists: true, - }, - ) - .await - .with_context(|| format!("creating commit message file {commit_message_file:?}"))?; - let buffer = project - .update(&mut cx, |project, cx| { - project.open_local_buffer(&commit_message_file, cx) - })? - .await - .with_context(|| { - format!("opening commit message buffer at {commit_message_file:?}",) - })?; - Ok(buffer) - }) - } - GitRepo::Remote { - project_id, - client, - worktree_id, - work_directory_id, - } => { - let request = client.request(proto::OpenCommitMessageBuffer { - project_id: project_id.0, - worktree_id: worktree_id.to_proto(), - work_directory_id: work_directory_id.to_proto(), - }); - let project = project.downgrade(); - cx.spawn(|mut cx| async move { - let response = request.await.context("requesting to open commit buffer")?; - let buffer_id = BufferId::new(response.buffer_id)?; - let buffer = project - .update(&mut cx, { - |project, cx| project.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - Ok(buffer) - }) - } - } -} - fn commit_message_editor( commit_message_buffer: Option>, window: &mut Window, @@ -360,7 +302,7 @@ impl GitPanel { let Some(git_repo) = self.active_repository.as_ref() else { return; }; - let Some(repo_path) = git_repo.project_path_to_repo_path(&path) else { + let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else { return; }; let Some(ix) = self.entries_by_path.get(&repo_path) else { @@ -578,7 +520,7 @@ impl GitPanel { .active_repository .as_ref() .map_or(false, |active_repository| { - active_repository.entry_count() > 0 + active_repository.read(cx).entry_count() > 0 }); if have_entries && self.selected_entry.is_none() { self.selected_entry = Some(0); @@ -655,11 +597,17 @@ impl GitPanel { let repo_paths = repo_paths.clone(); let active_repository = active_repository.clone(); |this, mut cx| async move { - let result = if stage { - active_repository.stage_entries(repo_paths.clone()).await - } else { - active_repository.unstage_entries(repo_paths.clone()).await - }; + let result = cx + .update(|cx| { + if stage { + active_repository.read(cx).stage_entries(repo_paths.clone()) + } else { + active_repository + .read(cx) + .unstage_entries(repo_paths.clone()) + } + })? + .await?; this.update(&mut cx, |this, cx| { for pending in this.pending.iter_mut() { @@ -697,7 +645,9 @@ impl GitPanel { let Some(active_repository) = self.active_repository.as_ref() else { return; }; - let Some(path) = active_repository.repo_path_to_project_path(&status_entry.repo_path) + let Some(path) = active_repository + .read(cx) + .repo_path_to_project_path(&status_entry.repo_path) else { return; }; @@ -725,18 +675,18 @@ impl GitPanel { if !self.can_commit { return; } - if self.commit_editor.read(cx).is_empty(cx) { + let message = self.commit_editor.read(cx).text(cx); + if message.trim().is_empty() { return; } self.commit_pending = true; - let save_task = self.commit_editor.update(cx, |editor, cx| { - editor.save(false, self.project.clone(), window, cx) - }); let commit_editor = self.commit_editor.clone(); self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move { + let commit = active_repository.update(&mut cx, |active_repository, _| { + active_repository.commit(SharedString::from(message), name_and_email) + })?; let result = maybe!(async { - save_task.await?; - active_repository.commit(name_and_email).await?; + commit.await??; cx.update(|window, cx| { commit_editor.update(cx, |editor, cx| editor.clear(window, cx)); }) @@ -768,14 +718,12 @@ impl GitPanel { if !self.can_commit_all { return; } - if self.commit_editor.read(cx).is_empty(cx) { + + let message = self.commit_editor.read(cx).text(cx); + if message.trim().is_empty() { return; } self.commit_pending = true; - let save_task = self.commit_editor.update(cx, |editor, cx| { - editor.save(false, self.project.clone(), window, cx) - }); - let commit_editor = self.commit_editor.clone(); let tracked_files = self .entries @@ -790,9 +738,15 @@ impl GitPanel { self.commit_task = cx.spawn_in(window, |git_panel, mut cx| async move { let result = maybe!(async { - save_task.await?; - active_repository.stage_entries(tracked_files).await?; - active_repository.commit(name_and_email).await + cx.update(|_, cx| active_repository.read(cx).stage_entries(tracked_files))? + .await??; + cx.update(|_, cx| { + active_repository + .read(cx) + .commit(SharedString::from(message), name_and_email) + })? + .await??; + Ok(()) }) .await; cx.update(|window, cx| match result { @@ -886,47 +840,56 @@ impl GitPanel { window: &mut Window, cx: &mut Context, ) { - let project = self.project.clone(); let handle = cx.entity().downgrade(); + self.reopen_commit_buffer(window, cx); self.update_visible_entries_task = cx.spawn_in(window, |_, mut cx| async move { cx.background_executor().timer(UPDATE_DEBOUNCE).await; if let Some(git_panel) = handle.upgrade() { - let Ok(commit_message_buffer) = git_panel.update_in(&mut cx, |git_panel, _, cx| { - git_panel - .active_repository - .as_ref() - .map(|active_repository| { - commit_message_buffer(&project, active_repository, cx) - }) - }) else { - return; - }; - let commit_message_buffer = match commit_message_buffer { - Some(commit_message_buffer) => match commit_message_buffer - .await - .context("opening commit buffer on repo update") - .log_err() - { - Some(buffer) => Some(buffer), - None => return, - }, - None => None, - }; - git_panel - .update_in(&mut cx, |git_panel, window, cx| { - git_panel.update_visible_entries(cx); + .update_in(&mut cx, |git_panel, _, cx| { if clear_pending { git_panel.clear_pending(); } - git_panel.commit_editor = - cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx)); + git_panel.update_visible_entries(cx); }) .ok(); } }); } + fn reopen_commit_buffer(&mut self, window: &mut Window, cx: &mut Context) { + let Some(active_repo) = self.active_repository.as_ref() else { + return; + }; + let load_buffer = active_repo.update(cx, |active_repo, cx| { + let project = self.project.read(cx); + active_repo.open_commit_buffer( + Some(project.languages().clone()), + project.buffer_store().clone(), + cx, + ) + }); + + cx.spawn_in(window, |git_panel, mut cx| async move { + let buffer = load_buffer.await?; + git_panel.update_in(&mut cx, |git_panel, window, cx| { + if git_panel + .commit_editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .as_ref() + != Some(&buffer) + { + git_panel.commit_editor = + cx.new(|cx| commit_message_editor(Some(buffer), window, cx)); + } + }) + }) + .detach_and_log_err(cx); + } + fn clear_pending(&mut self) { self.pending.retain(|v| !v.finished) } @@ -944,6 +907,7 @@ impl GitPanel { }; // First pass - collect all paths + let repo = repo.read(cx); let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path)); let mut has_changed_checked_boxes = false; @@ -1117,7 +1081,7 @@ impl GitPanel { let entry_count = self .active_repository .as_ref() - .map_or(0, RepositoryHandle::entry_count); + .map_or(0, |repo| repo.read(cx).entry_count()); let changes_string = match entry_count { 0 => "No changes".to_string(), @@ -1151,7 +1115,7 @@ impl GitPanel { let active_repository = self.project.read(cx).active_repository(cx); let repository_display_name = active_repository .as_ref() - .map(|repo| repo.display_name(self.project.read(cx), cx)) + .map(|repo| repo.read(cx).display_name(self.project.read(cx), cx)) .unwrap_or_default(); let entry_count = self.entries.len(); @@ -1619,7 +1583,7 @@ impl Render for GitPanel { .active_repository .as_ref() .map_or(false, |active_repository| { - active_repository.entry_count() > 0 + active_repository.read(cx).entry_count() > 0 }); let room = self .workspace diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 789dc8c21dd826..a78f097e244d33 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -163,6 +163,7 @@ impl ProjectDiff { }; let Some(path) = git_repo + .read(cx) .repo_path_to_project_path(&entry.repo_path) .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx)) else { @@ -234,43 +235,45 @@ impl ProjectDiff { let mut previous_paths = self.multibuffer.read(cx).paths().collect::>(); let mut result = vec![]; - for entry in repo.status() { - if !entry.status.has_changes() { - continue; + repo.update(cx, |repo, cx| { + for entry in repo.status() { + if !entry.status.has_changes() { + continue; + } + let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { + continue; + }; + // Craft some artificial paths so that created entries will appear last. + let path_key = if entry.status.is_created() { + PathKey::namespaced(ADDED_NAMESPACE, &abs_path) + } else { + PathKey::namespaced(CHANGED_NAMESPACE, &abs_path) + }; + + previous_paths.remove(&path_key); + let load_buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + + let project = self.project.clone(); + result.push(cx.spawn(|_, mut cx| async move { + let buffer = load_buffer.await?; + let changes = project + .update(&mut cx, |project, cx| { + project.open_uncommitted_changes(buffer.clone(), cx) + })? + .await?; + Ok(DiffBuffer { + path_key, + buffer, + change_set: changes, + }) + })); } - let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else { - continue; - }; - let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { - continue; - }; - // Craft some artificial paths so that created entries will appear last. - let path_key = if entry.status.is_created() { - PathKey::namespaced(ADDED_NAMESPACE, &abs_path) - } else { - PathKey::namespaced(CHANGED_NAMESPACE, &abs_path) - }; - - previous_paths.remove(&path_key); - let load_buffer = self - .project - .update(cx, |project, cx| project.open_buffer(project_path, cx)); - - let project = self.project.clone(); - result.push(cx.spawn(|_, mut cx| async move { - let buffer = load_buffer.await?; - let changes = project - .update(&mut cx, |project, cx| { - project.open_uncommitted_changes(buffer.clone(), cx) - })? - .await?; - Ok(DiffBuffer { - path_key, - buffer, - change_set: changes, - }) - })); - } + }); self.multibuffer.update(cx, |multibuffer, cx| { for path in previous_paths { multibuffer.remove_excerpts_for_path(path, cx); diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index 9c7f5f4e077888..81d5f06635d6a7 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -4,7 +4,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; use project::{ - git::{GitState, RepositoryHandle}, + git::{GitState, Repository}, Project, }; use std::sync::Arc; @@ -117,13 +117,13 @@ impl RenderOnce for RepositorySelectorPopoverMenu { pub struct RepositorySelectorDelegate { project: WeakEntity, repository_selector: WeakEntity, - repository_entries: Vec, - filtered_repositories: Vec, + repository_entries: Vec>, + filtered_repositories: Vec>, selected_index: usize, } impl RepositorySelectorDelegate { - pub fn update_repository_entries(&mut self, all_repositories: Vec) { + pub fn update_repository_entries(&mut self, all_repositories: Vec>) { self.repository_entries = all_repositories.clone(); self.filtered_repositories = all_repositories; self.selected_index = 0; @@ -194,7 +194,7 @@ impl PickerDelegate for RepositorySelectorDelegate { let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else { return; }; - selected_repo.activate(cx); + selected_repo.update(cx, |selected_repo, cx| selected_repo.activate(cx)); self.dismissed(window, cx); } @@ -222,7 +222,7 @@ impl PickerDelegate for RepositorySelectorDelegate { ) -> Option { let project = self.project.upgrade()?; let repo_info = self.filtered_repositories.get(ix)?; - let display_name = repo_info.display_name(project.read(cx), cx); + let display_name = repo_info.read(cx).display_name(project.read(cx), cx); // TODO: Implement repository item rendering Some( ListItem::new(ix) diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 5665b9b53ab25d..99ee6997fda822 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -19,6 +19,7 @@ load-grammars = [ "tree-sitter-cpp", "tree-sitter-css", "tree-sitter-diff", + "tree-sitter-gitcommit", "tree-sitter-go", "tree-sitter-go-mod", "tree-sitter-gowork", @@ -69,6 +70,7 @@ tree-sitter-c = { workspace = true, optional = true } tree-sitter-cpp = { workspace = true, optional = true } tree-sitter-css = { workspace = true, optional = true } tree-sitter-diff = { workspace = true, optional = true } +tree-sitter-gitcommit = {workspace = true, optional = true } tree-sitter-go = { workspace = true, optional = true } tree-sitter-go-mod = { workspace = true, optional = true } tree-sitter-gowork = { workspace = true, optional = true } diff --git a/crates/languages/src/gitcommit/config.toml b/crates/languages/src/gitcommit/config.toml new file mode 100644 index 00000000000000..c8ffca31056acb --- /dev/null +++ b/crates/languages/src/gitcommit/config.toml @@ -0,0 +1,18 @@ +name = "Git Commit" +grammar = "git_commit" +path_suffixes = [ + "TAG_EDITMSG", + "MERGE_MSG", + "COMMIT_EDITMSG", + "NOTES_EDITMSG", + "EDIT_DESCRIPTION", +] +line_comments = ["#"] +brackets = [ + { start = "(", end = ")", close = true, newline = false }, + { start = "`", end = "`", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "'", end = "'", close = true, newline = false }, + { start = "{", end = "}", close = true, newline = false }, + { start = "[", end = "]", close = true, newline = false }, +] diff --git a/crates/languages/src/gitcommit/highlights.scm b/crates/languages/src/gitcommit/highlights.scm new file mode 100644 index 00000000000000..319d76569e56f1 --- /dev/null +++ b/crates/languages/src/gitcommit/highlights.scm @@ -0,0 +1,18 @@ +(subject) @markup.heading +(path) @string.special.path +(branch) @string.special.symbol +(commit) @constant +(item) @markup.link.url +(header) @tag + +(change kind: "new file" @diff.plus) +(change kind: "deleted" @diff.minus) +(change kind: "modified" @diff.delta) +(change kind: "renamed" @diff.delta.moved) + +(trailer + key: (trailer_key) @variable.other.member + value: (trailer_value) @string) + +[":" "=" "->" (scissors)] @punctuation.delimiter +(comment) @comment diff --git a/crates/languages/src/gitcommit/injections.scm b/crates/languages/src/gitcommit/injections.scm new file mode 100644 index 00000000000000..db0af176578cfe --- /dev/null +++ b/crates/languages/src/gitcommit/injections.scm @@ -0,0 +1,5 @@ +((scissors) @content + (#set! "language" "diff")) + +((rebase_command) @content + (#set! "language" "git_rebase")) diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 574af6dc23ed69..fbfe7b371ce1fc 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -31,6 +31,25 @@ mod yaml; #[exclude = "*.rs"] struct LanguageDir; +/// A shared grammar for plain text, exposed for reuse by downstream crates. +#[cfg(feature = "tree-sitter-gitcommit")] +pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock> = + std::sync::LazyLock::new(|| { + Arc::new(Language::new( + LanguageConfig { + name: "Git Commit".into(), + soft_wrap: Some(language::language_settings::SoftWrap::EditorWidth), + matcher: LanguageMatcher { + path_suffixes: vec!["COMMIT_EDITMSG".to_owned()], + first_line_pattern: None, + }, + line_comments: vec![Arc::from("#")], + ..LanguageConfig::default() + }, + Some(tree_sitter_gitcommit::LANGUAGE.into()), + )) + }); + pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mut App) { #[cfg(feature = "load-grammars")] languages.register_native_grammars([ @@ -53,6 +72,7 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu ("tsx", tree_sitter_typescript::LANGUAGE_TSX), ("typescript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT), ("yaml", tree_sitter_yaml::LANGUAGE), + ("gitcommit", tree_sitter_gitcommit::LANGUAGE), ]); macro_rules! language { diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index 90dff1ed93c797..38a891005916fb 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -1,6 +1,7 @@ +use crate::buffer_store::BufferStore; use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent}; use crate::{Project, ProjectPath}; -use anyhow::{anyhow, Context as _}; +use anyhow::Context as _; use client::ProjectId; use futures::channel::{mpsc, oneshot}; use futures::StreamExt as _; @@ -8,24 +9,28 @@ use git::{ repository::{GitRepository, RepoPath}, status::{GitSummary, TrackedSummary}, }; -use gpui::{App, Context, Entity, EventEmitter, SharedString, Subscription, WeakEntity}; +use gpui::{ + App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity, +}; +use language::{Buffer, LanguageRegistry}; use rpc::{proto, AnyProtoClient}; use settings::WorktreeId; use std::sync::Arc; +use text::BufferId; use util::{maybe, ResultExt}; use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry}; pub struct GitState { project_id: Option, client: Option, - repositories: Vec, + repositories: Vec>, active_index: Option, update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender>)>, _subscription: Subscription, } -#[derive(Clone)] -pub struct RepositoryHandle { +pub struct Repository { + commit_message_buffer: Option>, git_state: WeakEntity, pub worktree_id: WorktreeId, pub repository_entry: RepositoryEntry, @@ -44,25 +49,10 @@ pub enum GitRepo { }, } -impl PartialEq for RepositoryHandle { - fn eq(&self, other: &Self) -> bool { - self.worktree_id == other.worktree_id - && self.repository_entry.work_directory_id() - == other.repository_entry.work_directory_id() - } -} - -impl Eq for RepositoryHandle {} - -impl PartialEq for RepositoryHandle { - fn eq(&self, other: &RepositoryEntry) -> bool { - self.repository_entry.work_directory_id() == other.work_directory_id() - } -} - enum Message { Commit { git_repo: GitRepo, + message: SharedString, name_and_email: Option<(SharedString, SharedString)>, }, Stage(GitRepo, Vec), @@ -97,7 +87,7 @@ impl GitState { } } - pub fn active_repository(&self) -> Option { + pub fn active_repository(&self) -> Option> { self.active_index .map(|index| self.repositories[index].clone()) } @@ -118,7 +108,7 @@ impl GitState { worktree_store.update(cx, |worktree_store, cx| { for worktree in worktree_store.worktrees() { - worktree.update(cx, |worktree, _| { + worktree.update(cx, |worktree, cx| { let snapshot = worktree.snapshot(); for repo in snapshot.repositories().iter() { let git_repo = worktree @@ -139,27 +129,34 @@ impl GitState { let Some(git_repo) = git_repo else { continue; }; - let existing = self - .repositories - .iter() - .enumerate() - .find(|(_, existing_handle)| existing_handle == &repo); + let worktree_id = worktree.id(); + let existing = + self.repositories + .iter() + .enumerate() + .find(|(_, existing_handle)| { + existing_handle.read(cx).id() + == (worktree_id, repo.work_directory_id()) + }); let handle = if let Some((index, handle)) = existing { if self.active_index == Some(index) { new_active_index = Some(new_repositories.len()); } // Update the statuses but keep everything else. - let mut existing_handle = handle.clone(); - existing_handle.repository_entry = repo.clone(); + let existing_handle = handle.clone(); + existing_handle.update(cx, |existing_handle, _| { + existing_handle.repository_entry = repo.clone(); + }); existing_handle } else { - RepositoryHandle { + cx.new(|_| Repository { git_state: this.clone(), - worktree_id: worktree.id(), + worktree_id, repository_entry: repo.clone(), git_repo, update_sender: self.update_sender.clone(), - } + commit_message_buffer: None, + }) }; new_repositories.push(handle); } @@ -184,7 +181,7 @@ impl GitState { } } - pub fn all_repositories(&self) -> Vec { + pub fn all_repositories(&self) -> Vec> { self.repositories.clone() } @@ -260,10 +257,12 @@ impl GitState { } Message::Commit { git_repo, + message, name_and_email, } => { match git_repo { GitRepo::Local(repo) => repo.commit( + message.as_ref(), name_and_email .as_ref() .map(|(name, email)| (name.as_ref(), email.as_ref())), @@ -280,6 +279,7 @@ impl GitState { project_id: project_id.0, worktree_id: worktree_id.to_proto(), work_directory_id: work_directory_id.to_proto(), + message: String::from(message), name: name.map(String::from), email: email.map(String::from), }) @@ -293,7 +293,11 @@ impl GitState { } } -impl RepositoryHandle { +impl Repository { + fn id(&self) -> (WorktreeId, ProjectEntryId) { + (self.worktree_id, self.repository_entry.work_directory_id()) + } + pub fn display_name(&self, project: &Project, cx: &App) -> SharedString { maybe!({ let path = self.repo_path_to_project_path(&"".into())?; @@ -318,7 +322,7 @@ impl RepositoryHandle { .repositories .iter() .enumerate() - .find(|(_, handle)| handle == &self) + .find(|(_, handle)| handle.read(cx).id() == self.id()) else { return; }; @@ -343,47 +347,121 @@ impl RepositoryHandle { self.repository_entry.relativize(&path.path).log_err() } - pub async fn stage_entries(&self, entries: Vec) -> anyhow::Result<()> { - if entries.is_empty() { - return Ok(()); + pub fn open_commit_buffer( + &mut self, + languages: Option>, + buffer_store: Entity, + cx: &mut Context, + ) -> Task>> { + if let Some(buffer) = self.commit_message_buffer.clone() { + return Task::ready(Ok(buffer)); } + + if let GitRepo::Remote { + project_id, + client, + worktree_id, + work_directory_id, + } = self.git_repo.clone() + { + let client = client.clone(); + cx.spawn(|repository, mut cx| async move { + let request = client.request(proto::OpenCommitMessageBuffer { + project_id: project_id.0, + worktree_id: worktree_id.to_proto(), + work_directory_id: work_directory_id.to_proto(), + }); + let response = request.await.context("requesting to open commit buffer")?; + let buffer_id = BufferId::new(response.buffer_id)?; + let buffer = buffer_store + .update(&mut cx, |buffer_store, cx| { + buffer_store.wait_for_remote_buffer(buffer_id, cx) + })? + .await?; + if let Some(language_registry) = languages { + let git_commit_language = + language_registry.language_for_name("Git Commit").await?; + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(Some(git_commit_language), cx); + })?; + } + repository.update(&mut cx, |repository, _| { + repository.commit_message_buffer = Some(buffer.clone()); + })?; + Ok(buffer) + }) + } else { + self.open_local_commit_buffer(languages, buffer_store, cx) + } + } + + fn open_local_commit_buffer( + &mut self, + language_registry: Option>, + buffer_store: Entity, + cx: &mut Context, + ) -> Task>> { + cx.spawn(|repository, mut cx| async move { + let buffer = buffer_store + .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))? + .await?; + + if let Some(language_registry) = language_registry { + let git_commit_language = language_registry.language_for_name("Git Commit").await?; + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(Some(git_commit_language), cx); + })?; + } + + repository.update(&mut cx, |repository, _| { + repository.commit_message_buffer = Some(buffer.clone()); + })?; + Ok(buffer) + }) + } + + pub fn stage_entries(&self, entries: Vec) -> oneshot::Receiver> { let (result_tx, result_rx) = futures::channel::oneshot::channel(); + if entries.is_empty() { + result_tx.send(Ok(())).ok(); + return result_rx; + } self.update_sender .unbounded_send((Message::Stage(self.git_repo.clone(), entries), result_tx)) - .map_err(|_| anyhow!("Failed to submit stage operation"))?; - - result_rx.await? + .ok(); + result_rx } - pub async fn unstage_entries(&self, entries: Vec) -> anyhow::Result<()> { + pub fn unstage_entries(&self, entries: Vec) -> oneshot::Receiver> { + let (result_tx, result_rx) = futures::channel::oneshot::channel(); if entries.is_empty() { - return Ok(()); + result_tx.send(Ok(())).ok(); + return result_rx; } - let (result_tx, result_rx) = futures::channel::oneshot::channel(); self.update_sender .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), result_tx)) - .map_err(|_| anyhow!("Failed to submit unstage operation"))?; - result_rx.await? + .ok(); + result_rx } - pub async fn stage_all(&self) -> anyhow::Result<()> { + pub fn stage_all(&self) -> oneshot::Receiver> { let to_stage = self .repository_entry .status() .filter(|entry| !entry.status.is_staged().unwrap_or(false)) .map(|entry| entry.repo_path.clone()) .collect(); - self.stage_entries(to_stage).await + self.stage_entries(to_stage) } - pub async fn unstage_all(&self) -> anyhow::Result<()> { + pub fn unstage_all(&self) -> oneshot::Receiver> { let to_unstage = self .repository_entry .status() .filter(|entry| entry.status.is_staged().unwrap_or(true)) .map(|entry| entry.repo_path.clone()) .collect(); - self.unstage_entries(to_unstage).await + self.unstage_entries(to_unstage) } /// Get a count of all entries in the active repository, including @@ -404,18 +482,22 @@ impl RepositoryHandle { return self.have_changes() && (commit_all || self.have_staged_changes()); } - pub async fn commit( + pub fn commit( &self, + message: SharedString, name_and_email: Option<(SharedString, SharedString)>, - ) -> anyhow::Result<()> { + ) -> oneshot::Receiver> { let (result_tx, result_rx) = futures::channel::oneshot::channel(); - self.update_sender.unbounded_send(( - Message::Commit { - git_repo: self.git_repo.clone(), - name_and_email, - }, - result_tx, - ))?; - result_rx.await? + self.update_sender + .unbounded_send(( + Message::Commit { + git_repo: self.git_repo.clone(), + message, + name_and_email, + }, + result_tx, + )) + .ok(); + result_rx } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6bd4eed559c0e5..0070fc42a5273d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -24,7 +24,7 @@ mod project_tests; mod direnv; mod environment; pub use environment::EnvironmentErrorMessage; -use git::RepositoryHandle; +use git::Repository; pub mod search_history; mod yarn; @@ -60,7 +60,6 @@ use ::git::{ blame::Blame, repository::{Branch, GitRepository, RepoPath}, status::FileStatus, - COMMIT_MESSAGE, }; use gpui::{ AnyEntity, App, AppContext, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Hsla, @@ -2366,12 +2365,15 @@ impl Project { project_id, id: id.into(), }); - cx.spawn(move |this, mut cx| async move { + cx.spawn(move |project, mut cx| async move { let buffer_id = BufferId::new(request.await?.buffer_id)?; - this.update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(buffer_id, cx) - })? - .await + project + .update(&mut cx, |project, cx| { + project.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.wait_for_remote_buffer(buffer_id, cx) + }) + })? + .await }) } else { Task::ready(Err(anyhow!("cannot open buffer while disconnected"))) @@ -3279,16 +3281,21 @@ impl Project { let proto_client = ssh_client.read(cx).proto_client(); - cx.spawn(|this, mut cx| async move { + cx.spawn(|project, mut cx| async move { let buffer = proto_client .request(proto::OpenServerSettings { project_id: SSH_PROJECT_ID, }) .await?; - let buffer = this - .update(&mut cx, |this, cx| { - anyhow::Ok(this.wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx)) + let buffer = project + .update(&mut cx, |project, cx| { + project.buffer_store.update(cx, |buffer_store, cx| { + anyhow::Ok( + buffer_store + .wait_for_remote_buffer(BufferId::new(buffer.buffer_id)?, cx), + ) + }) })?? .await; @@ -3619,13 +3626,15 @@ impl Project { }); let guard = self.retain_remotely_created_models(cx); - cx.spawn(move |this, mut cx| async move { + cx.spawn(move |project, mut cx| async move { let response = request.await?; for buffer_id in response.buffer_ids { let buffer_id = BufferId::new(buffer_id)?; - let buffer = this - .update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(buffer_id, cx) + let buffer = project + .update(&mut cx, |project, cx| { + project.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.wait_for_remote_buffer(buffer_id, cx) + }) })? .await?; let _ = tx.send(buffer).await; @@ -4454,7 +4463,11 @@ impl Project { .map(RepoPath::new) .collect(); - repository_handle.stage_entries(entries).await?; + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.stage_entries(entries) + })? + .await??; Ok(proto::Ack {}) } @@ -4476,7 +4489,11 @@ impl Project { .map(RepoPath::new) .collect(); - repository_handle.unstage_entries(entries).await?; + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.unstage_entries(entries) + })? + .await??; Ok(proto::Ack {}) } @@ -4490,9 +4507,14 @@ impl Project { let repository_handle = Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + let message = SharedString::from(envelope.payload.message); let name = envelope.payload.name.map(SharedString::from); let email = envelope.payload.email.map(SharedString::from); - repository_handle.commit(name.zip(email)).await?; + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.commit(message, name.zip(email)) + })? + .await??; Ok(proto::Ack {}) } @@ -4505,55 +4527,12 @@ impl Project { let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); let repository_handle = Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - let git_repository = match &repository_handle.git_repo { - git::GitRepo::Local(git_repository) => git_repository.clone(), - git::GitRepo::Remote { .. } => { - anyhow::bail!("Cannot handle open commit message buffer for remote git repo") - } - }; - let commit_message_file = git_repository.dot_git_dir().join(*COMMIT_MESSAGE); - let fs = this.update(&mut cx, |project, _| project.fs().clone())?; - fs.create_file( - &commit_message_file, - CreateOptions { - overwrite: false, - ignore_if_exists: true, - }, - ) - .await - .with_context(|| format!("creating commit message file {commit_message_file:?}"))?; - - let (worktree, relative_path) = this - .update(&mut cx, |headless_project, cx| { - headless_project - .worktree_store - .update(cx, |worktree_store, cx| { - worktree_store.find_or_create_worktree(&commit_message_file, false, cx) - }) - })? - .await - .with_context(|| { - format!("deriving worktree for commit message file {commit_message_file:?}") - })?; - - let buffer = this - .update(&mut cx, |headless_project, cx| { - headless_project - .buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.open_buffer( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: Arc::from(relative_path), - }, - cx, - ) - }) - }) - .with_context(|| { - format!("opening buffer for commit message file {commit_message_file:?}") + let buffer = repository_handle + .update(&mut cx, |repository_handle, cx| { + repository_handle.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx) })? .await?; + let peer_id = envelope.original_sender_id()?; Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx) } @@ -4563,7 +4542,7 @@ impl Project { worktree_id: WorktreeId, work_directory_id: ProjectEntryId, cx: &mut AsyncApp, - ) -> Result { + ) -> Result> { this.update(cx, |project, cx| { let repository_handle = project .git_state() @@ -4571,6 +4550,7 @@ impl Project { .all_repositories() .into_iter() .find(|repository_handle| { + let repository_handle = repository_handle.read(cx); repository_handle.worktree_id == worktree_id && repository_handle.repository_entry.work_directory_id() == work_directory_id @@ -4616,16 +4596,6 @@ impl Project { buffer.read(cx).remote_id() } - pub fn wait_for_remote_buffer( - &mut self, - id: BufferId, - cx: &mut Context, - ) -> Task>> { - self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.wait_for_remote_buffer(id, cx) - }) - } - fn synchronize_remote_buffers(&mut self, cx: &mut Context) -> Task> { let project_id = match self.client_state { ProjectClientState::Remote { @@ -4785,11 +4755,11 @@ impl Project { &self.git_state } - pub fn active_repository(&self, cx: &App) -> Option { + pub fn active_repository(&self, cx: &App) -> Option> { self.git_state.read(cx).active_repository() } - pub fn all_repositories(&self, cx: &App) -> Vec { + pub fn all_repositories(&self, cx: &App) -> Vec> { self.git_state.read(cx).all_repositories() } } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index dcaff99a5ded98..d462a170abf242 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -3216,6 +3216,7 @@ message Commit { uint64 work_directory_id = 3; optional string name = 4; optional string email = 5; + string message = 6; } message OpenCommitMessageBuffer { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index f1d552b70ba96a..30aeca631165d2 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Context as _, Result}; use extension::ExtensionHostProxy; use extension_host::headless_host::HeadlessExtensionStore; -use fs::{CreateOptions, Fs}; -use git::{repository::RepoPath, COMMIT_MESSAGE}; +use fs::Fs; +use git::repository::RepoPath; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString}; use http_client::HttpClient; use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry}; @@ -10,7 +10,7 @@ use node_runtime::NodeRuntime; use project::{ buffer_store::{BufferStore, BufferStoreEvent}, dap_store::DapStore, - git::{GitRepo, GitState, RepositoryHandle}, + git::{GitState, Repository}, project_settings::SettingsObserver, search::SearchQuery, task_store::TaskStore, @@ -654,7 +654,11 @@ impl HeadlessProject { .map(RepoPath::new) .collect(); - repository_handle.stage_entries(entries).await?; + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.stage_entries(entries) + })? + .await??; Ok(proto::Ack {}) } @@ -676,7 +680,11 @@ impl HeadlessProject { .map(RepoPath::new) .collect(); - repository_handle.unstage_entries(entries).await?; + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.unstage_entries(entries) + })? + .await??; Ok(proto::Ack {}) } @@ -691,10 +699,15 @@ impl HeadlessProject { let repository_handle = Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + let message = SharedString::from(envelope.payload.message); let name = envelope.payload.name.map(SharedString::from); let email = envelope.payload.email.map(SharedString::from); - repository_handle.commit(name.zip(email)).await?; + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.commit(message, name.zip(email)) + })? + .await??; Ok(proto::Ack {}) } @@ -705,55 +718,11 @@ impl HeadlessProject { ) -> Result { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = + let repository = Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - let git_repository = match &repository_handle.git_repo { - GitRepo::Local(git_repository) => git_repository.clone(), - GitRepo::Remote { .. } => { - anyhow::bail!("Cannot handle open commit message buffer for remote git repo") - } - }; - let commit_message_file = git_repository.dot_git_dir().join(*COMMIT_MESSAGE); - let fs = this.update(&mut cx, |headless_project, _| headless_project.fs.clone())?; - fs.create_file( - &commit_message_file, - CreateOptions { - overwrite: false, - ignore_if_exists: true, - }, - ) - .await - .with_context(|| format!("creating commit message file {commit_message_file:?}"))?; - - let (worktree, relative_path) = this - .update(&mut cx, |headless_project, cx| { - headless_project - .worktree_store - .update(cx, |worktree_store, cx| { - worktree_store.find_or_create_worktree(&commit_message_file, false, cx) - }) - })? - .await - .with_context(|| { - format!("deriving worktree for commit message file {commit_message_file:?}") - })?; - - let buffer = this - .update(&mut cx, |headless_project, cx| { - headless_project - .buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.open_buffer( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: Arc::from(relative_path), - }, - cx, - ) - }) - }) - .with_context(|| { - format!("opening buffer for commit message file {commit_message_file:?}") + let buffer = repository + .update(&mut cx, |repository, cx| { + repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx) })? .await?; @@ -778,7 +747,7 @@ impl HeadlessProject { worktree_id: WorktreeId, work_directory_id: ProjectEntryId, cx: &mut AsyncApp, - ) -> Result { + ) -> Result> { this.update(cx, |project, cx| { let repository_handle = project .git_state @@ -786,8 +755,11 @@ impl HeadlessProject { .all_repositories() .into_iter() .find(|repository_handle| { - repository_handle.worktree_id == worktree_id - && repository_handle.repository_entry.work_directory_id() + repository_handle.read(cx).worktree_id == worktree_id + && repository_handle + .read(cx) + .repository_entry + .work_directory_id() == work_directory_id }) .context("missing repository handle")?; diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 8ba52747d1f50a..7084fc7d3bab71 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -199,7 +199,7 @@ pub struct RepositoryEntry { /// - my_sub_folder_1/project_root/changed_file_1 /// - my_sub_folder_2/changed_file_2 pub(crate) statuses_by_path: SumTree, - pub work_directory_id: ProjectEntryId, + work_directory_id: ProjectEntryId, pub work_directory: WorkDirectory, pub(crate) branch: Option>, } From dac28730e5f077d86b77de4bf6e7f4037a568e93 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 5 Feb 2025 10:37:51 -0500 Subject: [PATCH 524/650] Fix the worktree's repository_for_path (#24279) Go back to a less optimized implementation for now since the custom cursor target seems to have some bugs. Release Notes: - Fixed missing git blame and status output in some projects with multiple git repositories --- crates/worktree/src/worktree.rs | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 7084fc7d3bab71..cc6075f07a68af 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2682,21 +2682,10 @@ impl Snapshot { /// Get the repository whose work directory contains the given path. pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> { - let mut cursor = self.repositories.cursor::(&()); - let mut repository = None; - - // Git repositories may contain other git repositories. As a side effect of - // lexicographic sorting by path, deeper repositories will be after higher repositories - // So, let's loop through every matching repository until we can't find any more to find - // the deepest repository that could contain this path. - while cursor.seek_forward(&PathTarget::Contains(path), Bias::Left, &()) - && cursor.item().is_some() - { - repository = cursor.item(); - cursor.next(&()); - } - - repository + self.repositories + .iter() + .filter(|repo| repo.work_directory.directory_contains(path)) + .last() } /// Given an ordered iterator of entries, returns an iterator of those entries, @@ -5982,7 +5971,6 @@ impl<'a> Iterator for Traversal<'a> { enum PathTarget<'a> { Path(&'a Path), Successor(&'a Path), - Contains(&'a Path), } impl<'a> PathTarget<'a> { @@ -5996,13 +5984,6 @@ impl<'a> PathTarget<'a> { Ordering::Equal } } - PathTarget::Contains(path) => { - if path.starts_with(other) { - Ordering::Equal - } else { - Ordering::Greater - } - } } } } From b6182d078198d76627ef7336d17340a2d4ee1a63 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:15:41 -0300 Subject: [PATCH 525/650] edit prediction: Fix license detection error logging + check for different spellings (#24281) Follow-up to https://github.com/zed-industries/zed/pull/24278 This PR ensures we're checking if there's a license-type file in both US & UK English spelling, and fixes the error logging again, treating for when the worktree contains just a single file or multiple. Release Notes: - N/A Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- crates/zeta/src/zeta.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 0be88b3c6a93a2..6e68a957c9bd92 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -953,20 +953,33 @@ impl LicenseDetectionWatcher { pub fn new(worktree: &Worktree, cx: &mut Context) -> Self { let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::(false); - let loaded_file_fut = worktree.load_file(Path::new("LICENSE"), cx); + const LICENSE_FILES_TO_CHECK: [&'static str; 2] = ["LICENSE", "LICENCE"]; // US and UK English spelling - Self { - is_open_source_rx, - _is_open_source_task: cx.spawn(|_, _| async move { - let Ok(loaded_file) = loaded_file_fut.await else { - return; - }; + // Check if worktree is a single file, if so we do not need to check for a LICENSE file + let task = if worktree.abs_path().is_file() { + Task::ready(()) + } else { + let loaded_files_task = futures::future::join_all( + LICENSE_FILES_TO_CHECK + .iter() + .map(|file| worktree.load_file(Path::new(file), cx)), + ); - let is_loaded_file_open_source_thing: bool = - is_license_eligible_for_data_collection(&loaded_file.text); + cx.background_executor().spawn(async move { + for loaded_file in loaded_files_task.await { + if let Some(content) = loaded_file.log_err() { + if is_license_eligible_for_data_collection(&content.text) { + *is_open_source_tx.borrow_mut() = true; + break; + } + } + } + }) + }; - *is_open_source_tx.borrow_mut() = is_loaded_file_open_source_thing; - }), + Self { + is_open_source_rx, + _is_open_source_task: task, } } From 2973b46c78b7f9f259de9279236cc0dd77b17472 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:39:27 -0300 Subject: [PATCH 526/650] Revise the `MessageNotification` component (#24287) This PR makes adding icons to the primary and secondary actions, in the `MessageNotification` component, optional. Also took the opportunity to remove a probably unnecessary "third action" from it; streamlining the component API (we had added that for a design that we're not using anymore). I did keep the "more info" possibility, which may be useful in the future, though. Release Notes: - N/A --- crates/extensions_ui/src/extension_suggest.rs | 13 +- crates/workspace/src/notifications.rs | 157 +++++++++--------- crates/workspace/src/workspace.rs | 5 +- crates/zed/src/zed.rs | 15 +- 4 files changed, 101 insertions(+), 89 deletions(-) diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index c131b4c3cf0a05..4844dce7558837 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -7,6 +7,7 @@ use editor::Editor; use extension_host::ExtensionStore; use gpui::{AppContext as _, Context, Entity, SharedString, Window}; use language::Buffer; +use ui::prelude::*; use workspace::notifications::simple_message_notification::MessageNotification; use workspace::{notifications::NotificationId, Workspace}; @@ -172,8 +173,10 @@ pub(crate) fn suggest(buffer: Entity, window: &mut Window, cx: &mut Cont "Do you want to install the recommended '{}' extension for '{}' files?", extension_id, file_name_or_extension )) - .with_click_message("Yes, install extension") - .on_click({ + .primary_message("Yes, install extension") + .primary_icon(IconName::Check) + .primary_icon_color(Color::Success) + .primary_on_click({ let extension_id = extension_id.clone(); move |_window, cx| { let extension_id = extension_id.clone(); @@ -183,8 +186,10 @@ pub(crate) fn suggest(buffer: Entity, window: &mut Window, cx: &mut Cont }); } }) - .with_secondary_click_message("No, don't install it") - .on_secondary_click(move |_window, cx| { + .secondary_message("No, don't install it") + .secondary_icon(IconName::Close) + .secondary_icon_color(Color::Error) + .secondary_on_click(move |_window, cx| { let key = language_extension_key(&extension_id); db::write_and_log(cx, move || { KEY_VALUE_STORE.write_kvp(key, "dismissed".to_string()) diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 3a850e5e5c47ed..dacca6067ef03a 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -124,8 +124,8 @@ impl Workspace { Some((click_msg, on_click)) => { let on_click = on_click.clone(); simple_message_notification::MessageNotification::new(toast.msg.clone()) - .with_click_message(click_msg.clone()) - .on_click(move |window, cx| on_click(window, cx)) + .primary_message(click_msg.clone()) + .primary_on_click(move |window, cx| on_click(window, cx)) } None => simple_message_notification::MessageNotification::new(toast.msg.clone()), }) @@ -375,12 +375,14 @@ pub mod simple_message_notification { pub struct MessageNotification { build_content: Box) -> AnyElement>, - on_click: Option)>>, - click_message: Option, - secondary_click_message: Option, + primary_message: Option, + primary_icon: Option, + primary_icon_color: Option, + primary_on_click: Option)>>, + secondary_message: Option, + secondary_icon: Option, + secondary_icon_color: Option, secondary_on_click: Option)>>, - tertiary_click_message: Option, - tertiary_on_click: Option)>>, more_info_message: Option, more_info_url: Option>, show_close_button: bool, @@ -404,12 +406,14 @@ pub mod simple_message_notification { { Self { build_content: Box::new(content), - on_click: None, - click_message: None, + primary_message: None, + primary_icon: None, + primary_icon_color: None, + primary_on_click: None, + secondary_message: None, + secondary_icon: None, + secondary_icon_color: None, secondary_on_click: None, - secondary_click_message: None, - tertiary_on_click: None, - tertiary_click_message: None, more_info_message: None, more_info_url: None, show_close_button: true, @@ -417,51 +421,55 @@ pub mod simple_message_notification { } } - pub fn with_click_message(mut self, message: S) -> Self + pub fn primary_message(mut self, message: S) -> Self where S: Into, { - self.click_message = Some(message.into()); + self.primary_message = Some(message.into()); self } - pub fn on_click(mut self, on_click: F) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.on_click = Some(Arc::new(on_click)); + pub fn primary_icon(mut self, icon: IconName) -> Self { + self.primary_icon = Some(icon); self } - pub fn with_secondary_click_message(mut self, message: S) -> Self - where - S: Into, - { - self.secondary_click_message = Some(message.into()); + pub fn primary_icon_color(mut self, color: Color) -> Self { + self.primary_icon_color = Some(color); self } - pub fn on_secondary_click(mut self, on_click: F) -> Self + pub fn primary_on_click(mut self, on_click: F) -> Self where F: 'static + Fn(&mut Window, &mut Context), { - self.secondary_on_click = Some(Arc::new(on_click)); + self.primary_on_click = Some(Arc::new(on_click)); self } - pub fn with_tertiary_click_message(mut self, message: S) -> Self + pub fn secondary_message(mut self, message: S) -> Self where S: Into, { - self.tertiary_click_message = Some(message.into()); + self.secondary_message = Some(message.into()); self } - pub fn on_tertiary_click(mut self, on_click: F) -> Self + pub fn secondary_icon(mut self, icon: IconName) -> Self { + self.secondary_icon = Some(icon); + self + } + + pub fn secondary_icon_color(mut self, color: Color) -> Self { + self.secondary_icon_color = Some(color); + self + } + + pub fn secondary_on_click(mut self, on_click: F) -> Self where F: 'static + Fn(&mut Window, &mut Context), { - self.tertiary_on_click = Some(Arc::new(on_click)); + self.secondary_on_click = Some(Arc::new(on_click)); self } @@ -529,66 +537,63 @@ pub mod simple_message_notification { .child( h_flex() .gap_1() - .children(self.click_message.iter().map(|message| { - Button::new(message.clone(), message.clone()) + .children(self.primary_message.iter().map(|message| { + let mut button = Button::new(message.clone(), message.clone()) .label_size(LabelSize::Small) - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Success) .on_click(cx.listener(|this, _, window, cx| { - if let Some(on_click) = this.on_click.as_ref() { + if let Some(on_click) = this.primary_on_click.as_ref() { (on_click)(window, cx) }; this.dismiss(cx) - })) + })); + + if let Some(icon) = self.primary_icon { + button = button + .icon(icon) + .icon_color(self.primary_icon_color.unwrap_or(Color::Muted)) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small); + } + + button })) - .children(self.secondary_click_message.iter().map(|message| { - Button::new(message.clone(), message.clone()) + .children(self.secondary_message.iter().map(|message| { + let mut button = Button::new(message.clone(), message.clone()) .label_size(LabelSize::Small) - .icon(IconName::Close) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Error) .on_click(cx.listener(|this, _, window, cx| { if let Some(on_click) = this.secondary_on_click.as_ref() { (on_click)(window, cx) }; this.dismiss(cx) - })) + })); + + if let Some(icon) = self.secondary_icon { + button = button + .icon(icon) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted)); + } + + button })) .child( - h_flex() - .w_full() - .gap_1() - .justify_end() - .children(self.tertiary_click_message.iter().map(|message| { - Button::new(message.clone(), message.clone()) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(on_click) = this.tertiary_on_click.as_ref() - { - (on_click)(window, cx) - }; - this.dismiss(cx) - })) - })) - .children( - self.more_info_message - .iter() - .zip(self.more_info_url.iter()) - .map(|(message, url)| { - let url = url.clone(); - Button::new(message.clone(), message.clone()) - .label_size(LabelSize::Small) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .on_click(cx.listener(move |_, _, _, cx| { - cx.open_url(&url); - })) - }), - ), + h_flex().w_full().justify_end().children( + self.more_info_message + .iter() + .zip(self.more_info_url.iter()) + .map(|(message, url)| { + let url = url.clone(); + Button::new(message.clone(), message.clone()) + .label_size(LabelSize::Small) + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::Indicator) + .icon_color(Color::Muted) + .on_click(cx.listener(move |_, _, _, cx| { + cx.open_url(&url); + })) + }), + ), ), ) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a81958d29b82b0..80d40fec6fe576 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5249,8 +5249,9 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA |cx| { cx.new(|_| { MessageNotification::new("Failed to load the database file.") - .with_click_message("File an issue") - .on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL)) + .primary_message("File an Issue") + .primary_icon(IconName::Plus) + .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL)) }) }, ); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 745e31a8eb0bce..86334331a63a8f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -53,7 +53,7 @@ use std::time::Duration; use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc}; use terminal_view::terminal_panel::{self, TerminalPanel}; use theme::{ActiveTheme, ThemeSettings}; -use ui::PopoverMenuHandle; +use ui::{prelude::*, PopoverMenuHandle}; use util::markdown::MarkdownString; use util::{asset_str, ResultExt}; use uuid::Uuid; @@ -1175,8 +1175,8 @@ fn show_keymap_file_json_error( show_app_notification(notification_id, cx, move |cx| { cx.new(|_cx| { MessageNotification::new(message.clone()) - .with_click_message("Open keymap file") - .on_click(|window, cx| { + .primary_message("Open Keymap File") + .primary_on_click(|window, cx| { window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx); cx.emit(DismissEvent); }) @@ -1218,8 +1218,8 @@ fn show_keymap_file_load_error( )) .into_any() }) - .with_click_message("Open keymap file") - .on_click(|window, cx| { + .primary_message("Open Keymap File") + .primary_on_click(|window, cx| { window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx); cx.emit(DismissEvent); }) @@ -1271,8 +1271,9 @@ pub fn handle_settings_changed(error: Option, cx: &mut App) { show_app_notification(id, cx, move |cx| { cx.new(|_cx| { MessageNotification::new(format!("Invalid user settings file\n{error}")) - .with_click_message("Open settings file") - .on_click(|window, cx| { + .primary_message("Open Settings File") + .primary_icon(IconName::Settings) + .primary_on_click(|window, cx| { window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx); cx.emit(DismissEvent); }) From 9ef8aceb81ef0ee047100a6d46942e4a04568e7a Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 5 Feb 2025 18:09:19 +0100 Subject: [PATCH 527/650] edit prediction: Improve UX around `disabled_globs` and `show_inline_completions` (#24207) Release Notes: - N/A --------- Co-authored-by: Danilo Co-authored-by: Danilo Leal --- Cargo.lock | 1 + assets/icons/zed_predict_disabled.svg | 6 + .../src/copilot_completion_provider.rs | 21 +- crates/editor/src/editor.rs | 143 +++-- crates/inline_completion_button/Cargo.toml | 5 +- .../src/inline_completion_button.rs | 250 +++++---- crates/language/src/language_settings.rs | 13 +- .../src/supermaven_completion_provider.rs | 14 +- crates/ui/src/components/context_menu.rs | 491 +++++++++++------- crates/ui/src/components/icon.rs | 1 + crates/vim/src/vim.rs | 2 +- crates/zed/src/zed/quick_action_bar.rs | 35 +- crates/zeta/src/zeta.rs | 15 +- 13 files changed, 578 insertions(+), 419 deletions(-) create mode 100644 assets/icons/zed_predict_disabled.svg diff --git a/Cargo.lock b/Cargo.lock index ad4b121f3957b6..f6dc64534df9a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6499,6 +6499,7 @@ dependencies = [ "lsp", "paths", "project", + "regex", "serde_json", "settings", "supermaven", diff --git a/assets/icons/zed_predict_disabled.svg b/assets/icons/zed_predict_disabled.svg new file mode 100644 index 00000000000000..d10c4d560a88c7 --- /dev/null +++ b/assets/icons/zed_predict_disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 9c25e295aa91f7..f953e5a1100371 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -2,10 +2,7 @@ use crate::{Completion, Copilot}; use anyhow::Result; use gpui::{App, Context, Entity, EntityId, Task}; use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider}; -use language::{ - language_settings::{all_language_settings, AllLanguageSettings}, - Buffer, OffsetRangeExt, ToOffset, -}; +use language::{language_settings::AllLanguageSettings, Buffer, OffsetRangeExt, ToOffset}; use settings::Settings; use std::{path::Path, time::Duration}; @@ -73,19 +70,11 @@ impl InlineCompletionProvider for CopilotCompletionProvider { fn is_enabled( &self, - buffer: &Entity, - cursor_position: language::Anchor, + _buffer: &Entity, + _cursor_position: language::Anchor, cx: &App, ) -> bool { - if !self.copilot.read(cx).status().is_authorized() { - return false; - } - - let buffer = buffer.read(cx); - let file = buffer.file(); - let language = buffer.language_at(cursor_position); - let settings = all_language_settings(file, cx); - settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) + self.copilot.read(cx).status().is_authorized() } fn refresh( @@ -205,7 +194,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { fn discard(&mut self, cx: &mut Context) { let settings = AllLanguageSettings::get_global(cx); - let copilot_enabled = settings.inline_completions_enabled(None, None, cx); + let copilot_enabled = settings.show_inline_completions(None, cx); if !copilot_enabled { return; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f2b265604898b6..5b1068d96d6e68 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -688,7 +688,7 @@ pub struct Editor { stale_inline_completion_in_menu: Option, // enable_inline_completions is a switch that Vim can use to disable // edit predictions based on its mode. - enable_inline_completions: bool, + show_inline_completions: bool, show_inline_completions_override: Option, menu_inline_completions_policy: MenuInlineCompletionsPolicy, inlay_hint_cache: InlayHintCache, @@ -1408,7 +1408,7 @@ impl Editor { next_editor_action_id: EditorActionId::default(), editor_actions: Rc::default(), show_inline_completions_override: None, - enable_inline_completions: true, + show_inline_completions: true, menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, custom_context_menu: None, show_git_blame_gutter: false, @@ -1842,9 +1842,9 @@ impl Editor { self.input_enabled = input_enabled; } - pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut Context) { - self.enable_inline_completions = enabled; - if !self.enable_inline_completions { + pub fn set_show_inline_completions_enabled(&mut self, enabled: bool, cx: &mut Context) { + self.show_inline_completions = enabled; + if !self.show_inline_completions { self.take_active_inline_completion(cx); cx.notify(); } @@ -1895,8 +1895,11 @@ impl Editor { if let Some((buffer, cursor_buffer_position)) = self.buffer.read(cx).text_anchor_for_position(cursor, cx) { - let show_inline_completions = - !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx); + let show_inline_completions = !self.should_show_inline_completions_in_buffer( + &buffer, + cursor_buffer_position, + cx, + ); self.set_show_inline_completions(Some(show_inline_completions), window, cx); } } @@ -1912,42 +1915,6 @@ impl Editor { self.refresh_inline_completion(false, true, window, cx); } - pub fn inline_completions_enabled(&self, cx: &App) -> bool { - let cursor = self.selections.newest_anchor().head(); - if let Some((buffer, buffer_position)) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx) - { - self.should_show_inline_completions(&buffer, buffer_position, cx) - } else { - false - } - } - - fn should_show_inline_completions( - &self, - buffer: &Entity, - buffer_position: language::Anchor, - cx: &App, - ) -> bool { - if !self.snippet_stack.is_empty() { - return false; - } - - if self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) { - return false; - } - - if let Some(provider) = self.inline_completion_provider() { - if let Some(show_inline_completions) = self.show_inline_completions_override { - show_inline_completions - } else { - self.mode == EditorMode::Full && provider.is_enabled(buffer, buffer_position, cx) - } - } else { - false - } - } - fn inline_completions_disabled_in_scope( &self, buffer: &Entity, @@ -4674,9 +4641,18 @@ impl Editor { let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + if !self.inline_completions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { + self.discard_inline_completion(false, cx); + return None; + } + if !user_requested - && (!self.enable_inline_completions - || !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx) + && (!self.show_inline_completions + || !self.should_show_inline_completions_in_buffer( + &buffer, + cursor_buffer_position, + cx, + ) || !self.is_focused(window) || buffer.read(cx).is_empty()) { @@ -4689,6 +4665,77 @@ impl Editor { Some(()) } + pub fn should_show_inline_completions(&self, cx: &App) -> bool { + let cursor = self.selections.newest_anchor().head(); + if let Some((buffer, cursor_position)) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx) + { + self.should_show_inline_completions_in_buffer(&buffer, cursor_position, cx) + } else { + false + } + } + + fn should_show_inline_completions_in_buffer( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> bool { + if !self.snippet_stack.is_empty() { + return false; + } + + if self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) { + return false; + } + + if let Some(show_inline_completions) = self.show_inline_completions_override { + show_inline_completions + } else { + let buffer = buffer.read(cx); + self.mode == EditorMode::Full + && language_settings( + buffer.language_at(buffer_position).map(|l| l.name()), + buffer.file(), + cx, + ) + .show_inline_completions + } + } + + pub fn inline_completions_enabled(&self, cx: &App) -> bool { + let cursor = self.selections.newest_anchor().head(); + if let Some((buffer, cursor_position)) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx) + { + self.inline_completions_enabled_in_buffer(&buffer, cursor_position, cx) + } else { + false + } + } + + fn inline_completions_enabled_in_buffer( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> bool { + maybe!({ + let provider = self.inline_completion_provider()?; + if !provider.is_enabled(&buffer, buffer_position, cx) { + return Some(false); + } + let buffer = buffer.read(cx); + let Some(file) = buffer.file() else { + return Some(true); + }; + let settings = all_language_settings(Some(file), cx); + Some(settings.inline_completions_enabled_for_path(file.path())) + }) + .unwrap_or(false) + } + fn cycle_inline_completion( &mut self, direction: Direction, @@ -4699,8 +4746,8 @@ impl Editor { let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if !self.enable_inline_completions - || !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx) + if !self.show_inline_completions + || !self.should_show_inline_completions_in_buffer(&buffer, cursor_buffer_position, cx) { return None; } @@ -5038,7 +5085,7 @@ impl Editor { || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); if completions_menu_has_precedence || !offset_selection.is_empty() - || !self.enable_inline_completions + || !self.show_inline_completions || self .active_inline_completion .as_ref() diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index e8c51efcaf3405..973e7d327301d1 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] anyhow.workspace = true +client.workspace = true copilot.workspace = true editor.workspace = true feature_flags.workspace = true @@ -22,14 +23,14 @@ gpui.workspace = true inline_completion.workspace = true language.workspace = true paths.workspace = true +regex.workspace = true settings.workspace = true supermaven.workspace = true +telemetry.workspace = true ui.workspace = true workspace.workspace = true zed_actions.workspace = true zeta.workspace = true -client.workspace = true -telemetry.workspace = true [dev-dependencies] copilot = { workspace = true, features = ["test-support"] } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index a2b72ed1c2ebc9..447141864688bf 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -17,8 +17,12 @@ use language::{ }, File, Language, }; +use regex::Regex; use settings::{update_settings_file, Settings, SettingsStore}; -use std::{path::Path, sync::Arc, time::Duration}; +use std::{ + sync::{Arc, LazyLock}, + time::Duration, +}; use supermaven::{AccountStatus, Supermaven}; use ui::{ prelude::*, Clickable, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, PopoverMenu, @@ -71,9 +75,7 @@ impl Render for InlineCompletionButton { }; let status = copilot.read(cx).status(); - let enabled = self.editor_enabled.unwrap_or_else(|| { - all_language_settings.inline_completions_enabled(None, None, cx) - }); + let enabled = self.editor_enabled.unwrap_or(false); let icon = match status { Status::Error(_) => IconName::CopilotError, @@ -228,25 +230,35 @@ impl Render for InlineCompletionButton { return div(); } - fn icon_button() -> IconButton { - IconButton::new("zed-predict-pending-button", IconName::ZedPredict) - .shape(IconButtonShape::Square) - } + let enabled = self.editor_enabled.unwrap_or(false); + + let zeta_icon = if enabled { + IconName::ZedPredict + } else { + IconName::ZedPredictDisabled + }; let current_user_terms_accepted = self.user_store.read(cx).current_user_has_accepted_terms(); - if !current_user_terms_accepted.unwrap_or(false) { - let signed_in = current_user_terms_accepted.is_some(); - let tooltip_meta = if signed_in { - "Read Terms of Service" - } else { - "Sign in to use" - }; + let icon_button = || { + let base = IconButton::new("zed-predict-pending-button", zeta_icon) + .shape(IconButtonShape::Square); + + match ( + current_user_terms_accepted, + self.popover_menu_handle.is_deployed(), + enabled, + ) { + (Some(false) | None, _, _) => { + let signed_in = current_user_terms_accepted.is_some(); + let tooltip_meta = if signed_in { + "Read Terms of Service" + } else { + "Sign in to use" + }; - return div().child( - icon_button() - .tooltip(move |window, cx| { + base.tooltip(move |window, cx| { Tooltip::with_meta( "Edit Predictions", None, @@ -255,27 +267,37 @@ impl Render for InlineCompletionButton { cx, ) }) - .on_click(cx.listener(move |_, _, window, cx| { - telemetry::event!( - "Pending ToS Clicked", - source = "Edit Prediction Status Button" - ); - window.dispatch_action( - zed_actions::OpenZedPredictOnboarding.boxed_clone(), - cx, - ); - })), - ); - } + .on_click(cx.listener( + move |_, _, window, cx| { + telemetry::event!( + "Pending ToS Clicked", + source = "Edit Prediction Status Button" + ); + window.dispatch_action( + zed_actions::OpenZedPredictOnboarding.boxed_clone(), + cx, + ); + }, + )) + } + (Some(true), true, _) => base, + (Some(true), false, true) => base.tooltip(|window, cx| { + Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx) + }), + (Some(true), false, false) => base.tooltip(|window, cx| { + Tooltip::with_meta( + "Edit Prediction", + Some(&ToggleMenu), + "Disabled For This File", + window, + cx, + ) + }), + } + }; let this = cx.entity().clone(); - if !self.popover_menu_handle.is_deployed() { - icon_button().tooltip(|window, cx| { - Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx) - }); - } - let mut popover_menu = PopoverMenu::new("zeta") .menu(move |window, cx| { Some(this.update(cx, |this, cx| this.build_zeta_context_menu(window, cx))) @@ -362,15 +384,10 @@ impl InlineCompletionButton { }) } - // Predict Edits at Cursor – alt-tab - // Automatically Predict: - // ✓ PATH - // ✓ Rust - // ✓ All Files pub fn build_language_settings_menu(&self, mut menu: ContextMenu, cx: &mut App) -> ContextMenu { let fs = self.fs.clone(); - menu = menu.header("Predict Edits For:"); + menu = menu.header("Show Predict Edits For"); if let Some(language) = self.language.clone() { let fs = fs.clone(); @@ -381,66 +398,39 @@ impl InlineCompletionButton { menu = menu.toggleable_entry( language.name(), language_enabled, - IconPosition::Start, + IconPosition::End, None, move |_, cx| { - toggle_inline_completions_for_language(language.clone(), fs.clone(), cx) + toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx) }, ); } let settings = AllLanguageSettings::get_global(cx); - if let Some(file) = &self.file { - let path = file.path().clone(); - let path_enabled = settings.inline_completions_enabled_for_path(&path); - - menu = menu.toggleable_entry( - "This File", - path_enabled, - IconPosition::Start, - None, - move |window, cx| { - if let Some(workspace) = window.root().flatten() { - let workspace = workspace.downgrade(); - window - .spawn(cx, |cx| { - configure_disabled_globs( - workspace, - path_enabled.then_some(path.clone()), - cx, - ) - }) - .detach_and_log_err(cx); - } - }, - ); - } - - let globally_enabled = settings.inline_completions_enabled(None, None, cx); + let globally_enabled = settings.show_inline_completions(None, cx); menu = menu.toggleable_entry( "All Files", globally_enabled, - IconPosition::Start, + IconPosition::End, None, move |_, cx| toggle_inline_completions_globally(fs.clone(), cx), ); + menu = menu.separator().header("Privacy Settings"); if let Some(provider) = &self.inline_completion_provider { let data_collection = provider.data_collection_state(cx); - if data_collection.is_supported() { let provider = provider.clone(); let enabled = data_collection.is_enabled(); - menu = menu - .separator() - .header("Help Improve The Model") - .header("Valid Only For OSS Projects"); menu = menu.item( // TODO: We want to add something later that communicates whether // the current project is open-source. ContextMenuEntry::new("Share Training Data") - .toggleable(IconPosition::Start, enabled) + .toggleable(IconPosition::End, data_collection.is_enabled()) + .documentation_aside(|_| { + Label::new("Zed automatically detects if your project is open-source. This setting is only applicable in such cases.").into_any_element() + }) .handler(move |_, cx| { provider.toggle_data_collection(cx); @@ -455,11 +445,42 @@ impl InlineCompletionButton { source = "Edit Prediction Status Menu" ); } - }), - ); + }) + ) } } + menu = menu.item( + ContextMenuEntry::new("Exclude Files") + .documentation_aside(|_| { + Label::new("This item takes you to the settings where you can specify files that will never be captured by any edit prediction model. You can list both specific file extensions and individual file names.").into_any_element() + }) + .handler(move |window, cx| { + if let Some(workspace) = window.root().flatten() { + let workspace = workspace.downgrade(); + window + .spawn(cx, |cx| { + open_disabled_globs_setting_in_editor( + workspace, + cx, + ) + }) + .detach_and_log_err(cx); + } + }), + ); + + if self.file.as_ref().map_or(false, |file| { + !all_language_settings(Some(file), cx).inline_completions_enabled_for_path(file.path()) + }) { + menu = menu.item( + ContextMenuEntry::new("This file is excluded.") + .disabled(true) + .icon(IconName::ZedPredictDisabled) + .icon_size(IconSize::Small), + ); + } + if let Some(editor_focus_handle) = self.editor_focus_handle.clone() { menu = menu .separator() @@ -546,12 +567,11 @@ impl InlineCompletionButton { self.editor_enabled = { let file = file.as_ref(); Some( - file.map(|file| !file.is_private()).unwrap_or(true) - && all_language_settings(file, cx).inline_completions_enabled( - language, - file.map(|file| file.path().as_ref()), - cx, - ), + file.map(|file| { + all_language_settings(Some(file), cx) + .inline_completions_enabled_for_path(file.path()) + }) + .unwrap_or(true), ) }; self.inline_completion_provider = editor.inline_completion_provider(); @@ -616,9 +636,8 @@ impl SupermavenButtonStatus { } } -async fn configure_disabled_globs( +async fn open_disabled_globs_setting_in_editor( workspace: WeakEntity, - path_to_disable: Option>, mut cx: AsyncWindowContext, ) -> Result<()> { let settings_editor = workspace @@ -637,34 +656,34 @@ async fn configure_disabled_globs( let text = item.buffer().read(cx).snapshot(cx).text(); let settings = cx.global::(); - let edits = settings.edits_for_update::(&text, |file| { - let copilot = file.inline_completions.get_or_insert_with(Default::default); - let globs = copilot.disabled_globs.get_or_insert_with(|| { - settings - .get::(None) - .inline_completions - .disabled_globs - .iter() - .map(|glob| glob.glob().to_string()) - .collect() - }); - if let Some(path_to_disable) = &path_to_disable { - globs.push(path_to_disable.to_string_lossy().into_owned()); - } else { - globs.clear(); - } + // Ensure that we always have "inline_completions { "disabled_globs": [] }" + let edits = settings.edits_for_update::(&text, |file| { + file.inline_completions + .get_or_insert_with(Default::default) + .disabled_globs + .get_or_insert_with(Vec::new); }); if !edits.is_empty() { + item.edit(edits.iter().cloned(), cx); + } + + let text = item.buffer().read(cx).snapshot(cx).text(); + + static DISABLED_GLOBS_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r#""disabled_globs":\s*\[\s*(?P(?:.|\n)*?)\s*\]"#).unwrap() + }); + // Only capture [...] + let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| { + captures + .name("content") + .map(|inner_match| inner_match.start()..inner_match.end()) + }); + if let Some(range) = range { item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { - selections.select_ranges(edits.iter().map(|e| e.0.clone())); + selections.select_ranges(vec![range]); }); - - // When *enabling* a path, don't actually perform an edit, just select the range. - if path_to_disable.is_some() { - item.edit(edits.iter().cloned(), cx); - } } })?; @@ -672,8 +691,7 @@ async fn configure_disabled_globs( } fn toggle_inline_completions_globally(fs: Arc, cx: &mut App) { - let show_inline_completions = - all_language_settings(None, cx).inline_completions_enabled(None, None, cx); + let show_inline_completions = all_language_settings(None, cx).show_inline_completions(None, cx); update_settings_file::(fs, cx, move |file, _| { file.defaults.show_inline_completions = Some(!show_inline_completions) }); @@ -687,9 +705,13 @@ fn set_completion_provider(fs: Arc, cx: &mut App, provider: InlineComple }); } -fn toggle_inline_completions_for_language(language: Arc, fs: Arc, cx: &mut App) { +fn toggle_show_inline_completions_for_language( + language: Arc, + fs: Arc, + cx: &mut App, +) { let show_inline_completions = - all_language_settings(None, cx).inline_completions_enabled(Some(&language), None, cx); + all_language_settings(None, cx).show_inline_completions(Some(&language), cx); update_settings_file::(fs, cx, move |file, _| { file.languages .entry(language.name()) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 55d284fedb2e15..ac57e566f4c36a 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -886,18 +886,7 @@ impl AllLanguageSettings { } /// Returns whether edit predictions are enabled for the given language and path. - pub fn inline_completions_enabled( - &self, - language: Option<&Arc>, - path: Option<&Path>, - cx: &App, - ) -> bool { - if let Some(path) = path { - if !self.inline_completions_enabled_for_path(path) { - return false; - } - } - + pub fn show_inline_completions(&self, language: Option<&Arc>, cx: &App) -> bool { self.language(None, language.map(|l| l.name()).as_ref(), cx) .show_inline_completions } diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 01e52b2f8420fe..f80551a3f39d3f 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -3,7 +3,7 @@ use anyhow::Result; use futures::StreamExt as _; use gpui::{App, Context, Entity, EntityId, Task}; use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider}; -use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot}; +use language::{Anchor, Buffer, BufferSnapshot}; use std::{ ops::{AddAssign, Range}, path::Path, @@ -113,16 +113,8 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { false } - fn is_enabled(&self, buffer: &Entity, cursor_position: Anchor, cx: &App) -> bool { - if !self.supermaven.read(cx).is_enabled() { - return false; - } - - let buffer = buffer.read(cx); - let file = buffer.file(); - let language = buffer.language_at(cursor_position); - let settings = all_language_settings(file, cx); - settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) + fn is_enabled(&self, _buffer: &Entity, _cursor_position: Anchor, cx: &App) -> bool { + self.supermaven.read(cx).is_enabled() } fn is_refreshing(&self) -> bool { diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 765c216ccd0c79..db9632d4ff31e3 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -47,6 +47,7 @@ pub struct ContextMenuEntry { handler: Rc, &mut Window, &mut App)>, action: Option>, disabled: bool, + documentation_aside: Option AnyElement>>, } impl ContextMenuEntry { @@ -61,6 +62,7 @@ impl ContextMenuEntry { handler: Rc::new(|_, _, _| {}), action: None, disabled: false, + documentation_aside: None, } } @@ -108,6 +110,14 @@ impl ContextMenuEntry { self.disabled = disabled; self } + + pub fn documentation_aside( + mut self, + element: impl Fn(&mut App) -> AnyElement + 'static, + ) -> Self { + self.documentation_aside = Some(Rc::new(element)); + self + } } impl From for ContextMenuItem { @@ -125,6 +135,7 @@ pub struct ContextMenu { clicked: bool, _on_blur_subscription: Subscription, keep_open_on_confirm: bool, + documentation_aside: Option<(usize, Rc AnyElement>)>, } impl Focusable for ContextMenu { @@ -161,6 +172,7 @@ impl ContextMenu { clicked: false, _on_blur_subscription, keep_open_on_confirm: false, + documentation_aside: None, }, window, cx, @@ -209,6 +221,7 @@ impl ContextMenu { icon_color: None, action, disabled: false, + documentation_aside: None, })); self } @@ -231,6 +244,7 @@ impl ContextMenu { icon_color: None, action, disabled: false, + documentation_aside: None, })); self } @@ -281,6 +295,7 @@ impl ContextMenu { icon_size: IconSize::Small, icon_color: None, disabled: false, + documentation_aside: None, })); self } @@ -294,7 +309,6 @@ impl ContextMenu { toggle: None, label: label.into(), action: Some(action.boxed_clone()), - handler: Rc::new(move |context, window, cx| { if let Some(context) = &context { window.focus(context); @@ -306,6 +320,7 @@ impl ContextMenu { icon_position: IconPosition::End, icon_color: None, disabled: true, + documentation_aside: None, })); self } @@ -314,7 +329,6 @@ impl ContextMenu { self.items.push(ContextMenuItem::Entry(ContextMenuEntry { toggle: None, label: label.into(), - action: Some(action.boxed_clone()), handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), icon: Some(IconName::ArrowUpRight), @@ -322,6 +336,7 @@ impl ContextMenu { icon_position: IconPosition::End, icon_color: None, disabled: false, + documentation_aside: None, })); self } @@ -356,15 +371,16 @@ impl ContextMenu { } fn select_first(&mut self, _: &SelectFirst, _: &mut Window, cx: &mut Context) { - self.selected_index = self.items.iter().position(|item| item.is_selectable()); + if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) { + self.select_index(ix); + } cx.notify(); } pub fn select_last(&mut self) -> Option { for (ix, item) in self.items.iter().enumerate().rev() { if item.is_selectable() { - self.selected_index = Some(ix); - return Some(ix); + return self.select_index(ix); } } None @@ -384,7 +400,7 @@ impl ContextMenu { } else { for (ix, item) in self.items.iter().enumerate().skip(next_index) { if item.is_selectable() { - self.selected_index = Some(ix); + self.select_index(ix); cx.notify(); break; } @@ -402,7 +418,7 @@ impl ContextMenu { } else { for (ix, item) in self.items.iter().enumerate().take(ix).rev() { if item.is_selectable() { - self.selected_index = Some(ix); + self.select_index(ix); cx.notify(); break; } @@ -413,6 +429,20 @@ impl ContextMenu { } } + fn select_index(&mut self, ix: usize) -> Option { + self.documentation_aside = None; + let item = self.items.get(ix)?; + if item.is_selectable() { + self.selected_index = Some(ix); + if let ContextMenuItem::Entry(entry) = item { + if let Some(callback) = &entry.documentation_aside { + self.documentation_aside = Some((ix, callback.clone())); + } + } + } + Some(ix) + } + pub fn on_action_dispatch( &mut self, dispatched: &dyn Action, @@ -436,7 +466,7 @@ impl ContextMenu { false } }) { - self.selected_index = Some(ix); + self.select_index(ix); self.delayed = true; cx.notify(); let action = dispatched.boxed_clone(); @@ -479,198 +509,275 @@ impl Render for ContextMenu { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .flex() - .flex_row() + let aside = self + .documentation_aside + .as_ref() + .map(|(_, callback)| callback.clone()); + + h_flex() + .w_full() + .items_start() + .gap_1() + .when_some(aside, |this, aside| { + this.child( + WithRemSize::new(ui_font_size) + .occlude() + .elevation_2(cx) + .p_2() + .max_w_80() + .child(aside(cx)), + ) + }) .child( - v_flex() - .id("context-menu") - .min_w(px(200.)) - .max_h(vh(0.75, window)) - .flex_1() - .overflow_y_scroll() - .track_focus(&self.focus_handle(cx)) - .on_mouse_down_out( - cx.listener(|this, _, window, cx| this.cancel(&menu::Cancel, window, cx)), - ) - .key_context("menu") - .on_action(cx.listener(ContextMenu::select_first)) - .on_action(cx.listener(ContextMenu::handle_select_last)) - .on_action(cx.listener(ContextMenu::select_next)) - .on_action(cx.listener(ContextMenu::select_prev)) - .on_action(cx.listener(ContextMenu::confirm)) - .on_action(cx.listener(ContextMenu::cancel)) - .when(!self.delayed, |mut el| { - for item in self.items.iter() { - if let ContextMenuItem::Entry(ContextMenuEntry { - action: Some(action), - disabled: false, - .. - }) = item - { - el = el.on_boxed_action( - &**action, - cx.listener(ContextMenu::on_action_dispatch), - ); - } - } - el - }) - .child(List::new().children(self.items.iter_mut().enumerate().map( - |(ix, item)| { - match item { - ContextMenuItem::Separator => ListSeparator.into_any_element(), - ContextMenuItem::Header(header) => { - ListSubHeader::new(header.clone()) - .inset(true) - .into_any_element() + WithRemSize::new(ui_font_size) + .occlude() + .elevation_2(cx) + .flex() + .flex_row() + .child( + v_flex() + .id("context-menu") + .min_w(px(200.)) + .max_h(vh(0.75, window)) + .flex_1() + .overflow_y_scroll() + .track_focus(&self.focus_handle(cx)) + .on_mouse_down_out(cx.listener(|this, _, window, cx| { + this.cancel(&menu::Cancel, window, cx) + })) + .key_context("menu") + .on_action(cx.listener(ContextMenu::select_first)) + .on_action(cx.listener(ContextMenu::handle_select_last)) + .on_action(cx.listener(ContextMenu::select_next)) + .on_action(cx.listener(ContextMenu::select_prev)) + .on_action(cx.listener(ContextMenu::confirm)) + .on_action(cx.listener(ContextMenu::cancel)) + .when(!self.delayed, |mut el| { + for item in self.items.iter() { + if let ContextMenuItem::Entry(ContextMenuEntry { + action: Some(action), + disabled: false, + .. + }) = item + { + el = el.on_boxed_action( + &**action, + cx.listener(ContextMenu::on_action_dispatch), + ); + } } - ContextMenuItem::Label(label) => ListItem::new(ix) - .inset(true) - .disabled(true) - .child(Label::new(label.clone())) - .into_any_element(), - ContextMenuItem::Entry(ContextMenuEntry { - toggle, - label, - handler, - icon, - icon_position, - icon_size, - icon_color, - action, - disabled, - }) => { - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - let icon_color = if *disabled { - Color::Muted - } else { - icon_color.unwrap_or(Color::Default) - }; - let label_color = if *disabled { - Color::Muted - } else { - Color::Default - }; - let label_element = if let Some(icon_name) = icon { - h_flex() - .gap_1p5() - .when(*icon_position == IconPosition::Start, |flex| { - flex.child( - Icon::new(*icon_name) - .size(*icon_size) - .color(icon_color), - ) - }) - .child(Label::new(label.clone()).color(label_color)) - .when(*icon_position == IconPosition::End, |flex| { - flex.child( - Icon::new(*icon_name) - .size(*icon_size) - .color(icon_color), - ) - }) - .into_any_element() - } else { - Label::new(label.clone()) - .color(label_color) - .into_any_element() - }; - - ListItem::new(ix) - .inset(true) - .disabled(*disabled) - .toggle_state(Some(ix) == self.selected_index) - .when_some(*toggle, |list_item, (position, toggled)| { - let contents = if toggled { - v_flex().flex_none().child( - Icon::new(IconName::Check).color(Color::Accent), - ) + el + }) + .child(List::new().children(self.items.iter_mut().enumerate().map( + |(ix, item)| { + match item { + ContextMenuItem::Separator => { + ListSeparator.into_any_element() + } + ContextMenuItem::Header(header) => { + ListSubHeader::new(header.clone()) + .inset(true) + .into_any_element() + } + ContextMenuItem::Label(label) => ListItem::new(ix) + .inset(true) + .disabled(true) + .child(Label::new(label.clone())) + .into_any_element(), + ContextMenuItem::Entry(ContextMenuEntry { + toggle, + label, + handler, + icon, + icon_position, + icon_size, + icon_color, + action, + disabled, + documentation_aside, + }) => { + let handler = handler.clone(); + let menu = cx.entity().downgrade(); + let icon_color = if *disabled { + Color::Muted + } else { + icon_color.unwrap_or(Color::Default) + }; + let label_color = if *disabled { + Color::Muted } else { - v_flex() - .flex_none() - .size(IconSize::default().rems()) + Color::Default }; - match position { - IconPosition::Start => { - list_item.start_slot(contents) - } - IconPosition::End => list_item.end_slot(contents), - } - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(label_element) - .debug_selector(|| format!("MENU_ITEM-{}", label)) - .children(action.as_ref().and_then(|action| { - self.action_context - .as_ref() - .map(|focus| { - KeyBinding::for_action_in( - &**action, focus, window, + let label_element = if let Some(icon_name) = icon { + h_flex() + .gap_1p5() + .when( + *icon_position == IconPosition::Start, + |flex| { + flex.child( + Icon::new(*icon_name) + .size(*icon_size) + .color(icon_color), ) - }) - .unwrap_or_else(|| { - KeyBinding::for_action( - &**action, window, + }, + ) + .child( + Label::new(label.clone()) + .color(label_color), + ) + .when( + *icon_position == IconPosition::End, + |flex| { + flex.child( + Icon::new(*icon_name) + .size(*icon_size) + .color(icon_color), ) - }) - .map(|binding| div().ml_4().child(binding)) - })), - ) - .on_click({ - let context = self.action_context.clone(); - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); + }, + ) + .into_any_element() + } else { + Label::new(label.clone()) + .color(label_color) + .into_any_element() + }; + let documentation_aside_callback = + documentation_aside.clone(); + div() + .id(("context-menu-child", ix)) + .when_some( + documentation_aside_callback, + |this, documentation_aside_callback| { + this.occlude().on_hover(cx.listener( + move |menu, hovered, _, cx| { + if *hovered { + menu.documentation_aside = Some((ix, documentation_aside_callback.clone())); + cx.notify(); + } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) { + menu.documentation_aside = None; + cx.notify(); + } + }, + )) + }, + ) + .child( + ListItem::new(ix) + .inset(true) + .disabled(*disabled) + .toggle_state( + Some(ix) == self.selected_index, + ) + .when_some( + *toggle, + |list_item, (position, toggled)| { + let contents = if toggled { + v_flex().flex_none().child( + Icon::new(IconName::Check) + .color(Color::Accent), + ) + } else { + v_flex().flex_none().size( + IconSize::default().rems(), + ) + }; + match position { + IconPosition::Start => { + list_item + .start_slot(contents) + } + IconPosition::End => { + list_item.end_slot(contents) + } + } + }, + ) + .child( + h_flex() + .w_full() + .justify_between() + .child(label_element) + .debug_selector(|| { + format!("MENU_ITEM-{}", label) + }) + .children( + action.as_ref().and_then( + |action| { + self.action_context + .as_ref() + .map(|focus| { + KeyBinding::for_action_in( + &**action, focus, + window, + ) + }) + .unwrap_or_else(|| { + KeyBinding::for_action( + &**action, window, + ) + }) + .map(|binding| { + div().ml_4().child(binding) + }) + }, + ), + ), + ) + .on_click({ + let context = + self.action_context.clone(); + move |_, window, cx| { + handler( + context.as_ref(), + window, + cx, + ); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + } + }), + ) + .into_any_element() + } + ContextMenuItem::CustomEntry { + entry_render, + handler, + selectable, + } => { + let handler = handler.clone(); + let menu = cx.entity().downgrade(); + let selectable = *selectable; + ListItem::new(ix) + .inset(true) + .toggle_state(if selectable { + Some(ix) == self.selected_index + } else { + false }) - .ok(); - } - }) - .into_any_element() - } - ContextMenuItem::CustomEntry { - entry_render, - handler, - selectable, - } => { - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - let selectable = *selectable; - ListItem::new(ix) - .inset(true) - .toggle_state(if selectable { - Some(ix) == self.selected_index - } else { - false - }) - .selectable(selectable) - .when(selectable, |item| { - item.on_click({ - let context = self.action_context.clone(); - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - cx.emit(DismissEvent); + .selectable(selectable) + .when(selectable, |item| { + item.on_click({ + let context = self.action_context.clone(); + move |_, window, cx| { + handler(context.as_ref(), window, cx); + menu.update(cx, |menu, cx| { + menu.clicked = true; + cx.emit(DismissEvent); + }) + .ok(); + } }) - .ok(); - } - }) - }) - .child(entry_render(window, cx)) - .into_any_element() - } - } - }, - ))), + }) + .child(entry_render(window, cx)) + .into_any_element() + } + } + }, + ))), + ), ) } } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index ebe85b8255701a..3c35285056363b 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -336,6 +336,7 @@ pub enum IconName { ZedAssistant2, ZedAssistantFilled, ZedPredict, + ZedPredictDisabled, ZedXCopilot, } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 1e47a08d2a458b..e331260faa22db 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1289,7 +1289,7 @@ impl Vim { .map_or(false, |provider| provider.show_completions_in_normal_mode()), _ => false, }; - editor.set_inline_completions_enabled(enable_inline_completions, cx); + editor.set_show_inline_completions_enabled(enable_inline_completions, cx); }); cx.notify() } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index bd498a126d2b60..96e839d523b506 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -16,8 +16,8 @@ use gpui::{ use search::{buffer_search, BufferSearchBar}; use settings::{Settings, SettingsStore}; use ui::{ - prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize, - PopoverMenu, PopoverMenuHandle, Tooltip, + prelude::*, ButtonStyle, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, IconName, + IconSize, PopoverMenu, PopoverMenuHandle, Tooltip, }; use vim_mode_setting::VimModeSetting; use workspace::{ @@ -94,7 +94,8 @@ impl Render for QuickActionBar { git_blame_inline_enabled, show_git_blame_gutter, auto_signature_help_enabled, - inline_completions_enabled, + show_inline_completions, + inline_completion_enabled, ) = { let editor = editor.read(cx); let selection_menu_enabled = editor.selection_menu_enabled(cx); @@ -103,7 +104,8 @@ impl Render for QuickActionBar { let git_blame_inline_enabled = editor.git_blame_inline_enabled(); let show_git_blame_gutter = editor.show_git_blame_gutter(); let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx); - let inline_completions_enabled = editor.inline_completions_enabled(cx); + let show_inline_completions = editor.should_show_inline_completions(cx); + let inline_completion_enabled = editor.inline_completions_enabled(cx); ( selection_menu_enabled, @@ -112,7 +114,8 @@ impl Render for QuickActionBar { git_blame_inline_enabled, show_git_blame_gutter, auto_signature_help_enabled, - inline_completions_enabled, + show_inline_completions, + inline_completion_enabled, ) }; @@ -294,12 +297,12 @@ impl Render for QuickActionBar { }, ); - menu = menu.toggleable_entry( - "Edit Predictions", - inline_completions_enabled, - IconPosition::Start, - Some(editor::actions::ToggleInlineCompletions.boxed_clone()), - { + let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions") + .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions) + .disabled(!inline_completion_enabled) + .action(Some( + editor::actions::ToggleInlineCompletions.boxed_clone(), + )).handler({ let editor = editor.clone(); move |window, cx| { editor @@ -312,8 +315,14 @@ impl Render for QuickActionBar { }) .ok(); } - }, - ); + }); + if !inline_completion_enabled { + inline_completion_entry = inline_completion_entry.documentation_aside(|_| { + Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() + }); + } + + menu = menu.item(inline_completion_entry); menu = menu.separator(); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 6e68a957c9bd92..7ef46959008e14 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -25,8 +25,7 @@ use gpui::{ }; use http_client::{HttpClient, Method}; use language::{ - language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview, - OffsetRangeExt, Point, ToOffset, ToPoint, + Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, Point, ToOffset, ToPoint, }; use language_models::LlmApiToken; use postage::watch; @@ -1469,15 +1468,11 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide fn is_enabled( &self, - buffer: &Entity, - cursor_position: language::Anchor, - cx: &App, + _buffer: &Entity, + _cursor_position: language::Anchor, + _cx: &App, ) -> bool { - let buffer = buffer.read(cx); - let file = buffer.file(); - let language = buffer.language_at(cursor_position); - let settings = all_language_settings(file, cx); - settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) + true } fn needs_terms_acceptance(&self, cx: &App) -> bool { From 64bc112fe56a397029b2050319e8e9c077a62922 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 5 Feb 2025 19:17:26 +0200 Subject: [PATCH 528/650] Revert recent anti-aliasing improvements (#24289) This reverts commit 31fa4144226fcbd140bb4f26d80001f0abd6facd. This reverts commit b9e0aae49fad996ddb0ce55225873e5c1d5abecd. `lyon` commit revert: ![image](https://github.com/user-attachments/assets/0243f61c-0713-416d-b8db-47372e04abaa) `MSAA` commit revert: ![image](https://github.com/user-attachments/assets/b1a4a9fe-0192-47ef-be6f-52e03c025724) cc @huacnlee , @\as-cii had decided to revert this PR due to a selection right corner rendering bug. Not sure what to propose for a fix from my side Release Notes: - N/A --- Cargo.lock | 76 +----- Cargo.toml | 6 +- crates/editor/src/element.rs | 48 ++-- crates/gpui/Cargo.toml | 2 - crates/gpui/examples/gradient.rs | 14 +- crates/gpui/examples/painting.rs | 141 ++++------ crates/gpui/src/gpui.rs | 2 - crates/gpui/src/path_builder.rs | 241 ------------------ crates/gpui/src/platform/blade/blade_atlas.rs | 49 +--- .../gpui/src/platform/blade/blade_renderer.rs | 60 ++--- crates/gpui/src/platform/mac/metal_atlas.rs | 22 +- .../gpui/src/platform/mac/metal_renderer.rs | 27 +- crates/gpui/src/scene.rs | 10 +- 13 files changed, 113 insertions(+), 585 deletions(-) delete mode 100644 crates/gpui/src/path_builder.rs diff --git a/Cargo.lock b/Cargo.lock index f6dc64534df9a2..3f2adc8722cc18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1820,7 +1820,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.6.0" -source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f" +source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5" dependencies = [ "ash", "ash-window", @@ -1852,7 +1852,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.3.0" -source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f" +source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5" dependencies = [ "proc-macro2", "quote", @@ -1862,7 +1862,7 @@ dependencies = [ [[package]] name = "blade-util" version = "0.2.0" -source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f" +source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5" dependencies = [ "blade-graphics", "bytemuck", @@ -4783,12 +4783,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" -[[package]] -name = "float_next_after" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" - [[package]] name = "flume" version = "0.11.1" @@ -5533,7 +5527,6 @@ dependencies = [ "inventory", "itertools 0.14.0", "log", - "lyon", "media", "metal", "naga", @@ -7550,69 +7543,6 @@ dependencies = [ "url", ] -[[package]] -name = "lyon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f" -dependencies = [ - "lyon_algorithms", - "lyon_extra", - "lyon_tessellation", -] - -[[package]] -name = "lyon_algorithms" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" -dependencies = [ - "lyon_path", - "num-traits", -] - -[[package]] -name = "lyon_extra" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca94c7bf1e2557c2798989c43416822c12fc5dcc5e17cc3307ef0e71894a955" -dependencies = [ - "lyon_path", - "thiserror 1.0.69", -] - -[[package]] -name = "lyon_geom" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570" -dependencies = [ - "arrayvec", - "euclid", - "num-traits", -] - -[[package]] -name = "lyon_path" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e0b8aec2f58586f6eef237985b9a9b7cb3a3aff4417c575075cf95bf925252e" -dependencies = [ - "lyon_geom", - "num-traits", -] - -[[package]] -name = "lyon_tessellation" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c" -dependencies = [ - "float_next_after", - "lyon_path", - "num-traits", -] - [[package]] name = "mac" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 9e55f821507eae..b86fdd9af394b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -383,9 +383,9 @@ async-watch = "0.3.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } base64 = "0.22" bitflags = "2.6.0" -blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" } -blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" } -blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" } +blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } +blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } naga = { version = "23.1.0", features = ["wgsl-in"] } blake3 = "1.5.3" bytes = "1.0" diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8db6d81b1c32e0..6df0e0a1f51ced 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8337,8 +8337,8 @@ impl HighlightedRange { }; let top_curve_width = curve_width(first_line.start_x, first_line.end_x); - let mut builder = gpui::PathBuilder::fill(); - builder.curve_to(first_top_right + curve_height, first_top_right); + let mut path = gpui::Path::new(first_top_right - top_curve_width); + path.curve_to(first_top_right + curve_height, first_top_right); let mut iter = lines.iter().enumerate().peekable(); while let Some((ix, line)) = iter.next() { @@ -8349,42 +8349,42 @@ impl HighlightedRange { match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() { Ordering::Equal => { - builder.line_to(bottom_right); + path.line_to(bottom_right); } Ordering::Less => { let curve_width = curve_width(next_top_right.x, bottom_right.x); - builder.line_to(bottom_right - curve_height); + path.line_to(bottom_right - curve_height); if self.corner_radius > Pixels::ZERO { - builder.curve_to(bottom_right - curve_width, bottom_right); + path.curve_to(bottom_right - curve_width, bottom_right); } - builder.line_to(next_top_right + curve_width); + path.line_to(next_top_right + curve_width); if self.corner_radius > Pixels::ZERO { - builder.curve_to(next_top_right + curve_height, next_top_right); + path.curve_to(next_top_right + curve_height, next_top_right); } } Ordering::Greater => { let curve_width = curve_width(bottom_right.x, next_top_right.x); - builder.line_to(bottom_right - curve_height); + path.line_to(bottom_right - curve_height); if self.corner_radius > Pixels::ZERO { - builder.curve_to(bottom_right + curve_width, bottom_right); + path.curve_to(bottom_right + curve_width, bottom_right); } - builder.line_to(next_top_right - curve_width); + path.line_to(next_top_right - curve_width); if self.corner_radius > Pixels::ZERO { - builder.curve_to(next_top_right + curve_height, next_top_right); + path.curve_to(next_top_right + curve_height, next_top_right); } } } } else { let curve_width = curve_width(line.start_x, line.end_x); - builder.line_to(bottom_right - curve_height); + path.line_to(bottom_right - curve_height); if self.corner_radius > Pixels::ZERO { - builder.curve_to(bottom_right - curve_width, bottom_right); + path.curve_to(bottom_right - curve_width, bottom_right); } let bottom_left = point(line.start_x, bottom_right.y); - builder.line_to(bottom_left + curve_width); + path.line_to(bottom_left + curve_width); if self.corner_radius > Pixels::ZERO { - builder.curve_to(bottom_left - curve_height, bottom_left); + path.curve_to(bottom_left - curve_height, bottom_left); } } } @@ -8392,26 +8392,24 @@ impl HighlightedRange { if first_line.start_x > last_line.start_x { let curve_width = curve_width(last_line.start_x, first_line.start_x); let second_top_left = point(last_line.start_x, start_y + self.line_height); - builder.line_to(second_top_left + curve_height); + path.line_to(second_top_left + curve_height); if self.corner_radius > Pixels::ZERO { - builder.curve_to(second_top_left + curve_width, second_top_left); + path.curve_to(second_top_left + curve_width, second_top_left); } let first_bottom_left = point(first_line.start_x, second_top_left.y); - builder.line_to(first_bottom_left - curve_width); + path.line_to(first_bottom_left - curve_width); if self.corner_radius > Pixels::ZERO { - builder.curve_to(first_bottom_left - curve_height, first_bottom_left); + path.curve_to(first_bottom_left - curve_height, first_bottom_left); } } - builder.line_to(first_top_left + curve_height); + path.line_to(first_top_left + curve_height); if self.corner_radius > Pixels::ZERO { - builder.curve_to(first_top_left + top_curve_width, first_top_left); + path.curve_to(first_top_left + top_curve_width, first_top_left); } - builder.line_to(first_top_right - top_curve_width); + path.line_to(first_top_right - top_curve_width); - if let Ok(path) = builder.build() { - window.paint_path(path, self.color); - } + window.paint_path(path, self.color); } } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 05a5b28e764dc5..a0220cd572c32d 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -108,7 +108,6 @@ thiserror.workspace = true util.workspace = true uuid.workspace = true waker-fn = "1.2.0" -lyon = "1.0" [target.'cfg(target_os = "macos")'.dependencies] block = "0.1" @@ -206,7 +205,6 @@ rand.workspace = true util = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } unicode-segmentation.workspace = true -lyon = { version = "1.0", features = ["extra"] } [target.'cfg(target_os = "windows")'.build-dependencies] embed-resource = "3.0" diff --git a/crates/gpui/examples/gradient.rs b/crates/gpui/examples/gradient.rs index ec4cdf9bfcdf97..45de8cdd0afa3d 100644 --- a/crates/gpui/examples/gradient.rs +++ b/crates/gpui/examples/gradient.rs @@ -218,17 +218,13 @@ impl Render for GradientViewer { let height = square_bounds.size.height; let horizontal_offset = height; let vertical_offset = px(30.); - let mut builder = gpui::PathBuilder::fill(); - builder.move_to(square_bounds.bottom_left()); - builder - .line_to(square_bounds.origin + point(horizontal_offset, vertical_offset)); - builder.line_to( + let mut path = gpui::Path::new(square_bounds.bottom_left()); + path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset)); + path.line_to( square_bounds.top_right() + point(-horizontal_offset, vertical_offset), ); - - builder.line_to(square_bounds.bottom_right()); - builder.line_to(square_bounds.bottom_left()); - let path = builder.build().unwrap(); + path.line_to(square_bounds.bottom_right()); + path.line_to(square_bounds.bottom_left()); window.paint_path( path, linear_gradient( diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs index 7c1a6a367d1335..9a8ab790650130 100644 --- a/crates/gpui/examples/painting.rs +++ b/crates/gpui/examples/painting.rs @@ -1,62 +1,46 @@ use gpui::{ - canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, Application, - Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels, - Point, Render, StrokeOptions, Window, WindowOptions, + canvas, div, point, prelude::*, px, size, App, Application, Bounds, Context, MouseDownEvent, + Path, Pixels, Point, Render, Window, WindowOptions, }; - struct PaintingViewer { - default_lines: Vec<(Path, Background)>, + default_lines: Vec>, lines: Vec>>, start: Point, _painting: bool, } impl PaintingViewer { - fn new(_window: &mut Window, _cx: &mut Context) -> Self { + fn new() -> Self { let mut lines = vec![]; - // draw a Rust logo - let mut builder = lyon::path::Path::svg_builder(); - lyon::extra::rust_logo::build_logo_path(&mut builder); - // move down the Path - let mut builder: PathBuilder = builder.into(); - builder.translate(point(px(10.), px(100.))); - builder.scale(0.9); - let path = builder.build().unwrap(); - lines.push((path, gpui::black().into())); + // draw a line + let mut path = Path::new(point(px(50.), px(180.))); + path.line_to(point(px(100.), px(120.))); + // go back to close the path + path.line_to(point(px(100.), px(121.))); + path.line_to(point(px(50.), px(181.))); + lines.push(path); // draw a lightening bolt ⚡ - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(150.), px(200.))); - builder.line_to(point(px(200.), px(125.))); - builder.line_to(point(px(200.), px(175.))); - builder.line_to(point(px(250.), px(100.))); - let path = builder.build().unwrap(); - lines.push((path, rgb(0x1d4ed8).into())); + let mut path = Path::new(point(px(150.), px(200.))); + path.line_to(point(px(200.), px(125.))); + path.line_to(point(px(200.), px(175.))); + path.line_to(point(px(250.), px(100.))); + lines.push(path); // draw a ⭐ - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(350.), px(100.))); - builder.line_to(point(px(370.), px(160.))); - builder.line_to(point(px(430.), px(160.))); - builder.line_to(point(px(380.), px(200.))); - builder.line_to(point(px(400.), px(260.))); - builder.line_to(point(px(350.), px(220.))); - builder.line_to(point(px(300.), px(260.))); - builder.line_to(point(px(320.), px(200.))); - builder.line_to(point(px(270.), px(160.))); - builder.line_to(point(px(330.), px(160.))); - builder.line_to(point(px(350.), px(100.))); - let path = builder.build().unwrap(); - lines.push(( - path, - linear_gradient( - 180., - linear_color_stop(rgb(0xFACC15), 0.7), - linear_color_stop(rgb(0xD56D0C), 1.), - ) - .color_space(ColorSpace::Oklab), - )); + let mut path = Path::new(point(px(350.), px(100.))); + path.line_to(point(px(370.), px(160.))); + path.line_to(point(px(430.), px(160.))); + path.line_to(point(px(380.), px(200.))); + path.line_to(point(px(400.), px(260.))); + path.line_to(point(px(350.), px(220.))); + path.line_to(point(px(300.), px(260.))); + path.line_to(point(px(320.), px(200.))); + path.line_to(point(px(270.), px(160.))); + path.line_to(point(px(330.), px(160.))); + path.line_to(point(px(350.), px(100.))); + lines.push(path); let square_bounds = Bounds { origin: point(px(450.), px(100.)), @@ -65,42 +49,18 @@ impl PaintingViewer { let height = square_bounds.size.height; let horizontal_offset = height; let vertical_offset = px(30.); - let mut builder = PathBuilder::fill(); - builder.move_to(square_bounds.bottom_left()); - builder.curve_to( + let mut path = Path::new(square_bounds.bottom_left()); + path.curve_to( square_bounds.origin + point(horizontal_offset, vertical_offset), square_bounds.origin + point(px(0.0), vertical_offset), ); - builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset)); - builder.curve_to( + path.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset)); + path.curve_to( square_bounds.bottom_right(), square_bounds.top_right() + point(px(0.0), vertical_offset), ); - builder.line_to(square_bounds.bottom_left()); - let path = builder.build().unwrap(); - lines.push(( - path, - linear_gradient( - 180., - linear_color_stop(gpui::blue(), 0.4), - linear_color_stop(gpui::red(), 1.), - ), - )); - - // draw a wave - let options = StrokeOptions::default() - .with_line_width(1.) - .with_line_join(lyon::path::LineJoin::Bevel); - let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); - builder.move_to(point(px(40.), px(320.))); - for i in 0..50 { - builder.line_to(point( - px(40.0 + i as f32 * 10.0), - px(320.0 + (i as f32 * 10.0).sin() * 40.0), - )); - } - let path = builder.build().unwrap(); - lines.push((path, gpui::green().into())); + path.line_to(square_bounds.bottom_left()); + lines.push(path); Self { default_lines: lines.clone(), @@ -155,28 +115,27 @@ impl Render for PaintingViewer { canvas( move |_, _, _| {}, move |_, _, window, _| { - - for (path, color) in default_lines { - window.paint_path(path, color); + const STROKE_WIDTH: Pixels = px(2.0); + for path in default_lines { + window.paint_path(path, gpui::black()); } - for points in lines { - if points.len() < 2 { - continue; + let mut path = Path::new(points[0]); + for p in points.iter().skip(1) { + path.line_to(*p); } - let mut builder = PathBuilder::stroke(px(1.)); - for (i, p) in points.into_iter().enumerate() { - if i == 0 { - builder.move_to(p); - } else { - builder.line_to(p); + let mut last = points.last().unwrap(); + for p in points.iter().rev() { + let mut offset_x = px(0.); + if last.x == p.x { + offset_x = STROKE_WIDTH; } + path.line_to(point(p.x + offset_x, p.y + STROKE_WIDTH)); + last = p; } - if let Ok(path) = builder.build() { - window.paint_path(path, gpui::black()); - } + window.paint_path(path, gpui::black()); } }, ) @@ -226,13 +185,13 @@ impl Render for PaintingViewer { } fn main() { - Application::new().run(|cx| { + Application::new().run(|cx: &mut App| { cx.open_window( WindowOptions { focus: true, ..Default::default() }, - |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), + |_, cx| cx.new(|_| PaintingViewer::new()), ) .unwrap(); cx.activate(true); diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 1ebfc643ee57cc..db33bfca2e5126 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -82,7 +82,6 @@ mod input; mod interactive; mod key_dispatch; mod keymap; -mod path_builder; mod platform; pub mod prelude; mod scene; @@ -136,7 +135,6 @@ pub use input::*; pub use interactive::*; use key_dispatch::*; pub use keymap::*; -pub use path_builder::*; pub use platform::*; pub use refineable::*; pub use scene::*; diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs deleted file mode 100644 index 0fd8eb6fa54583..00000000000000 --- a/crates/gpui/src/path_builder.rs +++ /dev/null @@ -1,241 +0,0 @@ -use anyhow::Error; -use etagere::euclid::Vector2D; -use lyon::geom::Angle; -use lyon::tessellation::{ - BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers, -}; - -pub use lyon::math::Transform; -pub use lyon::tessellation::{FillOptions, FillRule, StrokeOptions}; - -use crate::{point, px, Path, Pixels, Point}; - -/// Style of the PathBuilder -pub enum PathStyle { - /// Stroke style - Stroke(StrokeOptions), - /// Fill style - Fill(FillOptions), -} - -/// A [`Path`] builder. -pub struct PathBuilder { - raw: lyon::path::builder::WithSvg, - transform: Option, - /// PathStyle of the PathBuilder - pub style: PathStyle, -} - -impl From for PathBuilder { - fn from(builder: lyon::path::Builder) -> Self { - Self { - raw: builder.with_svg(), - ..Default::default() - } - } -} - -impl From> for PathBuilder { - fn from(raw: lyon::path::builder::WithSvg) -> Self { - Self { - raw, - ..Default::default() - } - } -} - -impl From for Point { - fn from(p: lyon::math::Point) -> Self { - point(px(p.x), px(p.y)) - } -} - -impl From> for lyon::math::Point { - fn from(p: Point) -> Self { - lyon::math::point(p.x.0, p.y.0) - } -} - -impl Default for PathBuilder { - fn default() -> Self { - Self { - raw: lyon::path::Path::builder().with_svg(), - style: PathStyle::Fill(FillOptions::default()), - transform: None, - } - } -} - -impl PathBuilder { - /// Creates a new [`PathBuilder`] to build a Stroke path. - pub fn stroke(width: Pixels) -> Self { - Self { - style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)), - ..Self::default() - } - } - - /// Creates a new [`PathBuilder`] to build a Fill path. - pub fn fill() -> Self { - Self::default() - } - - /// Sets the style of the [`PathBuilder`]. - pub fn with_style(self, style: PathStyle) -> Self { - Self { style, ..self } - } - - /// Move the current point to the given point. - #[inline] - pub fn move_to(&mut self, to: Point) { - self.raw.move_to(to.into()); - } - - /// Draw a straight line from the current point to the given point. - #[inline] - pub fn line_to(&mut self, to: Point) { - self.raw.line_to(to.into()); - } - - /// Draw a curve from the current point to the given point, using the given control point. - #[inline] - pub fn curve_to(&mut self, to: Point, ctrl: Point) { - self.raw.quadratic_bezier_to(ctrl.into(), to.into()); - } - - /// Adds a cubic Bézier to the [`Path`] given its two control points - /// and its end point. - #[inline] - pub fn cubic_bezier_to( - &mut self, - to: Point, - control_a: Point, - control_b: Point, - ) { - self.raw - .cubic_bezier_to(control_a.into(), control_b.into(), to.into()); - } - - /// Close the current sub-path. - #[inline] - pub fn close(&mut self) { - self.raw.close(); - } - - /// Applies a transform to the path. - #[inline] - pub fn transform(&mut self, transform: Transform) { - self.transform = Some(transform); - } - - /// Applies a translation to the path. - #[inline] - pub fn translate(&mut self, to: Point) { - if let Some(transform) = self.transform { - self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0))); - } else { - self.transform = Some(Transform::translation(to.x.0, to.y.0)) - } - } - - /// Applies a scale to the path. - #[inline] - pub fn scale(&mut self, scale: f32) { - if let Some(transform) = self.transform { - self.transform = Some(transform.then_scale(scale, scale)); - } else { - self.transform = Some(Transform::scale(scale, scale)); - } - } - - /// Applies a rotation to the path. - /// - /// The `angle` is in degrees value in the range 0.0 to 360.0. - #[inline] - pub fn rotate(&mut self, angle: f32) { - let radians = angle.to_radians(); - if let Some(transform) = self.transform { - self.transform = Some(transform.then_rotate(Angle::radians(radians))); - } else { - self.transform = Some(Transform::rotation(Angle::radians(radians))); - } - } - - /// Builds into a [`Path`]. - #[inline] - pub fn build(self) -> Result, Error> { - let path = if let Some(transform) = self.transform { - self.raw.build().transformed(&transform) - } else { - self.raw.build() - }; - - match self.style { - PathStyle::Stroke(options) => Self::tessellate_stroke(&path, &options), - PathStyle::Fill(options) => Self::tessellate_fill(&path, &options), - } - } - - fn tessellate_fill( - path: &lyon::path::Path, - options: &FillOptions, - ) -> Result, Error> { - // Will contain the result of the tessellation. - let mut buf: VertexBuffers = VertexBuffers::new(); - let mut tessellator = FillTessellator::new(); - - // Compute the tessellation. - tessellator.tessellate_path( - path, - options, - &mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()), - )?; - - Ok(Self::build_path(buf)) - } - - fn tessellate_stroke( - path: &lyon::path::Path, - options: &StrokeOptions, - ) -> Result, Error> { - // Will contain the result of the tessellation. - let mut buf: VertexBuffers = VertexBuffers::new(); - let mut tessellator = StrokeTessellator::new(); - - // Compute the tessellation. - tessellator.tessellate_path( - path, - options, - &mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()), - )?; - - Ok(Self::build_path(buf)) - } - - /// Builds a [`Path`] from a [`lyon::VertexBuffers`]. - pub fn build_path(buf: VertexBuffers) -> Path { - if buf.vertices.is_empty() { - return Path::new(Point::default()); - } - - let first_point = buf.vertices[0]; - - let mut path = Path::new(first_point.into()); - for i in 0..buf.indices.len() / 3 { - let i0 = buf.indices[i * 3] as usize; - let i1 = buf.indices[i * 3 + 1] as usize; - let i2 = buf.indices[i * 3 + 2] as usize; - - let v0 = buf.vertices[i0]; - let v1 = buf.vertices[i1]; - let v2 = buf.vertices[i2]; - - path.push_triangle( - (v0.into(), v1.into(), v2.into()), - (point(0., 1.), point(0., 1.), point(0., 1.)), - ); - } - - path - } -} diff --git a/crates/gpui/src/platform/blade/blade_atlas.rs b/crates/gpui/src/platform/blade/blade_atlas.rs index 2783d57127e6c8..fb703f2a411c07 100644 --- a/crates/gpui/src/platform/blade/blade_atlas.rs +++ b/crates/gpui/src/platform/blade/blade_atlas.rs @@ -27,7 +27,6 @@ struct BladeAtlasState { tiles_by_key: FxHashMap, initializations: Vec, uploads: Vec, - path_sample_count: u32, } #[cfg(gles)] @@ -43,11 +42,10 @@ impl BladeAtlasState { pub struct BladeTextureInfo { pub size: gpu::Extent, pub raw_view: gpu::TextureView, - pub msaa_view: Option, } impl BladeAtlas { - pub(crate) fn new(gpu: &Arc, path_sample_count: u32) -> Self { + pub(crate) fn new(gpu: &Arc) -> Self { BladeAtlas(Mutex::new(BladeAtlasState { gpu: Arc::clone(gpu), upload_belt: BufferBelt::new(BufferBeltDescriptor { @@ -59,7 +57,6 @@ impl BladeAtlas { tiles_by_key: Default::default(), initializations: Vec::new(), uploads: Vec::new(), - path_sample_count, })) } @@ -109,7 +106,6 @@ impl BladeAtlas { depth: 1, }, raw_view: texture.raw_view, - msaa_view: texture.msaa_view, } } } @@ -208,39 +204,6 @@ impl BladeAtlasState { } } - // We currently only enable MSAA for path textures. - let (msaa, msaa_view) = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path { - let msaa = self.gpu.create_texture(gpu::TextureDesc { - name: "msaa path texture", - format, - size: gpu::Extent { - width: size.width.into(), - height: size.height.into(), - depth: 1, - }, - array_layer_count: 1, - mip_level_count: 1, - sample_count: self.path_sample_count, - dimension: gpu::TextureDimension::D2, - usage: gpu::TextureUsage::TARGET, - }); - - ( - Some(msaa), - Some(self.gpu.create_texture_view( - msaa, - gpu::TextureViewDesc { - name: "msaa texture view", - format, - dimension: gpu::ViewDimension::D2, - subresources: &Default::default(), - }, - )), - ) - } else { - (None, None) - }; - let raw = self.gpu.create_texture(gpu::TextureDesc { name: "atlas", format, @@ -277,8 +240,6 @@ impl BladeAtlasState { format, raw, raw_view, - msaa, - msaa_view, live_atlas_keys: 0, }; @@ -393,8 +354,6 @@ struct BladeAtlasTexture { allocator: BucketedAtlasAllocator, raw: gpu::Texture, raw_view: gpu::TextureView, - msaa: Option, - msaa_view: Option, format: gpu::TextureFormat, live_atlas_keys: u32, } @@ -422,12 +381,6 @@ impl BladeAtlasTexture { fn destroy(&mut self, gpu: &gpu::Context) { gpu.destroy_texture(self.raw); gpu.destroy_texture_view(self.raw_view); - if let Some(msaa) = self.msaa { - gpu.destroy_texture(msaa); - } - if let Some(msaa_view) = self.msaa_view { - gpu.destroy_texture_view(msaa_view); - } } fn bytes_per_pixel(&self) -> u8 { diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 200ebaaf07fd2b..ee8ffdfda7fa26 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -7,18 +7,16 @@ use crate::{ MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Underline, }; -use blade_graphics as gpu; -use blade_util::{BufferBelt, BufferBeltDescriptor}; use bytemuck::{Pod, Zeroable}; use collections::HashMap; #[cfg(target_os = "macos")] use media::core_video::CVMetalTextureCache; + +use blade_graphics as gpu; +use blade_util::{BufferBelt, BufferBeltDescriptor}; use std::{mem, sync::Arc}; const MAX_FRAME_TIME_MS: u32 = 10000; -// Use 4x MSAA, all devices support it. -// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount -const PATH_SAMPLE_COUNT: u32 = 4; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] @@ -210,10 +208,7 @@ impl BladePipelines { blend: Some(gpu::BlendState::ADDITIVE), write_mask: gpu::ColorWrites::default(), }], - multisample_state: gpu::MultisampleState { - sample_count: PATH_SAMPLE_COUNT, - ..Default::default() - }, + multisample_state: gpu::MultisampleState::default(), }), paths: gpu.create_render_pipeline(gpu::RenderPipelineDesc { name: "paths", @@ -353,7 +348,7 @@ impl BladeRenderer { min_chunk_size: 0x1000, alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe }); - let atlas = Arc::new(BladeAtlas::new(&context.gpu, PATH_SAMPLE_COUNT)); + let atlas = Arc::new(BladeAtlas::new(&context.gpu)); let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc { name: "atlas", mag_filter: gpu::FilterMode::Linear, @@ -502,38 +497,27 @@ impl BladeRenderer { }; let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; - let frame_view = tex_info.raw_view; - let color_target = if let Some(msaa_view) = tex_info.msaa_view { - gpu::RenderTarget { - view: msaa_view, - init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), - finish_op: gpu::FinishOp::ResolveTo(frame_view), - } - } else { - gpu::RenderTarget { - view: frame_view, - init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), - finish_op: gpu::FinishOp::Store, - } - }; - - if let mut pass = self.command_encoder.render( + let mut pass = self.command_encoder.render( "paths", gpu::RenderTargetSet { - colors: &[color_target], + colors: &[gpu::RenderTarget { + view: tex_info.raw_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), + finish_op: gpu::FinishOp::Store, + }], depth_stencil: None, }, - ) { - let mut encoder = pass.with(&self.pipelines.path_rasterization); - encoder.bind( - 0, - &ShaderPathRasterizationData { - globals, - b_path_vertices: vertex_buf, - }, - ); - encoder.draw(0, vertices.len() as u32, 0, 1); - } + ); + + let mut encoder = pass.with(&self.pipelines.path_rasterization); + encoder.bind( + 0, + &ShaderPathRasterizationData { + globals, + b_path_vertices: vertex_buf, + }, + ); + encoder.draw(0, vertices.len() as u32, 0, 1); } } diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index 4662761a7d32a2..ca595c5ce34753 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -13,14 +13,13 @@ use std::borrow::Cow; pub(crate) struct MetalAtlas(Mutex); impl MetalAtlas { - pub(crate) fn new(device: Device, path_sample_count: u32) -> Self { + pub(crate) fn new(device: Device) -> Self { MetalAtlas(Mutex::new(MetalAtlasState { device: AssertSend(device), monochrome_textures: Default::default(), polychrome_textures: Default::default(), path_textures: Default::default(), tiles_by_key: Default::default(), - path_sample_count, })) } @@ -28,10 +27,6 @@ impl MetalAtlas { self.0.lock().texture(id).metal_texture.clone() } - pub(crate) fn msaa_texture(&self, id: AtlasTextureId) -> Option { - self.0.lock().texture(id).msaa_texture.clone() - } - pub(crate) fn allocate( &self, size: Size, @@ -59,7 +54,6 @@ struct MetalAtlasState { polychrome_textures: AtlasTextureList, path_textures: AtlasTextureList, tiles_by_key: FxHashMap, - path_sample_count: u32, } impl PlatformAtlas for MetalAtlas { @@ -182,18 +176,6 @@ impl MetalAtlasState { texture_descriptor.set_usage(usage); let metal_texture = self.device.new_texture(&texture_descriptor); - // We currently only enable MSAA for path textures. - let msaa_texture = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path { - let mut descriptor = texture_descriptor.clone(); - descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); - descriptor.set_storage_mode(metal::MTLStorageMode::Private); - descriptor.set_sample_count(self.path_sample_count as _); - let msaa_texture = self.device.new_texture(&descriptor); - Some(msaa_texture) - } else { - None - }; - let texture_list = match kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, @@ -209,7 +191,6 @@ impl MetalAtlasState { }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), metal_texture: AssertSend(metal_texture), - msaa_texture: AssertSend(msaa_texture), live_atlas_keys: 0, }; @@ -236,7 +217,6 @@ struct MetalAtlasTexture { id: AtlasTextureId, allocator: BucketedAtlasAllocator, metal_texture: AssertSend, - msaa_texture: AssertSend>, live_atlas_keys: u32, } diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 56109d2ff6e5cd..c290d12f7e7521 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -28,9 +28,6 @@ pub(crate) type PointF = crate::Point; const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); #[cfg(feature = "runtime_shaders")] const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal")); -// Use 4x MSAA, all devices support it. -// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount -const PATH_SAMPLE_COUNT: u32 = 4; pub type Context = Arc>; pub type Renderer = MetalRenderer; @@ -173,7 +170,6 @@ impl MetalRenderer { "path_rasterization_vertex", "path_rasterization_fragment", MTLPixelFormat::R16Float, - PATH_SAMPLE_COUNT, ); let path_sprites_pipeline_state = build_pipeline_state( &device, @@ -233,7 +229,7 @@ impl MetalRenderer { ); let command_queue = device.new_command_queue(); - let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), PATH_SAMPLE_COUNT)); + let sprite_atlas = Arc::new(MetalAtlas::new(device.clone())); let core_video_texture_cache = unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() }; @@ -535,20 +531,10 @@ impl MetalRenderer { .unwrap(); let texture = self.sprite_atlas.metal_texture(texture_id); - let msaa_texture = self.sprite_atlas.msaa_texture(texture_id); - - if let Some(msaa_texture) = msaa_texture { - color_attachment.set_texture(Some(&msaa_texture)); - color_attachment.set_resolve_texture(Some(&texture)); - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve); - } else { - color_attachment.set_texture(Some(&texture)); - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_store_action(metal::MTLStoreAction::Store); - } + color_attachment.set_texture(Some(&texture)); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_store_action(metal::MTLStoreAction::Store); color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.)); - let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state); command_encoder.set_vertex_buffer( @@ -1174,7 +1160,6 @@ fn build_path_rasterization_pipeline_state( vertex_fn_name: &str, fragment_fn_name: &str, pixel_format: metal::MTLPixelFormat, - path_sample_count: u32, ) -> metal::RenderPipelineState { let vertex_fn = library .get_function(vertex_fn_name, None) @@ -1187,10 +1172,6 @@ fn build_path_rasterization_pipeline_state( descriptor.set_label(label); descriptor.set_vertex_function(Some(vertex_fn.as_ref())); descriptor.set_fragment_function(Some(fragment_fn.as_ref())); - if path_sample_count > 1 { - descriptor.set_raster_sample_count(path_sample_count as _); - descriptor.set_alpha_to_coverage_enabled(true); - } let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); color_attachment.set_pixel_format(pixel_format); color_attachment.set_blending_enabled(true); diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index b837f2ad9131b6..778a5d1f273418 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -715,13 +715,6 @@ impl Path { } } - /// Move the start, current point to the given point. - pub fn move_to(&mut self, to: Point) { - self.contour_count += 1; - self.start = to; - self.current = to; - } - /// Draw a straight line from the current point to the given point. pub fn line_to(&mut self, to: Point) { self.contour_count += 1; @@ -751,8 +744,7 @@ impl Path { self.current = to; } - /// Push a triangle to the Path. - pub fn push_triangle( + fn push_triangle( &mut self, xy: (Point, Point, Point), st: (Point, Point, Point), From af77b2f99fa716c9f5775711506e2f000cc24c8e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 7 Feb 2025 03:12:53 +0100 Subject: [PATCH 529/650] WIP refactor (#110) * WIP Co-Authored-By: Piotr Osiewicz Co-Authored-By: Anthony Eid * Tear stuff out and make the world burn Co-authored-by: Remco Smits Co-authored-by: Piotr * Fix compile errors * Remove dap_store from module list and fix some warnings Module list now uses Entity to get active modules and handle remote/local state so dap_store is no longer needed * Add Cacheable Command trait This gets rid of ClientRequest or whatever the name was; we don't need to enumerate every possible request and repeat ourselves, instead letting you mark any request as cached with no extra boilerplate. * Add Eq requirement for RequestSlot * Implement DapCommand for Arc That way we can use a single allocated block for each dap command to store it as both the cache key and the command itself. * Clone Arc on demand * Add request helper * Start work on setting up a new dap_command_handler * Make clippy pass * Add loaded sources dap command * Set up module list test to see if Modules request is called * Fix compile warnings * Add basic local module_list test * Add module list event testing to test_module_list * Bring back as_any_arc * Only reset module list's list state when modules_len changes --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Co-authored-by: Anthony Eid Co-authored-by: Piotr --- Cargo.lock | 2 +- crates/collab/src/tests/debug_panel_tests.rs | 12 +- crates/dap/Cargo.toml | 2 +- crates/dap/src/lib.rs | 1 - crates/dap/src/session.rs | 146 ------- crates/debugger_tools/src/dap_log.rs | 7 +- crates/debugger_ui/src/attach_modal.rs | 3 +- crates/debugger_ui/src/console.rs | 5 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 32 +- crates/debugger_ui/src/module_list.rs | 162 +++----- crates/debugger_ui/src/stack_frame_list.rs | 2 +- crates/debugger_ui/src/tests.rs | 1 + crates/debugger_ui/src/tests/module_list.rs | 233 +++++++++++ crates/debugger_ui/src/variable_list.rs | 6 +- crates/project/src/dap_command.rs | 235 +++++++++-- crates/project/src/dap_session.rs | 366 ++++++++++++++++++ crates/project/src/dap_store.rs | 41 +- crates/project/src/project.rs | 7 +- crates/proto/proto/zed.proto | 26 +- crates/proto/src/proto.rs | 6 + 21 files changed, 969 insertions(+), 328 deletions(-) delete mode 100644 crates/dap/src/session.rs create mode 100644 crates/debugger_ui/src/tests/module_list.rs create mode 100644 crates/project/src/dap_session.rs diff --git a/Cargo.lock b/Cargo.lock index 59b6b0e9415c4e..92e0f712cc4a4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3633,7 +3633,7 @@ dependencies = [ [[package]] name = "dap-types" version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=f44d7d8af3a8af3e0ca09933271bee17c99e15b1#f44d7d8af3a8af3e0ca09933271bee17c99e15b1" +source = "git+https://github.com/zed-industries/dap-types?rev=bf5632dc19f806e8a435c9f04a4bfe7322badea2#bf5632dc19f806e8a435c9f04a4bfe7322badea2" dependencies = [ "schemars", "serde", diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 8ab704be4cdc1a..cd1b30d3616676 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1255,7 +1255,7 @@ async fn test_module_list( "Local supports modules request should be true" ); - let local_module_list = item.module_list().read(cx).modules(); + let local_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); assert_eq!( 2usize, @@ -1263,7 +1263,7 @@ async fn test_module_list( "Local module list should have two items in it" ); assert_eq!( - &modules.clone(), + modules.clone(), local_module_list, "Local module list should match module list from response" ); @@ -1282,7 +1282,7 @@ async fn test_module_list( item.capabilities(cx).supports_modules_request.unwrap(), "Remote capabilities supports modules request should be true" ); - let remote_module_list = item.module_list().read(cx).modules(); + let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); assert_eq!( 2usize, @@ -1290,7 +1290,7 @@ async fn test_module_list( "Remote module list should have two items in it" ); assert_eq!( - &modules.clone(), + modules.clone(), remote_module_list, "Remote module list should match module list from response" ); @@ -1313,7 +1313,7 @@ async fn test_module_list( item.capabilities(cx).supports_modules_request.unwrap(), "Remote (mid session join) capabilities supports modules request should be true" ); - let remote_module_list = item.module_list().read(cx).modules(); + let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); assert_eq!( 2usize, @@ -1321,7 +1321,7 @@ async fn test_module_list( "Remote (mid session join) module list should have two items in it" ); assert_eq!( - &modules.clone(), + modules.clone(), remote_module_list, "Remote (mid session join) module list should match module list from response" ); diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml index b604c269100711..f2f1a7f613e8e1 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -25,7 +25,7 @@ async-tar.workspace = true async-trait.workspace = true client.workspace = true collections.workspace = true -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "f44d7d8af3a8af3e0ca09933271bee17c99e15b1" } +dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "bf5632dc19f806e8a435c9f04a4bfe7322badea2" } fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index d87f24038d5225..fc458dc8763c88 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -2,7 +2,6 @@ pub mod adapters; pub mod client; pub mod debugger_settings; pub mod proto_conversions; -pub mod session; pub mod transport; pub use dap_types::*; diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs deleted file mode 100644 index eda6129cb86a35..00000000000000 --- a/crates/dap/src/session.rs +++ /dev/null @@ -1,146 +0,0 @@ -use collections::HashMap; -use gpui::Context; -use std::sync::Arc; -use task::DebugAdapterConfig; - -use crate::client::{DebugAdapterClient, DebugAdapterClientId}; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct DebugSessionId(pub usize); - -impl DebugSessionId { - pub fn from_proto(session_id: u64) -> Self { - Self(session_id as usize) - } - - pub fn to_proto(&self) -> u64 { - self.0 as u64 - } -} - -pub struct DebugSession { - id: DebugSessionId, - mode: DebugSessionMode, - ignore_breakpoints: bool, -} - -pub enum DebugSessionMode { - Local(LocalDebugSession), - Remote(RemoteDebugSession), -} - -pub struct LocalDebugSession { - configuration: DebugAdapterConfig, - clients: HashMap>, -} - -impl LocalDebugSession { - pub fn configuration(&self) -> &DebugAdapterConfig { - &self.configuration - } - - pub fn update_configuration( - &mut self, - f: impl FnOnce(&mut DebugAdapterConfig), - cx: &mut Context, - ) { - f(&mut self.configuration); - cx.notify(); - } - - pub fn add_client(&mut self, client: Arc, cx: &mut Context) { - self.clients.insert(client.id(), client); - cx.notify(); - } - - pub fn remove_client( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) -> Option> { - let client = self.clients.remove(client_id); - cx.notify(); - - client - } - - pub fn client_by_id( - &self, - client_id: &DebugAdapterClientId, - ) -> Option> { - self.clients.get(client_id).cloned() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn clients_len(&self) -> usize { - self.clients.len() - } - - pub fn clients(&self) -> impl Iterator> + '_ { - self.clients.values().cloned() - } - - pub fn client_ids(&self) -> impl Iterator + '_ { - self.clients.keys().cloned() - } -} - -pub struct RemoteDebugSession { - label: String, -} - -impl DebugSession { - pub fn new_local(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { - Self { - id, - ignore_breakpoints: false, - mode: DebugSessionMode::Local(LocalDebugSession { - configuration, - clients: HashMap::default(), - }), - } - } - - pub fn as_local(&self) -> Option<&LocalDebugSession> { - match &self.mode { - DebugSessionMode::Local(local) => Some(local), - _ => None, - } - } - - pub fn as_local_mut(&mut self) -> Option<&mut LocalDebugSession> { - match &mut self.mode { - DebugSessionMode::Local(local) => Some(local), - _ => None, - } - } - - pub fn new_remote(id: DebugSessionId, label: String, ignore_breakpoints: bool) -> Self { - Self { - id, - ignore_breakpoints, - mode: DebugSessionMode::Remote(RemoteDebugSession { label }), - } - } - - pub fn id(&self) -> DebugSessionId { - self.id - } - - pub fn name(&self) -> String { - match &self.mode { - DebugSessionMode::Local(local) => local.configuration.label.clone(), - DebugSessionMode::Remote(remote) => remote.label.clone(), - } - } - - pub fn ignore_breakpoints(&self) -> bool { - self.ignore_breakpoints - } - - pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut Context) { - self.ignore_breakpoints = ignore; - cx.notify(); - } -} diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 8ff2bf7b46d716..e57556ec64b9f0 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -1,7 +1,6 @@ use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, debugger_settings::DebuggerSettings, - session::{DebugSession, DebugSessionId}, transport::{IoKind, LogKind}, }; use editor::{Editor, EditorEvent}; @@ -13,7 +12,11 @@ use gpui::{ actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; -use project::{search::SearchQuery, Project}; +use project::{ + dap_session::{DebugSession, DebugSessionId}, + search::SearchQuery, + Project, +}; use settings::Settings as _; use std::{ borrow::Cow, diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index bbbcd61b112c80..4ab394b6c262b0 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -1,10 +1,9 @@ use dap::client::DebugAdapterClientId; -use dap::session::DebugSessionId; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::Subscription; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; use picker::{Picker, PickerDelegate}; -use project::dap_store::DapStore; +use project::{dap_session::DebugSessionId, dap_store::DapStore}; use std::sync::Arc; use sysinfo::System; use ui::{prelude::*, Context, Tooltip}; diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 1eb05867c51cd4..2a5fef571c7393 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -3,8 +3,7 @@ use crate::{ variable_list::VariableList, }; use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSession, - OutputEvent, OutputEventGroup, + client::DebugAdapterClientId, proto_conversions::ProtoConversion, OutputEvent, OutputEventGroup, }; use editor::{ display_map::{Crease, CreaseId}, @@ -14,7 +13,7 @@ use fuzzy::StringMatchCandidate; use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16}; use menu::Confirm; -use project::{dap_store::DapStore, Completion}; +use project::{dap_session::DebugSession, dap_store::DapStore, Completion}; use rpc::proto; use settings::Settings; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index e937635d452984..54172bc5fff747 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -8,7 +8,6 @@ use dap::{ debugger_settings::DebuggerSettings, messages::{Events, Message}, requests::{Request, RunInTerminal, StartDebugging}, - session::DebugSessionId, Capabilities, CapabilitiesEvent, ContinuedEvent, ErrorResponse, ExitedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, RunInTerminalRequestArguments, RunInTerminalResponse, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason, @@ -18,6 +17,7 @@ use gpui::{ Focusable, Subscription, Task, WeakEntity, }; use project::{ + dap_session::DebugSessionId, dap_store::{DapStore, DapStoreEvent}, terminals::TerminalKind, }; diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index e25fd62fe1d353..27c151948a632e 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -6,7 +6,6 @@ use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; use dap::proto_conversions::{self, ProtoConversion}; -use dap::session::DebugSession; use dap::{ client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, @@ -16,6 +15,7 @@ use editor::Editor; use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; +use project::dap_session::DebugSession; use project::dap_store::DapStore; use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem, UpdateDebugAdapter}; use settings::Settings; @@ -33,6 +33,16 @@ pub enum DebugPanelItemEvent { } #[derive(Clone, PartialEq, Eq)] +#[cfg(any(test, feature = "test-support"))] +pub enum ThreadItem { + Console, + LoadedSource, + Modules, + Variables, +} + +#[derive(Clone, PartialEq, Eq)] +#[cfg(not(any(test, feature = "test-support")))] enum ThreadItem { Console, LoadedSource, @@ -120,8 +130,7 @@ impl DebugPanelItem { ) }); - let module_list = - cx.new(|cx| ModuleList::new(dap_store.clone(), session.clone(), &client_id, cx)); + let module_list = cx.new(|cx| ModuleList::new(session.clone(), &client_id, cx)); let loaded_source_list = cx.new(|cx| LoadedSourceList::new(&this, dap_store.clone(), &client_id, cx)); @@ -253,11 +262,6 @@ impl DebugPanelItem { .update(cx, |this, cx| this.set_from_proto(variable_list_state, cx)); } - if let Some(module_list_state) = state.module_list.as_ref() { - self.module_list - .update(cx, |this, cx| this.set_from_proto(module_list_state, cx)); - } - cx.notify(); } @@ -470,11 +474,7 @@ impl DebugPanelItem { proto::update_debug_adapter::Variant::AddToVariableList(variables_to_add) => self .variable_list .update(cx, |this, _| this.add_variables(variables_to_add.clone())), - proto::update_debug_adapter::Variant::Modules(module_list) => { - self.module_list.update(cx, |this, cx| { - this.set_from_proto(module_list, cx); - }) - } + proto::update_debug_adapter::Variant::Modules(_) => {} proto::update_debug_adapter::Variant::OutputEvent(output_event) => { self.console.update(cx, |this, cx| { this.add_message(OutputEvent::from_proto(output_event.clone()), window, cx); @@ -496,6 +496,12 @@ impl DebugPanelItem { self.thread_id } + #[cfg(any(test, feature = "test-support"))] + pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context) { + self.active_thread_item = thread_item; + cx.notify() + } + #[cfg(any(test, feature = "test-support"))] pub fn stack_frame_list(&self) -> &Entity { &self.stack_frame_list diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 13d21189cf0182..07f73a9144b314 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,26 +1,18 @@ -use anyhow::Result; -use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSession, - Module, ModuleEvent, -}; -use gpui::{list, AnyElement, Entity, FocusHandle, Focusable, ListState, Task}; -use project::dap_store::DapStore; -use rpc::proto::{DebuggerModuleList, UpdateDebugAdapter}; +use dap::{client::DebugAdapterClientId, ModuleEvent}; +use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState}; +use project::dap_session::DebugSession; use ui::prelude::*; -use util::ResultExt; pub struct ModuleList { list: ListState, - modules: Vec, focus_handle: FocusHandle, - dap_store: Entity, session: Entity, client_id: DebugAdapterClientId, + modules_len: usize, } impl ModuleList { pub fn new( - dap_store: Entity, session: Entity, client_id: &DebugAdapterClientId, cx: &mut Context, @@ -40,109 +32,48 @@ impl ModuleList { }, ); - let this = Self { + session.update(cx, |session, _cx| { + session.client_state(*client_id).unwrap(); + }); + + Self { list, session, - dap_store, focus_handle, client_id: *client_id, - modules: Vec::default(), - }; - - if this.dap_store.read(cx).as_local().is_some() { - this.fetch_modules(cx).detach_and_log_err(cx); - } - this - } - - pub(crate) fn set_from_proto( - &mut self, - module_list: &DebuggerModuleList, - cx: &mut Context, - ) { - self.modules = module_list - .modules - .iter() - .filter_map(|payload| Module::from_proto(payload.clone()).log_err()) - .collect(); - - self.client_id = DebugAdapterClientId::from_proto(module_list.client_id); - - self.list.reset(self.modules.len()); - cx.notify(); - } - - pub(crate) fn to_proto(&self) -> DebuggerModuleList { - DebuggerModuleList { - client_id: self.client_id.to_proto(), - modules: self - .modules - .iter() - .map(|module| module.to_proto()) - .collect(), + modules_len: 0, } } pub fn on_module_event(&mut self, event: &ModuleEvent, cx: &mut Context) { - match event.reason { - dap::ModuleEventReason::New => self.modules.push(event.module.clone()), - dap::ModuleEventReason::Changed => { - if let Some(module) = self.modules.iter_mut().find(|m| m.id == event.module.id) { - *module = event.module.clone(); - } + if let Some(state) = self.session.read(cx).client_state(self.client_id) { + let modules_len = state.update(cx, |state, cx| { + state.handle_module_event(event); + state.modules(cx).len() + }); + + if modules_len != self.modules_len { + self.modules_len = modules_len; + self.list.reset(self.modules_len); } - dap::ModuleEventReason::Removed => self.modules.retain(|m| m.id != event.module.id), - } - - self.list.reset(self.modules.len()); - cx.notify(); - - let task = cx.spawn(|this, mut cx| async move { - this.update(&mut cx, |this, cx| { - this.propagate_updates(cx); - }) - .log_err(); - }); - - cx.background_executor().spawn(task).detach(); - } - - fn fetch_modules(&self, cx: &mut Context) -> Task> { - let task = self - .dap_store - .update(cx, |store, cx| store.modules(&self.client_id, cx)); - - cx.spawn(|this, mut cx| async move { - let mut modules = task.await?; - - this.update(&mut cx, |this, cx| { - std::mem::swap(&mut this.modules, &mut modules); - this.list.reset(this.modules.len()); - cx.notify(); - - this.propagate_updates(cx); - }) - }) - } - fn propagate_updates(&self, cx: &Context) { - if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { - let request = UpdateDebugAdapter { - session_id: self.session.read(cx).id().to_proto(), - client_id: self.client_id.to_proto(), - project_id: *id, - thread_id: None, - variant: Some(rpc::proto::update_debug_adapter::Variant::Modules( - self.to_proto(), - )), - }; - - client.send(request).log_err(); + cx.notify() } } fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { - let module = &self.modules[ix]; + let Some((module_name, module_path)) = self.session.update(cx, |session, cx| { + session + .client_state(self.client_id)? + .update(cx, |state, cx| { + state + .modules(cx) + .get(ix) + .map(|module| (module.name.clone(), module.path.clone())) + }) + }) else { + return Empty.into_any(); + }; v_flex() .rounded_md() @@ -150,12 +81,12 @@ impl ModuleList { .group("") .p_1() .hover(|s| s.bg(cx.theme().colors().element_hover)) - .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone())) + .child(h_flex().gap_0p5().text_ui_sm(cx).child(module_name)) .child( h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(module.path.clone(), |this, path| this.child(path)), + .when_some(module_path, |this, path| this.child(path)), ) .into_any() } @@ -168,7 +99,19 @@ impl Focusable for ModuleList { } impl Render for ModuleList { - fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let session = self.session.read(cx); + let modules_len = session + .client_state(self.client_id) + .map_or(0usize, |state| { + state.update(cx, |state, cx| state.modules(cx).len()) + }); + + if modules_len != self.modules_len { + self.modules_len = modules_len; + self.list.reset(self.modules_len); + } + div() .size_full() .p_1() @@ -176,9 +119,16 @@ impl Render for ModuleList { } } +#[cfg(any(test, feature = "test-support"))] +use dap::Module; + #[cfg(any(test, feature = "test-support"))] impl ModuleList { - pub fn modules(&self) -> &Vec { - &self.modules + pub fn modules(&self, cx: &mut Context) -> Vec { + let Some(state) = self.session.read(cx).client_state(self.client_id) else { + return vec![]; + }; + + state.update(cx, |state, cx| state.modules(cx).iter().cloned().collect()) } } diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 07949e3af64254..524b7ba61fa051 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -3,12 +3,12 @@ use std::path::Path; use anyhow::{anyhow, Result}; use dap::client::DebugAdapterClientId; use dap::proto_conversions::ProtoConversion; -use dap::session::DebugSession; use dap::StackFrame; use gpui::{ list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, WeakEntity, }; +use project::dap_session::DebugSession; use project::dap_store::DapStore; use project::ProjectPath; use rpc::proto::{DebuggerStackFrameList, UpdateDebugAdapter}; diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 76682fc46cda26..a5dd323b6499c4 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -9,6 +9,7 @@ use crate::{debugger_panel::DebugPanel, debugger_panel_item::DebugPanelItem}; mod attach_modal; mod console; mod debugger_panel; +mod module_list; mod stack_frame_list; mod variable_list; diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs new file mode 100644 index 00000000000000..3e6fe3fbb95a4c --- /dev/null +++ b/crates/debugger_ui/src/tests/module_list.rs @@ -0,0 +1,233 @@ +use crate::{ + debugger_panel_item::ThreadItem, + tests::{active_debug_panel_item, init_test, init_test_workspace}, +}; +use dap::{ + requests::{Disconnect, Initialize, Launch, Modules, StackTrace}, + StoppedEvent, +}; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use project::{FakeFs, Project}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +#[gpui::test] +async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.start_debug_session( + task::DebugAdapterConfig { + label: "test config".into(), + kind: task::DebugAdapterKind::Fake, + request: task::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_modules_request: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + let called_modules = Arc::new(AtomicBool::new(false)); + let modules = vec![ + dap::Module { + id: dap::ModuleId::Number(1), + name: "First Module".into(), + address_range: None, + date_time_stamp: None, + path: None, + symbol_file_path: None, + symbol_status: None, + version: None, + is_optimized: None, + is_user_code: None, + }, + dap::Module { + id: dap::ModuleId::Number(2), + name: "Second Module".into(), + address_range: None, + date_time_stamp: None, + path: None, + symbol_file_path: None, + symbol_status: None, + version: None, + is_optimized: None, + is_user_code: None, + }, + ]; + + client + .on_request::({ + let called_modules = called_modules.clone(); + let modules = modules.clone(); + move |_, _| unsafe { + static mut REQUEST_COUNT: i32 = 1; + assert_eq!( + 1, REQUEST_COUNT, + "This request should only be called once from the host" + ); + REQUEST_COUNT += 1; + called_modules.store(true, Ordering::SeqCst); + + Ok(dap::ModulesResponse { + modules: modules.clone(), + total_modules: Some(2u64), + }) + } + }) + .await; + + client + .fake_event(dap::messages::Events::Stopped(StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + cx.run_until_parked(); + + assert!( + !called_modules.load(std::sync::atomic::Ordering::SeqCst), + "Request Modules shouldn't be called before it's needed" + ); + + active_debug_panel_item(workspace, cx).update(cx, |item, cx| { + item.set_thread_item(ThreadItem::Modules, cx); + }); + + cx.run_until_parked(); + + assert!( + called_modules.load(std::sync::atomic::Ordering::SeqCst), + "Request Modules should be called because a user clicked on the module list" + ); + + active_debug_panel_item(workspace, cx).update(cx, |item, cx| { + item.set_thread_item(ThreadItem::Modules, cx); + + let actual_modules = item.module_list().update(cx, |list, cx| list.modules(cx)); + assert_eq!(modules, actual_modules); + }); + + // Test all module events now + // New Module + // Changed + // Removed + + let new_module = dap::Module { + id: dap::ModuleId::Number(3), + name: "Third Module".into(), + address_range: None, + date_time_stamp: None, + path: None, + symbol_file_path: None, + symbol_status: None, + version: None, + is_optimized: None, + is_user_code: None, + }; + + client + .fake_event(dap::messages::Events::Module(dap::ModuleEvent { + reason: dap::ModuleEventReason::New, + module: new_module.clone(), + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |item, cx| { + let actual_modules = item.module_list().update(cx, |list, cx| list.modules(cx)); + assert_eq!(actual_modules.len(), 3); + assert!(actual_modules.contains(&new_module)); + }); + + let changed_module = dap::Module { + id: dap::ModuleId::Number(2), + name: "Modified Second Module".into(), + address_range: None, + date_time_stamp: None, + path: None, + symbol_file_path: None, + symbol_status: None, + version: None, + is_optimized: None, + is_user_code: None, + }; + + client + .fake_event(dap::messages::Events::Module(dap::ModuleEvent { + reason: dap::ModuleEventReason::Changed, + module: changed_module.clone(), + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |item, cx| { + let actual_modules = item.module_list().update(cx, |list, cx| list.modules(cx)); + assert_eq!(actual_modules.len(), 3); + assert!(actual_modules.contains(&changed_module)); + }); + + client + .fake_event(dap::messages::Events::Module(dap::ModuleEvent { + reason: dap::ModuleEventReason::Removed, + module: changed_module.clone(), + })) + .await; + + cx.run_until_parked(); + + active_debug_panel_item(workspace, cx).update(cx, |item, cx| { + let actual_modules = item.module_list().update(cx, |list, cx| list.modules(cx)); + assert_eq!(actual_modules.len(), 2); + assert!(!actual_modules.contains(&changed_module)); + }); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 5171fae947902d..ae6bf3cafb481c 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -1,8 +1,8 @@ use crate::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, session::DebugSession, Scope, - ScopePresentationHint, Variable, + client::DebugAdapterClientId, proto_conversions::ProtoConversion, Scope, ScopePresentationHint, + Variable, }; use editor::{actions::SelectAll, Editor, EditorEvent}; use gpui::{ @@ -10,7 +10,7 @@ use gpui::{ FocusHandle, Focusable, Hsla, ListOffset, ListState, MouseDownEvent, Point, Subscription, Task, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::dap_store::DapStore; +use project::{dap_session::DebugSession, dap_store::DapStore}; use rpc::proto::{ self, DebuggerScopeVariableIndex, DebuggerVariableContainer, UpdateDebugAdapter, VariableListScopes, VariableListVariables, diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs index 3ee50cc2ea3816..73b5c2e4e739bf 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/dap_command.rs @@ -1,9 +1,10 @@ +use std::sync::Arc; + use anyhow::{Ok, Result}; use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, requests::{Continue, Next}, - session::DebugSessionId, ContinueArguments, NextArguments, StepInArguments, StepOutArguments, SteppingGranularity, ValueFormat, Variable, VariablesArgumentsFilter, }; @@ -11,9 +12,9 @@ use gpui::{AsyncApp, WeakEntity}; use rpc::proto; use util::ResultExt; -use crate::dap_store::DapStore; +use crate::{dap_session::DebugSessionId, dap_store::DapStore}; -pub trait DapCommand: 'static + Sized + Send + Sync + std::fmt::Debug { +pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; @@ -44,7 +45,7 @@ pub trait DapCommand: 'static + Sized + Send + Sync + std::fmt::Debug { ) -> ::Response; fn response_from_proto( - self, + &self, message: ::Response, ) -> Result; @@ -56,7 +57,54 @@ pub trait DapCommand: 'static + Sized + Send + Sync + std::fmt::Debug { ) -> Result; } -#[derive(Debug)] +impl DapCommand for Arc { + type Response = T::Response; + type DapRequest = T::DapRequest; + type ProtoRequest = T::ProtoRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + T::client_id_from_proto(request) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Arc::new(T::from_proto(request)) + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest { + T::to_proto(self, debug_client_id, upstream_project_id) + } + + fn response_to_proto( + debug_client_id: &DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + T::response_to_proto(debug_client_id, message) + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + T::response_from_proto(self, message) + } + + fn to_dap(&self) -> ::Arguments { + T::to_dap(self) + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + T::response_from_dap(self, message) + } +} + +#[derive(Debug, Hash, PartialEq, Eq)] pub struct StepCommand { pub thread_id: u64, pub granularity: Option, @@ -82,7 +130,7 @@ impl StepCommand { } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct NextCommand { pub inner: StepCommand, } @@ -139,14 +187,14 @@ impl DapCommand for NextCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct StepInCommand { pub inner: StepCommand, } @@ -211,14 +259,14 @@ impl DapCommand for StepInCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct StepOutCommand { pub inner: StepCommand, } @@ -311,14 +359,14 @@ impl DapCommand for StepOutCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct StepBackCommand { pub inner: StepCommand, } @@ -381,14 +429,14 @@ impl DapCommand for StepBackCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct ContinueCommand { pub args: ContinueArguments, } @@ -466,7 +514,7 @@ impl DapCommand for ContinueCommand { } fn response_from_proto( - self, + &self, message: ::Response, ) -> Result { Ok(Self::Response { @@ -485,7 +533,7 @@ impl DapCommand for ContinueCommand { } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct PauseCommand { pub thread_id: u64, } @@ -538,14 +586,14 @@ impl DapCommand for PauseCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct DisconnectCommand { pub restart: Option, pub terminate_debuggee: Option, @@ -606,14 +654,14 @@ impl DapCommand for DisconnectCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct TerminateThreadsCommand { pub thread_ids: Option>, } @@ -670,14 +718,14 @@ impl DapCommand for TerminateThreadsCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct TerminateCommand { pub restart: Option, } @@ -730,14 +778,14 @@ impl DapCommand for TerminateCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub(crate) struct RestartCommand { pub raw: serde_json::Value, } @@ -794,14 +842,14 @@ impl DapCommand for RestartCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } -#[derive(Debug)] +#[derive(Debug, Hash, PartialEq, Eq)] pub struct VariablesCommand { pub stack_frame_id: u64, pub scope_id: u64, @@ -919,14 +967,14 @@ impl DapCommand for VariablesCommand { } fn response_from_proto( - self, + &self, message: ::Response, ) -> Result { Ok(Vec::from_proto(message.variables)) } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) struct RestartStackFrameCommand { pub stack_frame_id: u64, } @@ -979,9 +1027,138 @@ impl DapCommand for RestartStackFrameCommand { } fn response_from_proto( - self, + &self, _message: ::Response, ) -> Result { Ok(()) } } + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct ModulesCommand; + +impl DapCommand for ModulesCommand { + type Response = Vec; + type DapRequest = dap::requests::Modules; + type ProtoRequest = proto::DapModulesRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(_request: &Self::ProtoRequest) -> Self { + Self {} + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapModulesRequest { + proto::DapModulesRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + } + } + + fn response_to_proto( + debug_client_id: &DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapModulesResponse { + modules: message + .into_iter() + .map(|module| module.to_proto()) + .collect(), + client_id: debug_client_id.to_proto(), + } + } + + fn to_dap(&self) -> ::Arguments { + dap::ModulesArguments { + start_module: None, + module_count: None, + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.modules) + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + Ok(message + .modules + .into_iter() + .filter_map(|module| dap::Module::from_proto(module).ok()) + .collect()) + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct LoadedSourcesCommand; + +impl DapCommand for LoadedSourcesCommand { + type Response = Vec; + type DapRequest = dap::requests::LoadedSources; + type ProtoRequest = proto::DapLoadedSourcesRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(_request: &Self::ProtoRequest) -> Self { + Self {} + } + + fn to_proto( + &self, + debug_client_id: &DebugAdapterClientId, + upstream_project_id: u64, + ) -> proto::DapLoadedSourcesRequest { + proto::DapLoadedSourcesRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + } + } + + fn response_to_proto( + debug_client_id: &DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapLoadedSourcesResponse { + sources: message + .into_iter() + .map(|source| source.to_proto()) + .collect(), + client_id: debug_client_id.to_proto(), + } + } + + fn to_dap(&self) -> ::Arguments { + dap::LoadedSourcesArguments {} + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.sources) + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + Ok(message + .sources + .into_iter() + .map(dap::Source::from_proto) + .collect()) + } +} diff --git a/crates/project/src/dap_session.rs b/crates/project/src/dap_session.rs new file mode 100644 index 00000000000000..18339bed99e5d5 --- /dev/null +++ b/crates/project/src/dap_session.rs @@ -0,0 +1,366 @@ +use collections::{BTreeMap, HashMap}; +use dap::{Module, Source}; +use futures::{future::Shared, FutureExt}; +use gpui::{AppContext, Context, Entity, Task, WeakEntity}; +use std::{ + any::Any, + collections::hash_map::Entry, + hash::{Hash, Hasher}, + sync::Arc, +}; +use task::DebugAdapterConfig; +use util::ResultExt; + +use crate::{ + dap_command::{self, DapCommand}, + dap_store::DapStore, +}; +use dap::client::{DebugAdapterClient, DebugAdapterClientId}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct DebugSessionId(pub usize); + +impl DebugSessionId { + pub fn from_proto(session_id: u64) -> Self { + Self(session_id as usize) + } + + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } +} + +#[derive(Copy, Clone, PartialEq, PartialOrd)] +#[repr(transparent)] +struct ThreadId(u64); + +struct Variable { + _variable: dap::Variable, + _variables: Vec, +} + +struct Scope { + _scope: dap::Scope, + _variables: Vec, +} + +struct StackFrame { + _stack_frame: dap::StackFrame, + _scopes: Vec, +} + +#[derive(Copy, Clone, Default, PartialEq, Eq)] +pub enum ThreadStatus { + #[default] + Running, + Stopped, + Exited, + Ended, +} + +struct Thread { + _thread: dap::Thread, + _stack_frames: Vec, + _status: ThreadStatus, + _has_stopped: bool, +} + +pub struct DebugAdapterClientState { + dap_store: WeakEntity, + client_id: DebugAdapterClientId, + modules: Vec, + loaded_sources: Vec, + _threads: BTreeMap, + requests: HashMap>>>, +} + +trait CacheableCommand: 'static + Send + Sync { + fn as_any(&self) -> &dyn Any; + fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool; + fn dyn_hash(&self, hasher: &mut dyn Hasher); + fn as_any_arc(self: Arc) -> Arc; +} + +impl CacheableCommand for T +where + T: DapCommand + PartialEq + Eq + Hash, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool { + rhs.as_any() + .downcast_ref::() + .map_or(false, |rhs| self == rhs) + } + fn dyn_hash(&self, mut hasher: &mut dyn Hasher) { + T::hash(self, &mut hasher); + } + fn as_any_arc(self: Arc) -> Arc { + self + } +} + +pub(crate) struct RequestSlot(Arc); + +impl From for RequestSlot { + fn from(request: T) -> Self { + Self(Arc::new(request)) + } +} + +impl PartialEq for RequestSlot { + fn eq(&self, other: &Self) -> bool { + self.0.dyn_eq(other.0.as_ref()) + } +} + +impl Eq for RequestSlot {} + +impl Hash for RequestSlot { + fn hash(&self, state: &mut H) { + self.0.dyn_hash(state); + self.0.as_any().type_id().hash(state); + } +} + +impl DebugAdapterClientState { + pub(crate) fn _wait_for_request( + &self, + request: R, + ) -> Option>>> { + let request_slot = RequestSlot::from(request); + self.requests.get(&request_slot).cloned() + } + + /// Ensure that there's a request in flight for the given command, and if not, send it. + fn request( + &mut self, + request: T, + process_result: impl FnOnce(&mut Self, T::Response) + 'static + Send + Sync, + cx: &mut Context, + ) { + let slot = request.into(); + let entry = self.requests.entry(slot); + + if let Entry::Vacant(vacant) = entry { + let client_id = self.client_id; + let command = vacant.key().0.clone().as_any_arc().downcast::().unwrap(); + + if let Ok(request) = self.dap_store.update(cx, |dap_store, cx| { + dap_store.request_dap(&client_id, command, cx) + }) { + let task = cx + .spawn(|this, mut cx| async move { + let result = request.await.log_err()?; + this.update(&mut cx, |this, _| { + process_result(this, result); + }) + .log_err() + }) + .shared(); + + vacant.insert(task); + } + } + } + + pub fn modules(&mut self, cx: &mut Context) -> &[Module] { + self.request( + dap_command::ModulesCommand, + |this, result| { + this.modules = result; + }, + cx, + ); + &self.modules + } + + pub fn handle_module_event(&mut self, event: &dap::ModuleEvent) { + match event.reason { + dap::ModuleEventReason::New => self.modules.push(event.module.clone()), + dap::ModuleEventReason::Changed => { + if let Some(module) = self.modules.iter_mut().find(|m| m.id == event.module.id) { + *module = event.module.clone(); + } + } + dap::ModuleEventReason::Removed => self.modules.retain(|m| m.id != event.module.id), + } + } + + pub fn loaded_sources(&mut self, cx: &mut Context) -> &[Source] { + self.request( + dap_command::LoadedSourcesCommand, + |this, result| { + this.loaded_sources = result; + }, + cx, + ); + &self.loaded_sources + } +} + +pub struct DebugSession { + id: DebugSessionId, + mode: DebugSessionMode, + states: HashMap>, + ignore_breakpoints: bool, +} + +pub enum DebugSessionMode { + Local(LocalDebugSession), + Remote(RemoteDebugSession), +} + +pub struct LocalDebugSession { + configuration: DebugAdapterConfig, + clients: HashMap>, +} + +impl LocalDebugSession { + pub fn configuration(&self) -> &DebugAdapterConfig { + &self.configuration + } + + pub fn update_configuration( + &mut self, + f: impl FnOnce(&mut DebugAdapterConfig), + cx: &mut Context, + ) { + f(&mut self.configuration); + cx.notify(); + } + + fn add_client(&mut self, client: Arc, cx: &mut Context) { + self.clients.insert(client.id(), client); + cx.notify(); + } + + pub fn remove_client( + &mut self, + client_id: &DebugAdapterClientId, + cx: &mut Context, + ) -> Option> { + let client = self.clients.remove(client_id); + cx.notify(); + + client + } + + pub fn client_by_id( + &self, + client_id: &DebugAdapterClientId, + ) -> Option> { + self.clients.get(client_id).cloned() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn clients_len(&self) -> usize { + self.clients.len() + } + + pub fn clients(&self) -> impl Iterator> + '_ { + self.clients.values().cloned() + } + + pub fn client_ids(&self) -> impl Iterator + '_ { + self.clients.keys().cloned() + } +} + +pub struct RemoteDebugSession { + label: String, +} + +impl DebugSession { + pub fn new_local(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { + Self { + id, + ignore_breakpoints: false, + states: HashMap::default(), + mode: DebugSessionMode::Local(LocalDebugSession { + configuration, + clients: HashMap::default(), + }), + } + } + + pub fn as_local(&self) -> Option<&LocalDebugSession> { + match &self.mode { + DebugSessionMode::Local(local) => Some(local), + _ => None, + } + } + + pub fn as_local_mut(&mut self) -> Option<&mut LocalDebugSession> { + match &mut self.mode { + DebugSessionMode::Local(local) => Some(local), + _ => None, + } + } + + pub fn new_remote(id: DebugSessionId, label: String, ignore_breakpoints: bool) -> Self { + Self { + id, + ignore_breakpoints, + states: HashMap::default(), + mode: DebugSessionMode::Remote(RemoteDebugSession { label }), + } + } + + pub fn id(&self) -> DebugSessionId { + self.id + } + + pub fn name(&self) -> String { + match &self.mode { + DebugSessionMode::Local(local) => local.configuration.label.clone(), + DebugSessionMode::Remote(remote) => remote.label.clone(), + } + } + + pub fn ignore_breakpoints(&self) -> bool { + self.ignore_breakpoints + } + + pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut Context) { + self.ignore_breakpoints = ignore; + cx.notify(); + } + + pub fn client_state( + &self, + client_id: DebugAdapterClientId, + ) -> Option> { + self.states.get(&client_id).cloned() + } + + pub fn add_client( + &mut self, + client: Option>, + client_id: DebugAdapterClientId, + weak_dap: WeakEntity, + cx: &mut Context, + ) { + if !self.states.contains_key(&client_id) { + let state = cx.new(|_cx| DebugAdapterClientState { + dap_store: weak_dap, + client_id, + modules: Vec::default(), + loaded_sources: Vec::default(), + _threads: BTreeMap::default(), + requests: HashMap::default(), + }); + + self.states.insert(client_id, state); + } + + if let Some(client) = client { + self.as_local_mut() + .expect("Client can only exist on local Zed instances") + .add_client(client, cx); + } + } +} diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 517fe8cea3efd4..60a825b5f44c78 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -4,12 +4,14 @@ use crate::{ RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, VariablesCommand, }, + dap_session::{DebugSession, DebugSessionId}, project_settings::ProjectSettings, ProjectEnvironment, ProjectItem as _, ProjectPath, }; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; +use dap::ContinueResponse; use dap::{ adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}, client::{DebugAdapterClient, DebugAdapterClientId}, @@ -28,10 +30,6 @@ use dap::{ StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, SteppingGranularity, TerminateArguments, Variable, }; -use dap::{ - session::{DebugSession, DebugSessionId}, - ContinueResponse, -}; use dap_adapters::build_adapter; use fs::Fs; use futures::future::Shared; @@ -640,8 +638,10 @@ impl DapStore { .insert(client_id, session_id); let session = store.session_by_id(&session_id).unwrap(); + let weak_dap = cx.weak_entity(); session.update(cx, |session, cx| { + session.add_client(Some(Arc::new(client)), client_id, weak_dap, cx); let local_session = session.as_local_mut().unwrap(); local_session.update_configuration( @@ -650,7 +650,6 @@ impl DapStore { }, cx, ); - local_session.add_client(Arc::new(client), cx); }); // don't emit this event ourself in tests, so we can add request, @@ -788,11 +787,10 @@ impl DapStore { }; this.update(&mut cx, |store, cx| { + let weak_dap = cx.weak_entity(); + session.update(cx, |session, cx| { - session - .as_local_mut() - .unwrap() - .add_client(client.clone(), cx); + session.add_client(Some(client.clone()), client.id(), weak_dap, cx); }); let client_id = client.id(); @@ -1252,7 +1250,7 @@ impl DapStore { self.request_dap(client_id, command, cx) } - fn request_dap( + pub(crate) fn request_dap( &self, client_id: &DebugAdapterClientId, request: R, @@ -1817,6 +1815,29 @@ impl DapStore { Ok(proto::Ack {}) } + async fn _handle_dap_command_2( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result<::Response> + where + ::Arguments: Send, + ::Response: Send, + { + let request = T::from_proto(&envelope.payload); + let client_id = T::client_id_from_proto(&envelope.payload); + + let _state = this.update(&mut cx, |this, cx| { + this.session_by_client_id(&client_id)? + .read(cx) + .client_state(client_id)? + .read(cx) + ._wait_for_request(request) + }); + + todo!() + } + async fn handle_dap_command( this: Entity, envelope: TypedEnvelope, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0070fc42a5273d..fdec049750d61d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2,6 +2,7 @@ pub mod buffer_store; mod color_extractor; pub mod connection_manager; pub mod dap_command; +pub mod dap_session; pub mod dap_store; pub mod debounced_delay; pub mod git; @@ -28,7 +29,10 @@ use git::Repository; pub mod search_history; mod yarn; -use crate::git::GitState; +use crate::{ + dap_session::{DebugSession, DebugSessionId}, + git::GitState, +}; use anyhow::{anyhow, Context as _, Result}; use buffer_store::{BufferChangeSet, BufferStore, BufferStoreEvent}; use client::{ @@ -40,7 +44,6 @@ use dap::{ client::{DebugAdapterClient, DebugAdapterClientId}, debugger_settings::DebuggerSettings, messages::Message, - session::{DebugSession, DebugSessionId}, DebugAdapterConfig, }; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index d462a170abf242..72c8997b6cb3fb 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -343,7 +343,11 @@ message Envelope { DapRestartStackFrameRequest dap_restart_stack_frame_request = 321; IgnoreBreakpointState ignore_breakpoint_state = 322; ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 323; - DebuggerSessionEnded debugger_session_ended = 324; // current max + DebuggerSessionEnded debugger_session_ended = 324; + DapModulesRequest dap_modules_request = 325; + DapModulesResponse dap_modules_response = 326; + DapLoadedSourcesRequest dap_loaded_sources_request = 327; + DapLoadedSourcesResponse dap_loaded_sources_response = 328; // current max } reserved 87 to 88; @@ -2825,6 +2829,26 @@ message DapContinueResponse { optional bool all_threads_continued = 2; } +message DapModulesRequest { + uint64 project_id = 1; + uint64 client_id = 2; +} + +message DapModulesResponse { + uint64 client_id = 1; + repeated DapModule modules = 2; +} + +message DapLoadedSourcesRequest { + uint64 project_id = 1; + uint64 client_id = 2; +} + +message DapLoadedSourcesResponse { + uint64 client_id = 1; + repeated DapSource sources = 2; +} + message DapStackFrame { uint64 id = 1; string name = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 121468db42fdbd..6f55f5c566561d 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -182,6 +182,10 @@ messages!( (CreateRoomResponse, Foreground), (DapContinueRequest, Background), (DapContinueResponse, Background), + (DapModulesRequest, Background), + (DapModulesResponse, Background), + (DapLoadedSourcesRequest, Background), + (DapLoadedSourcesResponse, Background), (DapDisconnectRequest, Background), (DapNextRequest, Background), (DapPauseRequest, Background), @@ -546,6 +550,8 @@ request_messages!( (DapStepOutRequest, Ack), (DapStepBackRequest, Ack), (DapContinueRequest, DapContinueResponse), + (DapModulesRequest, DapModulesResponse), + (DapLoadedSourcesRequest, DapLoadedSourcesResponse), (DapPauseRequest, Ack), (DapDisconnectRequest, Ack), (DapTerminateThreadsRequest, Ack), From 2fc7c17497764c61686e988d3799671edb915018 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 6 Feb 2025 22:29:32 -0500 Subject: [PATCH 530/650] Remove debugger collab db tables (#111) * WIP setup active debug sessions request * Set up active debug sessions request that's send when joining a project * Fix test debug panel console * Remove debugger tables from collab db * Wip request active debug sessions --- .../20221109000000_test_schema.sql | 35 --- .../20250107082721_create_debugger_tables.sql | 33 --- .../20250121181012_add_ignore_breakpoints.sql | 2 - crates/collab/src/db.rs | 1 - crates/collab/src/db/queries/projects.rs | 278 ------------------ crates/collab/src/db/tables.rs | 2 - crates/collab/src/db/tables/debug_clients.rs | 113 ------- .../collab/src/db/tables/debug_panel_items.rs | 167 ----------- crates/collab/src/db/tables/project.rs | 16 - crates/collab/src/rpc.rs | 129 +------- crates/collab/src/tests/debug_panel_tests.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 32 +- crates/debugger_ui/src/tests.rs | 1 - crates/project/src/dap_store.rs | 23 ++ crates/project/src/project.rs | 2 +- crates/proto/proto/zed.proto | 21 +- crates/proto/src/proto.rs | 6 +- 17 files changed, 85 insertions(+), 778 deletions(-) delete mode 100644 crates/collab/migrations/20250107082721_create_debugger_tables.sql delete mode 100644 crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql delete mode 100644 crates/collab/src/db/tables/debug_clients.rs delete mode 100644 crates/collab/src/db/tables/debug_panel_items.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 64140c4156f2bf..afa91dbcc78349 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -469,38 +469,3 @@ CREATE TABLE IF NOT EXISTS "breakpoints" ( "kind" VARCHAR NOT NULL ); CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id"); - -CREATE TABLE IF NOT EXISTS "debug_clients" ( - id BIGINT NOT NULL, - project_id INTEGER NOT NULL, - session_id BIGINT NOT NULL, - capabilities INTEGER NOT NULL, - ignore_breakpoints BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (id, project_id, session_id), - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE -); - -CREATE INDEX "index_debug_client_on_project_id" ON "debug_clients" ("project_id"); - -CREATE TABLE IF NOT EXISTS "debug_panel_items" ( - id BIGINT NOT NULL, - project_id INTEGER NOT NULL, - thread_id BIGINT NOT NULL, - session_id BIGINT NOT NULL, - active_thread_item INTEGER NOT NULL, - seassion_name TEXT NOT NULL, - console BYTEA NOT NULL, - module_list BYTEA NOT NULL, - thread_state BYTEA NOT NULL, - variable_list BYTEA NOT NULL, - stack_frame_list BYTEA NOT NULL, - loaded_source_list BYTEA NOT NULL, - PRIMARY KEY (id, project_id, session_id, thread_id), - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (id, project_id, session_id) REFERENCES debug_clients (id, project_id, session_id) ON DELETE CASCADE -); - -CREATE INDEX "index_debug_panel_items_on_project_id" ON "debug_panel_items" ("project_id"); -CREATE INDEX "index_debug_panel_items_on_session_id" ON "debug_panel_items" ("session_id"); -CREATE INDEX "index_debug_panel_items_on_thread_id" ON "debug_panel_items" ("thread_id"); -CREATE INDEX "index_debug_panel_items_on_debug_client" ON "debug_panel_items" ("id", "project_id", "session_id"); diff --git a/crates/collab/migrations/20250107082721_create_debugger_tables.sql b/crates/collab/migrations/20250107082721_create_debugger_tables.sql deleted file mode 100644 index 79c94faa566b62..00000000000000 --- a/crates/collab/migrations/20250107082721_create_debugger_tables.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE IF NOT EXISTS "debug_clients" ( - id BIGINT NOT NULL, - project_id INTEGER NOT NULL, - session_id BIGINT NOT NULL, - capabilities INTEGER NOT NULL, - PRIMARY KEY (id, project_id, session_id), - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE -); - -CREATE INDEX "index_debug_client_on_project_id" ON "debug_clients" ("project_id"); - -CREATE TABLE IF NOT EXISTS "debug_panel_items" ( - id BIGINT NOT NULL, - project_id INTEGER NOT NULL, - thread_id BIGINT NOT NULL, - session_id BIGINT NOT NULL, - active_thread_item INTEGER NOT NULL, - seassion_name TEXT NOT NULL, - console BYTEA NOT NULL, - module_list BYTEA NOT NULL, - thread_state BYTEA NOT NULL, - variable_list BYTEA NOT NULL, - stack_frame_list BYTEA NOT NULL, - loaded_source_list BYTEA NOT NULL, - PRIMARY KEY (id, project_id, session_id, thread_id), - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (id, project_id, session_id) REFERENCES debug_clients (id, project_id, session_id) ON DELETE CASCADE -); - -CREATE INDEX "index_debug_panel_items_on_project_id" ON "debug_panel_items" ("project_id"); -CREATE INDEX "index_debug_panel_items_on_session_id" ON "debug_panel_items" ("session_id"); -CREATE INDEX "index_debug_panel_items_on_thread_id" ON "debug_panel_items" ("thread_id"); -CREATE INDEX "index_debug_panel_items_on_debug_client" ON "debug_panel_items" ("id", "project_id", "session_id"); diff --git a/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql b/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql deleted file mode 100644 index e1e362c5cf84ad..00000000000000 --- a/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql +++ /dev/null @@ -1,2 +0,0 @@ - -ALTER TABLE debug_clients ADD COLUMN ignore_breakpoints BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f5a00214c04e35..711685c6e96538 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -739,7 +739,6 @@ pub struct Project { pub worktrees: BTreeMap, pub language_servers: Vec, pub breakpoints: HashMap>, - pub debug_sessions: Vec, } pub struct ProjectCollaborator { diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index b25c754d83c7a6..d6f80ce952b082 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -556,239 +556,6 @@ impl Database { .await } - pub async fn ignore_breakpoint_state( - &self, - connection_id: ConnectionId, - update: &proto::IgnoreBreakpointState, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - self.project_transaction(project_id, |tx| async move { - let debug_clients = debug_clients::Entity::find() - .filter( - Condition::all() - .add(debug_clients::Column::ProjectId.eq(project_id)) - .add(debug_clients::Column::SessionId.eq(update.session_id)), - ) - .all(&*tx) - .await?; - - for debug_client in debug_clients { - debug_clients::Entity::update(debug_clients::ActiveModel { - id: ActiveValue::Unchanged(debug_client.id), - project_id: ActiveValue::Unchanged(debug_client.project_id), - session_id: ActiveValue::Unchanged(debug_client.session_id), - capabilities: ActiveValue::Unchanged(debug_client.capabilities), - ignore_breakpoints: ActiveValue::Set(update.ignore), - }) - .exec(&*tx) - .await?; - } - - self.internal_project_connection_ids(project_id, connection_id, true, &tx) - .await - }) - .await - } - - pub async fn update_debug_adapter( - &self, - connection_id: ConnectionId, - update: &proto::UpdateDebugAdapter, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - self.project_transaction(project_id, |tx| async move { - let mut debug_panel_items = debug_panel_items::Entity::find() - .filter( - Condition::all() - .add(debug_panel_items::Column::ProjectId.eq(project_id)) - .add(debug_panel_items::Column::SessionId.eq(update.session_id)) - .add(debug_panel_items::Column::Id.eq(update.client_id)), - ) - .all(&*tx) - .await?; - - if let Some(thread_id) = update.thread_id { - debug_panel_items.retain(|item| item.thread_id == thread_id as i64); - } - - for mut item in debug_panel_items { - item.update_panel_item(&update)?; - - debug_panel_items::Entity::update(debug_panel_items::ActiveModel { - id: ActiveValue::Unchanged(item.id), - project_id: ActiveValue::Unchanged(item.project_id), - session_id: ActiveValue::Unchanged(item.session_id), - thread_id: ActiveValue::Unchanged(item.thread_id), - active_thread_item: ActiveValue::Unchanged(item.active_thread_item), - seassion_name: ActiveValue::Unchanged(item.seassion_name), - console: ActiveValue::Unchanged(item.console), - module_list: ActiveValue::Set(item.module_list), - thread_state: ActiveValue::Set(item.thread_state), - variable_list: ActiveValue::Set(item.variable_list), - stack_frame_list: ActiveValue::Set(item.stack_frame_list), - loaded_source_list: ActiveValue::Unchanged(item.loaded_source_list), - }) - .exec(&*tx) - .await?; - } - - self.internal_project_connection_ids(project_id, connection_id, true, &tx) - .await - }) - .await - } - - pub async fn shutdown_debug_client( - &self, - connection_id: ConnectionId, - update: &proto::ShutdownDebugClient, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - self.project_transaction(project_id, |tx| async move { - debug_clients::Entity::delete_by_id(( - update.client_id as i64, - ProjectId::from_proto(update.project_id), - update.session_id as i64, - )) - .exec(&*tx) - .await?; - - self.internal_project_connection_ids(project_id, connection_id, true, &tx) - .await - }) - .await - } - - pub async fn set_debug_client_panel_item( - &self, - connection_id: ConnectionId, - update: &proto::SetDebuggerPanelItem, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - self.project_transaction(project_id, |tx| async move { - let debug_client = debug_clients::Entity::find() - .filter( - Condition::all() - .add(debug_clients::Column::ProjectId.eq(project_id)) - .add(debug_clients::Column::SessionId.eq(update.session_id)), - ) - .one(&*tx) - .await?; - - if debug_client.is_none() { - let new_debug_client = debug_clients::ActiveModel { - id: ActiveValue::Set(update.client_id as i64), - project_id: ActiveValue::Set(project_id), - session_id: ActiveValue::Set(update.session_id as i64), - capabilities: ActiveValue::Set(0), - ignore_breakpoints: ActiveValue::Set(false), - }; - new_debug_client.insert(&*tx).await?; - } - - let mut debug_panel_item = debug_panel_items::Entity::find() - .filter( - Condition::all() - .add(debug_panel_items::Column::ProjectId.eq(project_id)) - .add(debug_panel_items::Column::SessionId.eq(update.session_id as i64)) - .add(debug_panel_items::Column::ThreadId.eq(update.thread_id as i64)) - .add(debug_panel_items::Column::Id.eq(update.client_id as i64)), - ) - .one(&*tx) - .await?; - - if debug_panel_item.is_none() { - let new_debug_panel_item = debug_panel_items::ActiveModel { - id: ActiveValue::Set(update.client_id as i64), - project_id: ActiveValue::Set(project_id), - session_id: ActiveValue::Set(update.session_id as i64), - thread_id: ActiveValue::Set(update.thread_id as i64), - seassion_name: ActiveValue::Set(update.session_name.clone()), - active_thread_item: ActiveValue::Set(0), - console: ActiveValue::Set(Vec::new()), - module_list: ActiveValue::Set(Vec::new()), - thread_state: ActiveValue::Set(Vec::new()), - variable_list: ActiveValue::Set(Vec::new()), - stack_frame_list: ActiveValue::Set(Vec::new()), - loaded_source_list: ActiveValue::Set(Vec::new()), - }; - - debug_panel_item = Some(new_debug_panel_item.insert(&*tx).await?); - }; - - let mut debug_panel_item = debug_panel_item.unwrap(); - - debug_panel_item.set_panel_item(&update); - debug_panel_items::Entity::update(debug_panel_items::ActiveModel { - id: ActiveValue::Unchanged(debug_panel_item.id), - project_id: ActiveValue::Unchanged(debug_panel_item.project_id), - session_id: ActiveValue::Unchanged(debug_panel_item.session_id), - thread_id: ActiveValue::Unchanged(debug_panel_item.thread_id), - seassion_name: ActiveValue::Unchanged(debug_panel_item.seassion_name), - active_thread_item: ActiveValue::Set(debug_panel_item.active_thread_item), - console: ActiveValue::Set(debug_panel_item.console), - module_list: ActiveValue::Set(debug_panel_item.module_list), - thread_state: ActiveValue::Set(debug_panel_item.thread_state), - variable_list: ActiveValue::Set(debug_panel_item.variable_list), - stack_frame_list: ActiveValue::Set(debug_panel_item.stack_frame_list), - loaded_source_list: ActiveValue::Set(debug_panel_item.loaded_source_list), - }) - .exec(&*tx) - .await?; - - self.internal_project_connection_ids(project_id, connection_id, true, &tx) - .await - }) - .await - } - - pub async fn update_debug_client_capabilities( - &self, - connection_id: ConnectionId, - update: &proto::SetDebugClientCapabilities, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - self.project_transaction(project_id, |tx| async move { - let mut debug_client = debug_clients::Entity::find() - .filter( - Condition::all() - .add(debug_clients::Column::ProjectId.eq(project_id)) - .add(debug_clients::Column::SessionId.eq(update.session_id)), - ) - .one(&*tx) - .await?; - - if debug_client.is_none() { - let new_debug_client = debug_clients::ActiveModel { - id: ActiveValue::Set(update.client_id as i64), - project_id: ActiveValue::Set(project_id), - session_id: ActiveValue::Set(update.session_id as i64), - capabilities: ActiveValue::Set(0), - ignore_breakpoints: ActiveValue::Set(false), - }; - debug_client = Some(new_debug_client.insert(&*tx).await?); - } - - let mut debug_client = debug_client.unwrap(); - - debug_client.set_capabilities(update); - - debug_clients::Entity::update(debug_clients::ActiveModel { - id: ActiveValue::Unchanged(debug_client.id), - project_id: ActiveValue::Unchanged(debug_client.project_id), - session_id: ActiveValue::Unchanged(debug_client.session_id), - capabilities: ActiveValue::Set(debug_client.capabilities), - ignore_breakpoints: ActiveValue::Set(debug_client.ignore_breakpoints), - }) - .exec(&*tx) - .await?; - - self.internal_project_connection_ids(project_id, connection_id, true, &tx) - .await - }) - .await - } - pub async fn update_breakpoints( &self, connection_id: ConnectionId, @@ -1108,50 +875,6 @@ impl Database { } } - let project_id = project.id.to_proto(); - let debug_clients = project.find_related(debug_clients::Entity).all(tx).await?; - let mut debug_sessions: HashMap<_, Vec<_>> = HashMap::default(); - - for debug_client in debug_clients { - debug_sessions - .entry(debug_client.session_id) - .or_default() - .push(debug_client); - } - - let mut sessions: Vec<_> = Vec::default(); - - for (session_id, clients) in debug_sessions.into_iter() { - let mut debug_clients = Vec::default(); - let ignore_breakpoints = clients.iter().any(|debug| debug.ignore_breakpoints); // Temp solution until client -> session change - - for debug_client in clients.into_iter() { - let debug_panel_items = debug_client - .find_related(debug_panel_items::Entity) - .all(tx) - .await? - .into_iter() - .map(|item| item.panel_item()) - .collect(); - - debug_clients.push(proto::DebugClient { - client_id: debug_client.id as u64, - capabilities: Some(debug_client.capabilities()), - debug_panel_items, - active_debug_line: None, - }); - } - - sessions.push(proto::DebuggerSession { - project_id, - session_id: session_id as u64, - clients: debug_clients, - ignore_breakpoints, - }); - } - - let debug_sessions = sessions; - let mut breakpoints: HashMap> = HashMap::default(); @@ -1207,7 +930,6 @@ impl Database { }) .collect(), breakpoints, - debug_sessions, }; Ok((project, replica_id as ReplicaId)) } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index a81bc108aad498..fd4553c52b41c4 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -14,8 +14,6 @@ pub mod channel_message; pub mod channel_message_mention; pub mod contact; pub mod contributor; -pub mod debug_clients; -pub mod debug_panel_items; pub mod embedding; pub mod extension; pub mod extension_version; diff --git a/crates/collab/src/db/tables/debug_clients.rs b/crates/collab/src/db/tables/debug_clients.rs deleted file mode 100644 index 498b9ed7359091..00000000000000 --- a/crates/collab/src/db/tables/debug_clients.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::db::ProjectId; -use anyhow::Result; -use rpc::proto::SetDebugClientCapabilities; -use sea_orm::entity::prelude::*; - -const SUPPORTS_LOADED_SOURCES_REQUEST_BIT: u32 = 0; -const SUPPORTS_MODULES_REQUEST_BIT: u32 = 1; -const SUPPORTS_RESTART_REQUEST_BIT: u32 = 2; -const SUPPORTS_SET_EXPRESSION_BIT: u32 = 3; -const SUPPORTS_SINGLE_THREAD_EXECUTION_REQUESTS_BIT: u32 = 4; -const SUPPORTS_STEP_BACK_BIT: u32 = 5; -const SUPPORTS_STEPPING_GRANULARITY_BIT: u32 = 6; -const SUPPORTS_TERMINATE_THREADS_REQUEST_BIT: u32 = 7; -const SUPPORTS_RESTART_FRAME_REQUEST_BIT: u32 = 8; -const SUPPORTS_CLIPBOARD_CONTEXT_BIT: u32 = 9; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "debug_clients")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(primary_key)] - pub project_id: ProjectId, - #[sea_orm(primary_key)] - pub session_id: i64, - #[sea_orm(column_type = "Integer")] - pub capabilities: i32, - pub ignore_breakpoints: bool, -} - -impl Model { - pub fn capabilities(&self) -> SetDebugClientCapabilities { - SetDebugClientCapabilities { - session_id: self.session_id as u64, - client_id: self.id as u64, - project_id: ProjectId::to_proto(self.project_id), - supports_loaded_sources_request: (self.capabilities - & (1 << SUPPORTS_LOADED_SOURCES_REQUEST_BIT)) - != 0, - supports_modules_request: (self.capabilities & (1 << SUPPORTS_MODULES_REQUEST_BIT)) - != 0, - supports_restart_request: (self.capabilities & (1 << SUPPORTS_RESTART_REQUEST_BIT)) - != 0, - supports_single_thread_execution_requests: (self.capabilities - & (1 << SUPPORTS_SINGLE_THREAD_EXECUTION_REQUESTS_BIT)) - != 0, - supports_set_expression: (self.capabilities & (1 << SUPPORTS_SET_EXPRESSION_BIT)) != 0, - supports_step_back: (self.capabilities & (1 << SUPPORTS_STEP_BACK_BIT)) != 0, - supports_stepping_granularity: (self.capabilities - & (1 << SUPPORTS_STEPPING_GRANULARITY_BIT)) - != 0, - supports_terminate_threads_request: (self.capabilities - & (1 << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT)) - != 0, - supports_restart_frame_request: (self.capabilities - & (1 << SUPPORTS_RESTART_FRAME_REQUEST_BIT)) - != 0, - supports_clipboard_context: (self.capabilities & (1 << SUPPORTS_CLIPBOARD_CONTEXT_BIT)) - != 0, - } - } - - pub fn set_capabilities(&mut self, capabilities: &SetDebugClientCapabilities) { - let mut capabilities_bit_mask = 0i32; - capabilities_bit_mask |= (capabilities.supports_loaded_sources_request as i32) - << SUPPORTS_LOADED_SOURCES_REQUEST_BIT; - capabilities_bit_mask |= - (capabilities.supports_modules_request as i32) << SUPPORTS_MODULES_REQUEST_BIT; - capabilities_bit_mask |= - (capabilities.supports_restart_request as i32) << SUPPORTS_RESTART_REQUEST_BIT; - capabilities_bit_mask |= - (capabilities.supports_set_expression as i32) << SUPPORTS_SET_EXPRESSION_BIT; - capabilities_bit_mask |= (capabilities.supports_single_thread_execution_requests as i32) - << SUPPORTS_SINGLE_THREAD_EXECUTION_REQUESTS_BIT; - capabilities_bit_mask |= (capabilities.supports_step_back as i32) << SUPPORTS_STEP_BACK_BIT; - capabilities_bit_mask |= (capabilities.supports_stepping_granularity as i32) - << SUPPORTS_STEPPING_GRANULARITY_BIT; - capabilities_bit_mask |= (capabilities.supports_terminate_threads_request as i32) - << SUPPORTS_TERMINATE_THREADS_REQUEST_BIT; - capabilities_bit_mask |= (capabilities.supports_restart_frame_request as i32) - << SUPPORTS_RESTART_FRAME_REQUEST_BIT; - capabilities_bit_mask |= - (capabilities.supports_clipboard_context as i32) << SUPPORTS_CLIPBOARD_CONTEXT_BIT; - - self.capabilities = capabilities_bit_mask; - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::project::Entity", - from = "Column::ProjectId", - to = "super::project::Column::Id" - )] - Project, - #[sea_orm(has_many = "super::debug_panel_items::Entity")] - DebugPanelItems, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Project.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::DebugPanelItems.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/debug_panel_items.rs b/crates/collab/src/db/tables/debug_panel_items.rs deleted file mode 100644 index 152328cf965939..00000000000000 --- a/crates/collab/src/db/tables/debug_panel_items.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::db::ProjectId; -use anyhow::{anyhow, Context, Result}; -use prost::Message; -use rpc::{proto, proto::SetDebuggerPanelItem}; -use sea_orm::entity::prelude::*; -use util::ResultExt; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "debug_panel_items")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(primary_key)] - pub project_id: ProjectId, - #[sea_orm(primary_key)] - pub session_id: i64, - #[sea_orm(primary_key)] - pub thread_id: i64, - // Below are fields for a debug panel item - pub active_thread_item: i32, - pub seassion_name: String, - pub console: Vec, - pub module_list: Vec, - pub thread_state: Vec, - pub variable_list: Vec, - pub stack_frame_list: Vec, - pub loaded_source_list: Vec, -} - -impl Model { - pub fn set_panel_item(&mut self, item: &SetDebuggerPanelItem) { - let mut buf = Vec::new(); - - self.active_thread_item = item.active_thread_item; - - if let Some(console) = item.console.as_ref() { - if let Some(()) = console.encode(&mut buf).log_err() { - self.console.clone_from(&buf); - } - } - - buf.clear(); - if let Some(module_list) = item.module_list.as_ref() { - if let Some(()) = module_list.encode(&mut buf).log_err() { - self.module_list.clone_from(&buf); - } - } - - buf.clear(); - if let Some(thread_state) = item.thread_state.as_ref() { - if let Some(()) = thread_state.encode(&mut buf).log_err() { - self.thread_state.clone_from(&buf); - } - } - - buf.clear(); - if let Some(variable_list) = item.variable_list.as_ref() { - if let Some(()) = variable_list.encode(&mut buf).log_err() { - self.variable_list.clone_from(&buf); - } - } - - buf.clear(); - if let Some(stack_frame_list) = item.stack_frame_list.as_ref() { - if let Some(()) = stack_frame_list.encode(&mut buf).log_err() { - self.stack_frame_list.clone_from(&buf); - } - } - - buf.clear(); - if let Some(loaded_source_list) = item.loaded_source_list.as_ref() { - if let Some(()) = loaded_source_list.encode(&mut buf).log_err() { - self.loaded_source_list.clone_from(&buf); - } - } - } - - pub fn panel_item(&self) -> SetDebuggerPanelItem { - SetDebuggerPanelItem { - project_id: self.project_id.to_proto(), - session_id: self.session_id as u64, - client_id: self.id as u64, - thread_id: self.thread_id as u64, - session_name: self.seassion_name.clone(), - active_thread_item: self.active_thread_item, - console: proto::DebuggerConsole::decode(&self.console[..]).log_err(), - module_list: proto::DebuggerModuleList::decode(&self.module_list[..]).log_err(), - thread_state: proto::DebuggerThreadState::decode(&self.thread_state[..]).log_err(), - variable_list: proto::DebuggerVariableList::decode(&self.variable_list[..]).log_err(), - stack_frame_list: proto::DebuggerStackFrameList::decode(&self.stack_frame_list[..]) - .log_err(), - loaded_source_list: proto::DebuggerLoadedSourceList::decode( - &self.loaded_source_list[..], - ) - .log_err(), - } - } - - pub fn update_panel_item(&mut self, update: &proto::UpdateDebugAdapter) -> Result<()> { - match update - .variant - .as_ref() - .ok_or(anyhow!("All update debug adapter RPCs must have a variant"))? - { - proto::update_debug_adapter::Variant::ThreadState(thread_state) => { - let encoded = thread_state.encode_to_vec(); - self.thread_state = encoded; - } - proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => { - let encoded = stack_frame_list.encode_to_vec(); - self.stack_frame_list = encoded; - } - proto::update_debug_adapter::Variant::VariableList(variable_list) => { - let encoded = variable_list.encode_to_vec(); - self.variable_list = encoded; - } - proto::update_debug_adapter::Variant::AddToVariableList(added_variables) => { - let mut variable_list = proto::DebuggerVariableList::decode( - &self.variable_list[..], - ) - .with_context(|| { - "Failed to decode DebuggerVariableList during AddToVariableList variant update" - })?; - - variable_list.added_variables.push(added_variables.clone()); - self.variable_list = variable_list.encode_to_vec(); - } - proto::update_debug_adapter::Variant::Modules(module_list) => { - let encoded = module_list.encode_to_vec(); - self.module_list = encoded; - } - proto::update_debug_adapter::Variant::OutputEvent(_) => {} - } - - Ok(()) - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::project::Entity", - from = "Column::ProjectId", - to = "super::project::Column::Id" - )] - Project, - #[sea_orm( - belongs_to = "super::debug_clients::Entity", - from = "(Column::Id, Column::ProjectId, Column::SessionId)", - to = "(super::debug_clients::Column::Id, super::debug_clients::Column::ProjectId, super::debug_clients::Column::SessionId)" - )] - DebugClient, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Project.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::DebugClient.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/project.rs b/crates/collab/src/db/tables/project.rs index b87dd70a1dbb6a..09d20bce625435 100644 --- a/crates/collab/src/db/tables/project.rs +++ b/crates/collab/src/db/tables/project.rs @@ -51,10 +51,6 @@ pub enum Relation { LanguageServers, #[sea_orm(has_many = "super::breakpoints::Entity")] Breakpoints, - #[sea_orm(has_many = "super::debug_clients::Entity")] - DebugClients, - #[sea_orm(has_many = "super::debug_panel_items::Entity")] - DebugPanelItems, } impl Related for Entity { @@ -93,16 +89,4 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::DebugClients.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::DebugPanelItems.def() - } -} - impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index adcef6a23203c8..cfe6649f9523b7 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -425,10 +425,12 @@ impl Server { .add_message_handler( broadcast_project_message_from_host::, ) - .add_message_handler(set_debug_client_panel_item) - .add_message_handler(update_debug_adapter) - .add_message_handler(update_debug_client_capabilities) - .add_message_handler(shutdown_debug_client) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler( + broadcast_project_message_from_host::, + ) + .add_message_handler(broadcast_project_message_from_host::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) @@ -450,9 +452,12 @@ impl Server { .add_message_handler( broadcast_project_message_from_host::, ) - .add_message_handler(ignore_breakpoint_state) .add_message_handler( - broadcast_project_message_from_host::, + broadcast_project_message_from_host::, + ) + .add_message_handler(broadcast_project_message_from_host::) + .add_request_handler( + forward_mutating_project_request::, ); Arc::new(server) @@ -1914,7 +1919,6 @@ fn join_project_internal( language_servers: project.language_servers.clone(), role: project.role.into(), breakpoints, - debug_sessions: project.debug_sessions.clone(), // Todo(Debugger) Figure out how to avoid cloning })?; for (worktree_id, worktree) in mem::take(&mut project.worktrees) { @@ -2147,117 +2151,6 @@ async fn update_language_server( Ok(()) } -/// Notify other participants that a debug client has shutdown -async fn shutdown_debug_client( - request: proto::ShutdownDebugClient, - session: Session, -) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .shutdown_debug_client(session.connection_id, &request) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn ignore_breakpoint_state( - request: proto::IgnoreBreakpointState, - session: Session, -) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .ignore_breakpoint_state(session.connection_id, &request) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -/// Notify other participants that a debug panel item has been updated -async fn update_debug_adapter(request: proto::UpdateDebugAdapter, session: Session) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .update_debug_adapter(session.connection_id, &request) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -/// Notify other participants that there's a new debug panel item -async fn set_debug_client_panel_item( - request: proto::SetDebuggerPanelItem, - session: Session, -) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .set_debug_client_panel_item(session.connection_id, &request) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -/// Notify other participants that a debug client's capabilities has been created or updated -async fn update_debug_client_capabilities( - request: proto::SetDebugClientCapabilities, - session: Session, -) -> Result<()> { - let guest_connection_ids = session - .db() - .await - .update_debug_client_capabilities(session.connection_id, &request) - .await?; - - broadcast( - Some(session.connection_id), - guest_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - /// Notify other participants that breakpoints have changed. async fn update_breakpoints( request: proto::SynchronizeBreakpoints, diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index cd1b30d3616676..9379442b99b9cb 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -2496,7 +2496,7 @@ async fn test_debug_panel_console(host_cx: &mut TestAppContext, remote_cx: &mut console.editor().update(cx, |editor, cx| { pretty_assertions::assert_eq!( " - First line + ()) } + pub fn debug_panel_items_by_client( + &self, + client_id: &DebugAdapterClientId, + cx: &Context, + ) -> Vec> { + self.pane + .read(cx) + .items() + .filter_map(|item| item.downcast::()) + .filter(|item| &item.read(cx).client_id() == client_id) + .map(|item| item.clone()) + .collect() + } + pub fn debug_panel_item_by_client( &self, client_id: &DebugAdapterClientId, @@ -945,6 +958,9 @@ impl DebugPanel { project::dap_store::DapStoreEvent::UpdateThreadStatus(thread_status_update) => { self.handle_thread_status_update(thread_status_update, cx); } + project::dap_store::DapStoreEvent::RemoteHasInitialized => { + self.handle_remote_has_initialized(window, cx) + } _ => {} } } @@ -1014,6 +1030,18 @@ impl DebugPanel { } } + fn handle_remote_has_initialized(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(mut dap_event_queue) = self + .dap_store + .clone() + .update(cx, |this, _| this.remote_event_queue()) + { + while let Some(dap_event) = dap_event_queue.pop_front() { + self.on_dap_store_event(&self.dap_store.clone(), &dap_event, window, cx); + } + } + } + pub(crate) fn handle_set_debug_panel_item( &mut self, payload: &SetDebuggerPanelItem, diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index a5dd323b6499c4..616cd971983b23 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -27,7 +27,6 @@ pub fn init_test(cx: &mut gpui::TestAppContext) { language::init(cx); workspace::init_settings(cx); Project::init_settings(cx); - crate::init(cx); editor::init(cx); }); } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 60a825b5f44c78..2a6eeb51a9ce21 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -78,6 +78,7 @@ pub enum DapStoreEvent { source_changed: bool, }, ActiveDebugLineChanged, + RemoteHasInitialized, SetDebugPanelItem(SetDebuggerPanelItem), UpdateDebugAdapter(UpdateDebugAdapter), UpdateThreadStatus(UpdateThreadStatus), @@ -1734,6 +1735,27 @@ impl DapStore { }) } + pub fn request_active_debug_sessions(&mut self, cx: &mut Context) { + if let Some((client, project_id)) = self.upstream_client() { + cx.spawn(|this, mut cx| async move { + let response = dbg!( + client + .request(proto::ActiveDebugSessionsRequest { project_id }) + .await + ) + .log_err(); + + if let Some(response) = response { + this.update(&mut cx, |dap_store, cx| { + dap_store.set_debug_sessions_from_proto(response.sessions, cx) + }) + .log_err(); + } + }) + .detach(); + } + } + pub fn set_debug_sessions_from_proto( &mut self, debug_sessions: Vec, @@ -1752,6 +1774,7 @@ impl DapStore { queue.push_back(DapStoreEvent::SetDebugPanelItem(item)); }); } + cx.emit(DapStoreEvent::RemoteHasInitialized); } let client = DebugAdapterClientId::from_proto(debug_client.client_id); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fdec049750d61d..aa7234d661c241 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1055,7 +1055,7 @@ impl Project { let mut dap_store = DapStore::new_remote(remote_id, client.clone().into()); dap_store.set_breakpoints_from_proto(response.payload.breakpoints, cx); - dap_store.set_debug_sessions_from_proto(response.payload.debug_sessions, cx); + dap_store.request_active_debug_sessions(cx); dap_store })?; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 72c8997b6cb3fb..60809f57dde1c0 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -347,7 +347,9 @@ message Envelope { DapModulesRequest dap_modules_request = 325; DapModulesResponse dap_modules_response = 326; DapLoadedSourcesRequest dap_loaded_sources_request = 327; - DapLoadedSourcesResponse dap_loaded_sources_response = 328; // current max + DapLoadedSourcesResponse dap_loaded_sources_response = 328; + ActiveDebugSessionsRequest active_debug_sessions_request = 329; + ActiveDebugSessionsResponse active_debug_sessions_response = 330; // current max } reserved 87 to 88; @@ -605,7 +607,6 @@ message JoinProjectResponse { ChannelRole role = 6; reserved 7; repeated SynchronizeBreakpoints breakpoints = 8; - repeated DebuggerSession debug_sessions = 9; } message LeaveProject { @@ -2533,18 +2534,24 @@ message DebuggerSessionEnded { uint64 session_id = 2; } +message ActiveDebugSessionsRequest { + uint64 project_id = 1; +} + +message ActiveDebugSessionsResponse { + repeated DebuggerSession sessions = 1; +} + message DebuggerSession { uint64 session_id = 1; - uint64 project_id = 2; - bool ignore_breakpoints = 3; - repeated DebugClient clients = 4; + bool ignore_breakpoints = 2; + repeated DebugClient clients = 3; } message DebugClient { uint64 client_id = 1; SetDebugClientCapabilities capabilities = 2; - SetActiveDebugLine active_debug_line = 3; - repeated SetDebuggerPanelItem debug_panel_items = 4; + repeated SetDebuggerPanelItem debug_panel_items = 3; } message ShutdownDebugClient { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 6f55f5c566561d..bb1d94a07c2bf0 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -412,6 +412,8 @@ messages!( (IgnoreBreakpointState, Background), (ToggleIgnoreBreakpoints, Background), (DebuggerSessionEnded, Background), + (ActiveDebugSessionsRequest, Foreground), + (ActiveDebugSessionsResponse, Foreground), ); request_messages!( @@ -559,7 +561,8 @@ request_messages!( (DapRestartRequest, Ack), (DapRestartStackFrameRequest, Ack), (DapShutdownSession, Ack), - (VariablesRequest, DapVariables) + (VariablesRequest, DapVariables), + (ActiveDebugSessionsRequest, ActiveDebugSessionsResponse) ); entity_messages!( @@ -676,6 +679,7 @@ entity_messages!( IgnoreBreakpointState, ToggleIgnoreBreakpoints, DebuggerSessionEnded, + ActiveDebugSessionsRequest, ); entity_messages!( From 05ca096a5bbeff640ad0fecb47e6eacff4db5169 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 8 Feb 2025 13:44:45 +0100 Subject: [PATCH 531/650] Use observe for module list instead of clearing only when the length is different This should fix issues when we update a module it's content. That wouldn't show up anymore. So by always resetting the list we should get the most up-to-date value. NOTE: We probably want to reduce the amount of resets and notifies every time we update the client state, even though we didn't update the modules. We could send and specific event from the client state, each time we update a module. So instead of using a observer we could use a normal event subscriber and only update the list based on that event --- crates/debugger_ui/src/debugger_panel_item.rs | 10 ---- crates/debugger_ui/src/module_list.rs | 59 ++++++++----------- crates/project/src/dap_session.rs | 3 +- 3 files changed, 27 insertions(+), 45 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 27c151948a632e..c123303a09eb4c 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -33,7 +33,6 @@ pub enum DebugPanelItemEvent { } #[derive(Clone, PartialEq, Eq)] -#[cfg(any(test, feature = "test-support"))] pub enum ThreadItem { Console, LoadedSource, @@ -41,15 +40,6 @@ pub enum ThreadItem { Variables, } -#[derive(Clone, PartialEq, Eq)] -#[cfg(not(any(test, feature = "test-support")))] -enum ThreadItem { - Console, - LoadedSource, - Modules, - Variables, -} - impl ThreadItem { fn to_proto(&self) -> proto::DebuggerThreadItem { match self { diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 07f73a9144b314..c0ff1c1385b717 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,14 +1,14 @@ use dap::{client::DebugAdapterClientId, ModuleEvent}; -use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState}; +use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; use project::dap_session::DebugSession; use ui::prelude::*; pub struct ModuleList { list: ListState, focus_handle: FocusHandle, + _subscription: Subscription, session: Entity, client_id: DebugAdapterClientId, - modules_len: usize, } impl ModuleList { @@ -32,45 +32,40 @@ impl ModuleList { }, ); - session.update(cx, |session, _cx| { - session.client_state(*client_id).unwrap(); + let client_state = session.read(cx).client_state(*client_id).unwrap(); + + let _subscription = cx.observe(&client_state, |module_list, state, cx| { + let modules_len = state.update(cx, |state, cx| state.modules(cx).len()); + + module_list.list.reset(modules_len); + cx.notify(); }); Self { list, session, focus_handle, + _subscription, client_id: *client_id, - modules_len: 0, } } pub fn on_module_event(&mut self, event: &ModuleEvent, cx: &mut Context) { if let Some(state) = self.session.read(cx).client_state(self.client_id) { - let modules_len = state.update(cx, |state, cx| { - state.handle_module_event(event); - state.modules(cx).len() - }); - - if modules_len != self.modules_len { - self.modules_len = modules_len; - self.list.reset(self.modules_len); - } - - cx.notify() + state.update(cx, |state, cx| state.handle_module_event(event, cx)); } } fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { - let Some((module_name, module_path)) = self.session.update(cx, |session, cx| { - session - .client_state(self.client_id)? - .update(cx, |state, cx| { - state - .modules(cx) - .get(ix) - .map(|module| (module.name.clone(), module.path.clone())) - }) + let Some((module_name, module_path)) = maybe!({ + let client_state = self.session.read(cx).client_state(self.client_id)?; + + client_state.update(cx, |state, cx| { + state + .modules(cx) + .get(ix) + .map(|module| (module.name.clone(), module.path.clone())) + }) }) else { return Empty.into_any(); }; @@ -100,19 +95,14 @@ impl Focusable for ModuleList { impl Render for ModuleList { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let session = self.session.read(cx); - let modules_len = session - .client_state(self.client_id) - .map_or(0usize, |state| { - state.update(cx, |state, cx| state.modules(cx).len()) + if let Some(state) = self.session.read(cx).client_state(self.client_id) { + state.update(cx, |state, cx| { + state.modules(cx); }); - - if modules_len != self.modules_len { - self.modules_len = modules_len; - self.list.reset(self.modules_len); } div() + .track_focus(&self.focus_handle) .size_full() .p_1() .child(list(self.list.clone()).size_full()) @@ -121,6 +111,7 @@ impl Render for ModuleList { #[cfg(any(test, feature = "test-support"))] use dap::Module; +use util::maybe; #[cfg(any(test, feature = "test-support"))] impl ModuleList { diff --git a/crates/project/src/dap_session.rs b/crates/project/src/dap_session.rs index 18339bed99e5d5..690a33a02ceb17 100644 --- a/crates/project/src/dap_session.rs +++ b/crates/project/src/dap_session.rs @@ -178,7 +178,7 @@ impl DebugAdapterClientState { &self.modules } - pub fn handle_module_event(&mut self, event: &dap::ModuleEvent) { + pub fn handle_module_event(&mut self, event: &dap::ModuleEvent, cx: &mut Context) { match event.reason { dap::ModuleEventReason::New => self.modules.push(event.module.clone()), dap::ModuleEventReason::Changed => { @@ -188,6 +188,7 @@ impl DebugAdapterClientState { } dap::ModuleEventReason::Removed => self.modules.retain(|m| m.id != event.module.id), } + cx.notify(); } pub fn loaded_sources(&mut self, cx: &mut Context) -> &[Source] { From dad39abeebce578729ffece5549a09ef06d1c9de Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 8 Feb 2025 14:25:12 +0100 Subject: [PATCH 532/650] Move module list to new structure --- crates/debugger_ui/src/debugger_panel_item.rs | 9 +- crates/debugger_ui/src/loaded_source_list.rs | 113 +++++------------- crates/debugger_ui/src/module_list.rs | 18 ++- crates/project/src/dap_session.rs | 42 ++++++- 4 files changed, 79 insertions(+), 103 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index c123303a09eb4c..8eaf43f9ef0fb2 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -123,7 +123,7 @@ impl DebugPanelItem { let module_list = cx.new(|cx| ModuleList::new(session.clone(), &client_id, cx)); let loaded_source_list = - cx.new(|cx| LoadedSourceList::new(&this, dap_store.clone(), &client_id, cx)); + cx.new(|cx| LoadedSourceList::new(session.clone(), &client_id, cx)); let console = cx.new(|cx| { Console::new( @@ -374,10 +374,9 @@ impl DebugPanelItem { return; } - self.loaded_source_list - .update(cx, |loaded_source_list, cx| { - loaded_source_list.on_loaded_source_event(event, cx); - }); + if let Some(state) = self.session.read(cx).client_state(self.client_id) { + state.update(cx, |state, cx| state.handle_loaded_source_event(event, cx)); + } } fn handle_client_shutdown_event( diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs index 90726b3615c1ca..7a0366030b30d8 100644 --- a/crates/debugger_ui/src/loaded_source_list.rs +++ b/crates/debugger_ui/src/loaded_source_list.rs @@ -1,24 +1,20 @@ -use anyhow::Result; -use dap::{client::DebugAdapterClientId, LoadedSourceEvent, Source}; -use gpui::{list, AnyElement, Entity, FocusHandle, Focusable, ListState, Subscription, Task}; -use project::dap_store::DapStore; +use dap::client::DebugAdapterClientId; +use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; +use project::dap_session::DebugSession; use ui::prelude::*; - -use crate::debugger_panel_item::{self, DebugPanelItem, DebugPanelItemEvent}; +use util::maybe; pub struct LoadedSourceList { list: ListState, - sources: Vec, focus_handle: FocusHandle, - dap_store: Entity, + _subscription: Subscription, + session: Entity, client_id: DebugAdapterClientId, - _subscriptions: Vec, } impl LoadedSourceList { pub fn new( - debug_panel_item: &Entity, - dap_store: Entity, + session: Entity, client_id: &DebugAdapterClientId, cx: &mut Context, ) -> Self { @@ -39,84 +35,32 @@ impl LoadedSourceList { }, ); - let _subscriptions = - vec![cx.subscribe(debug_panel_item, Self::handle_debug_panel_item_event)]; + let client_state = session.read(cx).client_state(*client_id).unwrap(); + let _subscription = cx.observe(&client_state, |loaded_source_list, state, cx| { + let len = state.update(cx, |state, cx| state.loaded_sources(cx).len()); + + loaded_source_list.list.reset(len); + cx.notify(); + }); Self { list, - dap_store, + session, focus_handle, - _subscriptions, + _subscription, client_id: *client_id, - sources: Vec::default(), - } - } - - fn handle_debug_panel_item_event( - &mut self, - _: Entity, - event: &debugger_panel_item::DebugPanelItemEvent, - cx: &mut Context, - ) { - match event { - DebugPanelItemEvent::Stopped { .. } => { - self.fetch_loaded_sources(cx).detach_and_log_err(cx); - } - _ => {} - } - } - - pub fn on_loaded_source_event(&mut self, event: &LoadedSourceEvent, cx: &mut Context) { - match event.reason { - dap::LoadedSourceEventReason::New => self.sources.push(event.source.clone()), - dap::LoadedSourceEventReason::Changed => { - let updated_source = - if let Some(ref_id) = event.source.source_reference.filter(|&r| r != 0) { - self.sources - .iter_mut() - .find(|s| s.source_reference == Some(ref_id)) - } else if let Some(path) = &event.source.path { - self.sources - .iter_mut() - .find(|s| s.path.as_ref() == Some(path)) - } else { - self.sources - .iter_mut() - .find(|s| s.name == event.source.name) - }; - - if let Some(loaded_source) = updated_source { - *loaded_source = event.source.clone(); - } - } - dap::LoadedSourceEventReason::Removed => { - self.sources.retain(|source| *source != event.source) - } } - - self.list.reset(self.sources.len()); - cx.notify(); - } - - fn fetch_loaded_sources(&self, cx: &mut Context) -> Task> { - let task = self - .dap_store - .update(cx, |store, cx| store.loaded_sources(&self.client_id, cx)); - - cx.spawn(|this, mut cx| async move { - let mut sources = task.await?; - - this.update(&mut cx, |this, cx| { - std::mem::swap(&mut this.sources, &mut sources); - this.list.reset(this.sources.len()); - - cx.notify(); - }) - }) } fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { - let source = &self.sources[ix]; + let Some(source) = maybe!({ + self.session + .read(cx) + .client_state(self.client_id)? + .update(cx, |state, cx| state.loaded_sources(cx).get(ix).cloned()) + }) else { + return Empty.into_any(); + }; v_flex() .rounded_md() @@ -147,8 +91,15 @@ impl Focusable for LoadedSourceList { } impl Render for LoadedSourceList { - fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + if let Some(state) = self.session.read(cx).client_state(self.client_id) { + state.update(cx, |state, cx| { + state.loaded_sources(cx); + }); + } + div() + .track_focus(&self.focus_handle) .size_full() .p_1() .child(list(self.list.clone()).size_full()) diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index c0ff1c1385b717..63fd6c23e9b633 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -57,15 +57,11 @@ impl ModuleList { } fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { - let Some((module_name, module_path)) = maybe!({ - let client_state = self.session.read(cx).client_state(self.client_id)?; - - client_state.update(cx, |state, cx| { - state - .modules(cx) - .get(ix) - .map(|module| (module.name.clone(), module.path.clone())) - }) + let Some(module) = maybe!({ + self.session + .read(cx) + .client_state(self.client_id)? + .update(cx, |state, cx| state.modules(cx).get(ix).cloned()) }) else { return Empty.into_any(); }; @@ -76,12 +72,12 @@ impl ModuleList { .group("") .p_1() .hover(|s| s.bg(cx.theme().colors().element_hover)) - .child(h_flex().gap_0p5().text_ui_sm(cx).child(module_name)) + .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone())) .child( h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(module_path, |this, path| this.child(path)), + .when_some(module.path.clone(), |this, path| this.child(path)), ) .into_any() } diff --git a/crates/project/src/dap_session.rs b/crates/project/src/dap_session.rs index 690a33a02ceb17..5e55c0c91a81d3 100644 --- a/crates/project/src/dap_session.rs +++ b/crates/project/src/dap_session.rs @@ -142,15 +142,11 @@ impl DebugAdapterClientState { process_result: impl FnOnce(&mut Self, T::Response) + 'static + Send + Sync, cx: &mut Context, ) { - let slot = request.into(); - let entry = self.requests.entry(slot); - - if let Entry::Vacant(vacant) = entry { - let client_id = self.client_id; + if let Entry::Vacant(vacant) = self.requests.entry(request.into()) { let command = vacant.key().0.clone().as_any_arc().downcast::().unwrap(); if let Ok(request) = self.dap_store.update(cx, |dap_store, cx| { - dap_store.request_dap(&client_id, command, cx) + dap_store.request_dap(&self.client_id, command, cx) }) { let task = cx .spawn(|this, mut cx| async move { @@ -201,6 +197,40 @@ impl DebugAdapterClientState { ); &self.loaded_sources } + + pub fn handle_loaded_source_event( + &mut self, + event: &dap::LoadedSourceEvent, + cx: &mut Context, + ) { + match event.reason { + dap::LoadedSourceEventReason::New => self.loaded_sources.push(event.source.clone()), + dap::LoadedSourceEventReason::Changed => { + let updated_source = + if let Some(ref_id) = event.source.source_reference.filter(|&r| r != 0) { + self.loaded_sources + .iter_mut() + .find(|s| s.source_reference == Some(ref_id)) + } else if let Some(path) = &event.source.path { + self.loaded_sources + .iter_mut() + .find(|s| s.path.as_ref() == Some(path)) + } else { + self.loaded_sources + .iter_mut() + .find(|s| s.name == event.source.name) + }; + + if let Some(loaded_source) = updated_source { + *loaded_source = event.source.clone(); + } + } + dap::LoadedSourceEventReason::Removed => { + self.loaded_sources.retain(|source| *source != event.source) + } + } + cx.notify(); + } } pub struct DebugSession { From 062e64d22787837c3a6e3cb363b3e5539ed06fdc Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 10 Feb 2025 02:15:29 +0100 Subject: [PATCH 533/650] cargo fmt --- crates/editor/src/editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a5e91a1cb2805d..ce74f390f09cc8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -82,8 +82,8 @@ use gpui::{ ElementId, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, - Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, TextRun, - UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, + Styled, StyledText, Subscription, Task, TextRun, TextStyle, TextStyleRefinement, + UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; From e216805e66ebb6358b66ccbdb483dc0f3d5ddaf1 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 10 Feb 2025 10:45:46 +0100 Subject: [PATCH 534/650] Add check if request is support inside dap command This fixes also an issue that Piotr was having with step debugging Python. Because we now always send the request event though the adapter didn't support it. This is only for the new request structure --- crates/project/src/dap_command.rs | 70 ++++++++++++++++++++++++++++++- crates/project/src/dap_store.rs | 7 ++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/crates/project/src/dap_command.rs b/crates/project/src/dap_command.rs index 73b5c2e4e739bf..b1d35fb2a48141 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/dap_command.rs @@ -5,8 +5,8 @@ use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, requests::{Continue, Next}, - ContinueArguments, NextArguments, StepInArguments, StepOutArguments, SteppingGranularity, - ValueFormat, Variable, VariablesArgumentsFilter, + Capabilities, ContinueArguments, NextArguments, StepInArguments, StepOutArguments, + SteppingGranularity, ValueFormat, Variable, VariablesArgumentsFilter, }; use gpui::{AsyncApp, WeakEntity}; use rpc::proto; @@ -19,6 +19,8 @@ pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { type DapRequest: 'static + Send + dap::requests::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; + fn is_supported(&self, capabilities: &Capabilities) -> bool; + fn handle_response( &self, _dap_store: WeakEntity, @@ -62,6 +64,10 @@ impl DapCommand for Arc { type DapRequest = T::DapRequest; type ProtoRequest = T::ProtoRequest; + fn is_supported(&self, capabilities: &Capabilities) -> bool { + T::is_supported(self, capabilities) + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { T::client_id_from_proto(request) } @@ -140,6 +146,10 @@ impl DapCommand for NextCommand { type DapRequest = Next; type ProtoRequest = proto::DapNextRequest; + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -204,6 +214,10 @@ impl DapCommand for StepInCommand { type DapRequest = dap::requests::StepIn; type ProtoRequest = proto::DapStepInRequest; + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -276,6 +290,10 @@ impl DapCommand for StepOutCommand { type DapRequest = dap::requests::StepOut; type ProtoRequest = proto::DapStepOutRequest; + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + fn handle_response( &self, dap_store: WeakEntity, @@ -376,6 +394,10 @@ impl DapCommand for StepBackCommand { type DapRequest = dap::requests::StepBack; type ProtoRequest = proto::DapStepBackRequest; + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities.supports_step_back.unwrap_or_default() + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -446,6 +468,10 @@ impl DapCommand for ContinueCommand { type DapRequest = Continue; type ProtoRequest = proto::DapContinueRequest; + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + fn handle_response( &self, dap_store: WeakEntity, @@ -543,6 +569,10 @@ impl DapCommand for PauseCommand { type DapRequest = dap::requests::Pause; type ProtoRequest = proto::DapPauseRequest; + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -605,6 +635,10 @@ impl DapCommand for DisconnectCommand { type DapRequest = dap::requests::Disconnect; type ProtoRequest = proto::DapDisconnectRequest; + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -671,6 +705,12 @@ impl DapCommand for TerminateThreadsCommand { type DapRequest = dap::requests::TerminateThreads; type ProtoRequest = proto::DapTerminateThreadsRequest; + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities + .supports_terminate_threads_request + .unwrap_or_default() + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -735,6 +775,10 @@ impl DapCommand for TerminateCommand { type DapRequest = dap::requests::Terminate; type ProtoRequest = proto::DapTerminateRequest; + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities.supports_terminate_request.unwrap_or_default() + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -795,6 +839,10 @@ impl DapCommand for RestartCommand { type DapRequest = dap::requests::Restart; type ProtoRequest = proto::DapRestartRequest; + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities.supports_restart_request.unwrap_or_default() + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -867,6 +915,10 @@ impl DapCommand for VariablesCommand { type DapRequest = dap::requests::Variables; type ProtoRequest = proto::VariablesRequest; + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + fn handle_response( &self, dap_store: WeakEntity, @@ -984,6 +1036,10 @@ impl DapCommand for RestartStackFrameCommand { type DapRequest = dap::requests::RestartFrame; type ProtoRequest = proto::DapRestartStackFrameRequest; + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities.supports_restart_frame.unwrap_or_default() + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -1042,6 +1098,10 @@ impl DapCommand for ModulesCommand { type DapRequest = dap::requests::Modules; type ProtoRequest = proto::DapModulesRequest; + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities.supports_modules_request.unwrap_or_default() + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -1108,6 +1168,12 @@ impl DapCommand for LoadedSourcesCommand { type DapRequest = dap::requests::LoadedSources; type ProtoRequest = proto::DapLoadedSourcesRequest; + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities + .supports_loaded_sources_request + .unwrap_or_default() + } + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 2a6eeb51a9ce21..d0ebb67fb20a5b 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -1275,6 +1275,13 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; + if !request.is_supported(&self.capabilities_by_id(client_id)) { + return Task::ready(Err(anyhow!( + "Request {} is not supported", + R::DapRequest::COMMAND + ))); + } + let client_id = *client_id; let request = Arc::new(request); From 8b45634d39808b5b29c82e3366ede148e8f7728e Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 10 Feb 2025 11:51:38 +0100 Subject: [PATCH 535/650] Invalidate dap session information on stopped event --- crates/debugger_ui/src/debugger_panel_item.rs | 4 ++++ crates/project/src/dap_session.rs | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 8eaf43f9ef0fb2..229fcd3975d6be 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -298,6 +298,10 @@ impl DebugPanelItem { return; } + if let Some(client_state) = self.session.read(cx).client_state(*client_id) { + client_state.update(cx, |state, cx| state.invalidate(cx)); + } + cx.emit(DebugPanelItemEvent::Stopped { go_to_stack_frame }); if let Some((downstream_client, project_id)) = self.dap_store.read(cx).downstream_client() { diff --git a/crates/project/src/dap_session.rs b/crates/project/src/dap_session.rs index 5e55c0c91a81d3..a9f37366f60714 100644 --- a/crates/project/src/dap_session.rs +++ b/crates/project/src/dap_session.rs @@ -151,8 +151,9 @@ impl DebugAdapterClientState { let task = cx .spawn(|this, mut cx| async move { let result = request.await.log_err()?; - this.update(&mut cx, |this, _| { + this.update(&mut cx, |this, cx| { process_result(this, result); + cx.notify(); }) .log_err() }) @@ -163,6 +164,14 @@ impl DebugAdapterClientState { } } + pub fn invalidate(&mut self, cx: &mut Context) { + self.requests.clear(); + self.modules.clear(); + self.loaded_sources.clear(); + + cx.notify(); + } + pub fn modules(&mut self, cx: &mut Context) -> &[Module] { self.request( dap_command::ModulesCommand, From 9ac18e94048012a69e53e74e3daa0678065463e2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 11 Feb 2025 00:22:39 +0100 Subject: [PATCH 536/650] Add assert for unexpected debug command --- crates/debugger_ui/src/debugger_panel.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c65b117323cb6c..38147f7851cb39 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -187,6 +187,8 @@ impl DebugPanel { window, cx, ); + } else { + debug_assert!("Encountered unexpected command type"); } } _ => unreachable!(), From 9d5525e0fb632cfc4206c8c8b24718aa787acf61 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 11 Feb 2025 00:35:25 +0100 Subject: [PATCH 537/650] Move DAP modules in project into debugger submodule --- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/attach_modal.rs | 2 +- crates/debugger_ui/src/console.rs | 5 ++++- crates/debugger_ui/src/debugger_panel.rs | 18 ++++++++---------- crates/debugger_ui/src/debugger_panel_item.rs | 3 +-- crates/debugger_ui/src/loaded_source_list.rs | 2 +- crates/debugger_ui/src/module_list.rs | 2 +- crates/debugger_ui/src/stack_frame_list.rs | 3 +-- crates/debugger_ui/src/variable_list.rs | 2 +- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/element.rs | 2 +- crates/project/src/buffer_store.rs | 2 +- crates/project/src/debugger.rs | 3 +++ .../project/src/{ => debugger}/dap_command.rs | 6 +++--- .../project/src/{ => debugger}/dap_session.rs | 2 +- crates/project/src/{ => debugger}/dap_store.rs | 5 ++--- crates/project/src/lsp_store.rs | 2 +- crates/project/src/project.rs | 10 +++++----- crates/workspace/src/persistence.rs | 2 +- crates/workspace/src/persistence/model.rs | 2 +- 20 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 crates/project/src/debugger.rs rename crates/project/src/{ => debugger}/dap_command.rs (99%) rename crates/project/src/{ => debugger}/dap_session.rs (99%) rename crates/project/src/{ => debugger}/dap_store.rs (99%) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 0bbd103a353ceb..3e2f70cbbaa683 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -13,7 +13,7 @@ use gpui::{ IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; use project::{ - dap_session::{DebugSession, DebugSessionId}, + debugger::dap_session::{DebugSession, DebugSessionId}, search::SearchQuery, Project, }; diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 4ab394b6c262b0..b3aa65d5a32850 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::Subscription; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; use picker::{Picker, PickerDelegate}; -use project::{dap_session::DebugSessionId, dap_store::DapStore}; +use project::debugger::{dap_session::DebugSessionId, dap_store::DapStore}; use std::sync::Arc; use sysinfo::System; use ui::{prelude::*, Context, Tooltip}; diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 78bcbf203d34de..9693bedc11f50f 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -13,7 +13,10 @@ use fuzzy::StringMatchCandidate; use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16}; use menu::Confirm; -use project::{dap_session::DebugSession, dap_store::DapStore, Completion}; +use project::{ + debugger::{dap_session::DebugSession, dap_store::DapStore}, + Completion, +}; use rpc::proto; use settings::Settings; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 38147f7851cb39..425d36686457f7 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -16,8 +16,8 @@ use gpui::{ Focusable, Subscription, Task, WeakEntity, }; use project::{ - dap_session::DebugSessionId, - dap_store::{DapStore, DapStoreEvent}, + debugger::dap_session::DebugSessionId, + debugger::dap_store::{DapStore, DapStoreEvent}, terminals::TerminalKind, }; use rpc::proto::{self, SetDebuggerPanelItem, UpdateDebugAdapter}; @@ -188,7 +188,7 @@ impl DebugPanel { cx, ); } else { - debug_assert!("Encountered unexpected command type"); + debug_assert!(false, "Encountered unexpected command type"); } } _ => unreachable!(), @@ -946,23 +946,21 @@ impl DebugPanel { fn on_dap_store_event( &mut self, _: &Entity, - event: &project::dap_store::DapStoreEvent, + event: &DapStoreEvent, window: &mut Window, cx: &mut Context, ) { match event { - project::dap_store::DapStoreEvent::SetDebugPanelItem(set_debug_panel_item) => { + DapStoreEvent::SetDebugPanelItem(set_debug_panel_item) => { self.handle_set_debug_panel_item(set_debug_panel_item, window, cx); } - project::dap_store::DapStoreEvent::UpdateDebugAdapter(debug_adapter_update) => { + DapStoreEvent::UpdateDebugAdapter(debug_adapter_update) => { self.handle_debug_adapter_update(debug_adapter_update, window, cx); } - project::dap_store::DapStoreEvent::UpdateThreadStatus(thread_status_update) => { + DapStoreEvent::UpdateThreadStatus(thread_status_update) => { self.handle_thread_status_update(thread_status_update, cx); } - project::dap_store::DapStoreEvent::RemoteHasInitialized => { - self.handle_remote_has_initialized(window, cx) - } + DapStoreEvent::RemoteHasInitialized => self.handle_remote_has_initialized(window, cx), _ => {} } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 229fcd3975d6be..db4deafd13071e 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -15,8 +15,7 @@ use editor::Editor; use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::dap_session::DebugSession; -use project::dap_store::DapStore; +use project::debugger::{dap_session::DebugSession, dap_store::DapStore}; use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem, UpdateDebugAdapter}; use settings::Settings; use ui::{prelude::*, Indicator, Tooltip}; diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs index 7a0366030b30d8..091edc95f693f1 100644 --- a/crates/debugger_ui/src/loaded_source_list.rs +++ b/crates/debugger_ui/src/loaded_source_list.rs @@ -1,6 +1,6 @@ use dap::client::DebugAdapterClientId; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; -use project::dap_session::DebugSession; +use project::debugger::dap_session::DebugSession; use ui::prelude::*; use util::maybe; diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 63fd6c23e9b633..074cccd65f9789 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,6 +1,6 @@ use dap::{client::DebugAdapterClientId, ModuleEvent}; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; -use project::dap_session::DebugSession; +use project::debugger::dap_session::DebugSession; use ui::prelude::*; pub struct ModuleList { diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 524b7ba61fa051..e86ddf2ad76bb8 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -8,8 +8,7 @@ use gpui::{ list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, WeakEntity, }; -use project::dap_session::DebugSession; -use project::dap_store::DapStore; +use project::debugger::{dap_session::DebugSession, dap_store::DapStore}; use project::ProjectPath; use rpc::proto::{DebuggerStackFrameList, UpdateDebugAdapter}; use ui::{prelude::*, Tooltip}; diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index ae6bf3cafb481c..c737458ea749ce 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -10,7 +10,7 @@ use gpui::{ FocusHandle, Focusable, Hsla, ListOffset, ListState, MouseDownEvent, Point, Subscription, Task, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::{dap_session::DebugSession, dap_store::DapStore}; +use project::debugger::{dap_session::DebugSession, dap_store::DapStore}; use rpc::proto::{ self, DebuggerScopeVariableIndex, DebuggerVariableContainer, UpdateDebugAdapter, VariableListScopes, VariableListVariables, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 153ea442f5db52..8deebbfccb5757 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -105,7 +105,7 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange} use linked_editing_ranges::refresh_linked_ranges; use mouse_context_menu::MouseContextMenu; use project::{ - dap_store::{BreakpointEditAction, DapStoreEvent}, + debugger::dap_store::{BreakpointEditAction, DapStoreEvent}, ProjectPath, }; pub use proposed_changes_editor::{ @@ -133,7 +133,7 @@ use multi_buffer::{ }; use parking_lot::Mutex; use project::{ - dap_store::{Breakpoint, BreakpointKind, DapStore}, + debugger::dap_store::{Breakpoint, BreakpointKind, DapStore}, lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, project_settings::{GitGutterSetting, ProjectSettings}, CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6965e97e34dd42..01da29b2cacb31 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -55,7 +55,7 @@ use multi_buffer::{ RowInfo, ToOffset, }; use project::{ - dap_store::{Breakpoint, BreakpointKind}, + debugger::dap_store::{Breakpoint, BreakpointKind}, project_settings::{GitGutterSetting, ProjectSettings}, }; use settings::{KeyBindingValidator, KeyBindingValidatorRegistration, Settings}; diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 8bb4aa8c9b6e42..13ddb0c31b3fa6 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1,5 +1,5 @@ use crate::{ - dap_store::DapStore, + debugger::dap_store::DapStore, lsp_store::OpenLspBufferHandle, search::SearchQuery, worktree_store::{WorktreeStore, WorktreeStoreEvent}, diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs new file mode 100644 index 00000000000000..fc2a0107a270a1 --- /dev/null +++ b/crates/project/src/debugger.rs @@ -0,0 +1,3 @@ +pub mod dap_command; +pub mod dap_session; +pub mod dap_store; diff --git a/crates/project/src/dap_command.rs b/crates/project/src/debugger/dap_command.rs similarity index 99% rename from crates/project/src/dap_command.rs rename to crates/project/src/debugger/dap_command.rs index b1d35fb2a48141..cc1b1430b8b4c3 100644 --- a/crates/project/src/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -12,7 +12,7 @@ use gpui::{AsyncApp, WeakEntity}; use rpc::proto; use util::ResultExt; -use crate::{dap_session::DebugSessionId, dap_store::DapStore}; +use super::{dap_session::DebugSessionId, dap_store::DapStore}; pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; @@ -312,7 +312,7 @@ impl DapCommand for StepOutCommand { status: proto::DebuggerThreadStatus::Running.into(), }; - cx.emit(crate::dap_store::DapStoreEvent::UpdateThreadStatus( + cx.emit(super::dap_store::DapStoreEvent::UpdateThreadStatus( thread_message.clone(), )); @@ -490,7 +490,7 @@ impl DapCommand for ContinueCommand { status: proto::DebuggerThreadStatus::Running.into(), }; - cx.emit(crate::dap_store::DapStoreEvent::UpdateThreadStatus( + cx.emit(super::dap_store::DapStoreEvent::UpdateThreadStatus( thread_message.clone(), )); diff --git a/crates/project/src/dap_session.rs b/crates/project/src/debugger/dap_session.rs similarity index 99% rename from crates/project/src/dap_session.rs rename to crates/project/src/debugger/dap_session.rs index a9f37366f60714..08cb8255c67e9f 100644 --- a/crates/project/src/dap_session.rs +++ b/crates/project/src/debugger/dap_session.rs @@ -11,7 +11,7 @@ use std::{ use task::DebugAdapterConfig; use util::ResultExt; -use crate::{ +use super::{ dap_command::{self, DapCommand}, dap_store::DapStore, }; diff --git a/crates/project/src/dap_store.rs b/crates/project/src/debugger/dap_store.rs similarity index 99% rename from crates/project/src/dap_store.rs rename to crates/project/src/debugger/dap_store.rs index d0ebb67fb20a5b..a162aeec548ba6 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1,13 +1,12 @@ -use crate::{ +use super::{ dap_command::{ ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, VariablesCommand, }, dap_session::{DebugSession, DebugSessionId}, - project_settings::ProjectSettings, - ProjectEnvironment, ProjectItem as _, ProjectPath, }; +use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectItem as _, ProjectPath}; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 4c2d15e13de624..788a83b845460d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1,6 +1,6 @@ use crate::{ buffer_store::{BufferStore, BufferStoreEvent}, - dap_store::DapStore, + debugger::dap_store::DapStore, deserialize_code_actions, environment::ProjectEnvironment, lsp_command::{self, *}, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4d77800c297a63..ce4d96ce411262 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,10 +1,8 @@ pub mod buffer_store; mod color_extractor; pub mod connection_manager; -pub mod dap_command; -pub mod dap_session; -pub mod dap_store; pub mod debounced_delay; +pub mod debugger; pub mod git; pub mod image_store; pub mod lsp_command; @@ -31,7 +29,7 @@ pub mod search_history; mod yarn; use crate::{ - dap_session::{DebugSession, DebugSessionId}, + debugger::dap_session::{DebugSession, DebugSessionId}, git::GitState, }; use anyhow::{anyhow, Context as _, Result}; @@ -49,8 +47,10 @@ use dap::{ }; use collections::{BTreeSet, HashMap, HashSet}; -use dap_store::{Breakpoint, BreakpointEditAction, DapStore, DapStoreEvent, SerializedBreakpoint}; use debounced_delay::DebouncedDelay; +use debugger::dap_store::{ + Breakpoint, BreakpointEditAction, DapStore, DapStoreEvent, SerializedBreakpoint, +}; pub use environment::ProjectEnvironment; use futures::{ channel::mpsc::{self, UnboundedReceiver}, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 1278ddd0982a4b..0fadc801d8a223 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -12,7 +12,7 @@ use client::DevServerProjectId; use collections::HashMap; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId}; -use project::dap_store::{BreakpointKind, SerializedBreakpoint}; +use project::debugger::dap_store::{BreakpointKind, SerializedBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 060868ce9946f8..97005e5532cf19 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -11,7 +11,7 @@ use db::sqlez::{ }; use gpui::{AsyncWindowContext, Entity, WeakEntity}; use itertools::Itertools as _; -use project::dap_store::SerializedBreakpoint; +use project::debugger::dap_store::SerializedBreakpoint; use project::Project; use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; From c8ba6d7c57c3843ed474b0736bc9d78a0c146118 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 11 Feb 2025 02:12:13 +0100 Subject: [PATCH 538/650] Move caps onto the debug client state --- crates/debugger_ui/src/console.rs | 5 +- crates/debugger_ui/src/debugger_panel.rs | 3 +- crates/debugger_ui/src/debugger_panel_item.rs | 73 +++++++---- crates/debugger_ui/src/stack_frame_list.rs | 5 +- crates/debugger_ui/src/variable_list.rs | 15 ++- crates/project/src/debugger/dap_session.rs | 6 +- crates/project/src/debugger/dap_store.rs | 124 ++++++++++++------ 7 files changed, 154 insertions(+), 77 deletions(-) diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 9693bedc11f50f..3af540456786c8 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -385,8 +385,9 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { .read(cx) .dap_store .read(cx) - .capabilities_by_id(&console.read(cx).client_id) - .supports_completions_request + .capabilities_by_id(&console.read(cx).client_id, cx) + .map(|caps| caps.supports_completions_request) + .flatten() .unwrap_or_default(); if support_completions { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 425d36686457f7..fab1f9d27efc77 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -258,7 +258,8 @@ impl DebugPanel { ( true, item.update(cx, |this, cx| this.capabilities(cx)) - .supports_step_back + .map(|caps| caps.supports_step_back) + .flatten() .unwrap_or(false), ) }) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index db4deafd13071e..8625c244bbbbdc 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -431,14 +431,16 @@ impl DebugPanelItem { cx.notify(); if let Some((downstream_client, project_id)) = self.dap_store.read(cx).downstream_client() { - let message = proto_conversions::capabilities_to_proto( - &self.dap_store.read(cx).capabilities_by_id(client_id), - *project_id, - self.session.read(cx).id().to_proto(), - self.client_id.to_proto(), - ); - - downstream_client.send(message).log_err(); + if let Some(caps) = self.dap_store.read(cx).capabilities_by_id(client_id, cx) { + let message = proto_conversions::capabilities_to_proto( + &caps, + *project_id, + self.session.read(cx).id().to_proto(), + self.client_id.to_proto(), + ); + + downstream_client.send(message).log_err(); + } } } @@ -524,8 +526,10 @@ impl DebugPanelItem { self.session.read(cx).ignore_breakpoints() } - pub fn capabilities(&self, cx: &mut Context) -> Capabilities { - self.dap_store.read(cx).capabilities_by_id(&self.client_id) + pub fn capabilities(&self, cx: &mut Context) -> Option { + self.dap_store + .read(cx) + .capabilities_by_id(&self.client_id, cx) } fn clear_highlights(&self, cx: &mut Context) { @@ -903,19 +907,26 @@ impl Render for DebugPanelItem { ) } }) - .when(capabilities.supports_step_back.unwrap_or(false), |this| { - this.child( - IconButton::new("debug-step-back", IconName::DebugStepBack) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.step_back(cx); - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Step back")(window, cx) - }), - ) - }) + .when( + capabilities + .as_ref() + .map(|caps| caps.supports_step_back) + .flatten() + .unwrap_or(false), + |this| { + this.child( + IconButton::new("debug-step-back", IconName::DebugStepBack) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.step_back(cx); + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |window, cx| { + Tooltip::text("Step back")(window, cx) + }), + ) + }, + ) .child( IconButton::new("debug-step-over", IconName::DebugStepOver) .icon_size(IconSize::Small) @@ -956,7 +967,11 @@ impl Render for DebugPanelItem { this.restart_client(cx); })) .disabled( - !capabilities.supports_restart_request.unwrap_or_default(), + !capabilities + .as_ref() + .map(|caps| caps.supports_restart_request) + .flatten() + .unwrap_or_default(), ) .tooltip(move |window, cx| { Tooltip::text("Restart")(window, cx) @@ -1036,7 +1051,11 @@ impl Render for DebugPanelItem { cx, )) .when( - capabilities.supports_modules_request.unwrap_or_default(), + capabilities + .as_ref() + .map(|caps| caps.supports_modules_request) + .flatten() + .unwrap_or_default(), |this| { this.child(self.render_entry_button( &SharedString::from("Modules"), @@ -1047,7 +1066,9 @@ impl Render for DebugPanelItem { ) .when( capabilities - .supports_loaded_sources_request + .as_ref() + .map(|caps| caps.supports_loaded_sources_request) + .flatten() .unwrap_or_default(), |this| { this.child(self.render_entry_button( diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index e86ddf2ad76bb8..37d5e59396e818 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -337,8 +337,9 @@ impl StackFrameList { let supports_frame_restart = self .dap_store .read(cx) - .capabilities_by_id(&self.client_id) - .supports_restart_frame + .capabilities_by_id(&self.client_id, cx) + .map(|caps| caps.supports_restart_frame) + .flatten() .unwrap_or_default(); let origin = stack_frame diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index c737458ea749ce..08b7ba55244653 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -991,12 +991,14 @@ impl VariableList { window: &mut Window, cx: &mut Context, ) { - let support_set_variable = self + let Some(caps) = self .dap_store .read(cx) - .capabilities_by_id(&self.client_id) - .supports_set_variable - .unwrap_or_default(); + .capabilities_by_id(&self.client_id, cx) + else { + return; + }; + let support_set_variable = caps.supports_set_variable.unwrap_or_default(); let this = cx.entity(); @@ -1016,8 +1018,9 @@ impl VariableList { window.handler_for(&this.clone(), move |this, _window, cx| { this.dap_store.update(cx, |dap_store, cx| { if dap_store - .capabilities_by_id(&this.client_id) - .supports_clipboard_context + .capabilities_by_id(&this.client_id, cx) + .map(|caps| caps.supports_clipboard_context) + .flatten() .unwrap_or_default() { let task = dap_store.evaluate( diff --git a/crates/project/src/debugger/dap_session.rs b/crates/project/src/debugger/dap_session.rs index 08cb8255c67e9f..1d671f4800b928 100644 --- a/crates/project/src/debugger/dap_session.rs +++ b/crates/project/src/debugger/dap_session.rs @@ -1,5 +1,5 @@ use collections::{BTreeMap, HashMap}; -use dap::{Module, Source}; +use dap::{Capabilities, Module, Source}; use futures::{future::Shared, FutureExt}; use gpui::{AppContext, Context, Entity, Task, WeakEntity}; use std::{ @@ -68,6 +68,7 @@ struct Thread { pub struct DebugAdapterClientState { dap_store: WeakEntity, + pub(super) capabilities: Capabilities, client_id: DebugAdapterClientId, modules: Vec, loaded_sources: Vec, @@ -245,7 +246,7 @@ impl DebugAdapterClientState { pub struct DebugSession { id: DebugSessionId, mode: DebugSessionMode, - states: HashMap>, + pub(super) states: HashMap>, ignore_breakpoints: bool, } @@ -392,6 +393,7 @@ impl DebugSession { loaded_sources: Vec::default(), _threads: BTreeMap::default(), requests: HashMap::default(), + capabilities: Default::default(), }); self.states.insert(client_id, state); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index a162aeec548ba6..246765e7ed36ab 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -144,7 +144,6 @@ pub struct DapStore { mode: DapStoreMode, downstream_client: Option<(AnyProtoClient, u64)>, breakpoints: BTreeMap>, - capabilities: HashMap, active_debug_line: Option<(DebugAdapterClientId, ProjectPath, u32)>, } @@ -207,7 +206,6 @@ impl DapStore { downstream_client: None, active_debug_line: None, breakpoints: Default::default(), - capabilities: Default::default(), } } @@ -223,7 +221,6 @@ impl DapStore { downstream_client: None, active_debug_line: None, breakpoints: Default::default(), - capabilities: Default::default(), } } @@ -371,11 +368,17 @@ impl DapStore { Some((local_session, client)) } - pub fn capabilities_by_id(&self, client_id: &DebugAdapterClientId) -> Capabilities { - self.capabilities - .get(client_id) - .cloned() - .unwrap_or_default() + pub fn capabilities_by_id( + &self, + client_id: &DebugAdapterClientId, + cx: &App, + ) -> Option { + self.session_by_client_id(client_id).and_then(|session| { + session + .read(cx) + .client_state(*client_id) + .map(|state| state.read(cx).capabilities.clone()) + }) } pub fn update_capabilities_for_client( @@ -385,10 +388,14 @@ impl DapStore { capabilities: &Capabilities, cx: &mut Context, ) { - if let Some(old_capabilities) = self.capabilities.get_mut(client_id) { - *old_capabilities = old_capabilities.merge(capabilities.clone()); - } else { - self.capabilities.insert(*client_id, capabilities.clone()); + if let Some((client, _)) = self.client_by_id(client_id, cx) { + client.update(cx, |this, cx| { + if let Some(state) = this.client_state(*client_id) { + state.update(cx, |this, _| { + this.capabilities = this.capabilities.merge(capabilities.clone()); + }); + } + }); } cx.notify(); @@ -947,8 +954,9 @@ impl DapStore { }; if !self - .capabilities_by_id(client_id) - .supports_modules_request + .capabilities_by_id(client_id, cx) + .map(|caps| caps.supports_modules_request) + .flatten() .unwrap_or_default() { return Task::ready(Ok(Vec::default())); @@ -975,8 +983,9 @@ impl DapStore { }; if !self - .capabilities_by_id(client_id) - .supports_loaded_sources_request + .capabilities_by_id(client_id, cx) + .map(|caps| caps.supports_loaded_sources_request) + .flatten() .unwrap_or_default() { return Task::ready(Ok(Vec::default())); @@ -1020,8 +1029,9 @@ impl DapStore { cx: &mut Context, ) -> Task> { if !self - .capabilities_by_id(client_id) - .supports_restart_frame + .capabilities_by_id(client_id, cx) + .map(|caps| caps.supports_restart_frame) + .flatten() .unwrap_or_default() { return Task::ready(Ok(())); @@ -1060,8 +1070,9 @@ impl DapStore { }; if self - .capabilities_by_id(client_id) - .supports_configuration_done_request + .capabilities_by_id(client_id, cx) + .map(|caps| caps.supports_configuration_done_request) + .flatten() .unwrap_or_default() { cx.background_executor().spawn(async move { @@ -1270,11 +1281,18 @@ impl DapStore { ); } - let Some((_, client)) = self.client_by_id(client_id, cx) else { + let Some((session, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; - if !request.is_supported(&self.capabilities_by_id(client_id)) { + let Some(caps) = session + .read(cx) + .client_state(*client_id) + .map(|state| state.read(cx).capabilities.clone()) + else { + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + }; + if !request.is_supported(&caps) { return Task::ready(Err(anyhow!( "Request {} is not supported", R::DapRequest::COMMAND @@ -1318,7 +1336,9 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut Context, ) -> Task> { - let capabilities = self.capabilities_by_id(client_id); + let Some(capabilities) = self.capabilities_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + }; let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests .unwrap_or_default(); @@ -1344,7 +1364,9 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut Context, ) -> Task> { - let capabilities = self.capabilities_by_id(client_id); + let Some(capabilities) = self.capabilities_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + }; let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests .unwrap_or_default(); @@ -1370,7 +1392,9 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut Context, ) -> Task> { - let capabilities = self.capabilities_by_id(client_id); + let Some(capabilities) = self.capabilities_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + }; let supports_single_thread_execution_requests = capabilities .supports_single_thread_execution_requests .unwrap_or_default(); @@ -1396,7 +1420,9 @@ impl DapStore { granularity: SteppingGranularity, cx: &mut Context, ) -> Task> { - let capabilities = self.capabilities_by_id(client_id); + let Some(capabilities) = self.capabilities_by_id(client_id, cx) else { + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + }; if !capabilities.supports_step_back.unwrap_or_default() { return Task::ready(Ok(())); } @@ -1513,8 +1539,9 @@ impl DapStore { }; let supports_set_expression = self - .capabilities_by_id(client_id) - .supports_set_expression + .capabilities_by_id(client_id, cx) + .map(|caps| caps.supports_set_expression) + .flatten() .unwrap_or_default(); cx.background_executor().spawn(async move { @@ -1559,8 +1586,9 @@ impl DapStore { cx: &mut Context, ) -> Task> { if self - .capabilities_by_id(client_id) - .supports_terminate_threads_request + .capabilities_by_id(client_id, cx) + .map(|caps| caps.supports_terminate_threads_request) + .flatten() .unwrap_or_default() { self.request_dap(client_id, TerminateThreadsCommand { thread_ids }, cx) @@ -1590,8 +1618,9 @@ impl DapStore { cx: &mut Context, ) -> Task> { let supports_restart = self - .capabilities_by_id(client_id) - .supports_restart_request + .capabilities_by_id(client_id, cx) + .map(|caps| caps.supports_restart_request) + .flatten() .unwrap_or_default(); if supports_restart { @@ -1704,10 +1733,19 @@ impl DapStore { let client_id = client.id(); cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); - + let Some(capabilities) = local_store + .session_by_client_id(&client_id) + .and_then(|session| { + session + .read(cx) + .client_state(client_id) + .map(|state| state.read(cx).capabilities.clone()) + }) + else { + return Task::ready(Err(anyhow!("Client not found"))); + }; + let session = session.clone(); local_store.client_by_session.remove(&client_id); - let capabilities = self.capabilities.remove(&client_id).unwrap_or_default(); - if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { downstream_client .send(proto::ShutdownDebugClient { @@ -1718,7 +1756,7 @@ impl DapStore { .log_err(); } - cx.spawn(|_, _| async move { + cx.spawn(|_, mut cx| async move { if capabilities.supports_terminate_request.unwrap_or_default() { let _ = client .request::(TerminateArguments { @@ -1737,7 +1775,13 @@ impl DapStore { .log_err(); } - client.shutdown().await + client.shutdown().await?; + + let _ = session.update(&mut cx, |this, _| { + this.states.remove(&client_id); + }); + + Ok(()) }) } @@ -1977,7 +2021,11 @@ impl DapStore { this.update(&mut cx, |dap_store, cx| { let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); - dap_store.capabilities.remove(&client_id); + dap_store.session_by_client_id(&client_id).map(|state| { + state.update(cx, |this, _| { + this.states.remove(&client_id); + }) + }); cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); cx.notify(); From 041e1eef38bb3dfbb197bbd6229eefceddf76a75 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:44:36 +0100 Subject: [PATCH 539/650] Move sessions mapping to DapStore itself Co-authored-by: hello@anthonyeid.me --- crates/project/src/debugger/dap_store.rs | 163 +++++++---------------- 1 file changed, 46 insertions(+), 117 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 246765e7ed36ab..b44ace8048a811 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -98,8 +98,6 @@ pub struct LocalDapStore { environment: Entity, language_registry: Arc, toolchain_store: Arc, - sessions: HashMap>, - client_by_session: HashMap, } impl LocalDapStore { @@ -110,41 +108,21 @@ impl LocalDapStore { fn next_session_id(&self) -> DebugSessionId { DebugSessionId(self.next_session_id.fetch_add(1, SeqCst)) } - - pub fn session_by_client_id( - &self, - client_id: &DebugAdapterClientId, - ) -> Option> { - self.sessions - .get(self.client_by_session.get(client_id)?) - .cloned() - } } pub struct RemoteDapStore { upstream_client: Option, upstream_project_id: u64, - sessions: HashMap>, - client_by_session: HashMap, event_queue: Option>, } -impl RemoteDapStore { - pub fn session_by_client_id( - &self, - client_id: &DebugAdapterClientId, - ) -> Option> { - self.client_by_session - .get(client_id) - .and_then(|session_id| self.sessions.get(session_id).cloned()) - } -} - pub struct DapStore { mode: DapStoreMode, downstream_client: Option<(AnyProtoClient, u64)>, breakpoints: BTreeMap>, active_debug_line: Option<(DebugAdapterClientId, ProjectPath, u32)>, + sessions: HashMap>, + client_by_session: HashMap, } impl EventEmitter for DapStore {} @@ -198,14 +176,14 @@ impl DapStore { node_runtime, toolchain_store, language_registry, - sessions: HashMap::default(), next_client_id: Default::default(), next_session_id: Default::default(), - client_by_session: Default::default(), }), downstream_client: None, active_debug_line: None, breakpoints: Default::default(), + sessions: Default::default(), + client_by_session: Default::default(), } } @@ -214,13 +192,13 @@ impl DapStore { mode: DapStoreMode::Remote(RemoteDapStore { upstream_client: Some(upstream_client), upstream_project_id: project_id, - sessions: Default::default(), - client_by_session: Default::default(), event_queue: Some(VecDeque::default()), }), downstream_client: None, active_debug_line: None, breakpoints: Default::default(), + sessions: Default::default(), + client_by_session: Default::default(), } } @@ -279,18 +257,14 @@ impl DapStore { ignore: Option, cx: &mut Context, ) { - match &mut self.mode { - DapStoreMode::Remote(remote) => { - remote.sessions.entry(session_id).or_insert(cx.new(|_| { - DebugSession::new_remote( - session_id, - "Remote-Debug".to_owned(), - ignore.unwrap_or(false), - ) - })); - } - _ => {} - } + self.sessions.entry(session_id).or_insert(cx.new(|_| { + DebugSession::new_remote( + session_id, + "Remote-Debug".to_owned(), + ignore.unwrap_or(false), + ) + })); + debug_assert!(matches!(self.mode, DapStoreMode::Remote(_))); } pub fn add_client_to_session( @@ -298,63 +272,32 @@ impl DapStore { session_id: DebugSessionId, client_id: DebugAdapterClientId, ) { - match &mut self.mode { - DapStoreMode::Local(local) => { - if local.sessions.contains_key(&session_id) { - local.client_by_session.insert(client_id, session_id); - } - } - DapStoreMode::Remote(remote) => { - if remote.sessions.contains_key(&session_id) { - remote.client_by_session.insert(client_id, session_id); - } - } - } + self.sessions.entry(session_id).and_modify(|_| { + let existing_value = self.client_by_session.insert(client_id, session_id); + debug_assert!(existing_value.map_or(true, |old| old == session_id)); + }); } - pub fn remove_session(&mut self, session_id: DebugSessionId, cx: &mut Context) { - match &mut self.mode { - DapStoreMode::Local(local) => { - if let Some(session) = local.sessions.remove(&session_id) { - for client_id in session - .read(cx) - .as_local() - .map(|local| local.client_ids()) - .expect("Local Dap can only have local sessions") - { - local.client_by_session.remove(&client_id); - } - } - } - DapStoreMode::Remote(remote) => { - remote.sessions.remove(&session_id); - remote.client_by_session.retain(|_, val| val != &session_id) - } - } + pub fn remove_session(&mut self, session_id: DebugSessionId) { + self.sessions.remove(&session_id); + self.client_by_session.retain(|_, id| *id != session_id); } pub fn sessions(&self) -> impl Iterator> + '_ { - match &self.mode { - DapStoreMode::Local(local) => local.sessions.values().cloned(), - DapStoreMode::Remote(remote) => remote.sessions.values().cloned(), - } + self.sessions.values().cloned() } pub fn session_by_id(&self, session_id: &DebugSessionId) -> Option> { - match &self.mode { - DapStoreMode::Local(local) => local.sessions.get(session_id).cloned(), - DapStoreMode::Remote(remote) => remote.sessions.get(session_id).cloned(), - } + self.sessions.get(session_id).cloned() } pub fn session_by_client_id( &self, client_id: &DebugAdapterClientId, ) -> Option> { - match &self.mode { - DapStoreMode::Local(local) => local.session_by_client_id(client_id), - DapStoreMode::Remote(remote) => remote.session_by_client_id(client_id), - } + self.sessions + .get(self.client_by_session.get(client_id)?) + .cloned() } pub fn client_by_id( @@ -463,8 +406,8 @@ impl DapStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.remove_session(DebugSessionId::from_proto(envelope.payload.session_id), cx); + this.update(&mut cx, |this, _| { + this.remove_session(DebugSessionId::from_proto(envelope.payload.session_id)); })?; Ok(()) @@ -638,18 +581,16 @@ impl DapStore { .await?; dap_store.update(&mut cx, |store, cx| { - store - .as_local_mut() - .unwrap() - .client_by_session - .insert(client_id, session_id); + store.client_by_session.insert(client_id, session_id); let session = store.session_by_id(&session_id).unwrap(); let weak_dap = cx.weak_entity(); session.update(cx, |session, cx| { session.add_client(Some(Arc::new(client)), client_id, weak_dap, cx); - let local_session = session.as_local_mut().unwrap(); + let local_session = session + .as_local_mut() + .expect("Only local sessions should attempt to reconnect"); local_session.update_configuration( |old_config| { @@ -802,9 +743,8 @@ impl DapStore { let client_id = client.id(); - let local_store = store.as_local_mut().unwrap(); - local_store.client_by_session.insert(client_id, session_id); - local_store.sessions.insert(session_id, session.clone()); + store.client_by_session.insert(client_id, session_id); + store.sessions.insert(session_id, session.clone()); cx.emit(DapStoreEvent::DebugClientStarted((session_id, client_id))); cx.notify(); @@ -1641,7 +1581,7 @@ impl DapStore { } pub fn shutdown_sessions(&mut self, cx: &mut Context) -> Task<()> { - let Some(local_store) = self.as_local() else { + let Some(_) = self.as_local() else { if let Some((upstream_client, project_id)) = self.upstream_client() { return cx.background_executor().spawn(async move { upstream_client @@ -1660,7 +1600,7 @@ impl DapStore { let mut tasks = Vec::new(); - for session_id in local_store.sessions.keys().cloned().collect::>() { + for session_id in self.sessions.keys().cloned().collect::>() { tasks.push(self.shutdown_session(&session_id, cx)); } @@ -1674,7 +1614,7 @@ impl DapStore { session_id: &DebugSessionId, cx: &mut Context, ) -> Task> { - let Some(local_store) = self.as_local_mut() else { + let Some(_) = self.as_local_mut() else { if let Some((upstream_client, project_id)) = self.upstream_client() { let future = upstream_client.request(proto::DapShutdownSession { project_id, @@ -1689,7 +1629,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Cannot shutdown session on remote side"))); }; - let Some(session) = local_store.sessions.remove(session_id) else { + let Some(session) = self.sessions.remove(session_id) else { return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); }; @@ -1726,26 +1666,19 @@ impl DapStore { client: Arc, cx: &mut Context, ) -> Task> { - let Some(local_store) = self.as_local_mut() else { - return Task::ready(Err(anyhow!("Cannot shutdown client on remote side"))); - }; - let client_id = client.id(); cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); - let Some(capabilities) = local_store - .session_by_client_id(&client_id) - .and_then(|session| { - session - .read(cx) - .client_state(client_id) - .map(|state| state.read(cx).capabilities.clone()) - }) - else { + let Some(capabilities) = self.session_by_client_id(&client_id).and_then(|session| { + session + .read(cx) + .client_state(client_id) + .map(|state| state.read(cx).capabilities.clone()) + }) else { return Task::ready(Err(anyhow!("Client not found"))); }; let session = session.clone(); - local_store.client_by_session.remove(&client_id); + self.client_by_session.remove(&client_id); if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { downstream_client .send(proto::ShutdownDebugClient { @@ -2171,10 +2104,6 @@ impl DapStore { source_changed: bool, cx: &Context, ) -> Task> { - let Some(local_store) = self.as_local() else { - return Task::ready(Err(anyhow!("cannot start session on remote side"))); - }; - let source_breakpoints = self .breakpoints .get(project_path) @@ -2185,7 +2114,7 @@ impl DapStore { .collect::>(); let mut tasks = Vec::new(); - for session in local_store + for session in self .sessions .values() .filter(|session| session.read(cx).as_local().is_some()) From ecfc0ef12dc0307fcd0601bf8fe006723975dae7 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 13 Feb 2025 22:25:32 +0100 Subject: [PATCH 540/650] Move requests into client (#112) * Move most of the requests into the client state itself. Co-authored-by: Anthony Eid * WIP * Fix some errors and create new errors * More teardown * Fix detach requests * Move set variable value to dap command * Fix dap command error and add evaluate command * FIx more compiler errors * Fix more compiler errors * Clipppyyyy * FIx more * One more * Fix one more * Use threadId from project instead u64 * Mostly fix stack frame list * More compile errors * More * WIP transfer completions to dap command Co-Authored-By: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Co-Authored-By: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> * Finish console completions DapCommand impl * Get Zed to build !! * Fix test compile errors: The debugger tests will still fail * Add threads reqeust to debug session Co-authored-by: Piotr Osiewicz --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Anthony Eid Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Co-authored-by: Piotr Osiewicz --- Cargo.lock | 1 + crates/collab/src/tests/debug_panel_tests.rs | 32 +- crates/dap/src/client.rs | 5 +- crates/dap/src/proto_conversions.rs | 152 +++- crates/debugger_tools/src/dap_log.rs | 15 +- crates/debugger_ui/src/attach_modal.rs | 12 +- crates/debugger_ui/src/console.rs | 151 ++-- crates/debugger_ui/src/debugger_panel.rs | 195 ++-- crates/debugger_ui/src/debugger_panel_item.rs | 390 ++++---- crates/debugger_ui/src/loaded_source_list.rs | 6 +- crates/debugger_ui/src/module_list.rs | 6 +- crates/debugger_ui/src/stack_frame_list.rs | 251 +++-- .../debugger_ui/src/tests/debugger_panel.rs | 19 +- .../debugger_ui/src/tests/stack_frame_list.rs | 51 +- crates/debugger_ui/src/tests/variable_list.rs | 80 +- crates/debugger_ui/src/variable_list.rs | 343 ++----- crates/editor/src/editor_tests.rs | 2 +- crates/project/Cargo.toml | 1 + crates/project/src/debugger/dap_command.rs | 644 ++++++++++--- crates/project/src/debugger/dap_session.rs | 855 +++++++++++++++--- crates/project/src/debugger/dap_store.rs | 760 ++++------------ crates/project/src/project.rs | 6 +- crates/proto/proto/zed.proto | 167 +++- crates/proto/src/proto.rs | 26 +- crates/remote_server/src/headless_project.rs | 2 +- crates/zed/src/zed.rs | 2 +- 26 files changed, 2397 insertions(+), 1777 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94d8414491aa3b..e4d62324dbb3e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10228,6 +10228,7 @@ dependencies = [ "gpui", "http_client", "image", + "indexmap", "itertools 0.14.0", "language", "log", diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 9379442b99b9cb..2da39f55e73070 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -269,7 +269,7 @@ async fn test_debug_panel_item_opens_on_remote( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); }); let shutdown_client = host_project.update(host_cx, |project, cx| { @@ -368,7 +368,7 @@ async fn test_active_debug_panel_item_set_on_join_project( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); }); let shutdown_client = host_project.update(host_cx, |project, cx| { @@ -481,7 +481,7 @@ async fn test_debug_panel_remote_button_presses( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); active_debug_panel_item }); @@ -496,7 +496,7 @@ async fn test_debug_panel_remote_button_presses( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); active_debug_panel_item }); @@ -1251,7 +1251,9 @@ async fn test_module_list( debug_panel_item.update(cx, |item, cx| { assert_eq!( true, - item.capabilities(cx).supports_modules_request.unwrap(), + item.capabilities(cx) + .and_then(|caps| caps.supports_modules_request) + .unwrap(), "Local supports modules request should be true" ); @@ -1279,7 +1281,9 @@ async fn test_module_list( debug_panel_item.update(cx, |item, cx| { assert_eq!( true, - item.capabilities(cx).supports_modules_request.unwrap(), + item.capabilities(cx) + .and_then(|caps| caps.supports_modules_request) + .unwrap(), "Remote capabilities supports modules request should be true" ); let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); @@ -1310,7 +1314,9 @@ async fn test_module_list( debug_panel_item.update(cx, |item, cx| { assert_eq!( true, - item.capabilities(cx).supports_modules_request.unwrap(), + item.capabilities(cx) + .and_then(|caps| caps.supports_modules_request) + .unwrap(), "Remote (mid session join) capabilities supports modules request should be true" ); let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); @@ -1950,9 +1956,7 @@ async fn test_ignore_breakpoints( let session_id = debug_panel.update(cx, |this, cx| { this.dap_store() .read(cx) - .as_remote() - .unwrap() - .session_by_client_id(&client.id()) + .session_by_client_id(client.id()) .unwrap() .read(cx) .id() @@ -1966,7 +1970,7 @@ async fn test_ignore_breakpoints( ); assert_eq!(false, breakpoints_ignored); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); active_debug_panel_item }); @@ -2004,7 +2008,7 @@ async fn test_ignore_breakpoints( active_debug_panel_item.read(cx).are_breakpoints_ignored(cx) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); active_debug_panel_item }); @@ -2064,7 +2068,7 @@ async fn test_ignore_breakpoints( assert_eq!(true, breakpoints_ignored); assert_eq!(client.id(), debug_panel.client_id()); - assert_eq!(1, debug_panel.thread_id()); + assert_eq!(1, debug_panel.thread_id().0); }); client @@ -2135,7 +2139,7 @@ async fn test_ignore_breakpoints( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); active_debug_panel_item }); diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 2d78a453b7d228..9499a83c201928 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -27,11 +27,11 @@ const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] -pub struct DebugAdapterClientId(pub usize); +pub struct DebugAdapterClientId(pub u32); impl DebugAdapterClientId { pub fn from_proto(client_id: u64) -> Self { - Self(client_id as usize) + Self(client_id as u32) } pub fn to_proto(&self) -> u64 { @@ -39,6 +39,7 @@ impl DebugAdapterClientId { } } +/// Represents a connection to the debug adapter process, either via stdout/stdin or a socket. pub struct DebugAdapterClient { id: DebugAdapterClientId, sequence_count: AtomicU64, diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index 440cc182d2d48c..153247dfc98bda 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -1,7 +1,8 @@ use anyhow::{anyhow, Result}; use client::proto::{ - self, DapChecksum, DapChecksumAlgorithm, DapModule, DapScope, DapScopePresentationHint, - DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable, SetDebugClientCapabilities, + self, DapChecksum, DapChecksumAlgorithm, DapEvaluateContext, DapModule, DapScope, + DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable, + SetDebugClientCapabilities, }; use dap_types::{ Capabilities, OutputEventCategory, OutputEventGroup, ScopePresentationHint, Source, @@ -490,3 +491,150 @@ impl ProtoConversion for dap_types::OutputEventGroup { } } } + +impl ProtoConversion for dap_types::CompletionItem { + type ProtoType = proto::DapCompletionItem; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + proto::DapCompletionItem { + label: self.label.clone(), + text: self.text.clone(), + detail: self.detail.clone(), + typ: self + .type_ + .as_ref() + .map(ProtoConversion::to_proto) + .map(|typ| typ.into()), + start: self.start, + length: self.length, + selection_start: self.selection_start, + selection_length: self.selection_length, + sort_text: self.sort_text.clone(), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + let typ = payload.typ(); // todo(debugger): This might be a potential issue/bug because it defaults to a type when it's None + + Self { + label: payload.label, + detail: payload.detail, + sort_text: payload.sort_text, + text: payload.text.clone(), + type_: Some(dap_types::CompletionItemType::from_proto(typ)), + start: payload.start, + length: payload.length, + selection_start: payload.selection_start, + selection_length: payload.selection_length, + } + } +} + +impl ProtoConversion for dap_types::EvaluateArgumentsContext { + type ProtoType = DapEvaluateContext; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + match self { + dap_types::EvaluateArgumentsContext::Variables => { + proto::DapEvaluateContext::EvaluateVariables + } + dap_types::EvaluateArgumentsContext::Watch => proto::DapEvaluateContext::Watch, + dap_types::EvaluateArgumentsContext::Hover => proto::DapEvaluateContext::Hover, + dap_types::EvaluateArgumentsContext::Repl => proto::DapEvaluateContext::Repl, + dap_types::EvaluateArgumentsContext::Clipboard => proto::DapEvaluateContext::Clipboard, + dap_types::EvaluateArgumentsContext::Unknown => { + proto::DapEvaluateContext::EvaluateUnknown + } + _ => proto::DapEvaluateContext::EvaluateUnknown, + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + match payload { + proto::DapEvaluateContext::EvaluateVariables => { + dap_types::EvaluateArgumentsContext::Variables + } + proto::DapEvaluateContext::Watch => dap_types::EvaluateArgumentsContext::Watch, + proto::DapEvaluateContext::Hover => dap_types::EvaluateArgumentsContext::Hover, + proto::DapEvaluateContext::Repl => dap_types::EvaluateArgumentsContext::Repl, + proto::DapEvaluateContext::Clipboard => dap_types::EvaluateArgumentsContext::Clipboard, + proto::DapEvaluateContext::EvaluateUnknown => { + dap_types::EvaluateArgumentsContext::Unknown + } + } + } +} + +impl ProtoConversion for dap_types::CompletionItemType { + type ProtoType = proto::DapCompletionItemType; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + match self { + dap_types::CompletionItemType::Class => proto::DapCompletionItemType::Class, + dap_types::CompletionItemType::Color => proto::DapCompletionItemType::Color, + dap_types::CompletionItemType::Constructor => proto::DapCompletionItemType::Constructor, + dap_types::CompletionItemType::Customcolor => proto::DapCompletionItemType::Customcolor, + dap_types::CompletionItemType::Enum => proto::DapCompletionItemType::Enum, + dap_types::CompletionItemType::Field => proto::DapCompletionItemType::Field, + dap_types::CompletionItemType::File => proto::DapCompletionItemType::CompletionItemFile, + dap_types::CompletionItemType::Function => proto::DapCompletionItemType::Function, + dap_types::CompletionItemType::Interface => proto::DapCompletionItemType::Interface, + dap_types::CompletionItemType::Keyword => proto::DapCompletionItemType::Keyword, + dap_types::CompletionItemType::Method => proto::DapCompletionItemType::Method, + dap_types::CompletionItemType::Module => proto::DapCompletionItemType::Module, + dap_types::CompletionItemType::Property => proto::DapCompletionItemType::Property, + dap_types::CompletionItemType::Reference => proto::DapCompletionItemType::Reference, + dap_types::CompletionItemType::Snippet => proto::DapCompletionItemType::Snippet, + dap_types::CompletionItemType::Text => proto::DapCompletionItemType::Text, + dap_types::CompletionItemType::Unit => proto::DapCompletionItemType::Unit, + dap_types::CompletionItemType::Value => proto::DapCompletionItemType::Value, + dap_types::CompletionItemType::Variable => proto::DapCompletionItemType::Variable, + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + match payload { + proto::DapCompletionItemType::Class => dap_types::CompletionItemType::Class, + proto::DapCompletionItemType::Color => dap_types::CompletionItemType::Color, + proto::DapCompletionItemType::CompletionItemFile => dap_types::CompletionItemType::File, + proto::DapCompletionItemType::Constructor => dap_types::CompletionItemType::Constructor, + proto::DapCompletionItemType::Customcolor => dap_types::CompletionItemType::Customcolor, + proto::DapCompletionItemType::Enum => dap_types::CompletionItemType::Enum, + proto::DapCompletionItemType::Field => dap_types::CompletionItemType::Field, + proto::DapCompletionItemType::Function => dap_types::CompletionItemType::Function, + proto::DapCompletionItemType::Interface => dap_types::CompletionItemType::Interface, + proto::DapCompletionItemType::Keyword => dap_types::CompletionItemType::Keyword, + proto::DapCompletionItemType::Method => dap_types::CompletionItemType::Method, + proto::DapCompletionItemType::Module => dap_types::CompletionItemType::Module, + proto::DapCompletionItemType::Property => dap_types::CompletionItemType::Property, + proto::DapCompletionItemType::Reference => dap_types::CompletionItemType::Reference, + proto::DapCompletionItemType::Snippet => dap_types::CompletionItemType::Snippet, + proto::DapCompletionItemType::Text => dap_types::CompletionItemType::Text, + proto::DapCompletionItemType::Unit => dap_types::CompletionItemType::Unit, + proto::DapCompletionItemType::Value => dap_types::CompletionItemType::Value, + proto::DapCompletionItemType::Variable => dap_types::CompletionItemType::Variable, + } + } +} + +impl ProtoConversion for dap_types::Thread { + type ProtoType = proto::DapThread; + type Output = Self; + + fn to_proto(&self) -> Self::ProtoType { + proto::DapThread { + id: self.id, + name: self.name.clone(), + } + } + + fn from_proto(payload: Self::ProtoType) -> Self { + Self { + id: payload.id, + name: payload.name, + } + } +} diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 3e2f70cbbaa683..14f20ffd4b6bb5 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -174,9 +174,13 @@ impl LogStore { this.add_debug_client( *client_id, project.update(cx, |project, cx| { - project - .dap_store() - .update(cx, |store, cx| store.client_by_id(client_id, cx)) + project.dap_store().update(cx, |store, cx| { + store.client_by_id(client_id, cx).and_then( + |(session, client)| { + Some((session, client.read(cx).adapter_client()?)) + }, + ) + }) }), ); } @@ -587,9 +591,8 @@ impl DapLogView { session_name: session.read(cx).name(), clients: { let mut clients = session - .read(cx) - .as_local()? - .clients() + .read_with(cx, |session, cx| session.clients(cx)) + .iter() .map(|client| DapMenuSubItem { client_id: client.id(), client_name: client.adapter_id(), diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index b3aa65d5a32850..98fe4c91beae95 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -53,14 +53,14 @@ pub(crate) struct AttachModal { impl AttachModal { pub fn new( session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, dap_store: Entity, window: &mut Window, cx: &mut Context, ) -> Self { let picker = cx.new(|cx| { Picker::uniform_list( - AttachModalDelegate::new(*session_id, *client_id, dap_store), + AttachModalDelegate::new(*session_id, client_id, dap_store), window, cx, ) @@ -130,8 +130,10 @@ impl PickerDelegate for AttachModalDelegate { if let Some(processes) = this.delegate.candidates.clone() { processes } else { - let Some((_, client)) = this.delegate.dap_store.update(cx, |store, cx| { - store.client_by_id(&this.delegate.client_id, cx) + let Some(client) = this.delegate.dap_store.update(cx, |store, cx| { + store + .client_by_id(&this.delegate.client_id, cx) + .and_then(|(_, client)| client.read(cx).adapter_client()) }) else { return Vec::new(); }; @@ -222,7 +224,7 @@ impl PickerDelegate for AttachModalDelegate { self.dap_store.update(cx, |store, cx| { store - .attach(&self.session_id, &self.client_id, candidate.pid, cx) + .attach(&self.session_id, self.client_id, candidate.pid, cx) .detach_and_log_err(cx); }); diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 3af540456786c8..c0d2275fd7164e 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -2,27 +2,24 @@ use crate::{ stack_frame_list::{StackFrameList, StackFrameListEvent}, variable_list::VariableList, }; -use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, OutputEvent, OutputEventGroup, -}; +use anyhow::anyhow; +use dap::{client::DebugAdapterClientId, OutputEvent, OutputEventGroup}; use editor::{ display_map::{Crease, CreaseId}, Anchor, CompletionProvider, Editor, EditorElement, EditorStyle, FoldPlaceholder, }; use fuzzy::StringMatchCandidate; use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; -use language::{Buffer, CodeLabel, LanguageServerId, ToOffsetUtf16}; +use language::{Buffer, CodeLabel, LanguageServerId}; use menu::Confirm; use project::{ - debugger::{dap_session::DebugSession, dap_store::DapStore}, + debugger::dap_session::{CompletionsQuery, DebugSession}, Completion, }; -use rpc::proto; use settings::Settings; -use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc, usize}; use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, Disclosure, ElevationIndex}; -use util::ResultExt; pub struct OutputGroup { pub start: Anchor, @@ -36,7 +33,6 @@ pub struct Console { groups: Vec, console: Entity, query_bar: Entity, - dap_store: Entity, session: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, @@ -47,8 +43,7 @@ pub struct Console { impl Console { pub fn new( session: Entity, - client_id: &DebugAdapterClientId, - dap_store: Entity, + client_id: DebugAdapterClientId, stack_frame_list: Entity, variable_list: Entity, window: &mut Window, @@ -91,12 +86,11 @@ impl Console { Self { session, console, - dap_store, query_bar, variable_list, _subscriptions, stack_frame_list, - client_id: *client_id, + client_id, groups: Vec::default(), } } @@ -111,8 +105,9 @@ impl Console { &self.query_bar } - fn is_local(&self, cx: &Context) -> bool { - self.dap_store.read(cx).as_local().is_some() + fn is_local(&self, _cx: &Context) -> bool { + // todo(debugger): Fix this function + true } fn handle_stack_frame_list_events( @@ -128,21 +123,6 @@ impl Console { } pub fn add_message(&mut self, event: OutputEvent, window: &mut Window, cx: &mut Context) { - if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { - client - .send(proto::UpdateDebugAdapter { - project_id: *project_id, - client_id: self.client_id.to_proto(), - thread_id: None, - session_id: self.session.read(cx).id().to_proto(), - variant: Some(proto::update_debug_adapter::Variant::OutputEvent( - event.to_proto(), - )), - }) - .log_err(); - return; - } - self.console.update(cx, |console, cx| { let output = event.output.trim_end().to_string(); @@ -254,45 +234,49 @@ impl Console { expression }); - let evaluate_task = self.dap_store.update(cx, |store, cx| { - store.evaluate( - &self.client_id, - self.stack_frame_list.read(cx).current_stack_frame_id(), + let Some(client_state) = self.session.read(cx).client_state(self.client_id) else { + return; + }; + + client_state.update(cx, |state, cx| { + state.evaluate( expression, - dap::EvaluateArgumentsContext::Variables, + Some(dap::EvaluateArgumentsContext::Variables), + Some(self.stack_frame_list.read(cx).current_stack_frame_id()), None, cx, - ) + ); }); - let weak_console = cx.weak_entity(); - - window - .spawn(cx, |mut cx| async move { - let response = evaluate_task.await?; - - weak_console.update_in(&mut cx, |console, window, cx| { - console.add_message( - OutputEvent { - category: None, - output: response.result, - group: None, - variables_reference: Some(response.variables_reference), - source: None, - line: None, - column: None, - data: None, - }, - window, - cx, - ); - - console.variable_list.update(cx, |variable_list, cx| { - variable_list.invalidate(window, cx); - }) - }) - }) - .detach_and_log_err(cx); + // TODO(debugger): make this work again + // let weak_console = cx.weak_entity(); + + // window + // .spawn(cx, |mut cx| async move { + // let response = evaluate_task.await?; + + // weak_console.update_in(&mut cx, |console, window, cx| { + // console.add_message( + // OutputEvent { + // category: None, + // output: response.result, + // group: None, + // variables_reference: Some(response.variables_reference), + // source: None, + // line: None, + // column: None, + // data: None, + // }, + // window, + // cx, + // ); + + // console.variable_list.update(cx, |variable_list, cx| { + // variable_list.invalidate(window, cx); + // }) + // }) + // }) + // .detach_and_log_err(cx); } fn render_console(&self, cx: &Context) -> impl IntoElement { @@ -383,9 +367,10 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { let support_completions = console .read(cx) - .dap_store + .session .read(cx) - .capabilities_by_id(&console.read(cx).client_id, cx) + .client_state(console.read(cx).client_id) + .map(|state| state.read(cx).capabilities()) .map(|caps| caps.supports_completions_request) .flatten() .unwrap_or_default(); @@ -470,7 +455,6 @@ impl ConsoleQueryBarCompletionProvider { }); let query = buffer.read(cx).text(); - let start_position = buffer.read(cx).anchor_before(0); cx.spawn(|_, cx| async move { let matches = fuzzy::match_strings( @@ -489,14 +473,14 @@ impl ConsoleQueryBarCompletionProvider { let variable_value = variables.get(&string_match.string)?; Some(project::Completion { - old_range: start_position..buffer_position, + old_range: buffer_position..buffer_position, new_text: string_match.string.clone(), label: CodeLabel { filter_range: 0..string_match.string.len(), text: format!("{} {}", string_match.string.clone(), variable_value), runs: Vec::new(), }, - server_id: LanguageServerId(0), // TODO debugger: read from client + server_id: LanguageServerId(usize::MAX), documentation: None, lsp_completion: Default::default(), confirm: None, @@ -514,20 +498,21 @@ impl ConsoleQueryBarCompletionProvider { buffer_position: language::Anchor, cx: &mut Context, ) -> gpui::Task>> { - let text = buffer.read(cx).text(); - let start_position = buffer.read(cx).anchor_before(0); - let snapshot = buffer.read(cx).snapshot(); + let client_id = console.read(cx).client_id; let completion_task = console.update(cx, |console, cx| { - console.dap_store.update(cx, |store, cx| { - store.completions( - &console.client_id, - console.stack_frame_list.read(cx).current_stack_frame_id(), - text, - buffer_position.to_offset_utf16(&snapshot).0 as u64, - cx, - ) - }) + if let Some(client_state) = console.session.read(cx).client_state(client_id) { + client_state.update(cx, |state, cx| { + let frame_id = Some(console.stack_frame_list.read(cx).current_stack_frame_id()); + + state.completions( + CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id), + cx, + ) + }) + } else { + Task::ready(Err(anyhow!("failed to fetch completions"))) + } }); cx.background_executor().spawn(async move { @@ -535,14 +520,14 @@ impl ConsoleQueryBarCompletionProvider { .await? .iter() .map(|completion| project::Completion { - old_range: start_position..buffer_position, + old_range: buffer_position..buffer_position, // TODO(debugger): change this new_text: completion.text.clone().unwrap_or(completion.label.clone()), label: CodeLabel { filter_range: 0..completion.label.len(), text: completion.label.clone(), runs: Vec::new(), }, - server_id: LanguageServerId(0), // TODO debugger: read from client + server_id: LanguageServerId(usize::MAX), documentation: None, lsp_completion: Default::default(), confirm: None, diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index fab1f9d27efc77..7ba512592bc7e7 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -16,8 +16,10 @@ use gpui::{ Focusable, Subscription, Task, WeakEntity, }; use project::{ - debugger::dap_session::DebugSessionId, - debugger::dap_store::{DapStore, DapStoreEvent}, + debugger::{ + dap_session::{DebugSessionId, ThreadId}, + dap_store::{DapStore, DapStoreEvent}, + }, terminals::TerminalKind, }; use rpc::proto::{self, SetDebuggerPanelItem, UpdateDebugAdapter}; @@ -118,7 +120,7 @@ pub struct DebugPanel { workspace: WeakEntity, _subscriptions: Vec, message_queue: HashMap>, - thread_states: BTreeMap<(DebugAdapterClientId, u64), Entity>, + thread_states: BTreeMap<(DebugAdapterClientId, ThreadId), Entity>, } impl DebugPanel { @@ -157,7 +159,7 @@ impl DebugPanel { cx.subscribe_in(&project, window, { move |this: &mut Self, _, event, window, cx| match event { project::Event::DebugClientStarted((session_id, client_id)) => { - this.handle_debug_client_started(session_id, client_id, window, cx); + this.handle_debug_client_started(session_id, *client_id, window, cx); } project::Event::DebugClientEvent { session_id, @@ -166,14 +168,14 @@ impl DebugPanel { } => match message { Message::Event(event) => { this.handle_debug_client_events( - session_id, client_id, event, window, cx, + session_id, *client_id, event, window, cx, ); } Message::Request(request) => { if StartDebugging::COMMAND == request.command { this.handle_start_debugging_request( session_id, - client_id, + *client_id, request.seq, request.arguments.clone(), cx, @@ -181,7 +183,7 @@ impl DebugPanel { } else if RunInTerminal::COMMAND == request.command { this.handle_run_in_terminal_request( session_id, - client_id, + *client_id, request.seq, request.arguments.clone(), window, @@ -335,8 +337,8 @@ impl DebugPanel { pub fn debug_panel_item_by_client( &self, - client_id: &DebugAdapterClientId, - thread_id: u64, + client_id: DebugAdapterClientId, + thread_id: ThreadId, cx: &mut Context, ) -> Option> { self.pane @@ -346,7 +348,7 @@ impl DebugPanel { .find(|item| { let item = item.read(cx); - &item.client_id() == client_id && item.thread_id() == thread_id + item.client_id() == client_id && item.thread_id() == thread_id }) } @@ -362,17 +364,23 @@ impl DebugPanel { let thread_panel = item.downcast::().unwrap(); let thread_id = thread_panel.read(cx).thread_id(); - let session_id = thread_panel.read(cx).session().read(cx).id(); let client_id = thread_panel.read(cx).client_id(); self.thread_states.remove(&(client_id, thread_id)); cx.notify(); - self.dap_store.update(cx, |store, cx| { - store - .terminate_threads(&session_id, &client_id, Some(vec![thread_id; 1]), cx) - .detach() + let Some(client_state) = thread_panel + .read(cx) + .session() + .read(cx) + .client_state(client_id) + else { + return; + }; + + client_state.update(cx, |state, cx| { + state.terminate_threads(Some(vec![thread_id; 1]), cx); }); } pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), @@ -405,7 +413,7 @@ impl DebugPanel { fn handle_start_debugging_request( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, seq: u64, request_args: Option, cx: &mut Context, @@ -426,7 +434,7 @@ impl DebugPanel { fn handle_run_in_terminal_request( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, seq: u64, request_args: Option, window: &mut Window, @@ -519,7 +527,7 @@ impl DebugPanel { }); let session_id = *session_id; - let client_id = *client_id; + cx.spawn(|this, mut cx| async move { // Ensure a response is always sent, even in error cases, // to maintain proper communication with the debug adapter @@ -577,14 +585,7 @@ impl DebugPanel { let respond_task = this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { - store.respond_to_run_in_terminal( - &session_id, - &client_id, - success, - seq, - body, - cx, - ) + store.respond_to_run_in_terminal(&session_id, client_id, success, seq, body, cx) }) }); @@ -596,7 +597,7 @@ impl DebugPanel { fn handle_debug_client_started( &self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, window: &mut Window, cx: &mut Context, ) { @@ -610,14 +611,12 @@ impl DebugPanel { }; let session_id = *session_id; - let client_id = *client_id; let workspace = self.workspace.clone(); let request_type = session.configuration().request.clone(); cx.spawn_in(window, |this, mut cx| async move { let task = this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - store.initialize(&session_id, &client_id, cx) - }) + this.dap_store + .update(cx, |store, cx| store.initialize(&session_id, client_id, cx)) })?; task.await?; @@ -626,7 +625,7 @@ impl DebugPanel { DebugRequestType::Launch => { let task = this.update(&mut cx, |this, cx| { this.dap_store - .update(cx, |store, cx| store.launch(&session_id, &client_id, cx)) + .update(cx, |store, cx| store.launch(&session_id, client_id, cx)) }); task?.await @@ -635,7 +634,7 @@ impl DebugPanel { if let Some(process_id) = config.process_id { let task = this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { - store.attach(&session_id, &client_id, process_id, cx) + store.attach(&session_id, client_id, process_id, cx) }) })?; @@ -646,7 +645,7 @@ impl DebugPanel { workspace.toggle_modal(window, cx, |window, cx| { AttachModal::new( &session_id, - &client_id, + client_id, this.dap_store.clone(), window, cx, @@ -680,28 +679,28 @@ impl DebugPanel { fn handle_debug_client_events( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &Events, window: &mut Window, cx: &mut Context, ) { match event { Events::Initialized(event) => { - self.handle_initialized_event(&session_id, &client_id, event, cx) + self.handle_initialized_event(&session_id, client_id, event, cx) } Events::Stopped(event) => { - self.handle_stopped_event(&session_id, &client_id, event, window, cx) + self.handle_stopped_event(&session_id, client_id, event, window, cx) } - Events::Continued(event) => self.handle_continued_event(&client_id, event, cx), - Events::Exited(event) => self.handle_exited_event(&client_id, event, cx), + Events::Continued(event) => self.handle_continued_event(client_id, event, cx), + Events::Exited(event) => self.handle_exited_event(client_id, event, cx), Events::Terminated(event) => { - self.handle_terminated_event(&session_id, &client_id, event, cx) + self.handle_terminated_event(&session_id, client_id, event, cx) } - Events::Thread(event) => self.handle_thread_event(&client_id, event, cx), - Events::Output(event) => self.handle_output_event(&client_id, event, cx), + Events::Thread(event) => self.handle_thread_event(client_id, event, cx), + Events::Output(event) => self.handle_output_event(client_id, event, cx), Events::Breakpoint(_) => {} - Events::Module(event) => self.handle_module_event(&client_id, event, cx), - Events::LoadedSource(event) => self.handle_loaded_source_event(&client_id, event, cx), + Events::Module(event) => self.handle_module_event(client_id, event, cx), + Events::LoadedSource(event) => self.handle_loaded_source_event(client_id, event, cx), Events::Capabilities(event) => { self.handle_capabilities_changed_event(session_id, client_id, event, cx); } @@ -718,26 +717,25 @@ impl DebugPanel { fn handle_initialized_event( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, capabilities: &Option, cx: &mut Context, ) { if let Some(capabilities) = capabilities { self.dap_store.update(cx, |store, cx| { - store.update_capabilities_for_client(&session_id, &client_id, capabilities, cx); + store.update_capabilities_for_client(&session_id, client_id, capabilities, cx); }); - cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id)); + cx.emit(DebugPanelEvent::CapabilitiesChanged(client_id)); } let session_id = *session_id; - let client_id = *client_id; cx.spawn(|this, mut cx| async move { this.update(&mut cx, |debug_panel, cx| { debug_panel.workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - project.initial_send_breakpoints(&session_id, &client_id, cx) + project.initial_send_breakpoints(&session_id, client_id, cx) }) }) })?? @@ -746,7 +744,7 @@ impl DebugPanel { this.update(&mut cx, |debug_panel, cx| { debug_panel .dap_store - .update(cx, |store, cx| store.configuration_done(&client_id, cx)) + .update(cx, |store, cx| store.configuration_done(client_id, cx)) })? .await }) @@ -755,22 +753,22 @@ impl DebugPanel { fn handle_continued_event( &mut self, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &ContinuedEvent, cx: &mut Context, ) { - cx.emit(DebugPanelEvent::Continued((*client_id, event.clone()))); + cx.emit(DebugPanelEvent::Continued((client_id, event.clone()))); } fn handle_stopped_event( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &StoppedEvent, window: &mut Window, cx: &mut Context, ) { - let Some(thread_id) = event.thread_id else { + let Some(thread_id) = event.thread_id.map(|thread_id| ThreadId(thread_id)) else { return; }; @@ -783,8 +781,6 @@ impl DebugPanel { return; // this can/should never happen }; - let client_id = *client_id; - cx.spawn_in(window, { let event = event.clone(); |this, mut cx| async move { @@ -800,17 +796,16 @@ impl DebugPanel { thread_state.status = ThreadStatus::Stopped; }); - let existing_item = this.debug_panel_item_by_client(&client_id, thread_id, cx); + let existing_item = this.debug_panel_item_by_client(client_id, thread_id, cx); if existing_item.is_none() { let debug_panel = cx.entity(); this.pane.update(cx, |pane, cx| { let tab = cx.new(|cx| { DebugPanelItem::new( session, - &client_id, + client_id, thread_id, thread_state, - this.dap_store.clone(), &debug_panel, this.workspace.clone(), window, @@ -858,13 +853,13 @@ impl DebugPanel { fn handle_thread_event( &mut self, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &ThreadEvent, cx: &mut Context, ) { - let thread_id = event.thread_id; + let thread_id = ThreadId(event.thread_id); - if let Some(thread_state) = self.thread_states.get(&(*client_id, thread_id)) { + if let Some(thread_state) = self.thread_states.get(&(client_id, thread_id)) { if !thread_state.read(cx).stopped && event.reason == ThreadEventReason::Exited { const MESSAGE: &'static str = "Debug session exited without hitting breakpoints\n\nTry adding a breakpoint, or define the correct path mapping for your debugger."; @@ -876,26 +871,26 @@ impl DebugPanel { if event.reason == ThreadEventReason::Started { self.thread_states - .insert((*client_id, thread_id), cx.new(|_| ThreadState::default())); + .insert((client_id, thread_id), cx.new(|_| ThreadState::default())); } - cx.emit(DebugPanelEvent::Thread((*client_id, event.clone()))); + cx.emit(DebugPanelEvent::Thread((client_id, event.clone()))); cx.notify(); } fn handle_exited_event( &mut self, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, _: &ExitedEvent, cx: &mut Context, ) { - cx.emit(DebugPanelEvent::Exited(*client_id)); + cx.emit(DebugPanelEvent::Exited(client_id)); } fn handle_terminated_event( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &Option, cx: &mut Context, ) { @@ -903,7 +898,7 @@ impl DebugPanel { for (_, thread_state) in self .thread_states - .range_mut(&(*client_id, u64::MIN)..&(*client_id, u64::MAX)) + .range_mut(&(client_id, ThreadId::MIN)..&(client_id, ThreadId::MAX)) { thread_state.update(cx, |thread_state, cx| { thread_state.status = ThreadStatus::Ended; @@ -917,9 +912,17 @@ impl DebugPanel { .as_ref() .is_some_and(|v| v.as_bool().unwrap_or(true)) { - store - .restart(&client_id, restart_args, cx) - .detach_and_log_err(cx); + let Some(session) = store.session_by_client_id(client_id) else { + return; + }; + + let Some(client_state) = session.read(cx).client_state(client_id) else { + return; + }; + + client_state.update(cx, |state, cx| { + state.restart(restart_args, cx); + }); } else { store .shutdown_session(&session_id, cx) @@ -927,21 +930,21 @@ impl DebugPanel { } }); - cx.emit(DebugPanelEvent::Terminated(*client_id)); + cx.emit(DebugPanelEvent::Terminated(client_id)); } fn handle_output_event( &mut self, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &OutputEvent, cx: &mut Context, ) { self.message_queue - .entry(*client_id) + .entry(client_id) .or_default() .push_back(event.clone()); - cx.emit(DebugPanelEvent::Output((*client_id, event.clone()))); + cx.emit(DebugPanelEvent::Output((client_id, event.clone()))); } fn on_dap_store_event( @@ -973,7 +976,7 @@ impl DebugPanel { ) { if let Some(thread_state) = self.thread_states.get_mut(&( DebugAdapterClientId::from_proto(update.client_id), - update.thread_id, + ThreadId(update.thread_id), )) { thread_state.update(cx, |thread_state, _| { thread_state.status = ThreadStatus::from_proto(update.status()); @@ -986,11 +989,11 @@ impl DebugPanel { pub(crate) fn handle_debug_adapter_update( &mut self, update: &UpdateDebugAdapter, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) { let client_id = DebugAdapterClientId::from_proto(update.client_id); - let thread_id = update.thread_id; + let thread_id = update.thread_id.map(|thread_id| ThreadId(thread_id)); let active_item = self .pane @@ -998,7 +1001,7 @@ impl DebugPanel { .active_item() .and_then(|item| item.downcast::()); - let search = self + let _search = self .pane .read(cx) .items() @@ -1020,15 +1023,16 @@ impl DebugPanel { } }); - if let Some((debug_panel_item, is_active_item)) = search { - debug_panel_item.update(cx, |this, cx| { - this.update_adapter(update, window, cx); + // if let Some((debug_panel_item, is_active_item)) = search { + // TODO(debugger): make this work again + // debug_panel_item.update(cx, |this, cx| { + // this.update_adapter(update, window, cx); - if is_active_item { - this.go_to_current_stack_frame(window, cx); - } - }); - } + // if is_active_item { + // this.go_to_current_stack_frame(window, cx); + // } + // }); + // } } fn handle_remote_has_initialized(&mut self, window: &mut Window, cx: &mut Context) { @@ -1055,7 +1059,7 @@ impl DebugPanel { }; let client_id = DebugAdapterClientId::from_proto(payload.client_id); - let thread_id = payload.thread_id; + let thread_id = ThreadId(payload.thread_id); let thread_state = payload.thread_state.clone().unwrap(); let thread_state = cx.new(|_| ThreadState::from_proto(thread_state)); @@ -1079,10 +1083,9 @@ impl DebugPanel { let debug_panel_item = cx.new(|cx| { DebugPanelItem::new( session, - &client_id, + client_id, thread_id, thread_state, - self.dap_store.clone(), &debug_panel, self.workspace.clone(), window, @@ -1118,26 +1121,26 @@ impl DebugPanel { fn handle_module_event( &mut self, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &ModuleEvent, cx: &mut Context, ) { - cx.emit(DebugPanelEvent::Module((*client_id, event.clone()))); + cx.emit(DebugPanelEvent::Module((client_id, event.clone()))); } fn handle_loaded_source_event( &mut self, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &LoadedSourceEvent, cx: &mut Context, ) { - cx.emit(DebugPanelEvent::LoadedSource((*client_id, event.clone()))); + cx.emit(DebugPanelEvent::LoadedSource((client_id, event.clone()))); } fn handle_capabilities_changed_event( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, event: &CapabilitiesEvent, cx: &mut Context, ) { @@ -1145,7 +1148,7 @@ impl DebugPanel { store.update_capabilities_for_client(session_id, client_id, &event.capabilities, cx); }); - cx.emit(DebugPanelEvent::CapabilitiesChanged(*client_id)); + cx.emit(DebugPanelEvent::CapabilitiesChanged(client_id)); } } diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 8625c244bbbbdc..46f8787c01be71 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -5,24 +5,21 @@ use crate::module_list::ModuleList; use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; use crate::variable_list::VariableList; -use dap::proto_conversions::{self, ProtoConversion}; use dap::{ client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, ThreadEvent, }; -use editor::Editor; use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::debugger::{dap_session::DebugSession, dap_store::DapStore}; -use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem, UpdateDebugAdapter}; +use project::debugger::dap_session::{DebugSession, ThreadId}; +use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem}; use settings::Settings; use ui::{prelude::*, Indicator, Tooltip}; -use util::ResultExt as _; use workspace::{ item::{self, Item, ItemEvent}, - FollowableItem, ItemHandle, ViewId, Workspace, + FollowableItem, ViewId, Workspace, }; #[derive(Debug)] @@ -60,11 +57,10 @@ impl ThreadItem { } pub struct DebugPanelItem { - thread_id: u64, + thread_id: ThreadId, console: Entity, focus_handle: FocusHandle, remote_id: Option, - dap_store: Entity, session: Entity, show_console_indicator: bool, module_list: Entity, @@ -82,10 +78,9 @@ impl DebugPanelItem { #[allow(clippy::too_many_arguments)] pub fn new( session: Entity, - client_id: &DebugAdapterClientId, - thread_id: u64, + client_id: DebugAdapterClientId, + thread_id: ThreadId, thread_state: Entity, - dap_store: Entity, debug_panel: &Entity, workspace: WeakEntity, window: &mut Window, @@ -93,13 +88,9 @@ impl DebugPanelItem { ) -> Self { let focus_handle = cx.focus_handle(); - let this = cx.entity(); - let stack_frame_list = cx.new(|cx| { StackFrameList::new( workspace.clone(), - &this, - dap_store.clone(), session.clone(), client_id, thread_id, @@ -112,23 +103,20 @@ impl DebugPanelItem { VariableList::new( session.clone(), client_id, - dap_store.clone(), stack_frame_list.clone(), window, cx, ) }); - let module_list = cx.new(|cx| ModuleList::new(session.clone(), &client_id, cx)); + let module_list = cx.new(|cx| ModuleList::new(session.clone(), client_id, cx)); - let loaded_source_list = - cx.new(|cx| LoadedSourceList::new(session.clone(), &client_id, cx)); + let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), client_id, cx)); let console = cx.new(|cx| { Console::new( session.clone(), client_id, - dap_store.clone(), stack_frame_list.clone(), variable_list.clone(), window, @@ -188,7 +176,6 @@ impl DebugPanelItem { session, console, thread_id, - dap_store, workspace, module_list, thread_state, @@ -198,33 +185,12 @@ impl DebugPanelItem { remote_id: None, stack_frame_list, loaded_source_list, - client_id: *client_id, + client_id, show_console_indicator: false, active_thread_item: ThreadItem::Variables, } } - pub(crate) fn to_proto(&self, project_id: u64, cx: &App) -> SetDebuggerPanelItem { - let thread_state = Some(self.thread_state.read(cx).to_proto()); - let variable_list = Some(self.variable_list.read(cx).to_proto()); - let stack_frame_list = Some(self.stack_frame_list.read(cx).to_proto()); - - SetDebuggerPanelItem { - project_id, - session_id: self.session.read(cx).id().to_proto(), - client_id: self.client_id.to_proto(), - thread_id: self.thread_id, - console: None, - module_list: None, - active_thread_item: self.active_thread_item.to_proto().into(), - thread_state, - variable_list, - stack_frame_list, - loaded_source_list: None, - session_name: self.session.read(cx).name(), - } - } - pub(crate) fn from_proto(&mut self, state: &SetDebuggerPanelItem, cx: &mut Context) { self.thread_state.update(cx, |thread_state, _| { let (status, stopped) = state @@ -240,12 +206,6 @@ impl DebugPanelItem { self.active_thread_item = ThreadItem::from_proto(state.active_thread_item()); - if let Some(stack_frame_list) = state.stack_frame_list.as_ref() { - self.stack_frame_list.update(cx, |this, cx| { - this.set_from_proto(stack_frame_list.clone(), cx); - }); - } - if let Some(variable_list_state) = state.variable_list.as_ref() { self.variable_list .update(cx, |this, cx| this.set_from_proto(variable_list_state, cx)); @@ -269,7 +229,7 @@ impl DebugPanelItem { } } - fn should_skip_event(&self, client_id: &DebugAdapterClientId, thread_id: u64) -> bool { + fn should_skip_event(&self, client_id: &DebugAdapterClientId, thread_id: ThreadId) -> bool { thread_id != self.thread_id || *client_id != self.client_id } @@ -279,7 +239,7 @@ impl DebugPanelItem { event: &ContinuedEvent, cx: &mut Context, ) { - if self.should_skip_event(client_id, event.thread_id) { + if self.should_skip_event(client_id, ThreadId(event.thread_id)) { return; } @@ -293,21 +253,20 @@ impl DebugPanelItem { go_to_stack_frame: bool, cx: &mut Context, ) { - if self.should_skip_event(client_id, event.thread_id.unwrap_or(self.thread_id)) { + if self.should_skip_event( + client_id, + event.thread_id.map(ThreadId).unwrap_or(self.thread_id), + ) { return; } if let Some(client_state) = self.session.read(cx).client_state(*client_id) { - client_state.update(cx, |state, cx| state.invalidate(cx)); + client_state.update(cx, |state, cx| { + state.invalidate(cx); + }); } cx.emit(DebugPanelItemEvent::Stopped { go_to_stack_frame }); - - if let Some((downstream_client, project_id)) = self.dap_store.read(cx).downstream_client() { - downstream_client - .send(self.to_proto(*project_id, cx)) - .log_err(); - } } fn handle_thread_event( @@ -316,7 +275,7 @@ impl DebugPanelItem { event: &ThreadEvent, cx: &mut Context, ) { - if self.should_skip_event(client_id, event.thread_id) { + if self.should_skip_event(client_id, ThreadId(event.thread_id)) { return; } @@ -393,9 +352,10 @@ impl DebugPanelItem { self.update_thread_state_status(ThreadStatus::Stopped, cx); - self.dap_store.update(cx, |store, cx| { - store.remove_active_debug_line_for_client(client_id, cx); - }); + // TODO(debugger): make this work again + // self.dap_store.update(cx, |store, cx| { + // store.remove_active_debug_line_for_client(client_id, cx); + // }); cx.emit(DebugPanelItemEvent::Close); } @@ -411,10 +371,6 @@ impl DebugPanelItem { self.update_thread_state_status(ThreadStatus::Exited, cx); - self.dap_store.update(cx, |store, cx| { - store.remove_active_debug_line_for_client(client_id, cx); - }); - cx.emit(DebugPanelItemEvent::Close); } @@ -427,56 +383,42 @@ impl DebugPanelItem { return; } - // notify the view that the capabilities have changed cx.notify(); - - if let Some((downstream_client, project_id)) = self.dap_store.read(cx).downstream_client() { - if let Some(caps) = self.dap_store.read(cx).capabilities_by_id(client_id, cx) { - let message = proto_conversions::capabilities_to_proto( - &caps, - *project_id, - self.session.read(cx).id().to_proto(), - self.client_id.to_proto(), - ); - - downstream_client.send(message).log_err(); - } - } } - pub(crate) fn update_adapter( - &mut self, - update: &UpdateDebugAdapter, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(update_variant) = update.variant.as_ref() { - match update_variant { - proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => { - self.stack_frame_list.update(cx, |this, cx| { - this.set_from_proto(stack_frame_list.clone(), cx); - }) - } - proto::update_debug_adapter::Variant::ThreadState(thread_state) => { - self.thread_state.update(cx, |this, _| { - *this = ThreadState::from_proto(thread_state.clone()); - }) - } - proto::update_debug_adapter::Variant::VariableList(variable_list) => self - .variable_list - .update(cx, |this, cx| this.set_from_proto(variable_list, cx)), - proto::update_debug_adapter::Variant::AddToVariableList(variables_to_add) => self - .variable_list - .update(cx, |this, _| this.add_variables(variables_to_add.clone())), - proto::update_debug_adapter::Variant::Modules(_) => {} - proto::update_debug_adapter::Variant::OutputEvent(output_event) => { - self.console.update(cx, |this, cx| { - this.add_message(OutputEvent::from_proto(output_event.clone()), window, cx); - }) - } - } - } - } + // pub(crate) fn update_adapter( + // &mut self, + // update: &UpdateDebugAdapter, + // window: &mut Window, + // cx: &mut Context, + // ) { + // if let Some(update_variant) = update.variant.as_ref() { + // match update_variant { + // proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => { + // self.stack_frame_list.update(cx, |this, cx| { + // this.set_from_proto(stack_frame_list.clone(), cx); + // }) + // } + // proto::update_debug_adapter::Variant::ThreadState(thread_state) => { + // self.thread_state.update(cx, |this, _| { + // *this = ThreadState::from_proto(thread_state.clone()); + // }) + // } + // proto::update_debug_adapter::Variant::VariableList(variable_list) => self + // .variable_list + // .update(cx, |this, cx| this.set_from_proto(variable_list, cx)), + // proto::update_debug_adapter::Variant::AddToVariableList(variables_to_add) => self + // .variable_list + // .update(cx, |this, _| this.add_variables(variables_to_add.clone())), + // proto::update_debug_adapter::Variant::Modules(_) => {} + // proto::update_debug_adapter::Variant::OutputEvent(output_event) => { + // self.console.update(cx, |this, cx| { + // this.add_message(OutputEvent::from_proto(output_event.clone()), window, cx); + // }) + // } + // } + // } + // } pub fn session(&self) -> &Entity { &self.session @@ -486,7 +428,7 @@ impl DebugPanelItem { self.client_id } - pub fn thread_id(&self) -> u64 { + pub fn thread_id(&self) -> ThreadId { self.thread_id } @@ -527,41 +469,43 @@ impl DebugPanelItem { } pub fn capabilities(&self, cx: &mut Context) -> Option { - self.dap_store + self.session() .read(cx) - .capabilities_by_id(&self.client_id, cx) + .client_state(self.client_id) + .map(|state| state.read(cx).capabilities().clone()) } fn clear_highlights(&self, cx: &mut Context) { - if let Some((_, project_path, _)) = self.dap_store.read(cx).active_debug_line() { - self.workspace - .update(cx, |workspace, cx| { - let editor = workspace - .items_of_type::(cx) - .find(|editor| Some(project_path.clone()) == editor.project_path(cx)); - - if let Some(editor) = editor { - editor.update(cx, |editor, cx| { - editor.clear_row_highlights::(); - - cx.notify(); - }); - } - }) - .ok(); - } + // TODO(debugger): make this work again + // if let Some((_, project_path, _)) = self.dap_store.read(cx).active_debug_line() { + // self.workspace + // .update(cx, |workspace, cx| { + // let editor = workspace + // .items_of_type::(cx) + // .find(|editor| Some(project_path.clone()) == editor.project_path(cx)); + + // if let Some(editor) = editor { + // editor.update(cx, |editor, cx| { + // editor.clear_row_highlights::(); + + // cx.notify(); + // }); + // } + // }) + // .ok(); + // } } pub fn go_to_current_stack_frame(&self, window: &mut Window, cx: &mut Context) { self.stack_frame_list.update(cx, |stack_frame_list, cx| { if let Some(stack_frame) = stack_frame_list - .stack_frames() + .stack_frames(cx) .iter() - .find(|frame| frame.id == stack_frame_list.current_stack_frame_id()) + .find(|frame| frame.dap.id == stack_frame_list.current_stack_frame_id()) .cloned() { stack_frame_list - .select_stack_frame(&stack_frame, true, window, cx) + .select_stack_frame(&stack_frame.dap, true, window, cx) .detach_and_log_err(cx); } }); @@ -603,134 +547,110 @@ impl DebugPanelItem { } pub fn continue_thread(&mut self, cx: &mut Context) { - self.update_thread_state_status(ThreadStatus::Running, cx); - - let task = self.dap_store.update(cx, |store, cx| { - store.continue_thread(&self.client_id, self.thread_id, cx) - }); - - cx.spawn(|this, mut cx| async move { - if task.await.log_err().is_none() { - this.update(&mut cx, |debug_panel_item, cx| { - debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); - }) - .log_err(); - } - }) - .detach(); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.continue_thread(self.thread_id, cx); + }); + }); } pub fn step_over(&mut self, cx: &mut Context) { - self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - let task = self.dap_store.update(cx, |store, cx| { - store.step_over(&self.client_id, self.thread_id, granularity, cx) - }); - - cx.spawn(|this, mut cx| async move { - if task.await.log_err().is_none() { - this.update(&mut cx, |debug_panel_item, cx| { - debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); - }) - .log_err(); - } - }) - .detach(); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.step_over(self.thread_id, granularity, cx); + }); + }); } pub fn step_in(&mut self, cx: &mut Context) { - self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - let task = self.dap_store.update(cx, |store, cx| { - store.step_in(&self.client_id, self.thread_id, granularity, cx) - }); - - cx.spawn(|this, mut cx| async move { - if task.await.log_err().is_none() { - this.update(&mut cx, |debug_panel_item, cx| { - debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); - }) - .log_err(); - } - }) - .detach(); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.step_in(self.thread_id, granularity, cx); + }); + }); } pub fn step_out(&mut self, cx: &mut Context) { - self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - let task = self.dap_store.update(cx, |store, cx| { - store.step_out(&self.client_id, self.thread_id, granularity, cx) - }); - - cx.spawn(|this, mut cx| async move { - if task.await.log_err().is_none() { - this.update(&mut cx, |debug_panel_item, cx| { - debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); - }) - .log_err(); - } - }) - .detach(); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.step_out(self.thread_id, granularity, cx); + }); + }); } pub fn step_back(&mut self, cx: &mut Context) { - self.update_thread_state_status(ThreadStatus::Running, cx); let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - let task = self.dap_store.update(cx, |store, cx| { - store.step_back(&self.client_id, self.thread_id, granularity, cx) - }); - - cx.spawn(|this, mut cx| async move { - if task.await.log_err().is_none() { - this.update(&mut cx, |debug_panel_item, cx| { - debug_panel_item.update_thread_state_status(ThreadStatus::Stopped, cx); - }) - .log_err(); - } - }) - .detach(); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.step_back(self.thread_id, granularity, cx); + }); + }); } pub fn restart_client(&self, cx: &mut Context) { - self.dap_store.update(cx, |store, cx| { - store - .restart(&self.client_id, None, cx) - .detach_and_log_err(cx); - }); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.restart(None, cx); + }); + }); } pub fn pause_thread(&self, cx: &mut Context) { - self.dap_store.update(cx, |store, cx| { - store - .pause_thread(&self.client_id, self.thread_id, cx) - .detach_and_log_err(cx) - }); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.pause_thread(self.thread_id, cx); + }); + }); } pub fn stop_thread(&self, cx: &mut Context) { - self.dap_store.update(cx, |store, cx| { - store - .terminate_threads( - &self.session.read(cx).id(), - &self.client_id, - Some(vec![self.thread_id; 1]), - cx, - ) - .detach_and_log_err(cx) - }); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.terminate_threads(Some(vec![self.thread_id; 1]), cx); + }); + }); } pub fn disconnect_client(&self, cx: &mut Context) { - self.dap_store.update(cx, |store, cx| { - store - .disconnect_client(&self.client_id, cx) - .detach_and_log_err(cx); - }); + self.session() + .read(cx) + .client_state(self.client_id) + .map(|entity| { + entity.update(cx, |state, cx| { + state.disconnect_client(cx); + }); + }); } pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context) { @@ -760,7 +680,7 @@ impl Item for DebugPanelItem { Label::new(format!( "{} - Thread {}", self.session.read(cx).name(), - self.thread_id + self.thread_id.0 )) .color(if params.selected { Color::Default @@ -774,7 +694,7 @@ impl Item for DebugPanelItem { Some(SharedString::from(format!( "{} Thread {} - {:?}", self.session.read(cx).name(), - self.thread_id, + self.thread_id.0, self.thread_state.read(cx).status, ))) } diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs index 091edc95f693f1..84296c728debb0 100644 --- a/crates/debugger_ui/src/loaded_source_list.rs +++ b/crates/debugger_ui/src/loaded_source_list.rs @@ -15,7 +15,7 @@ pub struct LoadedSourceList { impl LoadedSourceList { pub fn new( session: Entity, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, cx: &mut Context, ) -> Self { let weak_entity = cx.weak_entity(); @@ -35,7 +35,7 @@ impl LoadedSourceList { }, ); - let client_state = session.read(cx).client_state(*client_id).unwrap(); + let client_state = session.read(cx).client_state(client_id).unwrap(); let _subscription = cx.observe(&client_state, |loaded_source_list, state, cx| { let len = state.update(cx, |state, cx| state.loaded_sources(cx).len()); @@ -48,7 +48,7 @@ impl LoadedSourceList { session, focus_handle, _subscription, - client_id: *client_id, + client_id, } } diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 074cccd65f9789..00484d2e89cb14 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -14,7 +14,7 @@ pub struct ModuleList { impl ModuleList { pub fn new( session: Entity, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, cx: &mut Context, ) -> Self { let weak_entity = cx.weak_entity(); @@ -32,7 +32,7 @@ impl ModuleList { }, ); - let client_state = session.read(cx).client_state(*client_id).unwrap(); + let client_state = session.read(cx).client_state(client_id).unwrap(); let _subscription = cx.observe(&client_state, |module_list, state, cx| { let modules_len = state.update(cx, |state, cx| state.modules(cx).len()); @@ -46,7 +46,7 @@ impl ModuleList { session, focus_handle, _subscription, - client_id: *client_id, + client_id, } } diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 37d5e59396e818..b8197234def060 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -2,22 +2,15 @@ use std::path::Path; use anyhow::{anyhow, Result}; use dap::client::DebugAdapterClientId; -use dap::proto_conversions::ProtoConversion; -use dap::StackFrame; use gpui::{ list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, WeakEntity, }; -use project::debugger::{dap_session::DebugSession, dap_store::DapStore}; +use project::debugger::dap_session::{DebugSession, StackFrame, ThreadId}; use project::ProjectPath; -use rpc::proto::{DebuggerStackFrameList, UpdateDebugAdapter}; use ui::{prelude::*, Tooltip}; -use util::ResultExt; use workspace::Workspace; -use crate::debugger_panel_item::DebugPanelItemEvent::Stopped; -use crate::debugger_panel_item::{self, DebugPanelItem}; - pub type StackFrameId = u64; #[derive(Debug)] @@ -27,36 +20,31 @@ pub enum StackFrameListEvent { } pub struct StackFrameList { - thread_id: u64, list: ListState, + thread_id: ThreadId, focus_handle: FocusHandle, - dap_store: Entity, + _subscription: Subscription, session: Entity, - stack_frames: Vec, entries: Vec, workspace: WeakEntity, client_id: DebugAdapterClientId, - _subscriptions: Vec, current_stack_frame_id: StackFrameId, fetch_stack_frames_task: Option>>, } #[derive(Debug, PartialEq, Eq)] pub enum StackFrameEntry { - Normal(StackFrame), - Collapsed(Vec), + Normal(dap::StackFrame), + Collapsed(Vec), } impl StackFrameList { - #[allow(clippy::too_many_arguments)] pub fn new( workspace: WeakEntity, - debug_panel_item: &Entity, - dap_store: Entity, session: Entity, - client_id: &DebugAdapterClientId, - thread_id: u64, - window: &Window, + client_id: DebugAdapterClientId, + thread_id: ThreadId, + _window: &Window, cx: &mut Context, ) -> Self { let weak_entity = cx.weak_entity(); @@ -76,68 +64,59 @@ impl StackFrameList { }, ); - let _subscriptions = vec![cx.subscribe_in( - debug_panel_item, - window, - Self::handle_debug_panel_item_event, - )]; + let client_state = session.read(cx).client_state(client_id).unwrap(); + + let _subscription = cx.observe(&client_state, |stack_frame_list, state, cx| { + let _frame_len = state.update(cx, |state, cx| { + state.stack_frames(stack_frame_list.thread_id, cx).len() + }); + + stack_frame_list.build_entries(cx); + }); Self { list, session, workspace, - dap_store, thread_id, + client_id, focus_handle, - _subscriptions, - client_id: *client_id, + _subscription, entries: Default::default(), fetch_stack_frames_task: None, - stack_frames: Default::default(), current_stack_frame_id: Default::default(), } } - pub(crate) fn thread_id(&self) -> u64 { + pub(crate) fn thread_id(&self) -> ThreadId { self.thread_id } - pub(crate) fn to_proto(&self) -> DebuggerStackFrameList { - DebuggerStackFrameList { - thread_id: self.thread_id, - client_id: self.client_id.to_proto(), - current_stack_frame: self.current_stack_frame_id, - stack_frames: self.stack_frames.to_proto(), - } - } - - pub(crate) fn set_from_proto( - &mut self, - stack_frame_list: DebuggerStackFrameList, - cx: &mut Context, - ) { - self.thread_id = stack_frame_list.thread_id; - self.client_id = DebugAdapterClientId::from_proto(stack_frame_list.client_id); - self.current_stack_frame_id = stack_frame_list.current_stack_frame; - self.stack_frames = Vec::from_proto(stack_frame_list.stack_frames); - - self.build_entries(); - cx.notify(); - } - #[cfg(any(test, feature = "test-support"))] pub fn entries(&self) -> &Vec { &self.entries } - pub fn stack_frames(&self) -> &Vec { - &self.stack_frames + pub fn stack_frames(&self, cx: &mut App) -> Vec { + self.session + .read(cx) + .client_state(self.client_id) + .map(|state| state.update(cx, |client, cx| client.stack_frames(self.thread_id, cx))) + .unwrap_or_default() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn dap_stack_frames(&self, cx: &mut App) -> Vec { + self.stack_frames(cx) + .into_iter() + .map(|stack_frame| stack_frame.dap.clone()) + .collect() } - pub fn first_stack_frame_id(&self) -> u64 { - self.stack_frames + pub fn get_main_stack_frame_id(&self, cx: &mut Context) -> u64 { + self.stack_frames(cx) .first() - .map(|stack_frame| stack_frame.id) + .map(|stack_frame| stack_frame.dap.id) .unwrap_or(0) } @@ -145,33 +124,14 @@ impl StackFrameList { self.current_stack_frame_id } - fn handle_debug_panel_item_event( - &mut self, - _: &Entity, - event: &debugger_panel_item::DebugPanelItemEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - Stopped { go_to_stack_frame } => { - self.fetch_stack_frames(*go_to_stack_frame, window, cx); - } - _ => {} - } - } - - pub fn invalidate(&mut self, window: &mut Window, cx: &mut Context) { - self.fetch_stack_frames(true, window, cx); - } - - fn build_entries(&mut self) { + fn build_entries(&mut self, cx: &mut Context) { let mut entries = Vec::new(); let mut collapsed_entries = Vec::new(); - for stack_frame in &self.stack_frames { - match stack_frame.presentation_hint { + for stack_frame in &self.stack_frames(cx) { + match stack_frame.dap.presentation_hint { Some(dap::StackFramePresentationHint::Deemphasize) => { - collapsed_entries.push(stack_frame.clone()); + collapsed_entries.push(stack_frame.dap.clone()); } _ => { let collapsed_entries = std::mem::take(&mut collapsed_entries); @@ -179,7 +139,7 @@ impl StackFrameList { entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); } - entries.push(StackFrameEntry::Normal(stack_frame.clone())); + entries.push(StackFrameEntry::Normal(stack_frame.dap.clone())); } } } @@ -191,54 +151,55 @@ impl StackFrameList { std::mem::swap(&mut self.entries, &mut entries); self.list.reset(self.entries.len()); + cx.notify(); } - fn fetch_stack_frames( - &mut self, - go_to_stack_frame: bool, - window: &Window, - cx: &mut Context, - ) { - // If this is a remote debug session we never need to fetch stack frames ourselves - // because the host will fetch and send us stack frames whenever there's a stop event - if self.dap_store.read(cx).as_remote().is_some() { - return; - } + // fn fetch_stack_frames( + // &mut self, + // go_to_stack_frame: bool, + // window: &Window, + // cx: &mut Context, + // ) { + // // If this is a remote debug session we never need to fetch stack frames ourselves + // // because the host will fetch and send us stack frames whenever there's a stop event + // if self.dap_store.read(cx).as_remote().is_some() { + // return; + // } - let task = self.dap_store.update(cx, |store, cx| { - store.stack_frames(&self.client_id, self.thread_id, cx) - }); + // let task = self.dap_store.update(cx, |store, cx| { + // store.stack_frames(&self.client_id, self.thread_id, cx) + // }); - self.fetch_stack_frames_task = Some(cx.spawn_in(window, |this, mut cx| async move { - let mut stack_frames = task.await?; + // self.fetch_stack_frames_task = Some(cx.spawn_in(window, |this, mut cx| async move { + // let mut stack_frames = task.await?; - let task = this.update_in(&mut cx, |this, window, cx| { - std::mem::swap(&mut this.stack_frames, &mut stack_frames); + // let task = this.update_in(&mut cx, |this, window, cx| { + // std::mem::swap(&mut this.stack_frames, &mut stack_frames); - this.build_entries(); + // this.build_entries(); - cx.emit(StackFrameListEvent::StackFramesUpdated); + // cx.emit(StackFrameListEvent::StackFramesUpdated); - let stack_frame = this - .stack_frames - .first() - .cloned() - .ok_or_else(|| anyhow!("No stack frame found to select"))?; + // let stack_frame = this + // .stack_frames + // .first() + // .cloned() + // .ok_or_else(|| anyhow!("No stack frame found to select"))?; - anyhow::Ok(this.select_stack_frame(&stack_frame, go_to_stack_frame, window, cx)) - })?; + // anyhow::Ok(this.select_stack_frame(&stack_frame, go_to_stack_frame, window, cx)) + // })?; - task?.await?; + // task?.await?; - this.update(&mut cx, |this, _| { - this.fetch_stack_frames_task.take(); - }) - })); - } + // this.update(&mut cx, |this, _| { + // this.fetch_stack_frames_task.take(); + // }) + // })); + // } pub fn select_stack_frame( &mut self, - stack_frame: &StackFrame, + stack_frame: &dap::StackFrame, go_to_stack_frame: bool, window: &Window, cx: &mut Context, @@ -250,20 +211,6 @@ impl StackFrameList { )); cx.notify(); - if let Some((client, id)) = self.dap_store.read(cx).downstream_client() { - let request = UpdateDebugAdapter { - client_id: self.client_id.to_proto(), - session_id: self.session.read(cx).id().to_proto(), - project_id: *id, - thread_id: Some(self.thread_id), - variant: Some(rpc::proto::update_debug_adapter::Variant::StackFrameList( - self.to_proto(), - )), - }; - - client.send(request).log_err(); - } - if !go_to_stack_frame { return Task::ready(Ok(())); }; @@ -291,18 +238,21 @@ impl StackFrameList { })?? .await?; - this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - store.set_active_debug_line(&client_id, &project_path, row, cx); - }) - }) + Ok(()) + + // TODO(debugger): make this work again + // this.update(&mut cx, |this, cx| { + // this.dap_store.update(cx, |store, cx| { + // store.set_active_debug_line(client_id, &project_path, row, cx); + // }) + // }) } }) } - pub fn project_path_from_stack_frame( + fn project_path_from_stack_frame( &self, - stack_frame: &StackFrame, + stack_frame: &dap::StackFrame, cx: &mut Context, ) -> Option { let path = stack_frame.source.as_ref().and_then(|s| s.path.as_ref())?; @@ -317,14 +267,18 @@ impl StackFrameList { } pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context) { - self.dap_store.update(cx, |store, cx| { - store - .restart_stack_frame(&self.client_id, stack_frame_id, cx) - .detach_and_log_err(cx); - }); + if let Some(client_state) = self.session.read(cx).client_state(self.client_id) { + client_state.update(cx, |state, cx| { + state.restart_stack_frame(stack_frame_id, cx) + }); + } } - fn render_normal_entry(&self, stack_frame: &StackFrame, cx: &mut Context) -> AnyElement { + fn render_normal_entry( + &self, + stack_frame: &dap::StackFrame, + cx: &mut Context, + ) -> AnyElement { let source = stack_frame.source.clone(); let is_selected_frame = stack_frame.id == self.current_stack_frame_id; @@ -335,11 +289,10 @@ impl StackFrameList { ); let supports_frame_restart = self - .dap_store + .session .read(cx) - .capabilities_by_id(&self.client_id, cx) - .map(|caps| caps.supports_restart_frame) - .flatten() + .client_state(self.client_id) + .and_then(|client| client.read(cx).capabilities().supports_restart_frame) .unwrap_or_default(); let origin = stack_frame @@ -442,7 +395,7 @@ impl StackFrameList { pub fn expand_collapsed_entry( &mut self, ix: usize, - stack_frames: &Vec, + stack_frames: &Vec, cx: &mut Context, ) { self.entries.splice( @@ -458,7 +411,7 @@ impl StackFrameList { fn render_collapsed_entry( &self, ix: usize, - stack_frames: &Vec, + stack_frames: &Vec, cx: &mut Context, ) -> AnyElement { let first_stack_frame = &stack_frames[0]; diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 07a08e9ecf3e78..1f14d0f718e397 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -15,7 +15,7 @@ use editor::{ }; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{ - dap_store::{Breakpoint, BreakpointEditAction, BreakpointKind}, + debugger::dap_store::{Breakpoint, BreakpointEditAction, BreakpointKind}, FakeFs, Project, }; use serde_json::json; @@ -125,7 +125,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); }) .unwrap(); @@ -248,7 +248,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); }) .unwrap(); @@ -279,7 +279,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); }) .unwrap(); @@ -402,7 +402,7 @@ async fn test_client_can_open_multiple_thread_panels( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id().0); }) .unwrap(); @@ -433,7 +433,7 @@ async fn test_client_can_open_multiple_thread_panels( debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) ); assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); - assert_eq!(2, active_debug_panel_item.read(cx).thread_id()); + assert_eq!(2, active_debug_panel_item.read(cx).thread_id().0); }) .unwrap(); @@ -749,7 +749,7 @@ async fn test_handle_start_debugging_reverse_request( cx.run_until_parked(); project.update(cx, |_, cx| { - assert_eq!(2, session.read(cx).as_local().unwrap().clients_len()); + assert_eq!(2, session.read(cx).clients_len()); }); assert!( send_response.load(std::sync::atomic::Ordering::SeqCst), @@ -759,9 +759,10 @@ async fn test_handle_start_debugging_reverse_request( let second_client = project.update(cx, |_, cx| { session .read(cx) - .as_local() + .client_state(DebugAdapterClientId(1)) .unwrap() - .client_by_id(&DebugAdapterClientId(1)) + .read(cx) + .adapter_client() .unwrap() }); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 31c922c0423c2d..9413b8f6966c4c 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -162,10 +162,19 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( .unwrap(); active_debug_panel_item.update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + let (stack_frame_list, stack_frame_id) = + debug_panel_item.stack_frame_list().update(cx, |list, cx| { + ( + list.stack_frames(cx) + .into_iter() + .map(|frame| frame.dap) + .collect::>(), + list.current_stack_frame_id(), + ) + }); + + assert_eq!(1, stack_frame_id); + assert_eq!(stack_frames, stack_frame_list); }); }) .unwrap(); @@ -324,10 +333,19 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC .unwrap(); active_debug_panel_item.update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + let (stack_frame_list, stack_frame_id) = + debug_panel_item.stack_frame_list().update(cx, |list, cx| { + ( + list.stack_frames(cx) + .into_iter() + .map(|frame| frame.dap) + .collect::>(), + list.current_stack_frame_id(), + ) + }); + + assert_eq!(1, stack_frame_id); + assert_eq!(stack_frames, stack_frame_list); }); let editors = workspace.items_of_type::(cx).collect::>(); @@ -383,10 +401,19 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC .unwrap(); active_debug_panel_item.update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - - assert_eq!(2, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + let (stack_frame_list, stack_frame_id) = + debug_panel_item.stack_frame_list().update(cx, |list, cx| { + ( + list.stack_frames(cx) + .into_iter() + .map(|frame| frame.dap) + .collect::>(), + list.current_stack_frame_id(), + ) + }); + + assert_eq!(2, stack_frame_id); + assert_eq!(stack_frames, stack_frame_list); }); let editors = workspace.items_of_type::(cx).collect::>(); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 941350b7b4fa22..9173b6c5864719 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -191,10 +191,19 @@ async fn test_basic_fetch_initial_scope_and_variables( cx.run_until_parked(); active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let (stack_frame_list, stack_frame_id) = + debug_panel_item.stack_frame_list().update(cx, |list, cx| { + ( + list.stack_frames(cx) + .into_iter() + .map(|frame| frame.dap) + .collect::>(), + list.current_stack_frame_id(), + ) + }); - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + assert_eq!(1, stack_frame_id); + assert_eq!(stack_frames, stack_frame_list); debug_panel_item .variable_list() @@ -444,10 +453,19 @@ async fn test_fetch_variables_for_multiple_scopes( cx.run_until_parked(); active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let (stack_frame_list, stack_frame_id) = + debug_panel_item.stack_frame_list().update(cx, |list, cx| { + ( + list.stack_frames(cx) + .into_iter() + .map(|frame| frame.dap) + .collect::>(), + list.current_stack_frame_id(), + ) + }); - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + assert_eq!(1, stack_frame_id); + assert_eq!(stack_frames, stack_frame_list); debug_panel_item .variable_list() @@ -1324,11 +1342,21 @@ async fn test_it_only_fetches_scopes_and_variables_for_the_first_stack_frame( cx.run_until_parked(); active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); - let variable_list = debug_panel_item.variable_list().read(cx); + let (stack_frame_list, stack_frame_id) = + debug_panel_item.stack_frame_list().update(cx, |list, cx| { + ( + list.stack_frames(cx) + .into_iter() + .map(|frame| frame.dap) + .collect::>(), + list.current_stack_frame_id(), + ) + }); + + assert_eq!(1, stack_frame_id); + assert_eq!(stack_frames, stack_frame_list); - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + let variable_list = debug_panel_item.variable_list().read(cx); assert_eq!( frame_1_variables @@ -1560,11 +1588,21 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( cx.run_until_parked(); active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let (stack_frame_list, stack_frame_id) = + debug_panel_item.stack_frame_list().update(cx, |list, cx| { + ( + list.stack_frames(cx) + .into_iter() + .map(|frame| frame.dap) + .collect::>(), + list.current_stack_frame_id(), + ) + }); + let variable_list = debug_panel_item.variable_list().read(cx); - assert_eq!(1, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + assert_eq!(1, stack_frame_id); + assert_eq!(stack_frames, stack_frame_list); assert_eq!( frame_1_variables @@ -1663,11 +1701,21 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( cx.run_until_parked(); active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| { - let stack_frame_list = debug_panel_item.stack_frame_list().read(cx); + let (stack_frame_list, stack_frame_id) = + debug_panel_item.stack_frame_list().update(cx, |list, cx| { + ( + list.stack_frames(cx) + .into_iter() + .map(|frame| frame.dap) + .collect::>(), + list.current_stack_frame_id(), + ) + }); + let variable_list = debug_panel_item.variable_list().read(cx); - assert_eq!(2, stack_frame_list.current_stack_frame_id()); - assert_eq!(stack_frames, stack_frame_list.stack_frames().clone()); + assert_eq!(2, stack_frame_id); + assert_eq!(stack_frames, stack_frame_list); assert_eq!( frame_1_variables diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 08b7ba55244653..be222e0190ba9d 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -10,10 +10,10 @@ use gpui::{ FocusHandle, Focusable, Hsla, ListOffset, ListState, MouseDownEvent, Point, Subscription, Task, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::debugger::{dap_session::DebugSession, dap_store::DapStore}; +use project::debugger::dap_session::DebugSession; use rpc::proto::{ - self, DebuggerScopeVariableIndex, DebuggerVariableContainer, UpdateDebugAdapter, - VariableListScopes, VariableListVariables, + self, DebuggerScopeVariableIndex, DebuggerVariableContainer, VariableListScopes, + VariableListVariables, }; use std::{ collections::{BTreeMap, HashMap, HashSet}, @@ -329,7 +329,6 @@ type ScopeId = u64; pub struct VariableList { list: ListState, focus_handle: FocusHandle, - dap_store: Entity, open_entries: Vec, session: Entity, client_id: DebugAdapterClientId, @@ -348,8 +347,7 @@ pub struct VariableList { impl VariableList { pub fn new( session: Entity, - client_id: &DebugAdapterClientId, - dap_store: Entity, + client_id: DebugAdapterClientId, stack_frame_list: Entity, window: &mut Window, cx: &mut Context, @@ -387,13 +385,12 @@ impl VariableList { Self { list, session, - dap_store, focus_handle, _subscriptions, selection: None, stack_frame_list, set_variable_editor, - client_id: *client_id, + client_id, open_context_menu: None, set_variable_state: None, fetch_variables_task: None, @@ -426,11 +423,7 @@ impl VariableList { }) .collect(); - proto::DebuggerVariableList { - scopes, - variables, - added_variables: vec![], - } + proto::DebuggerVariableList { scopes, variables } } pub(crate) fn set_from_proto( @@ -465,44 +458,10 @@ impl VariableList { }) .collect(); - for variables in state.added_variables.iter() { - self.add_variables(variables.clone()); - } - self.build_entries(true, cx); cx.notify(); } - pub(crate) fn add_variables(&mut self, variables_to_add: proto::AddToVariableList) { - let variables: Vec = Vec::from_proto(variables_to_add.variables); - let variable_id = variables_to_add.variable_id; - let stack_frame_id = variables_to_add.stack_frame_id; - let scope_id = variables_to_add.scope_id; - let key = (stack_frame_id, scope_id); - - if let Some(depth) = self.variables.get(&key).and_then(|containers| { - containers - .variables - .iter() - .find(|container| container.variable.variables_reference == variable_id) - .map(|container| container.depth + 1usize) - }) { - if let Some(index) = self.variables.get_mut(&key) { - index.add_variables( - variable_id, - variables - .into_iter() - .map(|var| VariableContainer { - container_reference: variable_id, - variable: var, - depth, - }) - .collect(), - ); - } - } - } - fn handle_stack_frame_list_events( &mut self, _: Entity, @@ -526,35 +485,34 @@ impl VariableList { stack_frame_id: StackFrameId, cx: &mut Context, ) { - if self.scopes.contains_key(&stack_frame_id) { - return self.build_entries(true, cx); - } + // if self.scopes.contains_key(&stack_frame_id) { + // return self.build_entries(true, cx); + // } - self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { - let task = this.update(&mut cx, |variable_list, cx| { - variable_list.fetch_variables_for_stack_frame(stack_frame_id, cx) - })?; + // self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { + // let task = this.update(&mut cx, |variable_list, cx| { + // variable_list.fetch_variables_for_stack_frame(stack_frame_id, cx) + // })?; - let (scopes, variables) = task.await?; + // let (scopes, variables) = task.await?; - this.update(&mut cx, |variable_list, cx| { - variable_list.scopes.insert(stack_frame_id, scopes); + // this.update(&mut cx, |variable_list, cx| { + // variable_list.scopes.insert(stack_frame_id, scopes); - for (scope_id, variables) in variables.into_iter() { - let mut variable_index = ScopeVariableIndex::new(); - variable_index.add_variables(scope_id, variables); + // for (scope_id, variables) in variables.into_iter() { + // let mut variable_index = ScopeVariableIndex::new(); + // variable_index.add_variables(scope_id, variables); - variable_list - .variables - .insert((stack_frame_id, scope_id), variable_index); - } + // variable_list + // .variables + // .insert((stack_frame_id, scope_id), variable_index); + // } - variable_list.build_entries(true, cx); - variable_list.send_update_proto_message(cx); + // variable_list.build_entries(true, cx); - variable_list.fetch_variables_task.take(); - }) - })); + // variable_list.fetch_variables_task.take(); + // }) + // })); } #[cfg(any(test, feature = "test-support"))] @@ -595,7 +553,9 @@ impl VariableList { } pub fn completion_variables(&self, cx: &mut Context) -> Vec { - let stack_frame_id = self.stack_frame_list.read(cx).first_stack_frame_id(); + let stack_frame_id = self + .stack_frame_list + .update(cx, |this, cx| this.get_main_stack_frame_id(cx)); self.variables .range((stack_frame_id, u64::MIN)..(stack_frame_id, u64::MAX)) @@ -665,24 +625,27 @@ impl VariableList { return self.toggle_entry(&entry_id, cx); } - let fetch_variables_task = self.dap_store.update(cx, |store, cx| { - let thread_id = self.stack_frame_list.read(cx).thread_id(); - store.variables( - &self.client_id, - thread_id, - stack_frame_id, - scope_id, - self.session.read(cx).id(), - variable.variables_reference, - cx, - ) - }); + // let fetch_variables_task = self.dap_store.update(cx, |store, cx| { + // let thread_id = self.stack_frame_list.read(cx).thread_id(); + // store.variables( + // &self.client_id, + // thread_id, + // stack_frame_id, + // scope_id, + // self.session.read(cx).id(), + // variable.variables_reference, + // cx, + // ) + // }); + let fetch_variables_task = Task::ready(anyhow::Result::Err(anyhow!( + "Toggling variables isn't supported yet (dued to refactor)" + ))); let container_reference = variable.variables_reference; let entry_id = entry_id.clone(); self.fetch_variables_task = Some(cx.spawn(|this, mut cx| async move { - let new_variables = fetch_variables_task.await?; + let new_variables: Vec = fetch_variables_task.await?; this.update(&mut cx, |this, cx| { let Some(index) = this.variables.get_mut(&(stack_frame_id, scope_id)) else { @@ -867,119 +830,7 @@ impl VariableList { open_entries: &Vec, cx: &mut Context, ) -> Task>> { - let stack_frame_list = self.stack_frame_list.read(cx); - let thread_id = stack_frame_list.thread_id(); - let stack_frame_id = stack_frame_list.current_stack_frame_id(); - - let variables_task = self.dap_store.update(cx, |store, cx| { - store.variables( - &self.client_id, - thread_id, - stack_frame_id, - scope.variables_reference, - self.session.read(cx).id(), - container_reference, - cx, - ) - }); - - cx.spawn({ - let scope = scope.clone(); - let open_entries = open_entries.clone(); - |this, mut cx| async move { - let mut variables = Vec::new(); - - for variable in variables_task.await? { - variables.push(VariableContainer { - depth, - container_reference, - variable: variable.clone(), - }); - - if open_entries - .binary_search(&OpenEntry::Variable { - depth, - name: variable.name, - scope_name: scope.name.clone(), - }) - .is_ok() - { - let task = this.update(&mut cx, |this, cx| { - this.fetch_nested_variables( - &scope, - variable.variables_reference, - depth + 1, - &open_entries, - cx, - ) - })?; - - variables.extend(task.await?); - } - } - - anyhow::Ok(variables) - } - }) - } - - fn fetch_variables_for_stack_frame( - &self, - stack_frame_id: u64, - cx: &mut Context, - ) -> Task, HashMap>)>> { - let scopes_task = self.dap_store.update(cx, |store, cx| { - store.scopes(&self.client_id, stack_frame_id, cx) - }); - - cx.spawn({ - |this, mut cx| async move { - let mut variables = HashMap::new(); - - let scopes = scopes_task.await?; - - let open_entries = this.read_with(&cx, |variable_list, _| { - variable_list - .open_entries - .iter() - .filter(|entry| matches!(entry, OpenEntry::Variable { .. })) - .cloned() - .collect::>() - })?; - - for scope in scopes.iter() { - let variables_task = this.update(&mut cx, |this, cx| { - this.fetch_nested_variables( - scope, - scope.variables_reference, - 1, - &open_entries, - cx, - ) - })?; - - variables.insert(scope.variables_reference, variables_task.await?); - } - - Ok((scopes, variables)) - } - }) - } - - fn send_update_proto_message(&self, cx: &mut Context) { - if let Some((client, project_id)) = self.dap_store.read(cx).downstream_client() { - let request = UpdateDebugAdapter { - client_id: self.client_id.to_proto(), - session_id: self.session.read(cx).id().to_proto(), - thread_id: Some(self.stack_frame_list.read(cx).thread_id()), - project_id: *project_id, - variant: Some(rpc::proto::update_debug_adapter::Variant::VariableList( - self.to_proto(), - )), - }; - - client.send(request).log_err(); - }; + Task::ready(Ok(vec![])) } fn deploy_variable_context_menu( @@ -991,14 +842,20 @@ impl VariableList { window: &mut Window, cx: &mut Context, ) { - let Some(caps) = self - .dap_store + let Some((support_set_variable, support_clipboard_context)) = self + .session .read(cx) - .capabilities_by_id(&self.client_id, cx) + .client_state(self.client_id) + .map(|state| state.read(cx).capabilities()) + .map(|caps| { + ( + caps.supports_set_variable.unwrap_or_default(), + caps.supports_clipboard_context.unwrap_or_default(), + ) + }) else { return; }; - let support_set_variable = caps.supports_set_variable.unwrap_or_default(); let this = cx.entity(); @@ -1016,36 +873,26 @@ impl VariableList { let evaluate_name = variable.evaluate_name.clone(); window.handler_for(&this.clone(), move |this, _window, cx| { - this.dap_store.update(cx, |dap_store, cx| { - if dap_store - .capabilities_by_id(&this.client_id, cx) - .map(|caps| caps.supports_clipboard_context) - .flatten() - .unwrap_or_default() - { - let task = dap_store.evaluate( - &this.client_id, - this.stack_frame_list.read(cx).current_stack_frame_id(), + if support_clipboard_context { + let Some(client_state) = this.session.read(cx).client_state(this.client_id) + else { + return; + }; + + client_state.update(cx, |state, cx| { + state.evaluate( evaluate_name.clone().unwrap_or(variable_name.clone()), - dap::EvaluateArgumentsContext::Clipboard, + Some(dap::EvaluateArgumentsContext::Clipboard), + Some(this.stack_frame_list.read(cx).current_stack_frame_id()), source.clone(), cx, ); - - cx.spawn(|_, cx| async move { - let response = task.await?; - - cx.update(|cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - response.result, - )) - }) - }) - .detach_and_log_err(cx); - } else { - cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone())) - } - }); + }); + // TODO(debugger): make this work again: + // cx.write_to_clipboard(ClipboardItem::new_string(response.result)); + } else { + cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone())) + } }) }) .when_some( @@ -1126,46 +973,28 @@ impl VariableList { new_variable_value }); - let Some(state) = self.set_variable_state.take() else { + let Some(set_variable_state) = self.set_variable_state.take() else { return; }; - if new_variable_value == state.value - || state.stack_frame_id != self.stack_frame_list.read(cx).current_stack_frame_id() + if new_variable_value == set_variable_state.value + || set_variable_state.stack_frame_id + != self.stack_frame_list.read(cx).current_stack_frame_id() { return cx.notify(); } - let set_value_task = self.dap_store.update(cx, |store, cx| { - store.set_variable_value( - &self.client_id, - state.stack_frame_id, - state.parent_variables_reference, - state.name, + let Some(client_state) = self.session.read(cx).client_state(self.client_id) else { + return; + }; + + client_state.update(cx, |state, cx| { + state.set_variable_value( + set_variable_state.parent_variables_reference, + set_variable_state.name, new_variable_value, - state.evaluate_name, cx, - ) - }); - - cx.spawn_in(window, |this, mut cx| async move { - set_value_task.await?; - - this.update_in(&mut cx, |this, window, cx| { - this.build_entries(false, cx); - this.invalidate(window, cx); - }) - }) - .detach_and_log_err(cx); - } - - pub fn invalidate(&mut self, window: &mut Window, cx: &mut Context) { - self.variables.clear(); - self.scopes.clear(); - self.entries.clear(); - - self.stack_frame_list.update(cx, |stack_frame_list, cx| { - stack_frame_list.invalidate(window, cx); + ); }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6c1d8b8a5aceff..b220bb51516acb 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -29,7 +29,7 @@ use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; use project::FakeFs; use project::{ - dap_store::BreakpointKind, + debugger::dap_store::BreakpointKind, lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT, project_settings::{LspSettings, ProjectSettings}, }; diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 5c83fd04b45163..10be9c3c55c7ce 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -44,6 +44,7 @@ globset.workspace = true gpui.workspace = true http_client.workspace = true itertools.workspace = true +indexmap.workspace = true language.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index cc1b1430b8b4c3..b4b2e7f5424a4e 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -5,14 +5,13 @@ use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, requests::{Continue, Next}, - Capabilities, ContinueArguments, NextArguments, StepInArguments, StepOutArguments, - SteppingGranularity, ValueFormat, Variable, VariablesArgumentsFilter, + Capabilities, ContinueArguments, NextArguments, SetVariableResponse, StepInArguments, + StepOutArguments, SteppingGranularity, ValueFormat, Variable, VariablesArgumentsFilter, }; -use gpui::{AsyncApp, WeakEntity}; use rpc::proto; use util::ResultExt; -use super::{dap_session::DebugSessionId, dap_store::DapStore}; +use super::dap_session::DebugSessionId; pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; @@ -21,28 +20,18 @@ pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { fn is_supported(&self, capabilities: &Capabilities) -> bool; - fn handle_response( - &self, - _dap_store: WeakEntity, - _client_id: &DebugAdapterClientId, - response: Result, - _cx: &mut AsyncApp, - ) -> Result { - response - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId; fn from_proto(request: &Self::ProtoRequest) -> Self; fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> Self::ProtoRequest; fn response_to_proto( - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, message: Self::Response, ) -> ::Response; @@ -78,14 +67,14 @@ impl DapCommand for Arc { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> Self::ProtoRequest { T::to_proto(self, debug_client_id, upstream_project_id) } fn response_to_proto( - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, message: Self::Response, ) -> ::Response { T::response_to_proto(debug_client_id, message) @@ -161,7 +150,7 @@ impl DapCommand for NextCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -169,7 +158,7 @@ impl DapCommand for NextCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapNextRequest { proto::DapNextRequest { @@ -235,7 +224,7 @@ impl DapCommand for StepInCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -243,7 +232,7 @@ impl DapCommand for StepInCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapStepInRequest { proto::DapStepInRequest { @@ -294,36 +283,6 @@ impl DapCommand for StepOutCommand { true } - fn handle_response( - &self, - dap_store: WeakEntity, - client_id: &DebugAdapterClientId, - response: Result, - cx: &mut AsyncApp, - ) -> Result { - if response.is_ok() { - dap_store - .update(cx, |this, cx| { - if let Some((client, project_id)) = this.downstream_client() { - let thread_message = proto::UpdateThreadStatus { - project_id: *project_id, - client_id: client_id.to_proto(), - thread_id: self.inner.thread_id, - status: proto::DebuggerThreadStatus::Running.into(), - }; - - cx.emit(super::dap_store::DapStoreEvent::UpdateThreadStatus( - thread_message.clone(), - )); - - client.send(thread_message).log_err(); - } - }) - .log_err(); - } - response - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -341,7 +300,7 @@ impl DapCommand for StepOutCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -349,7 +308,7 @@ impl DapCommand for StepOutCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapStepOutRequest { proto::DapStepOutRequest { @@ -415,7 +374,7 @@ impl DapCommand for StepBackCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -423,7 +382,7 @@ impl DapCommand for StepBackCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapStepBackRequest { proto::DapStepBackRequest { @@ -472,43 +431,13 @@ impl DapCommand for ContinueCommand { true } - fn handle_response( - &self, - dap_store: WeakEntity, - client_id: &DebugAdapterClientId, - response: Result, - cx: &mut AsyncApp, - ) -> Result { - if response.is_ok() { - dap_store - .update(cx, |this, cx| { - if let Some((client, project_id)) = this.downstream_client() { - let thread_message = proto::UpdateThreadStatus { - project_id: *project_id, - client_id: client_id.to_proto(), - thread_id: self.args.thread_id, - status: proto::DebuggerThreadStatus::Running.into(), - }; - - cx.emit(super::dap_store::DapStoreEvent::UpdateThreadStatus( - thread_message.clone(), - )); - - client.send(thread_message).log_err(); - } - }) - .log_err(); - } - response - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapContinueRequest { proto::DapContinueRequest { @@ -549,7 +478,7 @@ impl DapCommand for ContinueCommand { } fn response_to_proto( - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, message: Self::Response, ) -> ::Response { proto::DapContinueResponse { @@ -585,7 +514,7 @@ impl DapCommand for PauseCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapPauseRequest { proto::DapPauseRequest { @@ -596,7 +525,7 @@ impl DapCommand for PauseCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -653,7 +582,7 @@ impl DapCommand for DisconnectCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapDisconnectRequest { proto::DapDisconnectRequest { @@ -666,7 +595,7 @@ impl DapCommand for DisconnectCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -727,7 +656,7 @@ impl DapCommand for TerminateThreadsCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapTerminateThreadsRequest { proto::DapTerminateThreadsRequest { @@ -738,7 +667,7 @@ impl DapCommand for TerminateThreadsCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -791,7 +720,7 @@ impl DapCommand for TerminateCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapTerminateRequest { proto::DapTerminateRequest { @@ -802,7 +731,7 @@ impl DapCommand for TerminateCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -857,7 +786,7 @@ impl DapCommand for RestartCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapRestartRequest { let raw_args = serde_json::to_vec(&self.raw).log_err().unwrap_or_default(); @@ -870,7 +799,7 @@ impl DapCommand for RestartCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -900,7 +829,6 @@ impl DapCommand for RestartCommand { #[derive(Debug, Hash, PartialEq, Eq)] pub struct VariablesCommand { pub stack_frame_id: u64, - pub scope_id: u64, pub thread_id: u64, pub variables_reference: u64, pub session_id: DebugSessionId, @@ -919,40 +847,6 @@ impl DapCommand for VariablesCommand { true } - fn handle_response( - &self, - dap_store: WeakEntity, - client_id: &DebugAdapterClientId, - response: Result, - cx: &mut AsyncApp, - ) -> Result { - let variables = response?; - - dap_store.update(cx, |this, _| { - if let Some((downstream_clients, project_id)) = this.downstream_client() { - let update = proto::UpdateDebugAdapter { - project_id: *project_id, - session_id: self.session_id.to_proto(), - client_id: client_id.to_proto(), - thread_id: Some(self.thread_id), - variant: Some(proto::update_debug_adapter::Variant::AddToVariableList( - proto::AddToVariableList { - scope_id: self.scope_id, - stack_frame_id: self.stack_frame_id, - variable_id: self.variables_reference, - variables: variables.to_proto(), - }, - )), - }; - - downstream_clients.send(update.clone()).log_err(); - // cx.emit(crate::dap_store::DapStoreEvent::UpdateDebugAdapter(update)); - } - })?; - - Ok(variables) - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -976,7 +870,7 @@ impl DapCommand for VariablesCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> Self::ProtoRequest { proto::VariablesRequest { @@ -985,7 +879,6 @@ impl DapCommand for VariablesCommand { thread_id: self.thread_id, session_id: self.session_id.to_proto(), stack_frame_id: self.stack_frame_id, - scope_id: self.scope_id, variables_reference: self.variables_reference, filter: None, start: self.start, @@ -999,7 +892,6 @@ impl DapCommand for VariablesCommand { thread_id: request.thread_id, session_id: DebugSessionId::from_proto(request.session_id), stack_frame_id: request.stack_frame_id, - scope_id: request.scope_id, variables_reference: request.variables_reference, filter: None, start: request.start, @@ -1009,7 +901,7 @@ impl DapCommand for VariablesCommand { } fn response_to_proto( - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, message: Self::Response, ) -> ::Response { proto::DapVariables { @@ -1026,6 +918,94 @@ impl DapCommand for VariablesCommand { } } +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) struct SetVariableValueCommand { + pub name: String, + pub value: String, + pub variables_reference: u64, +} + +impl DapCommand for SetVariableValueCommand { + type Response = SetVariableResponse; + type DapRequest = dap::requests::SetVariable; + type ProtoRequest = proto::DapSetVariableValueRequest; + + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities.supports_set_variable.unwrap_or_default() + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn to_dap(&self) -> ::Arguments { + dap::SetVariableArguments { + format: None, + name: self.name.clone(), + value: self.value.clone(), + variables_reference: self.variables_reference, + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message) + } + + fn to_proto( + &self, + debug_client_id: DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest { + proto::DapSetVariableValueRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + variables_reference: self.variables_reference, + value: self.value.clone(), + name: self.name.clone(), + } + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + variables_reference: request.variables_reference, + name: request.name.clone(), + value: request.value.clone(), + } + } + + fn response_to_proto( + debug_client_id: DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapSetVariableValueResponse { + client_id: debug_client_id.to_proto(), + value: message.value, + variable_type: message.type_, + named_variables: message.named_variables, + variables_reference: message.variables_reference, + indexed_variables: message.indexed_variables, + memory_reference: message.memory_reference, + } + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + Ok(SetVariableResponse { + value: message.value, + type_: message.variable_type, + variables_reference: message.variables_reference, + named_variables: message.named_variables, + indexed_variables: message.indexed_variables, + memory_reference: message.memory_reference, + }) + } +} + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) struct RestartStackFrameCommand { pub stack_frame_id: u64, @@ -1052,7 +1032,7 @@ impl DapCommand for RestartStackFrameCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapRestartStackFrameRequest { proto::DapRestartStackFrameRequest { @@ -1063,7 +1043,7 @@ impl DapCommand for RestartStackFrameCommand { } fn response_to_proto( - _debug_client_id: &DebugAdapterClientId, + _debug_client_id: DebugAdapterClientId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -1112,7 +1092,7 @@ impl DapCommand for ModulesCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapModulesRequest { proto::DapModulesRequest { @@ -1122,7 +1102,7 @@ impl DapCommand for ModulesCommand { } fn response_to_proto( - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, message: Self::Response, ) -> ::Response { proto::DapModulesResponse { @@ -1184,7 +1164,7 @@ impl DapCommand for LoadedSourcesCommand { fn to_proto( &self, - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, upstream_project_id: u64, ) -> proto::DapLoadedSourcesRequest { proto::DapLoadedSourcesRequest { @@ -1194,7 +1174,7 @@ impl DapCommand for LoadedSourcesCommand { } fn response_to_proto( - debug_client_id: &DebugAdapterClientId, + debug_client_id: DebugAdapterClientId, message: Self::Response, ) -> ::Response { proto::DapLoadedSourcesResponse { @@ -1228,3 +1208,381 @@ impl DapCommand for LoadedSourcesCommand { .collect()) } } + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct StackTraceCommand { + pub thread_id: u64, + pub start_frame: Option, + pub levels: Option, +} + +impl DapCommand for StackTraceCommand { + type Response = Vec; + type DapRequest = dap::requests::StackTrace; + type ProtoRequest = proto::DapStackTraceRequest; + + fn to_dap(&self) -> ::Arguments { + dap::StackTraceArguments { + thread_id: self.thread_id, + start_frame: self.start_frame, + levels: self.levels, + format: None, + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.stack_frames) + } + + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + + fn to_proto( + &self, + debug_client_id: DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest { + proto::DapStackTraceRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.thread_id, + start_frame: self.start_frame, + stack_trace_levels: self.levels, + } + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + thread_id: request.thread_id, + start_frame: request.start_frame, + levels: request.stack_trace_levels, + } + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + Ok(message + .frames + .into_iter() + .map(dap::StackFrame::from_proto) + .collect()) + } + + fn response_to_proto( + _debug_client_id: DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapStackTraceResponse { + frames: message.to_proto(), + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct ScopesCommand { + pub thread_id: u64, + pub stack_frame_id: u64, +} + +impl DapCommand for ScopesCommand { + type Response = Vec; + type DapRequest = dap::requests::Scopes; + type ProtoRequest = proto::DapScopesRequest; + + fn to_dap(&self) -> ::Arguments { + dap::ScopesArguments { + frame_id: self.stack_frame_id, + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.scopes) + } + + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + + fn to_proto( + &self, + debug_client_id: DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest { + proto::DapScopesRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + thread_id: self.thread_id, + stack_frame_id: self.stack_frame_id, + } + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + thread_id: request.thread_id, + stack_frame_id: request.stack_frame_id, + } + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + Ok(Vec::from_proto(message.scopes)) + } + + fn response_to_proto( + _debug_client_id: DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapScopesResponse { + scopes: message.to_proto(), + } + } +} + +impl DapCommand for super::dap_session::CompletionsQuery { + type Response = dap::CompletionsResponse; + type DapRequest = dap::requests::Completions; + type ProtoRequest = proto::DapCompletionRequest; + + fn to_dap(&self) -> ::Arguments { + dap::CompletionsArguments { + text: self.query.clone(), + frame_id: self.frame_id, + column: self.column, + line: None, + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message) + } + + fn is_supported(&self, capabilities: &Capabilities) -> bool { + capabilities + .supports_completions_request + .unwrap_or_default() + } + + fn to_proto( + &self, + debug_client_id: DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest { + proto::DapCompletionRequest { + client_id: debug_client_id.to_proto(), + project_id: upstream_project_id, + frame_id: self.frame_id, + query: self.query.clone(), + column: self.column, + line: self.line.map(u64::from), + } + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + query: request.query.clone(), + frame_id: request.frame_id, + column: request.column, + line: request.line, + } + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + Ok(dap::CompletionsResponse { + targets: Vec::from_proto(message.completions), + }) + } + + fn response_to_proto( + _debug_client_id: DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapCompletionResponse { + client_id: _debug_client_id.to_proto(), + completions: message.targets.to_proto(), + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct EvaluateCommand { + pub expression: String, + pub frame_id: Option, + pub context: Option, + pub source: Option, +} + +impl DapCommand for EvaluateCommand { + type Response = dap::EvaluateResponse; + type DapRequest = dap::requests::Evaluate; + type ProtoRequest = proto::DapEvaluateRequest; + + fn to_dap(&self) -> ::Arguments { + dap::EvaluateArguments { + expression: self.expression.clone(), + frame_id: self.frame_id, + context: self.context.clone(), + source: self.source.clone(), + line: None, + column: None, + format: None, + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message) + } + + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + + fn to_proto( + &self, + debug_client_id: DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest { + proto::DapEvaluateRequest { + client_id: debug_client_id.to_proto(), + project_id: upstream_project_id, + expression: self.expression.clone(), + frame_id: self.frame_id, + context: self + .context + .clone() + .map(|context| context.to_proto().into()), + } + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn from_proto(request: &Self::ProtoRequest) -> Self { + Self { + expression: request.expression.clone(), + frame_id: request.frame_id, + context: Some(dap::EvaluateArgumentsContext::from_proto(request.context())), + source: None, + } + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + Ok(dap::EvaluateResponse { + result: message.result.clone(), + type_: message.evaluate_type.clone(), + presentation_hint: None, + variables_reference: message.variable_reference, + named_variables: message.named_variables, + indexed_variables: message.indexed_variables, + memory_reference: message.memory_reference.clone(), + }) + } + + fn response_to_proto( + _debug_client_id: DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapEvaluateResponse { + result: message.result, + evaluate_type: message.type_, + variable_reference: message.variables_reference, + named_variables: message.named_variables, + indexed_variables: message.indexed_variables, + memory_reference: message.memory_reference, + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct ThreadsCommand; + +impl DapCommand for ThreadsCommand { + type Response = Vec; + type DapRequest = dap::requests::Threads; + type ProtoRequest = proto::DapThreadsRequest; + + fn to_dap(&self) -> ::Arguments { + () + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.threads) + } + + fn is_supported(&self, _capabilities: &Capabilities) -> bool { + true + } + + fn to_proto( + &self, + debug_client_id: DebugAdapterClientId, + upstream_project_id: u64, + ) -> Self::ProtoRequest { + proto::DapThreadsRequest { + project_id: upstream_project_id, + client_id: debug_client_id.to_proto(), + } + } + + fn from_proto(_request: &Self::ProtoRequest) -> Self { + Self {} + } + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } + + fn response_from_proto( + &self, + message: ::Response, + ) -> Result { + Ok(Vec::from_proto(message.threads)) + } + + fn response_to_proto( + _debug_client_id: DebugAdapterClientId, + message: Self::Response, + ) -> ::Response { + proto::DapThreadsResponse { + threads: message.to_proto(), + } + } +} diff --git a/crates/project/src/debugger/dap_session.rs b/crates/project/src/debugger/dap_session.rs index 1d671f4800b928..ce9bb72ffd3d67 100644 --- a/crates/project/src/debugger/dap_session.rs +++ b/crates/project/src/debugger/dap_session.rs @@ -1,7 +1,23 @@ -use collections::{BTreeMap, HashMap}; -use dap::{Capabilities, Module, Source}; +use super::dap_command::{ + self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, NextCommand, + PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, SetVariableValueCommand, + StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, + TerminateThreadsCommand, VariablesCommand, +}; +use anyhow::{anyhow, Result}; +use collections::{BTreeMap, HashMap, IndexMap}; +use dap::client::{DebugAdapterClient, DebugAdapterClientId}; +use dap::requests::Request; +use dap::{ + Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, SteppingGranularity, +}; use futures::{future::Shared, FutureExt}; -use gpui::{AppContext, Context, Entity, Task, WeakEntity}; +use gpui::{App, AppContext, Context, Entity, Task}; +use rpc::AnyProtoClient; +use serde_json::Value; +use std::borrow::Borrow; +use std::collections::btree_map::Entry as BTreeMapEntry; +use std::u64; use std::{ any::Any, collections::hash_map::Entry, @@ -9,14 +25,9 @@ use std::{ sync::Arc, }; use task::DebugAdapterConfig; +use text::{PointUtf16, ToPointUtf16}; use util::ResultExt; -use super::{ - dap_command::{self, DapCommand}, - dap_store::DapStore, -}; -use dap::client::{DebugAdapterClient, DebugAdapterClientId}; - #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct DebugSessionId(pub usize); @@ -31,23 +42,58 @@ impl DebugSessionId { } } -#[derive(Copy, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)] #[repr(transparent)] -struct ThreadId(u64); +pub struct ThreadId(pub u64); + +impl ThreadId { + pub const MIN: ThreadId = ThreadId(u64::MIN); + pub const MAX: ThreadId = ThreadId(u64::MAX); +} -struct Variable { - _variable: dap::Variable, - _variables: Vec, +#[derive(Clone)] +pub struct Variable { + dap: dap::Variable, + variables: Vec, } -struct Scope { - _scope: dap::Scope, - _variables: Vec, +impl From for Variable { + fn from(dap: dap::Variable) -> Self { + Self { + dap, + variables: vec![], + } + } +} + +#[derive(Clone)] +pub struct Scope { + pub dap: dap::Scope, + pub variables: Vec, } -struct StackFrame { - _stack_frame: dap::StackFrame, - _scopes: Vec, +impl From for Scope { + fn from(scope: dap::Scope) -> Self { + Self { + dap: scope, + variables: vec![], + } + } +} + +#[derive(Clone)] +pub struct StackFrame { + pub dap: dap::StackFrame, + pub scopes: Vec, +} + +impl From for StackFrame { + fn from(stack_frame: dap::StackFrame) -> Self { + Self { + scopes: vec![], + dap: stack_frame, + } + } } #[derive(Copy, Clone, Default, PartialEq, Eq)] @@ -59,20 +105,140 @@ pub enum ThreadStatus { Ended, } -struct Thread { - _thread: dap::Thread, - _stack_frames: Vec, +pub struct Thread { + dap: dap::Thread, + stack_frames: Vec, _status: ThreadStatus, _has_stopped: bool, } -pub struct DebugAdapterClientState { - dap_store: WeakEntity, +impl From for Thread { + fn from(dap: dap::Thread) -> Self { + Self { + dap, + stack_frames: vec![], + _status: ThreadStatus::default(), + _has_stopped: false, + } + } +} + +type UpstreamProjectId = u64; + +pub struct RemoteConnection { + client: AnyProtoClient, + upstream_project_id: UpstreamProjectId, +} + +impl RemoteConnection { + fn send_proto_client_request( + &self, + request: R, + client_id: DebugAdapterClientId, + cx: &mut App, + ) -> Task> { + let message = request.to_proto(client_id, self.upstream_project_id); + let upstream_client = self.client.clone(); + cx.background_executor().spawn(async move { + let response = upstream_client.request(message).await?; + request.response_from_proto(response) + }) + } + fn request_remote( + &self, + request: R, + client_id: DebugAdapterClientId, + cx: &mut App, + ) -> Task> + where + ::Response: 'static, + ::Arguments: 'static + Send, + { + return self.send_proto_client_request::(request, client_id, cx); + } +} + +pub enum Mode { + Local(Arc), + Remote(RemoteConnection), +} + +impl From for Mode { + fn from(value: RemoteConnection) -> Self { + Self::Remote(value) + } +} + +impl From> for Mode { + fn from(client: Arc) -> Self { + Mode::Local(client) + } +} + +impl Mode { + fn request_local( + connection: &Arc, + caps: &Capabilities, + request: R, + cx: &mut Context, + ) -> Task> + where + ::Response: 'static, + ::Arguments: 'static + Send, + { + if !request.is_supported(&caps) { + return Task::ready(Err(anyhow!( + "Request {} is not supported", + R::DapRequest::COMMAND + ))); + } + + let request = Arc::new(request); + + let request_clone = request.clone(); + let connection = connection.clone(); + let request_task = cx.background_executor().spawn(async move { + let args = request_clone.to_dap(); + connection.request::(args).await + }); + + cx.background_executor().spawn(async move { + let response = request.response_from_dap(request_task.await?); + response + }) + } + + fn request_dap( + &self, + caps: &Capabilities, + client_id: DebugAdapterClientId, + request: R, + cx: &mut Context, + ) -> Task> + where + ::Response: 'static, + ::Arguments: 'static + Send, + { + match self { + Mode::Local(debug_adapter_client) => { + Self::request_local(&debug_adapter_client, caps, request, cx) + } + Mode::Remote(remote_connection) => { + remote_connection.request_remote(request, client_id, cx) + } + } + } +} + +/// Represents a current state of a single debug adapter and provides ways to mutate it. +pub struct Client { + mode: Mode, + pub(super) capabilities: Capabilities, client_id: DebugAdapterClientId, modules: Vec, loaded_sources: Vec, - _threads: BTreeMap, + threads: IndexMap, requests: HashMap>>>, } @@ -127,7 +293,35 @@ impl Hash for RequestSlot { } } -impl DebugAdapterClientState { +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct CompletionsQuery { + pub query: String, + pub column: u64, + pub line: Option, + pub frame_id: Option, +} + +impl CompletionsQuery { + pub fn new( + buffer: &language::Buffer, + cursor_position: language::Anchor, + frame_id: Option, + ) -> Self { + let PointUtf16 { row, column } = cursor_position.to_point_utf16(&buffer.snapshot()); + Self { + query: buffer.text(), + column: column as u64, + frame_id, + line: Some(row as u64), + } + } +} + +impl Client { + pub fn capabilities(&self) -> &Capabilities { + &self.capabilities + } + pub(crate) fn _wait_for_request( &self, request: R, @@ -136,48 +330,102 @@ impl DebugAdapterClientState { self.requests.get(&request_slot).cloned() } - /// Ensure that there's a request in flight for the given command, and if not, send it. - fn request( + /// Ensure that there's a request in flight for the given command, and if not, send it. Use this to run requests that are idempotent. + fn fetch( &mut self, request: T, - process_result: impl FnOnce(&mut Self, T::Response) + 'static + Send + Sync, + process_result: impl FnOnce(&mut Self, &T::Response, &mut Context) + 'static, cx: &mut Context, ) { if let Entry::Vacant(vacant) = self.requests.entry(request.into()) { let command = vacant.key().0.clone().as_any_arc().downcast::().unwrap(); - if let Ok(request) = self.dap_store.update(cx, |dap_store, cx| { - dap_store.request_dap(&self.client_id, command, cx) - }) { - let task = cx - .spawn(|this, mut cx| async move { - let result = request.await.log_err()?; - this.update(&mut cx, |this, cx| { - process_result(this, result); - cx.notify(); - }) - .log_err() - }) - .shared(); - - vacant.insert(task); - } + let task = Self::request_inner::>( + &self.capabilities, + self.client_id, + &self.mode, + command, + process_result, + cx, + ); + let task = cx + .background_executor() + .spawn(async move { + let _ = task.await?; + Some(()) + }) + .shared(); + + vacant.insert(task); } } + fn request_inner( + capabilities: &Capabilities, + client_id: DebugAdapterClientId, + mode: &Mode, + request: T, + process_result: impl FnOnce(&mut Self, &T::Response, &mut Context) + 'static, + cx: &mut Context, + ) -> Task> { + let request = mode.request_dap(&capabilities, client_id, request, cx); + cx.spawn(|this, mut cx| async move { + let result = request.await.log_err()?; + this.update(&mut cx, |this, cx| { + process_result(this, &result, cx); + }) + .log_err(); + Some(result) + }) + } + + fn request( + &self, + request: T, + process_result: impl FnOnce(&mut Self, &T::Response, &mut Context) + 'static, + cx: &mut Context, + ) -> Task> { + Self::request_inner( + &self.capabilities, + self.client_id, + &self.mode, + request, + process_result, + cx, + ) + } + pub fn invalidate(&mut self, cx: &mut Context) { self.requests.clear(); self.modules.clear(); self.loaded_sources.clear(); - cx.notify(); } + pub fn threads(&mut self, cx: &mut Context) -> Vec { + self.fetch( + dap_command::ThreadsCommand, + |this, result, cx| { + this.threads.extend( + result + .iter() + .map(|thread| (ThreadId(thread.id), Thread::from(thread.clone()))), + ); + }, + cx, + ); + self.threads + .values() + .map(|thread| thread.dap.clone()) + .collect() + } + pub fn modules(&mut self, cx: &mut Context) -> &[Module] { - self.request( + self.fetch( dap_command::ModulesCommand, - |this, result| { - this.modules = result; + |this, result, cx| { + this.modules = result.clone(); + cx.notify(); }, cx, ); @@ -198,16 +446,224 @@ impl DebugAdapterClientState { } pub fn loaded_sources(&mut self, cx: &mut Context) -> &[Source] { - self.request( + self.fetch( dap_command::LoadedSourcesCommand, - |this, result| { - this.loaded_sources = result; + |this, result, cx| { + this.loaded_sources = result.clone(); + cx.notify(); }, cx, ); &self.loaded_sources } + fn empty_response(&mut self, _: &(), _cx: &mut Context) {} + + pub fn pause_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { + self.request( + PauseCommand { + thread_id: thread_id.0, + }, + Self::empty_response, + cx, + ) + .detach(); + } + + pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context) { + self.request( + RestartStackFrameCommand { stack_frame_id }, + Self::empty_response, + cx, + ) + .detach(); + } + + pub fn restart(&mut self, args: Option, cx: &mut Context) { + if self.capabilities.supports_restart_request.unwrap_or(false) { + self.request( + RestartCommand { + raw: args.unwrap_or(Value::Null), + }, + Self::empty_response, + cx, + ) + .detach(); + } else { + self.request( + DisconnectCommand { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }, + Self::empty_response, + cx, + ) + .detach(); + } + } + + fn shutdown(&mut self, cx: &mut Context) { + if self + .capabilities + .supports_terminate_request + .unwrap_or_default() + { + self.request( + TerminateCommand { + restart: Some(false), + }, + Self::empty_response, + cx, + ) + .detach(); + } else { + self.request( + DisconnectCommand { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }, + Self::empty_response, + cx, + ) + .detach(); + } + } + + pub fn completions( + &mut self, + query: CompletionsQuery, + cx: &mut Context, + ) -> Task>> { + let task = self.request(query, |_, _, _| {}, cx); + + cx.background_executor().spawn(async move { + anyhow::Ok( + task.await + .map(|response| response.targets) + .ok_or_else(|| anyhow!("failed to fetch completions"))?, + ) + }) + } + + pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { + self.request( + ContinueCommand { + args: ContinueArguments { + thread_id: thread_id.0, + single_thread: Some(true), + }, + }, + |_, _, _| {}, // todo: what do we do about the payload here? + cx, + ) + .detach(); + } + + pub fn adapter_client(&self) -> Option> { + match self.mode { + Mode::Local(ref adapter_client) => Some(adapter_client.clone()), + Mode::Remote(_) => None, + } + } + + pub fn step_over( + &mut self, + thread_id: ThreadId, + granularity: SteppingGranularity, + cx: &mut Context, + ) { + let supports_single_thread_execution_requests = + self.capabilities.supports_single_thread_execution_requests; + let supports_stepping_granularity = self + .capabilities + .supports_stepping_granularity + .unwrap_or_default(); + + let command = NextCommand { + inner: StepCommand { + thread_id: thread_id.0, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests, + }, + }; + + self.request(command, Self::empty_response, cx).detach(); + } + + pub fn step_in( + &self, + thread_id: ThreadId, + granularity: SteppingGranularity, + cx: &mut Context, + ) { + let supports_single_thread_execution_requests = + self.capabilities.supports_single_thread_execution_requests; + let supports_stepping_granularity = self + .capabilities + .supports_stepping_granularity + .unwrap_or_default(); + + let command = StepInCommand { + inner: StepCommand { + thread_id: thread_id.0, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests, + }, + }; + + self.request(command, Self::empty_response, cx).detach(); + } + + pub fn step_out( + &self, + thread_id: ThreadId, + granularity: SteppingGranularity, + cx: &mut Context, + ) { + let supports_single_thread_execution_requests = + self.capabilities.supports_single_thread_execution_requests; + let supports_stepping_granularity = self + .capabilities + .supports_stepping_granularity + .unwrap_or_default(); + + let command = StepOutCommand { + inner: StepCommand { + thread_id: thread_id.0, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests, + }, + }; + + self.request(command, Self::empty_response, cx).detach(); + } + + pub fn step_back( + &self, + thread_id: ThreadId, + granularity: SteppingGranularity, + cx: &mut Context, + ) { + let supports_single_thread_execution_requests = + self.capabilities.supports_single_thread_execution_requests; + let supports_stepping_granularity = self + .capabilities + .supports_stepping_granularity + .unwrap_or_default(); + + let command = StepBackCommand { + inner: StepCommand { + thread_id: thread_id.0, + granularity: supports_stepping_granularity.then(|| granularity), + single_thread: supports_single_thread_execution_requests, + }, + }; + + self.request(command, Self::empty_response, cx).detach(); + } + pub fn handle_loaded_source_event( &mut self, event: &dap::LoadedSourceEvent, @@ -241,12 +697,205 @@ impl DebugAdapterClientState { } cx.notify(); } + + pub fn stack_frames(&mut self, thread_id: ThreadId, cx: &mut Context) -> Vec { + self.fetch( + super::dap_command::StackTraceCommand { + thread_id: thread_id.0, + start_frame: None, + levels: None, + }, + move |this, stack_frames, cx| { + let entry = this.threads.entry(thread_id).and_modify(|thread| { + thread.stack_frames = stack_frames.iter().cloned().map(From::from).collect(); + }); + debug_assert!( + matches!(entry, indexmap::map::Entry::Occupied(_)), + "Sent request for thread_id that doesn't exist" + ); + + cx.notify(); + }, + cx, + ); + + self.threads + .get(&thread_id) + .map(|thread| thread.stack_frames.clone()) + .unwrap_or_default() + } + + pub fn scopes( + &mut self, + thread_id: ThreadId, + stack_frame_id: u64, + cx: &mut Context, + ) -> Vec { + self.fetch( + ScopesCommand { + thread_id: thread_id.0, + stack_frame_id, + }, + move |this, scopes, cx| { + this.threads.entry(thread_id).and_modify(|thread| { + if let Some(stack_frame) = thread + .stack_frames + .iter_mut() + .find(|frame| frame.dap.id == stack_frame_id) + { + stack_frame.scopes = scopes.iter().cloned().map(From::from).collect(); + cx.notify(); + } + }); + }, + cx, + ); + self.threads + .get(&thread_id) + .and_then(|thread| { + thread.stack_frames.iter().find_map(|stack_frame| { + (stack_frame.dap.id == stack_frame_id).then(|| stack_frame.scopes.clone()) + }) + }) + .unwrap_or_default() + } + + fn find_scope( + &mut self, + thread_id: ThreadId, + stack_frame_id: u64, + variables_reference: u64, + ) -> Option<&mut Scope> { + self.threads.get_mut(&thread_id).and_then(|thread| { + let stack_frame = thread + .stack_frames + .iter_mut() + .find(|stack_frame| (stack_frame.dap.id == stack_frame_id))?; + stack_frame + .scopes + .iter_mut() + .find(|scope| scope.dap.variables_reference == variables_reference) + }) + } + + #[allow(clippy::too_many_arguments)] + pub fn variables( + &mut self, + thread_id: ThreadId, + stack_frame_id: u64, + session_id: DebugSessionId, + variables_reference: u64, + cx: &mut Context, + ) -> Vec { + let command = VariablesCommand { + stack_frame_id, + session_id, + thread_id: thread_id.0, + variables_reference, + filter: None, + start: None, + count: None, + format: None, + }; + + self.fetch( + command, + move |this, variables, cx| { + if let Some(scope) = this.find_scope(thread_id, stack_frame_id, variables_reference) + { + // This is only valid if scope.variable[x].ref_id == variables_reference + // otherwise we have to search the tree for the right index to add variables too + // todo(debugger): Fix this ^ + scope.variables = variables.iter().cloned().map(From::from).collect(); + cx.notify(); + } + }, + cx, + ); + + self.find_scope(thread_id, stack_frame_id, variables_reference) + .map(|scope| scope.variables.clone()) + .unwrap_or_default() + } + + pub fn set_variable_value( + &mut self, + variables_reference: u64, + name: String, + value: String, + cx: &mut Context, + ) { + if self.capabilities.supports_set_variable.unwrap_or_default() { + self.request( + SetVariableValueCommand { + name, + value, + variables_reference, + }, + |this, _response, cx| { + this.invalidate(cx); + }, + cx, + ) + .detach() + } + } + + pub fn evaluate( + &mut self, + expression: String, + context: Option, + frame_id: Option, + source: Option, + cx: &mut Context, + ) { + self.request( + EvaluateCommand { + expression, + context, + frame_id, + source, + }, + |this, _response, cx| { + this.invalidate(cx); + }, + cx, + ) + .detach() + } + + pub fn disconnect_client(&mut self, cx: &mut Context) { + let command = DisconnectCommand { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }; + + self.request(command, Self::empty_response, cx).detach() + } + + pub fn terminate_threads(&mut self, thread_ids: Option>, cx: &mut Context) { + if self + .capabilities + .supports_terminate_threads_request + .unwrap_or_default() + { + self.request( + TerminateThreadsCommand { + thread_ids: thread_ids.map(|ids| ids.into_iter().map(|id| id.0).collect()), + }, + Self::empty_response, + cx, + ) + .detach(); + } + } } pub struct DebugSession { id: DebugSessionId, mode: DebugSessionMode, - pub(super) states: HashMap>, + pub(super) states: BTreeMap>, ignore_breakpoints: bool, } @@ -257,7 +906,6 @@ pub enum DebugSessionMode { pub struct LocalDebugSession { configuration: DebugAdapterConfig, - clients: HashMap>, } impl LocalDebugSession { @@ -273,42 +921,6 @@ impl LocalDebugSession { f(&mut self.configuration); cx.notify(); } - - fn add_client(&mut self, client: Arc, cx: &mut Context) { - self.clients.insert(client.id(), client); - cx.notify(); - } - - pub fn remove_client( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) -> Option> { - let client = self.clients.remove(client_id); - cx.notify(); - - client - } - - pub fn client_by_id( - &self, - client_id: &DebugAdapterClientId, - ) -> Option> { - self.clients.get(client_id).cloned() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn clients_len(&self) -> usize { - self.clients.len() - } - - pub fn clients(&self) -> impl Iterator> + '_ { - self.clients.values().cloned() - } - - pub fn client_ids(&self) -> impl Iterator + '_ { - self.clients.keys().cloned() - } } pub struct RemoteDebugSession { @@ -320,11 +932,8 @@ impl DebugSession { Self { id, ignore_breakpoints: false, - states: HashMap::default(), - mode: DebugSessionMode::Local(LocalDebugSession { - configuration, - clients: HashMap::default(), - }), + states: BTreeMap::default(), + mode: DebugSessionMode::Local(LocalDebugSession { configuration }), } } @@ -346,7 +955,7 @@ impl DebugSession { Self { id, ignore_breakpoints, - states: HashMap::default(), + states: BTreeMap::default(), mode: DebugSessionMode::Remote(RemoteDebugSession { label }), } } @@ -371,38 +980,64 @@ impl DebugSession { cx.notify(); } - pub fn client_state( - &self, - client_id: DebugAdapterClientId, - ) -> Option> { + pub fn client_state(&self, client_id: DebugAdapterClientId) -> Option> { self.states.get(&client_id).cloned() } + pub(super) fn client_ids(&self) -> impl Iterator + '_ { + self.states.keys().copied() + } + + pub fn clients(&self, cx: &App) -> Vec> { + self.states + .values() + .filter_map(|state| state.read(cx).adapter_client()) + .collect() + } + pub fn add_client( &mut self, - client: Option>, + client: impl Into, client_id: DebugAdapterClientId, - weak_dap: WeakEntity, cx: &mut Context, ) { if !self.states.contains_key(&client_id) { - let state = cx.new(|_cx| DebugAdapterClientState { - dap_store: weak_dap, + let mode = client.into(); + let state = cx.new(|_cx| Client { client_id, modules: Vec::default(), loaded_sources: Vec::default(), - _threads: BTreeMap::default(), + threads: IndexMap::default(), requests: HashMap::default(), capabilities: Default::default(), + mode, }); self.states.insert(client_id, state); } + } - if let Some(client) = client { - self.as_local_mut() - .expect("Client can only exist on local Zed instances") - .add_client(client, cx); + pub(crate) fn client_by_id( + &self, + client_id: impl Borrow, + ) -> Option> { + self.states.get(client_id.borrow()).cloned() + } + + pub(crate) fn shutdown_client( + &mut self, + client_id: DebugAdapterClientId, + cx: &mut Context, + ) { + if let Some(client) = self.states.remove(&client_id) { + client.update(cx, |this, cx| { + this.shutdown(cx); + }) } } + + #[cfg(any(test, feature = "test-support"))] + pub fn clients_len(&self) -> usize { + self.states.len() + } } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index b44ace8048a811..017764355bcecd 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1,33 +1,30 @@ use super::{ - dap_command::{ - ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, - RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, - TerminateCommand, TerminateThreadsCommand, VariablesCommand, - }, - dap_session::{DebugSession, DebugSessionId}, + // Will need to uncomment this once we implement rpc message handler again + // dap_command::{ + // ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, + // RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, + // TerminateCommand, TerminateThreadsCommand, VariablesCommand, + // }, + dap_command::DapCommand, + dap_session::{self, DebugSession, DebugSessionId}, }; use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectItem as _, ProjectPath}; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; -use dap::ContinueResponse; use dap::{ adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}, client::{DebugAdapterClient, DebugAdapterClientId}, messages::{Message, Response}, requests::{ - Attach, Completions, ConfigurationDone, Disconnect, Evaluate, Initialize, Launch, - LoadedSources, Modules, Request as _, RunInTerminal, Scopes, SetBreakpoints, SetExpression, - SetVariable, StackTrace, StartDebugging, Terminate, + Attach, Completions, Evaluate, Initialize, Launch, Request as _, RunInTerminal, + SetBreakpoints, SetExpression, SetVariable, StartDebugging, }, - AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, - ConfigurationDoneArguments, ContinueArguments, DisconnectArguments, ErrorResponse, + AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ErrorResponse, EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, - InitializeRequestArgumentsPathFormat, LaunchRequestArguments, LoadedSourcesArguments, Module, - ModulesArguments, Scope, ScopesArguments, SetBreakpointsArguments, SetExpressionArguments, - SetVariableArguments, Source, SourceBreakpoint, StackFrame, StackTraceArguments, - StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, SteppingGranularity, - TerminateArguments, Variable, + InitializeRequestArgumentsPathFormat, LaunchRequestArguments, SetBreakpointsArguments, + SetExpressionArguments, SetVariableArguments, Source, SourceBreakpoint, + StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, }; use dap_adapters::build_adapter; use fs::Fs; @@ -47,8 +44,8 @@ use rpc::{ use serde_json::Value; use settings::{Settings as _, WorktreeId}; use smol::lock::Mutex; -use std::collections::VecDeque; use std::{ + borrow::Borrow, collections::{BTreeMap, HashSet}, ffi::OsStr, hash::{Hash, Hasher}, @@ -58,6 +55,7 @@ use std::{ Arc, }, }; +use std::{collections::VecDeque, sync::atomic::AtomicU32}; use task::{AttachConfig, DebugAdapterConfig, DebugRequestType}; use text::Point; use util::{merge_json_value_into, ResultExt as _}; @@ -92,7 +90,7 @@ pub enum DapStoreMode { pub struct LocalDapStore { fs: Arc, node_runtime: NodeRuntime, - next_client_id: AtomicUsize, + next_client_id: AtomicU32, next_session_id: AtomicUsize, http_client: Arc, environment: Entity, @@ -142,18 +140,19 @@ impl DapStore { client.add_entity_message_handler(Self::handle_ignore_breakpoint_state); client.add_entity_message_handler(Self::handle_session_has_shutdown); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_dap_command::); + // todo(debugger): Reenable these after we finish handle_dap_command refactor + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); + // client.add_entity_request_handler(Self::handle_dap_command::); client.add_entity_request_handler(Self::handle_shutdown_session_request); } @@ -293,29 +292,32 @@ impl DapStore { pub fn session_by_client_id( &self, - client_id: &DebugAdapterClientId, + client_id: impl Borrow, ) -> Option> { self.sessions - .get(self.client_by_session.get(client_id)?) + .get(self.client_by_session.get(client_id.borrow())?) .cloned() } pub fn client_by_id( &self, - client_id: &DebugAdapterClientId, + client_id: impl Borrow, cx: &Context, - ) -> Option<(Entity, Arc)> { - let local_session = self.session_by_client_id(client_id)?; - let client = local_session.read(cx).as_local()?.client_by_id(client_id)?; + ) -> Option<(Entity, Entity)> { + let client_id = client_id.borrow(); + let session = self.session_by_client_id(client_id)?; - Some((local_session, client)) + let client = session.read(cx).client_by_id(*client_id)?; + + Some((session, client)) } pub fn capabilities_by_id( &self, - client_id: &DebugAdapterClientId, + client_id: impl Borrow, cx: &App, ) -> Option { + let client_id = client_id.borrow(); self.session_by_client_id(client_id).and_then(|session| { session .read(cx) @@ -327,13 +329,13 @@ impl DapStore { pub fn update_capabilities_for_client( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, capabilities: &Capabilities, cx: &mut Context, ) { if let Some((client, _)) = self.client_by_id(client_id, cx) { client.update(cx, |this, cx| { - if let Some(state) = this.client_state(*client_id) { + if let Some(state) = this.client_state(client_id) { state.update(cx, |this, _| { this.capabilities = this.capabilities.merge(capabilities.clone()); }); @@ -584,10 +586,9 @@ impl DapStore { store.client_by_session.insert(client_id, session_id); let session = store.session_by_id(&session_id).unwrap(); - let weak_dap = cx.weak_entity(); session.update(cx, |session, cx| { - session.add_client(Some(Arc::new(client)), client_id, weak_dap, cx); + session.add_client(Arc::new(client), client_id, cx); let local_session = session .as_local_mut() .expect("Only local sessions should attempt to reconnect"); @@ -735,10 +736,8 @@ impl DapStore { }; this.update(&mut cx, |store, cx| { - let weak_dap = cx.weak_entity(); - session.update(cx, |session, cx| { - session.add_client(Some(client.clone()), client.id(), weak_dap, cx); + session.add_client(client.clone(), client.id(), cx); }); let client_id = client.id(); @@ -757,10 +756,13 @@ impl DapStore { pub fn initialize( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, cx: &mut Context, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { + let Some(client) = self + .client_by_id(client_id, cx) + .and_then(|(_, client)| client.read(cx).adapter_client()) + else { return Task::ready(Err(anyhow!( "Could not find debug client: {:?} for session {:?}", client_id, @@ -769,7 +771,6 @@ impl DapStore { }; let session_id = *session_id; - let client_id = *client_id; cx.spawn(|this, mut cx| async move { let capabilities = client @@ -794,18 +795,49 @@ impl DapStore { .await?; this.update(&mut cx, |store, cx| { - store.update_capabilities_for_client(&session_id, &client_id, &capabilities, cx); + store.update_capabilities_for_client(&session_id, client_id, &capabilities, cx); }) }) } + pub fn configuration_done( + &self, + client_id: DebugAdapterClientId, + cx: &mut Context, + ) -> Task> { + let Some(client) = self + .client_by_id(client_id, cx) + .and_then(|(_, client)| client.read(cx).adapter_client()) + else { + return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + }; + + if self + .capabilities_by_id(client_id, cx) + .map(|caps| caps.supports_configuration_done_request) + .flatten() + .unwrap_or_default() + { + cx.background_executor().spawn(async move { + client + .request::(dap::ConfigurationDoneArguments) + .await + }) + } else { + Task::ready(Ok(())) + } + } + pub fn launch( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, cx: &mut Context, ) -> Task> { - let Some((session, client)) = self.client_by_id(client_id, cx) else { + let Some((session, client)) = self + .client_by_id(client_id, cx) + .and_then(|(session, client)| Some((session, client.read(cx).adapter_client()?))) + else { return Task::ready(Err(anyhow!( "Could not find debug client: {:?} for session {:?}", client_id, @@ -844,11 +876,14 @@ impl DapStore { pub fn attach( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, process_id: u32, cx: &mut Context, ) -> Task> { - let Some((session, client)) = self.client_by_id(client_id, cx) else { + let Some((session, client)) = self + .client_by_id(client_id, cx) + .and_then(|(session, client)| Some((session, client.read(cx).adapter_client()?))) + else { return Task::ready(Err(anyhow!( "Could not find debug client: {:?} for session {:?}", client_id, @@ -884,156 +919,18 @@ impl DapStore { }) } - pub fn modules( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) -> Task>> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Client was not found"))); - }; - - if !self - .capabilities_by_id(client_id, cx) - .map(|caps| caps.supports_modules_request) - .flatten() - .unwrap_or_default() - { - return Task::ready(Ok(Vec::default())); - } - - cx.background_executor().spawn(async move { - Ok(client - .request::(ModulesArguments { - start_module: None, - module_count: None, - }) - .await? - .modules) - }) - } - - pub fn loaded_sources( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) -> Task>> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Client was not found"))); - }; - - if !self - .capabilities_by_id(client_id, cx) - .map(|caps| caps.supports_loaded_sources_request) - .flatten() - .unwrap_or_default() - { - return Task::ready(Ok(Vec::default())); - } - - cx.background_executor().spawn(async move { - Ok(client - .request::(LoadedSourcesArguments {}) - .await? - .sources) - }) - } - - pub fn stack_frames( - &mut self, - client_id: &DebugAdapterClientId, - thread_id: u64, - cx: &mut Context, - ) -> Task>> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Client was not found"))); - }; - - cx.background_executor().spawn(async move { - Ok(client - .request::(StackTraceArguments { - thread_id, - start_frame: None, - levels: None, - format: None, - }) - .await? - .stack_frames) - }) - } - - pub fn restart_stack_frame( - &mut self, - client_id: &DebugAdapterClientId, - stack_frame_id: u64, - cx: &mut Context, - ) -> Task> { - if !self - .capabilities_by_id(client_id, cx) - .map(|caps| caps.supports_restart_frame) - .flatten() - .unwrap_or_default() - { - return Task::ready(Ok(())); - } - - self.request_dap(client_id, RestartStackFrameCommand { stack_frame_id }, cx) - } - - pub fn scopes( - &mut self, - client_id: &DebugAdapterClientId, - stack_frame_id: u64, - cx: &mut Context, - ) -> Task>> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Client was not found"))); - }; - - cx.background_executor().spawn(async move { - Ok(client - .request::(ScopesArguments { - frame_id: stack_frame_id, - }) - .await? - .scopes) - }) - } - - pub fn configuration_done( - &self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - - if self - .capabilities_by_id(client_id, cx) - .map(|caps| caps.supports_configuration_done_request) - .flatten() - .unwrap_or_default() - { - cx.background_executor().spawn(async move { - client - .request::(ConfigurationDoneArguments) - .await - }) - } else { - Task::ready(Ok(())) - } - } - pub fn respond_to_start_debugging( &mut self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, seq: u64, args: Option, cx: &mut Context, ) -> Task> { - let Some((session, client)) = self.client_by_id(client_id, cx) else { + let Some((session, client)) = self + .client_by_id(client_id, cx) + .and_then(|(session, client)| Some((session, client.read(cx).adapter_client()?))) + else { return Task::ready(Err(anyhow!( "Could not find debug client: {:?} for session {:?}", client_id, @@ -1158,13 +1055,16 @@ impl DapStore { pub fn respond_to_run_in_terminal( &self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, success: bool, seq: u64, body: Option, cx: &mut Context, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { + let Some(client) = self + .client_by_id(client_id, cx) + .and_then(|(_, client)| client.read(cx).adapter_client()) + else { return Task::ready(Err(anyhow!( "Could not find debug client: {:?} for session {:?}", client_id, @@ -1185,231 +1085,6 @@ impl DapStore { }) } - pub fn continue_thread( - &self, - client_id: &DebugAdapterClientId, - thread_id: u64, - cx: &mut Context, - ) -> Task> { - let command = ContinueCommand { - args: ContinueArguments { - thread_id, - single_thread: Some(true), - }, - }; - - self.request_dap(client_id, command, cx) - } - - pub(crate) fn request_dap( - &self, - client_id: &DebugAdapterClientId, - request: R, - cx: &mut Context, - ) -> Task> - where - ::Response: 'static, - ::Arguments: 'static + Send, - { - if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { - return self.send_proto_client_request::( - upstream_client, - upstream_project_id, - client_id, - request, - cx, - ); - } - - let Some((session, client)) = self.client_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - - let Some(caps) = session - .read(cx) - .client_state(*client_id) - .map(|state| state.read(cx).capabilities.clone()) - else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - if !request.is_supported(&caps) { - return Task::ready(Err(anyhow!( - "Request {} is not supported", - R::DapRequest::COMMAND - ))); - } - - let client_id = *client_id; - let request = Arc::new(request); - - let request_clone = request.clone(); - let request_task = cx.background_executor().spawn(async move { - let args = request_clone.to_dap(); - client.request::(args).await - }); - - cx.spawn(|this, mut cx| async move { - let response = request.response_from_dap(request_task.await?); - request.handle_response(this, &client_id, response, &mut cx) - }) - } - - fn send_proto_client_request( - &self, - upstream_client: AnyProtoClient, - upstream_project_id: u64, - client_id: &DebugAdapterClientId, - request: R, - cx: &mut Context, - ) -> Task> { - let message = request.to_proto(&client_id, upstream_project_id); - cx.background_executor().spawn(async move { - let response = upstream_client.request(message).await?; - request.response_from_proto(response) - }) - } - - pub fn step_over( - &self, - client_id: &DebugAdapterClientId, - thread_id: u64, - granularity: SteppingGranularity, - cx: &mut Context, - ) -> Task> { - let Some(capabilities) = self.capabilities_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - let supports_single_thread_execution_requests = capabilities - .supports_single_thread_execution_requests - .unwrap_or_default(); - let supports_stepping_granularity = capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = NextCommand { - inner: StepCommand { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }, - }; - - self.request_dap(client_id, command, cx) - } - - pub fn step_in( - &self, - client_id: &DebugAdapterClientId, - thread_id: u64, - granularity: SteppingGranularity, - cx: &mut Context, - ) -> Task> { - let Some(capabilities) = self.capabilities_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - let supports_single_thread_execution_requests = capabilities - .supports_single_thread_execution_requests - .unwrap_or_default(); - let supports_stepping_granularity = capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepInCommand { - inner: StepCommand { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }, - }; - - self.request_dap(client_id, command, cx) - } - - pub fn step_out( - &self, - client_id: &DebugAdapterClientId, - thread_id: u64, - granularity: SteppingGranularity, - cx: &mut Context, - ) -> Task> { - let Some(capabilities) = self.capabilities_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - let supports_single_thread_execution_requests = capabilities - .supports_single_thread_execution_requests - .unwrap_or_default(); - let supports_stepping_granularity = capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepOutCommand { - inner: StepCommand { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }, - }; - - self.request_dap(client_id, command, cx) - } - - pub fn step_back( - &self, - client_id: &DebugAdapterClientId, - thread_id: u64, - granularity: SteppingGranularity, - cx: &mut Context, - ) -> Task> { - let Some(capabilities) = self.capabilities_by_id(client_id, cx) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); - }; - if !capabilities.supports_step_back.unwrap_or_default() { - return Task::ready(Ok(())); - } - - let supports_single_thread_execution_requests = capabilities - .supports_single_thread_execution_requests - .unwrap_or_default(); - let supports_stepping_granularity = capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepBackCommand { - inner: StepCommand { - thread_id, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests.then(|| true), - }, - }; - - self.request_dap(client_id, command, cx) - } - #[allow(clippy::too_many_arguments)] - pub fn variables( - &self, - client_id: &DebugAdapterClientId, - thread_id: u64, - stack_frame_id: u64, - scope_id: u64, - session_id: DebugSessionId, - variables_reference: u64, - cx: &mut Context, - ) -> Task>> { - let command = VariablesCommand { - stack_frame_id, - scope_id, - session_id, - thread_id, - variables_reference, - filter: None, - start: None, - count: None, - format: None, - }; - - self.request_dap(&client_id, command, cx) - } - pub fn evaluate( &self, client_id: &DebugAdapterClientId, @@ -1419,7 +1094,10 @@ impl DapStore { source: Option, cx: &mut Context, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { + let Some(client) = self + .client_by_id(client_id, cx) + .and_then(|(_, client)| client.read(cx).adapter_client()) + else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1446,7 +1124,10 @@ impl DapStore { completion_column: u64, cx: &mut Context, ) -> Task>> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { + let Some(client) = self + .client_by_id(client_id, cx) + .and_then(|(_, client)| client.read(cx).adapter_client()) + else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1474,7 +1155,10 @@ impl DapStore { evaluate_name: Option, cx: &mut Context, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { + let Some(client) = self + .client_by_id(client_id, cx) + .and_then(|(_, client)| client.read(cx).adapter_client()) + else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1509,76 +1193,10 @@ impl DapStore { }) } - pub fn pause_thread( - &mut self, - client_id: &DebugAdapterClientId, - thread_id: u64, - cx: &mut Context, - ) -> Task> { - self.request_dap(client_id, PauseCommand { thread_id }, cx) - } - - pub fn terminate_threads( - &mut self, - session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, - thread_ids: Option>, - cx: &mut Context, - ) -> Task> { - if self - .capabilities_by_id(client_id, cx) - .map(|caps| caps.supports_terminate_threads_request) - .flatten() - .unwrap_or_default() - { - self.request_dap(client_id, TerminateThreadsCommand { thread_ids }, cx) - } else { - self.shutdown_session(session_id, cx) - } - } - - pub fn disconnect_client( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) -> Task> { - let command = DisconnectCommand { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }; - - self.request_dap(client_id, command, cx) - } - - pub fn restart( - &mut self, - client_id: &DebugAdapterClientId, - args: Option, - cx: &mut Context, - ) -> Task> { - let supports_restart = self - .capabilities_by_id(client_id, cx) - .map(|caps| caps.supports_restart_request) - .flatten() - .unwrap_or_default(); - - if supports_restart { - let command = RestartCommand { - raw: args.unwrap_or(Value::Null), - }; - - self.request_dap(client_id, command, cx) - } else { - let command = DisconnectCommand { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }; - - self.request_dap(client_id, command, cx) - } - } + // .. get the client and what not + // let _ = client.modules(); // This can fire a request to a dap adapter or be a cheap getter. + // client.wait_for_request(request::Modules); // This ensures that the request that we've fired off runs to completions + // let returned_value = client.modules(); // this is a cheap getter. pub fn shutdown_sessions(&mut self, cx: &mut Context) -> Task<()> { let Some(_) = self.as_local() else { @@ -1633,89 +1251,12 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); }; - let Some(local_session) = session.read(cx).as_local() else { - return Task::ready(Err(anyhow!( - "Cannot shutdown session on remote side: {:?}", - session_id - ))); - }; - - let mut tasks = Vec::new(); - for client in local_session.clients().collect::>() { - tasks.push(self.shutdown_client(&session, client, cx)); - } - - if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { - downstream_client - .send(proto::DebuggerSessionEnded { - project_id: *project_id, - session_id: session_id.to_proto(), - }) - .log_err(); - } - - cx.background_executor().spawn(async move { - futures::future::join_all(tasks).await; - Ok(()) - }) - } - - fn shutdown_client( - &mut self, - session: &Entity, - client: Arc, - cx: &mut Context, - ) -> Task> { - let client_id = client.id(); - - cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); - let Some(capabilities) = self.session_by_client_id(&client_id).and_then(|session| { - session - .read(cx) - .client_state(client_id) - .map(|state| state.read(cx).capabilities.clone()) - }) else { - return Task::ready(Err(anyhow!("Client not found"))); - }; - let session = session.clone(); - self.client_by_session.remove(&client_id); - if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { - downstream_client - .send(proto::ShutdownDebugClient { - session_id: session.read(cx).id().to_proto(), - client_id: client_id.to_proto(), - project_id: *project_id, - }) - .log_err(); - } - - cx.spawn(|_, mut cx| async move { - if capabilities.supports_terminate_request.unwrap_or_default() { - let _ = client - .request::(TerminateArguments { - restart: Some(false), - }) - .await - .log_err(); - } else { - let _ = client - .request::(DisconnectArguments { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }) - .await - .log_err(); - } - - client.shutdown().await?; - - let _ = session.update(&mut cx, |this, _| { - this.states.remove(&client_id); + for client_id in session.read(cx).client_ids().collect::>() { + session.update(cx, |this, cx| { + this.shutdown_client(client_id, cx); }); - - Ok(()) - }) + } + Task::ready(Ok(())) } pub fn request_active_debug_sessions(&mut self, cx: &mut Context) { @@ -1766,7 +1307,7 @@ impl DapStore { self.update_capabilities_for_client( &session_id, - &client, + client, &dap::proto_conversions::capabilities_from_proto( &debug_client.capabilities.unwrap_or_default(), ), @@ -1834,7 +1375,7 @@ impl DapStore { let client_id = T::client_id_from_proto(&envelope.payload); let _state = this.update(&mut cx, |this, cx| { - this.session_by_client_id(&client_id)? + this.session_by_client_id(client_id)? .read(cx) .client_state(client_id)? .read(cx) @@ -1844,27 +1385,27 @@ impl DapStore { todo!() } - async fn handle_dap_command( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<::Response> - where - ::Arguments: Send, - ::Response: Send, - { - let _sender_id = envelope.original_sender_id().unwrap_or_default(); - let client_id = T::client_id_from_proto(&envelope.payload); - - let request = T::from_proto(&envelope.payload); - let response = this - .update(&mut cx, |this, cx| { - this.request_dap::(&client_id, request, cx) - })? - .await?; - - Ok(T::response_to_proto(&client_id, response)) - } + // async fn handle_dap_command( + // this: Entity, + // envelope: TypedEnvelope, + // mut cx: AsyncApp, + // ) -> Result<::Response> + // where + // ::Arguments: Send, + // ::Response: Send, + // { + // let _sender_id = envelope.original_sender_id().unwrap_or_default(); + // let client_id = T::client_id_from_proto(&envelope.payload); + + // let request = T::from_proto(&envelope.payload); + // let response = this + // .update(&mut cx, |this, cx| { + // this.request_dap::(&client_id, request, cx) + // })? + // .await?; + + // Ok(T::response_to_proto(&client_id, response)) + // } async fn handle_synchronize_breakpoints( this: Entity, @@ -1939,7 +1480,7 @@ impl DapStore { this.update(&mut cx, |dap_store, cx| { dap_store.update_capabilities_for_client( &DebugSessionId::from_proto(envelope.payload.session_id), - &DebugAdapterClientId::from_proto(envelope.payload.client_id), + DebugAdapterClientId::from_proto(envelope.payload.client_id), &dap::proto_conversions::capabilities_from_proto(&envelope.payload), cx, ); @@ -1954,7 +1495,7 @@ impl DapStore { this.update(&mut cx, |dap_store, cx| { let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); - dap_store.session_by_client_id(&client_id).map(|state| { + dap_store.session_by_client_id(client_id).map(|state| { state.update(cx, |this, _| { this.states.remove(&client_id); }) @@ -2056,14 +1597,17 @@ impl DapStore { pub fn send_breakpoints( &self, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, absolute_file_path: Arc, mut breakpoints: Vec, ignore: bool, source_changed: bool, cx: &Context, ) -> Task> { - let Some((_, client)) = self.client_by_id(client_id, cx) else { + let Some(client) = self + .client_by_id(client_id, cx) + .and_then(|(_, client)| client.read(cx).adapter_client()) + else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -2121,9 +1665,9 @@ impl DapStore { { let session = session.read(cx); let ignore_breakpoints = session.ignore_breakpoints(); - for client in session.as_local().unwrap().clients().collect::>() { + for client_id in session.client_ids().collect::>() { tasks.push(self.send_breakpoints( - &client.id(), + client_id, Arc::from(absolute_path.clone()), source_breakpoints.clone(), ignore_breakpoints, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1673721749b214..565a5a5bcbdc8d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1311,7 +1311,7 @@ impl Project { pub fn initial_send_breakpoints( &self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, cx: &mut Context, ) -> Task<()> { let mut tasks = Vec::new(); @@ -1439,7 +1439,7 @@ impl Project { project .toggle_ignore_breakpoints( &DebugSessionId::from_proto(envelope.payload.session_id), - &DebugAdapterClientId::from_proto(envelope.payload.client_id), + DebugAdapterClientId::from_proto(envelope.payload.client_id), cx, ) .detach_and_log_err(cx); @@ -1450,7 +1450,7 @@ impl Project { pub fn toggle_ignore_breakpoints( &self, session_id: &DebugSessionId, - client_id: &DebugAdapterClientId, + client_id: DebugAdapterClientId, cx: &mut Context, ) -> Task> { let tasks = self.dap_store.update(cx, |store, cx| { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index e871715f0b7dbb..1489618e21ee38 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -355,7 +355,19 @@ message Envelope { DapLoadedSourcesRequest dap_loaded_sources_request = 331; DapLoadedSourcesResponse dap_loaded_sources_response = 332; ActiveDebugSessionsRequest active_debug_sessions_request = 333; - ActiveDebugSessionsResponse active_debug_sessions_response = 334; // current max + ActiveDebugSessionsResponse active_debug_sessions_response = 334; + DapStackTraceRequest dap_stack_trace_request = 335; + DapStackTraceResponse dap_stack_trace_response = 336; + DapScopesRequest dap_scopes_request = 337; + DapScopesResponse dap_scopes_response = 338; + DapSetVariableValueRequest dap_set_variable_value_request = 339; + DapSetVariableValueResponse dap_set_variable_value_response = 340; + DapEvaluateRequest dap_evaluate_request = 341; + DapEvaluateResponse dap_evaluate_response = 342; + DapCompletionRequest dap_completion_request = 343; + DapCompletionResponse dap_completion_response = 344; + DapThreadsRequest dap_threads_request = 345; + DapThreadsResponse dap_threads_response = 346; // current max } reserved 87 to 88; @@ -2708,7 +2720,6 @@ message VariableListVariables { message DebuggerVariableList { repeated VariableListScopes scopes = 1; repeated VariableListVariables variables = 2; - repeated AddToVariableList added_variables = 3; } enum VariablesArgumentsFilter { @@ -2726,20 +2737,13 @@ message VariablesRequest { uint64 thread_id = 3; uint64 session_id = 4; uint64 stack_frame_id = 5; - uint64 scope_id = 6; - uint64 variables_reference = 7; - optional VariablesArgumentsFilter filter = 8; - optional uint64 start = 9; - optional uint64 count = 10; - optional ValueFormat format = 11; + uint64 variables_reference = 6; + optional VariablesArgumentsFilter filter = 7; + optional uint64 start = 8; + optional uint64 count = 9; + optional ValueFormat format = 10; } -message AddToVariableList { - uint64 variable_id = 1; - uint64 stack_frame_id = 2; - uint64 scope_id = 3; - repeated DapVariable variables = 4; -} message DebuggerStackFrameList { uint64 thread_id = 1; @@ -2754,6 +2758,110 @@ enum SteppingGranularity { Instruction = 2; } +enum DapEvaluateContext { + Repl = 0; + Watch = 1; + Hover = 2; + Clipboard = 3; + EvaluateVariables = 4; + EvaluateUnknown = 5; +} + +message DapEvaluateRequest { + uint64 project_id = 1; + uint64 client_id = 2; + string expression = 3; + optional uint64 frame_id = 4; + optional DapEvaluateContext context = 5; +} + +message DapEvaluateResponse { + string result = 1; + optional string evaluate_type = 2; + uint64 variable_reference = 3; + optional uint64 named_variables = 4; + optional uint64 indexed_variables = 5; + optional string memory_reference = 6; +} + + +message DapCompletionRequest { + uint64 project_id = 1; + uint64 client_id = 2; + string query = 3; + optional uint64 frame_id = 4; + optional uint64 line = 5; + uint64 column = 6; +} + +enum DapCompletionItemType { + Method = 0; + Function = 1; + Constructor = 2; + Field = 3; + Variable = 4; + Class = 5; + Interface = 6; + Module = 7; + Property = 8; + Unit = 9; + Value = 10; + Enum = 11; + Keyword = 12; + Snippet = 13; + Text = 14; + Color = 15; + CompletionItemFile = 16; + Reference = 17; + Customcolor = 19; +} + +message DapCompletionItem { + string label = 1; + optional string text = 2; + optional string sort_text = 3; + optional string detail = 4; + optional DapCompletionItemType typ = 5; + optional uint64 start = 6; + optional uint64 length = 7; + optional uint64 selection_start = 8; + optional uint64 selection_length = 9; +} + +message DapCompletionResponse { + uint64 client_id = 1; + repeated DapCompletionItem completions = 2; +} + +message DapScopesRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + uint64 stack_frame_id = 4; +} + +message DapScopesResponse { + repeated DapScope scopes = 1; +} + +message DapSetVariableValueRequest { + uint64 project_id = 1; + uint64 client_id = 2; + string name = 3; + string value = 4; + uint64 variables_reference = 5; +} + +message DapSetVariableValueResponse { + uint64 client_id = 1; + string value = 2; + optional string variable_type = 3; + optional uint64 variables_reference = 4; + optional uint64 named_variables = 5; + optional uint64 indexed_variables = 6; + optional string memory_reference = 7; +} + message DapPauseRequest { uint64 project_id = 1; uint64 client_id = 2; @@ -2774,6 +2882,15 @@ message DapTerminateThreadsRequest { repeated uint64 thread_ids = 3; } +message DapThreadsRequest { + uint64 project_id = 1; + uint64 client_id = 2; +} + +message DapThreadsResponse { + repeated DapThread threads = 1; +} + message DapTerminateRequest { uint64 project_id = 1; uint64 client_id = 2; @@ -2874,6 +2991,18 @@ message DapLoadedSourcesResponse { repeated DapSource sources = 2; } +message DapStackTraceRequest { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 thread_id = 3; + optional uint64 start_frame = 4; + optional uint64 stack_trace_levels = 5; +} + +message DapStackTraceResponse { + repeated DapStackFrame frames = 1; +} + message DapStackFrame { uint64 id = 1; string name = 2; @@ -2926,9 +3055,8 @@ message UpdateDebugAdapter { DebuggerThreadState thread_state = 5; DebuggerStackFrameList stack_frame_list = 6; DebuggerVariableList variable_list = 7; - AddToVariableList add_to_variable_list = 8; - DebuggerModuleList modules = 9; - DapOutputEvent output_event = 10; + DebuggerModuleList modules = 8; + DapOutputEvent output_event = 9; } } @@ -2952,6 +3080,11 @@ message DapVariable { optional string memory_reference = 9; } +message DapThread { + uint64 id = 1; + string name = 2; +} + message DapScope { string name = 1; optional DapScopePresentationHint presentation_hint = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index dc8c2fe2978ed2..d7fc560522f0e1 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -476,6 +476,18 @@ messages!( (DebuggerSessionEnded, Background), (ActiveDebugSessionsRequest, Foreground), (ActiveDebugSessionsResponse, Foreground), + (DapStackTraceRequest, Background), + (DapStackTraceResponse, Background), + (DapScopesRequest, Background), + (DapScopesResponse, Background), + (DapSetVariableValueRequest, Background), + (DapSetVariableValueResponse, Background), + (DapEvaluateRequest, Background), + (DapEvaluateResponse, Background), + (DapCompletionRequest, Background), + (DapCompletionResponse, Background), + (DapThreadsRequest, Background), + (DapThreadsResponse, Background), ); request_messages!( @@ -627,7 +639,13 @@ request_messages!( (DapRestartStackFrameRequest, Ack), (DapShutdownSession, Ack), (VariablesRequest, DapVariables), - (ActiveDebugSessionsRequest, ActiveDebugSessionsResponse) + (ActiveDebugSessionsRequest, ActiveDebugSessionsResponse), + (DapStackTraceRequest, DapStackTraceResponse), + (DapScopesRequest, DapScopesResponse), + (DapSetVariableValueRequest, DapSetVariableValueResponse), + (DapEvaluateRequest, DapEvaluateResponse), + (DapCompletionRequest, DapCompletionResponse), + (DapThreadsRequest, DapThreadsResponse), ); entity_messages!( @@ -748,6 +766,12 @@ entity_messages!( ToggleIgnoreBreakpoints, DebuggerSessionEnded, ActiveDebugSessionsRequest, + DapStackTraceRequest, + DapScopesRequest, + DapSetVariableValueRequest, + DapEvaluateRequest, + DapCompletionRequest, + DapThreadsRequest, ); entity_messages!( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 3a8549bdb0fe56..2c84862c24bf27 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -9,7 +9,7 @@ use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry use node_runtime::NodeRuntime; use project::{ buffer_store::{BufferStore, BufferStoreEvent}, - dap_store::DapStore, + debugger::dap_store::DapStore, git::GitStore, project_settings::SettingsObserver, search::SearchQuery, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 37377ce4017b37..ce658982b06cee 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4208,7 +4208,7 @@ mod tests { repl::init(app_state.fs.clone(), cx); repl::notebook::init(cx); tasks_ui::init(cx); - project::dap_store::DapStore::init(&app_state.client.clone().into()); + project::debugger::dap_store::DapStore::init(&app_state.client.clone().into()); debugger_ui::init(cx); initialize_workspace(app_state.clone(), prompt_builder, cx); search::init(cx); From 91e60d79bb5cfeff9e503bbdcaa6a9148502f60e Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 14 Feb 2025 07:55:03 -0500 Subject: [PATCH 541/650] Add `BreakpointStore` to debugger crate (#114) * Initial setup for breakpoint store * WIP Move more methods to breakpoint store * Move event handler to breakpoint store * Fix compiler errrors * Fix more compiler errors * Get Zed to compile --------- Co-authored-by: Remco Smits --- crates/editor/src/editor.rs | 27 +- crates/editor/src/element.rs | 2 +- crates/project/src/buffer_store.rs | 16 +- crates/project/src/debugger.rs | 1 + .../project/src/debugger/breakpoint_store.rs | 548 ++++++++++++++++++ crates/project/src/debugger/dap_session.rs | 2 +- crates/project/src/debugger/dap_store.rs | 478 +-------------- crates/project/src/lsp_store.rs | 8 +- crates/project/src/project.rs | 160 +++-- crates/project_panel/src/project_panel.rs | 4 +- crates/remote_server/src/headless_project.rs | 7 +- crates/workspace/src/persistence.rs | 2 +- crates/workspace/src/persistence/model.rs | 2 +- crates/workspace/src/workspace.rs | 29 +- 14 files changed, 742 insertions(+), 544 deletions(-) create mode 100644 crates/project/src/debugger/breakpoint_store.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3e4b7648430580..172e27ea441b68 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -108,7 +108,7 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange} use linked_editing_ranges::refresh_linked_ranges; use mouse_context_menu::MouseContextMenu; use project::{ - debugger::dap_store::{BreakpointEditAction, DapStoreEvent}, + debugger::breakpoint_store::{BreakpointEditAction, BreakpointStoreEvent}, ProjectPath, }; pub use proposed_changes_editor::{ @@ -136,7 +136,10 @@ use multi_buffer::{ }; use parking_lot::Mutex; use project::{ - debugger::dap_store::{Breakpoint, BreakpointKind, DapStore}, + debugger::{ + breakpoint_store::{Breakpoint, BreakpointKind}, + dap_store::DapStore, + }, lsp_store::{FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, project_settings::{GitGutterSetting, ProjectSettings}, CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, @@ -5546,7 +5549,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); - let breakpoints = dap_store.read(cx).breakpoints(); + let breakpoints = &dap_store.read(cx).breakpoint_store().read(cx).breakpoints; if let Some(buffer) = self.buffer.read(cx).as_singleton() { let buffer = buffer.read(cx); @@ -7356,8 +7359,12 @@ impl Editor { .summary_for_anchor::(&breakpoint_position) .row; - let bp = self.dap_store.clone()?.read_with(cx, |store, _cx| { - store.breakpoint_at_row(row, &project_path, buffer_snapshot) + let bp = self.dap_store.clone()?.read_with(cx, |dap_store, cx| { + dap_store.breakpoint_store().read(cx).breakpoint_at_row( + row, + &project_path, + buffer_snapshot, + ) })?; Some((bp.active_position?, bp.kind)) @@ -14725,10 +14732,12 @@ impl Editor { if let Some(dap_store) = &self.dap_store { if let Some(project_path) = self.project_path(cx) { - dap_store.update(cx, |_, cx| { - cx.emit(DapStoreEvent::BreakpointsChanged { - project_path, - source_changed: true, + dap_store.update(cx, |dap_store, cx| { + dap_store.breakpoint_store().update(cx, |_, cx| { + cx.emit(BreakpointStoreEvent::BreakpointsChanged { + project_path, + source_changed: true, + }); }); }); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7fc82faf65b133..90beb72e3ed2a6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -55,7 +55,7 @@ use multi_buffer::{ RowInfo, ToOffset, }; use project::{ - debugger::dap_store::{Breakpoint, BreakpointKind}, + debugger::breakpoint_store::{Breakpoint, BreakpointKind}, project_settings::{GitGutterSetting, ProjectSettings}, }; use settings::{KeyBindingValidator, KeyBindingValidatorRegistration, Settings}; diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index eb701c9ce444eb..dbd66df87f98bf 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1,5 +1,5 @@ use crate::{ - debugger::dap_store::DapStore, + debugger::breakpoint_store::BreakpointStore, lsp_store::OpenLspBufferHandle, search::SearchQuery, worktree_store::{WorktreeStore, WorktreeStoreEvent}, @@ -317,7 +317,7 @@ struct RemoteBufferStore { struct LocalBufferStore { local_buffer_ids_by_path: HashMap, local_buffer_ids_by_entry_id: HashMap, - dap_store: Entity, + breakpoint_store: Entity, worktree_store: Entity, _subscription: Subscription, } @@ -1242,14 +1242,14 @@ impl BufferStore { /// Creates a buffer store, optionally retaining its buffers. pub fn local( worktree_store: Entity, - dap_store: Entity, + breakpoint_store: Entity, cx: &mut Context, ) -> Self { Self { state: BufferStoreState::Local(LocalBufferStore { local_buffer_ids_by_path: Default::default(), local_buffer_ids_by_entry_id: Default::default(), - dap_store, + breakpoint_store, worktree_store: worktree_store.clone(), _subscription: cx.subscribe(&worktree_store, |this, _, event, cx| { if let WorktreeStoreEvent::WorktreeAdded(worktree) = event { @@ -1291,14 +1291,14 @@ impl BufferStore { } } - pub fn dap_on_buffer_open( + pub fn breakpoint_store_on_buffer_open( &mut self, project_path: &ProjectPath, buffer: &Entity, cx: &mut Context, ) { if let Some(local_store) = self.as_local_mut() { - local_store.dap_store.update(cx, |store, cx| { + local_store.breakpoint_store.update(cx, |store, cx| { store.on_open_buffer(&project_path, buffer, cx); }); } @@ -1338,7 +1338,7 @@ impl BufferStore { cx: &mut Context, ) -> Task>> { if let Some(buffer) = self.get_by_path(&project_path, cx) { - self.dap_on_buffer_open(&project_path, &buffer, cx); + self.breakpoint_store_on_buffer_open(&project_path, &buffer, cx); return Task::ready(Ok(buffer)); } @@ -1368,7 +1368,7 @@ impl BufferStore { this.loading_buffers.remove(&project_path); let buffer = load_result.map_err(Arc::new)?; - this.dap_on_buffer_open(&project_path, &buffer, cx); + this.breakpoint_store_on_buffer_open(&project_path, &buffer, cx); Ok(buffer) })? }) diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index fc2a0107a270a1..1ca4f576c1f8d0 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -1,3 +1,4 @@ +pub mod breakpoint_store; pub mod dap_command; pub mod dap_session; pub mod dap_store; diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs new file mode 100644 index 00000000000000..c71d533bf68094 --- /dev/null +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -0,0 +1,548 @@ +use crate::{ProjectItem as _, ProjectPath}; +use anyhow::{Context as _, Result}; +use collections::{BTreeMap, HashSet}; +use dap::SourceBreakpoint; +use gpui::{AsyncApp, Context, Entity, EventEmitter}; +use language::{ + proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, + Buffer, BufferSnapshot, +}; +use rpc::{proto, AnyProtoClient, TypedEnvelope}; +use settings::WorktreeId; +use std::{ + hash::{Hash, Hasher}, + path::Path, + sync::Arc, +}; +use text::Point; +use util::ResultExt as _; + +struct RemoteBreakpointStore { + upstream_client: Option, + upstream_project_id: u64, +} + +enum BreakpointMode { + Local, + Remote(RemoteBreakpointStore), +} + +pub struct BreakpointStore { + pub breakpoints: BTreeMap>, + downstream_client: Option<(AnyProtoClient, u64)>, + mode: BreakpointMode, +} + +pub enum BreakpointStoreEvent { + BreakpointsChanged { + project_path: ProjectPath, + source_changed: bool, + }, +} + +impl EventEmitter for BreakpointStore {} + +impl BreakpointStore { + pub fn init(client: &AnyProtoClient) { + client.add_entity_message_handler(Self::handle_synchronize_breakpoints); + } + + pub fn local() -> Self { + BreakpointStore { + breakpoints: BTreeMap::new(), + mode: BreakpointMode::Local, + downstream_client: None, + } + } + + pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self { + BreakpointStore { + breakpoints: BTreeMap::new(), + mode: BreakpointMode::Remote(RemoteBreakpointStore { + upstream_client: Some(upstream_client), + upstream_project_id, + }), + downstream_client: None, + } + } + + pub fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) { + self.downstream_client = Some((downstream_client.clone(), project_id)); + + for (project_path, breakpoints) in self.breakpoints.iter() { + downstream_client + .send(proto::SynchronizeBreakpoints { + project_id, + project_path: Some(project_path.to_proto()), + breakpoints: breakpoints + .iter() + .filter_map(|breakpoint| breakpoint.to_proto()) + .collect(), + }) + .log_err(); + } + } + + pub fn unshared(&mut self, cx: &mut Context) { + self.downstream_client.take(); + + cx.notify(); + } + + pub fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> { + match &self.mode { + BreakpointMode::Remote(RemoteBreakpointStore { + upstream_client: Some(upstream_client), + upstream_project_id, + .. + }) => Some((upstream_client.clone(), *upstream_project_id)), + + BreakpointMode::Remote(RemoteBreakpointStore { + upstream_client: None, + .. + }) => None, + BreakpointMode::Local => None, + } + } + + pub fn set_breakpoints_from_proto( + &mut self, + breakpoints: Vec, + cx: &mut Context, + ) { + let mut new_breakpoints = BTreeMap::new(); + for project_breakpoints in breakpoints { + let Some(project_path) = project_breakpoints.project_path else { + continue; + }; + + new_breakpoints.insert( + ProjectPath::from_proto(project_path), + project_breakpoints + .breakpoints + .into_iter() + .filter_map(Breakpoint::from_proto) + .collect::>(), + ); + } + + std::mem::swap(&mut self.breakpoints, &mut new_breakpoints); + cx.notify(); + } + + pub fn on_open_buffer( + &mut self, + project_path: &ProjectPath, + buffer: &Entity, + cx: &mut Context, + ) { + let entry = self.breakpoints.remove(project_path).unwrap_or_default(); + let mut set_bp: HashSet = HashSet::default(); + + let buffer = buffer.read(cx); + + for mut bp in entry.into_iter() { + bp.set_active_position(&buffer); + set_bp.insert(bp); + } + + self.breakpoints.insert(project_path.clone(), set_bp); + + cx.emit(BreakpointStoreEvent::BreakpointsChanged { + project_path: project_path.clone(), + source_changed: true, + }); + cx.notify(); + } + + pub fn on_file_rename( + &mut self, + old_project_path: ProjectPath, + new_project_path: ProjectPath, + cx: &mut Context, + ) { + if let Some(breakpoints) = self.breakpoints.remove(&old_project_path) { + self.breakpoints + .insert(new_project_path.clone(), breakpoints); + + cx.emit(BreakpointStoreEvent::BreakpointsChanged { + project_path: new_project_path, + source_changed: false, + }); + cx.notify(); + } + } + + pub fn sync_open_breakpoints_to_closed_breakpoints( + &mut self, + buffer: &Entity, + cx: &mut Context, + ) { + let Some(project_path) = buffer.read(cx).project_path(cx) else { + return; + }; + + if let Some(breakpoint_set) = self.breakpoints.remove(&project_path) { + let breakpoint_iter = breakpoint_set.into_iter().map(|mut breakpoint| { + breakpoint.cached_position = breakpoint.point_for_buffer(buffer.read(cx)).row; + breakpoint.active_position = None; + breakpoint + }); + + self.breakpoints.insert( + project_path.clone(), + breakpoint_iter.collect::>(), + ); + + cx.emit(BreakpointStoreEvent::BreakpointsChanged { + project_path, + source_changed: false, + }); + cx.notify(); + } + } + + pub fn breakpoint_at_row( + &self, + row: u32, + project_path: &ProjectPath, + buffer_snapshot: BufferSnapshot, + ) -> Option { + let breakpoint_set = self.breakpoints.get(project_path)?; + + breakpoint_set + .iter() + .find(|breakpoint| breakpoint.point_for_buffer_snapshot(&buffer_snapshot).row == row) + .cloned() + } + + pub fn toggle_breakpoint_for_buffer( + &mut self, + project_path: &ProjectPath, + mut breakpoint: Breakpoint, + edit_action: BreakpointEditAction, + cx: &mut Context, + ) { + let upstream_client = self.upstream_client(); + + let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); + + match edit_action { + BreakpointEditAction::Toggle => { + if !breakpoint_set.remove(&breakpoint) { + breakpoint_set.insert(breakpoint); + } + } + BreakpointEditAction::EditLogMessage(log_message) => { + if !log_message.is_empty() { + breakpoint.kind = BreakpointKind::Log(log_message.clone()); + breakpoint_set.remove(&breakpoint); + breakpoint_set.insert(breakpoint); + } else if matches!(&breakpoint.kind, BreakpointKind::Log(_)) { + breakpoint_set.remove(&breakpoint); + } + } + } + + if let Some((client, project_id)) = upstream_client.or(self.downstream_client.clone()) { + client + .send(client::proto::SynchronizeBreakpoints { + project_id, + project_path: Some(project_path.to_proto()), + breakpoints: breakpoint_set + .iter() + .filter_map(|breakpoint| breakpoint.to_proto()) + .collect(), + }) + .log_err(); + } + + if breakpoint_set.is_empty() { + self.breakpoints.remove(project_path); + } + + cx.emit(BreakpointStoreEvent::BreakpointsChanged { + project_path: project_path.clone(), + source_changed: false, + }); + cx.notify(); + } + + pub fn deserialize_breakpoints( + &mut self, + worktree_id: WorktreeId, + serialize_breakpoints: Vec, + ) { + for serialize_breakpoint in serialize_breakpoints { + self.breakpoints + .entry(ProjectPath { + worktree_id, + path: serialize_breakpoint.path.clone(), + }) + .or_default() + .insert(Breakpoint { + active_position: None, + cached_position: serialize_breakpoint.position, + kind: serialize_breakpoint.kind, + }); + } + } + + async fn handle_synchronize_breakpoints( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result<()> { + let project_path = ProjectPath::from_proto( + envelope + .payload + .project_path + .context("Invalid Breakpoint call")?, + ); + + this.update(&mut cx, |store, cx| { + let breakpoints = envelope + .payload + .breakpoints + .into_iter() + .filter_map(Breakpoint::from_proto) + .collect::>(); + + if breakpoints.is_empty() { + store.breakpoints.remove(&project_path); + } else { + store.breakpoints.insert(project_path.clone(), breakpoints); + } + + cx.emit(BreakpointStoreEvent::BreakpointsChanged { + project_path, + source_changed: false, + }); + cx.notify(); + }) + } +} + +type LogMessage = Arc; + +#[derive(Clone, Debug)] +pub enum BreakpointEditAction { + Toggle, + EditLogMessage(LogMessage), +} + +#[derive(Clone, Debug)] +pub enum BreakpointKind { + Standard, + Log(LogMessage), +} + +impl BreakpointKind { + pub fn to_int(&self) -> i32 { + match self { + BreakpointKind::Standard => 0, + BreakpointKind::Log(_) => 1, + } + } + + pub fn log_message(&self) -> Option { + match self { + BreakpointKind::Standard => None, + BreakpointKind::Log(message) => Some(message.clone()), + } + } +} + +impl PartialEq for BreakpointKind { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } +} + +impl Eq for BreakpointKind {} + +impl Hash for BreakpointKind { + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + } +} + +#[derive(Clone, Debug)] +pub struct Breakpoint { + pub active_position: Option, + pub cached_position: u32, + pub kind: BreakpointKind, +} + +// Custom implementation for PartialEq, Eq, and Hash is done +// to get toggle breakpoint to solely be based on a breakpoint's +// location. Otherwise, a user can get in situation's where there's +// overlapping breakpoint's with them being aware. +impl PartialEq for Breakpoint { + fn eq(&self, other: &Self) -> bool { + match (&self.active_position, &other.active_position) { + (None, None) => self.cached_position == other.cached_position, + (None, Some(_)) => false, + (Some(_), None) => false, + (Some(self_position), Some(other_position)) => self_position == other_position, + } + } +} + +impl Eq for Breakpoint {} + +impl Hash for Breakpoint { + fn hash(&self, state: &mut H) { + if self.active_position.is_some() { + self.active_position.hash(state); + } else { + self.cached_position.hash(state); + } + } +} + +impl Breakpoint { + pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { + let line = self + .active_position + .map(|position| buffer.summary_for_anchor::(&position).row) + .unwrap_or(self.cached_position) as u64; + + let log_message = match &self.kind { + BreakpointKind::Standard => None, + BreakpointKind::Log(message) => Some(message.clone().to_string()), + }; + + SourceBreakpoint { + line, + condition: None, + hit_condition: None, + log_message, + column: None, + mode: None, + } + } + + pub fn set_active_position(&mut self, buffer: &Buffer) { + if self.active_position.is_none() { + self.active_position = + Some(buffer.breakpoint_anchor(Point::new(self.cached_position, 0))); + } + } + + pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { + self.active_position + .map(|position| buffer.summary_for_anchor::(&position)) + .unwrap_or(Point::new(self.cached_position, 0)) + } + + pub fn point_for_buffer_snapshot(&self, buffer_snapshot: &BufferSnapshot) -> Point { + self.active_position + .map(|position| buffer_snapshot.summary_for_anchor::(&position)) + .unwrap_or(Point::new(self.cached_position, 0)) + } + + pub fn source_for_snapshot(&self, snapshot: Option<&BufferSnapshot>) -> SourceBreakpoint { + let line = match snapshot { + Some(snapshot) => self + .active_position + .map(|position| snapshot.summary_for_anchor::(&position).row) + .unwrap_or(self.cached_position) as u64, + None => self.cached_position as u64, + }; + + let log_message = match &self.kind { + BreakpointKind::Standard => None, + BreakpointKind::Log(log_message) => Some(log_message.clone().to_string()), + }; + + SourceBreakpoint { + line, + condition: None, + hit_condition: None, + log_message, + column: None, + mode: None, + } + } + + pub fn to_serialized(&self, buffer: Option<&Buffer>, path: Arc) -> SerializedBreakpoint { + match buffer { + Some(buffer) => SerializedBreakpoint { + position: self + .active_position + .map(|position| buffer.summary_for_anchor::(&position).row) + .unwrap_or(self.cached_position), + path, + kind: self.kind.clone(), + }, + None => SerializedBreakpoint { + position: self.cached_position, + path, + kind: self.kind.clone(), + }, + } + } + + pub fn to_proto(&self) -> Option { + Some(client::proto::Breakpoint { + position: if let Some(position) = &self.active_position { + Some(serialize_text_anchor(position)) + } else { + None + }, + cached_position: self.cached_position, + kind: match self.kind { + BreakpointKind::Standard => proto::BreakpointKind::Standard.into(), + BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(), + }, + message: if let BreakpointKind::Log(message) = &self.kind { + Some(message.to_string()) + } else { + None + }, + }) + } + + pub fn from_proto(breakpoint: client::proto::Breakpoint) -> Option { + Some(Self { + active_position: if let Some(position) = breakpoint.position.clone() { + deserialize_anchor(position) + } else { + None + }, + cached_position: breakpoint.cached_position, + kind: match proto::BreakpointKind::from_i32(breakpoint.kind) { + Some(proto::BreakpointKind::Log) => { + BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into()) + } + None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard, + }, + }) + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct SerializedBreakpoint { + pub position: u32, + pub path: Arc, + pub kind: BreakpointKind, +} + +impl SerializedBreakpoint { + pub fn to_source_breakpoint(&self) -> SourceBreakpoint { + let log_message = match &self.kind { + BreakpointKind::Standard => None, + BreakpointKind::Log(message) => Some(message.clone().to_string()), + }; + + SourceBreakpoint { + line: self.position as u64, + condition: None, + hit_condition: None, + log_message, + column: None, + mode: None, + } + } +} diff --git a/crates/project/src/debugger/dap_session.rs b/crates/project/src/debugger/dap_session.rs index ce9bb72ffd3d67..ddfb67817b6edd 100644 --- a/crates/project/src/debugger/dap_session.rs +++ b/crates/project/src/debugger/dap_session.rs @@ -16,7 +16,6 @@ use gpui::{App, AppContext, Context, Entity, Task}; use rpc::AnyProtoClient; use serde_json::Value; use std::borrow::Borrow; -use std::collections::btree_map::Entry as BTreeMapEntry; use std::u64; use std::{ any::Any, @@ -411,6 +410,7 @@ impl Client { .iter() .map(|thread| (ThreadId(thread.id), Thread::from(thread.clone()))), ); + cx.notify(); }, cx, ); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 017764355bcecd..18dd429d3e9665 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1,4 +1,5 @@ use super::{ + breakpoint_store::BreakpointStore, // Will need to uncomment this once we implement rpc message handler again // dap_command::{ // ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, @@ -8,7 +9,7 @@ use super::{ dap_command::DapCommand, dap_session::{self, DebugSession, DebugSessionId}, }; -use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectItem as _, ProjectPath}; +use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectPath}; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; @@ -31,10 +32,7 @@ use fs::Fs; use futures::future::Shared; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; use http_client::HttpClient; -use language::{ - proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, - BinaryStatus, Buffer, BufferSnapshot, LanguageRegistry, LanguageToolchainStore, -}; +use language::{BinaryStatus, BufferSnapshot, LanguageRegistry, LanguageToolchainStore}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; use rpc::{ @@ -46,9 +44,9 @@ use settings::{Settings as _, WorktreeId}; use smol::lock::Mutex; use std::{ borrow::Borrow, - collections::{BTreeMap, HashSet}, + collections::HashSet, ffi::OsStr, - hash::{Hash, Hasher}, + hash::Hash, path::{Path, PathBuf}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -57,7 +55,6 @@ use std::{ }; use std::{collections::VecDeque, sync::atomic::AtomicU32}; use task::{AttachConfig, DebugAdapterConfig, DebugRequestType}; -use text::Point; use util::{merge_json_value_into, ResultExt as _}; use worktree::Worktree; @@ -70,10 +67,6 @@ pub enum DapStoreEvent { message: Message, }, Notification(String), - BreakpointsChanged { - project_path: ProjectPath, - source_changed: bool, - }, ActiveDebugLineChanged, RemoteHasInitialized, SetDebugPanelItem(SetDebuggerPanelItem), @@ -117,7 +110,7 @@ pub struct RemoteDapStore { pub struct DapStore { mode: DapStoreMode, downstream_client: Option<(AnyProtoClient, u64)>, - breakpoints: BTreeMap>, + breakpoint_store: Entity, active_debug_line: Option<(DebugAdapterClientId, ProjectPath, u32)>, sessions: HashMap>, client_by_session: HashMap, @@ -134,7 +127,6 @@ impl DapStore { client.add_entity_message_handler(Self::handle_set_active_debug_line); client.add_entity_message_handler(Self::handle_set_debug_client_capabilities); client.add_entity_message_handler(Self::handle_set_debug_panel_item); - client.add_entity_message_handler(Self::handle_synchronize_breakpoints); client.add_entity_message_handler(Self::handle_update_debug_adapter); client.add_entity_message_handler(Self::handle_update_thread_status); client.add_entity_message_handler(Self::handle_ignore_breakpoint_state); @@ -163,6 +155,7 @@ impl DapStore { language_registry: Arc, environment: Entity, toolchain_store: Arc, + breakpoint_store: Entity, cx: &mut Context, ) -> Self { cx.on_app_quit(Self::shutdown_sessions).detach(); @@ -180,13 +173,17 @@ impl DapStore { }), downstream_client: None, active_debug_line: None, - breakpoints: Default::default(), + breakpoint_store, sessions: Default::default(), client_by_session: Default::default(), } } - pub fn new_remote(project_id: u64, upstream_client: AnyProtoClient) -> Self { + pub fn new_remote( + project_id: u64, + upstream_client: AnyProtoClient, + breakpoint_store: Entity, + ) -> Self { Self { mode: DapStoreMode::Remote(RemoteDapStore { upstream_client: Some(upstream_client), @@ -195,7 +192,7 @@ impl DapStore { }), downstream_client: None, active_debug_line: None, - breakpoints: Default::default(), + breakpoint_store, sessions: Default::default(), client_by_session: Default::default(), } @@ -302,7 +299,7 @@ impl DapStore { pub fn client_by_id( &self, client_id: impl Borrow, - cx: &Context, + cx: &App, ) -> Option<(Entity, Entity)> { let client_id = client_id.borrow(); let session = self.session_by_client_id(client_id)?; @@ -393,14 +390,8 @@ impl DapStore { } } - pub fn on_file_rename(&mut self, old_project_path: ProjectPath, new_project_path: ProjectPath) { - if let Some(breakpoints) = self.breakpoints.remove(&old_project_path) { - self.breakpoints.insert(new_project_path, breakpoints); - } - } - - pub fn breakpoints(&self) -> &BTreeMap> { - &self.breakpoints + pub fn breakpoint_store(&self) -> &Entity { + &self.breakpoint_store } async fn handle_session_has_shutdown( @@ -464,84 +455,6 @@ impl DapStore { } } - pub fn breakpoint_at_row( - &self, - row: u32, - project_path: &ProjectPath, - buffer_snapshot: BufferSnapshot, - ) -> Option { - let breakpoint_set = self.breakpoints.get(project_path)?; - - breakpoint_set - .iter() - .find(|bp| bp.point_for_buffer_snapshot(&buffer_snapshot).row == row) - .cloned() - } - - pub fn on_open_buffer( - &mut self, - project_path: &ProjectPath, - buffer: &Entity, - cx: &mut Context, - ) { - let entry = self.breakpoints.remove(project_path).unwrap_or_default(); - let mut set_bp: HashSet = HashSet::default(); - - let buffer = buffer.read(cx); - - for mut bp in entry.into_iter() { - bp.set_active_position(&buffer); - set_bp.insert(bp); - } - - self.breakpoints.insert(project_path.clone(), set_bp); - - cx.notify(); - } - - pub fn deserialize_breakpoints( - &mut self, - worktree_id: WorktreeId, - serialize_breakpoints: Vec, - ) { - for serialize_breakpoint in serialize_breakpoints { - self.breakpoints - .entry(ProjectPath { - worktree_id, - path: serialize_breakpoint.path.clone(), - }) - .or_default() - .insert(Breakpoint { - active_position: None, - cached_position: serialize_breakpoint.position, - kind: serialize_breakpoint.kind, - }); - } - } - - pub fn sync_open_breakpoints_to_closed_breakpoints( - &mut self, - buffer: &Entity, - cx: &mut Context, - ) { - let Some(project_path) = buffer.read(cx).project_path(cx) else { - return; - }; - - if let Some(breakpoint_set) = self.breakpoints.remove(&project_path) { - let breakpoint_iter = breakpoint_set.into_iter().map(|mut bp| { - bp.cached_position = bp.point_for_buffer(buffer.read(cx)).row; - bp.active_position = None; - bp - }); - - self.breakpoints - .insert(project_path, breakpoint_iter.collect::>()); - - cx.notify(); - } - } - fn reconnect_client( &mut self, session_id: &DebugSessionId, @@ -1319,31 +1232,6 @@ impl DapStore { cx.notify(); } - pub fn set_breakpoints_from_proto( - &mut self, - breakpoints: Vec, - cx: &mut Context, - ) { - let mut new_breakpoints = BTreeMap::new(); - for project_breakpoints in breakpoints { - let Some(project_path) = project_breakpoints.project_path else { - continue; - }; - - new_breakpoints.insert( - ProjectPath::from_proto(project_path), - project_breakpoints - .breakpoints - .into_iter() - .filter_map(Breakpoint::from_proto) - .collect::>(), - ); - } - - std::mem::swap(&mut self.breakpoints, &mut new_breakpoints); - cx.notify(); - } - async fn handle_shutdown_session_request( this: Entity, envelope: TypedEnvelope, @@ -1407,41 +1295,6 @@ impl DapStore { // Ok(T::response_to_proto(&client_id, response)) // } - async fn handle_synchronize_breakpoints( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let project_path = ProjectPath::from_proto( - envelope - .payload - .project_path - .context("Invalid Breakpoint call")?, - ); - - this.update(&mut cx, |store, cx| { - let breakpoints = envelope - .payload - .breakpoints - .into_iter() - .filter_map(Breakpoint::from_proto) - .collect::>(); - - if breakpoints.is_empty() { - store.breakpoints.remove(&project_path); - } else { - store.breakpoints.insert(project_path.clone(), breakpoints); - } - - cx.emit(DapStoreEvent::BreakpointsChanged { - project_path, - source_changed: false, - }); - - cx.notify(); - }) - } - async fn handle_set_debug_panel_item( this: Entity, envelope: TypedEnvelope, @@ -1543,58 +1396,6 @@ impl DapStore { }) } - pub fn toggle_breakpoint_for_buffer( - &mut self, - project_path: &ProjectPath, - mut breakpoint: Breakpoint, - edit_action: BreakpointEditAction, - cx: &mut Context, - ) { - let upstream_client = self.upstream_client(); - - let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); - - match edit_action { - BreakpointEditAction::Toggle => { - if !breakpoint_set.remove(&breakpoint) { - breakpoint_set.insert(breakpoint); - } - } - BreakpointEditAction::EditLogMessage(log_message) => { - if !log_message.is_empty() { - breakpoint.kind = BreakpointKind::Log(log_message.clone()); - breakpoint_set.remove(&breakpoint); - breakpoint_set.insert(breakpoint); - } else if matches!(&breakpoint.kind, BreakpointKind::Log(_)) { - breakpoint_set.remove(&breakpoint); - } - } - } - - if let Some((client, project_id)) = upstream_client.or(self.downstream_client.clone()) { - client - .send(client::proto::SynchronizeBreakpoints { - project_id, - project_path: Some(project_path.to_proto()), - breakpoints: breakpoint_set - .iter() - .filter_map(|breakpoint| breakpoint.to_proto()) - .collect(), - }) - .log_err(); - } - - if breakpoint_set.is_empty() { - self.breakpoints.remove(project_path); - } - - cx.emit(DapStoreEvent::BreakpointsChanged { - project_path: project_path.clone(), - source_changed: false, - }); - cx.notify(); - } - pub fn send_breakpoints( &self, client_id: DebugAdapterClientId, @@ -1602,7 +1403,7 @@ impl DapStore { mut breakpoints: Vec, ignore: bool, source_changed: bool, - cx: &Context, + cx: &App, ) -> Task> { let Some(client) = self .client_by_id(client_id, cx) @@ -1646,15 +1447,17 @@ impl DapStore { absolute_path: PathBuf, buffer_snapshot: Option, source_changed: bool, - cx: &Context, + cx: &App, ) -> Task> { let source_breakpoints = self + .breakpoint_store + .read(cx) .breakpoints .get(project_path) .cloned() .unwrap_or_default() .iter() - .map(|bp| bp.source_for_snapshot(buffer_snapshot.as_ref())) + .map(|breakpoint| breakpoint.source_for_snapshot(buffer_snapshot.as_ref())) .collect::>(); let mut tasks = Vec::new(); @@ -1694,19 +1497,6 @@ impl DapStore { _: &mut Context, ) { self.downstream_client = Some((downstream_client.clone(), project_id)); - - for (project_path, breakpoints) in self.breakpoints.iter() { - downstream_client - .send(proto::SynchronizeBreakpoints { - project_id, - project_path: Some(project_path.to_proto()), - breakpoints: breakpoints - .iter() - .filter_map(|breakpoint| breakpoint.to_proto()) - .collect(), - }) - .log_err(); - } } pub fn unshared(&mut self, cx: &mut Context) { @@ -1716,230 +1506,6 @@ impl DapStore { } } -type LogMessage = Arc; - -#[derive(Clone, Debug)] -pub enum BreakpointEditAction { - Toggle, - EditLogMessage(LogMessage), -} - -#[derive(Clone, Debug)] -pub enum BreakpointKind { - Standard, - Log(LogMessage), -} - -impl BreakpointKind { - pub fn to_int(&self) -> i32 { - match self { - BreakpointKind::Standard => 0, - BreakpointKind::Log(_) => 1, - } - } - - pub fn log_message(&self) -> Option { - match self { - BreakpointKind::Standard => None, - BreakpointKind::Log(message) => Some(message.clone()), - } - } -} - -impl PartialEq for BreakpointKind { - fn eq(&self, other: &Self) -> bool { - std::mem::discriminant(self) == std::mem::discriminant(other) - } -} - -impl Eq for BreakpointKind {} - -impl Hash for BreakpointKind { - fn hash(&self, state: &mut H) { - std::mem::discriminant(self).hash(state); - } -} - -#[derive(Clone, Debug)] -pub struct Breakpoint { - pub active_position: Option, - pub cached_position: u32, - pub kind: BreakpointKind, -} - -// Custom implementation for PartialEq, Eq, and Hash is done -// to get toggle breakpoint to solely be based on a breakpoint's -// location. Otherwise, a user can get in situation's where there's -// overlapping breakpoint's with them being aware. -impl PartialEq for Breakpoint { - fn eq(&self, other: &Self) -> bool { - match (&self.active_position, &other.active_position) { - (None, None) => self.cached_position == other.cached_position, - (None, Some(_)) => false, - (Some(_), None) => false, - (Some(self_position), Some(other_position)) => self_position == other_position, - } - } -} - -impl Eq for Breakpoint {} - -impl Hash for Breakpoint { - fn hash(&self, state: &mut H) { - if self.active_position.is_some() { - self.active_position.hash(state); - } else { - self.cached_position.hash(state); - } - } -} - -impl Breakpoint { - pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { - let line = self - .active_position - .map(|position| buffer.summary_for_anchor::(&position).row) - .unwrap_or(self.cached_position) as u64; - - let log_message = match &self.kind { - BreakpointKind::Standard => None, - BreakpointKind::Log(message) => Some(message.clone().to_string()), - }; - - SourceBreakpoint { - line, - condition: None, - hit_condition: None, - log_message, - column: None, - mode: None, - } - } - - pub fn set_active_position(&mut self, buffer: &Buffer) { - if self.active_position.is_none() { - self.active_position = - Some(buffer.breakpoint_anchor(Point::new(self.cached_position, 0))); - } - } - - pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { - self.active_position - .map(|position| buffer.summary_for_anchor::(&position)) - .unwrap_or(Point::new(self.cached_position, 0)) - } - - pub fn point_for_buffer_snapshot(&self, buffer_snapshot: &BufferSnapshot) -> Point { - self.active_position - .map(|position| buffer_snapshot.summary_for_anchor::(&position)) - .unwrap_or(Point::new(self.cached_position, 0)) - } - - pub fn source_for_snapshot(&self, snapshot: Option<&BufferSnapshot>) -> SourceBreakpoint { - let line = match snapshot { - Some(snapshot) => self - .active_position - .map(|position| snapshot.summary_for_anchor::(&position).row) - .unwrap_or(self.cached_position) as u64, - None => self.cached_position as u64, - }; - - let log_message = match &self.kind { - BreakpointKind::Standard => None, - BreakpointKind::Log(log_message) => Some(log_message.clone().to_string()), - }; - - SourceBreakpoint { - line, - condition: None, - hit_condition: None, - log_message, - column: None, - mode: None, - } - } - - pub fn to_serialized(&self, buffer: Option<&Buffer>, path: Arc) -> SerializedBreakpoint { - match buffer { - Some(buffer) => SerializedBreakpoint { - position: self - .active_position - .map(|position| buffer.summary_for_anchor::(&position).row) - .unwrap_or(self.cached_position), - path, - kind: self.kind.clone(), - }, - None => SerializedBreakpoint { - position: self.cached_position, - path, - kind: self.kind.clone(), - }, - } - } - - pub fn to_proto(&self) -> Option { - Some(client::proto::Breakpoint { - position: if let Some(position) = &self.active_position { - Some(serialize_text_anchor(position)) - } else { - None - }, - cached_position: self.cached_position, - kind: match self.kind { - BreakpointKind::Standard => proto::BreakpointKind::Standard.into(), - BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(), - }, - message: if let BreakpointKind::Log(message) = &self.kind { - Some(message.to_string()) - } else { - None - }, - }) - } - - pub fn from_proto(breakpoint: client::proto::Breakpoint) -> Option { - Some(Self { - active_position: if let Some(position) = breakpoint.position.clone() { - deserialize_anchor(position) - } else { - None - }, - cached_position: breakpoint.cached_position, - kind: match proto::BreakpointKind::from_i32(breakpoint.kind) { - Some(proto::BreakpointKind::Log) => { - BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into()) - } - None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard, - }, - }) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct SerializedBreakpoint { - pub position: u32, - pub path: Arc, - pub kind: BreakpointKind, -} - -impl SerializedBreakpoint { - pub fn to_source_breakpoint(&self) -> SourceBreakpoint { - let log_message = match &self.kind { - BreakpointKind::Standard => None, - BreakpointKind::Log(message) => Some(message.clone().to_string()), - }; - - SourceBreakpoint { - line: self.position as u64, - condition: None, - hit_condition: None, - log_message, - column: None, - mode: None, - } - } -} - #[derive(Clone)] pub struct DapAdapterDelegate { fs: Arc, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e41b355c03faab..50c4c4a49b23bd 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3128,8 +3128,12 @@ impl LspStore { if let Some(local) = self.as_local_mut() { local.initialize_buffer(buffer, cx); - local.dap_store.update(cx, |store, cx| { - store.sync_open_breakpoints_to_closed_breakpoints(buffer, cx); + local.dap_store.update(cx, |dap_store, cx| { + dap_store + .breakpoint_store() + .update(cx, |breakpoint_store, cx| { + breakpoint_store.sync_open_breakpoints_to_closed_breakpoints(buffer, cx); + }); }); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 565a5a5bcbdc8d..7390614f29375c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -49,8 +49,12 @@ use dap::{ use collections::{BTreeSet, HashMap, HashSet}; use debounced_delay::DebouncedDelay; -use debugger::dap_store::{ - Breakpoint, BreakpointEditAction, DapStore, DapStoreEvent, SerializedBreakpoint, +use debugger::{ + breakpoint_store::{ + Breakpoint, BreakpointEditAction, BreakpointStore, BreakpointStoreEvent, + SerializedBreakpoint, + }, + dap_store::{DapStore, DapStoreEvent}, }; pub use environment::ProjectEnvironment; use futures::{ @@ -175,6 +179,7 @@ pub struct Project { buffer_ordered_messages_tx: mpsc::UnboundedSender, languages: Arc, dap_store: Entity, + breakpoint_store: Entity, client: Arc, join_project_response_message_id: u32, task_store: Entity, @@ -687,6 +692,8 @@ impl Project { ) }); + let breakpoint_store = cx.new(|_| BreakpointStore::local()); + let dap_store = cx.new(|cx| { DapStore::new_local( client.http_client(), @@ -695,13 +702,16 @@ impl Project { languages.clone(), environment.clone(), toolchain_store.read(cx).as_language_toolchain_store(), + breakpoint_store.clone(), cx, ) }); cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); + cx.subscribe(&breakpoint_store, Self::on_breakpoint_store_event) + .detach(); - let buffer_store = - cx.new(|cx| BufferStore::local(worktree_store.clone(), dap_store.clone(), cx)); + let buffer_store = cx + .new(|cx| BufferStore::local(worktree_store.clone(), breakpoint_store.clone(), cx)); cx.subscribe(&buffer_store, Self::on_buffer_store_event) .detach(); @@ -782,6 +792,7 @@ impl Project { settings_observer, fs, ssh_client: None, + breakpoint_store, dap_store, buffers_needing_diff: Default::default(), git_diff_debouncer: DebouncedDelay::new(), @@ -878,7 +889,16 @@ impl Project { }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - let dap_store = cx.new(|_| DapStore::new_remote(SSH_PROJECT_ID, client.clone().into())); + let breakpoint_store = + cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, client.clone().into())); + + let dap_store = cx.new(|_| { + DapStore::new_remote( + SSH_PROJECT_ID, + client.clone().into(), + breakpoint_store.clone(), + ) + }); let git_store = cx.new(|cx| { GitStore::new( @@ -900,6 +920,7 @@ impl Project { buffer_store, image_store, lsp_store, + breakpoint_store, dap_store, join_project_response_message_id: 0, client_state: ProjectClientState::Local, @@ -1059,10 +1080,16 @@ impl Project { let environment = cx.update(|cx| ProjectEnvironment::new(&worktree_store, None, cx))?; + let breakpoint_store = cx.new(|cx| { + let mut bp_store = BreakpointStore::remote(SSH_PROJECT_ID, client.clone().into()); + bp_store.set_breakpoints_from_proto(response.payload.breakpoints, cx); + bp_store + })?; + let dap_store = cx.new(|cx| { - let mut dap_store = DapStore::new_remote(remote_id, client.clone().into()); + let mut dap_store = + DapStore::new_remote(remote_id, client.clone().into(), breakpoint_store.clone()); - dap_store.set_breakpoints_from_proto(response.payload.breakpoints, cx); dap_store.request_active_debug_sessions(cx); dap_store })?; @@ -1138,6 +1165,8 @@ impl Project { .detach(); cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); + cx.subscribe(&breakpoint_store, Self::on_breakpoint_store_event) + .detach(); let mut this = Self { buffer_ordered_messages_tx: tx, @@ -1164,6 +1193,7 @@ impl Project { remote_id, replica_id, }, + breakpoint_store, dap_store: dap_store.clone(), git_store, buffers_needing_diff: Default::default(), @@ -1272,8 +1302,7 @@ impl Project { ) -> HashMap, Vec> { let mut all_breakpoints: HashMap, Vec> = Default::default(); - let open_breakpoints = self.dap_store.read(cx).breakpoints(); - for (project_path, breakpoints) in open_breakpoints.iter() { + for (project_path, breakpoints) in &self.breakpoint_store.read(cx).breakpoints { let buffer = maybe!({ let buffer_store = self.buffer_store.read(cx); let buffer_id = buffer_store.buffer_id_for_project_path(project_path)?; @@ -1384,11 +1413,11 @@ impl Project { .read(cx) .abs_path(); - let breakpoints = self.dap_store.read(cx).breakpoints(); - Some(( worktree_path, - breakpoints + self.breakpoint_store + .read(cx) + .breakpoints .get(&project_path)? .iter() .map(|bp| bp.to_serialized(buffer, project_path.path.clone())) @@ -1412,10 +1441,9 @@ impl Project { return result; } - let breakpoints = self.dap_store.read(cx).breakpoints(); - for project_path in breakpoints.keys() { + for project_path in self.breakpoint_store.read(cx).breakpoints.keys() { if let Some((worktree_path, mut serialized_breakpoint)) = - self.serialize_breakpoints_for_project_path(&project_path, cx) + self.serialize_breakpoints_for_project_path(project_path, cx) { result .entry(worktree_path.clone()) @@ -1480,15 +1508,15 @@ impl Project { let mut tasks = Vec::new(); - for (project_path, breakpoints) in store.breakpoints() { + for (project_path, breakpoints) in &self.breakpoint_store.read(cx).breakpoints { let Some((buffer, buffer_path)) = maybe!({ let buffer = self .buffer_store - .read_with(cx, |store, cx| store.get_by_path(project_path, cx))?; + .read_with(cx, |store, cx| store.get_by_path(&project_path, cx))?; let buffer = buffer.read(cx); let project_path = buffer.project_path(cx)?; - let worktree = self.worktree_for_id(project_path.clone().worktree_id, cx)?; + let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; Some(( buffer, worktree.read(cx).absolutize(&project_path.path).ok()?, @@ -1539,8 +1567,17 @@ impl Project { }; if let Some(project_path) = buffer.read(cx).project_path(cx) { - self.dap_store.update(cx, |store, cx| { - store.toggle_breakpoint_for_buffer(&project_path, breakpoint, edit_action, cx) + self.dap_store.update(cx, |dap_store, cx| { + dap_store + .breakpoint_store() + .update(cx, |breakpoint_store, cx| { + breakpoint_store.toggle_breakpoint_for_buffer( + &project_path, + breakpoint, + edit_action, + cx, + ) + }) }); } } @@ -1630,6 +1667,10 @@ impl Project { self.dap_store.clone() } + pub fn breakpoint_store(&self) -> Entity { + self.breakpoint_store.clone() + } + pub fn lsp_store(&self) -> Entity { self.lsp_store.clone() } @@ -2051,6 +2092,9 @@ impl Project { self.client .subscribe_to_entity(project_id)? .set_entity(&self.dap_store, &mut cx.to_async()), + self.client + .subscribe_to_entity(project_id)? + .set_entity(&self.breakpoint_store, &mut cx.to_async()), self.client .subscribe_to_entity(project_id)? .set_entity(&self.git_store, &mut cx.to_async()), @@ -2065,6 +2109,9 @@ impl Project { self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.shared(project_id, self.client.clone().into(), cx) }); + self.breakpoint_store.update(cx, |breakpoint_store, _| { + breakpoint_store.shared(project_id, self.client.clone().into()) + }); self.dap_store.update(cx, |dap_store, cx| { dap_store.shared(project_id, self.client.clone().into(), cx); }); @@ -2123,8 +2170,8 @@ impl Project { self.lsp_store.update(cx, |lsp_store, _| { lsp_store.set_language_server_statuses_from_proto(message.language_servers) }); - self.dap_store.update(cx, |dap_store, cx| { - dap_store.set_breakpoints_from_proto(message.breakpoints, cx); + self.breakpoint_store.update(cx, |breakpoint_store, cx| { + breakpoint_store.set_breakpoints_from_proto(message.breakpoints, cx); }); self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync) .unwrap(); @@ -2158,6 +2205,9 @@ impl Project { self.task_store.update(cx, |task_store, cx| { task_store.unshared(cx); }); + self.breakpoint_store.update(cx, |breakpoint_store, cx| { + breakpoint_store.unshared(cx); + }); self.dap_store.update(cx, |dap_store, cx| { dap_store.unshared(cx); }); @@ -2664,6 +2714,44 @@ impl Project { } } + fn on_breakpoint_store_event( + &mut self, + _: Entity, + event: &BreakpointStoreEvent, + cx: &mut Context, + ) { + match event { + BreakpointStoreEvent::BreakpointsChanged { + project_path, + source_changed, + } => { + cx.notify(); // so the UI updates + + let buffer_snapshot = self + .buffer_store + .read(cx) + .get_by_path(&project_path, cx) + .map(|buffer| buffer.read(cx).snapshot()); + + let Some(absolute_path) = self.absolute_path(project_path, cx) else { + return; + }; + + self.dap_store.read_with(cx, |dap_store, cx| { + dap_store + .send_changed_breakpoints( + project_path, + absolute_path, + buffer_snapshot, + *source_changed, + cx, + ) + .detach_and_log_err(cx) + }); + } + } + } + fn on_dap_store_event( &mut self, _: Entity, @@ -2694,34 +2782,6 @@ impl Project { message: message.clone(), }); } - DapStoreEvent::BreakpointsChanged { - project_path, - source_changed, - } => { - cx.notify(); // so the UI updates - - let buffer_snapshot = self - .buffer_store - .read(cx) - .get_by_path(&project_path, cx) - .map(|buffer| buffer.read(cx).snapshot()); - - let Some(absolute_path) = self.absolute_path(project_path, cx) else { - return; - }; - - self.dap_store.update(cx, |store, cx| { - store - .send_changed_breakpoints( - project_path, - absolute_path, - buffer_snapshot, - *source_changed, - cx, - ) - .detach_and_log_err(cx); - }); - } DapStoreEvent::ActiveDebugLineChanged => { cx.emit(Event::ActiveDebugLineChanged); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 1ac83374363e54..53d2d0de0866cd 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1184,8 +1184,8 @@ impl ProjectPanel { path: new_entry.path.clone() }; - project.dap_store().update(cx, |dap_store, _| { - dap_store.on_file_rename(old_path, new_path); + project.breakpoint_store().update(cx, |breakpoint_store, cx| { + breakpoint_store.on_file_rename(old_path, new_path, cx); }); }); diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 2c84862c24bf27..a320ad0748c322 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -9,7 +9,7 @@ use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry use node_runtime::NodeRuntime; use project::{ buffer_store::{BufferStore, BufferStoreEvent}, - debugger::dap_store::DapStore, + debugger::breakpoint_store::BreakpointStore, git::GitStore, project_settings::SettingsObserver, search::SearchQuery, @@ -93,6 +93,8 @@ impl HeadlessProject { ) }); + let breakpoint_store = cx.new(|_| BreakpointStore::local()); + let dap_store = cx.new(|cx| { DapStore::new_local( http_client.clone(), @@ -101,13 +103,14 @@ impl HeadlessProject { languages.clone(), environment.clone(), toolchain_store.read(cx).as_language_toolchain_store(), + breakpoint_store.clone(), cx, ) }); let buffer_store = cx.new(|cx| { let mut buffer_store = - BufferStore::local(worktree_store.clone(), dap_store.clone(), cx); + BufferStore::local(worktree_store.clone(), breakpoint_store.clone(), cx); buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); buffer_store }); diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 0fadc801d8a223..575ff40beb6736 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -12,7 +12,7 @@ use client::DevServerProjectId; use collections::HashMap; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId}; -use project::debugger::dap_store::{BreakpointKind, SerializedBreakpoint}; +use project::debugger::breakpoint_store::{BreakpointKind, SerializedBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 97005e5532cf19..2847bfea19ef79 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -11,7 +11,7 @@ use db::sqlez::{ }; use gpui::{AsyncWindowContext, Entity, WeakEntity}; use itertools::Itertools as _; -use project::debugger::dap_store::SerializedBreakpoint; +use project::debugger::breakpoint_store::SerializedBreakpoint; use project::Project; use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 92a5f17f2d2f5f..053e9436e9aa7a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4702,17 +4702,24 @@ impl Workspace { // Add unopened breakpoints to project before opening any items workspace.update(&mut cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - for worktree in project.worktrees(cx) { - let (worktree_id, worktree_path) = - worktree.read_with(cx, |tree, _cx| (tree.id(), tree.abs_path())); - - if let Some(serialized_breakpoints) = - serialized_workspace.breakpoints.remove(&worktree_path) - { - store.deserialize_breakpoints(worktree_id, serialized_breakpoints); - } - } + project.dap_store().update(cx, |dap_store, cx| { + dap_store + .breakpoint_store() + .update(cx, |breakpoint_store, cx| { + for worktree in project.worktrees(cx) { + let (worktree_id, worktree_path) = worktree + .read_with(cx, |tree, _cx| (tree.id(), tree.abs_path())); + + if let Some(serialized_breakpoints) = + serialized_workspace.breakpoints.remove(&worktree_path) + { + breakpoint_store.deserialize_breakpoints( + worktree_id, + serialized_breakpoints, + ); + } + } + }) }); }) })?; From 3833788cbcd660d2f5ff231d9a18a922b51e75e6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 14 Feb 2025 14:23:27 +0100 Subject: [PATCH 542/650] Add missing init for breakpointstore --- crates/project/src/project.rs | 2 ++ crates/zed/src/zed.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7390614f29375c..27603bffc4e86f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -661,6 +661,7 @@ impl Project { SettingsObserver::init(&client); TaskStore::init(Some(&client)); ToolchainStore::init(&client); + BreakpointStore::init(&client); DapStore::init(&client); } @@ -989,6 +990,7 @@ impl Project { SettingsObserver::init(&ssh_proto); TaskStore::init(Some(&ssh_proto)); ToolchainStore::init(&ssh_proto); + BreakpointStore::init(&ssh_proto); DapStore::init(&ssh_proto); GitStore::init(&ssh_proto); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ce658982b06cee..e95a804080d6bd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4208,6 +4208,9 @@ mod tests { repl::init(app_state.fs.clone(), cx); repl::notebook::init(cx); tasks_ui::init(cx); + project::debugger::breakpoint_store::BreakpointStore::init( + &app_state.client.clone().into(), + ); project::debugger::dap_store::DapStore::init(&app_state.client.clone().into()); debugger_ui::init(cx); initialize_workspace(app_state.clone(), prompt_builder, cx); From c820f12e9c81b7435f46270b3211d243c1738317 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:24:54 +0100 Subject: [PATCH 543/650] Check capabilities outside of dap_session That way we don't need to roundtrip for RPC peers. Plus the logs for unsupported features will be less noisy --- crates/project/src/debugger/dap_session.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/project/src/debugger/dap_session.rs b/crates/project/src/debugger/dap_session.rs index ddfb67817b6edd..df2f9f06efe2dc 100644 --- a/crates/project/src/debugger/dap_session.rs +++ b/crates/project/src/debugger/dap_session.rs @@ -177,7 +177,6 @@ impl From> for Mode { impl Mode { fn request_local( connection: &Arc, - caps: &Capabilities, request: R, cx: &mut Context, ) -> Task> @@ -185,13 +184,6 @@ impl Mode { ::Response: 'static, ::Arguments: 'static + Send, { - if !request.is_supported(&caps) { - return Task::ready(Err(anyhow!( - "Request {} is not supported", - R::DapRequest::COMMAND - ))); - } - let request = Arc::new(request); let request_clone = request.clone(); @@ -209,7 +201,6 @@ impl Mode { fn request_dap( &self, - caps: &Capabilities, client_id: DebugAdapterClientId, request: R, cx: &mut Context, @@ -220,7 +211,7 @@ impl Mode { { match self { Mode::Local(debug_adapter_client) => { - Self::request_local(&debug_adapter_client, caps, request, cx) + Self::request_local(&debug_adapter_client, request, cx) } Mode::Remote(remote_connection) => { remote_connection.request_remote(request, client_id, cx) @@ -367,7 +358,10 @@ impl Client { process_result: impl FnOnce(&mut Self, &T::Response, &mut Context) + 'static, cx: &mut Context, ) -> Task> { - let request = mode.request_dap(&capabilities, client_id, request, cx); + if !request.is_supported(&capabilities) { + return Task::ready(None); + } + let request = mode.request_dap(client_id, request, cx); cx.spawn(|this, mut cx| async move { let result = request.await.log_err()?; this.update(&mut cx, |this, cx| { From 00c24ce2897879b1cdf70e30d9e3356624569a47 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:27:44 +0100 Subject: [PATCH 544/650] Simplify DapCommand::is_supported --- crates/project/src/debugger/dap_command.rs | 68 +++++----------------- 1 file changed, 13 insertions(+), 55 deletions(-) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index b4b2e7f5424a4e..f6d87a1b55e5a6 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -18,7 +18,9 @@ pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { type DapRequest: 'static + Send + dap::requests::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; - fn is_supported(&self, capabilities: &Capabilities) -> bool; + fn is_supported(capabilities: &Capabilities) -> bool { + true + } fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId; @@ -53,8 +55,8 @@ impl DapCommand for Arc { type DapRequest = T::DapRequest; type ProtoRequest = T::ProtoRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { - T::is_supported(self, capabilities) + fn is_supported(capabilities: &Capabilities) -> bool { + T::is_supported(capabilities) } fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { @@ -135,10 +137,6 @@ impl DapCommand for NextCommand { type DapRequest = Next; type ProtoRequest = proto::DapNextRequest; - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -203,10 +201,6 @@ impl DapCommand for StepInCommand { type DapRequest = dap::requests::StepIn; type ProtoRequest = proto::DapStepInRequest; - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -279,10 +273,6 @@ impl DapCommand for StepOutCommand { type DapRequest = dap::requests::StepOut; type ProtoRequest = proto::DapStepOutRequest; - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -353,7 +343,7 @@ impl DapCommand for StepBackCommand { type DapRequest = dap::requests::StepBack; type ProtoRequest = proto::DapStepBackRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_step_back.unwrap_or_default() } @@ -427,10 +417,6 @@ impl DapCommand for ContinueCommand { type DapRequest = Continue; type ProtoRequest = proto::DapContinueRequest; - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -498,10 +484,6 @@ impl DapCommand for PauseCommand { type DapRequest = dap::requests::Pause; type ProtoRequest = proto::DapPauseRequest; - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -564,10 +546,6 @@ impl DapCommand for DisconnectCommand { type DapRequest = dap::requests::Disconnect; type ProtoRequest = proto::DapDisconnectRequest; - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -634,7 +612,7 @@ impl DapCommand for TerminateThreadsCommand { type DapRequest = dap::requests::TerminateThreads; type ProtoRequest = proto::DapTerminateThreadsRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities .supports_terminate_threads_request .unwrap_or_default() @@ -704,7 +682,7 @@ impl DapCommand for TerminateCommand { type DapRequest = dap::requests::Terminate; type ProtoRequest = proto::DapTerminateRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_terminate_request.unwrap_or_default() } @@ -768,7 +746,7 @@ impl DapCommand for RestartCommand { type DapRequest = dap::requests::Restart; type ProtoRequest = proto::DapRestartRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_restart_request.unwrap_or_default() } @@ -843,10 +821,6 @@ impl DapCommand for VariablesCommand { type DapRequest = dap::requests::Variables; type ProtoRequest = proto::VariablesRequest; - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -930,7 +904,7 @@ impl DapCommand for SetVariableValueCommand { type DapRequest = dap::requests::SetVariable; type ProtoRequest = proto::DapSetVariableValueRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_set_variable.unwrap_or_default() } @@ -1016,7 +990,7 @@ impl DapCommand for RestartStackFrameCommand { type DapRequest = dap::requests::RestartFrame; type ProtoRequest = proto::DapRestartStackFrameRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_restart_frame.unwrap_or_default() } @@ -1078,7 +1052,7 @@ impl DapCommand for ModulesCommand { type DapRequest = dap::requests::Modules; type ProtoRequest = proto::DapModulesRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_modules_request.unwrap_or_default() } @@ -1148,7 +1122,7 @@ impl DapCommand for LoadedSourcesCommand { type DapRequest = dap::requests::LoadedSources; type ProtoRequest = proto::DapLoadedSourcesRequest; - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities .supports_loaded_sources_request .unwrap_or_default() @@ -1237,10 +1211,6 @@ impl DapCommand for StackTraceCommand { Ok(message.stack_frames) } - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn to_proto( &self, debug_client_id: DebugAdapterClientId, @@ -1312,10 +1282,6 @@ impl DapCommand for ScopesCommand { Ok(message.scopes) } - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn to_proto( &self, debug_client_id: DebugAdapterClientId, @@ -1464,10 +1430,6 @@ impl DapCommand for EvaluateCommand { Ok(message) } - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn to_proto( &self, debug_client_id: DebugAdapterClientId, @@ -1547,10 +1509,6 @@ impl DapCommand for ThreadsCommand { Ok(message.threads) } - fn is_supported(&self, _capabilities: &Capabilities) -> bool { - true - } - fn to_proto( &self, debug_client_id: DebugAdapterClientId, From 90440d9652aadaa425003df635887cb813b439d0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:44:46 +0100 Subject: [PATCH 545/650] fixup! Check capabilities outside of dap_session --- crates/project/src/debugger/dap_command.rs | 2 +- crates/project/src/debugger/dap_session.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index f6d87a1b55e5a6..0a746f84bbf408 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -1344,7 +1344,7 @@ impl DapCommand for super::dap_session::CompletionsQuery { Ok(message) } - fn is_supported(&self, capabilities: &Capabilities) -> bool { + fn is_supported(capabilities: &Capabilities) -> bool { capabilities .supports_completions_request .unwrap_or_default() diff --git a/crates/project/src/debugger/dap_session.rs b/crates/project/src/debugger/dap_session.rs index df2f9f06efe2dc..8508b4627abb17 100644 --- a/crates/project/src/debugger/dap_session.rs +++ b/crates/project/src/debugger/dap_session.rs @@ -358,7 +358,7 @@ impl Client { process_result: impl FnOnce(&mut Self, &T::Response, &mut Context) + 'static, cx: &mut Context, ) -> Task> { - if !request.is_supported(&capabilities) { + if !T::is_supported(&capabilities) { return Task::ready(None); } let request = mode.request_dap(client_id, request, cx); From debcb1f26f90641525fa400f434088e16a17acfc Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 14 Feb 2025 19:30:14 +0100 Subject: [PATCH 546/650] WIP Co-Authored-By: Anthony Eid --- crates/collab/src/tests/debug_panel_tests.rs | 14 +- crates/dap/src/proto_conversions.rs | 2 - crates/debugger_ui/src/attach_modal.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 8 +- crates/debugger_ui/src/tests/attach_modal.rs | 4 +- crates/debugger_ui/src/tests/console.rs | 6 +- .../debugger_ui/src/tests/debugger_panel.rs | 18 +- crates/debugger_ui/src/tests/module_list.rs | 2 +- .../debugger_ui/src/tests/stack_frame_list.rs | 6 +- crates/debugger_ui/src/tests/variable_list.rs | 10 +- crates/project/src/debugger.rs | 2 +- .../debugger/{dap_session.rs => client.rs} | 139 ++++------ crates/project/src/debugger/dap_command.rs | 7 +- crates/project/src/debugger/dap_store.rs | 246 ++++++------------ crates/project/src/project.rs | 15 +- crates/proto/proto/zed.proto | 141 ++++------ crates/proto/src/proto.rs | 9 - 17 files changed, 217 insertions(+), 414 deletions(-) rename crates/project/src/debugger/{dap_session.rs => client.rs} (92%) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 2da39f55e73070..975b124d0a8e03 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -274,7 +274,7 @@ async fn test_debug_panel_item_opens_on_remote( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -373,7 +373,7 @@ async fn test_active_debug_panel_item_set_on_join_project( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -866,7 +866,7 @@ async fn test_restart_stack_frame(host_cx: &mut TestAppContext, remote_cx: &mut let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -1097,7 +1097,7 @@ async fn test_updated_breakpoints_send_to_dap( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -1338,7 +1338,7 @@ async fn test_module_list( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -2198,7 +2198,7 @@ async fn test_ignore_breakpoints( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -2522,7 +2522,7 @@ async fn test_debug_panel_console(host_cx: &mut TestAppContext, remote_cx: &mut let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index 153247dfc98bda..b88ca92c036cb8 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -356,11 +356,9 @@ pub fn capabilities_from_proto(payload: &SetDebugClientCapabilities) -> Capabili pub fn capabilities_to_proto( capabilities: &Capabilities, project_id: u64, - session_id: u64, client_id: u64, ) -> SetDebugClientCapabilities { SetDebugClientCapabilities { - session_id, client_id, project_id, supports_loaded_sources_request: capabilities diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 98fe4c91beae95..2202f71a3c3339 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -236,7 +236,7 @@ impl PickerDelegate for AttachModalDelegate { self.candidates.take(); self.dap_store.update(cx, |store, cx| { - store.shutdown_session(&self.session_id, cx).detach(); + store.shutdown_client(&self.session_id, cx).detach(); }); cx.emit(DismissEvent); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 7ba512592bc7e7..708dd3a8b7f0ef 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -665,7 +665,7 @@ impl DebugPanel { )); store - .shutdown_session(&session_id, cx) + .shutdown_client(&session_id, cx) .detach_and_log_err(cx); }); })?; @@ -723,7 +723,7 @@ impl DebugPanel { ) { if let Some(capabilities) = capabilities { self.dap_store.update(cx, |store, cx| { - store.update_capabilities_for_client(&session_id, client_id, capabilities, cx); + store.update_capabilities_for_client(client_id, capabilities, cx); }); cx.emit(DebugPanelEvent::CapabilitiesChanged(client_id)); @@ -925,7 +925,7 @@ impl DebugPanel { }); } else { store - .shutdown_session(&session_id, cx) + .shutdown_client(&session_id, cx) .detach_and_log_err(cx); } }); @@ -1145,7 +1145,7 @@ impl DebugPanel { cx: &mut Context, ) { self.dap_store.update(cx, |store, cx| { - store.update_capabilities_for_client(session_id, client_id, &event.capabilities, cx); + store.update_capabilities_for_client(client_id, &event.capabilities, cx); }); cx.emit(DebugPanelEvent::CapabilitiesChanged(client_id)); diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index c5161c76a8fa20..4b48391c69bc4b 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -90,7 +90,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -201,7 +201,7 @@ async fn test_show_attach_modal_and_select_process( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 85db5619dbb0a7..6de89e3ee6367a 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -174,7 +174,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -459,7 +459,7 @@ async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppConte let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -875,7 +875,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 1f14d0f718e397..10bf5b11d8b62c 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -131,7 +131,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -285,7 +285,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -439,7 +439,7 @@ async fn test_client_can_open_multiple_thread_panels( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -561,7 +561,7 @@ async fn test_handle_successful_run_in_terminal_reverse_request( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -665,7 +665,7 @@ async fn test_handle_error_run_in_terminal_reverse_request( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -804,7 +804,7 @@ async fn test_handle_start_debugging_reverse_request( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -1005,7 +1005,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -1194,7 +1194,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -1332,7 +1332,7 @@ async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs index 3e6fe3fbb95a4c..8ee9af6ed5d1b4 100644 --- a/crates/debugger_ui/src/tests/module_list.rs +++ b/crates/debugger_ui/src/tests/module_list.rs @@ -225,7 +225,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 9413b8f6966c4c..b824a37babee75 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -181,7 +181,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -444,7 +444,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -667,7 +667,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo }); let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 9173b6c5864719..5267dfdcd5fbcc 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -235,7 +235,7 @@ async fn test_basic_fetch_initial_scope_and_variables( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -514,7 +514,7 @@ async fn test_fetch_variables_for_multiple_scopes( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -1129,7 +1129,7 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -1375,7 +1375,7 @@ async fn test_it_only_fetches_scopes_and_variables_for_the_first_stack_frame( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); @@ -1743,7 +1743,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(&session.read(cx).id(), cx) + dap_store.shutdown_client(&session.read(cx).id(), cx) }) }); diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index 1ca4f576c1f8d0..e1b952ac81fd39 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -1,4 +1,4 @@ pub mod breakpoint_store; +pub mod client; pub mod dap_command; -pub mod dap_session; pub mod dap_store; diff --git a/crates/project/src/debugger/dap_session.rs b/crates/project/src/debugger/client.rs similarity index 92% rename from crates/project/src/debugger/dap_session.rs rename to crates/project/src/debugger/client.rs index 8508b4627abb17..275add8c242602 100644 --- a/crates/project/src/debugger/dap_session.rs +++ b/crates/project/src/debugger/client.rs @@ -27,20 +27,6 @@ use task::DebugAdapterConfig; use text::{PointUtf16, ToPointUtf16}; use util::ResultExt; -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(transparent)] -pub struct DebugSessionId(pub usize); - -impl DebugSessionId { - pub fn from_proto(session_id: u64) -> Self { - Self(session_id as usize) - } - - pub fn to_proto(&self) -> u64 { - self.0 as u64 - } -} - #[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)] #[repr(transparent)] pub struct ThreadId(pub u64); @@ -226,6 +212,7 @@ pub struct Client { pub(super) capabilities: Capabilities, client_id: DebugAdapterClientId, + ignore_breakpoints: bool, modules: Vec, loaded_sources: Vec, threads: IndexMap, @@ -308,6 +295,42 @@ impl CompletionsQuery { } impl Client { + pub(crate) fn local(adapter: Arc, capabilities: Capabilities) -> Self { + let client_id = adapter.id(); + + Self { + mode: Mode::Local(adapter), + client_id, + capabilities, + ignore_breakpoints: false, + requests: HashMap::default(), + modules: Vec::default(), + loaded_sources: Vec::default(), + threads: IndexMap::default(), + } + } + + pub(crate) fn remote( + client_id: DebugAdapterClientId, + client: AnyProtoClient, + upstream_project_id: u64, + ignore_breakpoints: bool, + ) -> Self { + Self { + mode: Mode::Remote(RemoteConnection { + client, + upstream_project_id, + }), + client_id, + capabilities: Capabilities::default(), + ignore_breakpoints, + requests: HashMap::default(), + modules: Vec::default(), + loaded_sources: Vec::default(), + threads: IndexMap::default(), + } + } + pub fn capabilities(&self) -> &Capabilities { &self.capabilities } @@ -426,6 +449,12 @@ impl Client { &self.modules } + pub(crate) fn set_ignore_breakpoints(&mut self, ignore: bool) { + self.ignore_breakpoints = ignore; + } + pub(crate) fn breakpoints_enabled(&self) -> bool { + self.ignore_breakpoints + } pub fn handle_module_event(&mut self, event: &dap::ModuleEvent, cx: &mut Context) { match event.reason { dap::ModuleEventReason::New => self.modules.push(event.module.clone()), @@ -497,7 +526,7 @@ impl Client { } } - fn shutdown(&mut self, cx: &mut Context) { + pub(super) fn shutdown(&mut self, cx: &mut Context) { if self .capabilities .supports_terminate_request @@ -777,13 +806,11 @@ impl Client { &mut self, thread_id: ThreadId, stack_frame_id: u64, - session_id: DebugSessionId, variables_reference: u64, cx: &mut Context, ) -> Vec { let command = VariablesCommand { stack_frame_id, - session_id, thread_id: thread_id.0, variables_reference, filter: None, @@ -886,85 +913,7 @@ impl Client { } } -pub struct DebugSession { - id: DebugSessionId, - mode: DebugSessionMode, - pub(super) states: BTreeMap>, - ignore_breakpoints: bool, -} - -pub enum DebugSessionMode { - Local(LocalDebugSession), - Remote(RemoteDebugSession), -} - -pub struct LocalDebugSession { - configuration: DebugAdapterConfig, -} - -impl LocalDebugSession { - pub fn configuration(&self) -> &DebugAdapterConfig { - &self.configuration - } - - pub fn update_configuration( - &mut self, - f: impl FnOnce(&mut DebugAdapterConfig), - cx: &mut Context, - ) { - f(&mut self.configuration); - cx.notify(); - } -} - -pub struct RemoteDebugSession { - label: String, -} - impl DebugSession { - pub fn new_local(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { - Self { - id, - ignore_breakpoints: false, - states: BTreeMap::default(), - mode: DebugSessionMode::Local(LocalDebugSession { configuration }), - } - } - - pub fn as_local(&self) -> Option<&LocalDebugSession> { - match &self.mode { - DebugSessionMode::Local(local) => Some(local), - _ => None, - } - } - - pub fn as_local_mut(&mut self) -> Option<&mut LocalDebugSession> { - match &mut self.mode { - DebugSessionMode::Local(local) => Some(local), - _ => None, - } - } - - pub fn new_remote(id: DebugSessionId, label: String, ignore_breakpoints: bool) -> Self { - Self { - id, - ignore_breakpoints, - states: BTreeMap::default(), - mode: DebugSessionMode::Remote(RemoteDebugSession { label }), - } - } - - pub fn id(&self) -> DebugSessionId { - self.id - } - - pub fn name(&self) -> String { - match &self.mode { - DebugSessionMode::Local(local) => local.configuration.label.clone(), - DebugSessionMode::Remote(remote) => remote.label.clone(), - } - } - pub fn ignore_breakpoints(&self) -> bool { self.ignore_breakpoints } diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 0a746f84bbf408..33c32e1047d2a9 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -11,8 +11,6 @@ use dap::{ use rpc::proto; use util::ResultExt; -use super::dap_session::DebugSessionId; - pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; @@ -809,7 +807,6 @@ pub struct VariablesCommand { pub stack_frame_id: u64, pub thread_id: u64, pub variables_reference: u64, - pub session_id: DebugSessionId, pub filter: Option, pub start: Option, pub count: Option, @@ -851,7 +848,6 @@ impl DapCommand for VariablesCommand { project_id: upstream_project_id, client_id: debug_client_id.to_proto(), thread_id: self.thread_id, - session_id: self.session_id.to_proto(), stack_frame_id: self.stack_frame_id, variables_reference: self.variables_reference, filter: None, @@ -864,7 +860,6 @@ impl DapCommand for VariablesCommand { fn from_proto(request: &Self::ProtoRequest) -> Self { Self { thread_id: request.thread_id, - session_id: DebugSessionId::from_proto(request.session_id), stack_frame_id: request.stack_frame_id, variables_reference: request.variables_reference, filter: None, @@ -1323,7 +1318,7 @@ impl DapCommand for ScopesCommand { } } -impl DapCommand for super::dap_session::CompletionsQuery { +impl DapCommand for super::client::CompletionsQuery { type Response = dap::CompletionsResponse; type DapRequest = dap::requests::Completions; type ProtoRequest = proto::DapCompletionRequest; diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 18dd429d3e9665..5052ec67b0ae03 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1,5 +1,6 @@ use super::{ breakpoint_store::BreakpointStore, + client::{self, Client}, // Will need to uncomment this once we implement rpc message handler again // dap_command::{ // ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, @@ -7,9 +8,11 @@ use super::{ // TerminateCommand, TerminateThreadsCommand, VariablesCommand, // }, dap_command::DapCommand, - dap_session::{self, DebugSession, DebugSessionId}, }; -use crate::{project_settings::ProjectSettings, ProjectEnvironment, ProjectPath}; +use crate::{ + debugger, project_settings::ProjectSettings, DebugAdapterClientState, ProjectEnvironment, + ProjectPath, +}; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; @@ -44,7 +47,7 @@ use settings::{Settings as _, WorktreeId}; use smol::lock::Mutex; use std::{ borrow::Borrow, - collections::HashSet, + collections::{BTreeMap, HashSet}, ffi::OsStr, hash::Hash, path::{Path, PathBuf}, @@ -59,10 +62,9 @@ use util::{merge_json_value_into, ResultExt as _}; use worktree::Worktree; pub enum DapStoreEvent { - DebugClientStarted((DebugSessionId, DebugAdapterClientId)), + DebugClientStarted((DebugAdapterClientId)), DebugClientShutdown(DebugAdapterClientId), DebugClientEvent { - session_id: DebugSessionId, client_id: DebugAdapterClientId, message: Message, }, @@ -84,7 +86,6 @@ pub struct LocalDapStore { fs: Arc, node_runtime: NodeRuntime, next_client_id: AtomicU32, - next_session_id: AtomicUsize, http_client: Arc, environment: Entity, language_registry: Arc, @@ -95,10 +96,6 @@ impl LocalDapStore { fn next_client_id(&self) -> DebugAdapterClientId { DebugAdapterClientId(self.next_client_id.fetch_add(1, SeqCst)) } - - fn next_session_id(&self) -> DebugSessionId { - DebugSessionId(self.next_session_id.fetch_add(1, SeqCst)) - } } pub struct RemoteDapStore { @@ -112,8 +109,7 @@ pub struct DapStore { downstream_client: Option<(AnyProtoClient, u64)>, breakpoint_store: Entity, active_debug_line: Option<(DebugAdapterClientId, ProjectPath, u32)>, - sessions: HashMap>, - client_by_session: HashMap, + clients: BTreeMap>, } impl EventEmitter for DapStore {} @@ -169,13 +165,11 @@ impl DapStore { toolchain_store, language_registry, next_client_id: Default::default(), - next_session_id: Default::default(), }), downstream_client: None, active_debug_line: None, breakpoint_store, - sessions: Default::default(), - client_by_session: Default::default(), + clients: Default::default(), } } @@ -193,8 +187,7 @@ impl DapStore { downstream_client: None, active_debug_line: None, breakpoint_store, - sessions: Default::default(), - client_by_session: Default::default(), + clients: Default::default(), } } @@ -247,66 +240,33 @@ impl DapStore { self.downstream_client.as_ref() } - pub fn add_remote_session( + pub fn add_remote_client( &mut self, - session_id: DebugSessionId, + client_id: DebugAdapterClientId, ignore: Option, cx: &mut Context, ) { - self.sessions.entry(session_id).or_insert(cx.new(|_| { - DebugSession::new_remote( - session_id, - "Remote-Debug".to_owned(), - ignore.unwrap_or(false), - ) - })); + self.clients.insert( + client_id, + cx.new(|_| { + debugger::client::Client::new( + client_id, + "Remote-Debug".to_owned(), + ignore.unwrap_or(false), + ) + }), + ); debug_assert!(matches!(self.mode, DapStoreMode::Remote(_))); } - pub fn add_client_to_session( - &mut self, - session_id: DebugSessionId, - client_id: DebugAdapterClientId, - ) { - self.sessions.entry(session_id).and_modify(|_| { - let existing_value = self.client_by_session.insert(client_id, session_id); - debug_assert!(existing_value.map_or(true, |old| old == session_id)); - }); - } - - pub fn remove_session(&mut self, session_id: DebugSessionId) { - self.sessions.remove(&session_id); - self.client_by_session.retain(|_, id| *id != session_id); - } - - pub fn sessions(&self) -> impl Iterator> + '_ { - self.sessions.values().cloned() - } - - pub fn session_by_id(&self, session_id: &DebugSessionId) -> Option> { - self.sessions.get(session_id).cloned() - } - - pub fn session_by_client_id( - &self, - client_id: impl Borrow, - ) -> Option> { - self.sessions - .get(self.client_by_session.get(client_id.borrow())?) - .cloned() - } - pub fn client_by_id( &self, client_id: impl Borrow, - cx: &App, - ) -> Option<(Entity, Entity)> { + ) -> Option> { let client_id = client_id.borrow(); - let session = self.session_by_client_id(client_id)?; - - let client = session.read(cx).client_by_id(*client_id)?; + let client = self.clients.get(client_id).cloned(); - Some((session, client)) + client } pub fn capabilities_by_id( @@ -315,28 +275,20 @@ impl DapStore { cx: &App, ) -> Option { let client_id = client_id.borrow(); - self.session_by_client_id(client_id).and_then(|session| { - session - .read(cx) - .client_state(*client_id) - .map(|state| state.read(cx).capabilities.clone()) - }) + self.clients + .get(client_id) + .map(|client| client.read(cx).capabilities.clone()) } pub fn update_capabilities_for_client( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, capabilities: &Capabilities, cx: &mut Context, ) { - if let Some((client, _)) = self.client_by_id(client_id, cx) { + if let Some(client) = self.client_by_id(client_id) { client.update(cx, |this, cx| { - if let Some(state) = this.client_state(client_id) { - state.update(cx, |this, _| { - this.capabilities = this.capabilities.merge(capabilities.clone()); - }); - } + this.capabilities = this.capabilities.merge(capabilities.clone()); }); } @@ -347,7 +299,6 @@ impl DapStore { .send(dap::proto_conversions::capabilities_to_proto( &capabilities, *project_id, - session_id.to_proto(), client_id.to_proto(), )) .log_err(); @@ -394,29 +345,17 @@ impl DapStore { &self.breakpoint_store } - async fn handle_session_has_shutdown( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - this.remove_session(DebugSessionId::from_proto(envelope.payload.session_id)); - })?; - - Ok(()) - } - async fn handle_ignore_breakpoint_state( this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { - let session_id = DebugSessionId::from_proto(envelope.payload.session_id); + let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); this.update(&mut cx, |this, cx| { - if let Some(session) = this.session_by_id(&session_id) { - session.update(cx, |session, cx| { - session.set_ignore_breakpoints(envelope.payload.ignore, cx) + if let Some(client) = this.client_by_id(&client_id) { + client.update(cx, |client, cx| { + client.set_ignore_breakpoints(envelope.payload.ignore) }); } })?; @@ -426,38 +365,37 @@ impl DapStore { pub fn set_ignore_breakpoints( &mut self, - session_id: &DebugSessionId, + session_id: &DebugAdapterClientId, ignore: bool, cx: &mut Context, ) { - if let Some(session) = self.session_by_id(session_id) { - session.update(cx, |session, cx| { - session.set_ignore_breakpoints(ignore, cx); + if let Some(session) = self.client_by_id(session_id) { + session.update(cx, |session, _| { + session.set_ignore_breakpoints(ignore); }); } } - pub fn ignore_breakpoints(&self, session_id: &DebugSessionId, cx: &App) -> bool { - self.session_by_id(session_id) - .map(|session| session.read(cx).ignore_breakpoints()) + pub fn ignore_breakpoints(&self, client_id: &DebugAdapterClientId, cx: &App) -> bool { + self.client_by_id(client_id) + .map(|client| client.read(cx).breakpoints_enabled()) .unwrap_or_default() } pub fn toggle_ignore_breakpoints( &mut self, - session_id: &DebugSessionId, + client_id: &DebugAdapterClientId, cx: &mut Context, ) { - if let Some(session) = self.session_by_id(session_id) { - session.update(cx, |session, cx| { - session.set_ignore_breakpoints(!session.ignore_breakpoints(), cx); + if let Some(client) = self.client_by_id(client_id) { + client.update(cx, |client, _| { + client.set_ignore_breakpoints(!client.breakpoints_enabled()); }); } } fn reconnect_client( &mut self, - session_id: &DebugSessionId, adapter: Arc, binary: DebugAdapterBinary, config: DebugAdapterConfig, @@ -469,7 +407,6 @@ impl DapStore { ))); } - let session_id = *session_id; let client_id = self.as_local().unwrap().next_client_id(); cx.spawn(|dap_store, mut cx| async move { @@ -482,11 +419,7 @@ impl DapStore { move |message, cx| { dap_store .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { - session_id, - client_id, - message, - }) + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) }) .log_err(); } @@ -496,9 +429,12 @@ impl DapStore { .await?; dap_store.update(&mut cx, |store, cx| { - store.client_by_session.insert(client_id, session_id); + cx.new(|cx| { + let client_state = + debugger::client::Client::local(Arc::new(client), capabilities); + }); - let session = store.session_by_id(&session_id).unwrap(); + store.clients.insert(Arc::new(client), client_id); session.update(cx, |session, cx| { session.add_client(Arc::new(client), client_id, cx); @@ -517,7 +453,7 @@ impl DapStore { // don't emit this event ourself in tests, so we can add request, // response and event handlers for this client if !cfg!(any(test, feature = "test-support")) { - cx.emit(DapStoreEvent::DebugClientStarted((session_id, client_id))); + cx.emit(DapStoreEvent::DebugClientStarted(client_id)); } cx.notify(); @@ -527,7 +463,6 @@ impl DapStore { fn start_client_internal( &mut self, - session_id: DebugSessionId, delegate: DapAdapterDelegate, config: DebugAdapterConfig, cx: &mut Context, @@ -629,7 +564,6 @@ impl DapStore { }), ); - let session_id = local_store.next_session_id(); let start_client_task = self.start_client_internal(session_id, delegate, config.clone(), cx); @@ -668,23 +602,18 @@ impl DapStore { pub fn initialize( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id, cx) - .and_then(|(_, client)| client.read(cx).adapter_client()) + .client_by_id(client_id) + .and_then(|client| client.read(cx).adapter_client()) else { - return Task::ready(Err(anyhow!( - "Could not find debug client: {:?} for session {:?}", - client_id, - session_id - ))); + return Task::ready(Err( + anyhow!("Could not find debug client: {:?}", client_id,), + )); }; - let session_id = *session_id; - cx.spawn(|this, mut cx| async move { let capabilities = client .request::(InitializeRequestArguments { @@ -708,7 +637,7 @@ impl DapStore { .await?; this.update(&mut cx, |store, cx| { - store.update_capabilities_for_client(&session_id, client_id, &capabilities, cx); + store.update_capabilities_for_client(client_id, &capabilities, cx); }) }) } @@ -719,8 +648,8 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id, cx) - .and_then(|(_, client)| client.read(cx).adapter_client()) + .client_by_id(client_id) + .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -743,13 +672,12 @@ impl DapStore { pub fn launch( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, cx: &mut Context, ) -> Task> { - let Some((session, client)) = self - .client_by_id(client_id, cx) - .and_then(|(session, client)| Some((session, client.read(cx).adapter_client()?))) + let Some(client) = self + .client_by_id(client_id) + .and_then(|client| Some(client.read(cx).adapter_client()?)) else { return Task::ready(Err(anyhow!( "Could not find debug client: {:?} for session {:?}", @@ -1069,8 +997,8 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id, cx) - .and_then(|(_, client)| client.read(cx).adapter_client()) + .client_by_id(client_id) + .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1132,7 +1060,7 @@ impl DapStore { let mut tasks = Vec::new(); for session_id in self.sessions.keys().cloned().collect::>() { - tasks.push(self.shutdown_session(&session_id, cx)); + tasks.push(self.shutdown_client(&session_id, cx)); } cx.background_executor().spawn(async move { @@ -1140,16 +1068,16 @@ impl DapStore { }) } - pub fn shutdown_session( + pub fn shutdown_client( &mut self, - session_id: &DebugSessionId, + client_id: &DebugAdapterClientId, cx: &mut Context, ) -> Task> { let Some(_) = self.as_local_mut() else { if let Some((upstream_client, project_id)) = self.upstream_client() { let future = upstream_client.request(proto::DapShutdownSession { project_id, - session_id: Some(session_id.to_proto()), + session_id: Some(client_id.to_proto()), }); return cx @@ -1160,15 +1088,14 @@ impl DapStore { return Task::ready(Err(anyhow!("Cannot shutdown session on remote side"))); }; - let Some(session) = self.sessions.remove(session_id) else { - return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); + let Some(client) = self.clients.remove(client_id) else { + return Task::ready(Err(anyhow!("Could not find session: {:?}", client_id))); }; - for client_id in session.read(cx).client_ids().collect::>() { - session.update(cx, |this, cx| { - this.shutdown_client(client_id, cx); - }); - } + client.update(cx, |this, cx| { + this.shutdown(cx); + }); + Task::ready(Ok(())) } @@ -1199,10 +1126,9 @@ impl DapStore { cx: &mut Context, ) { for session in debug_sessions.into_iter() { - let session_id = DebugSessionId::from_proto(session.session_id); let ignore_breakpoints = Some(session.ignore_breakpoints); - self.add_remote_session(session_id, ignore_breakpoints, cx); + self.add_remote_session(ignore_breakpoints, cx); for debug_client in session.clients { if let DapStoreMode::Remote(remote) = &mut self.mode { @@ -1219,7 +1145,6 @@ impl DapStore { self.add_client_to_session(session_id, client); self.update_capabilities_for_client( - &session_id, client, &dap::proto_conversions::capabilities_from_proto( &debug_client.capabilities.unwrap_or_default(), @@ -1232,24 +1157,6 @@ impl DapStore { cx.notify(); } - async fn handle_shutdown_session_request( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - if let Some(session_id) = envelope.payload.session_id { - this.update(&mut cx, |dap_store, cx| { - dap_store.shutdown_session(&DebugSessionId::from_proto(session_id), cx) - })? - .await?; - } else { - this.update(&mut cx, |dap_store, cx| dap_store.shutdown_sessions(cx))? - .await; - } - - Ok(proto::Ack {}) - } - async fn _handle_dap_command_2( this: Entity, envelope: TypedEnvelope, @@ -1332,7 +1239,6 @@ impl DapStore { ) -> Result<()> { this.update(&mut cx, |dap_store, cx| { dap_store.update_capabilities_for_client( - &DebugSessionId::from_proto(envelope.payload.session_id), DebugAdapterClientId::from_proto(envelope.payload.client_id), &dap::proto_conversions::capabilities_from_proto(&envelope.payload), cx, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b4e0f85c8e2ea7..920eb89c4a5969 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -29,10 +29,7 @@ use git::Repository; pub mod search_history; mod yarn; -use crate::{ - debugger::dap_session::{DebugSession, DebugSessionId}, - git::GitStore, -}; +use crate::git::GitStore; use anyhow::{anyhow, Context as _, Result}; use buffer_store::{BufferStore, BufferStoreEvent}; @@ -1502,9 +1499,9 @@ impl Project { if let Some((downstream_client, project_id)) = store.downstream_client() { downstream_client .send(proto::IgnoreBreakpointState { - session_id: session_id.to_proto(), + client_id: client_id.to_proto(), project_id: *project_id, - ignore: store.ignore_breakpoints(session_id, cx), + ignore: store.ignore_breakpoints(client_id, cx), }) .log_err(); } @@ -2777,11 +2774,7 @@ impl Project { DapStoreEvent::DebugClientShutdown(client_id) => { cx.emit(Event::DebugClientShutdown(*client_id)); } - DapStoreEvent::DebugClientEvent { - session_id, - client_id, - message, - } => { + DapStoreEvent::DebugClientEvent { client_id, message } => { cx.emit(Event::DebugClientEvent { session_id: *session_id, client_id: *client_id, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index a76f200f9d0899..c9ea8a01ee33e6 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -342,32 +342,28 @@ message Envelope { DapTerminateThreadsRequest dap_terminate_threads_request = 318; DapTerminateRequest dap_terminate_request = 319; DapRestartRequest dap_restart_request = 320; - DapShutdownSession dap_shutdown_session = 321; - UpdateThreadStatus update_thread_status = 322; - VariablesRequest variables_request = 323; - DapVariables dap_variables = 324; - DapRestartStackFrameRequest dap_restart_stack_frame_request = 325; - IgnoreBreakpointState ignore_breakpoint_state = 326; - ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 327; - DebuggerSessionEnded debugger_session_ended = 328; - DapModulesRequest dap_modules_request = 329; - DapModulesResponse dap_modules_response = 330; - DapLoadedSourcesRequest dap_loaded_sources_request = 331; - DapLoadedSourcesResponse dap_loaded_sources_response = 332; - ActiveDebugSessionsRequest active_debug_sessions_request = 333; - ActiveDebugSessionsResponse active_debug_sessions_response = 334; - DapStackTraceRequest dap_stack_trace_request = 335; - DapStackTraceResponse dap_stack_trace_response = 336; - DapScopesRequest dap_scopes_request = 337; - DapScopesResponse dap_scopes_response = 338; - DapSetVariableValueRequest dap_set_variable_value_request = 339; - DapSetVariableValueResponse dap_set_variable_value_response = 340; - DapEvaluateRequest dap_evaluate_request = 341; - DapEvaluateResponse dap_evaluate_response = 342; - DapCompletionRequest dap_completion_request = 343; - DapCompletionResponse dap_completion_response = 344; - DapThreadsRequest dap_threads_request = 345; - DapThreadsResponse dap_threads_response = 346; // current max + UpdateThreadStatus update_thread_status = 321; + VariablesRequest variables_request = 322; + DapVariables dap_variables = 323; + DapRestartStackFrameRequest dap_restart_stack_frame_request = 324; + IgnoreBreakpointState ignore_breakpoint_state = 325; + ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 326; + DapModulesRequest dap_modules_request = 327; + DapModulesResponse dap_modules_response = 328; + DapLoadedSourcesRequest dap_loaded_sources_request = 329; + DapLoadedSourcesResponse dap_loaded_sources_response = 330; + DapStackTraceRequest dap_stack_trace_request = 331; + DapStackTraceResponse dap_stack_trace_response = 332; + DapScopesRequest dap_scopes_request = 333; + DapScopesResponse dap_scopes_response = 334; + DapSetVariableValueRequest dap_set_variable_value_request = 335; + DapSetVariableValueResponse dap_set_variable_value_response = 336; + DapEvaluateRequest dap_evaluate_request = 337; + DapEvaluateResponse dap_evaluate_response = 338; + DapCompletionRequest dap_completion_request = 339; + DapCompletionResponse dap_completion_response = 340; + DapThreadsRequest dap_threads_request = 341; + DapThreadsResponse dap_threads_response = 342; // current max } reserved 87 to 88; @@ -2560,51 +2556,37 @@ enum BreakpointKind { Log = 1; } -message DebuggerSessionEnded { - uint64 project_id = 1; - uint64 session_id = 2; -} - -message ActiveDebugSessionsRequest { - uint64 project_id = 1; -} -message ActiveDebugSessionsResponse { - repeated DebuggerSession sessions = 1; -} -message DebuggerSession { - uint64 session_id = 1; - bool ignore_breakpoints = 2; - repeated DebugClient clients = 3; +message ActiveDebugClientsResponse { + repeated DebugClient clients = 1; } message DebugClient { uint64 client_id = 1; SetDebugClientCapabilities capabilities = 2; - repeated SetDebuggerPanelItem debug_panel_items = 3; + bool ignore_breakpoints = 3; + repeated SetDebuggerPanelItem debug_panel_items = 4; } message ShutdownDebugClient { - uint64 session_id = 1; + uint64 project_id = 1; uint64 client_id = 2; - uint64 project_id = 3; } message SetDebugClientCapabilities { - uint64 session_id = 1; - uint64 client_id = 2; - uint64 project_id = 3; - bool supports_loaded_sources_request = 4; - bool supports_modules_request = 5; - bool supports_restart_request = 6; - bool supports_set_expression = 7; - bool supports_single_thread_execution_requests = 8; - bool supports_step_back = 9; - bool supports_stepping_granularity = 10; - bool supports_terminate_threads_request = 11; - bool supports_restart_frame_request = 12; - bool supports_clipboard_context = 13; + uint64 client_id = 1; + uint64 project_id = 2; + bool supports_loaded_sources_request = 3; + bool supports_modules_request = 4; + bool supports_restart_request = 5; + bool supports_set_expression = 6; + bool supports_single_thread_execution_requests = 7; + bool supports_step_back = 8; + bool supports_stepping_granularity = 9; + bool supports_terminate_threads_request = 10; + bool supports_restart_frame_request = 11; + bool supports_clipboard_context = 12; } message Breakpoint { @@ -2736,13 +2718,12 @@ message VariablesRequest { uint64 project_id = 1; uint64 client_id = 2; uint64 thread_id = 3; - uint64 session_id = 4; - uint64 stack_frame_id = 5; - uint64 variables_reference = 6; - optional VariablesArgumentsFilter filter = 7; - optional uint64 start = 8; - optional uint64 count = 9; - optional ValueFormat format = 10; + uint64 stack_frame_id = 4; + uint64 variables_reference = 5; + optional VariablesArgumentsFilter filter = 6; + optional uint64 start = 7; + optional uint64 count = 8; + optional ValueFormat format = 9; } @@ -2910,20 +2891,14 @@ message DapRestartStackFrameRequest { uint64 stack_frame_id = 3; } -message DapShutdownSession { - uint64 project_id = 1; - optional uint64 session_id = 2; // Shutdown all sessions if this is None -} - message ToggleIgnoreBreakpoints { uint64 project_id = 1; uint64 client_id = 2; - uint64 session_id = 3; } message IgnoreBreakpointState { uint64 project_id = 1; - uint64 session_id = 2; + uint64 client_id = 2; bool ignore = 3; } @@ -3034,24 +3009,22 @@ message DebuggerModuleList { message SetDebuggerPanelItem { uint64 project_id = 1; - uint64 session_id = 2; - uint64 client_id = 3; - uint64 thread_id = 4; - string session_name = 5; - DebuggerConsole console = 6; - optional DebuggerModuleList module_list = 7; // We only send this when it's supported and once per client - DebuggerThreadItem active_thread_item = 8; - DebuggerThreadState thread_state = 9; - DebuggerVariableList variable_list = 10; - DebuggerStackFrameList stack_frame_list = 11; - DebuggerLoadedSourceList loaded_source_list = 12; + uint64 client_id = 2; + uint64 thread_id = 3; + string session_name = 4; + DebuggerConsole console = 5; + optional DebuggerModuleList module_list = 6; // We only send this when it's supported and once per client + DebuggerThreadItem active_thread_item = 7; + DebuggerThreadState thread_state = 8; + DebuggerVariableList variable_list = 9; + DebuggerStackFrameList stack_frame_list = 10; + DebuggerLoadedSourceList loaded_source_list = 11; } message UpdateDebugAdapter { uint64 project_id = 1; uint64 client_id = 2; optional uint64 thread_id = 3; - uint64 session_id = 4; oneof variant { DebuggerThreadState thread_state = 5; DebuggerStackFrameList stack_frame_list = 6; @@ -3061,8 +3034,6 @@ message UpdateDebugAdapter { } } - - message DapVariables { uint64 client_id = 1; repeated DapVariable variables = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index d7fc560522f0e1..486c508ce6021e 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -249,7 +249,6 @@ messages!( (DapPauseRequest, Background), (DapRestartRequest, Background), (DapRestartStackFrameRequest, Background), - (DapShutdownSession, Background), (DapStepBackRequest, Background), (DapStepInRequest, Background), (DapStepOutRequest, Background), @@ -473,9 +472,6 @@ messages!( (DapVariables, Background), (IgnoreBreakpointState, Background), (ToggleIgnoreBreakpoints, Background), - (DebuggerSessionEnded, Background), - (ActiveDebugSessionsRequest, Foreground), - (ActiveDebugSessionsResponse, Foreground), (DapStackTraceRequest, Background), (DapStackTraceResponse, Background), (DapScopesRequest, Background), @@ -637,9 +633,7 @@ request_messages!( (DapTerminateRequest, Ack), (DapRestartRequest, Ack), (DapRestartStackFrameRequest, Ack), - (DapShutdownSession, Ack), (VariablesRequest, DapVariables), - (ActiveDebugSessionsRequest, ActiveDebugSessionsResponse), (DapStackTraceRequest, DapStackTraceResponse), (DapScopesRequest, DapScopesResponse), (DapSetVariableValueRequest, DapSetVariableValueResponse), @@ -759,13 +753,10 @@ entity_messages!( DapTerminateRequest, DapRestartRequest, DapRestartStackFrameRequest, - DapShutdownSession, UpdateThreadStatus, VariablesRequest, IgnoreBreakpointState, ToggleIgnoreBreakpoints, - DebuggerSessionEnded, - ActiveDebugSessionsRequest, DapStackTraceRequest, DapScopesRequest, DapSetVariableValueRequest, From 936ac212e71b96bc2fdfaf321205b1f2299e260f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:18:37 +0100 Subject: [PATCH 547/650] Another WIP --- crates/debugger_tools/src/dap_log.rs | 6 +- crates/debugger_ui/src/attach_modal.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 11 +- crates/project/src/debugger/client.rs | 75 +---------- crates/project/src/debugger/dap_store.rs | 160 +++++++++-------------- crates/project/src/project.rs | 12 +- 6 files changed, 75 insertions(+), 191 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 14f20ffd4b6bb5..5b9067d4986c61 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -12,11 +12,7 @@ use gpui::{ actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; -use project::{ - debugger::dap_session::{DebugSession, DebugSessionId}, - search::SearchQuery, - Project, -}; +use project::{search::SearchQuery, Project}; use settings::Settings as _; use std::{ borrow::Cow, diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 2202f71a3c3339..3f4bc1524f0a2c 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::Subscription; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; use picker::{Picker, PickerDelegate}; -use project::debugger::{dap_session::DebugSessionId, dap_store::DapStore}; +use project::debugger::dap_store::DapStore; use std::sync::Arc; use sysinfo::System; use ui::{prelude::*, Context, Tooltip}; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 708dd3a8b7f0ef..2d4ca31d2cc5af 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -17,7 +17,7 @@ use gpui::{ }; use project::{ debugger::{ - dap_session::{DebugSessionId, ThreadId}, + dap_session::ThreadId, dap_store::{DapStore, DapStoreEvent}, }, terminals::TerminalKind, @@ -412,7 +412,6 @@ impl DebugPanel { fn handle_start_debugging_request( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, seq: u64, request_args: Option, @@ -433,7 +432,6 @@ impl DebugPanel { fn handle_run_in_terminal_request( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, seq: u64, request_args: Option, @@ -596,7 +594,6 @@ impl DebugPanel { fn handle_debug_client_started( &self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, window: &mut Window, cx: &mut Context, @@ -678,7 +675,6 @@ impl DebugPanel { fn handle_debug_client_events( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, event: &Events, window: &mut Window, @@ -716,7 +712,6 @@ impl DebugPanel { fn handle_initialized_event( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, capabilities: &Option, cx: &mut Context, @@ -735,7 +730,7 @@ impl DebugPanel { this.update(&mut cx, |debug_panel, cx| { debug_panel.workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |project, cx| { - project.initial_send_breakpoints(&session_id, client_id, cx) + project.initial_send_breakpoints(client_id, cx) }) }) })?? @@ -762,7 +757,6 @@ impl DebugPanel { fn handle_stopped_event( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, event: &StoppedEvent, window: &mut Window, @@ -889,7 +883,6 @@ impl DebugPanel { fn handle_terminated_event( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, event: &Option, cx: &mut Context, diff --git a/crates/project/src/debugger/client.rs b/crates/project/src/debugger/client.rs index 275add8c242602..1ac2125deda1a6 100644 --- a/crates/project/src/debugger/client.rs +++ b/crates/project/src/debugger/client.rs @@ -334,6 +334,9 @@ impl Client { pub fn capabilities(&self) -> &Capabilities { &self.capabilities } + pub fn configuration(&self) -> DebugAdapterConfig { + Configuration::default() + } pub(crate) fn _wait_for_request( &self, @@ -912,75 +915,3 @@ impl Client { } } } - -impl DebugSession { - pub fn ignore_breakpoints(&self) -> bool { - self.ignore_breakpoints - } - - pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut Context) { - self.ignore_breakpoints = ignore; - cx.notify(); - } - - pub fn client_state(&self, client_id: DebugAdapterClientId) -> Option> { - self.states.get(&client_id).cloned() - } - - pub(super) fn client_ids(&self) -> impl Iterator + '_ { - self.states.keys().copied() - } - - pub fn clients(&self, cx: &App) -> Vec> { - self.states - .values() - .filter_map(|state| state.read(cx).adapter_client()) - .collect() - } - - pub fn add_client( - &mut self, - client: impl Into, - client_id: DebugAdapterClientId, - cx: &mut Context, - ) { - if !self.states.contains_key(&client_id) { - let mode = client.into(); - let state = cx.new(|_cx| Client { - client_id, - modules: Vec::default(), - loaded_sources: Vec::default(), - threads: IndexMap::default(), - requests: HashMap::default(), - capabilities: Default::default(), - mode, - }); - - self.states.insert(client_id, state); - } - } - - pub(crate) fn client_by_id( - &self, - client_id: impl Borrow, - ) -> Option> { - self.states.get(client_id.borrow()).cloned() - } - - pub(crate) fn shutdown_client( - &mut self, - client_id: DebugAdapterClientId, - cx: &mut Context, - ) { - if let Some(client) = self.states.remove(&client_id) { - client.update(cx, |this, cx| { - this.shutdown(cx); - }) - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn clients_len(&self) -> usize { - self.states.len() - } -} diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 5052ec67b0ae03..88e04f136cf642 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -51,10 +51,7 @@ use std::{ ffi::OsStr, hash::Hash, path::{Path, PathBuf}, - sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, - Arc, - }, + sync::{atomic::Ordering::SeqCst, Arc}, }; use std::{collections::VecDeque, sync::atomic::AtomicU32}; use task::{AttachConfig, DebugAdapterConfig, DebugRequestType}; @@ -62,7 +59,7 @@ use util::{merge_json_value_into, ResultExt as _}; use worktree::Worktree; pub enum DapStoreEvent { - DebugClientStarted((DebugAdapterClientId)), + DebugClientStarted(DebugAdapterClientId), DebugClientShutdown(DebugAdapterClientId), DebugClientEvent { client_id: DebugAdapterClientId, @@ -126,7 +123,6 @@ impl DapStore { client.add_entity_message_handler(Self::handle_update_debug_adapter); client.add_entity_message_handler(Self::handle_update_thread_status); client.add_entity_message_handler(Self::handle_ignore_breakpoint_state); - client.add_entity_message_handler(Self::handle_session_has_shutdown); // todo(debugger): Reenable these after we finish handle_dap_command refactor // client.add_entity_request_handler(Self::handle_dap_command::); @@ -141,7 +137,6 @@ impl DapStore { // client.add_entity_request_handler(Self::handle_dap_command::); // client.add_entity_request_handler(Self::handle_dap_command::); // client.add_entity_request_handler(Self::handle_dap_command::); - client.add_entity_request_handler(Self::handle_shutdown_session_request); } pub fn new_local( @@ -154,7 +149,7 @@ impl DapStore { breakpoint_store: Entity, cx: &mut Context, ) -> Self { - cx.on_app_quit(Self::shutdown_sessions).detach(); + cx.on_app_quit(Self::shutdown_clients).detach(); Self { mode: DapStoreMode::Local(LocalDapStore { @@ -334,7 +329,7 @@ impl DapStore { if let Some((client, project_id)) = self.downstream_client.clone() { client - .send(client::proto::RemoveActiveDebugLine { project_id }) + .send(::client::proto::RemoveActiveDebugLine { project_id }) .log_err(); } } @@ -365,11 +360,11 @@ impl DapStore { pub fn set_ignore_breakpoints( &mut self, - session_id: &DebugAdapterClientId, + client_id: &DebugAdapterClientId, ignore: bool, cx: &mut Context, ) { - if let Some(session) = self.client_by_id(session_id) { + if let Some(session) = self.client_by_id(client_id) { session.update(cx, |session, _| { session.set_ignore_breakpoints(ignore); }); @@ -524,11 +519,7 @@ impl DapStore { move |message, cx| { dap_store .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { - session_id, - client_id, - message, - }) + cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) }) .log_err(); } @@ -546,7 +537,7 @@ impl DapStore { config: DebugAdapterConfig, worktree: &Entity, cx: &mut Context, - ) -> Task, Arc)>> { + ) -> Task>> { let Some(local_store) = self.as_local() else { return Task::ready(Err(anyhow!("cannot start session on remote side"))); }; @@ -564,8 +555,7 @@ impl DapStore { }), ); - let start_client_task = - self.start_client_internal(session_id, delegate, config.clone(), cx); + let start_client_task = self.start_client_internal(delegate, config.clone(), cx); cx.spawn(|this, mut cx| async move { let session = cx.new(|_| DebugSession::new_local(session_id, config))?; @@ -589,10 +579,9 @@ impl DapStore { let client_id = client.id(); - store.client_by_session.insert(client_id, session_id); store.sessions.insert(session_id, session.clone()); - cx.emit(DapStoreEvent::DebugClientStarted((session_id, client_id))); + cx.emit(DapStoreEvent::DebugClientStarted(client_id)); cx.notify(); (session, client) @@ -679,11 +668,7 @@ impl DapStore { .client_by_id(client_id) .and_then(|client| Some(client.read(cx).adapter_client()?)) else { - return Task::ready(Err(anyhow!( - "Could not find debug client: {:?} for session {:?}", - client_id, - session_id - ))); + return Task::ready(Err(anyhow!("Could not find debug client: {:?}", client_id))); }; let config = session.read(cx).as_local().unwrap().configuration(); @@ -716,20 +701,17 @@ impl DapStore { pub fn attach( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, process_id: u32, cx: &mut Context, ) -> Task> { - let Some((session, client)) = self - .client_by_id(client_id, cx) - .and_then(|(session, client)| Some((session, client.read(cx).adapter_client()?))) + let Some(client) = self + .client_by_id(client_id) + .and_then(|client| Some(client.read(cx).adapter_client()?)) else { - return Task::ready(Err(anyhow!( - "Could not find debug client: {:?} for session {:?}", - client_id, - session_id - ))); + return Task::ready(Err( + anyhow!("Could not find debug client: {:?}", client_id,), + )); }; // update the process id on the config, so when the `startDebugging` reverse request @@ -762,30 +744,21 @@ impl DapStore { pub fn respond_to_start_debugging( &mut self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, seq: u64, args: Option, cx: &mut Context, ) -> Task> { - let Some((session, client)) = self - .client_by_id(client_id, cx) - .and_then(|(session, client)| Some((session, client.read(cx).adapter_client()?))) + let Some((client, adapter)) = self + .client_by_id(client_id) + .and_then(|client| Some((client, client.read(cx).adapter_client()?))) else { - return Task::ready(Err(anyhow!( - "Could not find debug client: {:?} for session {:?}", - client_id, - session_id - ))); + return Task::ready(Err( + anyhow!("Could not find debug client: {:?}", client_id,), + )); }; - let Some(config) = session - .read(cx) - .as_local() - .map(|session| session.configuration()) - else { - return Task::ready(Err(anyhow!("Cannot find debug session: {:?}", session_id))); - }; + let config = client.read(cx).configuration(); let session_id = *session_id; @@ -830,7 +803,6 @@ impl DapStore { ))) } else { store.reconnect_client( - &session_id, client.adapter().clone(), client.binary().clone(), new_config, @@ -895,7 +867,6 @@ impl DapStore { pub fn respond_to_run_in_terminal( &self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, success: bool, seq: u64, @@ -903,14 +874,10 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id, cx) - .and_then(|(_, client)| client.read(cx).adapter_client()) + .client_by_id(client_id) + .and_then(|client| client.read(cx).adapter_client()) else { - return Task::ready(Err(anyhow!( - "Could not find debug client: {:?} for session {:?}", - client_id, - session_id - ))); + return Task::ready(Err(anyhow!("Could not find debug client: {:?}", client_id))); }; cx.background_executor().spawn(async move { @@ -936,8 +903,8 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id, cx) - .and_then(|(_, client)| client.read(cx).adapter_client()) + .client_by_id(client_id) + .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -966,8 +933,8 @@ impl DapStore { cx: &mut Context, ) -> Task>> { let Some(client) = self - .client_by_id(client_id, cx) - .and_then(|(_, client)| client.read(cx).adapter_client()) + .client_by_id(client_id) + .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1039,7 +1006,7 @@ impl DapStore { // client.wait_for_request(request::Modules); // This ensures that the request that we've fired off runs to completions // let returned_value = client.modules(); // this is a cheap getter. - pub fn shutdown_sessions(&mut self, cx: &mut Context) -> Task<()> { + pub fn shutdown_clients(&mut self, cx: &mut Context) -> Task<()> { let Some(_) = self.as_local() else { if let Some((upstream_client, project_id)) = self.upstream_client() { return cx.background_executor().spawn(async move { @@ -1059,8 +1026,8 @@ impl DapStore { let mut tasks = Vec::new(); - for session_id in self.sessions.keys().cloned().collect::>() { - tasks.push(self.shutdown_client(&session_id, cx)); + for client_id in self.clients.keys().cloned().collect::>() { + tasks.push(self.shutdown_client(&client_id, cx)); } cx.background_executor().spawn(async move { @@ -1169,13 +1136,17 @@ impl DapStore { let request = T::from_proto(&envelope.payload); let client_id = T::client_id_from_proto(&envelope.payload); - let _state = this.update(&mut cx, |this, cx| { - this.session_by_client_id(client_id)? - .read(cx) - .client_state(client_id)? - .read(cx) - ._wait_for_request(request) - }); + let _state = this + .update(&mut cx, |this, cx| { + this.client_by_id(client_id)? + .read(cx) + ._wait_for_request(request) + }) + .ok() + .flatten(); + if let Some(_state) = _state { + let _ = _state.await; + } todo!() } @@ -1254,9 +1225,9 @@ impl DapStore { this.update(&mut cx, |dap_store, cx| { let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); - dap_store.session_by_client_id(client_id).map(|state| { - state.update(cx, |this, _| { - this.states.remove(&client_id); + dap_store.client_by_id(client_id).map(|state| { + state.update(cx, |state, cx| { + state.shutdown(cx); }) }); @@ -1312,8 +1283,8 @@ impl DapStore { cx: &App, ) -> Task> { let Some(client) = self - .client_by_id(client_id, cx) - .and_then(|(_, client)| client.read(cx).adapter_client()) + .client_by_id(client_id) + .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; @@ -1367,23 +1338,22 @@ impl DapStore { .collect::>(); let mut tasks = Vec::new(); - for session in self - .sessions - .values() - .filter(|session| session.read(cx).as_local().is_some()) + for (client_id, client) in self + .clients + .iter() + .filter(|(_, client)| client.read(cx).adapter_client().is_some()) { - let session = session.read(cx); - let ignore_breakpoints = session.ignore_breakpoints(); - for client_id in session.client_ids().collect::>() { - tasks.push(self.send_breakpoints( - client_id, - Arc::from(absolute_path.clone()), - source_breakpoints.clone(), - ignore_breakpoints, - source_changed, - cx, - )); - } + let client = client.read(cx); + let ignore_breakpoints = !client.breakpoints_enabled(); + + tasks.push(self.send_breakpoints( + *client_id, + Arc::from(absolute_path.clone()), + source_breakpoints.clone(), + ignore_breakpoints, + source_changed, + cx, + )); } if tasks.is_empty() { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 920eb89c4a5969..788c54546deadf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -275,12 +275,11 @@ pub enum Event { notification_id: SharedString, }, LanguageServerPrompt(LanguageServerPromptRequest), - DebugClientStarted((DebugSessionId, DebugAdapterClientId)), + DebugClientStarted(DebugAdapterClientId), DebugClientShutdown(DebugAdapterClientId), SetDebugClient(SetDebuggerPanelItem), ActiveDebugLineChanged, DebugClientEvent { - session_id: DebugSessionId, client_id: DebugAdapterClientId, message: Message, }, @@ -1339,7 +1338,6 @@ impl Project { pub fn initial_send_breakpoints( &self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, cx: &mut Context, ) -> Task<()> { @@ -1376,7 +1374,7 @@ impl Project { &mut self, config: DebugAdapterConfig, cx: &mut Context, - ) -> Task, Arc)>> { + ) -> Task>> { let worktree = maybe!({ if let Some(cwd) = &config.cwd { Some(self.find_worktree(cwd.as_path(), cx)?.0) @@ -1466,7 +1464,6 @@ impl Project { if let Some((_, _)) = project.dap_store.read(cx).downstream_client() { project .toggle_ignore_breakpoints( - &DebugSessionId::from_proto(envelope.payload.session_id), DebugAdapterClientId::from_proto(envelope.payload.client_id), cx, ) @@ -1477,7 +1474,6 @@ impl Project { pub fn toggle_ignore_breakpoints( &self, - session_id: &DebugSessionId, client_id: DebugAdapterClientId, cx: &mut Context, ) -> Task> { @@ -1485,7 +1481,6 @@ impl Project { if let Some((upstream_client, project_id)) = store.upstream_client() { upstream_client .send(proto::ToggleIgnoreBreakpoints { - session_id: session_id.to_proto(), client_id: client_id.to_proto(), project_id, }) @@ -1501,7 +1496,7 @@ impl Project { .send(proto::IgnoreBreakpointState { client_id: client_id.to_proto(), project_id: *project_id, - ignore: store.ignore_breakpoints(client_id, cx), + ignore: store.ignore_breakpoints(&client_id, cx), }) .log_err(); } @@ -2776,7 +2771,6 @@ impl Project { } DapStoreEvent::DebugClientEvent { client_id, message } => { cx.emit(Event::DebugClientEvent { - session_id: *session_id, client_id: *client_id, message: message.clone(), }); From cdb5af2906807f04e746a957fe67c7d7d2085913 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 14 Feb 2025 15:38:32 -0500 Subject: [PATCH 548/650] Remove project from toggle breakpoint dependencies Project is no longer responsible for toggle breakpoints or telling breakpoint store to toggle breakpoints. Now editor toggles breakpoints by directly telling breakpoint store too. In the future I plan on removing on breakpoint related handling from project to breakpoint store. I also fix some debugger related test compile errors. Plenty of them won't pass because we're still in a refactor, but they build now --- crates/collab/src/tests/editor_tests.rs | 16 +-- .../debugger_ui/src/tests/debugger_panel.rs | 34 +++--- crates/editor/src/editor.rs | 33 ++++-- crates/editor/src/editor_tests.rs | 20 ++-- crates/project/src/buffer_store.rs | 37 +++--- .../project/src/debugger/breakpoint_store.rs | 112 +++++++++++++++++- crates/project/src/lsp_store.rs | 1 + crates/project/src/project.rs | 65 +++++----- crates/remote_server/src/headless_project.rs | 19 +-- 9 files changed, 220 insertions(+), 117 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index da5283c8c905f3..f493fc812847cd 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2472,7 +2472,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor - .dap_store + .breakpoint_store() .clone() .unwrap() .read(cx) @@ -2481,7 +2481,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor - .dap_store + .breakpoint_store() .clone() .unwrap() .read(cx) @@ -2505,7 +2505,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor - .dap_store + .breakpoint_store() .clone() .unwrap() .read(cx) @@ -2514,7 +2514,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor - .dap_store + .breakpoint_store() .clone() .unwrap() .read(cx) @@ -2538,7 +2538,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor - .dap_store + .breakpoint_store() .clone() .unwrap() .read(cx) @@ -2547,7 +2547,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor - .dap_store + .breakpoint_store() .clone() .unwrap() .read(cx) @@ -2571,7 +2571,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte let breakpoints_a = editor_a.update(cx_a, |editor, cx| { editor - .dap_store + .breakpoint_store() .clone() .unwrap() .read(cx) @@ -2580,7 +2580,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte }); let breakpoints_b = editor_b.update(cx_b, |editor, cx| { editor - .dap_store + .breakpoint_store() .clone() .unwrap() .read(cx) diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 1f14d0f718e397..35ae2f39c64115 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -15,7 +15,7 @@ use editor::{ }; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use project::{ - debugger::dap_store::{Breakpoint, BreakpointEditAction, BreakpointKind}, + debugger::breakpoint_store::{Breakpoint, BreakpointEditAction, BreakpointKind}, FakeFs, Project, }; use serde_json::json; @@ -1306,21 +1306,23 @@ async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( // add breakpoint for file/buffer that has not been opened yet project.update(cx, |project, cx| { - project.dap_store().update(cx, |dap_store, cx| { - dap_store.toggle_breakpoint_for_buffer( - &project::ProjectPath { - worktree_id, - path: Arc::from(Path::new(&"main.rs")), - }, - Breakpoint { - active_position: None, - cached_position: 1, - kind: BreakpointKind::Standard, - }, - BreakpointEditAction::Toggle, - cx, - ); - }); + project + .breakpoint_store() + .update(cx, |breakpoint_store, cx| { + breakpoint_store.toggle_breakpoint_for_buffer( + &project::ProjectPath { + worktree_id, + path: Arc::from(Path::new(&"main.rs")), + }, + Breakpoint { + active_position: None, + cached_position: 1, + kind: BreakpointKind::Standard, + }, + BreakpointEditAction::Toggle, + cx, + ); + }); }); cx.run_until_parked(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f9d4277d34d5fb..9390106ea63ef5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -108,7 +108,9 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange} use linked_editing_ranges::refresh_linked_ranges; use mouse_context_menu::MouseContextMenu; use project::{ - debugger::breakpoint_store::{BreakpointEditAction, BreakpointStoreEvent}, + debugger::breakpoint_store::{ + self, BreakpointEditAction, BreakpointStore, BreakpointStoreEvent, + }, ProjectPath, }; pub use proposed_changes_editor::{ @@ -762,6 +764,7 @@ pub struct Editor { tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, pub dap_store: Option>, + pub breakpoint_store: Option>, /// Allow's a user to create a breakpoint by selecting this indicator /// It should be None while a user is not hovering over the gutter /// Otherwise it represents the point that the breakpoint will be shown @@ -1332,11 +1335,15 @@ impl Editor { None }; - let dap_store = if mode == EditorMode::Full { - project.as_ref().map(|project| project.read(cx).dap_store()) - } else { - None + let (dap_store, breakpoint_store) = match (mode, project.as_ref()) { + (EditorMode::Full, Some(project)) => { + let dap_store = project.read(cx).dap_store(); + let breakpoint_store = project.read(cx).breakpoint_store(); + (Some(dap_store), Some(breakpoint_store)) + } + _ => (None, None), }; + let mut code_action_providers = Vec::new(); let mut load_uncommitted_diff = None; if let Some(project) = project.clone() { @@ -1466,6 +1473,7 @@ impl Editor { blame_subscription: None, tasks: Default::default(), dap_store, + breakpoint_store, gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -7428,14 +7436,10 @@ impl Editor { edit_action: BreakpointEditAction, cx: &mut Context, ) { - let Some(project) = &self.project else { + let Some(breakpoint_store) = &self.breakpoint_store else { return; }; - if self.dap_store.is_none() { - return; - } - let Some(buffer_id) = breakpoint_position.buffer_id else { return; }; @@ -7451,8 +7455,8 @@ impl Editor { return; }; - project.update(cx, |project, cx| { - project.toggle_breakpoint( + breakpoint_store.update(cx, |breakpoint_store, cx| { + breakpoint_store.toggle_breakpoint( buffer_id, Breakpoint { cached_position: cache_position, @@ -7467,6 +7471,11 @@ impl Editor { cx.notify(); } + #[cfg(any(test, feature = "test-support"))] + pub fn breakpoint_store(&self) -> Option> { + self.breakpoint_store.clone() + } + pub fn prepare_revert_change( &self, revert_changes: &mut HashMap, Rope)>>, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b220bb51516acb..c2b09c737993ed 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -29,7 +29,7 @@ use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_ne}; use project::FakeFs; use project::{ - debugger::dap_store::BreakpointKind, + debugger::breakpoint_store::BreakpointKind, lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT, project_settings::{LspSettings, ProjectSettings}, }; @@ -15719,7 +15719,7 @@ async fn assert_highlighted_edits( #[track_caller] fn assert_breakpoint( - breakpoints: &BTreeMap>, + breakpoints: &BTreeMap>, project_path: &ProjectPath, expected: Vec<(u32, BreakpointKind)>, ) { @@ -15839,7 +15839,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .as_ref() .unwrap() .read(cx) - .dap_store() + .breakpoint_store() .read(cx) .breakpoints() .clone() @@ -15863,7 +15863,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .as_ref() .unwrap() .read(cx) - .dap_store() + .breakpoint_store() .read(cx) .breakpoints() .clone() @@ -15887,7 +15887,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .as_ref() .unwrap() .read(cx) - .dap_store() + .breakpoint_store() .read(cx) .breakpoints() .clone() @@ -15951,7 +15951,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .as_ref() .unwrap() .read(cx) - .dap_store() + .breakpoint_store() .read(cx) .breakpoints() .clone() @@ -15974,7 +15974,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .as_ref() .unwrap() .read(cx) - .dap_store() + .breakpoint_store() .read(cx) .breakpoints() .clone() @@ -15996,7 +15996,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .as_ref() .unwrap() .read(cx) - .dap_store() + .breakpoint_store() .read(cx) .breakpoints() .clone() @@ -16018,7 +16018,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .as_ref() .unwrap() .read(cx) - .dap_store() + .breakpoint_store() .read(cx) .breakpoints() .clone() @@ -16043,7 +16043,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .as_ref() .unwrap() .read(cx) - .dap_store() + .breakpoint_store() .read(cx) .breakpoints() .clone() diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index dbd66df87f98bf..93af53a69db83e 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1,5 +1,4 @@ use crate::{ - debugger::breakpoint_store::BreakpointStore, lsp_store::OpenLspBufferHandle, search::SearchQuery, worktree_store::{WorktreeStore, WorktreeStoreEvent}, @@ -317,7 +316,6 @@ struct RemoteBufferStore { struct LocalBufferStore { local_buffer_ids_by_path: HashMap, local_buffer_ids_by_entry_id: HashMap, - breakpoint_store: Entity, worktree_store: Entity, _subscription: Subscription, } @@ -332,6 +330,10 @@ enum OpenBuffer { pub enum BufferStoreEvent { BufferAdded(Entity), + BufferOpened { + buffer: Entity, + project_path: ProjectPath, + }, BufferDropped(BufferId), BufferChangedFilePath { buffer: Entity, @@ -1240,16 +1242,11 @@ impl BufferStore { } /// Creates a buffer store, optionally retaining its buffers. - pub fn local( - worktree_store: Entity, - breakpoint_store: Entity, - cx: &mut Context, - ) -> Self { + pub fn local(worktree_store: Entity, cx: &mut Context) -> Self { Self { state: BufferStoreState::Local(LocalBufferStore { local_buffer_ids_by_path: Default::default(), local_buffer_ids_by_entry_id: Default::default(), - breakpoint_store, worktree_store: worktree_store.clone(), _subscription: cx.subscribe(&worktree_store, |this, _, event, cx| { if let WorktreeStoreEvent::WorktreeAdded(worktree) = event { @@ -1291,19 +1288,6 @@ impl BufferStore { } } - pub fn breakpoint_store_on_buffer_open( - &mut self, - project_path: &ProjectPath, - buffer: &Entity, - cx: &mut Context, - ) { - if let Some(local_store) = self.as_local_mut() { - local_store.breakpoint_store.update(cx, |store, cx| { - store.on_open_buffer(&project_path, buffer, cx); - }); - } - } - fn as_local(&self) -> Option<&LocalBufferStore> { match &self.state { BufferStoreState::Local(state) => Some(state), @@ -1338,7 +1322,10 @@ impl BufferStore { cx: &mut Context, ) -> Task>> { if let Some(buffer) = self.get_by_path(&project_path, cx) { - self.breakpoint_store_on_buffer_open(&project_path, &buffer, cx); + cx.emit(BufferStoreEvent::BufferOpened { + buffer: buffer.clone(), + project_path, + }); return Task::ready(Ok(buffer)); } @@ -1368,7 +1355,11 @@ impl BufferStore { this.loading_buffers.remove(&project_path); let buffer = load_result.map_err(Arc::new)?; - this.breakpoint_store_on_buffer_open(&project_path, &buffer, cx); + cx.emit(BufferStoreEvent::BufferOpened { + buffer: buffer.clone(), + project_path, + }); + Ok(buffer) })? }) diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index c71d533bf68094..52438322b2a89d 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -1,4 +1,7 @@ -use crate::{ProjectItem as _, ProjectPath}; +use crate::{ + buffer_store::{BufferStore, BufferStoreEvent}, + BufferId, ProjectItem as _, ProjectPath, WorktreeStore, +}; use anyhow::{Context as _, Result}; use collections::{BTreeMap, HashSet}; use dap::SourceBreakpoint; @@ -29,6 +32,8 @@ enum BreakpointMode { pub struct BreakpointStore { pub breakpoints: BTreeMap>, + buffer_store: Entity, + worktree_store: Entity, downstream_client: Option<(AnyProtoClient, u64)>, mode: BreakpointMode, } @@ -47,17 +52,37 @@ impl BreakpointStore { client.add_entity_message_handler(Self::handle_synchronize_breakpoints); } - pub fn local() -> Self { + pub fn local( + buffer_store: Entity, + worktree_store: Entity, + cx: &mut Context, + ) -> Self { + cx.subscribe(&buffer_store, Self::handle_buffer_event) + .detach(); + BreakpointStore { breakpoints: BTreeMap::new(), + buffer_store, + worktree_store, mode: BreakpointMode::Local, downstream_client: None, } } - pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self { + pub(crate) fn remote( + upstream_project_id: u64, + upstream_client: AnyProtoClient, + buffer_store: Entity, + worktree_store: Entity, + cx: &mut Context, + ) -> Self { + cx.subscribe(&buffer_store, Self::handle_buffer_event) + .detach(); + BreakpointStore { breakpoints: BTreeMap::new(), + buffer_store, + worktree_store, mode: BreakpointMode::Remote(RemoteBreakpointStore { upstream_client: Some(upstream_client), upstream_project_id, @@ -130,6 +155,82 @@ impl BreakpointStore { cx.notify(); } + pub fn toggle_breakpoint( + &mut self, + buffer_id: BufferId, + mut breakpoint: Breakpoint, + edit_action: BreakpointEditAction, + cx: &mut Context, + ) { + let Some(project_path) = self + .buffer_store + .read(cx) + .get(buffer_id) + .and_then(|buffer| buffer.read(cx).project_path(cx)) + else { + return; + }; + + let upstream_client = self.upstream_client(); + let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); + + match edit_action { + BreakpointEditAction::Toggle => { + if !breakpoint_set.remove(&breakpoint) { + breakpoint_set.insert(breakpoint); + } + } + BreakpointEditAction::EditLogMessage(log_message) => { + if !log_message.is_empty() { + breakpoint.kind = BreakpointKind::Log(log_message.clone()); + breakpoint_set.remove(&breakpoint); + breakpoint_set.insert(breakpoint); + } else if matches!(&breakpoint.kind, BreakpointKind::Log(_)) { + breakpoint_set.remove(&breakpoint); + } + } + } + + if let Some((client, project_id)) = upstream_client.or(self.downstream_client.clone()) { + client + .send(client::proto::SynchronizeBreakpoints { + project_id, + project_path: Some(project_path.to_proto()), + breakpoints: breakpoint_set + .iter() + .filter_map(|breakpoint| breakpoint.to_proto()) + .collect(), + }) + .log_err(); + } + + if breakpoint_set.is_empty() { + self.breakpoints.remove(&project_path); + } + + cx.emit(BreakpointStoreEvent::BreakpointsChanged { + project_path: project_path.clone(), + source_changed: false, + }); + + cx.notify(); + } + + fn handle_buffer_event( + &mut self, + _buffer_store: Entity, + event: &BufferStoreEvent, + cx: &mut Context, + ) { + match event { + BufferStoreEvent::BufferOpened { + buffer, + project_path, + } => self.on_open_buffer(&project_path, &buffer, cx), + _ => {} + } + } + pub fn on_open_buffer( &mut self, project_path: &ProjectPath, @@ -321,6 +422,11 @@ impl BreakpointStore { cx.notify(); }) } + + #[cfg(any(test, feature = "test-support"))] + pub fn breakpoints(&self) -> &BTreeMap> { + &self.breakpoints + } } type LogMessage = Arc; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index dedc42b27bb2ea..8494f2c8bcad36 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3120,6 +3120,7 @@ impl LspStore { } } BufferStoreEvent::BufferDropped(_) => {} + BufferStoreEvent::BufferOpened { .. } => {} } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b4e0f85c8e2ea7..39625ca8a75720 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -694,7 +694,12 @@ impl Project { ) }); - let breakpoint_store = cx.new(|_| BreakpointStore::local()); + let buffer_store = cx.new(|cx| BufferStore::local(worktree_store.clone(), cx)); + cx.subscribe(&buffer_store, Self::on_buffer_store_event) + .detach(); + + let breakpoint_store = cx + .new(|cx| BreakpointStore::local(buffer_store.clone(), worktree_store.clone(), cx)); let dap_store = cx.new(|cx| { DapStore::new_local( @@ -712,11 +717,6 @@ impl Project { cx.subscribe(&breakpoint_store, Self::on_breakpoint_store_event) .detach(); - let buffer_store = cx - .new(|cx| BufferStore::local(worktree_store.clone(), breakpoint_store.clone(), cx)); - cx.subscribe(&buffer_store, Self::on_buffer_store_event) - .detach(); - let image_store = cx.new(|cx| ImageStore::local(worktree_store.clone(), cx)); cx.subscribe(&image_store, Self::on_image_store_event) .detach(); @@ -891,8 +891,15 @@ impl Project { }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - let breakpoint_store = - cx.new(|_| BreakpointStore::remote(SSH_PROJECT_ID, client.clone().into())); + let breakpoint_store = cx.new(|cx| { + BreakpointStore::remote( + SSH_PROJECT_ID, + client.clone().into(), + buffer_store.clone(), + worktree_store.clone(), + cx, + ) + }); let dap_store = cx.new(|_| { DapStore::new_remote( @@ -1084,7 +1091,16 @@ impl Project { let environment = cx.update(|cx| ProjectEnvironment::new(&worktree_store, None, cx))?; let breakpoint_store = cx.new(|cx| { - let mut bp_store = BreakpointStore::remote(SSH_PROJECT_ID, client.clone().into()); + let mut bp_store = { + BreakpointStore::remote( + SSH_PROJECT_ID, + client.clone().into(), + buffer_store.clone(), + worktree_store.clone(), + cx, + ) + }; + bp_store.set_breakpoints_from_proto(response.payload.breakpoints, cx); bp_store })?; @@ -1553,37 +1569,13 @@ impl Project { }) } - /// Sends updated breakpoint information of one file to all active debug adapters + /// Toggles a breakpoint + /// + /// Also sends updated breakpoint information of one file to all active debug adapters /// /// This function is called whenever a breakpoint is toggled, and it doesn't need /// to send breakpoints from closed files because those breakpoints can't change /// without opening a buffer. - pub fn toggle_breakpoint( - &self, - buffer_id: BufferId, - breakpoint: Breakpoint, - edit_action: BreakpointEditAction, - cx: &mut Context, - ) { - let Some(buffer) = self.buffer_for_id(buffer_id, cx) else { - return; - }; - - if let Some(project_path) = buffer.read(cx).project_path(cx) { - self.dap_store.update(cx, |dap_store, cx| { - dap_store - .breakpoint_store() - .update(cx, |breakpoint_store, cx| { - breakpoint_store.toggle_breakpoint_for_buffer( - &project_path, - breakpoint, - edit_action, - cx, - ) - }) - }); - } - } #[cfg(any(test, feature = "test-support"))] pub async fn example( @@ -2707,6 +2699,7 @@ impl Project { .log_err(); } } + BufferStoreEvent::BufferOpened { .. } => {} } } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index a320ad0748c322..28a08b2b6b063d 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -9,7 +9,7 @@ use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry use node_runtime::NodeRuntime; use project::{ buffer_store::{BufferStore, BufferStoreEvent}, - debugger::breakpoint_store::BreakpointStore, + debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore}, git::GitStore, project_settings::SettingsObserver, search::SearchQuery, @@ -93,7 +93,14 @@ impl HeadlessProject { ) }); - let breakpoint_store = cx.new(|_| BreakpointStore::local()); + let buffer_store = cx.new(|cx| { + let mut buffer_store = BufferStore::local(worktree_store.clone(), cx); + buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); + buffer_store + }); + + let breakpoint_store = + cx.new(|cx| BreakpointStore::local(buffer_store.clone(), worktree_store.clone(), cx)); let dap_store = cx.new(|cx| { DapStore::new_local( @@ -108,13 +115,6 @@ impl HeadlessProject { ) }); - let buffer_store = cx.new(|cx| { - let mut buffer_store = - BufferStore::local(worktree_store.clone(), breakpoint_store.clone(), cx); - buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); - buffer_store - }); - let git_store = cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx)); let prettier_store = cx.new(|cx| { @@ -234,6 +234,7 @@ impl HeadlessProject { TaskStore::init(Some(&client)); ToolchainStore::init(&client); DapStore::init(&client); + BreakpointStore::init(&client); GitStore::init(&client); HeadlessProject { From 3aab70c7bb519088f181e3fda75e9030ff776b9c Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 14 Feb 2025 15:51:29 -0500 Subject: [PATCH 549/650] Fix some warnings --- crates/debugger_ui/src/debugger_panel_item.rs | 8 ++++---- crates/debugger_ui/src/stack_frame_list.rs | 8 ++++---- crates/debugger_ui/src/variable_list.rs | 2 +- crates/editor/src/editor.rs | 18 ++++++++---------- crates/project/src/debugger/dap_command.rs | 2 +- crates/project/src/debugger/dap_session.rs | 9 ++++----- crates/project/src/lsp_store.rs | 6 +++--- crates/project/src/project.rs | 5 +---- 8 files changed, 26 insertions(+), 32 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 46f8787c01be71..cba5dda0870449 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -37,7 +37,7 @@ pub enum ThreadItem { } impl ThreadItem { - fn to_proto(&self) -> proto::DebuggerThreadItem { + fn _to_proto(&self) -> proto::DebuggerThreadItem { match self { ThreadItem::Console => proto::DebuggerThreadItem::Console, ThreadItem::LoadedSource => proto::DebuggerThreadItem::LoadedSource, @@ -65,7 +65,7 @@ pub struct DebugPanelItem { show_console_indicator: bool, module_list: Entity, active_thread_item: ThreadItem, - workspace: WeakEntity, + _workspace: WeakEntity, client_id: DebugAdapterClientId, thread_state: Entity, variable_list: Entity, @@ -176,7 +176,7 @@ impl DebugPanelItem { session, console, thread_id, - workspace, + _workspace: workspace, module_list, thread_state, focus_handle, @@ -475,7 +475,7 @@ impl DebugPanelItem { .map(|state| state.read(cx).capabilities().clone()) } - fn clear_highlights(&self, cx: &mut Context) { + fn clear_highlights(&self, _cx: &mut Context) { // TODO(debugger): make this work again // if let Some((_, project_path, _)) = self.dap_store.read(cx).active_debug_line() { // self.workspace diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index b8197234def060..d9af8caa4b32d5 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -29,7 +29,7 @@ pub struct StackFrameList { workspace: WeakEntity, client_id: DebugAdapterClientId, current_stack_frame_id: StackFrameId, - fetch_stack_frames_task: Option>>, + _fetch_stack_frames_task: Option>>, } #[derive(Debug, PartialEq, Eq)] @@ -83,7 +83,7 @@ impl StackFrameList { focus_handle, _subscription, entries: Default::default(), - fetch_stack_frames_task: None, + _fetch_stack_frames_task: None, current_stack_frame_id: Default::default(), } } @@ -215,14 +215,14 @@ impl StackFrameList { return Task::ready(Ok(())); }; - let row = (stack_frame.line.saturating_sub(1)) as u32; + let _row = (stack_frame.line.saturating_sub(1)) as u32; let Some(project_path) = self.project_path_from_stack_frame(&stack_frame, cx) else { return Task::ready(Err(anyhow!("Project path not found"))); }; cx.spawn_in(window, { - let client_id = self.client_id; + // let client_id = self.client_id; move |this, mut cx| async move { this.update_in(&mut cx, |this, window, cx| { this.workspace.update(cx, |workspace, cx| { diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index be222e0190ba9d..38677c11764008 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -401,7 +401,7 @@ impl VariableList { } } - pub(crate) fn to_proto(&self) -> proto::DebuggerVariableList { + pub(crate) fn _to_proto(&self) -> proto::DebuggerVariableList { let variables = self .variables .iter() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9390106ea63ef5..6e718578063b7c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -80,13 +80,13 @@ use code_context_menus::{ use git::blame::GitBlame; use gpui::{ div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation, - AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds, - ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, Entity, - EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, - Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton, - MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled, - StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, - UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, + AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Bounds, ClickEvent, + ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler, + EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, + HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton, MouseDownEvent, + PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, + Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, + WeakEntity, WeakFocusHandle, Window, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; @@ -108,9 +108,7 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange} use linked_editing_ranges::refresh_linked_ranges; use mouse_context_menu::MouseContextMenu; use project::{ - debugger::breakpoint_store::{ - self, BreakpointEditAction, BreakpointStore, BreakpointStoreEvent, - }, + debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore, BreakpointStoreEvent}, ProjectPath, }; pub use proposed_changes_editor::{ diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 0a746f84bbf408..ef8c6c6eb2f5a6 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -18,7 +18,7 @@ pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { type DapRequest: 'static + Send + dap::requests::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; - fn is_supported(capabilities: &Capabilities) -> bool { + fn is_supported(_capabilities: &Capabilities) -> bool { true } diff --git a/crates/project/src/debugger/dap_session.rs b/crates/project/src/debugger/dap_session.rs index 8508b4627abb17..ccbd7645a4c893 100644 --- a/crates/project/src/debugger/dap_session.rs +++ b/crates/project/src/debugger/dap_session.rs @@ -7,7 +7,6 @@ use super::dap_command::{ use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, IndexMap}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; -use dap::requests::Request; use dap::{ Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, SteppingGranularity, }; @@ -52,15 +51,15 @@ impl ThreadId { #[derive(Clone)] pub struct Variable { - dap: dap::Variable, - variables: Vec, + _dap: dap::Variable, + _variables: Vec, } impl From for Variable { fn from(dap: dap::Variable) -> Self { Self { - dap, - variables: vec![], + _dap: dap, + _variables: vec![], } } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 8494f2c8bcad36..2127a82ec3d10e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -39,9 +39,9 @@ use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, CompletionDocumentation, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, - File as _, Language, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile, - LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, - Transaction, Unclipped, + File as _, Language, LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, + LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{ notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 39625ca8a75720..eb28e7e8811afa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -51,10 +51,7 @@ use dap::{ use collections::{BTreeSet, HashMap, HashSet}; use debounced_delay::DebouncedDelay; use debugger::{ - breakpoint_store::{ - Breakpoint, BreakpointEditAction, BreakpointStore, BreakpointStoreEvent, - SerializedBreakpoint, - }, + breakpoint_store::{BreakpointStore, BreakpointStoreEvent, SerializedBreakpoint}, dap_store::{DapStore, DapStoreEvent}, }; pub use environment::ProjectEnvironment; From 53336eedb75c8f7ef3bcbbfe11bb357f1fdd98a2 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 14 Feb 2025 16:56:21 -0500 Subject: [PATCH 550/650] Move breakpoint serialization/deserialization to breakpoint store from project --- .../project/src/debugger/breakpoint_store.rs | 105 +++++++++++++++++- crates/project/src/project.rs | 101 +---------------- 2 files changed, 104 insertions(+), 102 deletions(-) diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 52438322b2a89d..ec7db5e7470d1c 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -3,14 +3,15 @@ use crate::{ BufferId, ProjectItem as _, ProjectPath, WorktreeStore, }; use anyhow::{Context as _, Result}; -use collections::{BTreeMap, HashSet}; -use dap::SourceBreakpoint; -use gpui::{AsyncApp, Context, Entity, EventEmitter}; +use collections::{BTreeMap, HashMap, HashSet}; +use dap::{debugger_settings::DebuggerSettings, SourceBreakpoint}; +use gpui::{App, AsyncApp, Context, Entity, EventEmitter}; use language::{ proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, Buffer, BufferSnapshot, }; use rpc::{proto, AnyProtoClient, TypedEnvelope}; +use settings::Settings; use settings::WorktreeId; use std::{ hash::{Hash, Hasher}, @@ -18,7 +19,7 @@ use std::{ sync::Arc, }; use text::Point; -use util::ResultExt as _; +use util::{maybe, ResultExt as _}; struct RemoteBreakpointStore { upstream_client: Option, @@ -423,6 +424,102 @@ impl BreakpointStore { }) } + fn serialize_breakpoints_for_project_path( + &self, + project_path: &ProjectPath, + cx: &App, + ) -> Option<(Arc, Vec)> { + let buffer = maybe!({ + let buffer_id = self + .buffer_store + .read(cx) + .buffer_id_for_project_path(project_path)?; + Some(self.buffer_store.read(cx).get(*buffer_id)?.read(cx)) + }); + + let worktree_path = self + .worktree_store + .read(cx) + .worktree_for_id(project_path.worktree_id, cx)? + .read(cx) + .abs_path(); + + Some(( + worktree_path, + self.breakpoints + .get(&project_path)? + .iter() + .map(|bp| bp.to_serialized(buffer, project_path.path.clone())) + .collect(), + )) + } + + pub fn serialize_breakpoints(&self, cx: &App) -> HashMap, Vec> { + let mut result: HashMap, Vec> = Default::default(); + + if !DebuggerSettings::get_global(cx).save_breakpoints { + return result; + } + + for project_path in self.breakpoints.keys() { + if let Some((worktree_path, mut serialized_breakpoint)) = + self.serialize_breakpoints_for_project_path(project_path, cx) + { + result + .entry(worktree_path.clone()) + .or_default() + .append(&mut serialized_breakpoint) + } + } + + result + } + + pub fn all_breakpoints( + &self, + as_abs_path: bool, + cx: &App, + ) -> HashMap, Vec> { + let mut all_breakpoints: HashMap, Vec> = Default::default(); + + for (project_path, breakpoints) in &self.breakpoints { + let buffer = maybe!({ + let buffer_store = self.buffer_store.read(cx); + let buffer_id = buffer_store.buffer_id_for_project_path(project_path)?; + let buffer = buffer_store.get(*buffer_id)?; + Some(buffer.read(cx)) + }); + + let Some(path) = maybe!({ + if as_abs_path { + let worktree = self + .worktree_store + .read(cx) + .worktree_for_id(project_path.worktree_id, cx)?; + Some(Arc::from( + worktree + .read(cx) + .absolutize(&project_path.path) + .ok()? + .as_path(), + )) + } else { + Some(project_path.clone().path) + } + }) else { + continue; + }; + + all_breakpoints.entry(path).or_default().extend( + breakpoints + .into_iter() + .map(|bp| bp.to_serialized(buffer, project_path.clone().path)), + ); + } + + all_breakpoints + } + #[cfg(any(test, feature = "test-support"))] pub fn breakpoints(&self) -> &BTreeMap> { &self.breakpoints diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index eb28e7e8811afa..82fc4d9e8917de 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1311,48 +1311,6 @@ impl Project { } } - pub fn all_breakpoints( - &self, - as_abs_path: bool, - cx: &mut Context, - ) -> HashMap, Vec> { - let mut all_breakpoints: HashMap, Vec> = Default::default(); - - for (project_path, breakpoints) in &self.breakpoint_store.read(cx).breakpoints { - let buffer = maybe!({ - let buffer_store = self.buffer_store.read(cx); - let buffer_id = buffer_store.buffer_id_for_project_path(project_path)?; - let buffer = self.buffer_for_id(*buffer_id, cx)?; - Some(buffer.read(cx)) - }); - - let Some(path) = maybe!({ - if as_abs_path { - let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; - Some(Arc::from( - worktree - .read(cx) - .absolutize(&project_path.path) - .ok()? - .as_path(), - )) - } else { - Some(project_path.clone().path) - } - }) else { - continue; - }; - - all_breakpoints.entry(path).or_default().extend( - breakpoints - .into_iter() - .map(|bp| bp.to_serialized(buffer, project_path.clone().path)), - ); - } - - all_breakpoints - } - pub fn initial_send_breakpoints( &self, session_id: &DebugSessionId, @@ -1362,7 +1320,8 @@ impl Project { let mut tasks = Vec::new(); for (abs_path, serialized_breakpoints) in self - .all_breakpoints(true, cx) + .breakpoint_store() + .read_with(cx, |store, cx| store.all_breakpoints(true, cx)) .into_iter() .filter(|(_, bps)| !bps.is_empty()) { @@ -1410,65 +1369,11 @@ impl Project { }) } - /// Get all serialized breakpoints that belong to a buffer - pub fn serialize_breakpoints_for_project_path( - &self, - project_path: &ProjectPath, - cx: &Context, - ) -> Option<(Arc, Vec)> { - let buffer = maybe!({ - let buffer_id = self - .buffer_store - .read(cx) - .buffer_id_for_project_path(project_path)?; - Some(self.buffer_for_id(*buffer_id, cx)?.read(cx)) - }); - - let worktree_path = self - .worktree_for_id(project_path.worktree_id, cx)? - .read(cx) - .abs_path(); - - Some(( - worktree_path, - self.breakpoint_store - .read(cx) - .breakpoints - .get(&project_path)? - .iter() - .map(|bp| bp.to_serialized(buffer, project_path.path.clone())) - .collect(), - )) - } - - /// Serialize all breakpoints to save within workspace's database - /// - /// # Return - /// HashMap: - /// Key: A valid worktree path - /// Value: All serialized breakpoints that belong to a worktree pub fn serialize_breakpoints( &self, cx: &Context, ) -> HashMap, Vec> { - let mut result: HashMap, Vec> = Default::default(); - - if !DebuggerSettings::get_global(cx).save_breakpoints { - return result; - } - - for project_path in self.breakpoint_store.read(cx).breakpoints.keys() { - if let Some((worktree_path, mut serialized_breakpoint)) = - self.serialize_breakpoints_for_project_path(project_path, cx) - { - result - .entry(worktree_path.clone()) - .or_default() - .append(&mut serialized_breakpoint) - } - } - - result + self.breakpoint_store.read(cx).serialize_breakpoints(cx) } async fn handle_toggle_ignore_breakpoints( From e285ae60c357f611c68e34cd09758986bcc6de3b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 14 Feb 2025 23:27:13 +0100 Subject: [PATCH 551/650] WIP --- crates/debugger_ui/src/debugger_panel.rs | 12 +---- crates/debugger_ui/src/lib.rs | 2 +- crates/project/src/debugger/client.rs | 2 +- crates/project/src/debugger/dap_store.rs | 62 +----------------------- crates/project/src/project.rs | 2 +- crates/proto/proto/zed.proto | 48 +++++++++--------- crates/proto/src/proto.rs | 3 -- 7 files changed, 31 insertions(+), 100 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 2d4ca31d2cc5af..f9d5070e89371f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -161,20 +161,13 @@ impl DebugPanel { project::Event::DebugClientStarted((session_id, client_id)) => { this.handle_debug_client_started(session_id, *client_id, window, cx); } - project::Event::DebugClientEvent { - session_id, - client_id, - message, - } => match message { + project::Event::DebugClientEvent { client_id, message } => match message { Message::Event(event) => { - this.handle_debug_client_events( - session_id, *client_id, event, window, cx, - ); + this.handle_debug_client_events(*client_id, event, window, cx); } Message::Request(request) => { if StartDebugging::COMMAND == request.command { this.handle_start_debugging_request( - session_id, *client_id, request.seq, request.arguments.clone(), @@ -182,7 +175,6 @@ impl DebugPanel { ); } else if RunInTerminal::COMMAND == request.command { this.handle_run_in_terminal_request( - session_id, *client_id, request.seq, request.arguments.clone(), diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index e79233a2e0d214..25bc54aa224ac7 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -41,7 +41,7 @@ pub fn init(cx: &mut App) { |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| { workspace.project().update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.shutdown_sessions(cx).detach(); + store.shutdown_clients(cx).detach(); }) }) }, diff --git a/crates/project/src/debugger/client.rs b/crates/project/src/debugger/client.rs index 1ac2125deda1a6..77d251668b9bb7 100644 --- a/crates/project/src/debugger/client.rs +++ b/crates/project/src/debugger/client.rs @@ -335,7 +335,7 @@ impl Client { &self.capabilities } pub fn configuration(&self) -> DebugAdapterConfig { - Configuration::default() + DebugAdapterConfig::default() } pub(crate) fn _wait_for_request( diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 88e04f136cf642..65a4b0e71751a5 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -244,7 +244,7 @@ impl DapStore { self.clients.insert( client_id, cx.new(|_| { - debugger::client::Client::new( + debugger::client::Client::remote( client_id, "Remote-Debug".to_owned(), ignore.unwrap_or(false), @@ -760,8 +760,6 @@ impl DapStore { let config = client.read(cx).configuration(); - let session_id = *session_id; - let request_args = args.unwrap_or_else(|| StartDebuggingRequestArguments { configuration: config.initialize_args.clone().unwrap_or_default(), request: match config.request { @@ -1007,25 +1005,6 @@ impl DapStore { // let returned_value = client.modules(); // this is a cheap getter. pub fn shutdown_clients(&mut self, cx: &mut Context) -> Task<()> { - let Some(_) = self.as_local() else { - if let Some((upstream_client, project_id)) = self.upstream_client() { - return cx.background_executor().spawn(async move { - upstream_client - .request(proto::DapShutdownSession { - project_id, - session_id: None, - }) - .await - .log_err(); - - () - }); - } - return Task::ready(()); - }; - - let mut tasks = Vec::new(); - for client_id in self.clients.keys().cloned().collect::>() { tasks.push(self.shutdown_client(&client_id, cx)); } @@ -1042,7 +1021,7 @@ impl DapStore { ) -> Task> { let Some(_) = self.as_local_mut() else { if let Some((upstream_client, project_id)) = self.upstream_client() { - let future = upstream_client.request(proto::DapShutdownSession { + let future = upstream_client.request(proto::DapShutdownClient { project_id, session_id: Some(client_id.to_proto()), }); @@ -1087,43 +1066,6 @@ impl DapStore { } } - pub fn set_debug_sessions_from_proto( - &mut self, - debug_sessions: Vec, - cx: &mut Context, - ) { - for session in debug_sessions.into_iter() { - let ignore_breakpoints = Some(session.ignore_breakpoints); - - self.add_remote_session(ignore_breakpoints, cx); - - for debug_client in session.clients { - if let DapStoreMode::Remote(remote) = &mut self.mode { - if let Some(queue) = &mut remote.event_queue { - debug_client.debug_panel_items.into_iter().for_each(|item| { - queue.push_back(DapStoreEvent::SetDebugPanelItem(item)); - }); - } - cx.emit(DapStoreEvent::RemoteHasInitialized); - } - - let client = DebugAdapterClientId::from_proto(debug_client.client_id); - - self.add_client_to_session(session_id, client); - - self.update_capabilities_for_client( - client, - &dap::proto_conversions::capabilities_from_proto( - &debug_client.capabilities.unwrap_or_default(), - ), - cx, - ); - } - } - - cx.notify(); - } - async fn _handle_dap_command_2( this: Entity, envelope: TypedEnvelope, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 788c54546deadf..215a1bbbc0e716 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1080,7 +1080,7 @@ impl Project { let environment = cx.update(|cx| ProjectEnvironment::new(&worktree_store, None, cx))?; let breakpoint_store = cx.new(|cx| { - let mut bp_store = BreakpointStore::remote(SSH_PROJECT_ID, client.clone().into()); + let mut bp_store = BreakpointStore::remote(remote_id, client.clone().into()); bp_store.set_breakpoints_from_proto(response.payload.breakpoints, cx); bp_store })?; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index c9ea8a01ee33e6..9089ef2f965f19 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -340,30 +340,30 @@ message Envelope { DapPauseRequest dap_pause_request = 316; DapDisconnectRequest dap_disconnect_request = 317; DapTerminateThreadsRequest dap_terminate_threads_request = 318; - DapTerminateRequest dap_terminate_request = 319; - DapRestartRequest dap_restart_request = 320; - UpdateThreadStatus update_thread_status = 321; - VariablesRequest variables_request = 322; - DapVariables dap_variables = 323; - DapRestartStackFrameRequest dap_restart_stack_frame_request = 324; - IgnoreBreakpointState ignore_breakpoint_state = 325; - ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 326; - DapModulesRequest dap_modules_request = 327; - DapModulesResponse dap_modules_response = 328; - DapLoadedSourcesRequest dap_loaded_sources_request = 329; - DapLoadedSourcesResponse dap_loaded_sources_response = 330; - DapStackTraceRequest dap_stack_trace_request = 331; - DapStackTraceResponse dap_stack_trace_response = 332; - DapScopesRequest dap_scopes_request = 333; - DapScopesResponse dap_scopes_response = 334; - DapSetVariableValueRequest dap_set_variable_value_request = 335; - DapSetVariableValueResponse dap_set_variable_value_response = 336; - DapEvaluateRequest dap_evaluate_request = 337; - DapEvaluateResponse dap_evaluate_response = 338; - DapCompletionRequest dap_completion_request = 339; - DapCompletionResponse dap_completion_response = 340; - DapThreadsRequest dap_threads_request = 341; - DapThreadsResponse dap_threads_response = 342; // current max + DapRestartRequest dap_restart_request = 319; + UpdateThreadStatus update_thread_status = 320; + VariablesRequest variables_request = 321; + DapVariables dap_variables = 322; + DapRestartStackFrameRequest dap_restart_stack_frame_request = 323; + IgnoreBreakpointState ignore_breakpoint_state = 324; + ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 325; + DapModulesRequest dap_modules_request = 326; + DapModulesResponse dap_modules_response = 327; + DapLoadedSourcesRequest dap_loaded_sources_request = 328; + DapLoadedSourcesResponse dap_loaded_sources_response = 329; + DapStackTraceRequest dap_stack_trace_request = 330; + DapStackTraceResponse dap_stack_trace_response = 331; + DapScopesRequest dap_scopes_request = 332; + DapScopesResponse dap_scopes_response = 333; + DapSetVariableValueRequest dap_set_variable_value_request = 334; + DapSetVariableValueResponse dap_set_variable_value_response = 335; + DapEvaluateRequest dap_evaluate_request = 336; + DapEvaluateResponse dap_evaluate_response = 337; + DapCompletionRequest dap_completion_request = 338; + DapCompletionResponse dap_completion_response = 339; + DapThreadsRequest dap_threads_request = 340; + DapThreadsResponse dap_threads_response = 341; + DapTerminateRequest dap_terminate_request = 342;// current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 486c508ce6021e..72cb07b6d9fb17 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -252,7 +252,6 @@ messages!( (DapStepBackRequest, Background), (DapStepInRequest, Background), (DapStepOutRequest, Background), - (DapTerminateRequest, Background), (DapTerminateThreadsRequest, Background), (DeclineCall, Foreground), (DeleteChannel, Foreground), @@ -630,7 +629,6 @@ request_messages!( (DapPauseRequest, Ack), (DapDisconnectRequest, Ack), (DapTerminateThreadsRequest, Ack), - (DapTerminateRequest, Ack), (DapRestartRequest, Ack), (DapRestartStackFrameRequest, Ack), (VariablesRequest, DapVariables), @@ -750,7 +748,6 @@ entity_messages!( DapPauseRequest, DapDisconnectRequest, DapTerminateThreadsRequest, - DapTerminateRequest, DapRestartRequest, DapRestartStackFrameRequest, UpdateThreadStatus, From d0f64d475df1de841cdbc2071a5048c6fe88fc9c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 15 Feb 2025 00:07:29 +0100 Subject: [PATCH 552/650] Add placeholder for thread dropdown --- crates/debugger_ui/src/debugger_panel_item.rs | 286 ++++++++++-------- 1 file changed, 165 insertions(+), 121 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index cba5dda0870449..7a200608fa911e 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -16,7 +16,7 @@ use gpui::{ use project::debugger::dap_session::{DebugSession, ThreadId}; use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem}; use settings::Settings; -use ui::{prelude::*, Indicator, Tooltip}; +use ui::{prelude::*, ContextMenu, Indicator, PopoverMenu, Tooltip}; use workspace::{ item::{self, Item, ItemEvent}, FollowableItem, ViewId, Workspace, @@ -796,153 +796,197 @@ impl Render for DebugPanelItem { .items_start() .child( h_flex() - .p_1() - .border_b_1() .w_full() + .border_b_1() .border_color(cx.theme().colors().border_variant) - .gap_2() - .map(|this| { - if thread_status == ThreadStatus::Running { - this.child( - IconButton::new("debug-pause", IconName::DebugPause) + .justify_between() + .child( + h_flex() + .p_1() + .w_full() + .gap_2() + .map(|this| { + if thread_status == ThreadStatus::Running { + this.child( + IconButton::new( + "debug-pause", + IconName::DebugPause, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.pause_thread(cx); + })) + .tooltip(move |window, cx| { + Tooltip::text("Pause program")(window, cx) + }), + ) + } else { + this.child( + IconButton::new( + "debug-continue", + IconName::DebugContinue, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.continue_thread(cx) + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |window, cx| { + Tooltip::text("Continue program")(window, cx) + }), + ) + } + }) + .when( + capabilities + .as_ref() + .map(|caps| caps.supports_step_back) + .flatten() + .unwrap_or(false), + |this| { + this.child( + IconButton::new( + "debug-step-back", + IconName::DebugStepBack, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.step_back(cx); + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |window, cx| { + Tooltip::text("Step back")(window, cx) + }), + ) + }, + ) + .child( + IconButton::new("debug-step-over", IconName::DebugStepOver) .icon_size(IconSize::Small) .on_click(cx.listener(|this, _, _window, cx| { - this.pause_thread(cx); + this.step_over(cx); })) + .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |window, cx| { - Tooltip::text("Pause program")(window, cx) + Tooltip::text("Step over")(window, cx) }), ) - } else { - this.child( - IconButton::new("debug-continue", IconName::DebugContinue) + .child( + IconButton::new("debug-step-in", IconName::DebugStepInto) .icon_size(IconSize::Small) .on_click(cx.listener(|this, _, _window, cx| { - this.continue_thread(cx) + this.step_in(cx); })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |window, cx| { - Tooltip::text("Continue program")(window, cx) + Tooltip::text("Step in")(window, cx) }), ) - } - }) - .when( - capabilities - .as_ref() - .map(|caps| caps.supports_step_back) - .flatten() - .unwrap_or(false), - |this| { - this.child( - IconButton::new("debug-step-back", IconName::DebugStepBack) + .child( + IconButton::new("debug-step-out", IconName::DebugStepOut) .icon_size(IconSize::Small) .on_click(cx.listener(|this, _, _window, cx| { - this.step_back(cx); + this.step_out(cx); })) .disabled(thread_status != ThreadStatus::Stopped) .tooltip(move |window, cx| { - Tooltip::text("Step back")(window, cx) + Tooltip::text("Step out")(window, cx) }), ) - }, - ) - .child( - IconButton::new("debug-step-over", IconName::DebugStepOver) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.step_over(cx); - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Step over")(window, cx) - }), - ) - .child( - IconButton::new("debug-step-in", IconName::DebugStepInto) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.step_in(cx); - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Step in")(window, cx) - }), - ) - .child( - IconButton::new("debug-step-out", IconName::DebugStepOut) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.step_out(cx); - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Step out")(window, cx) - }), - ) - .child( - IconButton::new("debug-restart", IconName::DebugRestart) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.restart_client(cx); - })) - .disabled( - !capabilities - .as_ref() - .map(|caps| caps.supports_restart_request) - .flatten() - .unwrap_or_default(), + .child( + IconButton::new("debug-restart", IconName::DebugRestart) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.restart_client(cx); + })) + .disabled( + !capabilities + .as_ref() + .map(|caps| caps.supports_restart_request) + .flatten() + .unwrap_or_default(), + ) + .tooltip(move |window, cx| { + Tooltip::text("Restart")(window, cx) + }), ) - .tooltip(move |window, cx| { - Tooltip::text("Restart")(window, cx) - }), - ) - .child( - IconButton::new("debug-stop", IconName::DebugStop) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.stop_thread(cx); - })) - .disabled( - thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, + .child( + IconButton::new("debug-stop", IconName::DebugStop) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.stop_thread(cx); + })) + .disabled( + thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, + ) + .tooltip(move |window, cx| { + Tooltip::text("Stop")(window, cx) + }), ) - .tooltip(move |window, cx| Tooltip::text("Stop")(window, cx)), - ) - .child( - IconButton::new("debug-disconnect", IconName::DebugDisconnect) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.disconnect_client(cx); - })) - .disabled( - thread_status == ThreadStatus::Exited - || thread_status == ThreadStatus::Ended, + .child( + IconButton::new( + "debug-disconnect", + IconName::DebugDisconnect, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.disconnect_client(cx); + })) + .disabled( + thread_status == ThreadStatus::Exited + || thread_status == ThreadStatus::Ended, + ) + .tooltip( + move |window, cx| { + Tooltip::text("Disconnect")(window, cx) + }, + ), ) - .tooltip(move |window, cx| { - Tooltip::text("Disconnect")(window, cx) - }), + .child( + IconButton::new( + "debug-ignore-breakpoints", + if self.session.read(cx).ignore_breakpoints() { + IconName::DebugIgnoreBreakpoints + } else { + IconName::DebugBreakpoint + }, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.toggle_ignore_breakpoints(cx); + })) + .disabled( + thread_status == ThreadStatus::Exited + || thread_status == ThreadStatus::Ended, + ) + .tooltip( + move |window, cx| { + Tooltip::text("Ignore breakpoints")(window, cx) + }, + ), + ), ) + //.child(h_flex()) .child( - IconButton::new( - "debug-ignore-breakpoints", - if self.session.read(cx).ignore_breakpoints() { - IconName::DebugIgnoreBreakpoints - } else { - IconName::DebugBreakpoint - }, - ) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.toggle_ignore_breakpoints(cx); - })) - .disabled( - thread_status == ThreadStatus::Exited - || thread_status == ThreadStatus::Ended, - ) - .tooltip(move |window, cx| { - Tooltip::text("Ignore breakpoints")(window, cx) - }), + h_flex().p_1().mx_2().w_3_4().justify_end().child( + PopoverMenu::new("thread-list") + .trigger( + Button::new("thread-list-trigger", "Threads") + .style(ButtonStyle::Filled) + .disabled(thread_status != ThreadStatus::Stopped) + .size(ButtonSize::Compact), + ) + .menu(|window, cx| { + Some(ContextMenu::build(window, cx, |this, _, _| { + this.entry("Thread 1", None, |_, _| {}).entry( + "Thread 2", + None, + |_, _| {}, + ) + })) + }), + ), ), ) .child( From 6648d6299f58e74befc58c0fc6f2751ea0075ce0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 15 Feb 2025 00:09:08 +0100 Subject: [PATCH 553/650] Use dropdown menu instead --- crates/debugger_ui/src/debugger_panel_item.rs | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 7a200608fa911e..9983b2e6c7f974 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -16,7 +16,7 @@ use gpui::{ use project::debugger::dap_session::{DebugSession, ThreadId}; use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem}; use settings::Settings; -use ui::{prelude::*, ContextMenu, Indicator, PopoverMenu, Tooltip}; +use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, PopoverMenu, Tooltip}; use workspace::{ item::{self, Item, ItemEvent}, FollowableItem, ViewId, Workspace, @@ -779,7 +779,7 @@ impl FollowableItem for DebugPanelItem { } impl Render for DebugPanelItem { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let thread_status = self.thread_state.read(cx).status; let active_thread_item = &self.active_thread_item; @@ -968,26 +968,19 @@ impl Render for DebugPanelItem { ), ) //.child(h_flex()) - .child( - h_flex().p_1().mx_2().w_3_4().justify_end().child( - PopoverMenu::new("thread-list") - .trigger( - Button::new("thread-list-trigger", "Threads") - .style(ButtonStyle::Filled) - .disabled(thread_status != ThreadStatus::Stopped) - .size(ButtonSize::Compact), + .child(h_flex().p_1().mx_2().w_3_4().justify_end().child( + DropdownMenu::new( + "thread-list", + "Threads", + ContextMenu::build(window, cx, |this, _, _| { + this.entry("Thread 1", None, |_, _| {}).entry( + "Thread 2", + None, + |_, _| {}, ) - .menu(|window, cx| { - Some(ContextMenu::build(window, cx, |this, _, _| { - this.entry("Thread 1", None, |_, _| {}).entry( - "Thread 2", - None, - |_, _| {}, - ) - })) - }), + }), ), - ), + )), ) .child( h_flex() From 43e2c4de063b0cc0a2db837365fe1ef929519018 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 15 Feb 2025 00:41:48 +0100 Subject: [PATCH 554/650] Keep going --- crates/project/src/debugger/dap_store.rs | 61 ++++++++---------------- crates/project/src/project.rs | 4 +- crates/proto/src/proto.rs | 3 ++ 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 65a4b0e71751a5..7978b6b4f3617c 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -96,7 +96,7 @@ impl LocalDapStore { } pub struct RemoteDapStore { - upstream_client: Option, + upstream_client: AnyProtoClient, upstream_project_id: u64, event_queue: Option>, } @@ -175,7 +175,7 @@ impl DapStore { ) -> Self { Self { mode: DapStoreMode::Remote(RemoteDapStore { - upstream_client: Some(upstream_client), + upstream_client: upstream_client, upstream_project_id: project_id, event_queue: Some(VecDeque::default()), }), @@ -218,15 +218,11 @@ impl DapStore { pub fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> { match &self.mode { DapStoreMode::Remote(RemoteDapStore { - upstream_client: Some(upstream_client), + upstream_client, upstream_project_id, .. }) => Some((upstream_client.clone(), *upstream_project_id)), - DapStoreMode::Remote(RemoteDapStore { - upstream_client: None, - .. - }) => None, DapStoreMode::Local(_) => None, } } @@ -241,17 +237,21 @@ impl DapStore { ignore: Option, cx: &mut Context, ) { - self.clients.insert( - client_id, - cx.new(|_| { - debugger::client::Client::remote( - client_id, - "Remote-Debug".to_owned(), - ignore.unwrap_or(false), - ) - }), - ); - debug_assert!(matches!(self.mode, DapStoreMode::Remote(_))); + if let DapStoreMode::Remote(remote) = &self.mode { + self.clients.insert( + client_id, + cx.new(|_| { + debugger::client::Client::remote( + client_id, + remote.upstream_client.clone(), + remote.upstream_project_id, + ignore.unwrap_or(false), + ) + }), + ); + } else { + debug_assert!(false); + } } pub fn client_by_id( @@ -431,20 +431,6 @@ impl DapStore { store.clients.insert(Arc::new(client), client_id); - session.update(cx, |session, cx| { - session.add_client(Arc::new(client), client_id, cx); - let local_session = session - .as_local_mut() - .expect("Only local sessions should attempt to reconnect"); - - local_session.update_configuration( - |old_config| { - *old_config = config.clone(); - }, - cx, - ); - }); - // don't emit this event ourself in tests, so we can add request, // response and event handlers for this client if !cfg!(any(test, feature = "test-support")) { @@ -558,8 +544,6 @@ impl DapStore { let start_client_task = self.start_client_internal(delegate, config.clone(), cx); cx.spawn(|this, mut cx| async move { - let session = cx.new(|_| DebugSession::new_local(session_id, config))?; - let client = match start_client_task.await { Ok(client) => client, Err(error) => { @@ -573,18 +557,14 @@ impl DapStore { }; this.update(&mut cx, |store, cx| { - session.update(cx, |session, cx| { - session.add_client(client.clone(), client.id(), cx); - }); - let client_id = client.id(); - store.sessions.insert(session_id, session.clone()); + store.clients.insert(client_id, client); cx.emit(DapStoreEvent::DebugClientStarted(client_id)); cx.notify(); - (session, client) + client }) }) } @@ -1005,6 +985,7 @@ impl DapStore { // let returned_value = client.modules(); // this is a cheap getter. pub fn shutdown_clients(&mut self, cx: &mut Context) -> Task<()> { + let mut tasks = vec![]; for client_id in self.clients.keys().cloned().collect::>() { tasks.push(self.shutdown_client(&client_id, cx)); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 215a1bbbc0e716..b7f5abc586a328 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1489,7 +1489,7 @@ impl Project { return Vec::new(); } - store.toggle_ignore_breakpoints(session_id, cx); + store.toggle_ignore_breakpoints(&client_id, cx); if let Some((downstream_client, project_id)) = store.downstream_client() { downstream_client @@ -1528,7 +1528,7 @@ impl Project { .into_iter() .map(|breakpoint| breakpoint.to_source_breakpoint(buffer)) .collect::>(), - store.ignore_breakpoints(session_id, cx), + store.ignore_breakpoints(&client_id, cx), false, cx, ), diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 72cb07b6d9fb17..2a4e77d26888f7 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -483,6 +483,7 @@ messages!( (DapCompletionResponse, Background), (DapThreadsRequest, Background), (DapThreadsResponse, Background), + (DapTerminateRequest, Background) ); request_messages!( @@ -638,6 +639,7 @@ request_messages!( (DapEvaluateRequest, DapEvaluateResponse), (DapCompletionRequest, DapCompletionResponse), (DapThreadsRequest, DapThreadsResponse), + (DapTerminateRequest, Ack) ); entity_messages!( @@ -760,6 +762,7 @@ entity_messages!( DapEvaluateRequest, DapCompletionRequest, DapThreadsRequest, + DapTerminateRequest ); entity_messages!( From 7fe8c626d3ff31a6c4a593dfea3927a76d490d5d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 15 Feb 2025 01:26:57 +0100 Subject: [PATCH 555/650] Fix up one call site --- crates/project/src/project.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b7f5abc586a328..169613ed5c0984 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1358,7 +1358,7 @@ impl Project { client_id, abs_path, source_breakpoints, - store.ignore_breakpoints(session_id, cx), + store.ignore_breakpoints(&client_id, cx), false, cx, ) From 36b430a1aeea6bf36762393419dfec4f3c17fcac Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 15 Feb 2025 01:40:41 +0100 Subject: [PATCH 556/650] Add a doc comment for the debugger module --- crates/project/src/debugger.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index 1ca4f576c1f8d0..9cd45bf4cbb16a 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -1,3 +1,16 @@ +//! Zed's debugger data layer is implemented in terms of 3 concepts: +//! - DAP store - that knows about all of the available debug sessions. +//! - Debug sessions - that bear responsibility of communicating with debug adapters and managing the state of each individual session. +//! For the most part it is agnostic over the communication layer (it'll use RPC for peers and actual DAP requests for the host). +//! - Breakpoint store - that knows about all breakpoints set for a project. +//! +//! There are few reasons for this divide: +//! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them +//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. +//! - 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 +//! current set of breakpoints. +//! - 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. + pub mod breakpoint_store; pub mod dap_command; pub mod dap_session; From 12c02a12ca92fd6beea945fad772f53163fa5af1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 15 Feb 2025 01:51:57 +0100 Subject: [PATCH 557/650] WIP --- crates/project/src/debugger/client.rs | 6 ++---- crates/project/src/debugger/dap_command.rs | 2 +- crates/project/src/debugger/dap_store.rs | 21 --------------------- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/crates/project/src/debugger/client.rs b/crates/project/src/debugger/client.rs index 77d251668b9bb7..ea8a9b2e375744 100644 --- a/crates/project/src/debugger/client.rs +++ b/crates/project/src/debugger/client.rs @@ -5,17 +5,15 @@ use super::dap_command::{ TerminateThreadsCommand, VariablesCommand, }; use anyhow::{anyhow, Result}; -use collections::{BTreeMap, HashMap, IndexMap}; +use collections::{HashMap, IndexMap}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; -use dap::requests::Request; use dap::{ Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, SteppingGranularity, }; use futures::{future::Shared, FutureExt}; -use gpui::{App, AppContext, Context, Entity, Task}; +use gpui::{App, Context, Task}; use rpc::AnyProtoClient; use serde_json::Value; -use std::borrow::Borrow; use std::u64; use std::{ any::Any, diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 33c32e1047d2a9..df32856cf4473e 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -16,7 +16,7 @@ pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { type DapRequest: 'static + Send + dap::requests::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; - fn is_supported(capabilities: &Capabilities) -> bool { + fn is_supported(_capabilities: &Capabilities) -> bool { true } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 7978b6b4f3617c..95f1ab7908e83f 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1026,27 +1026,6 @@ impl DapStore { Task::ready(Ok(())) } - pub fn request_active_debug_sessions(&mut self, cx: &mut Context) { - if let Some((client, project_id)) = self.upstream_client() { - cx.spawn(|this, mut cx| async move { - let response = dbg!( - client - .request(proto::ActiveDebugSessionsRequest { project_id }) - .await - ) - .log_err(); - - if let Some(response) = response { - this.update(&mut cx, |dap_store, cx| { - dap_store.set_debug_sessions_from_proto(response.sessions, cx) - }) - .log_err(); - } - }) - .detach(); - } - } - async fn _handle_dap_command_2( this: Entity, envelope: TypedEnvelope, From bc5904e485f8e56871e3714da855fa4430a44fa3 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 16 Feb 2025 18:18:15 -0500 Subject: [PATCH 558/650] Fix breakpoint line numbers not being colored --- crates/editor/src/element.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 97ab40862e7cee..43bc5e3fa9dd0f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4667,32 +4667,32 @@ impl EditorElement { for LineNumberLayout { shaped_line, hitbox, - display_row, + .. } in layout.line_numbers.values() { let Some(hitbox) = hitbox else { continue; }; - let is_active = layout.active_rows.contains_key(&display_row); + let Some(()) = (if !is_singleton && hitbox.is_hovered(window) { + let color = cx.theme().colors().editor_hover_line_number; - let color = if is_active { - cx.theme().colors().editor_active_line_number - } else if !is_singleton && hitbox.is_hovered(window) { - cx.theme().colors().editor_hover_line_number - } else { - cx.theme().colors().editor_line_number - }; + let Some(line) = self + .shape_line_number(shaped_line.text.clone(), color, window) + .log_err() + else { + continue; + }; - let Some(line) = self - .shape_line_number(shaped_line.text.clone(), color, window) - .log_err() - else { - continue; - }; - let Some(()) = line.paint(hitbox.origin, line_height, window, cx).log_err() else { + line.paint(hitbox.origin, line_height, window, cx).log_err() + } else { + shaped_line + .paint(hitbox.origin, line_height, window, cx) + .log_err() + }) else { continue; }; + // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor. // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor. if is_singleton { From f7886adf9a3efadc6e970e88e35001a678e38709 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Sun, 16 Feb 2025 18:21:44 -0500 Subject: [PATCH 559/650] Remove display_row from line number layout struct --- crates/editor/src/element.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 43bc5e3fa9dd0f..08a74fad385f98 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2321,7 +2321,6 @@ impl EditorElement { let line_number = LineNumberLayout { shaped_line, hitbox, - display_row, }; Some((multi_buffer_row, line_number)) }) @@ -4667,7 +4666,6 @@ impl EditorElement { for LineNumberLayout { shaped_line, hitbox, - .. } in layout.line_numbers.values() { let Some(hitbox) = hitbox else { @@ -8004,7 +8002,6 @@ impl EditorLayout { struct LineNumberLayout { shaped_line: ShapedLine, hitbox: Option, - display_row: DisplayRow, } struct ColoredRange { From 294ce96b24b36cd1f3e3e4dfca36240e81108b30 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 00:50:05 +0100 Subject: [PATCH 560/650] WIP --- crates/project/src/debugger/dap_store.rs | 5 ++--- crates/proto/src/proto.rs | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 95f1ab7908e83f..a856a08d747756 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1002,9 +1002,9 @@ impl DapStore { ) -> Task> { let Some(_) = self.as_local_mut() else { if let Some((upstream_client, project_id)) = self.upstream_client() { - let future = upstream_client.request(proto::DapShutdownClient { + let future = upstream_client.request(proto::ShutdownDebugClient { project_id, - session_id: Some(client_id.to_proto()), + client_id: Some(client_id.to_proto()), }); return cx @@ -1014,7 +1014,6 @@ impl DapStore { return Task::ready(Err(anyhow!("Cannot shutdown session on remote side"))); }; - let Some(client) = self.clients.remove(client_id) else { return Task::ready(Err(anyhow!("Could not find session: {:?}", client_id))); }; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 2a4e77d26888f7..51d2e165f93731 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -639,7 +639,8 @@ request_messages!( (DapEvaluateRequest, DapEvaluateResponse), (DapCompletionRequest, DapCompletionResponse), (DapThreadsRequest, DapThreadsResponse), - (DapTerminateRequest, Ack) + (DapTerminateRequest, Ack), + (ShutdownDebugClient, Ack), ); entity_messages!( From 0233152bd11f1a0cc17ef6c1c89752c629ebdcbb Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:02:34 +0100 Subject: [PATCH 561/650] Get rid of supports_attach (we'll configure it differently later on) --- crates/dap/src/adapters.rs | 10 ---------- crates/dap_adapters/src/javascript.rs | 4 ---- crates/dap_adapters/src/lldb.rs | 4 ---- crates/project/src/debugger/dap_store.rs | 9 ++------- 4 files changed, 2 insertions(+), 25 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 4dcf37693ba97d..a15c1ebfc2e947 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -288,12 +288,6 @@ pub trait DebugAdapter: 'static + Send + Sync { /// Should return base configuration to make the debug adapter work fn request_args(&self, config: &DebugAdapterConfig) -> Value; - /// Whether the adapter supports `attach` request, - /// if not support and the request is selected we will show an error message - fn supports_attach(&self) -> bool { - false - } - /// Filters out the processes that the adapter can attach to for debugging fn attach_processes<'a>( &self, @@ -383,10 +377,6 @@ impl DebugAdapter for FakeAdapter { }) } - fn supports_attach(&self) -> bool { - true - } - fn attach_processes<'a>( &self, processes: &'a HashMap, diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index a68afc28f15445..7657cc9f4493de 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -136,10 +136,6 @@ impl DebugAdapter for JsDebugAdapter { }) } - fn supports_attach(&self) -> bool { - true - } - fn attach_processes<'a>( &self, processes: &'a HashMap, diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index ec274bd05d0ae0..7b5a804e0f586c 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -102,10 +102,6 @@ impl DebugAdapter for LldbDebugAdapter { }) } - fn supports_attach(&self) -> bool { - true - } - fn attach_processes<'a>( &self, processes: &'a HashMap, diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index a856a08d747756..a0923daefe8be7 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -780,12 +780,7 @@ impl DapStore { "Debug adapter does not support `attach` request" ))) } else { - store.reconnect_client( - client.adapter().clone(), - client.binary().clone(), - new_config, - cx, - ) + store.reconnect_client(client.binary().clone(), new_config, cx) } }); @@ -1004,7 +999,7 @@ impl DapStore { if let Some((upstream_client, project_id)) = self.upstream_client() { let future = upstream_client.request(proto::ShutdownDebugClient { project_id, - client_id: Some(client_id.to_proto()), + client_id: client_id.to_proto(), }); return cx From a0c91d620ab93113d3a40c9896728d4667d77240 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:28:22 +0100 Subject: [PATCH 562/650] WIP --- crates/project/src/debugger/dap_store.rs | 46 ++++++++++++------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index a0923daefe8be7..1427fd0e245abb 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -111,9 +111,27 @@ pub struct DapStore { impl EventEmitter for DapStore {} +fn dap_client_capabilities(adapter_id: String) -> InitializeRequestArguments { + InitializeRequestArguments { + client_id: Some("zed".to_owned()), + client_name: Some("Zed".to_owned()), + adapter_id, + locale: Some("en-US".to_owned()), + path_format: Some(InitializeRequestArgumentsPathFormat::Path), + supports_variable_type: Some(true), + supports_variable_paging: Some(false), + supports_run_in_terminal_request: Some(true), + supports_memory_references: Some(true), + supports_progress_reporting: Some(false), + supports_invalidated_event: Some(false), + lines_start_at1: Some(true), + columns_start_at1: Some(true), + supports_memory_event: Some(false), + supports_args_can_be_interpreted_by_shell: Some(false), + supports_start_debugging_request: Some(true), + } +} impl DapStore { - const INDEX_STARTS_AT_ONE: bool = true; - pub fn init(client: &AnyProtoClient) { client.add_entity_message_handler(Self::handle_remove_active_debug_line); client.add_entity_message_handler(Self::handle_shutdown_debug_client); @@ -585,24 +603,7 @@ impl DapStore { cx.spawn(|this, mut cx| async move { let capabilities = client - .request::(InitializeRequestArguments { - client_id: Some("zed".to_owned()), - client_name: Some("Zed".to_owned()), - adapter_id: client.adapter_id(), - locale: Some("en-US".to_owned()), - path_format: Some(InitializeRequestArgumentsPathFormat::Path), - supports_variable_type: Some(true), - supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(true), - supports_memory_references: Some(true), - supports_progress_reporting: Some(false), - supports_invalidated_event: Some(false), - lines_start_at1: Some(Self::INDEX_STARTS_AT_ONE), - columns_start_at1: Some(Self::INDEX_STARTS_AT_ONE), - supports_memory_event: Some(false), - supports_args_can_be_interpreted_by_shell: Some(false), - supports_start_debugging_request: Some(true), - }) + .request::(dap_client_capabilities(client.adapter_id())) .await?; this.update(&mut cx, |store, cx| { @@ -1185,9 +1186,8 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); }; - if Self::INDEX_STARTS_AT_ONE { - breakpoints.iter_mut().for_each(|bp| bp.line += 1u64) - } + // Adjust breakpoints as our client declares that indices start at one. + breakpoints.iter_mut().for_each(|bp| bp.line += 1u64); cx.background_executor().spawn(async move { client From 45db28a5cee1835e82ff30002eef1ccb969274a1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 12:50:55 +0100 Subject: [PATCH 563/650] Project compiles, yay --- crates/debugger_tools/src/dap_log.rs | 68 ++-- crates/project/src/debugger/client.rs | 10 +- crates/project/src/debugger/dap_store.rs | 464 +++++++++++------------ crates/project/src/project.rs | 2 +- crates/task/src/debug_format.rs | 3 + 5 files changed, 267 insertions(+), 280 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 5b9067d4986c61..17475fae09f856 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -12,7 +12,7 @@ use gpui::{ actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; -use project::{search::SearchQuery, Project}; +use project::{debugger::client::Client, search::SearchQuery, Project}; use settings::Settings as _; use std::{ borrow::Cow, @@ -166,19 +166,17 @@ impl LogStore { this.projects.remove(&weak_project); }), cx.subscribe(project, |this, project, event, cx| match event { - project::Event::DebugClientStarted((_, client_id)) => { - this.add_debug_client( - *client_id, - project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store.client_by_id(client_id, cx).and_then( - |(session, client)| { - Some((session, client.read(cx).adapter_client()?)) - }, - ) - }) - }), - ); + project::Event::DebugClientStarted(client_id) => { + let client = project.update(cx, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store + .client_by_id(client_id) + .and_then(|client| Some(client)) + }) + }); + if let Some(client) = client { + this.add_debug_client(*client_id, client, cx); + } } project::Event::DebugClientShutdown(client_id) => { this.remove_debug_client(*client_id, cx); @@ -292,35 +290,35 @@ impl LogStore { fn add_debug_client( &mut self, client_id: DebugAdapterClientId, - session_and_client: Option<(Entity, Arc)>, + client: Entity, + cx: &App, ) -> Option<&mut DebugAdapterState> { let client_state = self .debug_clients .entry(client_id) .or_insert_with(DebugAdapterState::new); - if let Some((_, client)) = session_and_client { - let io_tx = self.rpc_tx.clone(); + let io_tx = self.rpc_tx.clone(); - client.add_log_handler( - move |io_kind, message| { - io_tx - .unbounded_send((client_id, io_kind, message.to_string())) - .ok(); - }, - LogKind::Rpc, - ); + let client = client.read(cx).adapter_client()?; + client.add_log_handler( + move |io_kind, message| { + io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }, + LogKind::Rpc, + ); - let log_io_tx = self.adapter_log_tx.clone(); - client.add_log_handler( - move |io_kind, message| { - log_io_tx - .unbounded_send((client_id, io_kind, message.to_string())) - .ok(); - }, - LogKind::Adapter, - ); - } + let log_io_tx = self.adapter_log_tx.clone(); + client.add_log_handler( + move |io_kind, message| { + log_io_tx + .unbounded_send((client_id, io_kind, message.to_string())) + .ok(); + }, + LogKind::Adapter, + ); Some(client_state) } diff --git a/crates/project/src/debugger/client.rs b/crates/project/src/debugger/client.rs index 25a668bd6f68b2..76136df4ac8de7 100644 --- a/crates/project/src/debugger/client.rs +++ b/crates/project/src/debugger/client.rs @@ -207,7 +207,7 @@ impl Mode { /// Represents a current state of a single debug adapter and provides ways to mutate it. pub struct Client { mode: Mode, - + config: DebugAdapterConfig, pub(super) capabilities: Capabilities, client_id: DebugAdapterClientId, ignore_breakpoints: bool, @@ -293,13 +293,14 @@ impl CompletionsQuery { } impl Client { - pub(crate) fn local(adapter: Arc, capabilities: Capabilities) -> Self { + pub(crate) fn local(adapter: Arc, config: DebugAdapterConfig) -> Self { let client_id = adapter.id(); Self { mode: Mode::Local(adapter), client_id, - capabilities, + config, + capabilities: unimplemented!(), ignore_breakpoints: false, requests: HashMap::default(), modules: Vec::default(), @@ -326,6 +327,7 @@ impl Client { modules: Vec::default(), loaded_sources: Vec::default(), threads: IndexMap::default(), + config: todo!(), } } @@ -333,7 +335,7 @@ impl Client { &self.capabilities } pub fn configuration(&self) -> DebugAdapterConfig { - DebugAdapterConfig::default() + self.config.clone() } pub(crate) fn _wait_for_request( diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 1427fd0e245abb..0db48d2ed592ec 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -93,6 +93,109 @@ impl LocalDapStore { fn next_client_id(&self) -> DebugAdapterClientId { DebugAdapterClientId(self.next_client_id.fetch_add(1, SeqCst)) } + pub fn respond_to_start_debugging( + &mut self, + client: &Entity, + seq: u64, + args: Option, + cx: &mut Context, + ) -> Task> { + let config = client.read(cx).configuration(); + + let request_args = args.unwrap_or_else(|| StartDebuggingRequestArguments { + configuration: config.initialize_args.clone().unwrap_or_default(), + request: match config.request { + DebugRequestType::Launch => StartDebuggingRequestArgumentsRequest::Launch, + DebugRequestType::Attach(_) => StartDebuggingRequestArgumentsRequest::Attach, + }, + }); + + // Merge the new configuration over the existing configuration + let mut initialize_args = config.initialize_args.clone().unwrap_or_default(); + merge_json_value_into(request_args.configuration, &mut initialize_args); + + let new_config = DebugAdapterConfig { + label: config.label.clone(), + kind: config.kind.clone(), + request: match &request_args.request { + StartDebuggingRequestArgumentsRequest::Launch => DebugRequestType::Launch, + StartDebuggingRequestArgumentsRequest::Attach => DebugRequestType::Attach( + if let DebugRequestType::Attach(attach_config) = &config.request { + attach_config.clone() + } else { + AttachConfig::default() + }, + ), + }, + program: config.program.clone(), + cwd: config.cwd.clone(), + initialize_args: Some(initialize_args), + supports_attach: true, + }; + + cx.spawn(|this, mut cx| async move { + let (success, body) = { + let reconnect_task = this.update(&mut cx, |store, cx| { + if !unimplemented!("client.adapter().supports_attach()") + && matches!(new_config.request, DebugRequestType::Attach(_)) + { + Task::>::ready(Err(anyhow!( + "Debug adapter does not support `attach` request" + ))) + } else { + unimplemented!( + "store.reconnect_client(client.binary().clone(), new_config, cx)" + ); + } + }); + + match reconnect_task { + Ok(task) => match task.await { + Ok(_) => (true, None), + Err(error) => ( + false, + Some(serde_json::to_value(ErrorResponse { + error: Some(dap::Message { + id: seq, + format: error.to_string(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + })?), + ), + }, + Err(error) => ( + false, + Some(serde_json::to_value(ErrorResponse { + error: Some(dap::Message { + id: seq, + format: error.to_string(), + variables: None, + send_telemetry: None, + show_user: None, + url: None, + url_label: None, + }), + })?), + ), + } + }; + unimplemented!(); + Ok(()) + /*client + .send_message(Message::Response(Response { + seq, + body, + success, + request_seq: seq, + command: StartDebugging::COMMAND.to_string(), + })) + .await*/ + }) + } } pub struct RemoteDapStore { @@ -407,58 +510,58 @@ impl DapStore { } } - fn reconnect_client( - &mut self, - adapter: Arc, - binary: DebugAdapterBinary, - config: DebugAdapterConfig, - cx: &mut Context, - ) -> Task> { - if !adapter.supports_attach() && matches!(config.request, DebugRequestType::Attach(_)) { - return Task::ready(Err(anyhow!( - "Debug adapter does not support `attach` request" - ))); - } - - let client_id = self.as_local().unwrap().next_client_id(); - - cx.spawn(|dap_store, mut cx| async move { - let mut client = DebugAdapterClient::new(client_id, adapter, binary, &cx); - - client - .reconnect( - { - let dap_store = dap_store.clone(); - move |message, cx| { - dap_store - .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) - }) - .log_err(); - } - }, - &mut cx, - ) - .await?; - - dap_store.update(&mut cx, |store, cx| { - cx.new(|cx| { - let client_state = - debugger::client::Client::local(Arc::new(client), capabilities); - }); - - store.clients.insert(Arc::new(client), client_id); - - // don't emit this event ourself in tests, so we can add request, - // response and event handlers for this client - if !cfg!(any(test, feature = "test-support")) { - cx.emit(DapStoreEvent::DebugClientStarted(client_id)); - } - - cx.notify(); - }) - }) - } + // fn reconnect_client( + // &mut self, + // adapter: Arc, + // binary: DebugAdapterBinary, + // config: DebugAdapterConfig, + // cx: &mut Context, + // ) -> Task> { + // if !config.supports_attach && matches!(config.request, DebugRequestType::Attach(_)) { + // return Task::ready(Err(anyhow!( + // "Debug adapter does not support `attach` request" + // ))); + // } + + // let client_id = self.as_local().unwrap().next_client_id(); + + // cx.spawn(|dap_store, mut cx| async move { + // let mut client = DebugAdapterClient::new(client_id, adapter, binary, &cx); + + // client + // .reconnect( + // { + // let dap_store = dap_store.clone(); + // move |message, cx| { + // dap_store + // .update(cx, |_, cx| { + // cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + // }) + // .log_err(); + // } + // }, + // &mut cx, + // ) + // .await?; + + // dap_store.update(&mut cx, |store, cx| { + // cx.new(|cx| { + // let client_state = + // debugger::client::Client::local(Arc::new(client), capabilities); + // }); + + // store.clients.insert(Arc::new(client), client_id); + + // // don't emit this event ourself in tests, so we can add request, + // // response and event handlers for this client + // if !cfg!(any(test, feature = "test-support")) { + // cx.emit(DapStoreEvent::DebugClientStarted(client_id)); + // } + + // cx.notify(); + // }) + // }) + // } fn start_client_internal( &mut self, @@ -475,7 +578,9 @@ impl DapStore { cx.spawn(|this, mut cx| async move { let adapter = build_adapter(&config.kind).await?; - if !adapter.supports_attach() && matches!(config.request, DebugRequestType::Attach(_)) { + if !unimplemented!("adapter.supports_attach()") + && matches!(config.request, DebugRequestType::Attach(_)) + { bail!("Debug adapter does not support `attach` request"); } @@ -577,7 +682,7 @@ impl DapStore { this.update(&mut cx, |store, cx| { let client_id = client.id(); - store.clients.insert(client_id, client); + unimplemented!("store.clients.insert(client_id, client);"); cx.emit(DapStoreEvent::DebugClientStarted(client_id)); cx.notify(); @@ -640,44 +745,38 @@ impl DapStore { } } - pub fn launch( + pub fn new_session( &mut self, - client_id: DebugAdapterClientId, + config: DebugAdapterConfig, cx: &mut Context, ) -> Task> { - let Some(client) = self - .client_by_id(client_id) - .and_then(|client| Some(client.read(cx).adapter_client()?)) - else { - return Task::ready(Err(anyhow!("Could not find debug client: {:?}", client_id))); - }; - - let config = session.read(cx).as_local().unwrap().configuration(); - let mut adapter_args = client.adapter().request_args(&config); - if let Some(args) = config.initialize_args.clone() { - merge_json_value_into(args, &mut adapter_args); - } - - // TODO(debugger): GDB starts the debuggee program on launch instead of configurationDone - // causing our sent breakpoints to not be valid. This delay should eventually be taken out - let delay = if &client.adapter_id() == "gdb" { - Some( - cx.background_executor() - .timer(std::time::Duration::from_millis(20u64)), - ) - } else { - None - }; - - cx.background_executor().spawn(async move { - if let Some(delay) = delay { - delay.await; - } - - client - .request::(LaunchRequestArguments { raw: adapter_args }) - .await - }) + // let config = session.read(cx).as_local().unwrap().configuration(); + // let mut adapter_args = client.adapter().request_args(&config); + // if let Some(args) = config.initialize_args.clone() { + // merge_json_value_into(args, &mut adapter_args); + // } + + // // TODO(debugger): GDB starts the debuggee program on launch instead of configurationDone + // // causing our sent breakpoints to not be valid. This delay should eventually be taken out + // let delay = if &client.adapter_id() == "gdb" { + // Some( + // cx.background_executor() + // .timer(std::time::Duration::from_millis(20u64)), + // ) + // } else { + // None + // }; + + // cx.background_executor().spawn(async move { + // if let Some(delay) = delay { + // delay.await; + // } + + // client + // .request::(LaunchRequestArguments { raw: adapter_args }) + // .await + // }) + Task::ready(Ok(())) } pub fn attach( @@ -686,157 +785,42 @@ impl DapStore { process_id: u32, cx: &mut Context, ) -> Task> { - let Some(client) = self - .client_by_id(client_id) - .and_then(|client| Some(client.read(cx).adapter_client()?)) - else { - return Task::ready(Err( - anyhow!("Could not find debug client: {:?}", client_id,), - )); - }; - - // update the process id on the config, so when the `startDebugging` reverse request - // comes in we send another `attach` request with the already selected PID - // If we don't do this the user has to select the process twice if the adapter sends a `startDebugging` request - session.update(cx, |session, cx| { - session.as_local_mut().unwrap().update_configuration( - |config| { - config.request = DebugRequestType::Attach(task::AttachConfig { - process_id: Some(process_id), - }); - }, - cx, - ); - }); - - let config = session.read(cx).as_local().unwrap().configuration(); - let mut adapter_args = client.adapter().request_args(&config); - - if let Some(args) = config.initialize_args.clone() { - merge_json_value_into(args, &mut adapter_args); - } - - cx.background_executor().spawn(async move { - client - .request::(AttachRequestArguments { raw: adapter_args }) - .await - }) - } - - pub fn respond_to_start_debugging( - &mut self, - client_id: DebugAdapterClientId, - seq: u64, - args: Option, - cx: &mut Context, - ) -> Task> { - let Some((client, adapter)) = self - .client_by_id(client_id) - .and_then(|client| Some((client, client.read(cx).adapter_client()?))) - else { - return Task::ready(Err( - anyhow!("Could not find debug client: {:?}", client_id,), - )); - }; - - let config = client.read(cx).configuration(); - - let request_args = args.unwrap_or_else(|| StartDebuggingRequestArguments { - configuration: config.initialize_args.clone().unwrap_or_default(), - request: match config.request { - DebugRequestType::Launch => StartDebuggingRequestArgumentsRequest::Launch, - DebugRequestType::Attach(_) => StartDebuggingRequestArgumentsRequest::Attach, - }, - }); - - // Merge the new configuration over the existing configuration - let mut initialize_args = config.initialize_args.clone().unwrap_or_default(); - merge_json_value_into(request_args.configuration, &mut initialize_args); - - let new_config = DebugAdapterConfig { - label: config.label.clone(), - kind: config.kind.clone(), - request: match &request_args.request { - StartDebuggingRequestArgumentsRequest::Launch => DebugRequestType::Launch, - StartDebuggingRequestArgumentsRequest::Attach => DebugRequestType::Attach( - if let DebugRequestType::Attach(attach_config) = &config.request { - attach_config.clone() - } else { - AttachConfig::default() - }, - ), - }, - program: config.program.clone(), - cwd: config.cwd.clone(), - initialize_args: Some(initialize_args), - }; - - cx.spawn(|this, mut cx| async move { - let (success, body) = { - let reconnect_task = this.update(&mut cx, |store, cx| { - if !client.adapter().supports_attach() - && matches!(new_config.request, DebugRequestType::Attach(_)) - { - Task::ready(Err(anyhow!( - "Debug adapter does not support `attach` request" - ))) - } else { - store.reconnect_client(client.binary().clone(), new_config, cx) - } - }); - - match reconnect_task { - Ok(task) => match task.await { - Ok(_) => (true, None), - Err(error) => { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(error.to_string())); - }) - .log_err(); - - ( - false, - Some(serde_json::to_value(ErrorResponse { - error: Some(dap::Message { - id: seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - })?), - ) - } - }, - Err(error) => ( - false, - Some(serde_json::to_value(ErrorResponse { - error: Some(dap::Message { - id: seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - })?), - ), - } - }; - - client - .send_message(Message::Response(Response { - seq, - body, - success, - request_seq: seq, - command: StartDebugging::COMMAND.to_string(), - })) - .await - }) + unimplemented!(); + // let Some(client) = self + // .client_by_id(client_id) + // .and_then(|client| Some(client.read(cx).adapter_client()?)) + // else { + // return Task::ready(Err( + // anyhow!("Could not find debug client: {:?}", client_id,), + // )); + // }; + + // // update the process id on the config, so when the `startDebugging` reverse request + // // comes in we send another `attach` request with the already selected PID + // // If we don't do this the user has to select the process twice if the adapter sends a `startDebugging` request + // session.update(cx, |session, cx| { + // session.as_local_mut().unwrap().update_configuration( + // |config| { + // config.request = DebugRequestType::Attach(task::AttachConfig { + // process_id: Some(process_id), + // }); + // }, + // cx, + // ); + // }); + + // let config = session.read(cx).as_local().unwrap().configuration(); + // let mut adapter_args = client.adapter().request_args(&config); + + // if let Some(args) = config.initialize_args.clone() { + // merge_json_value_into(args, &mut adapter_args); + // } + + // cx.background_executor().spawn(async move { + // client + // .request::(AttachRequestArguments { raw: adapter_args }) + // .await + // }) } pub fn respond_to_run_in_terminal( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c7b3f6b27fa0da..e94aa6a084605d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1102,7 +1102,7 @@ impl Project { let mut dap_store = DapStore::new_remote(remote_id, client.clone().into(), breakpoint_store.clone()); - dap_store.request_active_debug_sessions(cx); + unimplemented!("dap_store.request_active_debug_sessions(cx)"); dap_store })?; diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 1cd8492c542d7e..f89728aa5e4ff9 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -131,6 +131,8 @@ pub struct DebugAdapterConfig { pub cwd: Option, /// Additional initialization arguments to be sent on DAP initialization pub initialize_args: Option, + /// Whether the debug adapter supports attaching to a running process. + pub supports_attach: bool, } /// Represents the type of the debugger adapter connection @@ -176,6 +178,7 @@ impl DebugTaskDefinition { program: self.program, cwd: cwd.clone(), initialize_args: self.initialize_args, + supports_attach: true, }); let args: Vec = Vec::new(); From ad356d9c8e234b06bd3a3fcfb165982d934b6f13 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:39:45 +0100 Subject: [PATCH 564/650] Building! And crashing --- crates/collab/src/rpc.rs | 1 - crates/debugger_tools/src/dap_log.rs | 108 +-- crates/debugger_ui/src/attach_modal.rs | 12 +- crates/debugger_ui/src/console.rs | 40 +- crates/debugger_ui/src/debugger_panel.rs | 823 +----------------- crates/debugger_ui/src/debugger_panel_item.rs | 201 ++--- crates/debugger_ui/src/loaded_source_list.rs | 19 +- crates/debugger_ui/src/module_list.rs | 25 +- crates/debugger_ui/src/stack_frame_list.rs | 28 +- crates/debugger_ui/src/variable_list.rs | 38 +- crates/project/src/debugger/client.rs | 4 +- crates/project/src/debugger/dap_store.rs | 17 +- crates/project/src/project.rs | 5 +- crates/proto/proto/zed.proto | 89 +- crates/proto/src/proto.rs | 2 - 15 files changed, 202 insertions(+), 1210 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 66b3c4b62583c3..29114afaf2cf5d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -429,7 +429,6 @@ impl Server { .add_message_handler( broadcast_project_message_from_host::, ) - .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) .add_message_handler( broadcast_project_message_from_host::, diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 17475fae09f856..cf43acb52b9a7b 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -373,11 +373,9 @@ impl Render for DapLogToolbarItemView { }); let current_client = current_client_id.and_then(|current_client_id| { - menu_rows.iter().find_map(|row| { - row.clients - .iter() - .find(|sub_item| sub_item.client_id == current_client_id) - }) + menu_rows + .iter() + .find(|row| row.client_id == current_client_id) }); let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView") @@ -403,56 +401,48 @@ impl Render for DapLogToolbarItemView { let menu_rows = menu_rows.clone(); ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| { for row in menu_rows.into_iter() { - menu = menu.header(format!("{}. {}", row.session_id.0, row.session_name)); - - for sub_item in row.clients.into_iter() { - menu = menu.custom_row(move |_window, _cx| { - div() - .w_full() - .pl_2() - .child( - Label::new(format!( - "{}. {}", - sub_item.client_id.0, sub_item.client_name, - )) - .color(workspace::ui::Color::Muted), + menu = menu.custom_row(move |_window, _cx| { + div() + .w_full() + .pl_2() + .child( + Label::new( + format!("{}. {}", row.client_id.0, row.client_name,), ) - .into_any_element() - }); - - if sub_item.has_adapter_logs { - menu = menu.custom_entry( - move |_window, _cx| { - div() - .w_full() - .pl_4() - .child(Label::new(ADAPTER_LOGS)) - .into_any_element() - }, - window.handler_for(&log_view, move |view, window, cx| { - view.show_log_messages_for_adapter( - sub_item.client_id, - window, - cx, - ); - }), - ); - } + .color(workspace::ui::Color::Muted), + ) + .into_any_element() + }); + if row.has_adapter_logs { menu = menu.custom_entry( move |_window, _cx| { div() .w_full() .pl_4() - .child(Label::new(RPC_MESSAGES)) + .child(Label::new(ADAPTER_LOGS)) .into_any_element() }, window.handler_for(&log_view, move |view, window, cx| { - view.show_rpc_trace_for_server(sub_item.client_id, window, cx); + view.show_log_messages_for_adapter(row.client_id, window, cx); }), ); } + + menu = menu.custom_entry( + move |_window, _cx| { + div() + .w_full() + .pl_4() + .child(Label::new(RPC_MESSAGES)) + .into_any_element() + }, + window.handler_for(&log_view, move |view, window, cx| { + view.show_rpc_trace_for_server(row.client_id, window, cx); + }), + ); } + menu }) .into() @@ -578,31 +568,18 @@ impl DapLogView { .read(cx) .dap_store() .read(cx) - .sessions() - .filter_map(|session| { + .clients() + .filter_map(|client| { + let client = client.read(cx).adapter_client()?; Some(DapMenuItem { - session_id: session.read(cx).id(), - session_name: session.read(cx).name(), - clients: { - let mut clients = session - .read_with(cx, |session, cx| session.clients(cx)) - .iter() - .map(|client| DapMenuSubItem { - client_id: client.id(), - client_name: client.adapter_id(), - has_adapter_logs: client.has_adapter_logs(), - selected_entry: self - .current_view - .map_or(LogKind::Adapter, |(_, kind)| kind), - }) - .collect::>(); - clients.sort_by_key(|item| item.client_id.0); - clients - }, + client_id: client.id(), + client_name: client.adapter_id(), + has_adapter_logs: client.has_adapter_logs(), + selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), }) }) .collect::>(); - menu_items.sort_by_key(|item| item.session_id.0); + menu_items.sort_by_key(|item| item.client_id.0); Some(menu_items) } @@ -691,13 +668,6 @@ fn log_contents(lines: &VecDeque) -> String { #[derive(Clone, PartialEq)] pub(crate) struct DapMenuItem { - pub session_id: DebugSessionId, - pub session_name: String, - pub clients: Vec, -} - -#[derive(Clone, PartialEq)] -pub(crate) struct DapMenuSubItem { pub client_id: DebugAdapterClientId, pub client_name: String, pub has_adapter_logs: bool, diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 3f4bc1524f0a2c..5b0685eb360b71 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -20,7 +20,7 @@ struct Candidate { pub(crate) struct AttachModalDelegate { selected_index: usize, matches: Vec, - session_id: DebugSessionId, + session_id: DebugAdapterClientId, placeholder_text: Arc, dap_store: Entity, client_id: DebugAdapterClientId, @@ -29,7 +29,7 @@ pub(crate) struct AttachModalDelegate { impl AttachModalDelegate { pub fn new( - session_id: DebugSessionId, + session_id: DebugAdapterClientId, client_id: DebugAdapterClientId, dap_store: Entity, ) -> Self { @@ -52,7 +52,7 @@ pub(crate) struct AttachModal { impl AttachModal { pub fn new( - session_id: &DebugSessionId, + session_id: &DebugAdapterClientId, client_id: DebugAdapterClientId, dap_store: Entity, window: &mut Window, @@ -132,8 +132,8 @@ impl PickerDelegate for AttachModalDelegate { } else { let Some(client) = this.delegate.dap_store.update(cx, |store, cx| { store - .client_by_id(&this.delegate.client_id, cx) - .and_then(|(_, client)| client.read(cx).adapter_client()) + .client_by_id(&this.delegate.client_id) + .and_then(|client| client.read(cx).adapter_client()) }) else { return Vec::new(); }; @@ -224,7 +224,7 @@ impl PickerDelegate for AttachModalDelegate { self.dap_store.update(cx, |store, cx| { store - .attach(&self.session_id, self.client_id, candidate.pid, cx) + .attach(self.client_id, candidate.pid, cx) .detach_and_log_err(cx); }); diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index c0d2275fd7164e..ce0da3e5c3025c 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -13,7 +13,7 @@ use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; use language::{Buffer, CodeLabel, LanguageServerId}; use menu::Confirm; use project::{ - debugger::dap_session::{CompletionsQuery, DebugSession}, + debugger::client::{Client, CompletionsQuery}, Completion, }; use settings::Settings; @@ -33,7 +33,7 @@ pub struct Console { groups: Vec, console: Entity, query_bar: Entity, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, variable_list: Entity, @@ -42,7 +42,7 @@ pub struct Console { impl Console { pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, stack_frame_list: Entity, variable_list: Entity, @@ -234,11 +234,7 @@ impl Console { expression }); - let Some(client_state) = self.session.read(cx).client_state(self.client_id) else { - return; - }; - - client_state.update(cx, |state, cx| { + self.session.update(cx, |state, cx| { state.evaluate( expression, Some(dap::EvaluateArgumentsContext::Variables), @@ -369,10 +365,8 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { .read(cx) .session .read(cx) - .client_state(console.read(cx).client_id) - .map(|state| state.read(cx).capabilities()) - .map(|caps| caps.supports_completions_request) - .flatten() + .capabilities() + .supports_completions_request .unwrap_or_default(); if support_completions { @@ -498,21 +492,15 @@ impl ConsoleQueryBarCompletionProvider { buffer_position: language::Anchor, cx: &mut Context, ) -> gpui::Task>> { - let client_id = console.read(cx).client_id; - let completion_task = console.update(cx, |console, cx| { - if let Some(client_state) = console.session.read(cx).client_state(client_id) { - client_state.update(cx, |state, cx| { - let frame_id = Some(console.stack_frame_list.read(cx).current_stack_frame_id()); - - state.completions( - CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id), - cx, - ) - }) - } else { - Task::ready(Err(anyhow!("failed to fetch completions"))) - } + console.session.update(cx, |state, cx| { + let frame_id = Some(console.stack_frame_list.read(cx).current_stack_frame_id()); + + state.completions( + CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id), + cx, + ) + }) }); cx.background_executor().spawn(async move { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index f9d5070e89371f..4989ee6de82c06 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -17,12 +17,12 @@ use gpui::{ }; use project::{ debugger::{ - dap_session::ThreadId, + client::ThreadId, dap_store::{DapStore, DapStoreEvent}, }, terminals::TerminalKind, }; -use rpc::proto::{self, SetDebuggerPanelItem, UpdateDebugAdapter}; +use rpc::proto::{self, UpdateDebugAdapter}; use serde_json::Value; use settings::Settings; use std::{any::TypeId, collections::VecDeque, path::PathBuf, u64}; @@ -120,7 +120,6 @@ pub struct DebugPanel { workspace: WeakEntity, _subscriptions: Vec, message_queue: HashMap>, - thread_states: BTreeMap<(DebugAdapterClientId, ThreadId), Entity>, } impl DebugPanel { @@ -155,53 +154,6 @@ impl DebugPanel { let _subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe_in(&pane, window, Self::handle_pane_event), - cx.subscribe_in(&dap_store, window, Self::on_dap_store_event), - cx.subscribe_in(&project, window, { - move |this: &mut Self, _, event, window, cx| match event { - project::Event::DebugClientStarted((session_id, client_id)) => { - this.handle_debug_client_started(session_id, *client_id, window, cx); - } - project::Event::DebugClientEvent { client_id, message } => match message { - Message::Event(event) => { - this.handle_debug_client_events(*client_id, event, window, cx); - } - Message::Request(request) => { - if StartDebugging::COMMAND == request.command { - this.handle_start_debugging_request( - *client_id, - request.seq, - request.arguments.clone(), - cx, - ); - } else if RunInTerminal::COMMAND == request.command { - this.handle_run_in_terminal_request( - *client_id, - request.seq, - request.arguments.clone(), - window, - cx, - ); - } else { - debug_assert!(false, "Encountered unexpected command type"); - } - } - _ => unreachable!(), - }, - project::Event::DebugClientShutdown(client_id) => { - cx.emit(DebugPanelEvent::ClientShutdown(*client_id)); - - this.message_queue.remove(client_id); - this.thread_states - .retain(|&(client_id_, _), _| client_id_ != *client_id); - - cx.notify(); - } - project::Event::SetDebugClient(set_debug_client) => { - this.handle_set_debug_panel_item(set_debug_client, window, cx); - } - _ => {} - } - }), ]; let dap_store = project.read(cx).dap_store(); @@ -211,27 +163,11 @@ impl DebugPanel { size: px(300.), _subscriptions, focus_handle: cx.focus_handle(), - thread_states: Default::default(), message_queue: Default::default(), workspace: workspace.weak_handle(), dap_store: dap_store.clone(), }; - if let Some(mut dap_event_queue) = debug_panel - .dap_store - .clone() - .update(cx, |this, _| this.remote_event_queue()) - { - while let Some(dap_event) = dap_event_queue.pop_front() { - debug_panel.on_dap_store_event( - &debug_panel.dap_store.clone(), - &dap_event, - window, - cx, - ); - } - } - debug_panel }) } @@ -252,8 +188,7 @@ impl DebugPanel { ( true, item.update(cx, |this, cx| this.capabilities(cx)) - .map(|caps| caps.supports_step_back) - .flatten() + .supports_step_back .unwrap_or(false), ) }) @@ -356,23 +291,13 @@ impl DebugPanel { let thread_panel = item.downcast::().unwrap(); let thread_id = thread_panel.read(cx).thread_id(); - let client_id = thread_panel.read(cx).client_id(); - - self.thread_states.remove(&(client_id, thread_id)); cx.notify(); - let Some(client_state) = thread_panel - .read(cx) - .session() - .read(cx) - .client_state(client_id) - else { - return; - }; - - client_state.update(cx, |state, cx| { - state.terminate_threads(Some(vec![thread_id; 1]), cx); + thread_panel.update(cx, |this, cx| { + this.session().update(cx, |state, cx| { + state.terminate_threads(Some(vec![thread_id; 1]), cx); + }) }); } pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), @@ -401,740 +326,6 @@ impl DebugPanel { _ => {} } } - - fn handle_start_debugging_request( - &mut self, - client_id: DebugAdapterClientId, - seq: u64, - request_args: Option, - cx: &mut Context, - ) { - let args = if let Some(args) = request_args { - serde_json::from_value(args.clone()).ok() - } else { - None - }; - - self.dap_store.update(cx, |store, cx| { - store - .respond_to_start_debugging(session_id, client_id, seq, args, cx) - .detach_and_log_err(cx); - }); - } - - fn handle_run_in_terminal_request( - &mut self, - client_id: DebugAdapterClientId, - seq: u64, - request_args: Option, - window: &mut Window, - cx: &mut Context, - ) { - let request_args = request_args.and_then(|request_args| { - serde_json::from_value::(request_args).ok() - }); - let Some(request_args) = request_args else { - self.dap_store.update(cx, |store, cx| { - store - .respond_to_run_in_terminal( - session_id, - client_id, - false, - seq, - serde_json::to_value(ErrorResponse { - error: Some(dap::Message { - id: seq, - format: - "Request arguments must be provided when spawnng debug terminal" - .into(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(), - cx, - ) - .detach_and_log_err(cx); - }); - return; - }; - - let mut envs: HashMap = Default::default(); - if let Some(Value::Object(env)) = request_args.env { - for (key, value) in env { - let value_str = match (key.as_str(), value) { - (_, Value::String(value)) => value, - _ => continue, - }; - - envs.insert(key, value_str); - } - } - - let terminal_task = self.workspace.update(cx, |workspace, cx| { - let terminal_panel = workspace.panel::(cx).unwrap(); - - terminal_panel.update(cx, |terminal_panel, cx| { - let mut args = request_args.args.clone(); - - // Handle special case for NodeJS debug adapter - // If only the Node binary path is provided, we set the command to None - // This prevents the NodeJS REPL from appearing, which is not the desired behavior - // The expected usage is for users to provide their own Node command, e.g., `node test.js` - // This allows the NodeJS debug client to attach correctly - let command = if args.len() > 1 { - Some(args.remove(0)) - } else { - None - }; - - let terminal_task = terminal_panel.add_terminal( - TerminalKind::Debug { - command, - args, - envs, - cwd: PathBuf::from(request_args.cwd), - title: request_args.title, - }, - task::RevealStrategy::Always, - window, - cx, - ); - - cx.spawn(|_, mut cx| async move { - let pid_task = async move { - let terminal = terminal_task.await?; - - terminal.read_with(&mut cx, |terminal, _| terminal.pty_info.pid()) - }; - - pid_task.await - }) - }) - }); - - let session_id = *session_id; - - cx.spawn(|this, mut cx| async move { - // Ensure a response is always sent, even in error cases, - // to maintain proper communication with the debug adapter - let (success, body) = match terminal_task { - Ok(pid_task) => match pid_task.await { - Ok(pid) => ( - true, - serde_json::to_value(RunInTerminalResponse { - process_id: None, - shell_process_id: pid.map(|pid| pid.as_u32() as u64), - }) - .ok(), - ), - Err(error) => { - this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(error.to_string())); - }) - }) - .log_err(); - - ( - false, - serde_json::to_value(ErrorResponse { - error: Some(dap::Message { - id: seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(), - ) - } - }, - Err(error) => ( - false, - serde_json::to_value(ErrorResponse { - error: Some(dap::Message { - id: seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(), - ), - }; - - let respond_task = this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - store.respond_to_run_in_terminal(&session_id, client_id, success, seq, body, cx) - }) - }); - - respond_task?.await - }) - .detach_and_log_err(cx); - } - - fn handle_debug_client_started( - &self, - client_id: DebugAdapterClientId, - window: &mut Window, - cx: &mut Context, - ) { - let Some(session) = self - .dap_store - .read(cx) - .session_by_id(session_id) - .and_then(|session| session.read(cx).as_local()) - else { - return; - }; - - let session_id = *session_id; - let workspace = self.workspace.clone(); - let request_type = session.configuration().request.clone(); - cx.spawn_in(window, |this, mut cx| async move { - let task = this.update(&mut cx, |this, cx| { - this.dap_store - .update(cx, |store, cx| store.initialize(&session_id, client_id, cx)) - })?; - - task.await?; - - let result = match request_type { - DebugRequestType::Launch => { - let task = this.update(&mut cx, |this, cx| { - this.dap_store - .update(cx, |store, cx| store.launch(&session_id, client_id, cx)) - }); - - task?.await - } - DebugRequestType::Attach(config) => { - if let Some(process_id) = config.process_id { - let task = this.update(&mut cx, |this, cx| { - this.dap_store.update(cx, |store, cx| { - store.attach(&session_id, client_id, process_id, cx) - }) - })?; - - task.await - } else { - this.update_in(&mut cx, |this, window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - AttachModal::new( - &session_id, - client_id, - this.dap_store.clone(), - window, - cx, - ) - }) - }) - })? - } - } - }; - - if result.is_err() { - this.update(&mut cx, |debug_panel, cx| { - debug_panel.dap_store.update(cx, |store, cx| { - cx.emit(DapStoreEvent::Notification( - "Failed to start debug session".into(), - )); - - store - .shutdown_client(&session_id, cx) - .detach_and_log_err(cx); - }); - })?; - } - - result - }) - .detach_and_log_err(cx); - } - - fn handle_debug_client_events( - &mut self, - client_id: DebugAdapterClientId, - event: &Events, - window: &mut Window, - cx: &mut Context, - ) { - match event { - Events::Initialized(event) => { - self.handle_initialized_event(&session_id, client_id, event, cx) - } - Events::Stopped(event) => { - self.handle_stopped_event(&session_id, client_id, event, window, cx) - } - Events::Continued(event) => self.handle_continued_event(client_id, event, cx), - Events::Exited(event) => self.handle_exited_event(client_id, event, cx), - Events::Terminated(event) => { - self.handle_terminated_event(&session_id, client_id, event, cx) - } - Events::Thread(event) => self.handle_thread_event(client_id, event, cx), - Events::Output(event) => self.handle_output_event(client_id, event, cx), - Events::Breakpoint(_) => {} - Events::Module(event) => self.handle_module_event(client_id, event, cx), - Events::LoadedSource(event) => self.handle_loaded_source_event(client_id, event, cx), - Events::Capabilities(event) => { - self.handle_capabilities_changed_event(session_id, client_id, event, cx); - } - Events::Memory(_) => {} - Events::Process(_) => {} - Events::ProgressEnd(_) => {} - Events::ProgressStart(_) => {} - Events::ProgressUpdate(_) => {} - Events::Invalidated(_) => {} - Events::Other(_) => {} - } - } - - fn handle_initialized_event( - &mut self, - client_id: DebugAdapterClientId, - capabilities: &Option, - cx: &mut Context, - ) { - if let Some(capabilities) = capabilities { - self.dap_store.update(cx, |store, cx| { - store.update_capabilities_for_client(client_id, capabilities, cx); - }); - - cx.emit(DebugPanelEvent::CapabilitiesChanged(client_id)); - } - - let session_id = *session_id; - - cx.spawn(|this, mut cx| async move { - this.update(&mut cx, |debug_panel, cx| { - debug_panel.workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.initial_send_breakpoints(client_id, cx) - }) - }) - })?? - .await; - - this.update(&mut cx, |debug_panel, cx| { - debug_panel - .dap_store - .update(cx, |store, cx| store.configuration_done(client_id, cx)) - })? - .await - }) - .detach_and_log_err(cx); - } - - fn handle_continued_event( - &mut self, - client_id: DebugAdapterClientId, - event: &ContinuedEvent, - cx: &mut Context, - ) { - cx.emit(DebugPanelEvent::Continued((client_id, event.clone()))); - } - - fn handle_stopped_event( - &mut self, - client_id: DebugAdapterClientId, - event: &StoppedEvent, - window: &mut Window, - cx: &mut Context, - ) { - let Some(thread_id) = event.thread_id.map(|thread_id| ThreadId(thread_id)) else { - return; - }; - - let Some(session) = self - .dap_store - .read(cx) - .session_by_id(session_id) - .map(|session| session) - else { - return; // this can/should never happen - }; - - cx.spawn_in(window, { - let event = event.clone(); - |this, mut cx| async move { - let workspace = this.update_in(&mut cx, |this, window, cx| { - let thread_state = this - .thread_states - .entry((client_id, thread_id)) - .or_insert(cx.new(|_| ThreadState::default())) - .clone(); - - thread_state.update(cx, |thread_state, _| { - thread_state.stopped = true; - thread_state.status = ThreadStatus::Stopped; - }); - - let existing_item = this.debug_panel_item_by_client(client_id, thread_id, cx); - if existing_item.is_none() { - let debug_panel = cx.entity(); - this.pane.update(cx, |pane, cx| { - let tab = cx.new(|cx| { - DebugPanelItem::new( - session, - client_id, - thread_id, - thread_state, - &debug_panel, - this.workspace.clone(), - window, - cx, - ) - }); - - pane.add_item(Box::new(tab), true, true, None, window, cx); - }); - - if let Some(message_queue) = this.message_queue.get(&client_id) { - for output in message_queue.iter() { - cx.emit(DebugPanelEvent::Output((client_id, output.clone()))); - } - } - } - - let go_to_stack_frame = if let Some(item) = this.pane.read(cx).active_item() { - item.downcast::().map_or(false, |pane| { - let pane = pane.read(cx); - pane.thread_id() == thread_id && pane.client_id() == client_id - }) - } else { - true - }; - - cx.emit(DebugPanelEvent::Stopped { - client_id, - event, - go_to_stack_frame, - }); - - this.workspace.clone() - })?; - - cx.update(|window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - }) - }) - } - }) - .detach_and_log_err(cx); - } - - fn handle_thread_event( - &mut self, - client_id: DebugAdapterClientId, - event: &ThreadEvent, - cx: &mut Context, - ) { - let thread_id = ThreadId(event.thread_id); - - if let Some(thread_state) = self.thread_states.get(&(client_id, thread_id)) { - if !thread_state.read(cx).stopped && event.reason == ThreadEventReason::Exited { - const MESSAGE: &'static str = "Debug session exited without hitting breakpoints\n\nTry adding a breakpoint, or define the correct path mapping for your debugger."; - - self.dap_store.update(cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(MESSAGE.into())); - }); - }; - } - - if event.reason == ThreadEventReason::Started { - self.thread_states - .insert((client_id, thread_id), cx.new(|_| ThreadState::default())); - } - - cx.emit(DebugPanelEvent::Thread((client_id, event.clone()))); - cx.notify(); - } - - fn handle_exited_event( - &mut self, - client_id: DebugAdapterClientId, - _: &ExitedEvent, - cx: &mut Context, - ) { - cx.emit(DebugPanelEvent::Exited(client_id)); - } - - fn handle_terminated_event( - &mut self, - client_id: DebugAdapterClientId, - event: &Option, - cx: &mut Context, - ) { - let restart_args = event.clone().and_then(|e| e.restart); - - for (_, thread_state) in self - .thread_states - .range_mut(&(client_id, ThreadId::MIN)..&(client_id, ThreadId::MAX)) - { - thread_state.update(cx, |thread_state, cx| { - thread_state.status = ThreadStatus::Ended; - - cx.notify(); - }); - } - - self.dap_store.update(cx, |store, cx| { - if restart_args - .as_ref() - .is_some_and(|v| v.as_bool().unwrap_or(true)) - { - let Some(session) = store.session_by_client_id(client_id) else { - return; - }; - - let Some(client_state) = session.read(cx).client_state(client_id) else { - return; - }; - - client_state.update(cx, |state, cx| { - state.restart(restart_args, cx); - }); - } else { - store - .shutdown_client(&session_id, cx) - .detach_and_log_err(cx); - } - }); - - cx.emit(DebugPanelEvent::Terminated(client_id)); - } - - fn handle_output_event( - &mut self, - client_id: DebugAdapterClientId, - event: &OutputEvent, - cx: &mut Context, - ) { - self.message_queue - .entry(client_id) - .or_default() - .push_back(event.clone()); - - cx.emit(DebugPanelEvent::Output((client_id, event.clone()))); - } - - fn on_dap_store_event( - &mut self, - _: &Entity, - event: &DapStoreEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - DapStoreEvent::SetDebugPanelItem(set_debug_panel_item) => { - self.handle_set_debug_panel_item(set_debug_panel_item, window, cx); - } - DapStoreEvent::UpdateDebugAdapter(debug_adapter_update) => { - self.handle_debug_adapter_update(debug_adapter_update, window, cx); - } - DapStoreEvent::UpdateThreadStatus(thread_status_update) => { - self.handle_thread_status_update(thread_status_update, cx); - } - DapStoreEvent::RemoteHasInitialized => self.handle_remote_has_initialized(window, cx), - _ => {} - } - } - - pub(crate) fn handle_thread_status_update( - &mut self, - update: &proto::UpdateThreadStatus, - cx: &mut Context, - ) { - if let Some(thread_state) = self.thread_states.get_mut(&( - DebugAdapterClientId::from_proto(update.client_id), - ThreadId(update.thread_id), - )) { - thread_state.update(cx, |thread_state, _| { - thread_state.status = ThreadStatus::from_proto(update.status()); - }); - - cx.notify(); - } - } - - pub(crate) fn handle_debug_adapter_update( - &mut self, - update: &UpdateDebugAdapter, - _window: &mut Window, - cx: &mut Context, - ) { - let client_id = DebugAdapterClientId::from_proto(update.client_id); - let thread_id = update.thread_id.map(|thread_id| ThreadId(thread_id)); - - let active_item = self - .pane - .read(cx) - .active_item() - .and_then(|item| item.downcast::()); - - let _search = self - .pane - .read(cx) - .items() - .filter_map(|item| item.downcast::()) - .find_map(|item_view| { - let item = item_view.read(cx); - - if item.client_id() == client_id - && thread_id.map(|id| id == item.thread_id()).unwrap_or(true) - { - Some(( - item_view.clone(), - active_item - .as_ref() - .map_or(false, |this| this == &item_view), - )) - } else { - None - } - }); - - // if let Some((debug_panel_item, is_active_item)) = search { - // TODO(debugger): make this work again - // debug_panel_item.update(cx, |this, cx| { - // this.update_adapter(update, window, cx); - - // if is_active_item { - // this.go_to_current_stack_frame(window, cx); - // } - // }); - // } - } - - fn handle_remote_has_initialized(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(mut dap_event_queue) = self - .dap_store - .clone() - .update(cx, |this, _| this.remote_event_queue()) - { - while let Some(dap_event) = dap_event_queue.pop_front() { - self.on_dap_store_event(&self.dap_store.clone(), &dap_event, window, cx); - } - } - } - - pub(crate) fn handle_set_debug_panel_item( - &mut self, - payload: &SetDebuggerPanelItem, - window: &mut Window, - cx: &mut Context, - ) { - let session_id = DebugSessionId::from_proto(payload.session_id); - let Some(session) = self.dap_store.read(cx).session_by_id(&session_id) else { - return; - }; - - let client_id = DebugAdapterClientId::from_proto(payload.client_id); - let thread_id = ThreadId(payload.thread_id); - let thread_state = payload.thread_state.clone().unwrap(); - let thread_state = cx.new(|_| ThreadState::from_proto(thread_state)); - - let mut existing_item = self - .pane - .read(cx) - .items() - .filter_map(|item| item.downcast::()) - .find(|item| { - let item = item.read(cx); - - item.client_id() == client_id && item.thread_id() == thread_id - }); - - let debug_panel_item = existing_item.get_or_insert_with(|| { - self.thread_states - .insert((client_id, thread_id), thread_state.clone()); - - let debug_panel = cx.entity(); - let debug_panel_item = self.pane.update(cx, |pane, cx| { - let debug_panel_item = cx.new(|cx| { - DebugPanelItem::new( - session, - client_id, - thread_id, - thread_state, - &debug_panel, - self.workspace.clone(), - window, - cx, - ) - }); - - self.dap_store.update(cx, |dap_store, cx| { - dap_store.add_remote_session(session_id, None, cx); - dap_store.add_client_to_session(session_id, client_id); - }); - - pane.add_item( - Box::new(debug_panel_item.clone()), - true, - true, - None, - window, - cx, - ); - debug_panel_item - }); - - debug_panel_item - }); - - debug_panel_item.update(cx, |this, cx| { - this.from_proto(payload, cx); - }); - - cx.notify(); - } - - fn handle_module_event( - &mut self, - client_id: DebugAdapterClientId, - event: &ModuleEvent, - cx: &mut Context, - ) { - cx.emit(DebugPanelEvent::Module((client_id, event.clone()))); - } - - fn handle_loaded_source_event( - &mut self, - client_id: DebugAdapterClientId, - event: &LoadedSourceEvent, - cx: &mut Context, - ) { - cx.emit(DebugPanelEvent::LoadedSource((client_id, event.clone()))); - } - - fn handle_capabilities_changed_event( - &mut self, - session_id: &DebugSessionId, - client_id: DebugAdapterClientId, - event: &CapabilitiesEvent, - cx: &mut Context, - ) { - self.dap_store.update(cx, |store, cx| { - store.update_capabilities_for_client(client_id, &event.capabilities, cx); - }); - - cx.emit(DebugPanelEvent::CapabilitiesChanged(client_id)); - } } impl EventEmitter for DebugPanel {} diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 9983b2e6c7f974..693165ab29b514 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -13,8 +13,9 @@ use dap::{ use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::debugger::dap_session::{DebugSession, ThreadId}; -use rpc::proto::{self, DebuggerThreadStatus, PeerId, SetDebuggerPanelItem}; +use project::debugger::client::Client; +use project::debugger::client::ThreadId; +use rpc::proto::{self, DebuggerThreadStatus, PeerId}; use settings::Settings; use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, PopoverMenu, Tooltip}; use workspace::{ @@ -61,7 +62,7 @@ pub struct DebugPanelItem { console: Entity, focus_handle: FocusHandle, remote_id: Option, - session: Entity, + session: Entity, show_console_indicator: bool, module_list: Entity, active_thread_item: ThreadItem, @@ -77,7 +78,7 @@ pub struct DebugPanelItem { impl DebugPanelItem { #[allow(clippy::too_many_arguments)] pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, thread_id: ThreadId, thread_state: Entity, @@ -191,29 +192,6 @@ impl DebugPanelItem { } } - pub(crate) fn from_proto(&mut self, state: &SetDebuggerPanelItem, cx: &mut Context) { - self.thread_state.update(cx, |thread_state, _| { - let (status, stopped) = state - .thread_state - .as_ref() - .map_or((DebuggerThreadStatus::Stopped, true), |thread_state| { - (thread_state.thread_status(), true) - }); - - thread_state.status = ThreadStatus::from_proto(status); - thread_state.stopped = stopped; - }); - - self.active_thread_item = ThreadItem::from_proto(state.active_thread_item()); - - if let Some(variable_list_state) = state.variable_list.as_ref() { - self.variable_list - .update(cx, |this, cx| this.set_from_proto(variable_list_state, cx)); - } - - cx.notify(); - } - pub fn update_thread_state_status(&mut self, status: ThreadStatus, cx: &mut Context) { self.thread_state.update(cx, |thread_state, cx| { thread_state.status = status; @@ -260,11 +238,9 @@ impl DebugPanelItem { return; } - if let Some(client_state) = self.session.read(cx).client_state(*client_id) { - client_state.update(cx, |state, cx| { - state.invalidate(cx); - }); - } + self.session.update(cx, |state, cx| { + state.invalidate(cx); + }); cx.emit(DebugPanelItemEvent::Stopped { go_to_stack_frame }); } @@ -336,9 +312,8 @@ impl DebugPanelItem { return; } - if let Some(state) = self.session.read(cx).client_state(self.client_id) { - state.update(cx, |state, cx| state.handle_loaded_source_event(event, cx)); - } + self.session + .update(cx, |state, cx| state.handle_loaded_source_event(event, cx)); } fn handle_client_shutdown_event( @@ -420,7 +395,7 @@ impl DebugPanelItem { // } // } - pub fn session(&self) -> &Entity { + pub fn session(&self) -> &Entity { &self.session } @@ -468,11 +443,8 @@ impl DebugPanelItem { self.session.read(cx).ignore_breakpoints() } - pub fn capabilities(&self, cx: &mut Context) -> Option { - self.session() - .read(cx) - .client_state(self.client_id) - .map(|state| state.read(cx).capabilities().clone()) + pub fn capabilities(&self, cx: &mut Context) -> Capabilities { + self.session().read(cx).capabilities().clone() } fn clear_highlights(&self, _cx: &mut Context) { @@ -547,115 +519,70 @@ impl DebugPanelItem { } pub fn continue_thread(&mut self, cx: &mut Context) { - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.continue_thread(self.thread_id, cx); - }); - }); + self.session().update(cx, |state, cx| { + state.continue_thread(self.thread_id, cx); + }); } pub fn step_over(&mut self, cx: &mut Context) { let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.step_over(self.thread_id, granularity, cx); - }); - }); + self.session().update(cx, |state, cx| { + state.step_over(self.thread_id, granularity, cx); + }); } pub fn step_in(&mut self, cx: &mut Context) { let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.step_in(self.thread_id, granularity, cx); - }); - }); + self.session().update(cx, |state, cx| { + state.step_in(self.thread_id, granularity, cx); + }); } pub fn step_out(&mut self, cx: &mut Context) { let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.step_out(self.thread_id, granularity, cx); - }); - }); + self.session().update(cx, |state, cx| { + state.step_out(self.thread_id, granularity, cx); + }); } pub fn step_back(&mut self, cx: &mut Context) { let granularity = DebuggerSettings::get_global(cx).stepping_granularity; - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.step_back(self.thread_id, granularity, cx); - }); - }); + self.session().update(cx, |state, cx| { + state.step_back(self.thread_id, granularity, cx); + }); } pub fn restart_client(&self, cx: &mut Context) { - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.restart(None, cx); - }); - }); + self.session().update(cx, |state, cx| { + state.restart(None, cx); + }); } pub fn pause_thread(&self, cx: &mut Context) { - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.pause_thread(self.thread_id, cx); - }); - }); + self.session().update(cx, |state, cx| { + state.pause_thread(self.thread_id, cx); + }); } pub fn stop_thread(&self, cx: &mut Context) { - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.terminate_threads(Some(vec![self.thread_id; 1]), cx); - }); - }); + self.session().update(cx, |state, cx| { + state.terminate_threads(Some(vec![self.thread_id; 1]), cx); + }); } pub fn disconnect_client(&self, cx: &mut Context) { - self.session() - .read(cx) - .client_state(self.client_id) - .map(|entity| { - entity.update(cx, |state, cx| { - state.disconnect_client(cx); - }); - }); + self.session().update(cx, |state, cx| { + state.disconnect_client(cx); + }); } pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context) { self.session.update(cx, |session, cx| { - session.set_ignore_breakpoints(!session.ignore_breakpoints(), cx); + session.set_ignore_breakpoints(!session.breakpoints_enabled()); }); } } @@ -677,23 +604,19 @@ impl Item for DebugPanelItem { _window: &Window, cx: &App, ) -> AnyElement { - Label::new(format!( - "{} - Thread {}", - self.session.read(cx).name(), - self.thread_id.0 - )) - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() + Label::new(format!("{} - Thread {}", todo!(), self.thread_id.0)) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } fn tab_tooltip_text(&self, cx: &App) -> Option { Some(SharedString::from(format!( "{} Thread {} - {:?}", - self.session.read(cx).name(), + todo!(), self.thread_id.0, self.thread_state.read(cx).status, ))) @@ -838,11 +761,7 @@ impl Render for DebugPanelItem { } }) .when( - capabilities - .as_ref() - .map(|caps| caps.supports_step_back) - .flatten() - .unwrap_or(false), + capabilities.supports_step_back.unwrap_or(false), |this| { this.child( IconButton::new( @@ -901,9 +820,7 @@ impl Render for DebugPanelItem { })) .disabled( !capabilities - .as_ref() - .map(|caps| caps.supports_restart_request) - .flatten() + .supports_restart_request .unwrap_or_default(), ) .tooltip(move |window, cx| { @@ -946,10 +863,10 @@ impl Render for DebugPanelItem { .child( IconButton::new( "debug-ignore-breakpoints", - if self.session.read(cx).ignore_breakpoints() { - IconName::DebugIgnoreBreakpoints - } else { + if self.session.read(cx).breakpoints_enabled() { IconName::DebugBreakpoint + } else { + IconName::DebugIgnoreBreakpoints }, ) .icon_size(IconSize::Small) @@ -1008,11 +925,7 @@ impl Render for DebugPanelItem { cx, )) .when( - capabilities - .as_ref() - .map(|caps| caps.supports_modules_request) - .flatten() - .unwrap_or_default(), + capabilities.supports_modules_request.unwrap_or_default(), |this| { this.child(self.render_entry_button( &SharedString::from("Modules"), @@ -1023,9 +936,7 @@ impl Render for DebugPanelItem { ) .when( capabilities - .as_ref() - .map(|caps| caps.supports_loaded_sources_request) - .flatten() + .supports_loaded_sources_request .unwrap_or_default(), |this| { this.child(self.render_entry_button( diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs index 84296c728debb0..8b0f6fd30e3a69 100644 --- a/crates/debugger_ui/src/loaded_source_list.rs +++ b/crates/debugger_ui/src/loaded_source_list.rs @@ -1,6 +1,6 @@ use dap::client::DebugAdapterClientId; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; -use project::debugger::dap_session::DebugSession; +use project::debugger::client::Client; use ui::prelude::*; use util::maybe; @@ -8,13 +8,13 @@ pub struct LoadedSourceList { list: ListState, focus_handle: FocusHandle, _subscription: Subscription, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, } impl LoadedSourceList { pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, cx: &mut Context, ) -> Self { @@ -35,8 +35,7 @@ impl LoadedSourceList { }, ); - let client_state = session.read(cx).client_state(client_id).unwrap(); - let _subscription = cx.observe(&client_state, |loaded_source_list, state, cx| { + let _subscription = cx.observe(&session, |loaded_source_list, state, cx| { let len = state.update(cx, |state, cx| state.loaded_sources(cx).len()); loaded_source_list.list.reset(len); @@ -55,8 +54,6 @@ impl LoadedSourceList { fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { let Some(source) = maybe!({ self.session - .read(cx) - .client_state(self.client_id)? .update(cx, |state, cx| state.loaded_sources(cx).get(ix).cloned()) }) else { return Empty.into_any(); @@ -92,11 +89,9 @@ impl Focusable for LoadedSourceList { impl Render for LoadedSourceList { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if let Some(state) = self.session.read(cx).client_state(self.client_id) { - state.update(cx, |state, cx| { - state.loaded_sources(cx); - }); - } + self.session.update(cx, |state, cx| { + state.loaded_sources(cx); + }); div() .track_focus(&self.focus_handle) diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 00484d2e89cb14..00568a9afdde74 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,19 +1,19 @@ use dap::{client::DebugAdapterClientId, ModuleEvent}; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; -use project::debugger::dap_session::DebugSession; +use project::debugger::client::Client; use ui::prelude::*; pub struct ModuleList { list: ListState, focus_handle: FocusHandle, _subscription: Subscription, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, } impl ModuleList { pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, cx: &mut Context, ) -> Self { @@ -32,9 +32,7 @@ impl ModuleList { }, ); - let client_state = session.read(cx).client_state(client_id).unwrap(); - - let _subscription = cx.observe(&client_state, |module_list, state, cx| { + let _subscription = cx.observe(&session, |module_list, state, cx| { let modules_len = state.update(cx, |state, cx| state.modules(cx).len()); module_list.list.reset(modules_len); @@ -51,16 +49,13 @@ impl ModuleList { } pub fn on_module_event(&mut self, event: &ModuleEvent, cx: &mut Context) { - if let Some(state) = self.session.read(cx).client_state(self.client_id) { - state.update(cx, |state, cx| state.handle_module_event(event, cx)); - } + self.session + .update(cx, |state, cx| state.handle_module_event(event, cx)); } fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { let Some(module) = maybe!({ self.session - .read(cx) - .client_state(self.client_id)? .update(cx, |state, cx| state.modules(cx).get(ix).cloned()) }) else { return Empty.into_any(); @@ -91,11 +86,9 @@ impl Focusable for ModuleList { impl Render for ModuleList { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if let Some(state) = self.session.read(cx).client_state(self.client_id) { - state.update(cx, |state, cx| { - state.modules(cx); - }); - } + self.session.update(cx, |state, cx| { + state.modules(cx); + }); div() .track_focus(&self.focus_handle) diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index d9af8caa4b32d5..b42477d95524db 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -6,7 +6,8 @@ use gpui::{ list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, WeakEntity, }; -use project::debugger::dap_session::{DebugSession, StackFrame, ThreadId}; + +use project::debugger::client::{Client, StackFrame, ThreadId}; use project::ProjectPath; use ui::{prelude::*, Tooltip}; use workspace::Workspace; @@ -24,7 +25,7 @@ pub struct StackFrameList { thread_id: ThreadId, focus_handle: FocusHandle, _subscription: Subscription, - session: Entity, + session: Entity, entries: Vec, workspace: WeakEntity, client_id: DebugAdapterClientId, @@ -41,7 +42,7 @@ pub enum StackFrameEntry { impl StackFrameList { pub fn new( workspace: WeakEntity, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, thread_id: ThreadId, _window: &Window, @@ -64,9 +65,7 @@ impl StackFrameList { }, ); - let client_state = session.read(cx).client_state(client_id).unwrap(); - - let _subscription = cx.observe(&client_state, |stack_frame_list, state, cx| { + let _subscription = cx.observe(&session, |stack_frame_list, state, cx| { let _frame_len = state.update(cx, |state, cx| { state.stack_frames(stack_frame_list.thread_id, cx).len() }); @@ -99,10 +98,7 @@ impl StackFrameList { pub fn stack_frames(&self, cx: &mut App) -> Vec { self.session - .read(cx) - .client_state(self.client_id) - .map(|state| state.update(cx, |client, cx| client.stack_frames(self.thread_id, cx))) - .unwrap_or_default() + .update(cx, |this, cx| this.stack_frames(self.thread_id, cx)) } #[cfg(any(test, feature = "test-support"))] @@ -267,11 +263,9 @@ impl StackFrameList { } pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context) { - if let Some(client_state) = self.session.read(cx).client_state(self.client_id) { - client_state.update(cx, |state, cx| { - state.restart_stack_frame(stack_frame_id, cx) - }); - } + self.session.update(cx, |state, cx| { + state.restart_stack_frame(stack_frame_id, cx) + }); } fn render_normal_entry( @@ -291,8 +285,8 @@ impl StackFrameList { let supports_frame_restart = self .session .read(cx) - .client_state(self.client_id) - .and_then(|client| client.read(cx).capabilities().supports_restart_frame) + .capabilities() + .supports_restart_frame .unwrap_or_default(); let origin = stack_frame diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 38677c11764008..93e0eafaaa2130 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -10,7 +10,7 @@ use gpui::{ FocusHandle, Focusable, Hsla, ListOffset, ListState, MouseDownEvent, Point, Subscription, Task, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::debugger::dap_session::DebugSession; +use project::debugger::client::Client; use rpc::proto::{ self, DebuggerScopeVariableIndex, DebuggerVariableContainer, VariableListScopes, VariableListVariables, @@ -330,7 +330,7 @@ pub struct VariableList { list: ListState, focus_handle: FocusHandle, open_entries: Vec, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, set_variable_editor: Entity, @@ -346,7 +346,7 @@ pub struct VariableList { impl VariableList { pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, stack_frame_list: Entity, window: &mut Window, @@ -842,20 +842,11 @@ impl VariableList { window: &mut Window, cx: &mut Context, ) { - let Some((support_set_variable, support_clipboard_context)) = self - .session - .read(cx) - .client_state(self.client_id) - .map(|state| state.read(cx).capabilities()) - .map(|caps| { - ( - caps.supports_set_variable.unwrap_or_default(), - caps.supports_clipboard_context.unwrap_or_default(), - ) - }) - else { - return; - }; + let caps = self.session.read(cx).capabilities(); + let (support_set_variable, support_clipboard_context) = ( + caps.supports_set_variable.unwrap_or_default(), + caps.supports_clipboard_context.unwrap_or_default(), + ); let this = cx.entity(); @@ -874,12 +865,7 @@ impl VariableList { window.handler_for(&this.clone(), move |this, _window, cx| { if support_clipboard_context { - let Some(client_state) = this.session.read(cx).client_state(this.client_id) - else { - return; - }; - - client_state.update(cx, |state, cx| { + this.session.update(cx, |state, cx| { state.evaluate( evaluate_name.clone().unwrap_or(variable_name.clone()), Some(dap::EvaluateArgumentsContext::Clipboard), @@ -984,11 +970,7 @@ impl VariableList { return cx.notify(); } - let Some(client_state) = self.session.read(cx).client_state(self.client_id) else { - return; - }; - - client_state.update(cx, |state, cx| { + self.session.update(cx, |state, cx| { state.set_variable_value( set_variable_state.parent_variables_reference, set_variable_state.name, diff --git a/crates/project/src/debugger/client.rs b/crates/project/src/debugger/client.rs index 76136df4ac8de7..0d065259d90760 100644 --- a/crates/project/src/debugger/client.rs +++ b/crates/project/src/debugger/client.rs @@ -452,10 +452,10 @@ impl Client { &self.modules } - pub(crate) fn set_ignore_breakpoints(&mut self, ignore: bool) { + pub fn set_ignore_breakpoints(&mut self, ignore: bool) { self.ignore_breakpoints = ignore; } - pub(crate) fn breakpoints_enabled(&self) -> bool { + pub fn breakpoints_enabled(&self) -> bool { self.ignore_breakpoints } pub fn handle_module_event(&mut self, event: &dap::ModuleEvent, cx: &mut Context) { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 0db48d2ed592ec..f10bc9e7324466 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -39,7 +39,7 @@ use language::{BinaryStatus, BufferSnapshot, LanguageRegistry, LanguageToolchain use lsp::LanguageServerName; use node_runtime::NodeRuntime; use rpc::{ - proto::{self, SetDebuggerPanelItem, UpdateDebugAdapter, UpdateThreadStatus}, + proto::{self, UpdateDebugAdapter, UpdateThreadStatus}, AnyProtoClient, TypedEnvelope, }; use serde_json::Value; @@ -68,7 +68,6 @@ pub enum DapStoreEvent { Notification(String), ActiveDebugLineChanged, RemoteHasInitialized, - SetDebugPanelItem(SetDebuggerPanelItem), UpdateDebugAdapter(UpdateDebugAdapter), UpdateThreadStatus(UpdateThreadStatus), } @@ -240,7 +239,6 @@ impl DapStore { client.add_entity_message_handler(Self::handle_shutdown_debug_client); client.add_entity_message_handler(Self::handle_set_active_debug_line); client.add_entity_message_handler(Self::handle_set_debug_client_capabilities); - client.add_entity_message_handler(Self::handle_set_debug_panel_item); client.add_entity_message_handler(Self::handle_update_debug_adapter); client.add_entity_message_handler(Self::handle_update_thread_status); client.add_entity_message_handler(Self::handle_ignore_breakpoint_state); @@ -384,6 +382,9 @@ impl DapStore { client } + pub fn clients(&self) -> impl Iterator> { + self.clients.values() + } pub fn capabilities_by_id( &self, @@ -1054,16 +1055,6 @@ impl DapStore { // Ok(T::response_to_proto(&client_id, response)) // } - async fn handle_set_debug_panel_item( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::SetDebugPanelItem(envelope.payload)); - }) - } - async fn handle_update_debug_adapter( this: Entity, envelope: TypedEnvelope, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e94aa6a084605d..41706b8ada3794 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -87,9 +87,7 @@ pub use prettier_store::PrettierStore; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; use remote::{SshConnectionOptions, SshRemoteClient}; use rpc::{ - proto::{ - FromProto, LanguageServerPromptResponse, SetDebuggerPanelItem, ToProto, SSH_PROJECT_ID, - }, + proto::{FromProto, LanguageServerPromptResponse, ToProto, SSH_PROJECT_ID}, AnyProtoClient, ErrorCode, }; use search::{SearchInputKind, SearchQuery, SearchResult}; @@ -274,7 +272,6 @@ pub enum Event { LanguageServerPrompt(LanguageServerPromptRequest), DebugClientStarted(DebugAdapterClientId), DebugClientShutdown(DebugAdapterClientId), - SetDebugClient(SetDebuggerPanelItem), ActiveDebugLineChanged, DebugClientEvent { client_id: DebugAdapterClientId, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index a1049b173c2b61..cb24e5fb3dfdf7 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -327,43 +327,42 @@ message Envelope { SynchronizeBreakpoints synchronize_breakpoints = 304; SetActiveDebugLine set_active_debug_line = 305; RemoveActiveDebugLine remove_active_debug_line = 306; - SetDebuggerPanelItem set_debugger_panel_item = 307; - UpdateDebugAdapter update_debug_adapter = 308; - ShutdownDebugClient shutdown_debug_client = 309; - SetDebugClientCapabilities set_debug_client_capabilities = 310; - DapNextRequest dap_next_request = 311; - DapStepInRequest dap_step_in_request = 312; - DapStepOutRequest dap_step_out_request = 313; - DapStepBackRequest dap_step_back_request = 314; - DapContinueRequest dap_continue_request = 315; - DapContinueResponse dap_continue_response = 316; - DapPauseRequest dap_pause_request = 317; - DapDisconnectRequest dap_disconnect_request = 318; - DapTerminateThreadsRequest dap_terminate_threads_request = 319; - DapTerminateRequest dap_terminate_request = 320; - DapRestartRequest dap_restart_request = 321; - UpdateThreadStatus update_thread_status = 322; - VariablesRequest variables_request = 323; - DapVariables dap_variables = 324; - DapRestartStackFrameRequest dap_restart_stack_frame_request = 325; - IgnoreBreakpointState ignore_breakpoint_state = 326; - ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 327; - DapModulesRequest dap_modules_request = 328; - DapModulesResponse dap_modules_response = 329; - DapLoadedSourcesRequest dap_loaded_sources_request = 330; - DapLoadedSourcesResponse dap_loaded_sources_response = 331; - DapStackTraceRequest dap_stack_trace_request = 332; - DapStackTraceResponse dap_stack_trace_response = 333; - DapScopesRequest dap_scopes_request = 334; - DapScopesResponse dap_scopes_response = 335; - DapSetVariableValueRequest dap_set_variable_value_request = 336; - DapSetVariableValueResponse dap_set_variable_value_response = 337; - DapEvaluateRequest dap_evaluate_request = 338; - DapEvaluateResponse dap_evaluate_response = 339; - DapCompletionRequest dap_completion_request = 340; - DapCompletionResponse dap_completion_response = 341; - DapThreadsRequest dap_threads_request = 342; - DapThreadsResponse dap_threads_response = 343;// current max + UpdateDebugAdapter update_debug_adapter = 307; + ShutdownDebugClient shutdown_debug_client = 308; + SetDebugClientCapabilities set_debug_client_capabilities = 309; + DapNextRequest dap_next_request = 310; + DapStepInRequest dap_step_in_request = 311; + DapStepOutRequest dap_step_out_request = 312; + DapStepBackRequest dap_step_back_request = 313; + DapContinueRequest dap_continue_request = 314; + DapContinueResponse dap_continue_response = 315; + DapPauseRequest dap_pause_request = 316; + DapDisconnectRequest dap_disconnect_request = 317; + DapTerminateThreadsRequest dap_terminate_threads_request = 318; + DapTerminateRequest dap_terminate_request = 319; + DapRestartRequest dap_restart_request = 320; + UpdateThreadStatus update_thread_status = 321; + VariablesRequest variables_request = 322; + DapVariables dap_variables = 323; + DapRestartStackFrameRequest dap_restart_stack_frame_request = 324; + IgnoreBreakpointState ignore_breakpoint_state = 325; + ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 326; + DapModulesRequest dap_modules_request = 327; + DapModulesResponse dap_modules_response = 328; + DapLoadedSourcesRequest dap_loaded_sources_request = 329; + DapLoadedSourcesResponse dap_loaded_sources_response = 330; + DapStackTraceRequest dap_stack_trace_request = 331; + DapStackTraceResponse dap_stack_trace_response = 332; + DapScopesRequest dap_scopes_request = 333; + DapScopesResponse dap_scopes_response = 334; + DapSetVariableValueRequest dap_set_variable_value_request = 335; + DapSetVariableValueResponse dap_set_variable_value_response = 336; + DapEvaluateRequest dap_evaluate_request = 337; + DapEvaluateResponse dap_evaluate_response = 338; + DapCompletionRequest dap_completion_request = 339; + DapCompletionResponse dap_completion_response = 340; + DapThreadsRequest dap_threads_request = 341; + DapThreadsResponse dap_threads_response = 342;// current max } reserved 87 to 88; @@ -2557,7 +2556,6 @@ enum BreakpointKind { } - message ActiveDebugClientsResponse { repeated DebugClient clients = 1; } @@ -2566,7 +2564,6 @@ message DebugClient { uint64 client_id = 1; SetDebugClientCapabilities capabilities = 2; bool ignore_breakpoints = 3; - repeated SetDebuggerPanelItem debug_panel_items = 4; } message ShutdownDebugClient { @@ -3007,20 +3004,6 @@ message DebuggerModuleList { uint64 client_id = 2; } -message SetDebuggerPanelItem { - uint64 project_id = 1; - uint64 client_id = 2; - uint64 thread_id = 3; - string session_name = 4; - DebuggerConsole console = 5; - optional DebuggerModuleList module_list = 6; // We only send this when it's supported and once per client - DebuggerThreadItem active_thread_item = 7; - DebuggerThreadState thread_state = 8; - DebuggerVariableList variable_list = 9; - DebuggerStackFrameList stack_frame_list = 10; - DebuggerLoadedSourceList loaded_source_list = 11; -} - message UpdateDebugAdapter { uint64 project_id = 1; uint64 client_id = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 183b06a8d28278..7dcc61a777316c 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -414,7 +414,6 @@ messages!( (SetChannelMemberRole, Foreground), (SetChannelVisibility, Foreground), (SetDebugClientCapabilities, Background), - (SetDebuggerPanelItem, Background), (SetRoomParticipantRole, Foreground), (ShareProject, Foreground), (ShareProjectResponse, Foreground), @@ -743,7 +742,6 @@ entity_messages!( SynchronizeBreakpoints, SetActiveDebugLine, RemoveActiveDebugLine, - SetDebuggerPanelItem, ShutdownDebugClient, SetDebugClientCapabilities, DapNextRequest, From fbf4eb9472ab3d3c39ee4ded198edc6d4482b462 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:40:42 +0100 Subject: [PATCH 565/650] fixup! Building! And crashing --- crates/debugger_ui/src/stack_frame_list.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index b42477d95524db..71dd4d934b1819 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -28,7 +28,6 @@ pub struct StackFrameList { session: Entity, entries: Vec, workspace: WeakEntity, - client_id: DebugAdapterClientId, current_stack_frame_id: StackFrameId, _fetch_stack_frames_task: Option>>, } From cf13a62bb5f697101c5d8e2aa53bdeb1da531b4a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:45:16 +0100 Subject: [PATCH 566/650] Rename client module to session --- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/console.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/debugger_panel_item.rs | 13 +++---------- crates/debugger_ui/src/loaded_source_list.rs | 2 +- crates/debugger_ui/src/module_list.rs | 2 +- crates/debugger_ui/src/stack_frame_list.rs | 5 ++--- crates/debugger_ui/src/variable_list.rs | 2 +- crates/project/src/debugger.rs | 2 +- crates/project/src/debugger/dap_command.rs | 2 +- crates/project/src/debugger/dap_store.rs | 6 +++--- .../project/src/debugger/{client.rs => session.rs} | 0 12 files changed, 16 insertions(+), 24 deletions(-) rename crates/project/src/debugger/{client.rs => session.rs} (100%) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index cf43acb52b9a7b..3f5870a25bfc0f 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -12,7 +12,7 @@ use gpui::{ actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; -use project::{debugger::client::Client, search::SearchQuery, Project}; +use project::{debugger::session::Client, search::SearchQuery, Project}; use settings::Settings as _; use std::{ borrow::Cow, diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index ce0da3e5c3025c..1932c7893340bf 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -13,7 +13,7 @@ use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; use language::{Buffer, CodeLabel, LanguageServerId}; use menu::Confirm; use project::{ - debugger::client::{Client, CompletionsQuery}, + debugger::session::{Client, CompletionsQuery}, Completion, }; use settings::Settings; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 4989ee6de82c06..534b2c83c5c420 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -17,8 +17,8 @@ use gpui::{ }; use project::{ debugger::{ - client::ThreadId, dap_store::{DapStore, DapStoreEvent}, + session::ThreadId, }, terminals::TerminalKind, }; diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 693165ab29b514..289c1629e4f53e 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -13,8 +13,8 @@ use dap::{ use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::debugger::client::Client; -use project::debugger::client::ThreadId; +use project::debugger::session::Client; +use project::debugger::session::ThreadId; use rpc::proto::{self, DebuggerThreadStatus, PeerId}; use settings::Settings; use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, PopoverMenu, Tooltip}; @@ -90,14 +90,7 @@ impl DebugPanelItem { let focus_handle = cx.focus_handle(); let stack_frame_list = cx.new(|cx| { - StackFrameList::new( - workspace.clone(), - session.clone(), - client_id, - thread_id, - window, - cx, - ) + StackFrameList::new(workspace.clone(), session.clone(), thread_id, window, cx) }); let variable_list = cx.new(|cx| { diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs index 8b0f6fd30e3a69..a2e1a7d1808e19 100644 --- a/crates/debugger_ui/src/loaded_source_list.rs +++ b/crates/debugger_ui/src/loaded_source_list.rs @@ -1,6 +1,6 @@ use dap::client::DebugAdapterClientId; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; -use project::debugger::client::Client; +use project::debugger::session::Client; use ui::prelude::*; use util::maybe; diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index 00568a9afdde74..dc314b85681aa1 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,6 +1,6 @@ use dap::{client::DebugAdapterClientId, ModuleEvent}; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; -use project::debugger::client::Client; +use project::debugger::session::Client; use ui::prelude::*; pub struct ModuleList { diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 71dd4d934b1819..62fcb34cd6d3b8 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -7,7 +7,7 @@ use gpui::{ WeakEntity, }; -use project::debugger::client::{Client, StackFrame, ThreadId}; +use project::debugger::session::{Client, StackFrame, ThreadId}; use project::ProjectPath; use ui::{prelude::*, Tooltip}; use workspace::Workspace; @@ -42,7 +42,6 @@ impl StackFrameList { pub fn new( workspace: WeakEntity, session: Entity, - client_id: DebugAdapterClientId, thread_id: ThreadId, _window: &Window, cx: &mut Context, @@ -77,7 +76,7 @@ impl StackFrameList { session, workspace, thread_id, - client_id, + focus_handle, _subscription, entries: Default::default(), diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 93e0eafaaa2130..2ff5960bdf147c 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -10,7 +10,7 @@ use gpui::{ FocusHandle, Focusable, Hsla, ListOffset, ListState, MouseDownEvent, Point, Subscription, Task, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::debugger::client::Client; +use project::debugger::session::Client; use rpc::proto::{ self, DebuggerScopeVariableIndex, DebuggerVariableContainer, VariableListScopes, VariableListVariables, diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index c3ac958c903d4b..695d5196ec7d45 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -12,6 +12,6 @@ //! - 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. pub mod breakpoint_store; -pub mod client; pub mod dap_command; pub mod dap_store; +pub mod session; diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index df32856cf4473e..ce34faf8751671 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -1318,7 +1318,7 @@ impl DapCommand for ScopesCommand { } } -impl DapCommand for super::client::CompletionsQuery { +impl DapCommand for super::session::CompletionsQuery { type Response = dap::CompletionsResponse; type DapRequest = dap::requests::Completions; type ProtoRequest = proto::DapCompletionRequest; diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index f10bc9e7324466..3524839e5094fa 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1,6 +1,5 @@ use super::{ breakpoint_store::BreakpointStore, - client::{self, Client}, // Will need to uncomment this once we implement rpc message handler again // dap_command::{ // ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand, @@ -8,6 +7,7 @@ use super::{ // TerminateCommand, TerminateThreadsCommand, VariablesCommand, // }, dap_command::DapCommand, + session::{self, Client}, }; use crate::{ debugger, project_settings::ProjectSettings, DebugAdapterClientState, ProjectEnvironment, @@ -360,7 +360,7 @@ impl DapStore { self.clients.insert( client_id, cx.new(|_| { - debugger::client::Client::remote( + debugger::session::Client::remote( client_id, remote.upstream_client.clone(), remote.upstream_project_id, @@ -376,7 +376,7 @@ impl DapStore { pub fn client_by_id( &self, client_id: impl Borrow, - ) -> Option> { + ) -> Option> { let client_id = client_id.borrow(); let client = self.clients.get(client_id).cloned(); diff --git a/crates/project/src/debugger/client.rs b/crates/project/src/debugger/session.rs similarity index 100% rename from crates/project/src/debugger/client.rs rename to crates/project/src/debugger/session.rs From c9a9ab22f09f9087e26fbcc08d7f94b5c7c9d994 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:46:17 +0100 Subject: [PATCH 567/650] And the rename is complete --- crates/debugger_tools/src/dap_log.rs | 4 ++-- crates/debugger_ui/src/console.rs | 6 +++--- crates/debugger_ui/src/debugger_panel_item.rs | 8 ++++---- crates/debugger_ui/src/loaded_source_list.rs | 6 +++--- crates/debugger_ui/src/module_list.rs | 6 +++--- crates/debugger_ui/src/stack_frame_list.rs | 6 +++--- crates/debugger_ui/src/variable_list.rs | 6 +++--- crates/project/src/debugger/dap_store.rs | 12 ++++++------ crates/project/src/debugger/session.rs | 8 ++++---- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 3f5870a25bfc0f..8bea2a632e5a6c 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -12,7 +12,7 @@ use gpui::{ actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; -use project::{debugger::session::Client, search::SearchQuery, Project}; +use project::{debugger::session::Session, search::SearchQuery, Project}; use settings::Settings as _; use std::{ borrow::Cow, @@ -290,7 +290,7 @@ impl LogStore { fn add_debug_client( &mut self, client_id: DebugAdapterClientId, - client: Entity, + client: Entity, cx: &App, ) -> Option<&mut DebugAdapterState> { let client_state = self diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/console.rs index 1932c7893340bf..6eb23a5000b240 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/console.rs @@ -13,7 +13,7 @@ use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity}; use language::{Buffer, CodeLabel, LanguageServerId}; use menu::Confirm; use project::{ - debugger::session::{Client, CompletionsQuery}, + debugger::session::{CompletionsQuery, Session}, Completion, }; use settings::Settings; @@ -33,7 +33,7 @@ pub struct Console { groups: Vec, console: Entity, query_bar: Entity, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, variable_list: Entity, @@ -42,7 +42,7 @@ pub struct Console { impl Console { pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, stack_frame_list: Entity, variable_list: Entity, diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 289c1629e4f53e..aa82c66938fd60 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -13,7 +13,7 @@ use dap::{ use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::debugger::session::Client; +use project::debugger::session::Session; use project::debugger::session::ThreadId; use rpc::proto::{self, DebuggerThreadStatus, PeerId}; use settings::Settings; @@ -62,7 +62,7 @@ pub struct DebugPanelItem { console: Entity, focus_handle: FocusHandle, remote_id: Option, - session: Entity, + session: Entity, show_console_indicator: bool, module_list: Entity, active_thread_item: ThreadItem, @@ -78,7 +78,7 @@ pub struct DebugPanelItem { impl DebugPanelItem { #[allow(clippy::too_many_arguments)] pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, thread_id: ThreadId, thread_state: Entity, @@ -388,7 +388,7 @@ impl DebugPanelItem { // } // } - pub fn session(&self) -> &Entity { + pub fn session(&self) -> &Entity { &self.session } diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/loaded_source_list.rs index a2e1a7d1808e19..0a64f6bed77229 100644 --- a/crates/debugger_ui/src/loaded_source_list.rs +++ b/crates/debugger_ui/src/loaded_source_list.rs @@ -1,6 +1,6 @@ use dap::client::DebugAdapterClientId; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; -use project::debugger::session::Client; +use project::debugger::session::Session; use ui::prelude::*; use util::maybe; @@ -8,13 +8,13 @@ pub struct LoadedSourceList { list: ListState, focus_handle: FocusHandle, _subscription: Subscription, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, } impl LoadedSourceList { pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, cx: &mut Context, ) -> Self { diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/module_list.rs index dc314b85681aa1..8ba20bac570572 100644 --- a/crates/debugger_ui/src/module_list.rs +++ b/crates/debugger_ui/src/module_list.rs @@ -1,19 +1,19 @@ use dap::{client::DebugAdapterClientId, ModuleEvent}; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; -use project::debugger::session::Client; +use project::debugger::session::Session; use ui::prelude::*; pub struct ModuleList { list: ListState, focus_handle: FocusHandle, _subscription: Subscription, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, } impl ModuleList { pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, cx: &mut Context, ) -> Self { diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/stack_frame_list.rs index 62fcb34cd6d3b8..1c727298901332 100644 --- a/crates/debugger_ui/src/stack_frame_list.rs +++ b/crates/debugger_ui/src/stack_frame_list.rs @@ -7,7 +7,7 @@ use gpui::{ WeakEntity, }; -use project::debugger::session::{Client, StackFrame, ThreadId}; +use project::debugger::session::{Session, StackFrame, ThreadId}; use project::ProjectPath; use ui::{prelude::*, Tooltip}; use workspace::Workspace; @@ -25,7 +25,7 @@ pub struct StackFrameList { thread_id: ThreadId, focus_handle: FocusHandle, _subscription: Subscription, - session: Entity, + session: Entity, entries: Vec, workspace: WeakEntity, current_stack_frame_id: StackFrameId, @@ -41,7 +41,7 @@ pub enum StackFrameEntry { impl StackFrameList { pub fn new( workspace: WeakEntity, - session: Entity, + session: Entity, thread_id: ThreadId, _window: &Window, cx: &mut Context, diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/variable_list.rs index 2ff5960bdf147c..4874029e11b2d1 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/variable_list.rs @@ -10,7 +10,7 @@ use gpui::{ FocusHandle, Focusable, Hsla, ListOffset, ListState, MouseDownEvent, Point, Subscription, Task, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::debugger::session::Client; +use project::debugger::session::Session; use rpc::proto::{ self, DebuggerScopeVariableIndex, DebuggerVariableContainer, VariableListScopes, VariableListVariables, @@ -330,7 +330,7 @@ pub struct VariableList { list: ListState, focus_handle: FocusHandle, open_entries: Vec, - session: Entity, + session: Entity, client_id: DebugAdapterClientId, _subscriptions: Vec, set_variable_editor: Entity, @@ -346,7 +346,7 @@ pub struct VariableList { impl VariableList { pub fn new( - session: Entity, + session: Entity, client_id: DebugAdapterClientId, stack_frame_list: Entity, window: &mut Window, diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 3524839e5094fa..e4b02580490b4d 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -7,7 +7,7 @@ use super::{ // TerminateCommand, TerminateThreadsCommand, VariablesCommand, // }, dap_command::DapCommand, - session::{self, Client}, + session::{self, Session}, }; use crate::{ debugger, project_settings::ProjectSettings, DebugAdapterClientState, ProjectEnvironment, @@ -94,7 +94,7 @@ impl LocalDapStore { } pub fn respond_to_start_debugging( &mut self, - client: &Entity, + client: &Entity, seq: u64, args: Option, cx: &mut Context, @@ -208,7 +208,7 @@ pub struct DapStore { downstream_client: Option<(AnyProtoClient, u64)>, breakpoint_store: Entity, active_debug_line: Option<(DebugAdapterClientId, ProjectPath, u32)>, - clients: BTreeMap>, + clients: BTreeMap>, } impl EventEmitter for DapStore {} @@ -360,7 +360,7 @@ impl DapStore { self.clients.insert( client_id, cx.new(|_| { - debugger::session::Client::remote( + debugger::session::Session::remote( client_id, remote.upstream_client.clone(), remote.upstream_project_id, @@ -376,13 +376,13 @@ impl DapStore { pub fn client_by_id( &self, client_id: impl Borrow, - ) -> Option> { + ) -> Option> { let client_id = client_id.borrow(); let client = self.clients.get(client_id).cloned(); client } - pub fn clients(&self) -> impl Iterator> { + pub fn clients(&self) -> impl Iterator> { self.clients.values() } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 0d065259d90760..ad5f8c593ca69d 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -162,7 +162,7 @@ impl Mode { fn request_local( connection: &Arc, request: R, - cx: &mut Context, + cx: &mut Context, ) -> Task> where ::Response: 'static, @@ -187,7 +187,7 @@ impl Mode { &self, client_id: DebugAdapterClientId, request: R, - cx: &mut Context, + cx: &mut Context, ) -> Task> where ::Response: 'static, @@ -205,7 +205,7 @@ impl Mode { } /// Represents a current state of a single debug adapter and provides ways to mutate it. -pub struct Client { +pub struct Session { mode: Mode, config: DebugAdapterConfig, pub(super) capabilities: Capabilities, @@ -292,7 +292,7 @@ impl CompletionsQuery { } } -impl Client { +impl Session { pub(crate) fn local(adapter: Arc, config: DebugAdapterConfig) -> Self { let client_id = adapter.id(); From f81463cc93a16e3043bcf1b9d99535bbdb2586da Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:06:02 +0100 Subject: [PATCH 568/650] WIP --- crates/collab/src/tests/debug_panel_tests.rs | 16 ++-- crates/debugger_ui/src/debugger_panel.rs | 85 +++---------------- crates/debugger_ui/src/debugger_panel_item.rs | 57 +++---------- crates/debugger_ui/src/lib.rs | 4 +- crates/debugger_ui/src/tests.rs | 4 +- 5 files changed, 33 insertions(+), 133 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 975b124d0a8e03..6c7437f9a8dc63 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -5,7 +5,7 @@ use dap::{ SourceBreakpoint, StackFrame, }; use debugger_ui::debugger_panel::DebugPanel; -use debugger_ui::debugger_panel_item::DebugPanelItem; +use debugger_ui::debugger_panel_item::DebugSession; use editor::Editor; use gpui::{Entity, TestAppContext, VisualTestContext}; use project::{Project, ProjectPath, WorktreeId}; @@ -52,7 +52,7 @@ async fn add_debugger_panel(workspace: &Entity, cx: &mut VisualTestCo pub fn active_debug_panel_item( workspace: Entity, cx: &mut VisualTestContext, -) -> Entity { +) -> Entity { workspace.update_in(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); debug_panel @@ -1251,9 +1251,7 @@ async fn test_module_list( debug_panel_item.update(cx, |item, cx| { assert_eq!( true, - item.capabilities(cx) - .and_then(|caps| caps.supports_modules_request) - .unwrap(), + item.capabilities(cx).supports_modules_request.unwrap(), "Local supports modules request should be true" ); @@ -1281,9 +1279,7 @@ async fn test_module_list( debug_panel_item.update(cx, |item, cx| { assert_eq!( true, - item.capabilities(cx) - .and_then(|caps| caps.supports_modules_request) - .unwrap(), + item.capabilities(cx).supports_modules_request.unwrap(), "Remote capabilities supports modules request should be true" ); let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); @@ -1314,9 +1310,7 @@ async fn test_module_list( debug_panel_item.update(cx, |item, cx| { assert_eq!( true, - item.capabilities(cx) - .and_then(|caps| caps.supports_modules_request) - .unwrap(), + item.capabilities(cx).supports_modules_request.unwrap(), "Remote (mid session join) capabilities supports modules request should be true" ); let remote_module_list = item.module_list().update(cx, |list, cx| list.modules(cx)); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 534b2c83c5c420..aaab0703a0271d 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,4 +1,4 @@ -use crate::{attach_modal::AttachModal, debugger_panel_item::DebugPanelItem}; +use crate::{attach_modal::AttachModal, debugger_panel_item::DebugSession}; use anyhow::Result; use collections::{BTreeMap, HashMap}; use command_palette_hooks::CommandPaletteFilter; @@ -54,72 +54,12 @@ pub enum DebugPanelEvent { } actions!(debug_panel, [ToggleFocus]); - -#[derive(Debug, Default, Clone)] -pub struct ThreadState { - pub status: ThreadStatus, - // we update this value only once we stopped, - // we will use this to indicated if we should show a warning when debugger thread was exited - pub stopped: bool, -} - -impl ThreadState { - pub fn from_proto(thread_state: proto::DebuggerThreadState) -> Self { - let status = ThreadStatus::from_proto(thread_state.thread_status()); - - Self { - status, - stopped: thread_state.stopped, - } - } - - pub fn to_proto(&self) -> proto::DebuggerThreadState { - let status = self.status.to_proto(); - - proto::DebuggerThreadState { - thread_status: status, - stopped: self.stopped, - } - } -} - -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ThreadStatus { - #[default] - Running, - Stopped, - Exited, - Ended, -} - -impl ThreadStatus { - pub fn from_proto(status: proto::DebuggerThreadStatus) -> Self { - match status { - proto::DebuggerThreadStatus::Running => Self::Running, - proto::DebuggerThreadStatus::Stopped => Self::Stopped, - proto::DebuggerThreadStatus::Exited => Self::Exited, - proto::DebuggerThreadStatus::Ended => Self::Ended, - } - } - - pub fn to_proto(&self) -> i32 { - match self { - Self::Running => proto::DebuggerThreadStatus::Running.into(), - Self::Stopped => proto::DebuggerThreadStatus::Stopped.into(), - Self::Exited => proto::DebuggerThreadStatus::Exited.into(), - Self::Ended => proto::DebuggerThreadStatus::Ended.into(), - } - } -} - pub struct DebugPanel { size: Pixels, pane: Entity, focus_handle: FocusHandle, - dap_store: Entity, workspace: WeakEntity, _subscriptions: Vec, - message_queue: HashMap>, } impl DebugPanel { @@ -149,23 +89,18 @@ impl DebugPanel { }); let project = workspace.project().clone(); - let dap_store = project.read(cx).dap_store(); let _subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe_in(&pane, window, Self::handle_pane_event), ]; - let dap_store = project.read(cx).dap_store(); - - let mut debug_panel = Self { + let debug_panel = Self { pane, size: px(300.), _subscriptions, focus_handle: cx.focus_handle(), - message_queue: Default::default(), workspace: workspace.weak_handle(), - dap_store: dap_store.clone(), }; debug_panel @@ -241,22 +176,22 @@ impl DebugPanel { self.dap_store.clone() } - pub fn active_debug_panel_item(&self, cx: &Context) -> Option> { + pub fn active_debug_panel_item(&self, cx: &Context) -> Option> { self.pane .read(cx) .active_item() - .and_then(|panel| panel.downcast::()) + .and_then(|panel| panel.downcast::()) } pub fn debug_panel_items_by_client( &self, client_id: &DebugAdapterClientId, cx: &Context, - ) -> Vec> { + ) -> Vec> { self.pane .read(cx) .items() - .filter_map(|item| item.downcast::()) + .filter_map(|item| item.downcast::()) .filter(|item| &item.read(cx).client_id() == client_id) .map(|item| item.clone()) .collect() @@ -267,11 +202,11 @@ impl DebugPanel { client_id: DebugAdapterClientId, thread_id: ThreadId, cx: &mut Context, - ) -> Option> { + ) -> Option> { self.pane .read(cx) .items() - .filter_map(|item| item.downcast::()) + .filter_map(|item| item.downcast::()) .find(|item| { let item = item.read(cx); @@ -288,7 +223,7 @@ impl DebugPanel { ) { match event { pane::Event::RemovedItem { item } => { - let thread_panel = item.downcast::().unwrap(); + let thread_panel = item.downcast::().unwrap(); let thread_id = thread_panel.read(cx).thread_id(); @@ -316,7 +251,7 @@ impl DebugPanel { } if let Some(active_item) = self.pane.read(cx).active_item() { - if let Some(debug_item) = active_item.downcast::() { + if let Some(debug_item) = active_item.downcast::() { debug_item.update(cx, |panel, cx| { panel.go_to_current_stack_frame(window, cx); }); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index aa82c66938fd60..cf4171337707d1 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -1,5 +1,5 @@ use crate::console::Console; -use crate::debugger_panel::{DebugPanel, DebugPanelEvent, ThreadState, ThreadStatus}; +use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; use crate::loaded_source_list::LoadedSourceList; use crate::module_list::ModuleList; use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; @@ -14,7 +14,8 @@ use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; use project::debugger::session::Session; -use project::debugger::session::ThreadId; +use project::debugger::session::{ThreadId, ThreadStatus}; + use rpc::proto::{self, DebuggerThreadStatus, PeerId}; use settings::Settings; use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, PopoverMenu, Tooltip}; @@ -57,31 +58,29 @@ impl ThreadItem { } } -pub struct DebugPanelItem { +pub struct DebugSession { + session: Entity, thread_id: ThreadId, console: Entity, focus_handle: FocusHandle, remote_id: Option, - session: Entity, show_console_indicator: bool, module_list: Entity, active_thread_item: ThreadItem, _workspace: WeakEntity, client_id: DebugAdapterClientId, - thread_state: Entity, variable_list: Entity, _subscriptions: Vec, stack_frame_list: Entity, loaded_source_list: Entity, } -impl DebugPanelItem { +impl DebugSession { #[allow(clippy::too_many_arguments)] pub fn new( session: Entity, client_id: DebugAdapterClientId, thread_id: ThreadId, - thread_state: Entity, debug_panel: &Entity, workspace: WeakEntity, window: &mut Window, @@ -172,7 +171,7 @@ impl DebugPanelItem { thread_id, _workspace: workspace, module_list, - thread_state, + focus_handle, variable_list, _subscriptions, @@ -185,21 +184,6 @@ impl DebugPanelItem { } } - pub fn update_thread_state_status(&mut self, status: ThreadStatus, cx: &mut Context) { - self.thread_state.update(cx, |thread_state, cx| { - thread_state.status = status; - - cx.notify(); - }); - - if status == ThreadStatus::Exited - || status == ThreadStatus::Ended - || status == ThreadStatus::Stopped - { - self.clear_highlights(cx); - } - } - fn should_skip_event(&self, client_id: &DebugAdapterClientId, thread_id: ThreadId) -> bool { thread_id != self.thread_id || *client_id != self.client_id } @@ -213,8 +197,6 @@ impl DebugPanelItem { if self.should_skip_event(client_id, ThreadId(event.thread_id)) { return; } - - self.update_thread_state_status(ThreadStatus::Running, cx); } fn handle_stopped_event( @@ -247,8 +229,6 @@ impl DebugPanelItem { if self.should_skip_event(client_id, ThreadId(event.thread_id)) { return; } - - self.update_thread_state_status(ThreadStatus::Running, cx); } fn handle_output_event( @@ -318,8 +298,6 @@ impl DebugPanelItem { return; } - self.update_thread_state_status(ThreadStatus::Stopped, cx); - // TODO(debugger): make this work again // self.dap_store.update(cx, |store, cx| { // store.remove_active_debug_line_for_client(client_id, cx); @@ -337,8 +315,6 @@ impl DebugPanelItem { return; } - self.update_thread_state_status(ThreadStatus::Exited, cx); - cx.emit(DebugPanelItemEvent::Close); } @@ -426,11 +402,6 @@ impl DebugPanelItem { &self.variable_list } - #[cfg(any(test, feature = "test-support"))] - pub fn thread_state(&self) -> &Entity { - &self.thread_state - } - #[cfg(any(test, feature = "test-support"))] pub fn are_breakpoints_ignored(&self, cx: &App) -> bool { self.session.read(cx).ignore_breakpoints() @@ -580,15 +551,15 @@ impl DebugPanelItem { } } -impl EventEmitter for DebugPanelItem {} +impl EventEmitter for DebugSession {} -impl Focusable for DebugPanelItem { +impl Focusable for DebugSession { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } -impl Item for DebugPanelItem { +impl Item for DebugSession { type Event = DebugPanelItemEvent; fn tab_content( @@ -611,7 +582,7 @@ impl Item for DebugPanelItem { "{} Thread {} - {:?}", todo!(), self.thread_id.0, - self.thread_state.read(cx).status, + todo!("thread state"), ))) } @@ -623,7 +594,7 @@ impl Item for DebugPanelItem { } } -impl FollowableItem for DebugPanelItem { +impl FollowableItem for DebugSession { fn remote_id(&self) -> Option { self.remote_id } @@ -694,9 +665,9 @@ impl FollowableItem for DebugPanelItem { } } -impl Render for DebugPanelItem { +impl Render for DebugSession { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let thread_status = self.thread_state.read(cx).status; + let thread_status = ThreadStatus::Running; let active_thread_item = &self.active_thread_item; let capabilities = self.capabilities(cx); diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 25bc54aa224ac7..c42dfc7da207df 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,6 +1,6 @@ use dap::debugger_settings::DebuggerSettings; use debugger_panel::{DebugPanel, ToggleFocus}; -use debugger_panel_item::DebugPanelItem; +use debugger_panel_item::DebugSession; use gpui::App; use settings::Settings; use workspace::{ @@ -22,7 +22,7 @@ mod tests; pub fn init(cx: &mut App) { DebuggerSettings::register(cx); - workspace::FollowableViewRegistry::register::(cx); + workspace::FollowableViewRegistry::register::(cx); cx.observe_new(|workspace: &mut Workspace, window, _cx| { let Some(_) = window else { diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 616cd971983b23..84a33d1bba8ffc 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -4,7 +4,7 @@ use settings::SettingsStore; use terminal_view::terminal_panel::TerminalPanel; use workspace::Workspace; -use crate::{debugger_panel::DebugPanel, debugger_panel_item::DebugPanelItem}; +use crate::{debugger_panel::DebugPanel, debugger_panel_item::DebugSession}; mod attach_modal; mod console; @@ -62,7 +62,7 @@ pub async fn init_test_workspace( pub fn active_debug_panel_item( workspace: WindowHandle, cx: &mut TestAppContext, -) -> Entity { +) -> Entity { workspace .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); From 1e0a0fa9f92225547eaffdefa57d9f05d55a475e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:07:05 +0100 Subject: [PATCH 569/650] Renames --- .../src/{debugger_panel_item.rs => debug_session.rs} | 0 crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/lib.rs | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename crates/debugger_ui/src/{debugger_panel_item.rs => debug_session.rs} (100%) diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debug_session.rs similarity index 100% rename from crates/debugger_ui/src/debugger_panel_item.rs rename to crates/debugger_ui/src/debug_session.rs diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index aaab0703a0271d..e142224a377d7f 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,4 +1,4 @@ -use crate::{attach_modal::AttachModal, debugger_panel_item::DebugSession}; +use crate::{attach_modal::AttachModal, debug_session::DebugSession}; use anyhow::Result; use collections::{BTreeMap, HashMap}; use command_palette_hooks::CommandPaletteFilter; diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index c42dfc7da207df..008bae4bce026a 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,6 +1,6 @@ use dap::debugger_settings::DebuggerSettings; +use debug_session::DebugSession; use debugger_panel::{DebugPanel, ToggleFocus}; -use debugger_panel_item::DebugSession; use gpui::App; use settings::Settings; use workspace::{ @@ -10,8 +10,8 @@ use workspace::{ pub mod attach_modal; pub mod console; +pub mod debug_session; pub mod debugger_panel; -pub mod debugger_panel_item; pub mod loaded_source_list; pub mod module_list; pub mod stack_frame_list; From 0dc90fd35e2aeaf4764049a177b4de5328940d35 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:51:46 +0100 Subject: [PATCH 570/650] UI shred --- crates/collab/src/tests/debug_panel_tests.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- crates/debugger_ui/src/lib.rs | 9 +- crates/debugger_ui/src/session.rs | 169 ++++ crates/debugger_ui/src/session/inert.rs | 22 + .../{debug_session.rs => session/running.rs} | 925 +++++++----------- .../src/{ => session/running}/console.rs | 2 +- .../running}/loaded_source_list.rs | 0 .../src/{ => session/running}/module_list.rs | 0 .../{ => session/running}/stack_frame_list.rs | 0 .../{ => session/running}/variable_list.rs | 2 +- crates/debugger_ui/src/session/starting.rs | 22 + crates/debugger_ui/src/tests.rs | 2 +- crates/debugger_ui/src/tests/module_list.rs | 2 +- 14 files changed, 562 insertions(+), 597 deletions(-) create mode 100644 crates/debugger_ui/src/session.rs create mode 100644 crates/debugger_ui/src/session/inert.rs rename crates/debugger_ui/src/{debug_session.rs => session/running.rs} (78%) rename crates/debugger_ui/src/{ => session/running}/console.rs (99%) rename crates/debugger_ui/src/{ => session/running}/loaded_source_list.rs (100%) rename crates/debugger_ui/src/{ => session/running}/module_list.rs (100%) rename crates/debugger_ui/src/{ => session/running}/stack_frame_list.rs (100%) rename crates/debugger_ui/src/{ => session/running}/variable_list.rs (99%) create mode 100644 crates/debugger_ui/src/session/starting.rs diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 6c7437f9a8dc63..86c01184ff2866 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -5,7 +5,7 @@ use dap::{ SourceBreakpoint, StackFrame, }; use debugger_ui::debugger_panel::DebugPanel; -use debugger_ui::debugger_panel_item::DebugSession; +use debugger_ui::session::DebugSession; use editor::Editor; use gpui::{Entity, TestAppContext, VisualTestContext}; use project::{Project, ProjectPath, WorktreeId}; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index e142224a377d7f..b5effbdd64ce65 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,4 +1,4 @@ -use crate::{attach_modal::AttachModal, debug_session::DebugSession}; +use crate::{attach_modal::AttachModal, session::DebugSession}; use anyhow::Result; use collections::{BTreeMap, HashMap}; use command_palette_hooks::CommandPaletteFilter; diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 008bae4bce026a..360b76de898879 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -1,7 +1,7 @@ use dap::debugger_settings::DebuggerSettings; -use debug_session::DebugSession; use debugger_panel::{DebugPanel, ToggleFocus}; use gpui::App; +use session::DebugSession; use settings::Settings; use workspace::{ Continue, Pause, Restart, ShutdownDebugAdapters, Start, StepBack, StepInto, StepOut, StepOver, @@ -9,13 +9,8 @@ use workspace::{ }; pub mod attach_modal; -pub mod console; -pub mod debug_session; pub mod debugger_panel; -pub mod loaded_source_list; -pub mod module_list; -pub mod stack_frame_list; -pub mod variable_list; +pub mod session; #[cfg(test)] mod tests; diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs new file mode 100644 index 00000000000000..0a17d741fed56a --- /dev/null +++ b/crates/debugger_ui/src/session.rs @@ -0,0 +1,169 @@ +mod inert; +mod running; +mod starting; + +use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; + +use dap::{ + client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, + ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, + ThreadEvent, +}; +use gpui::{ + AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, +}; +use inert::InertState; +use project::debugger::session::Session; +use project::debugger::session::{ThreadId, ThreadStatus}; + +use rpc::proto::{self, PeerId}; +use settings::Settings; +use starting::StartingState; +use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, Tooltip}; +use workspace::{ + item::{self, Item, ItemEvent}, + FollowableItem, ViewId, Workspace, +}; + +pub enum DebugSession { + Inert(Entity), + Starting(Entity), + Running(Entity), +} + +#[derive(Debug)] +pub enum DebugPanelItemEvent { + Close, + Stopped { go_to_stack_frame: bool }, +} + +#[derive(Clone, PartialEq, Eq)] +pub enum ThreadItem { + Console, + LoadedSource, + Modules, + Variables, +} + +impl ThreadItem { + fn _to_proto(&self) -> proto::DebuggerThreadItem { + match self { + ThreadItem::Console => proto::DebuggerThreadItem::Console, + ThreadItem::LoadedSource => proto::DebuggerThreadItem::LoadedSource, + ThreadItem::Modules => proto::DebuggerThreadItem::Modules, + ThreadItem::Variables => proto::DebuggerThreadItem::Variables, + } + } + + fn from_proto(active_thread_item: proto::DebuggerThreadItem) -> Self { + match active_thread_item { + proto::DebuggerThreadItem::Console => ThreadItem::Console, + proto::DebuggerThreadItem::LoadedSource => ThreadItem::LoadedSource, + proto::DebuggerThreadItem::Modules => ThreadItem::Modules, + proto::DebuggerThreadItem::Variables => ThreadItem::Variables, + } + } +} + +impl EventEmitter for DebugSession {} + +impl Focusable for DebugSession { + fn focus_handle(&self, cx: &App) -> FocusHandle { + match self { + DebugSession::Inert(inert_state) => inert_state.focus_handle(cx), + DebugSession::Starting(starting_state) => starting_state.focus_handle(cx), + DebugSession::Running(running_state) => running_state.focus_handle(cx), + } + } +} + +impl Item for DebugSession { + type Event = DebugPanelItemEvent; +} + +impl FollowableItem for DebugSession { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, _window: &Window, _cx: &App) -> Option { + None + } + + fn from_state_proto( + _workspace: Entity, + _remote_id: ViewId, + _state: &mut Option, + _window: &mut Window, + _cx: &mut App, + ) -> Option>>> { + None + } + + fn add_event_to_update_proto( + &self, + _event: &Self::Event, + _update: &mut Option, + _window: &Window, + _cx: &App, + ) -> bool { + // update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default())); + + true + } + + fn apply_update_proto( + &mut self, + _project: &Entity, + _message: proto::update_view::Variant, + _window: &mut Window, + _cx: &mut Context, + ) -> gpui::Task> { + Task::ready(Ok(())) + } + + fn set_leader_peer_id( + &mut self, + _leader_peer_id: Option, + _window: &mut Window, + _cx: &mut Context, + ) { + } + + fn to_follow_event(_event: &Self::Event) -> Option { + None + } + + fn dedup( + &self, + existing: &Self, + _window: &Window, + _cx: &App, + ) -> Option { + if existing.client_id == self.client_id && existing.thread_id == self.thread_id { + Some(item::Dedup::KeepExisting) + } else { + None + } + } + + fn is_project_item(&self, _window: &Window, _cx: &App) -> bool { + true + } +} + +impl Render for DebugSession { + fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { + match self { + DebugSession::Inert(inert_state) => { + inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) + } + DebugSession::Starting(starting_state) => { + starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) + } + DebugSession::Running(running_state) => { + running_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) + } + } + } +} diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs new file mode 100644 index 00000000000000..62c92b8801e009 --- /dev/null +++ b/crates/debugger_ui/src/session/inert.rs @@ -0,0 +1,22 @@ +use gpui::{App, FocusHandle, Focusable}; +use ui::{div, Element, ParentElement, Render, Styled}; + +pub(super) struct InertState { + focus_handle: FocusHandle, +} + +impl Focusable for InertState { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for InertState { + fn render( + &mut self, + _window: &mut ui::Window, + _cx: &mut ui::Context<'_, Self>, + ) -> impl ui::IntoElement { + div().size_full().child("No debug sessions") + } +} diff --git a/crates/debugger_ui/src/debug_session.rs b/crates/debugger_ui/src/session/running.rs similarity index 78% rename from crates/debugger_ui/src/debug_session.rs rename to crates/debugger_ui/src/session/running.rs index cf4171337707d1..33666d4da3f48e 100644 --- a/crates/debugger_ui/src/debug_session.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1,124 +1,366 @@ -use crate::console::Console; -use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; -use crate::loaded_source_list::LoadedSourceList; -use crate::module_list::ModuleList; -use crate::stack_frame_list::{StackFrameList, StackFrameListEvent}; -use crate::variable_list::VariableList; +mod console; +mod loaded_source_list; +mod module_list; +mod stack_frame_list; +mod variable_list; +use console::Console; use dap::{ - client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, - ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, - ThreadEvent, + client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, }; use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, + AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::debugger::session::Session; -use project::debugger::session::{ThreadId, ThreadStatus}; - -use rpc::proto::{self, DebuggerThreadStatus, PeerId}; +use loaded_source_list::LoadedSourceList; +use module_list::ModuleList; +use project::debugger::session::{Session, ThreadId, ThreadStatus}; +use rpc::proto::{self, ViewId}; use settings::Settings; -use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, PopoverMenu, Tooltip}; +use stack_frame_list::{StackFrameList, StackFrameListEvent}; +use ui::{ + div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Color, + Context, ContextMenu, Disableable, DropdownMenu, Element, FluentBuilder, IconButton, IconName, + IconSize, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, ParentElement, + Render, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window, +}; +use variable_list::VariableList; use workspace::{ - item::{self, Item, ItemEvent}, - FollowableItem, ViewId, Workspace, + item::{self, ItemEvent}, + FollowableItem, Item, Workspace, }; -#[derive(Debug)] -pub enum DebugPanelItemEvent { - Close, - Stopped { go_to_stack_frame: bool }, -} - -#[derive(Clone, PartialEq, Eq)] -pub enum ThreadItem { - Console, - LoadedSource, - Modules, - Variables, -} - -impl ThreadItem { - fn _to_proto(&self) -> proto::DebuggerThreadItem { - match self { - ThreadItem::Console => proto::DebuggerThreadItem::Console, - ThreadItem::LoadedSource => proto::DebuggerThreadItem::LoadedSource, - ThreadItem::Modules => proto::DebuggerThreadItem::Modules, - ThreadItem::Variables => proto::DebuggerThreadItem::Variables, - } - } +use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; - fn from_proto(active_thread_item: proto::DebuggerThreadItem) -> Self { - match active_thread_item { - proto::DebuggerThreadItem::Console => ThreadItem::Console, - proto::DebuggerThreadItem::LoadedSource => ThreadItem::LoadedSource, - proto::DebuggerThreadItem::Modules => ThreadItem::Modules, - proto::DebuggerThreadItem::Variables => ThreadItem::Variables, - } - } -} +use super::{DebugPanelItemEvent, ThreadItem}; -pub struct DebugSession { +pub struct RunningState { session: Entity, thread_id: ThreadId, - console: Entity, + console: Entity, focus_handle: FocusHandle, remote_id: Option, show_console_indicator: bool, - module_list: Entity, + module_list: Entity, active_thread_item: ThreadItem, _workspace: WeakEntity, client_id: DebugAdapterClientId, - variable_list: Entity, + variable_list: Entity, _subscriptions: Vec, - stack_frame_list: Entity, - loaded_source_list: Entity, + stack_frame_list: Entity, + loaded_source_list: Entity, } -impl DebugSession { - #[allow(clippy::too_many_arguments)] - pub fn new( - session: Entity, - client_id: DebugAdapterClientId, - thread_id: ThreadId, - debug_panel: &Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - - let stack_frame_list = cx.new(|cx| { - StackFrameList::new(workspace.clone(), session.clone(), thread_id, window, cx) - }); - - let variable_list = cx.new(|cx| { - VariableList::new( - session.clone(), - client_id, - stack_frame_list.clone(), - window, - cx, - ) - }); - - let module_list = cx.new(|cx| ModuleList::new(session.clone(), client_id, cx)); +impl Render for RunningState { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let thread_status = ThreadStatus::Running; + let active_thread_item = &self.active_thread_item; - let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), client_id, cx)); + let capabilities = self.capabilities(cx); - let console = cx.new(|cx| { - Console::new( - session.clone(), - client_id, - stack_frame_list.clone(), - variable_list.clone(), - window, - cx, + h_flex() + .key_context("DebugPanelItem") + .track_focus(&self.focus_handle(cx)) + .size_full() + .items_start() + .child( + v_flex() + .size_full() + .items_start() + .child( + h_flex() + .w_full() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .justify_between() + .child( + h_flex() + .p_1() + .w_full() + .gap_2() + .map(|this| { + if thread_status == ThreadStatus::Running { + this.child( + IconButton::new( + "debug-pause", + IconName::DebugPause, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.pause_thread(cx); + })) + .tooltip(move |window, cx| { + Tooltip::text("Pause program")(window, cx) + }), + ) + } else { + this.child( + IconButton::new( + "debug-continue", + IconName::DebugContinue, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.continue_thread(cx) + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |window, cx| { + Tooltip::text("Continue program")(window, cx) + }), + ) + } + }) + .when( + capabilities.supports_step_back.unwrap_or(false), + |this| { + this.child( + IconButton::new( + "debug-step-back", + IconName::DebugStepBack, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.step_back(cx); + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |window, cx| { + Tooltip::text("Step back")(window, cx) + }), + ) + }, + ) + .child( + IconButton::new("debug-step-over", IconName::DebugStepOver) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.step_over(cx); + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |window, cx| { + Tooltip::text("Step over")(window, cx) + }), + ) + .child( + IconButton::new("debug-step-in", IconName::DebugStepInto) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.step_in(cx); + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |window, cx| { + Tooltip::text("Step in")(window, cx) + }), + ) + .child( + IconButton::new("debug-step-out", IconName::DebugStepOut) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.step_out(cx); + })) + .disabled(thread_status != ThreadStatus::Stopped) + .tooltip(move |window, cx| { + Tooltip::text("Step out")(window, cx) + }), + ) + .child( + IconButton::new("debug-restart", IconName::DebugRestart) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.restart_client(cx); + })) + .disabled( + !capabilities + .supports_restart_request + .unwrap_or_default(), + ) + .tooltip(move |window, cx| { + Tooltip::text("Restart")(window, cx) + }), + ) + .child( + IconButton::new("debug-stop", IconName::DebugStop) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.stop_thread(cx); + })) + .disabled( + thread_status != ThreadStatus::Stopped + && thread_status != ThreadStatus::Running, + ) + .tooltip(move |window, cx| { + Tooltip::text("Stop")(window, cx) + }), + ) + .child( + IconButton::new( + "debug-disconnect", + IconName::DebugDisconnect, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.disconnect_client(cx); + })) + .disabled( + thread_status == ThreadStatus::Exited + || thread_status == ThreadStatus::Ended, + ) + .tooltip( + move |window, cx| { + Tooltip::text("Disconnect")(window, cx) + }, + ), + ) + .child( + IconButton::new( + "debug-ignore-breakpoints", + if self.session.read(cx).breakpoints_enabled() { + IconName::DebugBreakpoint + } else { + IconName::DebugIgnoreBreakpoints + }, + ) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, _window, cx| { + this.toggle_ignore_breakpoints(cx); + })) + .disabled( + thread_status == ThreadStatus::Exited + || thread_status == ThreadStatus::Ended, + ) + .tooltip( + move |window, cx| { + Tooltip::text("Ignore breakpoints")(window, cx) + }, + ), + ), + ) + //.child(h_flex()) + .child(h_flex().p_1().mx_2().w_3_4().justify_end().child( + DropdownMenu::new( + "thread-list", + "Threads", + ContextMenu::build(window, cx, |this, _, _| { + this.entry("Thread 1", None, |_, _| {}).entry( + "Thread 2", + None, + |_, _| {}, + ) + }), + ), + )), + ) + .child( + h_flex() + .size_full() + .items_start() + .p_1() + .gap_4() + .child(self.stack_frame_list.clone()), + ), ) - }); - - cx.observe(&module_list, |_, _, cx| cx.notify()).detach(); - + .child( + v_flex() + .border_l_1() + .border_color(cx.theme().colors().border_variant) + .size_full() + .items_start() + .child( + h_flex() + .border_b_1() + .w_full() + .border_color(cx.theme().colors().border_variant) + .child(self.render_entry_button( + &SharedString::from("Variables"), + ThreadItem::Variables, + cx, + )) + .when( + capabilities.supports_modules_request.unwrap_or_default(), + |this| { + this.child(self.render_entry_button( + &SharedString::from("Modules"), + ThreadItem::Modules, + cx, + )) + }, + ) + .when( + capabilities + .supports_loaded_sources_request + .unwrap_or_default(), + |this| { + this.child(self.render_entry_button( + &SharedString::from("Loaded Sources"), + ThreadItem::LoadedSource, + cx, + )) + }, + ) + .child(self.render_entry_button( + &SharedString::from("Console"), + ThreadItem::Console, + cx, + )), + ) + .when(*active_thread_item == ThreadItem::Variables, |this| { + this.size_full().child(self.variable_list.clone()) + }) + .when(*active_thread_item == ThreadItem::Modules, |this| { + this.size_full().child(self.module_list.clone()) + }) + .when(*active_thread_item == ThreadItem::LoadedSource, |this| { + this.size_full().child(self.loaded_source_list.clone()) + }) + .when(*active_thread_item == ThreadItem::Console, |this| { + this.child(self.console.clone()) + }), + ) + } +} + +impl RunningState { + #[allow(clippy::too_many_arguments)] + pub fn new( + session: Entity, + client_id: DebugAdapterClientId, + thread_id: ThreadId, + debug_panel: &Entity, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let focus_handle = cx.focus_handle(); + + let stack_frame_list = cx.new(|cx| { + StackFrameList::new(workspace.clone(), session.clone(), thread_id, window, cx) + }); + + let variable_list = cx.new(|cx| { + VariableList::new( + session.clone(), + client_id, + stack_frame_list.clone(), + window, + cx, + ) + }); + + let module_list = cx.new(|cx| ModuleList::new(session.clone(), client_id, cx)); + + let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), client_id, cx)); + + let console = cx.new(|cx| { + Console::new( + session.clone(), + client_id, + stack_frame_list.clone(), + variable_list.clone(), + window, + cx, + ) + }); + + cx.observe(&module_list, |_, _, cx| cx.notify()).detach(); + let _subscriptions = vec![ cx.subscribe_in(debug_panel, window, { move |this: &mut Self, _, event: &DebugPanelEvent, window, cx| { @@ -184,152 +426,6 @@ impl DebugSession { } } - fn should_skip_event(&self, client_id: &DebugAdapterClientId, thread_id: ThreadId) -> bool { - thread_id != self.thread_id || *client_id != self.client_id - } - - fn handle_thread_continued_event( - &mut self, - client_id: &DebugAdapterClientId, - event: &ContinuedEvent, - cx: &mut Context, - ) { - if self.should_skip_event(client_id, ThreadId(event.thread_id)) { - return; - } - } - - fn handle_stopped_event( - &mut self, - client_id: &DebugAdapterClientId, - event: &StoppedEvent, - go_to_stack_frame: bool, - cx: &mut Context, - ) { - if self.should_skip_event( - client_id, - event.thread_id.map(ThreadId).unwrap_or(self.thread_id), - ) { - return; - } - - self.session.update(cx, |state, cx| { - state.invalidate(cx); - }); - - cx.emit(DebugPanelItemEvent::Stopped { go_to_stack_frame }); - } - - fn handle_thread_event( - &mut self, - client_id: &DebugAdapterClientId, - event: &ThreadEvent, - cx: &mut Context, - ) { - if self.should_skip_event(client_id, ThreadId(event.thread_id)) { - return; - } - } - - fn handle_output_event( - &mut self, - client_id: &DebugAdapterClientId, - event: &OutputEvent, - window: &mut Window, - cx: &mut Context, - ) { - if self.should_skip_event(client_id, self.thread_id) { - return; - } - - // skip telemetry output as it pollutes the users output view - let output_category = event - .category - .as_ref() - .unwrap_or(&OutputEventCategory::Console); - - // skip telemetry output as it pollutes the users output view - if output_category == &OutputEventCategory::Telemetry { - return; - } - - self.console.update(cx, |console, cx| { - console.add_message(event.clone(), window, cx); - }); - self.show_console_indicator = true; - cx.notify(); - } - - fn handle_module_event( - &mut self, - client_id: &DebugAdapterClientId, - event: &ModuleEvent, - cx: &mut Context, - ) { - if self.should_skip_event(client_id, self.thread_id) { - return; - } - - self.module_list.update(cx, |module_list, cx| { - module_list.on_module_event(event, cx); - }); - } - - fn handle_loaded_source_event( - &mut self, - client_id: &DebugAdapterClientId, - event: &LoadedSourceEvent, - cx: &mut Context, - ) { - if self.should_skip_event(client_id, self.thread_id) { - return; - } - - self.session - .update(cx, |state, cx| state.handle_loaded_source_event(event, cx)); - } - - fn handle_client_shutdown_event( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) { - if self.should_skip_event(client_id, self.thread_id) { - return; - } - - // TODO(debugger): make this work again - // self.dap_store.update(cx, |store, cx| { - // store.remove_active_debug_line_for_client(client_id, cx); - // }); - - cx.emit(DebugPanelItemEvent::Close); - } - - fn handle_client_exited_and_terminated_event( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) { - if Self::should_skip_event(self, client_id, self.thread_id) { - return; - } - - cx.emit(DebugPanelItemEvent::Close); - } - - fn handle_capabilities_changed_event( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) { - if Self::should_skip_event(self, client_id, self.thread_id) { - return; - } - - cx.notify(); - } - // pub(crate) fn update_adapter( // &mut self, // update: &UpdateDebugAdapter, @@ -551,15 +647,15 @@ impl DebugSession { } } -impl EventEmitter for DebugSession {} +impl EventEmitter for RunningState {} -impl Focusable for DebugSession { +impl Focusable for RunningState { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } -impl Item for DebugSession { +impl Item for RunningState { type Event = DebugPanelItemEvent; fn tab_content( @@ -593,342 +689,3 @@ impl Item for DebugSession { } } } - -impl FollowableItem for DebugSession { - fn remote_id(&self) -> Option { - self.remote_id - } - - fn to_state_proto(&self, _window: &Window, _cx: &App) -> Option { - None - } - - fn from_state_proto( - _workspace: Entity, - _remote_id: ViewId, - _state: &mut Option, - _window: &mut Window, - _cx: &mut App, - ) -> Option>>> { - None - } - - fn add_event_to_update_proto( - &self, - _event: &Self::Event, - _update: &mut Option, - _window: &Window, - _cx: &App, - ) -> bool { - // update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default())); - - true - } - - fn apply_update_proto( - &mut self, - _project: &Entity, - _message: proto::update_view::Variant, - _window: &mut Window, - _cx: &mut Context, - ) -> gpui::Task> { - Task::ready(Ok(())) - } - - fn set_leader_peer_id( - &mut self, - _leader_peer_id: Option, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn to_follow_event(_event: &Self::Event) -> Option { - None - } - - fn dedup( - &self, - existing: &Self, - _window: &Window, - _cx: &App, - ) -> Option { - if existing.client_id == self.client_id && existing.thread_id == self.thread_id { - Some(item::Dedup::KeepExisting) - } else { - None - } - } - - fn is_project_item(&self, _window: &Window, _cx: &App) -> bool { - true - } -} - -impl Render for DebugSession { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let thread_status = ThreadStatus::Running; - let active_thread_item = &self.active_thread_item; - - let capabilities = self.capabilities(cx); - - h_flex() - .key_context("DebugPanelItem") - .track_focus(&self.focus_handle(cx)) - .size_full() - .items_start() - .child( - v_flex() - .size_full() - .items_start() - .child( - h_flex() - .w_full() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .justify_between() - .child( - h_flex() - .p_1() - .w_full() - .gap_2() - .map(|this| { - if thread_status == ThreadStatus::Running { - this.child( - IconButton::new( - "debug-pause", - IconName::DebugPause, - ) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.pause_thread(cx); - })) - .tooltip(move |window, cx| { - Tooltip::text("Pause program")(window, cx) - }), - ) - } else { - this.child( - IconButton::new( - "debug-continue", - IconName::DebugContinue, - ) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.continue_thread(cx) - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Continue program")(window, cx) - }), - ) - } - }) - .when( - capabilities.supports_step_back.unwrap_or(false), - |this| { - this.child( - IconButton::new( - "debug-step-back", - IconName::DebugStepBack, - ) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.step_back(cx); - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Step back")(window, cx) - }), - ) - }, - ) - .child( - IconButton::new("debug-step-over", IconName::DebugStepOver) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.step_over(cx); - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Step over")(window, cx) - }), - ) - .child( - IconButton::new("debug-step-in", IconName::DebugStepInto) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.step_in(cx); - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Step in")(window, cx) - }), - ) - .child( - IconButton::new("debug-step-out", IconName::DebugStepOut) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.step_out(cx); - })) - .disabled(thread_status != ThreadStatus::Stopped) - .tooltip(move |window, cx| { - Tooltip::text("Step out")(window, cx) - }), - ) - .child( - IconButton::new("debug-restart", IconName::DebugRestart) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.restart_client(cx); - })) - .disabled( - !capabilities - .supports_restart_request - .unwrap_or_default(), - ) - .tooltip(move |window, cx| { - Tooltip::text("Restart")(window, cx) - }), - ) - .child( - IconButton::new("debug-stop", IconName::DebugStop) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.stop_thread(cx); - })) - .disabled( - thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, - ) - .tooltip(move |window, cx| { - Tooltip::text("Stop")(window, cx) - }), - ) - .child( - IconButton::new( - "debug-disconnect", - IconName::DebugDisconnect, - ) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.disconnect_client(cx); - })) - .disabled( - thread_status == ThreadStatus::Exited - || thread_status == ThreadStatus::Ended, - ) - .tooltip( - move |window, cx| { - Tooltip::text("Disconnect")(window, cx) - }, - ), - ) - .child( - IconButton::new( - "debug-ignore-breakpoints", - if self.session.read(cx).breakpoints_enabled() { - IconName::DebugBreakpoint - } else { - IconName::DebugIgnoreBreakpoints - }, - ) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.toggle_ignore_breakpoints(cx); - })) - .disabled( - thread_status == ThreadStatus::Exited - || thread_status == ThreadStatus::Ended, - ) - .tooltip( - move |window, cx| { - Tooltip::text("Ignore breakpoints")(window, cx) - }, - ), - ), - ) - //.child(h_flex()) - .child(h_flex().p_1().mx_2().w_3_4().justify_end().child( - DropdownMenu::new( - "thread-list", - "Threads", - ContextMenu::build(window, cx, |this, _, _| { - this.entry("Thread 1", None, |_, _| {}).entry( - "Thread 2", - None, - |_, _| {}, - ) - }), - ), - )), - ) - .child( - h_flex() - .size_full() - .items_start() - .p_1() - .gap_4() - .child(self.stack_frame_list.clone()), - ), - ) - .child( - v_flex() - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .size_full() - .items_start() - .child( - h_flex() - .border_b_1() - .w_full() - .border_color(cx.theme().colors().border_variant) - .child(self.render_entry_button( - &SharedString::from("Variables"), - ThreadItem::Variables, - cx, - )) - .when( - capabilities.supports_modules_request.unwrap_or_default(), - |this| { - this.child(self.render_entry_button( - &SharedString::from("Modules"), - ThreadItem::Modules, - cx, - )) - }, - ) - .when( - capabilities - .supports_loaded_sources_request - .unwrap_or_default(), - |this| { - this.child(self.render_entry_button( - &SharedString::from("Loaded Sources"), - ThreadItem::LoadedSource, - cx, - )) - }, - ) - .child(self.render_entry_button( - &SharedString::from("Console"), - ThreadItem::Console, - cx, - )), - ) - .when(*active_thread_item == ThreadItem::Variables, |this| { - this.size_full().child(self.variable_list.clone()) - }) - .when(*active_thread_item == ThreadItem::Modules, |this| { - this.size_full().child(self.module_list.clone()) - }) - .when(*active_thread_item == ThreadItem::LoadedSource, |this| { - this.size_full().child(self.loaded_source_list.clone()) - }) - .when(*active_thread_item == ThreadItem::Console, |this| { - this.child(self.console.clone()) - }), - ) - .into_any() - } -} diff --git a/crates/debugger_ui/src/console.rs b/crates/debugger_ui/src/session/running/console.rs similarity index 99% rename from crates/debugger_ui/src/console.rs rename to crates/debugger_ui/src/session/running/console.rs index 6eb23a5000b240..ca5f5a27f0d1a5 100644 --- a/crates/debugger_ui/src/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -1,4 +1,4 @@ -use crate::{ +use super::{ stack_frame_list::{StackFrameList, StackFrameListEvent}, variable_list::VariableList, }; diff --git a/crates/debugger_ui/src/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs similarity index 100% rename from crates/debugger_ui/src/loaded_source_list.rs rename to crates/debugger_ui/src/session/running/loaded_source_list.rs diff --git a/crates/debugger_ui/src/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs similarity index 100% rename from crates/debugger_ui/src/module_list.rs rename to crates/debugger_ui/src/session/running/module_list.rs diff --git a/crates/debugger_ui/src/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs similarity index 100% rename from crates/debugger_ui/src/stack_frame_list.rs rename to crates/debugger_ui/src/session/running/stack_frame_list.rs diff --git a/crates/debugger_ui/src/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs similarity index 99% rename from crates/debugger_ui/src/variable_list.rs rename to crates/debugger_ui/src/session/running/variable_list.rs index 4874029e11b2d1..45a832de19647d 100644 --- a/crates/debugger_ui/src/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -1,4 +1,4 @@ -use crate::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; +use super::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, Scope, ScopePresentationHint, diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs new file mode 100644 index 00000000000000..d928a2b3f05e8a --- /dev/null +++ b/crates/debugger_ui/src/session/starting.rs @@ -0,0 +1,22 @@ +use gpui::{FocusHandle, Focusable}; +use ui::{div, Element, ParentElement, Render, Styled}; + +pub(super) struct StartingState { + focus_handle: FocusHandle, +} + +impl Focusable for StartingState { + fn focus_handle(&self, cx: &ui::App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for StartingState { + fn render( + &mut self, + window: &mut ui::Window, + cx: &mut ui::Context<'_, Self>, + ) -> impl ui::IntoElement { + div().size_full().child("Starting a debug adapter") + } +} diff --git a/crates/debugger_ui/src/tests.rs b/crates/debugger_ui/src/tests.rs index 84a33d1bba8ffc..4a0d158ea0af9f 100644 --- a/crates/debugger_ui/src/tests.rs +++ b/crates/debugger_ui/src/tests.rs @@ -4,7 +4,7 @@ use settings::SettingsStore; use terminal_view::terminal_panel::TerminalPanel; use workspace::Workspace; -use crate::{debugger_panel::DebugPanel, debugger_panel_item::DebugSession}; +use crate::{debugger_panel::DebugPanel, session::DebugSession}; mod attach_modal; mod console; diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs index 8ee9af6ed5d1b4..8ea6e16725d5dd 100644 --- a/crates/debugger_ui/src/tests/module_list.rs +++ b/crates/debugger_ui/src/tests/module_list.rs @@ -1,5 +1,5 @@ use crate::{ - debugger_panel_item::ThreadItem, + session::ThreadItem, tests::{active_debug_panel_item, init_test, init_test_workspace}, }; use dap::{ From 9786da2fe512624d2d5f313a3df47266c41f1d47 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:41:46 +0100 Subject: [PATCH 571/650] WIP --- crates/debugger_ui/src/debugger_panel.rs | 84 +++--------------- crates/debugger_ui/src/lib.rs | 103 +--------------------- crates/debugger_ui/src/session.rs | 46 ++++++---- crates/debugger_ui/src/session/inert.rs | 9 +- crates/debugger_ui/src/session/running.rs | 52 ++--------- crates/project/src/debugger/session.rs | 2 +- 6 files changed, 60 insertions(+), 236 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b5effbdd64ce65..315a1f815d4a86 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -84,7 +84,14 @@ impl DebugPanel { pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_window, _cx| true); pane.set_close_pane_if_empty(false, cx); - + pane.add_item( + Box::new(DebugSession::inert(cx)), + false, + false, + None, + window, + cx, + ); pane }); @@ -119,14 +126,7 @@ impl DebugPanel { let (has_active_session, support_step_back) = debug_panel.update(cx, |this, cx| { this.active_debug_panel_item(cx) - .map(|item| { - ( - true, - item.update(cx, |this, cx| this.capabilities(cx)) - .supports_step_back - .unwrap_or(false), - ) - }) + .map(|item| (true, false)) .unwrap_or((false, false)) }); @@ -192,7 +192,7 @@ impl DebugPanel { .read(cx) .items() .filter_map(|item| item.downcast::()) - .filter(|item| &item.read(cx).client_id() == client_id) + .filter(|item| item.read(cx).session_id(cx) == Some(*client_id)) .map(|item| item.clone()) .collect() } @@ -200,7 +200,6 @@ impl DebugPanel { pub fn debug_panel_item_by_client( &self, client_id: DebugAdapterClientId, - thread_id: ThreadId, cx: &mut Context, ) -> Option> { self.pane @@ -210,7 +209,7 @@ impl DebugPanel { .find(|item| { let item = item.read(cx); - item.client_id() == client_id && item.thread_id() == thread_id + item.session_id(cx) == Some(client_id) }) } @@ -222,19 +221,6 @@ impl DebugPanel { cx: &mut Context, ) { match event { - pane::Event::RemovedItem { item } => { - let thread_panel = item.downcast::().unwrap(); - - let thread_id = thread_panel.read(cx).thread_id(); - - cx.notify(); - - thread_panel.update(cx, |this, cx| { - this.session().update(cx, |state, cx| { - state.terminate_threads(Some(vec![thread_id; 1]), cx); - }) - }); - } pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), @@ -245,19 +231,6 @@ impl DebugPanel { }) .ok(); } - pane::Event::ActivateItem { local, .. } => { - if !local { - return; - } - - if let Some(active_item) = self.pane.read(cx).active_item() { - if let Some(debug_item) = active_item.downcast::() { - debug_item.update(cx, |panel, cx| { - panel.go_to_current_stack_frame(window, cx); - }); - } - } - } _ => {} } } @@ -332,43 +305,12 @@ impl Panel for DebugPanel { } impl Render for DebugPanel { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { v_flex() .key_context("DebugPanel") .track_focus(&self.focus_handle) .size_full() - .map(|this| { - if self.pane.read(cx).items_len() == 0 { - this.child( - h_flex().size_full().items_center().justify_center().child( - v_flex() - .gap_2() - .rounded_md() - .max_w_64() - .items_start() - .child( - Label::new("You can create a debug task by creating a new task and setting the `type` key to `debug`") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - h_flex().w_full().justify_end().child( - Button::new( - "start-debugger", - "Choose a debugger", - ) - .label_size(LabelSize::Small) - .on_click(move |_, window, cx| { - window.dispatch_action(Box::new(Start), cx); - }) - ), - ), - ), - ) - } else { - this.child(self.pane.clone()) - } - }) + .child(self.pane.clone()) .into_any() } } diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 360b76de898879..b725cd84aec6d5 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -40,108 +40,7 @@ pub fn init(cx: &mut App) { }) }) }, - ) - .register_action(|workspace: &mut Workspace, _: &Stop, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.stop_thread(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &Continue, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.continue_thread(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &StepInto, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.step_in(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &StepBack, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.step_back(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &StepOut, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.step_out(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &StepOver, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.step_over(cx)) - }); - }) - .register_action(|workspace: &mut Workspace, _: &Restart, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.restart_client(cx)) - }); - }) - .register_action( - |workspace: &mut Workspace, _: &ToggleIgnoreBreakpoints, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx)) - }); - }, - ) - .register_action(|workspace: &mut Workspace, _: &Pause, _window, cx| { - let debug_panel = workspace.panel::(cx).unwrap(); - - debug_panel.update(cx, |panel, cx| { - let Some(active_item) = panel.active_debug_panel_item(cx) else { - return; - }; - - active_item.update(cx, |item, cx| item.pause_thread(cx)) - }); - }); + ); }) .detach(); } diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 0a17d741fed56a..2ca5c876a05268 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -25,12 +25,16 @@ use workspace::{ FollowableItem, ViewId, Workspace, }; -pub enum DebugSession { +enum DebugSessionState { Inert(Entity), Starting(Entity), Running(Entity), } +pub struct DebugSession { + remote_id: Option, + mode: DebugSessionState, +} #[derive(Debug)] pub enum DebugPanelItemEvent { Close, @@ -65,14 +69,29 @@ impl ThreadItem { } } +impl DebugSession { + pub(super) fn inert(cx: &mut App) -> Entity { + cx.new(|cx| Self { + remote_id: None, + mode: DebugSessionState::Inert(cx.new(|cx| InertState::new(cx))), + }) + } + pub(crate) fn session_id(&self, cx: &App) -> Option { + match &self.mode { + DebugSessionState::Inert(_) => None, + DebugSessionState::Starting(_entity) => unimplemented!(), + DebugSessionState::Running(entity) => Some(entity.read(cx).client_id()), + } + } +} impl EventEmitter for DebugSession {} impl Focusable for DebugSession { fn focus_handle(&self, cx: &App) -> FocusHandle { - match self { - DebugSession::Inert(inert_state) => inert_state.focus_handle(cx), - DebugSession::Starting(starting_state) => starting_state.focus_handle(cx), - DebugSession::Running(running_state) => running_state.focus_handle(cx), + match &self.mode { + DebugSessionState::Inert(inert_state) => inert_state.focus_handle(cx), + DebugSessionState::Starting(starting_state) => starting_state.focus_handle(cx), + DebugSessionState::Running(running_state) => running_state.focus_handle(cx), } } } @@ -134,13 +153,8 @@ impl FollowableItem for DebugSession { None } - fn dedup( - &self, - existing: &Self, - _window: &Window, - _cx: &App, - ) -> Option { - if existing.client_id == self.client_id && existing.thread_id == self.thread_id { + fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option { + if existing.session_id(cx) == self.session_id(cx) { Some(item::Dedup::KeepExisting) } else { None @@ -154,14 +168,14 @@ impl FollowableItem for DebugSession { impl Render for DebugSession { fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { - match self { - DebugSession::Inert(inert_state) => { + match &self.mode { + DebugSessionState::Inert(inert_state) => { inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) } - DebugSession::Starting(starting_state) => { + DebugSessionState::Starting(starting_state) => { starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) } - DebugSession::Running(running_state) => { + DebugSessionState::Running(running_state) => { running_state.update(cx, |this, cx| this.render(window, cx).into_any_element()) } } diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 62c92b8801e009..7cb1fb6093fe11 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,10 +1,17 @@ use gpui::{App, FocusHandle, Focusable}; -use ui::{div, Element, ParentElement, Render, Styled}; +use ui::{div, Context, Element, ParentElement, Render, Styled}; pub(super) struct InertState { focus_handle: FocusHandle, } +impl InertState { + pub(super) fn new(cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } +} impl Focusable for InertState { fn focus_handle(&self, cx: &App) -> FocusHandle { self.focus_handle.clone() diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 33666d4da3f48e..e3b7dd924875db 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -361,51 +361,13 @@ impl RunningState { cx.observe(&module_list, |_, _, cx| cx.notify()).detach(); - let _subscriptions = vec![ - cx.subscribe_in(debug_panel, window, { - move |this: &mut Self, _, event: &DebugPanelEvent, window, cx| { - match event { - DebugPanelEvent::Stopped { - client_id, - event, - go_to_stack_frame, - } => this.handle_stopped_event(client_id, event, *go_to_stack_frame, cx), - DebugPanelEvent::Thread((client_id, event)) => { - this.handle_thread_event(client_id, event, cx) - } - DebugPanelEvent::Output((client_id, event)) => { - this.handle_output_event(client_id, event, window, cx) - } - DebugPanelEvent::Module((client_id, event)) => { - this.handle_module_event(client_id, event, cx) - } - DebugPanelEvent::LoadedSource((client_id, event)) => { - this.handle_loaded_source_event(client_id, event, cx) - } - DebugPanelEvent::ClientShutdown(client_id) => { - this.handle_client_shutdown_event(client_id, cx) - } - DebugPanelEvent::Continued((client_id, event)) => { - this.handle_thread_continued_event(client_id, event, cx); - } - DebugPanelEvent::Exited(client_id) - | DebugPanelEvent::Terminated(client_id) => { - this.handle_client_exited_and_terminated_event(client_id, cx); - } - DebugPanelEvent::CapabilitiesChanged(client_id) => { - this.handle_capabilities_changed_event(client_id, cx); - } - }; - } - }), - cx.subscribe( - &stack_frame_list, - move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { - StackFrameListEvent::SelectedStackFrameChanged(_) - | StackFrameListEvent::StackFramesUpdated => this.clear_highlights(cx), - }, - ), - ]; + let _subscriptions = vec![cx.subscribe( + &stack_frame_list, + move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event { + StackFrameListEvent::SelectedStackFrameChanged(_) + | StackFrameListEvent::StackFramesUpdated => this.clear_highlights(cx), + }, + )]; Self { session, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index ad5f8c593ca69d..3621d4ad685c14 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -11,7 +11,7 @@ use dap::{ Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, SteppingGranularity, }; use futures::{future::Shared, FutureExt}; -use gpui::{App, Context, Task}; +use gpui::{App, Context, Entity, Task}; use rpc::AnyProtoClient; use serde_json::Value; use std::u64; From 95590967ebddbc688fa2da13237fb319073c73ae Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:51:39 +0100 Subject: [PATCH 572/650] WIP --- crates/dap/src/adapters.rs | 21 ++- crates/dap/src/client.rs | 69 +++++----- crates/dap/src/transport.rs | 157 ++++++++++++++++------- crates/dap_adapters/src/custom.rs | 6 +- crates/dap_adapters/src/go.rs | 6 +- crates/dap_adapters/src/javascript.rs | 6 +- crates/dap_adapters/src/lldb.rs | 6 +- crates/dap_adapters/src/php.rs | 11 +- crates/dap_adapters/src/python.rs | 6 +- crates/debugger_tools/src/dap_log.rs | 2 +- crates/project/src/debugger/dap_store.rs | 2 +- crates/project/src/debugger/session.rs | 72 ++++++++--- 12 files changed, 227 insertions(+), 137 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index a15c1ebfc2e947..4758640698ede9 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,6 +1,5 @@ #[cfg(any(test, feature = "test-support"))] use crate::transport::FakeTransport; -use crate::transport::Transport; use ::fs::Fs; use anyhow::{anyhow, Context as _, Ok, Result}; use async_compression::futures::bufread::GzipDecoder; @@ -19,9 +18,11 @@ use std::{ collections::{HashMap, HashSet}, ffi::{OsStr, OsString}, fmt::Debug, + net::Ipv4Addr, ops::Deref, path::{Path, PathBuf}, sync::Arc, + time::Duration, }; use sysinfo::{Pid, Process}; use task::DebugAdapterConfig; @@ -89,12 +90,19 @@ impl<'a> From<&'a str> for DebugAdapterName { } } +#[derive(Debug, Clone)] +pub struct TcpArguments { + pub host: Ipv4Addr, + pub port: Option, + pub timeout: Option, +} #[derive(Debug, Clone)] pub struct DebugAdapterBinary { pub command: String, pub arguments: Option>, pub envs: Option>, pub cwd: Option, + pub connection: Option, } pub struct AdapterVersion { @@ -261,8 +269,6 @@ pub trait DebugAdapter: 'static + Send + Sync { .await } - fn transport(&self) -> Arc; - async fn fetch_latest_adapter_version( &self, delegate: &dyn DapDelegate, @@ -316,10 +322,6 @@ impl DebugAdapter for FakeAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Arc { - Arc::new(FakeTransport::new()) - } - async fn get_binary( &self, _: &dyn DapDelegate, @@ -330,6 +332,11 @@ impl DebugAdapter for FakeAdapter { Ok(DebugAdapterBinary { command: "command".into(), arguments: None, + connection: Some(TcpArguments { + host: Ipv4Addr::LOCALHOST, + port: None, + timeout: None, + }), envs: None, cwd: None, }) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 9499a83c201928..cd5d755295951f 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -45,35 +45,32 @@ pub struct DebugAdapterClient { sequence_count: AtomicU64, binary: DebugAdapterBinary, executor: BackgroundExecutor, - adapter: Arc, transport_delegate: TransportDelegate, } impl DebugAdapterClient { - pub fn new( + pub fn new(cx: &AsyncApp) -> Self {} + + pub async fn start( id: DebugAdapterClientId, - adapter: Arc, binary: DebugAdapterBinary, - cx: &AsyncApp, - ) -> Self { - let transport_delegate = TransportDelegate::new(adapter.transport()); - - Self { + message_handler: F, + cx: &mut AsyncApp, + ) -> Result<()> + where + F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, + { + let transport_delegate = TransportDelegate::new(binary.clone()); + let this = Self { id, binary, - adapter, + transport_delegate, sequence_count: AtomicU64::new(1), executor: cx.background_executor().clone(), - } - } - - pub async fn reconnect(&mut self, message_handler: F, cx: &mut AsyncApp) -> Result<()> - where - F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, - { - let (server_rx, server_tx) = self.transport_delegate.reconnect(cx).await?; - log::info!("Successfully reconnected to debug adapter"); + }; + let (server_rx, server_tx) = this.transport_delegate.start(&self.binary, cx).await?; + log::info!("Successfully connected to debug adapter"); let client_id = self.id; @@ -96,12 +93,12 @@ impl DebugAdapterClient { }) } - pub async fn start(&mut self, message_handler: F, cx: &mut AsyncApp) -> Result<()> + pub async fn reconnect(&mut self, message_handler: F, cx: &mut AsyncApp) -> Result<()> where F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, { - let (server_rx, server_tx) = self.transport_delegate.start(&self.binary, cx).await?; - log::info!("Successfully connected to debug adapter"); + let (server_rx, server_tx) = self.transport_delegate.reconnect(cx).await?; + log::info!("Successfully reconnected to debug adapter"); let client_id = self.id; @@ -123,7 +120,6 @@ impl DebugAdapterClient { .detach_and_log_err(cx); }) } - async fn handle_receive_messages( client_id: DebugAdapterClientId, server_rx: Receiver, @@ -229,18 +225,10 @@ impl DebugAdapterClient { self.id } - pub fn adapter(&self) -> &Arc { - &self.adapter - } - pub fn binary(&self) -> &DebugAdapterBinary { &self.binary } - pub fn adapter_id(&self) -> String { - self.adapter.name().to_string() - } - /// Get the next sequence id to be used in a request pub fn next_sequence_id(&self) -> u64 { self.sequence_count.fetch_add(1, Ordering::Relaxed) @@ -335,15 +323,17 @@ mod tests { pub async fn test_initialize_client(cx: &mut TestAppContext) { init_test(cx); - let adapter = Arc::new(FakeAdapter::new()); - let mut client = DebugAdapterClient::new( crate::client::DebugAdapterClientId(1), - adapter, DebugAdapterBinary { command: "command".into(), arguments: Default::default(), envs: Default::default(), + connection: Some(TcpArguments { + host: Ipv4Addr::LOCALHOST, + port: None, + timeout: None, + }), cwd: None, }, &mut cx.to_async(), @@ -412,11 +402,15 @@ mod tests { let mut client = DebugAdapterClient::new( crate::client::DebugAdapterClientId(1), - adapter, DebugAdapterBinary { command: "command".into(), arguments: Default::default(), envs: Default::default(), + connection: Some(TCPArguments { + host: Ipv4Addr::LOCALHOST, + port: None, + path: None, + }), cwd: None, }, &mut cx.to_async(), @@ -467,11 +461,16 @@ mod tests { let mut client = DebugAdapterClient::new( crate::client::DebugAdapterClientId(1), - adapter, DebugAdapterBinary { command: "command".into(), arguments: Default::default(), envs: Default::default(), + + connection: Some(TCPArguments { + host: Ipv4Addr::LOCALHOST, + port: None, + path: None, + }), cwd: None, }, &mut cx.to_async(), diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 2e95f639a9b8c2..911d3a2753529f 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -68,18 +68,88 @@ impl TransportPipe { type Requests = Arc>>>>; type LogHandlers = Arc>>; +enum Transport { + Stdio(StdioTransport), + Tcp(TcpTransport), + #[cfg(any(test, feature = "test-support"))] + Fake(FakeTransport), +} + +impl Transport { + async fn start(&self, binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result { + match self { + Transport::Stdio(stdio_transport) => stdio_transport.start(binary, cx).await, + Transport::Tcp(tcp_transport) => tcp_transport.start(binary, cx).await, + #[cfg(any(test, feature = "test-support"))] + Transport::Fake(fake_transport) => fake_transport.start(binary, cx).await, + } + } + + fn has_adapter_logs(&self) -> bool { + match self { + Transport::Stdio(stdio_transport) => stdio_transport.has_adapter_logs(), + Transport::Tcp(tcp_transport) => tcp_transport.has_adapter_logs(), + #[cfg(any(test, feature = "test-support"))] + Transport::Fake(fake_transport) => fake_transport.has_adapter_logs(), + } + } + + async fn kill(&self) -> Result<()> { + match self { + Transport::Stdio(stdio_transport) => stdio_transport.kill().await, + Transport::Tcp(tcp_transport) => tcp_transport.kill().await, + #[cfg(any(test, feature = "test-support"))] + Transport::Fake(fake_transport) => fake_transport.kill().await, + } + } + + async fn reconnect(&self, cx: &mut AsyncApp) -> Result { + match self { + Transport::Stdio(stdio_transport) => stdio_transport.reconnect(cx).await, + Transport::Tcp(tcp_transport) => tcp_transport.reconnect(cx).await, + #[cfg(any(test, feature = "test-support"))] + Transport::Fake(fake_transport) => fake_transport.reconnect(cx).await, + } + } + + #[cfg(any(test, feature = "test-support"))] + fn as_fake(&self) -> &FakeTransport { + match self { + Transport::Fake(fake_transport) => fake_transport, + _ => panic!("Not a fake transport layer"), + } + } +} + pub(crate) struct TransportDelegate { log_handlers: LogHandlers, current_requests: Requests, pending_requests: Requests, - transport: Arc, + transport: Transport, server_tx: Arc>>>, } +impl Transport { + #[cfg(any(test, feature = "test-support"))] + fn fake(args: DebugAdapterBinary) -> Self { + let this = Self::Fake(FakeTransport::new()); + } +} + impl TransportDelegate { - pub fn new(transport: Arc) -> Self { + pub async fn new(args: DebugAdapterBinary) -> Self { Self { - transport, + transport: Transport::new(args).await, + server_tx: Default::default(), + log_handlers: Default::default(), + current_requests: Default::default(), + pending_requests: Default::default(), + } + } + #[cfg(any(test, feature = "test-support"))] + pub fn fake(args: DebugAdapterBinary) -> Self { + Self { + transport: Transport::fake(args), server_tx: Default::default(), log_handlers: Default::default(), current_requests: Default::default(), @@ -475,41 +545,14 @@ impl TransportDelegate { } } -#[async_trait(?Send)] -pub trait Transport: 'static + Send + Sync + Any { - async fn start(&self, binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result; - - fn has_adapter_logs(&self) -> bool; - - async fn kill(&self) -> Result<()>; - - async fn reconnect(&self, _: &mut AsyncApp) -> Result { - bail!("Cannot reconnect to adapter") - } - - #[cfg(any(test, feature = "test-support"))] - fn as_fake(&self) -> &FakeTransport { - panic!("Called as_fake on a real adapter"); - } -} - pub struct TcpTransport { port: u16, host: Ipv4Addr, timeout: Option, - process: Arc>>, + process: Arc>, } impl TcpTransport { - pub fn new(host: Ipv4Addr, port: u16, timeout: Option) -> Self { - Self { - port, - host, - timeout, - process: Arc::new(Mutex::new(None)), - } - } - /// Get an open port to use with the tcp client when not supplied by debug config pub async fn port(host: &TCPHost) -> Result { if let Some(port) = host.port { @@ -521,10 +564,13 @@ impl TcpTransport { .port()) } } -} + async fn port_for_host(host: Ipv4Addr) -> Result { + Ok(TcpListener::bind(SocketAddrV4::new(host, 0)) + .await? + .local_addr()? + .port()) + } -#[async_trait(?Send)] -impl Transport for TcpTransport { async fn reconnect(&self, cx: &mut AsyncApp) -> Result { let address = SocketAddrV4::new(self.host, self.port); @@ -563,7 +609,26 @@ impl Transport for TcpTransport { )) } - async fn start(&self, binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result { + async fn start(binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result { + let Some(connection_args) = binary.connection.as_ref() else { + return Err(anyhow!("No connection arguments provided")); + }; + let host = connection_args.host; + let port = if let Some(port) = connection_args.port { + port + } else { + TcpListener::bind(SocketAddrV4::new(host, 0)) + .await + .with_context(|| { + format!( + "Failed to connect to debug adapter over tcp. host: {}", + host + ) + })? + .local_addr()? + .port() + }; + let mut command = util::command::new_smol_command(&binary.command); if let Some(cwd) = &binary.cwd { @@ -588,16 +653,16 @@ impl Transport for TcpTransport { .spawn() .with_context(|| "failed to start debug adapter.")?; - let address = SocketAddrV4::new(self.host, self.port); + let address = SocketAddrV4::new(host, port); - let timeout = self.timeout.unwrap_or_else(|| { + let timeout = connection_args.timeout.unwrap_or_else(|| { cx.update(|cx| DebuggerSettings::get_global(cx).timeout) .unwrap_or(2000u64) }); let (rx, tx) = select! { _ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => { - return Err(anyhow!(format!("Connection to TCP DAP timeout {}:{}", self.host, self.port))) + return Err(anyhow!(format!("Connection to TCP DAP timeout {}:{}", host, port))) }, result = cx.spawn(|cx| async move { loop { @@ -612,8 +677,8 @@ impl Transport for TcpTransport { }; log::info!( "Debug adapter has connected to TCP server {}:{}", - self.host, - self.port + host, + port ); let stdout = process.stdout.take(); @@ -654,10 +719,7 @@ impl StdioTransport { process: Arc::new(Mutex::new(None)), } } -} -#[async_trait(?Send)] -impl Transport for StdioTransport { async fn start(&self, binary: &DebugAdapterBinary, _: &mut AsyncApp) -> Result { let mut command = util::command::new_smol_command(&binary.command); @@ -721,6 +783,10 @@ impl Transport for StdioTransport { false } + async fn reconnect(&self, _: &mut AsyncApp) -> Result { + bail!("Cannot reconnect to adapter") + } + async fn kill(&self) -> Result<()> { if let Some(mut process) = self.process.lock().await.take() { process.kill()?; @@ -803,11 +869,7 @@ impl FakeTransport { .await .insert(R::COMMAND, Box::new(handler)); } -} -#[cfg(any(test, feature = "test-support"))] -#[async_trait(?Send)] -impl Transport for FakeTransport { async fn reconnect(&self, cx: &mut AsyncApp) -> Result { self.start( &DebugAdapterBinary { @@ -815,6 +877,7 @@ impl Transport for FakeTransport { arguments: None, envs: None, cwd: None, + connection: TcpTransport::new(None, None), }, cx, ) diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 6abec9407f328a..c2a8f883d3d6f0 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,6 +1,6 @@ use std::{ffi::OsString, path::PathBuf, sync::Arc}; -use dap::transport::{StdioTransport, TcpTransport, Transport}; +use dap::transport::{StdioTransport, TcpTransport}; use gpui::AsyncApp; use serde_json::Value; use task::DebugAdapterConfig; @@ -36,10 +36,6 @@ impl DebugAdapter for CustomDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Arc { - self.transport.clone() - } - async fn get_binary( &self, _: &dyn DapDelegate, diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index d19a3f2290a9e9..0a1d4703b40079 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -1,4 +1,4 @@ -use dap::transport::{TcpTransport, Transport}; +use dap::transport::TcpTransport; use gpui::AsyncApp; use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf, sync::Arc}; @@ -28,10 +28,6 @@ impl DebugAdapter for GoDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Arc { - Arc::new(TcpTransport::new(self.host, self.port, self.timeout)) - } - async fn get_binary( &self, delegate: &dyn DapDelegate, diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 7657cc9f4493de..b96633fefe7fc3 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,5 +1,5 @@ use adapters::latest_github_release; -use dap::transport::{TcpTransport, Transport}; +use dap::transport::TcpTransport; use gpui::AsyncApp; use regex::Regex; use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf, sync::Arc}; @@ -33,10 +33,6 @@ impl DebugAdapter for JsDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Arc { - Arc::new(TcpTransport::new(self.host, self.port, self.timeout)) - } - async fn fetch_latest_adapter_version( &self, delegate: &dyn DapDelegate, diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 7b5a804e0f586c..fbf54791297caf 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::Arc}; use anyhow::Result; use async_trait::async_trait; -use dap::transport::{StdioTransport, Transport}; +use dap::transport::StdioTransport; use gpui::AsyncApp; use sysinfo::{Pid, Process}; use task::{DebugAdapterConfig, DebugRequestType}; @@ -25,10 +25,6 @@ impl DebugAdapter for LldbDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Arc { - Arc::new(StdioTransport::new()) - } - async fn get_binary( &self, delegate: &dyn DapDelegate, diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 23aa2c30ef8a6d..98d7474e6f6d87 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,5 +1,5 @@ use adapters::latest_github_release; -use dap::transport::{TcpTransport, Transport}; +use dap::{adapters::TcpArguments, transport::TcpTransport}; use gpui::AsyncApp; use std::{net::Ipv4Addr, path::PathBuf, sync::Arc}; @@ -30,10 +30,6 @@ impl DebugAdapter for PhpDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Arc { - Arc::new(TcpTransport::new(self.host, self.port, self.timeout)) - } - async fn fetch_latest_adapter_version( &self, delegate: &dyn DapDelegate, @@ -92,6 +88,11 @@ impl DebugAdapter for PhpDebugAdapter { adapter_path.join(Self::ADAPTER_PATH).into(), format!("--server={}", self.port).into(), ]), + connection: Some(TcpArguments { + port: Some(self.port), + host: Ipv4Addr::LOCALHOST, + timeout: None, + }), cwd: config.cwd.clone(), envs: None, }) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index e44cadd22ef1e5..e33f8ffbfc14e6 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,5 +1,5 @@ use crate::*; -use dap::transport::{TcpTransport, Transport}; +use dap::transport::TcpTransport; use gpui::AsyncApp; use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf, sync::Arc}; @@ -29,10 +29,6 @@ impl DebugAdapter for PythonDebugAdapter { DebugAdapterName(Self::ADAPTER_NAME.into()) } - fn transport(&self) -> Arc { - Arc::new(TcpTransport::new(self.host, self.port, self.timeout)) - } - async fn fetch_latest_adapter_version( &self, delegate: &dyn DapDelegate, diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 8bea2a632e5a6c..241b6d71235f07 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -573,7 +573,7 @@ impl DapLogView { let client = client.read(cx).adapter_client()?; Some(DapMenuItem { client_id: client.id(), - client_name: client.adapter_id(), + client_name: unimplemented!(), has_adapter_logs: client.has_adapter_logs(), selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), }) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index e4b02580490b4d..2fd919e647be74 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -620,7 +620,7 @@ impl DapStore { } }; - let mut client = DebugAdapterClient::new(client_id, adapter, binary, &cx); + let mut client = DebugAdapterClient::new(client_id, binary, &cx); client .start( diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 3621d4ad685c14..05194c2e7757d6 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1,17 +1,20 @@ +use super::breakpoint_store::BreakpointStore; use super::dap_command::{ self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, SetVariableValueCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, VariablesCommand, }; +use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; use collections::{HashMap, IndexMap}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::{ Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, SteppingGranularity, }; +use dap_adapters::build_adapter; use futures::{future::Shared, FutureExt}; -use gpui::{App, Context, Entity, Task}; +use gpui::{App, AppContext, Context, Entity, Task}; use rpc::AnyProtoClient; use serde_json::Value; use std::u64; @@ -142,10 +145,37 @@ impl RemoteConnection { } pub enum Mode { - Local(Arc), + Local(LocalMode), Remote(RemoteConnection), } +struct LocalMode { + client: Arc, +} + +impl LocalMode { + fn new( + breakpoint_store: Entity, + disposition: DebugAdapterConfig, + delegate: DapAdapterDelegate, + cx: &mut App, + ) -> Task> { + cx.spawn(move |cx| async move { + let adapter = build_adapter(&config.kind).await?; + + let binary = cx.update(|cx| { + let name = DebugAdapterName::from(adapter.name().as_ref()); + + ProjectSettings::get_global(cx) + .dap + .get(&name) + .and_then(|s| s.binary.as_ref().map(PathBuf::from)) + })?; + + todo!() + }) + } +} impl From for Mode { fn from(value: RemoteConnection) -> Self { Self::Remote(value) @@ -293,20 +323,30 @@ impl CompletionsQuery { } impl Session { - pub(crate) fn local(adapter: Arc, config: DebugAdapterConfig) -> Self { - let client_id = adapter.id(); - - Self { - mode: Mode::Local(adapter), - client_id, - config, - capabilities: unimplemented!(), - ignore_breakpoints: false, - requests: HashMap::default(), - modules: Vec::default(), - loaded_sources: Vec::default(), - threads: IndexMap::default(), - } + pub(crate) fn local( + breakpoints: Entity, + client_id: DebugAdapterClientId, + delegate: DapAdapterDelegate, + config: DebugAdapterConfig, + cx: &mut App, + ) -> Task>> { + cx.spawn(move |cx| async move { + let adapter = build_adapter(&config.kind).await?; + let mode = LocalMode::new(breakpoints, config, adapter, cx).await?; + cx.update(|cx| { + cx.new(|cx| Self { + mode: Mode::Local(mode), + client_id, + config, + capabilities: unimplemented!(), + ignore_breakpoints: false, + requests: HashMap::default(), + modules: Vec::default(), + loaded_sources: Vec::default(), + threads: IndexMap::default(), + }) + }) + }) } pub(crate) fn remote( From 06338963f3891186563ec4c82c5b467c1d5ddb9d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:24:50 +0100 Subject: [PATCH 573/650] WIP --- crates/dap/src/client.rs | 36 +---- crates/dap/src/transport.rs | 188 +++++++++----------------- crates/dap_adapters/src/custom.rs | 29 ++-- crates/dap_adapters/src/go.rs | 1 + crates/dap_adapters/src/javascript.rs | 5 + crates/dap_adapters/src/lldb.rs | 1 + crates/dap_adapters/src/python.rs | 5 + 7 files changed, 97 insertions(+), 168 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index cd5d755295951f..cc69a87f39d60d 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -49,8 +49,6 @@ pub struct DebugAdapterClient { } impl DebugAdapterClient { - pub fn new(cx: &AsyncApp) -> Self {} - pub async fn start( id: DebugAdapterClientId, binary: DebugAdapterBinary, @@ -60,19 +58,18 @@ impl DebugAdapterClient { where F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, { - let transport_delegate = TransportDelegate::new(binary.clone()); + let ((server_rx, server_tx), transport_delegate) = + TransportDelegate::start(&binary, cx).await?; let this = Self { id, binary, - transport_delegate, sequence_count: AtomicU64::new(1), executor: cx.background_executor().clone(), }; - let (server_rx, server_tx) = this.transport_delegate.start(&self.binary, cx).await?; log::info!("Successfully connected to debug adapter"); - let client_id = self.id; + let client_id = this.id; // start handling events/reverse requests cx.update(|cx| { @@ -93,33 +90,6 @@ impl DebugAdapterClient { }) } - pub async fn reconnect(&mut self, message_handler: F, cx: &mut AsyncApp) -> Result<()> - where - F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, - { - let (server_rx, server_tx) = self.transport_delegate.reconnect(cx).await?; - log::info!("Successfully reconnected to debug adapter"); - - let client_id = self.id; - - // start handling events/reverse requests - cx.update(|cx| { - cx.spawn({ - let server_tx = server_tx.clone(); - |mut cx| async move { - Self::handle_receive_messages( - client_id, - server_rx, - server_tx, - message_handler, - &mut cx, - ) - .await - } - }) - .detach_and_log_err(cx); - }) - } async fn handle_receive_messages( client_id: DebugAdapterClientId, server_rx: Receiver, diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 911d3a2753529f..f07ce87b19943d 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -23,7 +23,7 @@ use std::{ sync::Arc, time::Duration, }; -use task::TCPHost; +use task::{DebugAdapterKind, TCPHost}; use util::ResultExt as _; use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; @@ -76,12 +76,25 @@ enum Transport { } impl Transport { - async fn start(&self, binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result { - match self { - Transport::Stdio(stdio_transport) => stdio_transport.start(binary, cx).await, - Transport::Tcp(tcp_transport) => tcp_transport.start(binary, cx).await, - #[cfg(any(test, feature = "test-support"))] - Transport::Fake(fake_transport) => fake_transport.start(binary, cx).await, + async fn start( + binary: &DebugAdapterBinary, + cx: &mut AsyncApp, + ) -> Result<(TransportPipe, Self)> { + #[cfg(any(test, feature = "test-support"))] + if binary.kind == DebugAdapterKind::Fake { + return FakeTransport::start(cx) + .await + .map(|(transports, fake)| (transports, Self::Fake(fake))); + } + + if let Some(connection) = &binary.connection { + TcpTransport::start(binary, cx) + .await + .map(|(transports, tcp)| (transports, Self::Tcp(tcp))) + } else { + StdioTransport::start(binary, cx) + .await + .map(|(transports, stdio)| (transports, Self::Stdio(stdio))) } } @@ -103,15 +116,6 @@ impl Transport { } } - async fn reconnect(&self, cx: &mut AsyncApp) -> Result { - match self { - Transport::Stdio(stdio_transport) => stdio_transport.reconnect(cx).await, - Transport::Tcp(tcp_transport) => tcp_transport.reconnect(cx).await, - #[cfg(any(test, feature = "test-support"))] - Transport::Fake(fake_transport) => fake_transport.reconnect(cx).await, - } - } - #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeTransport { match self { @@ -137,15 +141,6 @@ impl Transport { } impl TransportDelegate { - pub async fn new(args: DebugAdapterBinary) -> Self { - Self { - transport: Transport::new(args).await, - server_tx: Default::default(), - log_handlers: Default::default(), - current_requests: Default::default(), - pending_requests: Default::default(), - } - } #[cfg(any(test, feature = "test-support"))] pub fn fake(args: DebugAdapterBinary) -> Self { Self { @@ -157,21 +152,20 @@ impl TransportDelegate { } } - pub(crate) async fn reconnect( - &mut self, - cx: &mut AsyncApp, - ) -> Result<(Receiver, Sender)> { - self.start_handlers(self.transport.reconnect(cx).await?, cx) - .await - } - pub(crate) async fn start( - &mut self, binary: &DebugAdapterBinary, cx: &mut AsyncApp, - ) -> Result<(Receiver, Sender)> { - self.start_handlers(self.transport.start(binary, cx).await?, cx) - .await + ) -> Result<((Receiver, Sender), Self)> { + let (transport_pipes, transport) = Transport::start(binary, cx).await?; + let mut this = Self { + transport, + server_tx: Default::default(), + log_handlers: Default::default(), + current_requests: Default::default(), + pending_requests: Default::default(), + }; + let messages = this.start_handlers(transport_pipes, cx).await?; + Ok((messages, this)) } async fn start_handlers( @@ -548,8 +542,8 @@ impl TransportDelegate { pub struct TcpTransport { port: u16, host: Ipv4Addr, - timeout: Option, - process: Arc>, + timeout: u64, + process: Mutex, } impl TcpTransport { @@ -571,45 +565,10 @@ impl TcpTransport { .port()) } - async fn reconnect(&self, cx: &mut AsyncApp) -> Result { - let address = SocketAddrV4::new(self.host, self.port); - - let timeout = self.timeout.unwrap_or_else(|| { - cx.update(|cx| DebuggerSettings::get_global(cx).timeout) - .unwrap_or(2000u64) - }); - - let (rx, tx) = select! { - _ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => { - return Err(anyhow!(format!("Reconnect to TCP DAP timeout {}:{}", self.host, self.port))) - }, - result = cx.spawn(|cx| async move { - loop { - match TcpStream::connect(address).await { - Ok(stream) => return stream.split(), - Err(_) => { - cx.background_executor().timer(Duration::from_millis(100)).await; - } - } - } - }).fuse() => result - }; - - log::info!( - "Debug adapter has reconnected to TCP server {}:{}", - self.host, - self.port - ); - - Ok(TransportPipe::new( - Box::new(tx), - Box::new(BufReader::new(rx)), - None, - None, - )) - } - - async fn start(binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result { + async fn start( + binary: &DebugAdapterBinary, + cx: &mut AsyncApp, + ) -> Result<(TransportPipe, Self)> { let Some(connection_args) = binary.connection.as_ref() else { return Err(anyhow!("No connection arguments provided")); }; @@ -680,20 +639,24 @@ impl TcpTransport { host, port ); - let stdout = process.stdout.take(); let stderr = process.stderr.take(); - { - *self.process.lock().await = Some(process); - } + let this = Self { + port, + host, + process: Mutex::new(process), + timeout, + }; - Ok(TransportPipe::new( + let pipe = TransportPipe::new( Box::new(tx), Box::new(BufReader::new(rx)), stdout.map(|s| Box::new(s) as Box), stderr.map(|s| Box::new(s) as Box), - )) + ); + + Ok((pipe, this)) } fn has_adapter_logs(&self) -> bool { @@ -701,26 +664,18 @@ impl TcpTransport { } async fn kill(&self) -> Result<()> { - if let Some(mut process) = self.process.lock().await.take() { - process.kill()?; - } + self.process.lock().await.kill()?; Ok(()) } } pub struct StdioTransport { - process: Arc>>, + process: Mutex, } impl StdioTransport { - pub fn new() -> Self { - Self { - process: Arc::new(Mutex::new(None)), - } - } - - async fn start(&self, binary: &DebugAdapterBinary, _: &mut AsyncApp) -> Result { + async fn start(binary: &DebugAdapterBinary, _: &mut AsyncApp) -> Result<(TransportPipe, Self)> { let mut command = util::command::new_smol_command(&binary.command); if let Some(cwd) = &binary.cwd { @@ -767,15 +722,16 @@ impl StdioTransport { log::info!("Debug adapter has connected to stdio adapter"); - { - *self.process.lock().await = Some(process); - } + let process = Mutex::new(process); - Ok(TransportPipe::new( - Box::new(stdin), - Box::new(BufReader::new(stdout)), - None, - stderr, + Ok(( + TransportPipe::new( + Box::new(stdin), + Box::new(BufReader::new(stdout)), + None, + stderr, + ), + Self { process }, )) } @@ -788,10 +744,7 @@ impl StdioTransport { } async fn kill(&self) -> Result<()> { - if let Some(mut process) = self.process.lock().await.take() { - process.kill()?; - } - + self.process.lock().await.kill()?; Ok(()) } } @@ -819,13 +772,6 @@ pub struct FakeTransport { #[cfg(any(test, feature = "test-support"))] impl FakeTransport { - pub fn new() -> Self { - Self { - request_handlers: Arc::new(Mutex::new(HashMap::default())), - response_handlers: Arc::new(Mutex::new(HashMap::default())), - } - } - pub async fn on_request(&self, mut handler: F) where F: 'static + Send + FnMut(u64, R::Arguments) -> Result, @@ -884,19 +830,19 @@ impl FakeTransport { .await } - async fn start( - &self, - _binary: &DebugAdapterBinary, - cx: &mut AsyncApp, - ) -> Result { + async fn start(cx: &mut AsyncApp) -> Result { + let this = Self { + request_handlers: Arc::new(Mutex::new(HashMap::default())), + response_handlers: Arc::new(Mutex::new(HashMap::default())), + }; use dap_types::requests::{Request, RunInTerminal, StartDebugging}; use serde_json::json; let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); - let request_handlers = self.request_handlers.clone(); - let response_handlers = self.response_handlers.clone(); + let request_handlers = this.request_handlers.clone(); + let response_handlers = this.response_handlers.clone(); let stdout_writer = Arc::new(Mutex::new(stdout_writer)); cx.background_executor() diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index c2a8f883d3d6f0..a761dfb541081b 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -9,24 +9,13 @@ use crate::*; pub(crate) struct CustomDebugAdapter { custom_args: CustomArgs, - transport: Arc, } impl CustomDebugAdapter { const ADAPTER_NAME: &'static str = "custom_dap"; pub(crate) async fn new(custom_args: CustomArgs) -> Result { - Ok(CustomDebugAdapter { - transport: match &custom_args.connection { - DebugConnectionType::TCP(host) => Arc::new(TcpTransport::new( - host.host(), - TcpTransport::port(&host).await?, - host.timeout, - )), - DebugConnectionType::STDIO => Arc::new(StdioTransport::new()), - }, - custom_args, - }) + Ok(CustomDebugAdapter { custom_args }) } } @@ -43,7 +32,17 @@ impl DebugAdapter for CustomDebugAdapter { _: Option, _: &mut AsyncApp, ) -> Result { - Ok(DebugAdapterBinary { + let connection = if let DebugConnectionType::TCP(connection) = &self.custom_args.connection + { + Some(adapters::TcpArguments { + host: connection.host(), + port: connection.port, + timeout: connection.timeout, + }) + } else { + None + }; + let ret = DebugAdapterBinary { command: self.custom_args.command.clone(), arguments: self .custom_args @@ -52,7 +51,9 @@ impl DebugAdapter for CustomDebugAdapter { .map(|args| args.iter().map(OsString::from).collect()), cwd: config.cwd.clone(), envs: self.custom_args.envs.clone(), - }) + connection, + }; + Ok(ret) } async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result { diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index 0a1d4703b40079..95e7d453b2759c 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -82,6 +82,7 @@ impl DebugAdapter for GoDebugAdapter { ]), cwd: config.cwd.clone(), envs: None, + connection: None, }) } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index b96633fefe7fc3..61e1f8bfe14f8e 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -94,6 +94,11 @@ impl DebugAdapter for JsDebugAdapter { ]), cwd: config.cwd.clone(), envs: None, + connection: Some(adapters::TcpArguments { + host: self.host, + port: Some(self.port), + timeout: self.timeout, + }), }) } diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index fbf54791297caf..0015bf0b64704d 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -55,6 +55,7 @@ impl DebugAdapter for LldbDebugAdapter { arguments: None, envs: None, cwd: config.cwd.clone(), + connection: None, }) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index e33f8ffbfc14e6..bce937a6c36ea6 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -121,6 +121,11 @@ impl DebugAdapter for PythonDebugAdapter { format!("--port={}", self.port).into(), format!("--host={}", self.host).into(), ]), + connection: Some(adapters::TcpArguments { + host: self.host, + port: Some(self.port), + timeout: self.timeout, + }), cwd: config.cwd.clone(), envs: None, }) From b67a382e57a057ad4dd8755881dd85e590560846 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:15:57 +0100 Subject: [PATCH 574/650] WIP --- crates/dap/src/client.rs | 8 ++- crates/dap/src/transport.rs | 26 ++++----- crates/project/src/debugger/dap_store.rs | 20 +------ crates/project/src/debugger/session.rs | 72 ++++++++++++++++++------ 4 files changed, 71 insertions(+), 55 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index cc69a87f39d60d..c4b89f9a3cc92b 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -53,13 +53,13 @@ impl DebugAdapterClient { id: DebugAdapterClientId, binary: DebugAdapterBinary, message_handler: F, - cx: &mut AsyncApp, - ) -> Result<()> + cx: AsyncApp, + ) -> Result where F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, { let ((server_rx, server_tx), transport_delegate) = - TransportDelegate::start(&binary, cx).await?; + TransportDelegate::start(&binary, cx.clone()).await?; let this = Self { id, binary, @@ -87,6 +87,8 @@ impl DebugAdapterClient { } }) .detach_and_log_err(cx); + + this }) } diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index f07ce87b19943d..96f6d1c3bf912a 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -76,10 +76,7 @@ enum Transport { } impl Transport { - async fn start( - binary: &DebugAdapterBinary, - cx: &mut AsyncApp, - ) -> Result<(TransportPipe, Self)> { + async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> { #[cfg(any(test, feature = "test-support"))] if binary.kind == DebugAdapterKind::Fake { return FakeTransport::start(cx) @@ -87,7 +84,7 @@ impl Transport { .map(|(transports, fake)| (transports, Self::Fake(fake))); } - if let Some(connection) = &binary.connection { + if binary.connection.is_some() { TcpTransport::start(binary, cx) .await .map(|(transports, tcp)| (transports, Self::Tcp(tcp))) @@ -154,9 +151,9 @@ impl TransportDelegate { pub(crate) async fn start( binary: &DebugAdapterBinary, - cx: &mut AsyncApp, + cx: AsyncApp, ) -> Result<((Receiver, Sender), Self)> { - let (transport_pipes, transport) = Transport::start(binary, cx).await?; + let (transport_pipes, transport) = Transport::start(binary, cx.clone()).await?; let mut this = Self { transport, server_tx: Default::default(), @@ -171,7 +168,7 @@ impl TransportDelegate { async fn start_handlers( &mut self, mut params: TransportPipe, - cx: &mut AsyncApp, + cx: AsyncApp, ) -> Result<(Receiver, Sender)> { let (client_tx, server_rx) = unbounded::(); let (server_tx, client_rx) = unbounded::(); @@ -565,10 +562,7 @@ impl TcpTransport { .port()) } - async fn start( - binary: &DebugAdapterBinary, - cx: &mut AsyncApp, - ) -> Result<(TransportPipe, Self)> { + async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> { let Some(connection_args) = binary.connection.as_ref() else { return Err(anyhow!("No connection arguments provided")); }; @@ -675,7 +669,7 @@ pub struct StdioTransport { } impl StdioTransport { - async fn start(binary: &DebugAdapterBinary, _: &mut AsyncApp) -> Result<(TransportPipe, Self)> { + async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> { let mut command = util::command::new_smol_command(&binary.command); if let Some(cwd) = &binary.cwd { @@ -739,7 +733,7 @@ impl StdioTransport { false } - async fn reconnect(&self, _: &mut AsyncApp) -> Result { + async fn reconnect(&self, _: AsyncApp) -> Result { bail!("Cannot reconnect to adapter") } @@ -816,7 +810,7 @@ impl FakeTransport { .insert(R::COMMAND, Box::new(handler)); } - async fn reconnect(&self, cx: &mut AsyncApp) -> Result { + async fn reconnect(&self, cx: AsyncApp) -> Result { self.start( &DebugAdapterBinary { command: "command".into(), @@ -830,7 +824,7 @@ impl FakeTransport { .await } - async fn start(cx: &mut AsyncApp) -> Result { + async fn start(cx: AsyncApp) -> Result { let this = Self { request_handlers: Arc::new(Mutex::new(HashMap::default())), response_handlers: Arc::new(Mutex::new(HashMap::default())), diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 2fd919e647be74..c9b3bc04b9653a 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -620,23 +620,7 @@ impl DapStore { } }; - let mut client = DebugAdapterClient::new(client_id, binary, &cx); - - client - .start( - { - let dap_store = this.clone(); - move |message, cx| { - dap_store - .update(cx, |_, cx| { - cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) - }) - .log_err(); - } - }, - &mut cx, - ) - .await?; + let mut client = DebugAdapterClient::start(client_id, binary, |_, _| {}, cx).await?; Ok(Arc::new(client)) }) @@ -709,7 +693,7 @@ impl DapStore { cx.spawn(|this, mut cx| async move { let capabilities = client - .request::(dap_client_capabilities(client.adapter_id())) + .request::(dap_client_capabilities(todo!())) .await?; this.update(&mut cx, |store, cx| { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 05194c2e7757d6..e7c97cb28d2312 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1,3 +1,5 @@ +use crate::project_settings::ProjectSettings; + use super::breakpoint_store::BreakpointStore; use super::dap_command::{ self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, NextCommand, @@ -8,15 +10,19 @@ use super::dap_command::{ use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; use collections::{HashMap, IndexMap}; +use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; use dap::client::{DebugAdapterClient, DebugAdapterClientId}; use dap::{ - Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, SteppingGranularity, + messages::Message, Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, + SteppingGranularity, }; use dap_adapters::build_adapter; use futures::{future::Shared, FutureExt}; -use gpui::{App, AppContext, Context, Entity, Task}; +use gpui::{App, AppContext, AsyncApp, Context, Entity, Task}; use rpc::AnyProtoClient; use serde_json::Value; +use settings::Settings; +use std::path::PathBuf; use std::u64; use std::{ any::Any, @@ -154,14 +160,19 @@ struct LocalMode { } impl LocalMode { - fn new( + fn new( + client_id: DebugAdapterClientId, breakpoint_store: Entity, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, - cx: &mut App, - ) -> Task> { - cx.spawn(move |cx| async move { - let adapter = build_adapter(&config.kind).await?; + message_handler: F, + cx: AsyncApp, + ) -> Task> + where + F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, + { + cx.spawn(move |mut cx| async move { + let adapter = build_adapter(&disposition.kind).await?; let binary = cx.update(|cx| { let name = DebugAdapterName::from(adapter.name().as_ref()); @@ -172,7 +183,37 @@ impl LocalMode { .and_then(|s| s.binary.as_ref().map(PathBuf::from)) })?; - todo!() + let (adapter, binary) = match adapter + .get_binary(&delegate, &disposition, binary, &mut cx) + .await + { + Err(error) => { + delegate.update_status( + adapter.name(), + DapStatus::Failed { + error: error.to_string(), + }, + ); + + return Err(error); + } + Ok(mut binary) => { + delegate.update_status(adapter.name(), DapStatus::None); + + let shell_env = delegate.shell_env().await; + let mut envs = binary.envs.unwrap_or_default(); + envs.extend(shell_env); + binary.envs = Some(envs); + + (adapter, binary) + } + }; + + Ok(Self { + client: Arc::new( + DebugAdapterClient::start(client_id, binary, message_handler, cx).await?, + ), + }) }) } } @@ -182,12 +223,6 @@ impl From for Mode { } } -impl From> for Mode { - fn from(client: Arc) -> Self { - Mode::Local(client) - } -} - impl Mode { fn request_local( connection: &Arc, @@ -225,7 +260,7 @@ impl Mode { { match self { Mode::Local(debug_adapter_client) => { - Self::request_local(&debug_adapter_client, request, cx) + Self::request_local(&debug_adapter_client.client, request, cx) } Mode::Remote(remote_connection) => { remote_connection.request_remote(request, client_id, cx) @@ -330,9 +365,10 @@ impl Session { config: DebugAdapterConfig, cx: &mut App, ) -> Task>> { - cx.spawn(move |cx| async move { + cx.spawn(move |mut cx| async move { let adapter = build_adapter(&config.kind).await?; - let mode = LocalMode::new(breakpoints, config, adapter, cx).await?; + let mode = + LocalMode::new(client_id, breakpoints, config, adapter, |_, _| {}, cx).await?; cx.update(|cx| { cx.new(|cx| Self { mode: Mode::Local(mode), @@ -629,7 +665,7 @@ impl Session { pub fn adapter_client(&self) -> Option> { match self.mode { - Mode::Local(ref adapter_client) => Some(adapter_client.clone()), + Mode::Local(ref adapter_client) => Some(adapter_client.client.clone()), Mode::Remote(_) => None, } } From 24e816be38a066138e8183f27b6ee7d9bb8e7551 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:45:30 +0100 Subject: [PATCH 575/650] WIP --- crates/debugger_ui/src/attach_modal.rs | 9 +++---- crates/project/src/debugger/session.rs | 34 ++++++++++++++------------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 5b0685eb360b71..604297970e7bb2 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -139,11 +139,8 @@ impl PickerDelegate for AttachModalDelegate { }; let system = System::new_all(); - let Some(processes) = - client.adapter().attach_processes(&system.processes()) - else { - return Vec::new(); - }; + todo!("client.adapter().attach_processes(&system.processes())"); + let processes: Vec<(&sysinfo::Pid, &sysinfo::Process)> = vec![]; let processes = processes .into_iter() @@ -156,7 +153,7 @@ impl PickerDelegate for AttachModalDelegate { .map(|s| s.to_string_lossy().to_string()) .collect::>(), }) - .collect::>(); + .collect::>(); let _ = this.delegate.candidates.insert(processes.clone()); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index e7c97cb28d2312..c638f62e958f9f 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -366,21 +366,25 @@ impl Session { cx: &mut App, ) -> Task>> { cx.spawn(move |mut cx| async move { - let adapter = build_adapter(&config.kind).await?; - let mode = - LocalMode::new(client_id, breakpoints, config, adapter, |_, _| {}, cx).await?; - cx.update(|cx| { - cx.new(|cx| Self { - mode: Mode::Local(mode), - client_id, - config, - capabilities: unimplemented!(), - ignore_breakpoints: false, - requests: HashMap::default(), - modules: Vec::default(), - loaded_sources: Vec::default(), - threads: IndexMap::default(), - }) + let mode = LocalMode::new( + client_id, + breakpoints, + config.clone(), + delegate, + |_, _| {}, + cx.clone(), + ) + .await?; + cx.new(|_| Self { + mode: Mode::Local(mode), + client_id, + config, + capabilities: unimplemented!(), + ignore_breakpoints: false, + requests: HashMap::default(), + modules: Vec::default(), + loaded_sources: Vec::default(), + threads: IndexMap::default(), }) }) } From c07526f30bd22b5a078a55132b1dbab1f66cf291 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:55:18 +0100 Subject: [PATCH 576/650] Fix focus on inert state --- crates/debugger_ui/src/debugger_panel.rs | 11 +++++------ crates/debugger_ui/src/session/inert.rs | 7 +++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 315a1f815d4a86..a6c6a7bd848223 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -57,7 +57,7 @@ actions!(debug_panel, [ToggleFocus]); pub struct DebugPanel { size: Pixels, pane: Entity, - focus_handle: FocusHandle, + workspace: WeakEntity, _subscriptions: Vec, } @@ -106,7 +106,6 @@ impl DebugPanel { pane, size: px(300.), _subscriptions, - focus_handle: cx.focus_handle(), workspace: workspace.weak_handle(), }; @@ -241,8 +240,8 @@ impl EventEmitter for DebugPanel {} impl EventEmitter for DebugPanel {} impl Focusable for DebugPanel { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.pane.focus_handle(cx) } } @@ -305,10 +304,10 @@ impl Panel for DebugPanel { } impl Render for DebugPanel { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .key_context("DebugPanel") - .track_focus(&self.focus_handle) + .track_focus(&self.focus_handle(cx)) .size_full() .child(self.pane.clone()) .into_any() diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 7cb1fb6093fe11..dbb2bbeef9957d 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,5 +1,5 @@ use gpui::{App, FocusHandle, Focusable}; -use ui::{div, Context, Element, ParentElement, Render, Styled}; +use ui::{div, Context, Element, InteractiveElement, ParentElement, Render, Styled}; pub(super) struct InertState { focus_handle: FocusHandle, @@ -24,6 +24,9 @@ impl Render for InertState { _window: &mut ui::Window, _cx: &mut ui::Context<'_, Self>, ) -> impl ui::IntoElement { - div().size_full().child("No debug sessions") + div() + .track_focus(&self.focus_handle) + .size_full() + .child("No debug sessions") } } From 45cf5b5ab57e02507d252605b0285d7ac9537f07 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:00:40 +0100 Subject: [PATCH 577/650] Add basic tab content --- crates/debugger_ui/src/session.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 2ca5c876a05268..7eb453b2933d7e 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -98,6 +98,14 @@ impl Focusable for DebugSession { impl Item for DebugSession { type Event = DebugPanelItemEvent; + fn tab_content(&self, _: item::TabContentParams, _: &Window, _: &App) -> AnyElement { + let label = match &self.mode { + DebugSessionState::Inert(_) => "New Session", + DebugSessionState::Starting(_) => "Starting", + DebugSessionState::Running(_) => "Running", + }; + div().child(Label::new(label)).into_any_element() + } } impl FollowableItem for DebugSession { From 16dba67b38ed042dad73e42655f0b1748c1c0f44 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:12:47 +0100 Subject: [PATCH 578/650] UI touchups --- crates/debugger_ui/src/debugger_panel.rs | 15 ++++++++++++++- crates/debugger_ui/src/session/inert.rs | 22 +++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a6c6a7bd848223..31099b56210fcc 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -83,7 +83,20 @@ impl DebugPanel { pane.set_can_navigate(true, cx); pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_window, _cx| true); - pane.set_close_pane_if_empty(false, cx); + pane.set_close_pane_if_empty(true, cx); + pane.set_render_tab_bar_buttons(cx, |_, _, _| { + ( + None, + Some( + h_flex() + .child( + IconButton::new("new-debug-session", IconName::Plus) + .icon_size(IconSize::Small), + ) + .into_any_element(), + ), + ) + }); pane.add_item( Box::new(DebugSession::inert(cx)), false, diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index dbb2bbeef9957d..393f6fa57b4376 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,5 +1,8 @@ use gpui::{App, FocusHandle, Focusable}; -use ui::{div, Context, Element, InteractiveElement, ParentElement, Render, Styled}; +use ui::{ + div, h_flex, v_flex, Context, ContextMenu, DropdownMenu, Element, InteractiveElement, + ParentElement, Render, Styled, +}; pub(super) struct InertState { focus_handle: FocusHandle, @@ -21,12 +24,21 @@ impl Focusable for InertState { impl Render for InertState { fn render( &mut self, - _window: &mut ui::Window, - _cx: &mut ui::Context<'_, Self>, + window: &mut ui::Window, + cx: &mut ui::Context<'_, Self>, ) -> impl ui::IntoElement { - div() + v_flex() .track_focus(&self.focus_handle) .size_full() - .child("No debug sessions") + .p_1() + .child(h_flex().child(DropdownMenu::new( + "dap-adapter-picker", + "Select Debug Adapter", + ContextMenu::build(window, cx, |this, _, _| { + this.entry("GDB", None, |_, _| {}) + .entry("Delve", None, |_, _| {}) + .entry("LLDB", None, |_, _| {}) + }), + ))) } } From 6737555f5fa9f508229e5d6f6307711a031c639d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:51:12 +0100 Subject: [PATCH 579/650] Add launch/attach buttons --- crates/debugger_ui/src/session/inert.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 393f6fa57b4376..12738320b4fe3c 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,7 +1,7 @@ use gpui::{App, FocusHandle, Focusable}; use ui::{ - div, h_flex, v_flex, Context, ContextMenu, DropdownMenu, Element, InteractiveElement, - ParentElement, Render, Styled, + div, h_flex, v_flex, Button, ButtonCommon, ButtonStyle, Context, ContextMenu, DropdownMenu, + Element, InteractiveElement, ParentElement, Render, Styled, }; pub(super) struct InertState { @@ -30,6 +30,7 @@ impl Render for InertState { v_flex() .track_focus(&self.focus_handle) .size_full() + .gap_1() .p_1() .child(h_flex().child(DropdownMenu::new( "dap-adapter-picker", @@ -40,5 +41,11 @@ impl Render for InertState { .entry("LLDB", None, |_, _| {}) }), ))) + .child( + h_flex() + .gap_1() + .child(Button::new("launch-dap", "Launch").style(ButtonStyle::Filled)) + .child(Button::new("attach-dap", "Attach").style(ButtonStyle::Filled)), + ) } } From 7797b7021d3378c66f0391a9ec536e6130fb4f9f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:53:26 +0100 Subject: [PATCH 580/650] Wire up click handler --- crates/debugger_ui/src/debugger_panel.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 31099b56210fcc..9a228c579280eb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -84,14 +84,24 @@ impl DebugPanel { pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_window, _cx| true); pane.set_close_pane_if_empty(true, cx); - pane.set_render_tab_bar_buttons(cx, |_, _, _| { + pane.set_render_tab_bar_buttons(cx, |_, _, cx| { ( None, Some( h_flex() .child( IconButton::new("new-debug-session", IconName::Plus) - .icon_size(IconSize::Small), + .icon_size(IconSize::Small) + .on_click(cx.listener(|pane, _, window, cx| { + pane.add_item( + Box::new(DebugSession::inert(cx)), + false, + false, + None, + window, + cx, + ); + })), ) .into_any_element(), ), From edfeea2c28589082636aaffb4507b5f614b49fa1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:58:04 +0100 Subject: [PATCH 581/650] Add a new item to pane if it's empty --- crates/debugger_ui/src/debugger_panel.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9a228c579280eb..23428044c3d7ae 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -324,6 +324,21 @@ impl Panel for DebugPanel { fn activation_priority(&self) -> u32 { 9 } + fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { + if active && self.pane.read(cx).items_len() == 0 { + // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items). + self.pane.update(cx, |this, cx| { + this.add_item( + Box::new(DebugSession::inert(cx)), + false, + false, + None, + window, + cx, + ); + }); + } + } } impl Render for DebugPanel { From 9b8ea488838e95c98d4794cfa8f805e51816e745 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:02:45 +0100 Subject: [PATCH 582/650] Split out local part from DapCommand --- crates/project/src/debugger/dap_command.rs | 556 +++++++++++---------- crates/project/src/debugger/dap_store.rs | 2 +- crates/project/src/debugger/session.rs | 41 +- 3 files changed, 323 insertions(+), 276 deletions(-) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index ce34faf8751671..6139a098ffed6a 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -10,16 +10,25 @@ use dap::{ }; use rpc::proto; use util::ResultExt; - -pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { +pub(crate) trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; - type ProtoRequest: 'static + Send + proto::RequestMessage; fn is_supported(_capabilities: &Capabilities) -> bool { true } + fn to_dap(&self) -> ::Arguments; + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result; +} + +pub(crate) trait DapCommand: LocalDapCommand { + type ProtoRequest: 'static + Send + proto::RequestMessage; + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId; fn from_proto(request: &Self::ProtoRequest) -> Self; @@ -39,24 +48,31 @@ pub(crate) trait DapCommand: 'static + Send + Sync + std::fmt::Debug { &self, message: ::Response, ) -> Result; +} - fn to_dap(&self) -> ::Arguments; +impl LocalDapCommand for Arc { + type Response = T::Response; + type DapRequest = T::DapRequest; + + fn is_supported(capabilities: &Capabilities) -> bool { + T::is_supported(capabilities) + } + + fn to_dap(&self) -> ::Arguments { + T::to_dap(self) + } fn response_from_dap( &self, message: ::Response, - ) -> Result; + ) -> Result { + T::response_from_dap(self, message) + } } impl DapCommand for Arc { - type Response = T::Response; - type DapRequest = T::DapRequest; type ProtoRequest = T::ProtoRequest; - fn is_supported(capabilities: &Capabilities) -> bool { - T::is_supported(capabilities) - } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { T::client_id_from_proto(request) } @@ -86,17 +102,6 @@ impl DapCommand for Arc { ) -> Result { T::response_from_proto(self, message) } - - fn to_dap(&self) -> ::Arguments { - T::to_dap(self) - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - T::response_from_dap(self, message) - } } #[derive(Debug, Hash, PartialEq, Eq)] @@ -130,9 +135,26 @@ pub(crate) struct NextCommand { pub inner: StepCommand, } -impl DapCommand for NextCommand { +impl LocalDapCommand for NextCommand { type Response = ::Response; type DapRequest = Next; + + fn to_dap(&self) -> ::Arguments { + NextArguments { + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity, + } + } + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +impl DapCommand for NextCommand { type ProtoRequest = proto::DapNextRequest; fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { @@ -166,10 +188,28 @@ impl DapCommand for NextCommand { } } + fn response_from_proto( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) struct StepInCommand { + pub inner: StepCommand, +} + +impl LocalDapCommand for StepInCommand { + type Response = ::Response; + type DapRequest = dap::requests::StepIn; + fn to_dap(&self) -> ::Arguments { - NextArguments { + StepInArguments { thread_id: self.inner.thread_id, single_thread: self.inner.single_thread, + target_id: None, granularity: self.inner.granularity, } } @@ -180,23 +220,9 @@ impl DapCommand for NextCommand { ) -> Result { Ok(()) } - - fn response_from_proto( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct StepInCommand { - pub inner: StepCommand, } impl DapCommand for StepInCommand { - type Response = ::Response; - type DapRequest = dap::requests::StepIn; type ProtoRequest = proto::DapStepInRequest; fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { @@ -237,11 +263,27 @@ impl DapCommand for StepInCommand { } } + fn response_from_proto( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) struct StepOutCommand { + pub inner: StepCommand, +} + +impl LocalDapCommand for StepOutCommand { + type Response = ::Response; + type DapRequest = dap::requests::StepOut; + fn to_dap(&self) -> ::Arguments { - StepInArguments { + StepOutArguments { thread_id: self.inner.thread_id, single_thread: self.inner.single_thread, - target_id: None, granularity: self.inner.granularity, } } @@ -252,23 +294,9 @@ impl DapCommand for StepInCommand { ) -> Result { Ok(()) } - - fn response_from_proto( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct StepOutCommand { - pub inner: StepCommand, } impl DapCommand for StepOutCommand { - type Response = ::Response; - type DapRequest = dap::requests::StepOut; type ProtoRequest = proto::DapStepOutRequest; fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { @@ -308,21 +336,6 @@ impl DapCommand for StepOutCommand { } } - fn to_dap(&self) -> ::Arguments { - StepOutArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } - fn response_from_proto( &self, _message: ::Response, @@ -335,16 +348,33 @@ impl DapCommand for StepOutCommand { pub(crate) struct StepBackCommand { pub inner: StepCommand, } - -impl DapCommand for StepBackCommand { +impl LocalDapCommand for StepBackCommand { type Response = ::Response; type DapRequest = dap::requests::StepBack; - type ProtoRequest = proto::DapStepBackRequest; fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_step_back.unwrap_or_default() } + fn to_dap(&self) -> ::Arguments { + dap::StepBackArguments { + thread_id: self.inner.thread_id, + single_thread: self.inner.single_thread, + granularity: self.inner.granularity, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +impl DapCommand for StepBackCommand { + type ProtoRequest = proto::DapStepBackRequest; + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -382,21 +412,6 @@ impl DapCommand for StepBackCommand { } } - fn to_dap(&self) -> ::Arguments { - dap::StepBackArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } - fn response_from_proto( &self, _message: ::Response, @@ -410,9 +425,23 @@ pub(crate) struct ContinueCommand { pub args: ContinueArguments, } -impl DapCommand for ContinueCommand { +impl LocalDapCommand for ContinueCommand { type Response = ::Response; type DapRequest = Continue; + + fn to_dap(&self) -> ::Arguments { + self.args.clone() + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message) + } +} + +impl DapCommand for ContinueCommand { type ProtoRequest = proto::DapContinueRequest; fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { @@ -441,17 +470,6 @@ impl DapCommand for ContinueCommand { } } - fn to_dap(&self) -> ::Arguments { - self.args.clone() - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } - fn response_from_proto( &self, message: ::Response, @@ -477,9 +495,24 @@ pub(crate) struct PauseCommand { pub thread_id: u64, } -impl DapCommand for PauseCommand { +impl LocalDapCommand for PauseCommand { type Response = ::Response; type DapRequest = dap::requests::Pause; + fn to_dap(&self) -> ::Arguments { + dap::PauseArguments { + thread_id: self.thread_id, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +impl DapCommand for PauseCommand { type ProtoRequest = proto::DapPauseRequest; fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { @@ -511,19 +544,6 @@ impl DapCommand for PauseCommand { proto::Ack {} } - fn to_dap(&self) -> ::Arguments { - dap::PauseArguments { - thread_id: self.thread_id, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } - fn response_from_proto( &self, _message: ::Response, @@ -539,9 +559,27 @@ pub(crate) struct DisconnectCommand { pub suspend_debuggee: Option, } -impl DapCommand for DisconnectCommand { +impl LocalDapCommand for DisconnectCommand { type Response = ::Response; type DapRequest = dap::requests::Disconnect; + + fn to_dap(&self) -> ::Arguments { + dap::DisconnectArguments { + restart: self.restart, + terminate_debuggee: self.terminate_debuggee, + suspend_debuggee: self.suspend_debuggee, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +impl DapCommand for DisconnectCommand { type ProtoRequest = proto::DapDisconnectRequest; fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { @@ -569,27 +607,12 @@ impl DapCommand for DisconnectCommand { suspend_debuggee: self.suspend_debuggee, } } - - fn response_to_proto( - _debug_client_id: DebugAdapterClientId, - _message: Self::Response, - ) -> ::Response { - proto::Ack {} - } - - fn to_dap(&self) -> ::Arguments { - dap::DisconnectArguments { - restart: self.restart, - terminate_debuggee: self.terminate_debuggee, - suspend_debuggee: self.suspend_debuggee, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) + + fn response_to_proto( + _debug_client_id: DebugAdapterClientId, + _message: Self::Response, + ) -> ::Response { + proto::Ack {} } fn response_from_proto( @@ -605,10 +628,9 @@ pub(crate) struct TerminateThreadsCommand { pub thread_ids: Option>, } -impl DapCommand for TerminateThreadsCommand { +impl LocalDapCommand for TerminateThreadsCommand { type Response = ::Response; type DapRequest = dap::requests::TerminateThreads; - type ProtoRequest = proto::DapTerminateThreadsRequest; fn is_supported(capabilities: &Capabilities) -> bool { capabilities @@ -616,6 +638,23 @@ impl DapCommand for TerminateThreadsCommand { .unwrap_or_default() } + fn to_dap(&self) -> ::Arguments { + dap::TerminateThreadsArguments { + thread_ids: self.thread_ids.clone(), + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +impl DapCommand for TerminateThreadsCommand { + type ProtoRequest = proto::DapTerminateThreadsRequest; + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -649,19 +688,6 @@ impl DapCommand for TerminateThreadsCommand { proto::Ack {} } - fn to_dap(&self) -> ::Arguments { - dap::TerminateThreadsArguments { - thread_ids: self.thread_ids.clone(), - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } - fn response_from_proto( &self, _message: ::Response, @@ -675,14 +701,29 @@ pub(crate) struct TerminateCommand { pub restart: Option, } -impl DapCommand for TerminateCommand { +impl LocalDapCommand for TerminateCommand { type Response = ::Response; type DapRequest = dap::requests::Terminate; - type ProtoRequest = proto::DapTerminateRequest; fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_terminate_request.unwrap_or_default() } + fn to_dap(&self) -> ::Arguments { + dap::TerminateArguments { + restart: self.restart, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +impl DapCommand for TerminateCommand { + type ProtoRequest = proto::DapTerminateRequest; fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) @@ -713,19 +754,6 @@ impl DapCommand for TerminateCommand { proto::Ack {} } - fn to_dap(&self) -> ::Arguments { - dap::TerminateArguments { - restart: self.restart, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } - fn response_from_proto( &self, _message: ::Response, @@ -739,15 +767,31 @@ pub(crate) struct RestartCommand { pub raw: serde_json::Value, } -impl DapCommand for RestartCommand { +impl LocalDapCommand for RestartCommand { type Response = ::Response; type DapRequest = dap::requests::Restart; - type ProtoRequest = proto::DapRestartRequest; fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_restart_request.unwrap_or_default() } + fn to_dap(&self) -> ::Arguments { + dap::RestartArguments { + raw: self.raw.clone(), + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +impl DapCommand for RestartCommand { + type ProtoRequest = proto::DapRestartRequest; + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -781,19 +825,6 @@ impl DapCommand for RestartCommand { proto::Ack {} } - fn to_dap(&self) -> ::Arguments { - dap::RestartArguments { - raw: self.raw.clone(), - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } - fn response_from_proto( &self, _message: ::Response, @@ -813,14 +844,9 @@ pub struct VariablesCommand { pub format: Option, } -impl DapCommand for VariablesCommand { +impl LocalDapCommand for VariablesCommand { type Response = Vec; type DapRequest = dap::requests::Variables; - type ProtoRequest = proto::VariablesRequest; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) - } fn to_dap(&self) -> ::Arguments { dap::VariablesArguments { @@ -838,6 +864,14 @@ impl DapCommand for VariablesCommand { ) -> Result { Ok(message.variables) } +} + +impl DapCommand for VariablesCommand { + type ProtoRequest = proto::VariablesRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } fn to_proto( &self, @@ -893,20 +927,12 @@ pub(crate) struct SetVariableValueCommand { pub value: String, pub variables_reference: u64, } - -impl DapCommand for SetVariableValueCommand { +impl LocalDapCommand for SetVariableValueCommand { type Response = SetVariableResponse; type DapRequest = dap::requests::SetVariable; - type ProtoRequest = proto::DapSetVariableValueRequest; - fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_set_variable.unwrap_or_default() } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) - } - fn to_dap(&self) -> ::Arguments { dap::SetVariableArguments { format: None, @@ -915,13 +941,20 @@ impl DapCommand for SetVariableValueCommand { variables_reference: self.variables_reference, } } - fn response_from_dap( &self, message: ::Response, ) -> Result { Ok(message) } +} + +impl DapCommand for SetVariableValueCommand { + type ProtoRequest = proto::DapSetVariableValueRequest; + + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + DebugAdapterClientId::from_proto(request.client_id) + } fn to_proto( &self, @@ -980,15 +1013,31 @@ pub(crate) struct RestartStackFrameCommand { pub stack_frame_id: u64, } -impl DapCommand for RestartStackFrameCommand { +impl LocalDapCommand for RestartStackFrameCommand { type Response = ::Response; type DapRequest = dap::requests::RestartFrame; - type ProtoRequest = proto::DapRestartStackFrameRequest; fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_restart_frame.unwrap_or_default() } + fn to_dap(&self) -> ::Arguments { + dap::RestartFrameArguments { + frame_id: self.stack_frame_id, + } + } + + fn response_from_dap( + &self, + _message: ::Response, + ) -> Result { + Ok(()) + } +} + +impl DapCommand for RestartStackFrameCommand { + type ProtoRequest = proto::DapRestartStackFrameRequest; + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -1018,19 +1067,6 @@ impl DapCommand for RestartStackFrameCommand { proto::Ack {} } - fn to_dap(&self) -> ::Arguments { - dap::RestartFrameArguments { - frame_id: self.stack_frame_id, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } - fn response_from_proto( &self, _message: ::Response, @@ -1042,15 +1078,32 @@ impl DapCommand for RestartStackFrameCommand { #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) struct ModulesCommand; -impl DapCommand for ModulesCommand { +impl LocalDapCommand for ModulesCommand { type Response = Vec; type DapRequest = dap::requests::Modules; - type ProtoRequest = proto::DapModulesRequest; fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_modules_request.unwrap_or_default() } + fn to_dap(&self) -> ::Arguments { + dap::ModulesArguments { + start_module: None, + module_count: None, + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.modules) + } +} + +impl DapCommand for ModulesCommand { + type ProtoRequest = proto::DapModulesRequest; + fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) } @@ -1083,20 +1136,6 @@ impl DapCommand for ModulesCommand { } } - fn to_dap(&self) -> ::Arguments { - dap::ModulesArguments { - start_module: None, - module_count: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.modules) - } - fn response_from_proto( &self, message: ::Response, @@ -1112,16 +1151,28 @@ impl DapCommand for ModulesCommand { #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) struct LoadedSourcesCommand; -impl DapCommand for LoadedSourcesCommand { +impl LocalDapCommand for LoadedSourcesCommand { type Response = Vec; type DapRequest = dap::requests::LoadedSources; - type ProtoRequest = proto::DapLoadedSourcesRequest; - fn is_supported(capabilities: &Capabilities) -> bool { capabilities .supports_loaded_sources_request .unwrap_or_default() } + fn to_dap(&self) -> ::Arguments { + dap::LoadedSourcesArguments {} + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.sources) + } +} + +impl DapCommand for LoadedSourcesCommand { + type ProtoRequest = proto::DapLoadedSourcesRequest; fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { DebugAdapterClientId::from_proto(request.client_id) @@ -1155,17 +1206,6 @@ impl DapCommand for LoadedSourcesCommand { } } - fn to_dap(&self) -> ::Arguments { - dap::LoadedSourcesArguments {} - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.sources) - } - fn response_from_proto( &self, message: ::Response, @@ -1185,10 +1225,9 @@ pub(crate) struct StackTraceCommand { pub levels: Option, } -impl DapCommand for StackTraceCommand { +impl LocalDapCommand for StackTraceCommand { type Response = Vec; type DapRequest = dap::requests::StackTrace; - type ProtoRequest = proto::DapStackTraceRequest; fn to_dap(&self) -> ::Arguments { dap::StackTraceArguments { @@ -1205,6 +1244,10 @@ impl DapCommand for StackTraceCommand { ) -> Result { Ok(message.stack_frames) } +} + +impl DapCommand for StackTraceCommand { + type ProtoRequest = proto::DapStackTraceRequest; fn to_proto( &self, @@ -1259,10 +1302,9 @@ pub(crate) struct ScopesCommand { pub stack_frame_id: u64, } -impl DapCommand for ScopesCommand { +impl LocalDapCommand for ScopesCommand { type Response = Vec; type DapRequest = dap::requests::Scopes; - type ProtoRequest = proto::DapScopesRequest; fn to_dap(&self) -> ::Arguments { dap::ScopesArguments { @@ -1276,6 +1318,10 @@ impl DapCommand for ScopesCommand { ) -> Result { Ok(message.scopes) } +} + +impl DapCommand for ScopesCommand { + type ProtoRequest = proto::DapScopesRequest; fn to_proto( &self, @@ -1318,10 +1364,9 @@ impl DapCommand for ScopesCommand { } } -impl DapCommand for super::session::CompletionsQuery { +impl LocalDapCommand for super::session::CompletionsQuery { type Response = dap::CompletionsResponse; type DapRequest = dap::requests::Completions; - type ProtoRequest = proto::DapCompletionRequest; fn to_dap(&self) -> ::Arguments { dap::CompletionsArguments { @@ -1344,6 +1389,9 @@ impl DapCommand for super::session::CompletionsQuery { .supports_completions_request .unwrap_or_default() } +} +impl DapCommand for super::session::CompletionsQuery { + type ProtoRequest = proto::DapCompletionRequest; fn to_proto( &self, @@ -1401,11 +1449,9 @@ pub(crate) struct EvaluateCommand { pub source: Option, } -impl DapCommand for EvaluateCommand { +impl LocalDapCommand for EvaluateCommand { type Response = dap::EvaluateResponse; type DapRequest = dap::requests::Evaluate; - type ProtoRequest = proto::DapEvaluateRequest; - fn to_dap(&self) -> ::Arguments { dap::EvaluateArguments { expression: self.expression.clone(), @@ -1424,6 +1470,9 @@ impl DapCommand for EvaluateCommand { ) -> Result { Ok(message) } +} +impl DapCommand for EvaluateCommand { + type ProtoRequest = proto::DapEvaluateRequest; fn to_proto( &self, @@ -1488,10 +1537,9 @@ impl DapCommand for EvaluateCommand { #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) struct ThreadsCommand; -impl DapCommand for ThreadsCommand { +impl LocalDapCommand for ThreadsCommand { type Response = Vec; type DapRequest = dap::requests::Threads; - type ProtoRequest = proto::DapThreadsRequest; fn to_dap(&self) -> ::Arguments { () @@ -1503,6 +1551,10 @@ impl DapCommand for ThreadsCommand { ) -> Result { Ok(message.threads) } +} + +impl DapCommand for ThreadsCommand { + type ProtoRequest = proto::DapThreadsRequest; fn to_proto( &self, diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index c9b3bc04b9653a..a5e51de02419f7 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -294,7 +294,7 @@ impl DapStore { ) -> Self { Self { mode: DapStoreMode::Remote(RemoteDapStore { - upstream_client: upstream_client, + upstream_client, upstream_project_id: project_id, event_queue: Some(VecDeque::default()), }), diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index c638f62e958f9f..c5d815c1447fe4 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -136,7 +136,7 @@ impl RemoteConnection { request.response_from_proto(response) }) } - fn request_remote( + fn request( &self, request: R, client_id: DebugAdapterClientId, @@ -209,23 +209,15 @@ impl LocalMode { } }; - Ok(Self { - client: Arc::new( - DebugAdapterClient::start(client_id, binary, message_handler, cx).await?, - ), - }) + let client = + Arc::new(DebugAdapterClient::start(client_id, binary, message_handler, cx).await?); + let this = Self { client }; + + Ok(this) }) } -} -impl From for Mode { - fn from(value: RemoteConnection) -> Self { - Self::Remote(value) - } -} - -impl Mode { - fn request_local( - connection: &Arc, + fn request( + &self, request: R, cx: &mut Context, ) -> Task> @@ -236,7 +228,7 @@ impl Mode { let request = Arc::new(request); let request_clone = request.clone(); - let connection = connection.clone(); + let connection = self.client.clone(); let request_task = cx.background_executor().spawn(async move { let args = request_clone.to_dap(); connection.request::(args).await @@ -247,7 +239,14 @@ impl Mode { response }) } +} +impl From for Mode { + fn from(value: RemoteConnection) -> Self { + Self::Remote(value) + } +} +impl Mode { fn request_dap( &self, client_id: DebugAdapterClientId, @@ -259,12 +258,8 @@ impl Mode { ::Arguments: 'static + Send, { match self { - Mode::Local(debug_adapter_client) => { - Self::request_local(&debug_adapter_client.client, request, cx) - } - Mode::Remote(remote_connection) => { - remote_connection.request_remote(request, client_id, cx) - } + Mode::Local(debug_adapter_client) => debug_adapter_client.request(request, cx), + Mode::Remote(remote_connection) => remote_connection.request(request, client_id, cx), } } } From abda28fb6b511082aa2d2643bd2a3b4b036a9aa8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:04:01 +0100 Subject: [PATCH 583/650] Use LocalDapCommand in bounds --- crates/project/src/debugger/session.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index c5d815c1447fe4..e3f883b5930d3a 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -2,10 +2,10 @@ use crate::project_settings::ProjectSettings; use super::breakpoint_store::BreakpointStore; use super::dap_command::{ - self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, NextCommand, - PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, SetVariableValueCommand, - StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, - TerminateThreadsCommand, VariablesCommand, + self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, LocalDapCommand, + NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, + SetVariableValueCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, + TerminateCommand, TerminateThreadsCommand, VariablesCommand, }; use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; @@ -216,7 +216,8 @@ impl LocalMode { Ok(this) }) } - fn request( + + fn request( &self, request: R, cx: &mut Context, From 28d6e76298687b5aa57d4d7042af7aba9dc56508 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:16:26 +0100 Subject: [PATCH 584/650] Add Initialize command --- crates/project/src/debugger/dap_command.rs | 45 +++++++++++++++++++++- crates/project/src/debugger/session.rs | 42 ++++++++++++-------- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 6139a098ffed6a..412c0644d9127b 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -5,7 +5,8 @@ use dap::{ client::DebugAdapterClientId, proto_conversions::ProtoConversion, requests::{Continue, Next}, - Capabilities, ContinueArguments, NextArguments, SetVariableResponse, StepInArguments, + Capabilities, ContinueArguments, InitializeRequestArguments, + InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, StepInArguments, StepOutArguments, SteppingGranularity, ValueFormat, Variable, VariablesArgumentsFilter, }; use rpc::proto; @@ -1591,3 +1592,45 @@ impl DapCommand for ThreadsCommand { } } } + +#[derive(Clone, Debug, Hash, PartialEq)] +pub(super) struct Initialize { + pub(super) adapter_id: String, +} + +fn dap_client_capabilities(adapter_id: String) -> InitializeRequestArguments { + InitializeRequestArguments { + client_id: Some("zed".to_owned()), + client_name: Some("Zed".to_owned()), + adapter_id, + locale: Some("en-US".to_owned()), + path_format: Some(InitializeRequestArgumentsPathFormat::Path), + supports_variable_type: Some(true), + supports_variable_paging: Some(false), + supports_run_in_terminal_request: Some(true), + supports_memory_references: Some(true), + supports_progress_reporting: Some(false), + supports_invalidated_event: Some(false), + lines_start_at1: Some(true), + columns_start_at1: Some(true), + supports_memory_event: Some(false), + supports_args_can_be_interpreted_by_shell: Some(false), + supports_start_debugging_request: Some(true), + } +} + +impl LocalDapCommand for Initialize { + type Response = Capabilities; + type DapRequest = dap::requests::Initialize; + + fn to_dap(&self) -> ::Arguments { + dap_client_capabilities(self.adapter_id.clone()) + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message) + } +} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index e3f883b5930d3a..b5aa62983f9679 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -2,10 +2,10 @@ use crate::project_settings::ProjectSettings; use super::breakpoint_store::BreakpointStore; use super::dap_command::{ - self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, LocalDapCommand, - NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, - SetVariableValueCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, - TerminateCommand, TerminateThreadsCommand, VariablesCommand, + self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, Initialize, + LocalDapCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, + ScopesCommand, SetVariableValueCommand, StepBackCommand, StepCommand, StepInCommand, + StepOutCommand, TerminateCommand, TerminateThreadsCommand, VariablesCommand, }; use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; @@ -18,7 +18,7 @@ use dap::{ }; use dap_adapters::build_adapter; use futures::{future::Shared, FutureExt}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Task}; +use gpui::{App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Task}; use rpc::AnyProtoClient; use serde_json::Value; use settings::Settings; @@ -167,7 +167,7 @@ impl LocalMode { delegate: DapAdapterDelegate, message_handler: F, cx: AsyncApp, - ) -> Task> + ) -> Task> where F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, { @@ -209,18 +209,26 @@ impl LocalMode { } }; - let client = - Arc::new(DebugAdapterClient::start(client_id, binary, message_handler, cx).await?); + let client = Arc::new( + DebugAdapterClient::start(client_id, binary, message_handler, cx.clone()).await?, + ); let this = Self { client }; - - Ok(this) + let capabilities = this + .request( + Initialize { + adapter_id: "zed-dap-this-value-needs-changing".to_owned(), + }, + cx.background_executor().clone(), + ) + .await?; + Ok((this, capabilities)) }) } fn request( &self, request: R, - cx: &mut Context, + executor: BackgroundExecutor, ) -> Task> where ::Response: 'static, @@ -230,12 +238,12 @@ impl LocalMode { let request_clone = request.clone(); let connection = self.client.clone(); - let request_task = cx.background_executor().spawn(async move { + let request_task = executor.spawn(async move { let args = request_clone.to_dap(); connection.request::(args).await }); - cx.background_executor().spawn(async move { + executor.spawn(async move { let response = request.response_from_dap(request_task.await?); response }) @@ -259,7 +267,9 @@ impl Mode { ::Arguments: 'static + Send, { match self { - Mode::Local(debug_adapter_client) => debug_adapter_client.request(request, cx), + Mode::Local(debug_adapter_client) => { + debug_adapter_client.request(request, cx.background_executor().clone()) + } Mode::Remote(remote_connection) => remote_connection.request(request, client_id, cx), } } @@ -362,7 +372,7 @@ impl Session { cx: &mut App, ) -> Task>> { cx.spawn(move |mut cx| async move { - let mode = LocalMode::new( + let (mode, capabilities) = LocalMode::new( client_id, breakpoints, config.clone(), @@ -375,7 +385,7 @@ impl Session { mode: Mode::Local(mode), client_id, config, - capabilities: unimplemented!(), + capabilities, ignore_breakpoints: false, requests: HashMap::default(), modules: Vec::default(), From 05a35375a4370856f4dc879c2baec3cd0d66949a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:51:43 +0100 Subject: [PATCH 585/650] cleanups --- crates/project/src/debugger/dap_store.rs | 45 ------------------------ crates/project/src/debugger/session.rs | 4 +-- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index a5e51de02419f7..1b34bd824a109d 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -213,26 +213,6 @@ pub struct DapStore { impl EventEmitter for DapStore {} -fn dap_client_capabilities(adapter_id: String) -> InitializeRequestArguments { - InitializeRequestArguments { - client_id: Some("zed".to_owned()), - client_name: Some("Zed".to_owned()), - adapter_id, - locale: Some("en-US".to_owned()), - path_format: Some(InitializeRequestArgumentsPathFormat::Path), - supports_variable_type: Some(true), - supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(true), - supports_memory_references: Some(true), - supports_progress_reporting: Some(false), - supports_invalidated_event: Some(false), - lines_start_at1: Some(true), - columns_start_at1: Some(true), - supports_memory_event: Some(false), - supports_args_can_be_interpreted_by_shell: Some(false), - supports_start_debugging_request: Some(true), - } -} impl DapStore { pub fn init(client: &AnyProtoClient) { client.add_entity_message_handler(Self::handle_remove_active_debug_line); @@ -677,31 +657,6 @@ impl DapStore { }) } - pub fn initialize( - &mut self, - client_id: DebugAdapterClientId, - cx: &mut Context, - ) -> Task> { - let Some(client) = self - .client_by_id(client_id) - .and_then(|client| client.read(cx).adapter_client()) - else { - return Task::ready(Err( - anyhow!("Could not find debug client: {:?}", client_id,), - )); - }; - - cx.spawn(|this, mut cx| async move { - let capabilities = client - .request::(dap_client_capabilities(todo!())) - .await?; - - this.update(&mut cx, |store, cx| { - store.update_capabilities_for_client(client_id, &capabilities, cx); - }) - }) - } - pub fn configuration_done( &self, client_id: DebugAdapterClientId, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index b5aa62983f9679..6ccd8816ba1e98 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -183,7 +183,7 @@ impl LocalMode { .and_then(|s| s.binary.as_ref().map(PathBuf::from)) })?; - let (adapter, binary) = match adapter + let binary = match adapter .get_binary(&delegate, &disposition, binary, &mut cx) .await { @@ -205,7 +205,7 @@ impl LocalMode { envs.extend(shell_env); binary.envs = Some(envs); - (adapter, binary) + binary } }; From bd27f879e233c6178da3f6c61626eaaf5dfb0f11 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:56:57 +0100 Subject: [PATCH 586/650] Rename DebugAdapterClientId to SessionId --- crates/dap/src/client.rs | 18 +- crates/debugger_tools/src/dap_log.rs | 42 ++-- crates/debugger_ui/src/attach_modal.rs | 16 +- crates/debugger_ui/src/debugger_panel.rs | 28 +-- crates/debugger_ui/src/session.rs | 7 +- crates/debugger_ui/src/session/running.rs | 10 +- .../src/session/running/console.rs | 6 +- .../src/session/running/loaded_source_list.rs | 6 +- .../src/session/running/module_list.rs | 6 +- .../src/session/running/stack_frame_list.rs | 2 +- .../src/session/running/variable_list.rs | 7 +- .../debugger_ui/src/tests/debugger_panel.rs | 4 +- crates/project/src/debugger/dap_command.rs | 210 ++++++++---------- crates/project/src/debugger/dap_store.rs | 64 +++--- crates/project/src/debugger/session.rs | 18 +- crates/project/src/project.rs | 16 +- 16 files changed, 206 insertions(+), 254 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index c4b89f9a3cc92b..4cf340c2d46df2 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -27,9 +27,9 @@ const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] -pub struct DebugAdapterClientId(pub u32); +pub struct SessionId(pub u32); -impl DebugAdapterClientId { +impl SessionId { pub fn from_proto(client_id: u64) -> Self { Self(client_id as u32) } @@ -41,7 +41,7 @@ impl DebugAdapterClientId { /// Represents a connection to the debug adapter process, either via stdout/stdin or a socket. pub struct DebugAdapterClient { - id: DebugAdapterClientId, + id: SessionId, sequence_count: AtomicU64, binary: DebugAdapterBinary, executor: BackgroundExecutor, @@ -50,7 +50,7 @@ pub struct DebugAdapterClient { impl DebugAdapterClient { pub async fn start( - id: DebugAdapterClientId, + id: SessionId, binary: DebugAdapterBinary, message_handler: F, cx: AsyncApp, @@ -93,7 +93,7 @@ impl DebugAdapterClient { } async fn handle_receive_messages( - client_id: DebugAdapterClientId, + client_id: SessionId, server_rx: Receiver, client_tx: Sender, mut event_handler: F, @@ -193,7 +193,7 @@ impl DebugAdapterClient { self.transport_delegate.send_message(message).await } - pub fn id(&self) -> DebugAdapterClientId { + pub fn id(&self) -> SessionId { self.id } @@ -296,7 +296,7 @@ mod tests { init_test(cx); let mut client = DebugAdapterClient::new( - crate::client::DebugAdapterClientId(1), + crate::client::SessionId(1), DebugAdapterBinary { command: "command".into(), arguments: Default::default(), @@ -373,7 +373,7 @@ mod tests { let called_event_handler = Arc::new(AtomicBool::new(false)); let mut client = DebugAdapterClient::new( - crate::client::DebugAdapterClientId(1), + crate::client::SessionId(1), DebugAdapterBinary { command: "command".into(), arguments: Default::default(), @@ -432,7 +432,7 @@ mod tests { let called_event_handler = Arc::new(AtomicBool::new(false)); let mut client = DebugAdapterClient::new( - crate::client::DebugAdapterClientId(1), + crate::client::SessionId(1), DebugAdapterBinary { command: "command".into(), arguments: Default::default(), diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 241b6d71235f07..115f1a5f52dab3 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -1,5 +1,5 @@ use dap::{ - client::{DebugAdapterClient, DebugAdapterClientId}, + client::{DebugAdapterClient, SessionId}, debugger_settings::DebuggerSettings, transport::{IoKind, LogKind}, }; @@ -32,16 +32,16 @@ struct DapLogView { focus_handle: FocusHandle, log_store: Entity, editor_subscriptions: Vec, - current_view: Option<(DebugAdapterClientId, LogKind)>, + current_view: Option<(SessionId, LogKind)>, project: Entity, _subscriptions: Vec, } struct LogStore { projects: HashMap, ProjectState>, - debug_clients: HashMap, - rpc_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, - adapter_log_tx: UnboundedSender<(DebugAdapterClientId, IoKind, String)>, + debug_clients: HashMap, + rpc_tx: UnboundedSender<(SessionId, IoKind, String)>, + adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>, } struct ProjectState { @@ -98,7 +98,7 @@ impl DebugAdapterState { impl LogStore { fn new(cx: &Context) -> Self { - let (rpc_tx, mut rpc_rx) = unbounded::<(DebugAdapterClientId, IoKind, String)>(); + let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>(); cx.spawn(|this, mut cx| async move { while let Some((client_id, io_kind, message)) = rpc_rx.next().await { if let Some(this) = this.upgrade() { @@ -114,7 +114,7 @@ impl LogStore { .detach_and_log_err(cx); let (adapter_log_tx, mut adapter_log_rx) = - unbounded::<(DebugAdapterClientId, IoKind, String)>(); + unbounded::<(SessionId, IoKind, String)>(); cx.spawn(|this, mut cx| async move { while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await { if let Some(this) = this.upgrade() { @@ -138,7 +138,7 @@ impl LogStore { fn on_rpc_log( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, io_kind: IoKind, message: &str, cx: &mut Context, @@ -148,7 +148,7 @@ impl LogStore { fn on_adapter_log( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, io_kind: IoKind, message: &str, cx: &mut Context, @@ -191,14 +191,14 @@ impl LogStore { fn get_debug_adapter_state( &mut self, - id: DebugAdapterClientId, + id: SessionId, ) -> Option<&mut DebugAdapterState> { self.debug_clients.get_mut(&id) } fn add_debug_client_message( &mut self, - id: DebugAdapterClientId, + id: SessionId, io_kind: IoKind, message: String, cx: &mut Context, @@ -230,7 +230,7 @@ impl LogStore { fn add_debug_client_log( &mut self, - id: DebugAdapterClientId, + id: SessionId, io_kind: IoKind, message: String, cx: &mut Context, @@ -260,7 +260,7 @@ impl LogStore { fn add_debug_client_entry( log_lines: &mut VecDeque, - id: DebugAdapterClientId, + id: SessionId, message: String, kind: LogKind, cx: &mut Context, @@ -289,7 +289,7 @@ impl LogStore { fn add_debug_client( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, client: Entity, cx: &App, ) -> Option<&mut DebugAdapterState> { @@ -323,21 +323,21 @@ impl LogStore { Some(client_state) } - fn remove_debug_client(&mut self, client_id: DebugAdapterClientId, cx: &mut Context) { + fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context) { self.debug_clients.remove(&client_id); cx.notify(); } fn log_messages_for_client( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, ) -> Option<&mut VecDeque> { Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) } fn rpc_messages_for_client( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, ) -> Option<&mut VecDeque> { Some( &mut self @@ -585,7 +585,7 @@ impl DapLogView { fn show_rpc_trace_for_server( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, window: &mut Window, cx: &mut Context, ) { @@ -627,7 +627,7 @@ impl DapLogView { fn show_log_messages_for_adapter( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, window: &mut Window, cx: &mut Context, ) { @@ -668,7 +668,7 @@ fn log_contents(lines: &VecDeque) -> String { #[derive(Clone, PartialEq)] pub(crate) struct DapMenuItem { - pub client_id: DebugAdapterClientId, + pub client_id: SessionId, pub client_name: String, pub has_adapter_logs: bool, pub selected_entry: LogKind, @@ -834,7 +834,7 @@ impl Focusable for DapLogView { pub enum Event { NewLogEntry { - id: DebugAdapterClientId, + id: SessionId, entry: String, kind: LogKind, }, diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 604297970e7bb2..8cff2573b95e7c 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -1,4 +1,4 @@ -use dap::client::DebugAdapterClientId; +use dap::client::SessionId; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::Subscription; use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render}; @@ -20,19 +20,15 @@ struct Candidate { pub(crate) struct AttachModalDelegate { selected_index: usize, matches: Vec, - session_id: DebugAdapterClientId, + session_id: SessionId, placeholder_text: Arc, dap_store: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, candidates: Option>, } impl AttachModalDelegate { - pub fn new( - session_id: DebugAdapterClientId, - client_id: DebugAdapterClientId, - dap_store: Entity, - ) -> Self { + pub fn new(session_id: SessionId, client_id: SessionId, dap_store: Entity) -> Self { Self { client_id, dap_store, @@ -52,8 +48,8 @@ pub(crate) struct AttachModal { impl AttachModal { pub fn new( - session_id: &DebugAdapterClientId, - client_id: DebugAdapterClientId, + session_id: &SessionId, + client_id: SessionId, dap_store: Entity, window: &mut Window, cx: &mut Context, diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 23428044c3d7ae..52f5d6b834b8df 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -3,7 +3,7 @@ use anyhow::Result; use collections::{BTreeMap, HashMap}; use command_palette_hooks::CommandPaletteFilter; use dap::{ - client::DebugAdapterClientId, + client::SessionId, debugger_settings::DebuggerSettings, messages::{Events, Message}, requests::{Request, RunInTerminal, StartDebugging}, @@ -37,20 +37,20 @@ use workspace::{ }; pub enum DebugPanelEvent { - Exited(DebugAdapterClientId), - Terminated(DebugAdapterClientId), + Exited(SessionId), + Terminated(SessionId), Stopped { - client_id: DebugAdapterClientId, + client_id: SessionId, event: StoppedEvent, go_to_stack_frame: bool, }, - Thread((DebugAdapterClientId, ThreadEvent)), - Continued((DebugAdapterClientId, ContinuedEvent)), - Output((DebugAdapterClientId, OutputEvent)), - Module((DebugAdapterClientId, ModuleEvent)), - LoadedSource((DebugAdapterClientId, LoadedSourceEvent)), - ClientShutdown(DebugAdapterClientId), - CapabilitiesChanged(DebugAdapterClientId), + Thread((SessionId, ThreadEvent)), + Continued((SessionId, ContinuedEvent)), + Output((SessionId, OutputEvent)), + Module((SessionId, ModuleEvent)), + LoadedSource((SessionId, LoadedSourceEvent)), + ClientShutdown(SessionId), + CapabilitiesChanged(SessionId), } actions!(debug_panel, [ToggleFocus]); @@ -189,7 +189,7 @@ impl DebugPanel { } #[cfg(any(test, feature = "test-support"))] - pub fn message_queue(&self) -> &HashMap> { + pub fn message_queue(&self) -> &HashMap> { &self.message_queue } @@ -207,7 +207,7 @@ impl DebugPanel { pub fn debug_panel_items_by_client( &self, - client_id: &DebugAdapterClientId, + client_id: &SessionId, cx: &Context, ) -> Vec> { self.pane @@ -221,7 +221,7 @@ impl DebugPanel { pub fn debug_panel_item_by_client( &self, - client_id: DebugAdapterClientId, + client_id: SessionId, cx: &mut Context, ) -> Option> { self.pane diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 7eb453b2933d7e..5bb0d3f76e5593 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -5,9 +5,8 @@ mod starting; use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; use dap::{ - client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, - ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, - ThreadEvent, + client::SessionId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, + LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, ThreadEvent, }; use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, @@ -76,7 +75,7 @@ impl DebugSession { mode: DebugSessionState::Inert(cx.new(|cx| InertState::new(cx))), }) } - pub(crate) fn session_id(&self, cx: &App) -> Option { + pub(crate) fn session_id(&self, cx: &App) -> Option { match &self.mode { DebugSessionState::Inert(_) => None, DebugSessionState::Starting(_entity) => unimplemented!(), diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index e3b7dd924875db..e08321365295e7 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -5,9 +5,7 @@ mod stack_frame_list; mod variable_list; use console::Console; -use dap::{ - client::DebugAdapterClientId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, -}; +use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent}; use gpui::{ AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; @@ -43,7 +41,7 @@ pub struct RunningState { module_list: Entity, active_thread_item: ThreadItem, _workspace: WeakEntity, - client_id: DebugAdapterClientId, + client_id: SessionId, variable_list: Entity, _subscriptions: Vec, stack_frame_list: Entity, @@ -321,7 +319,7 @@ impl RunningState { #[allow(clippy::too_many_arguments)] pub fn new( session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, thread_id: ThreadId, debug_panel: &Entity, workspace: WeakEntity, @@ -426,7 +424,7 @@ impl RunningState { &self.session } - pub fn client_id(&self) -> DebugAdapterClientId { + pub fn client_id(&self) -> SessionId { self.client_id } diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index ca5f5a27f0d1a5..e06745bbaefa2c 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -3,7 +3,7 @@ use super::{ variable_list::VariableList, }; use anyhow::anyhow; -use dap::{client::DebugAdapterClientId, OutputEvent, OutputEventGroup}; +use dap::{client::SessionId, OutputEvent, OutputEventGroup}; use editor::{ display_map::{Crease, CreaseId}, Anchor, CompletionProvider, Editor, EditorElement, EditorStyle, FoldPlaceholder, @@ -34,7 +34,7 @@ pub struct Console { console: Entity, query_bar: Entity, session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, _subscriptions: Vec, variable_list: Entity, stack_frame_list: Entity, @@ -43,7 +43,7 @@ pub struct Console { impl Console { pub fn new( session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, stack_frame_list: Entity, variable_list: Entity, window: &mut Window, diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index 0a64f6bed77229..589e1f2653d66e 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -1,4 +1,4 @@ -use dap::client::DebugAdapterClientId; +use dap::client::SessionId; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; use project::debugger::session::Session; use ui::prelude::*; @@ -9,13 +9,13 @@ pub struct LoadedSourceList { focus_handle: FocusHandle, _subscription: Subscription, session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, } impl LoadedSourceList { pub fn new( session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, cx: &mut Context, ) -> Self { let weak_entity = cx.weak_entity(); diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 8ba20bac570572..198fb0454f4e05 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -1,4 +1,4 @@ -use dap::{client::DebugAdapterClientId, ModuleEvent}; +use dap::{client::SessionId, ModuleEvent}; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; use project::debugger::session::Session; use ui::prelude::*; @@ -8,13 +8,13 @@ pub struct ModuleList { focus_handle: FocusHandle, _subscription: Subscription, session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, } impl ModuleList { pub fn new( session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, cx: &mut Context, ) -> Self { let weak_entity = cx.weak_entity(); diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 1c727298901332..790009bc7d69d5 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -1,7 +1,7 @@ use std::path::Path; use anyhow::{anyhow, Result}; -use dap::client::DebugAdapterClientId; +use dap::client::SessionId; use gpui::{ list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, WeakEntity, diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 45a832de19647d..56c1174abc2336 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -1,8 +1,7 @@ use super::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; use dap::{ - client::DebugAdapterClientId, proto_conversions::ProtoConversion, Scope, ScopePresentationHint, - Variable, + client::SessionId, proto_conversions::ProtoConversion, Scope, ScopePresentationHint, Variable, }; use editor::{actions::SelectAll, Editor, EditorEvent}; use gpui::{ @@ -331,7 +330,7 @@ pub struct VariableList { focus_handle: FocusHandle, open_entries: Vec, session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, _subscriptions: Vec, set_variable_editor: Entity, selection: Option, @@ -347,7 +346,7 @@ pub struct VariableList { impl VariableList { pub fn new( session: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, stack_frame_list: Entity, window: &mut Window, cx: &mut Context, diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 29fedb04f2d25c..ce5afb9ca331ad 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1,6 +1,6 @@ use crate::*; use dap::{ - client::DebugAdapterClientId, + client::SessionId, requests::{ Continue, Disconnect, Initialize, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace, StartDebugging, StepBack, StepIn, StepOut, @@ -759,7 +759,7 @@ async fn test_handle_start_debugging_reverse_request( let second_client = project.update(cx, |_, cx| { session .read(cx) - .client_state(DebugAdapterClientId(1)) + .client_state(SessionId(1)) .unwrap() .read(cx) .adapter_client() diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 412c0644d9127b..e069e370369b16 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use anyhow::{Ok, Result}; use dap::{ - client::DebugAdapterClientId, + client::SessionId, proto_conversions::ProtoConversion, requests::{Continue, Next}, Capabilities, ContinueArguments, InitializeRequestArguments, @@ -30,18 +30,14 @@ pub(crate) trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { pub(crate) trait DapCommand: LocalDapCommand { type ProtoRequest: 'static + Send + proto::RequestMessage; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId; + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId; fn from_proto(request: &Self::ProtoRequest) -> Self; - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest; + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest; fn response_to_proto( - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, message: Self::Response, ) -> ::Response; @@ -74,7 +70,7 @@ impl LocalDapCommand for Arc { impl DapCommand for Arc { type ProtoRequest = T::ProtoRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { T::client_id_from_proto(request) } @@ -82,16 +78,12 @@ impl DapCommand for Arc { Arc::new(T::from_proto(request)) } - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest { + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { T::to_proto(self, debug_client_id, upstream_project_id) } fn response_to_proto( - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, message: Self::Response, ) -> ::Response { T::response_to_proto(debug_client_id, message) @@ -158,8 +150,8 @@ impl LocalDapCommand for NextCommand { impl DapCommand for NextCommand { type ProtoRequest = proto::DapNextRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -169,7 +161,7 @@ impl DapCommand for NextCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -177,7 +169,7 @@ impl DapCommand for NextCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapNextRequest { proto::DapNextRequest { @@ -226,8 +218,8 @@ impl LocalDapCommand for StepInCommand { impl DapCommand for StepInCommand { type ProtoRequest = proto::DapStepInRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -243,7 +235,7 @@ impl DapCommand for StepInCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -251,7 +243,7 @@ impl DapCommand for StepInCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapStepInRequest { proto::DapStepInRequest { @@ -300,8 +292,8 @@ impl LocalDapCommand for StepOutCommand { impl DapCommand for StepOutCommand { type ProtoRequest = proto::DapStepOutRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -317,7 +309,7 @@ impl DapCommand for StepOutCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -325,7 +317,7 @@ impl DapCommand for StepOutCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapStepOutRequest { proto::DapStepOutRequest { @@ -376,8 +368,8 @@ impl LocalDapCommand for StepBackCommand { impl DapCommand for StepBackCommand { type ProtoRequest = proto::DapStepBackRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -393,7 +385,7 @@ impl DapCommand for StepBackCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -401,7 +393,7 @@ impl DapCommand for StepBackCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapStepBackRequest { proto::DapStepBackRequest { @@ -445,13 +437,13 @@ impl LocalDapCommand for ContinueCommand { impl DapCommand for ContinueCommand { type ProtoRequest = proto::DapContinueRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapContinueRequest { proto::DapContinueRequest { @@ -481,7 +473,7 @@ impl DapCommand for ContinueCommand { } fn response_to_proto( - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapContinueResponse { @@ -516,8 +508,8 @@ impl LocalDapCommand for PauseCommand { impl DapCommand for PauseCommand { type ProtoRequest = proto::DapPauseRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -528,7 +520,7 @@ impl DapCommand for PauseCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapPauseRequest { proto::DapPauseRequest { @@ -539,7 +531,7 @@ impl DapCommand for PauseCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -583,8 +575,8 @@ impl LocalDapCommand for DisconnectCommand { impl DapCommand for DisconnectCommand { type ProtoRequest = proto::DapDisconnectRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -597,7 +589,7 @@ impl DapCommand for DisconnectCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapDisconnectRequest { proto::DapDisconnectRequest { @@ -610,7 +602,7 @@ impl DapCommand for DisconnectCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -656,8 +648,8 @@ impl LocalDapCommand for TerminateThreadsCommand { impl DapCommand for TerminateThreadsCommand { type ProtoRequest = proto::DapTerminateThreadsRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -672,7 +664,7 @@ impl DapCommand for TerminateThreadsCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapTerminateThreadsRequest { proto::DapTerminateThreadsRequest { @@ -683,7 +675,7 @@ impl DapCommand for TerminateThreadsCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -726,8 +718,8 @@ impl LocalDapCommand for TerminateCommand { impl DapCommand for TerminateCommand { type ProtoRequest = proto::DapTerminateRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -738,7 +730,7 @@ impl DapCommand for TerminateCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapTerminateRequest { proto::DapTerminateRequest { @@ -749,7 +741,7 @@ impl DapCommand for TerminateCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -793,8 +785,8 @@ impl LocalDapCommand for RestartCommand { impl DapCommand for RestartCommand { type ProtoRequest = proto::DapRestartRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -807,7 +799,7 @@ impl DapCommand for RestartCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapRestartRequest { let raw_args = serde_json::to_vec(&self.raw).log_err().unwrap_or_default(); @@ -820,7 +812,7 @@ impl DapCommand for RestartCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -870,15 +862,11 @@ impl LocalDapCommand for VariablesCommand { impl DapCommand for VariablesCommand { type ProtoRequest = proto::VariablesRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest { + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::VariablesRequest { project_id: upstream_project_id, client_id: debug_client_id.to_proto(), @@ -905,7 +893,7 @@ impl DapCommand for VariablesCommand { } fn response_to_proto( - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapVariables { @@ -953,15 +941,11 @@ impl LocalDapCommand for SetVariableValueCommand { impl DapCommand for SetVariableValueCommand { type ProtoRequest = proto::DapSetVariableValueRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest { + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapSetVariableValueRequest { project_id: upstream_project_id, client_id: debug_client_id.to_proto(), @@ -980,7 +964,7 @@ impl DapCommand for SetVariableValueCommand { } fn response_to_proto( - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapSetVariableValueResponse { @@ -1039,8 +1023,8 @@ impl LocalDapCommand for RestartStackFrameCommand { impl DapCommand for RestartStackFrameCommand { type ProtoRequest = proto::DapRestartStackFrameRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -1051,7 +1035,7 @@ impl DapCommand for RestartStackFrameCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapRestartStackFrameRequest { proto::DapRestartStackFrameRequest { @@ -1062,7 +1046,7 @@ impl DapCommand for RestartStackFrameCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, _message: Self::Response, ) -> ::Response { proto::Ack {} @@ -1105,8 +1089,8 @@ impl LocalDapCommand for ModulesCommand { impl DapCommand for ModulesCommand { type ProtoRequest = proto::DapModulesRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(_request: &Self::ProtoRequest) -> Self { @@ -1115,7 +1099,7 @@ impl DapCommand for ModulesCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapModulesRequest { proto::DapModulesRequest { @@ -1125,7 +1109,7 @@ impl DapCommand for ModulesCommand { } fn response_to_proto( - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapModulesResponse { @@ -1175,8 +1159,8 @@ impl LocalDapCommand for LoadedSourcesCommand { impl DapCommand for LoadedSourcesCommand { type ProtoRequest = proto::DapLoadedSourcesRequest; - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(_request: &Self::ProtoRequest) -> Self { @@ -1185,7 +1169,7 @@ impl DapCommand for LoadedSourcesCommand { fn to_proto( &self, - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, upstream_project_id: u64, ) -> proto::DapLoadedSourcesRequest { proto::DapLoadedSourcesRequest { @@ -1195,7 +1179,7 @@ impl DapCommand for LoadedSourcesCommand { } fn response_to_proto( - debug_client_id: DebugAdapterClientId, + debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapLoadedSourcesResponse { @@ -1250,11 +1234,7 @@ impl LocalDapCommand for StackTraceCommand { impl DapCommand for StackTraceCommand { type ProtoRequest = proto::DapStackTraceRequest; - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest { + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapStackTraceRequest { project_id: upstream_project_id, client_id: debug_client_id.to_proto(), @@ -1272,8 +1252,8 @@ impl DapCommand for StackTraceCommand { } } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn response_from_proto( @@ -1288,7 +1268,7 @@ impl DapCommand for StackTraceCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapStackTraceResponse { @@ -1324,11 +1304,7 @@ impl LocalDapCommand for ScopesCommand { impl DapCommand for ScopesCommand { type ProtoRequest = proto::DapScopesRequest; - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest { + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapScopesRequest { project_id: upstream_project_id, client_id: debug_client_id.to_proto(), @@ -1344,8 +1320,8 @@ impl DapCommand for ScopesCommand { } } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn response_from_proto( @@ -1356,7 +1332,7 @@ impl DapCommand for ScopesCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapScopesResponse { @@ -1394,11 +1370,7 @@ impl LocalDapCommand for super::session::CompletionsQuery { impl DapCommand for super::session::CompletionsQuery { type ProtoRequest = proto::DapCompletionRequest; - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest { + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapCompletionRequest { client_id: debug_client_id.to_proto(), project_id: upstream_project_id, @@ -1409,8 +1381,8 @@ impl DapCommand for super::session::CompletionsQuery { } } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -1432,7 +1404,7 @@ impl DapCommand for super::session::CompletionsQuery { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapCompletionResponse { @@ -1475,11 +1447,7 @@ impl LocalDapCommand for EvaluateCommand { impl DapCommand for EvaluateCommand { type ProtoRequest = proto::DapEvaluateRequest; - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest { + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapEvaluateRequest { client_id: debug_client_id.to_proto(), project_id: upstream_project_id, @@ -1492,8 +1460,8 @@ impl DapCommand for EvaluateCommand { } } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn from_proto(request: &Self::ProtoRequest) -> Self { @@ -1521,7 +1489,7 @@ impl DapCommand for EvaluateCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapEvaluateResponse { @@ -1557,11 +1525,7 @@ impl LocalDapCommand for ThreadsCommand { impl DapCommand for ThreadsCommand { type ProtoRequest = proto::DapThreadsRequest; - fn to_proto( - &self, - debug_client_id: DebugAdapterClientId, - upstream_project_id: u64, - ) -> Self::ProtoRequest { + fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapThreadsRequest { project_id: upstream_project_id, client_id: debug_client_id.to_proto(), @@ -1572,8 +1536,8 @@ impl DapCommand for ThreadsCommand { Self {} } - fn client_id_from_proto(request: &Self::ProtoRequest) -> DebugAdapterClientId { - DebugAdapterClientId::from_proto(request.client_id) + fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { + SessionId::from_proto(request.client_id) } fn response_from_proto( @@ -1584,7 +1548,7 @@ impl DapCommand for ThreadsCommand { } fn response_to_proto( - _debug_client_id: DebugAdapterClientId, + _debug_client_id: SessionId, message: Self::Response, ) -> ::Response { proto::DapThreadsResponse { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 1b34bd824a109d..cb6005ffea6be8 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -18,7 +18,7 @@ use async_trait::async_trait; use collections::HashMap; use dap::{ adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}, - client::{DebugAdapterClient, DebugAdapterClientId}, + client::{DebugAdapterClient, SessionId}, messages::{Message, Response}, requests::{ Attach, Completions, Evaluate, Initialize, Launch, Request as _, RunInTerminal, @@ -59,10 +59,10 @@ use util::{merge_json_value_into, ResultExt as _}; use worktree::Worktree; pub enum DapStoreEvent { - DebugClientStarted(DebugAdapterClientId), - DebugClientShutdown(DebugAdapterClientId), + DebugClientStarted(SessionId), + DebugClientShutdown(SessionId), DebugClientEvent { - client_id: DebugAdapterClientId, + client_id: SessionId, message: Message, }, Notification(String), @@ -89,8 +89,8 @@ pub struct LocalDapStore { } impl LocalDapStore { - fn next_client_id(&self) -> DebugAdapterClientId { - DebugAdapterClientId(self.next_client_id.fetch_add(1, SeqCst)) + fn next_client_id(&self) -> SessionId { + SessionId(self.next_client_id.fetch_add(1, SeqCst)) } pub fn respond_to_start_debugging( &mut self, @@ -207,8 +207,8 @@ pub struct DapStore { mode: DapStoreMode, downstream_client: Option<(AnyProtoClient, u64)>, breakpoint_store: Entity, - active_debug_line: Option<(DebugAdapterClientId, ProjectPath, u32)>, - clients: BTreeMap>, + active_debug_line: Option<(SessionId, ProjectPath, u32)>, + clients: BTreeMap>, } impl EventEmitter for DapStore {} @@ -332,7 +332,7 @@ impl DapStore { pub fn add_remote_client( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, ignore: Option, cx: &mut Context, ) { @@ -355,7 +355,7 @@ impl DapStore { pub fn client_by_id( &self, - client_id: impl Borrow, + client_id: impl Borrow, ) -> Option> { let client_id = client_id.borrow(); let client = self.clients.get(client_id).cloned(); @@ -368,7 +368,7 @@ impl DapStore { pub fn capabilities_by_id( &self, - client_id: impl Borrow, + client_id: impl Borrow, cx: &App, ) -> Option { let client_id = client_id.borrow(); @@ -379,7 +379,7 @@ impl DapStore { pub fn update_capabilities_for_client( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, capabilities: &Capabilities, cx: &mut Context, ) { @@ -402,13 +402,13 @@ impl DapStore { } } - pub fn active_debug_line(&self) -> Option<(DebugAdapterClientId, ProjectPath, u32)> { + pub fn active_debug_line(&self) -> Option<(SessionId, ProjectPath, u32)> { self.active_debug_line.clone() } pub fn set_active_debug_line( &mut self, - client_id: &DebugAdapterClientId, + client_id: &SessionId, project_path: &ProjectPath, row: u32, cx: &mut Context, @@ -420,7 +420,7 @@ impl DapStore { pub fn remove_active_debug_line_for_client( &mut self, - client_id: &DebugAdapterClientId, + client_id: &SessionId, cx: &mut Context, ) { if let Some(active_line) = &self.active_debug_line { @@ -447,7 +447,7 @@ impl DapStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { - let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); + let client_id = SessionId::from_proto(envelope.payload.client_id); this.update(&mut cx, |this, cx| { if let Some(client) = this.client_by_id(&client_id) { @@ -462,7 +462,7 @@ impl DapStore { pub fn set_ignore_breakpoints( &mut self, - client_id: &DebugAdapterClientId, + client_id: &SessionId, ignore: bool, cx: &mut Context, ) { @@ -473,17 +473,13 @@ impl DapStore { } } - pub fn ignore_breakpoints(&self, client_id: &DebugAdapterClientId, cx: &App) -> bool { + pub fn ignore_breakpoints(&self, client_id: &SessionId, cx: &App) -> bool { self.client_by_id(client_id) .map(|client| client.read(cx).breakpoints_enabled()) .unwrap_or_default() } - pub fn toggle_ignore_breakpoints( - &mut self, - client_id: &DebugAdapterClientId, - cx: &mut Context, - ) { + pub fn toggle_ignore_breakpoints(&mut self, client_id: &SessionId, cx: &mut Context) { if let Some(client) = self.client_by_id(client_id) { client.update(cx, |client, _| { client.set_ignore_breakpoints(!client.breakpoints_enabled()); @@ -659,7 +655,7 @@ impl DapStore { pub fn configuration_done( &self, - client_id: DebugAdapterClientId, + client_id: SessionId, cx: &mut Context, ) -> Task> { let Some(client) = self @@ -721,7 +717,7 @@ impl DapStore { pub fn attach( &mut self, - client_id: DebugAdapterClientId, + client_id: SessionId, process_id: u32, cx: &mut Context, ) -> Task> { @@ -765,7 +761,7 @@ impl DapStore { pub fn respond_to_run_in_terminal( &self, - client_id: DebugAdapterClientId, + client_id: SessionId, success: bool, seq: u64, body: Option, @@ -793,7 +789,7 @@ impl DapStore { pub fn evaluate( &self, - client_id: &DebugAdapterClientId, + client_id: &SessionId, stack_frame_id: u64, expression: String, context: EvaluateArgumentsContext, @@ -824,7 +820,7 @@ impl DapStore { pub fn completions( &self, - client_id: &DebugAdapterClientId, + client_id: &SessionId, stack_frame_id: u64, text: String, completion_column: u64, @@ -853,7 +849,7 @@ impl DapStore { #[allow(clippy::too_many_arguments)] pub fn set_variable_value( &self, - client_id: &DebugAdapterClientId, + client_id: &SessionId, stack_frame_id: u64, variables_reference: u64, name: String, @@ -917,7 +913,7 @@ impl DapStore { pub fn shutdown_client( &mut self, - client_id: &DebugAdapterClientId, + client_id: &SessionId, cx: &mut Context, ) -> Task> { let Some(_) = self.as_local_mut() else { @@ -1021,7 +1017,7 @@ impl DapStore { ) -> Result<()> { this.update(&mut cx, |dap_store, cx| { dap_store.update_capabilities_for_client( - DebugAdapterClientId::from_proto(envelope.payload.client_id), + SessionId::from_proto(envelope.payload.client_id), &dap::proto_conversions::capabilities_from_proto(&envelope.payload), cx, ); @@ -1034,7 +1030,7 @@ impl DapStore { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |dap_store, cx| { - let client_id = DebugAdapterClientId::from_proto(envelope.payload.client_id); + let client_id = SessionId::from_proto(envelope.payload.client_id); dap_store.client_by_id(client_id).map(|state| { state.update(cx, |state, cx| { @@ -1061,7 +1057,7 @@ impl DapStore { this.update(&mut cx, |store, cx| { store.active_debug_line = Some(( - DebugAdapterClientId::from_proto(envelope.payload.client_id), + SessionId::from_proto(envelope.payload.client_id), project_path, envelope.payload.row, )); @@ -1086,7 +1082,7 @@ impl DapStore { pub fn send_breakpoints( &self, - client_id: DebugAdapterClientId, + client_id: SessionId, absolute_file_path: Arc, mut breakpoints: Vec, ignore: bool, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 6ccd8816ba1e98..cd606ca0863be9 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -11,7 +11,7 @@ use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; use collections::{HashMap, IndexMap}; use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; -use dap::client::{DebugAdapterClient, DebugAdapterClientId}; +use dap::client::{DebugAdapterClient, SessionId}; use dap::{ messages::Message, Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, SteppingGranularity, @@ -126,7 +126,7 @@ impl RemoteConnection { fn send_proto_client_request( &self, request: R, - client_id: DebugAdapterClientId, + client_id: SessionId, cx: &mut App, ) -> Task> { let message = request.to_proto(client_id, self.upstream_project_id); @@ -139,7 +139,7 @@ impl RemoteConnection { fn request( &self, request: R, - client_id: DebugAdapterClientId, + client_id: SessionId, cx: &mut App, ) -> Task> where @@ -161,7 +161,7 @@ struct LocalMode { impl LocalMode { fn new( - client_id: DebugAdapterClientId, + client_id: SessionId, breakpoint_store: Entity, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, @@ -258,7 +258,7 @@ impl From for Mode { impl Mode { fn request_dap( &self, - client_id: DebugAdapterClientId, + client_id: SessionId, request: R, cx: &mut Context, ) -> Task> @@ -280,7 +280,7 @@ pub struct Session { mode: Mode, config: DebugAdapterConfig, pub(super) capabilities: Capabilities, - client_id: DebugAdapterClientId, + client_id: SessionId, ignore_breakpoints: bool, modules: Vec, loaded_sources: Vec, @@ -366,7 +366,7 @@ impl CompletionsQuery { impl Session { pub(crate) fn local( breakpoints: Entity, - client_id: DebugAdapterClientId, + client_id: SessionId, delegate: DapAdapterDelegate, config: DebugAdapterConfig, cx: &mut App, @@ -396,7 +396,7 @@ impl Session { } pub(crate) fn remote( - client_id: DebugAdapterClientId, + client_id: SessionId, client: AnyProtoClient, upstream_project_id: u64, ignore_breakpoints: bool, @@ -464,7 +464,7 @@ impl Session { fn request_inner( capabilities: &Capabilities, - client_id: DebugAdapterClientId, + client_id: SessionId, mode: &Mode, request: T, process_result: impl FnOnce(&mut Self, &T::Response, &mut Context) + 'static, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 41706b8ada3794..f9f5345d17451c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -39,7 +39,7 @@ use client::{ use clock::ReplicaId; use dap::{ - client::{DebugAdapterClient, DebugAdapterClientId}, + client::{DebugAdapterClient, SessionId}, debugger_settings::DebuggerSettings, messages::Message, DebugAdapterConfig, @@ -270,14 +270,14 @@ pub enum Event { notification_id: SharedString, }, LanguageServerPrompt(LanguageServerPromptRequest), - DebugClientStarted(DebugAdapterClientId), - DebugClientShutdown(DebugAdapterClientId), + DebugClientStarted(SessionId), + DebugClientShutdown(SessionId), ActiveDebugLineChanged, DebugClientEvent { - client_id: DebugAdapterClientId, + client_id: SessionId, message: Message, }, - DebugClientLog(DebugAdapterClientId, String), + DebugClientLog(SessionId, String), LanguageNotFound(Entity), ActiveEntryChanged(Option), ActivateProjectPanel, @@ -1306,7 +1306,7 @@ impl Project { pub fn initial_send_breakpoints( &self, - client_id: DebugAdapterClientId, + client_id: SessionId, cx: &mut Context, ) -> Task<()> { let mut tasks = Vec::new(); @@ -1379,7 +1379,7 @@ impl Project { if let Some((_, _)) = project.dap_store.read(cx).downstream_client() { project .toggle_ignore_breakpoints( - DebugAdapterClientId::from_proto(envelope.payload.client_id), + SessionId::from_proto(envelope.payload.client_id), cx, ) .detach_and_log_err(cx); @@ -1389,7 +1389,7 @@ impl Project { pub fn toggle_ignore_breakpoints( &self, - client_id: DebugAdapterClientId, + client_id: SessionId, cx: &mut Context, ) -> Task> { let tasks = self.dap_store.update(cx, |store, cx| { From b8383bedae2d0b5d163876abbd90963cab1fd80d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 02:07:14 +0100 Subject: [PATCH 587/650] Renames --- crates/dap/src/proto_conversions.rs | 4 +- crates/project/src/debugger/dap_store.rs | 202 +++++++++-------------- crates/project/src/debugger/session.rs | 36 ++-- crates/project/src/project.rs | 37 +++-- crates/proto/proto/zed.proto | 8 +- 5 files changed, 122 insertions(+), 165 deletions(-) diff --git a/crates/dap/src/proto_conversions.rs b/crates/dap/src/proto_conversions.rs index b88ca92c036cb8..41af22cc7dd672 100644 --- a/crates/dap/src/proto_conversions.rs +++ b/crates/dap/src/proto_conversions.rs @@ -356,10 +356,10 @@ pub fn capabilities_from_proto(payload: &SetDebugClientCapabilities) -> Capabili pub fn capabilities_to_proto( capabilities: &Capabilities, project_id: u64, - client_id: u64, + session_id: u64, ) -> SetDebugClientCapabilities { SetDebugClientCapabilities { - client_id, + session_id, project_id, supports_loaded_sources_request: capabilities .supports_loaded_sources_request diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index cb6005ffea6be8..afef6e751ce8f7 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -62,7 +62,7 @@ pub enum DapStoreEvent { DebugClientStarted(SessionId), DebugClientShutdown(SessionId), DebugClientEvent { - client_id: SessionId, + session_id: SessionId, message: Message, }, Notification(String), @@ -81,7 +81,7 @@ pub enum DapStoreMode { pub struct LocalDapStore { fs: Arc, node_runtime: NodeRuntime, - next_client_id: AtomicU32, + next_session_id: AtomicU32, http_client: Arc, environment: Entity, language_registry: Arc, @@ -89,8 +89,8 @@ pub struct LocalDapStore { } impl LocalDapStore { - fn next_client_id(&self) -> SessionId { - SessionId(self.next_client_id.fetch_add(1, SeqCst)) + fn next_session_id(&self) -> SessionId { + SessionId(self.next_session_id.fetch_add(1, SeqCst)) } pub fn respond_to_start_debugging( &mut self, @@ -258,7 +258,7 @@ impl DapStore { node_runtime, toolchain_store, language_registry, - next_client_id: Default::default(), + next_session_id: Default::default(), }), downstream_client: None, active_debug_line: None, @@ -332,16 +332,16 @@ impl DapStore { pub fn add_remote_client( &mut self, - client_id: SessionId, + session_id: SessionId, ignore: Option, cx: &mut Context, ) { if let DapStoreMode::Remote(remote) = &self.mode { self.clients.insert( - client_id, + session_id, cx.new(|_| { debugger::session::Session::remote( - client_id, + session_id, remote.upstream_client.clone(), remote.upstream_project_id, ignore.unwrap_or(false), @@ -355,10 +355,10 @@ impl DapStore { pub fn client_by_id( &self, - client_id: impl Borrow, + session_id: impl Borrow, ) -> Option> { - let client_id = client_id.borrow(); - let client = self.clients.get(client_id).cloned(); + let session_id = session_id.borrow(); + let client = self.clients.get(session_id).cloned(); client } @@ -368,22 +368,22 @@ impl DapStore { pub fn capabilities_by_id( &self, - client_id: impl Borrow, + session_id: impl Borrow, cx: &App, ) -> Option { - let client_id = client_id.borrow(); + let session_id = session_id.borrow(); self.clients - .get(client_id) + .get(session_id) .map(|client| client.read(cx).capabilities.clone()) } pub fn update_capabilities_for_client( &mut self, - client_id: SessionId, + session_id: SessionId, capabilities: &Capabilities, cx: &mut Context, ) { - if let Some(client) = self.client_by_id(client_id) { + if let Some(client) = self.client_by_id(session_id) { client.update(cx, |this, cx| { this.capabilities = this.capabilities.merge(capabilities.clone()); }); @@ -396,7 +396,7 @@ impl DapStore { .send(dap::proto_conversions::capabilities_to_proto( &capabilities, *project_id, - client_id.to_proto(), + session_id.to_proto(), )) .log_err(); } @@ -408,23 +408,23 @@ impl DapStore { pub fn set_active_debug_line( &mut self, - client_id: &SessionId, + session_id: &SessionId, project_path: &ProjectPath, row: u32, cx: &mut Context, ) { - self.active_debug_line = Some((*client_id, project_path.clone(), row)); + self.active_debug_line = Some((*session_id, project_path.clone(), row)); cx.emit(DapStoreEvent::ActiveDebugLineChanged); cx.notify(); } pub fn remove_active_debug_line_for_client( &mut self, - client_id: &SessionId, + session_id: &SessionId, cx: &mut Context, ) { if let Some(active_line) = &self.active_debug_line { - if active_line.0 == *client_id { + if active_line.0 == *session_id { self.active_debug_line.take(); cx.emit(DapStoreEvent::ActiveDebugLineChanged); cx.notify(); @@ -447,10 +447,10 @@ impl DapStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { - let client_id = SessionId::from_proto(envelope.payload.client_id); + let session_id = SessionId::from_proto(envelope.payload.session_id); this.update(&mut cx, |this, cx| { - if let Some(client) = this.client_by_id(&client_id) { + if let Some(client) = this.client_by_id(&session_id) { client.update(cx, |client, cx| { client.set_ignore_breakpoints(envelope.payload.ignore) }); @@ -462,25 +462,25 @@ impl DapStore { pub fn set_ignore_breakpoints( &mut self, - client_id: &SessionId, + session_id: &SessionId, ignore: bool, cx: &mut Context, ) { - if let Some(session) = self.client_by_id(client_id) { + if let Some(session) = self.client_by_id(session_id) { session.update(cx, |session, _| { session.set_ignore_breakpoints(ignore); }); } } - pub fn ignore_breakpoints(&self, client_id: &SessionId, cx: &App) -> bool { - self.client_by_id(client_id) + pub fn ignore_breakpoints(&self, session_id: &SessionId, cx: &App) -> bool { + self.client_by_id(session_id) .map(|client| client.read(cx).breakpoints_enabled()) .unwrap_or_default() } - pub fn toggle_ignore_breakpoints(&mut self, client_id: &SessionId, cx: &mut Context) { - if let Some(client) = self.client_by_id(client_id) { + pub fn toggle_ignore_breakpoints(&mut self, session_id: &SessionId, cx: &mut Context) { + if let Some(client) = self.client_by_id(session_id) { client.update(cx, |client, _| { client.set_ignore_breakpoints(!client.breakpoints_enabled()); }); @@ -500,10 +500,10 @@ impl DapStore { // ))); // } - // let client_id = self.as_local().unwrap().next_client_id(); + // let session_id = self.as_local().unwrap().next_session_id(); // cx.spawn(|dap_store, mut cx| async move { - // let mut client = DebugAdapterClient::new(client_id, adapter, binary, &cx); + // let mut client = DebugAdapterClient::new(session_id, adapter, binary, &cx); // client // .reconnect( @@ -512,7 +512,7 @@ impl DapStore { // move |message, cx| { // dap_store // .update(cx, |_, cx| { - // cx.emit(DapStoreEvent::DebugClientEvent { client_id, message }) + // cx.emit(DapStoreEvent::DebugClientEvent { session_id, message }) // }) // .log_err(); // } @@ -527,12 +527,12 @@ impl DapStore { // debugger::client::Client::local(Arc::new(client), capabilities); // }); - // store.clients.insert(Arc::new(client), client_id); + // store.clients.insert(Arc::new(client), session_id); // // don't emit this event ourself in tests, so we can add request, // // response and event handlers for this client // if !cfg!(any(test, feature = "test-support")) { - // cx.emit(DapStoreEvent::DebugClientStarted(client_id)); + // cx.emit(DapStoreEvent::DebugClientStarted(session_id)); // } // cx.notify(); @@ -550,7 +550,7 @@ impl DapStore { return Task::ready(Err(anyhow!("cannot start client on remote side"))); }; - let client_id = local_store.next_client_id(); + let session_id = local_store.next_session_id(); cx.spawn(|this, mut cx| async move { let adapter = build_adapter(&config.kind).await?; @@ -596,7 +596,7 @@ impl DapStore { } }; - let mut client = DebugAdapterClient::start(client_id, binary, |_, _| {}, cx).await?; + let mut client = DebugAdapterClient::start(session_id, binary, |_, _| {}, cx).await?; Ok(Arc::new(client)) }) @@ -641,11 +641,11 @@ impl DapStore { }; this.update(&mut cx, |store, cx| { - let client_id = client.id(); + let session_id = client.id(); - unimplemented!("store.clients.insert(client_id, client);"); + unimplemented!("store.clients.insert(session_id, client);"); - cx.emit(DapStoreEvent::DebugClientStarted(client_id)); + cx.emit(DapStoreEvent::DebugClientStarted(session_id)); cx.notify(); client @@ -655,18 +655,18 @@ impl DapStore { pub fn configuration_done( &self, - client_id: SessionId, + session_id: SessionId, cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id) + .client_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); }; if self - .capabilities_by_id(client_id, cx) + .capabilities_by_id(session_id, cx) .map(|caps| caps.supports_configuration_done_request) .flatten() .unwrap_or_default() @@ -717,17 +717,17 @@ impl DapStore { pub fn attach( &mut self, - client_id: SessionId, + session_id: SessionId, process_id: u32, cx: &mut Context, ) -> Task> { unimplemented!(); // let Some(client) = self - // .client_by_id(client_id) + // .client_by_id(session_id) // .and_then(|client| Some(client.read(cx).adapter_client()?)) // else { // return Task::ready(Err( - // anyhow!("Could not find debug client: {:?}", client_id,), + // anyhow!("Could not find debug client: {:?}", session_id,), // )); // }; @@ -761,17 +761,20 @@ impl DapStore { pub fn respond_to_run_in_terminal( &self, - client_id: SessionId, + session_id: SessionId, success: bool, seq: u64, body: Option, cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id) + .client_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { - return Task::ready(Err(anyhow!("Could not find debug client: {:?}", client_id))); + return Task::ready(Err(anyhow!( + "Could not find debug client: {:?}", + session_id + ))); }; cx.background_executor().spawn(async move { @@ -789,7 +792,7 @@ impl DapStore { pub fn evaluate( &self, - client_id: &SessionId, + session_id: &SessionId, stack_frame_id: u64, expression: String, context: EvaluateArgumentsContext, @@ -797,10 +800,10 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id) + .client_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); }; cx.background_executor().spawn(async move { @@ -820,17 +823,17 @@ impl DapStore { pub fn completions( &self, - client_id: &SessionId, + session_id: &SessionId, stack_frame_id: u64, text: String, completion_column: u64, cx: &mut Context, ) -> Task>> { let Some(client) = self - .client_by_id(client_id) + .client_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); }; cx.background_executor().spawn(async move { @@ -849,7 +852,7 @@ impl DapStore { #[allow(clippy::too_many_arguments)] pub fn set_variable_value( &self, - client_id: &SessionId, + session_id: &SessionId, stack_frame_id: u64, variables_reference: u64, name: String, @@ -858,14 +861,14 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(client_id) + .client_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); }; let supports_set_expression = self - .capabilities_by_id(client_id, cx) + .capabilities_by_id(session_id, cx) .map(|caps| caps.supports_set_expression) .flatten() .unwrap_or_default(); @@ -902,8 +905,8 @@ impl DapStore { pub fn shutdown_clients(&mut self, cx: &mut Context) -> Task<()> { let mut tasks = vec![]; - for client_id in self.clients.keys().cloned().collect::>() { - tasks.push(self.shutdown_client(&client_id, cx)); + for session_id in self.clients.keys().cloned().collect::>() { + tasks.push(self.shutdown_client(&session_id, cx)); } cx.background_executor().spawn(async move { @@ -913,14 +916,14 @@ impl DapStore { pub fn shutdown_client( &mut self, - client_id: &SessionId, + session_id: &SessionId, cx: &mut Context, ) -> Task> { let Some(_) = self.as_local_mut() else { if let Some((upstream_client, project_id)) = self.upstream_client() { let future = upstream_client.request(proto::ShutdownDebugClient { project_id, - client_id: client_id.to_proto(), + session_id: session_id.to_proto(), }); return cx @@ -930,8 +933,8 @@ impl DapStore { return Task::ready(Err(anyhow!("Cannot shutdown session on remote side"))); }; - let Some(client) = self.clients.remove(client_id) else { - return Task::ready(Err(anyhow!("Could not find session: {:?}", client_id))); + let Some(client) = self.clients.remove(session_id) else { + return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); }; client.update(cx, |this, cx| { @@ -941,55 +944,6 @@ impl DapStore { Task::ready(Ok(())) } - async fn _handle_dap_command_2( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<::Response> - where - ::Arguments: Send, - ::Response: Send, - { - let request = T::from_proto(&envelope.payload); - let client_id = T::client_id_from_proto(&envelope.payload); - - let _state = this - .update(&mut cx, |this, cx| { - this.client_by_id(client_id)? - .read(cx) - ._wait_for_request(request) - }) - .ok() - .flatten(); - if let Some(_state) = _state { - let _ = _state.await; - } - - todo!() - } - - // async fn handle_dap_command( - // this: Entity, - // envelope: TypedEnvelope, - // mut cx: AsyncApp, - // ) -> Result<::Response> - // where - // ::Arguments: Send, - // ::Response: Send, - // { - // let _sender_id = envelope.original_sender_id().unwrap_or_default(); - // let client_id = T::client_id_from_proto(&envelope.payload); - - // let request = T::from_proto(&envelope.payload); - // let response = this - // .update(&mut cx, |this, cx| { - // this.request_dap::(&client_id, request, cx) - // })? - // .await?; - - // Ok(T::response_to_proto(&client_id, response)) - // } - async fn handle_update_debug_adapter( this: Entity, envelope: TypedEnvelope, @@ -1017,7 +971,7 @@ impl DapStore { ) -> Result<()> { this.update(&mut cx, |dap_store, cx| { dap_store.update_capabilities_for_client( - SessionId::from_proto(envelope.payload.client_id), + SessionId::from_proto(envelope.payload.session_id), &dap::proto_conversions::capabilities_from_proto(&envelope.payload), cx, ); @@ -1030,15 +984,15 @@ impl DapStore { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |dap_store, cx| { - let client_id = SessionId::from_proto(envelope.payload.client_id); + let session_id = SessionId::from_proto(envelope.payload.session_id); - dap_store.client_by_id(client_id).map(|state| { + dap_store.client_by_id(session_id).map(|state| { state.update(cx, |state, cx| { state.shutdown(cx); }) }); - cx.emit(DapStoreEvent::DebugClientShutdown(client_id)); + cx.emit(DapStoreEvent::DebugClientShutdown(session_id)); cx.notify(); }) } @@ -1057,7 +1011,7 @@ impl DapStore { this.update(&mut cx, |store, cx| { store.active_debug_line = Some(( - SessionId::from_proto(envelope.payload.client_id), + SessionId::from_proto(envelope.payload.session_id), project_path, envelope.payload.row, )); @@ -1082,7 +1036,7 @@ impl DapStore { pub fn send_breakpoints( &self, - client_id: SessionId, + session_id: SessionId, absolute_file_path: Arc, mut breakpoints: Vec, ignore: bool, @@ -1090,10 +1044,10 @@ impl DapStore { cx: &App, ) -> Task> { let Some(client) = self - .client_by_id(client_id) + .client_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); + return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); }; // Adjust breakpoints as our client declares that indices start at one. @@ -1144,7 +1098,7 @@ impl DapStore { .collect::>(); let mut tasks = Vec::new(); - for (client_id, client) in self + for (session_id, client) in self .clients .iter() .filter(|(_, client)| client.read(cx).adapter_client().is_some()) @@ -1153,7 +1107,7 @@ impl DapStore { let ignore_breakpoints = !client.breakpoints_enabled(); tasks.push(self.send_breakpoints( - *client_id, + *session_id, Arc::from(absolute_path.clone()), source_breakpoints.clone(), ignore_breakpoints, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index cd606ca0863be9..e1ca4a657bce80 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -126,10 +126,10 @@ impl RemoteConnection { fn send_proto_client_request( &self, request: R, - client_id: SessionId, + session_id: SessionId, cx: &mut App, ) -> Task> { - let message = request.to_proto(client_id, self.upstream_project_id); + let message = request.to_proto(session_id, self.upstream_project_id); let upstream_client = self.client.clone(); cx.background_executor().spawn(async move { let response = upstream_client.request(message).await?; @@ -139,14 +139,14 @@ impl RemoteConnection { fn request( &self, request: R, - client_id: SessionId, + session_id: SessionId, cx: &mut App, ) -> Task> where ::Response: 'static, ::Arguments: 'static + Send, { - return self.send_proto_client_request::(request, client_id, cx); + return self.send_proto_client_request::(request, session_id, cx); } } @@ -161,7 +161,7 @@ struct LocalMode { impl LocalMode { fn new( - client_id: SessionId, + session_id: SessionId, breakpoint_store: Entity, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, @@ -210,7 +210,7 @@ impl LocalMode { }; let client = Arc::new( - DebugAdapterClient::start(client_id, binary, message_handler, cx.clone()).await?, + DebugAdapterClient::start(session_id, binary, message_handler, cx.clone()).await?, ); let this = Self { client }; let capabilities = this @@ -258,7 +258,7 @@ impl From for Mode { impl Mode { fn request_dap( &self, - client_id: SessionId, + session_id: SessionId, request: R, cx: &mut Context, ) -> Task> @@ -270,7 +270,7 @@ impl Mode { Mode::Local(debug_adapter_client) => { debug_adapter_client.request(request, cx.background_executor().clone()) } - Mode::Remote(remote_connection) => remote_connection.request(request, client_id, cx), + Mode::Remote(remote_connection) => remote_connection.request(request, session_id, cx), } } } @@ -280,7 +280,7 @@ pub struct Session { mode: Mode, config: DebugAdapterConfig, pub(super) capabilities: Capabilities, - client_id: SessionId, + id: SessionId, ignore_breakpoints: bool, modules: Vec, loaded_sources: Vec, @@ -366,14 +366,14 @@ impl CompletionsQuery { impl Session { pub(crate) fn local( breakpoints: Entity, - client_id: SessionId, + session_id: SessionId, delegate: DapAdapterDelegate, config: DebugAdapterConfig, cx: &mut App, ) -> Task>> { cx.spawn(move |mut cx| async move { let (mode, capabilities) = LocalMode::new( - client_id, + session_id, breakpoints, config.clone(), delegate, @@ -383,7 +383,7 @@ impl Session { .await?; cx.new(|_| Self { mode: Mode::Local(mode), - client_id, + id: session_id, config, capabilities, ignore_breakpoints: false, @@ -396,7 +396,7 @@ impl Session { } pub(crate) fn remote( - client_id: SessionId, + session_id: SessionId, client: AnyProtoClient, upstream_project_id: u64, ignore_breakpoints: bool, @@ -406,7 +406,7 @@ impl Session { client, upstream_project_id, }), - client_id, + id: session_id, capabilities: Capabilities::default(), ignore_breakpoints, requests: HashMap::default(), @@ -444,7 +444,7 @@ impl Session { let task = Self::request_inner::>( &self.capabilities, - self.client_id, + self.id, &self.mode, command, process_result, @@ -464,7 +464,7 @@ impl Session { fn request_inner( capabilities: &Capabilities, - client_id: SessionId, + session_id: SessionId, mode: &Mode, request: T, process_result: impl FnOnce(&mut Self, &T::Response, &mut Context) + 'static, @@ -473,7 +473,7 @@ impl Session { if !T::is_supported(&capabilities) { return Task::ready(None); } - let request = mode.request_dap(client_id, request, cx); + let request = mode.request_dap(session_id, request, cx); cx.spawn(|this, mut cx| async move { let result = request.await.log_err()?; this.update(&mut cx, |this, cx| { @@ -492,7 +492,7 @@ impl Session { ) -> Task> { Self::request_inner( &self.capabilities, - self.client_id, + self.id, &self.mode, request, process_result, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f9f5345d17451c..7211e83d2a4bb4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -274,7 +274,7 @@ pub enum Event { DebugClientShutdown(SessionId), ActiveDebugLineChanged, DebugClientEvent { - client_id: SessionId, + session_id: SessionId, message: Message, }, DebugClientLog(SessionId, String), @@ -1306,7 +1306,7 @@ impl Project { pub fn initial_send_breakpoints( &self, - client_id: SessionId, + session_id: SessionId, cx: &mut Context, ) -> Task<()> { let mut tasks = Vec::new(); @@ -1324,10 +1324,10 @@ impl Project { tasks.push(self.dap_store.update(cx, |store, cx| { store.send_breakpoints( - client_id, + session_id, abs_path, source_breakpoints, - store.ignore_breakpoints(&client_id, cx), + store.ignore_breakpoints(&session_id, cx), false, cx, ) @@ -1389,14 +1389,14 @@ impl Project { pub fn toggle_ignore_breakpoints( &self, - client_id: SessionId, + session_id: SessionId, cx: &mut Context, ) -> Task> { let tasks = self.dap_store.update(cx, |store, cx| { if let Some((upstream_client, project_id)) = store.upstream_client() { upstream_client .send(proto::ToggleIgnoreBreakpoints { - client_id: client_id.to_proto(), + client_id: session_id.to_proto(), project_id, }) .log_err(); @@ -1404,14 +1404,14 @@ impl Project { return Vec::new(); } - store.toggle_ignore_breakpoints(&client_id, cx); + store.toggle_ignore_breakpoints(&session_id, cx); if let Some((downstream_client, project_id)) = store.downstream_client() { downstream_client .send(proto::IgnoreBreakpointState { - client_id: client_id.to_proto(), + session_id: session_id.to_proto(), project_id: *project_id, - ignore: store.ignore_breakpoints(&client_id, cx), + ignore: store.ignore_breakpoints(&session_id, cx), }) .log_err(); } @@ -1437,13 +1437,13 @@ impl Project { tasks.push( store.send_breakpoints( - client_id, + session_id, Arc::from(buffer_path), breakpoints .into_iter() .map(|breakpoint| breakpoint.to_source_breakpoint(buffer)) .collect::>(), - store.ignore_breakpoints(&client_id, cx), + store.ignore_breakpoints(&session_id, cx), false, cx, ), @@ -2655,15 +2655,18 @@ impl Project { cx: &mut Context, ) { match event { - DapStoreEvent::DebugClientStarted(client_id) => { - cx.emit(Event::DebugClientStarted(*client_id)); + DapStoreEvent::DebugClientStarted(session_id) => { + cx.emit(Event::DebugClientStarted(*session_id)); } - DapStoreEvent::DebugClientShutdown(client_id) => { - cx.emit(Event::DebugClientShutdown(*client_id)); + DapStoreEvent::DebugClientShutdown(session_id) => { + cx.emit(Event::DebugClientShutdown(*session_id)); } - DapStoreEvent::DebugClientEvent { client_id, message } => { + DapStoreEvent::DebugClientEvent { + session_id, + message, + } => { cx.emit(Event::DebugClientEvent { - client_id: *client_id, + session_id: *session_id, message: message.clone(), }); } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index cb24e5fb3dfdf7..017b79bb331480 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2568,11 +2568,11 @@ message DebugClient { message ShutdownDebugClient { uint64 project_id = 1; - uint64 client_id = 2; + uint64 session_id = 2; } message SetDebugClientCapabilities { - uint64 client_id = 1; + uint64 session_id = 1; uint64 project_id = 2; bool supports_loaded_sources_request = 3; bool supports_modules_request = 4; @@ -2602,7 +2602,7 @@ message SynchronizeBreakpoints { message SetActiveDebugLine { uint64 project_id = 1; ProjectPath project_path = 2; - uint64 client_id = 3; + uint64 session_id = 3; uint32 row = 4; } @@ -2895,7 +2895,7 @@ message ToggleIgnoreBreakpoints { message IgnoreBreakpointState { uint64 project_id = 1; - uint64 client_id = 2; + uint64 session_id = 2; bool ignore = 3; } From afaaf2445574e4ad94ce1ddd02bd396273e5051a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 02:09:05 +0100 Subject: [PATCH 588/650] Renames --- crates/collab/src/tests/debug_panel_tests.rs | 14 ++-- crates/debugger_ui/src/attach_modal.rs | 2 +- crates/debugger_ui/src/lib.rs | 2 +- crates/debugger_ui/src/tests/attach_modal.rs | 4 +- crates/debugger_ui/src/tests/console.rs | 6 +- .../debugger_ui/src/tests/debugger_panel.rs | 18 ++--- crates/debugger_ui/src/tests/module_list.rs | 2 +- .../debugger_ui/src/tests/stack_frame_list.rs | 6 +- crates/debugger_ui/src/tests/variable_list.rs | 10 +-- crates/project/src/debugger/dap_store.rs | 81 +++++++++++++++---- 10 files changed, 97 insertions(+), 48 deletions(-) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 86c01184ff2866..5b54ef14a26cf7 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -274,7 +274,7 @@ async fn test_debug_panel_item_opens_on_remote( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -373,7 +373,7 @@ async fn test_active_debug_panel_item_set_on_join_project( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -866,7 +866,7 @@ async fn test_restart_stack_frame(host_cx: &mut TestAppContext, remote_cx: &mut let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -1097,7 +1097,7 @@ async fn test_updated_breakpoints_send_to_dap( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -1332,7 +1332,7 @@ async fn test_module_list( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -2192,7 +2192,7 @@ async fn test_ignore_breakpoints( let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -2516,7 +2516,7 @@ async fn test_debug_panel_console(host_cx: &mut TestAppContext, remote_cx: &mut let shutdown_client = host_project.update(host_cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 8cff2573b95e7c..328e1b1e7685f0 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -229,7 +229,7 @@ impl PickerDelegate for AttachModalDelegate { self.candidates.take(); self.dap_store.update(cx, |store, cx| { - store.shutdown_client(&self.session_id, cx).detach(); + store.shutdown_session(&self.session_id, cx).detach(); }); cx.emit(DismissEvent); diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index b725cd84aec6d5..7d4dc52538e968 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -36,7 +36,7 @@ pub fn init(cx: &mut App) { |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| { workspace.project().update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { - store.shutdown_clients(cx).detach(); + store.shutdown_sessions(cx).detach(); }) }) }, diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 4b48391c69bc4b..c5161c76a8fa20 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -90,7 +90,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -201,7 +201,7 @@ async fn test_show_attach_modal_and_select_process( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 6de89e3ee6367a..85db5619dbb0a7 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -174,7 +174,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -459,7 +459,7 @@ async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppConte let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -875,7 +875,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index ce5afb9ca331ad..e1477a8c466a15 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -131,7 +131,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -285,7 +285,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -439,7 +439,7 @@ async fn test_client_can_open_multiple_thread_panels( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -561,7 +561,7 @@ async fn test_handle_successful_run_in_terminal_reverse_request( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -665,7 +665,7 @@ async fn test_handle_error_run_in_terminal_reverse_request( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -804,7 +804,7 @@ async fn test_handle_start_debugging_reverse_request( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -1005,7 +1005,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -1194,7 +1194,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -1334,7 +1334,7 @@ async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs index 8ea6e16725d5dd..13da9e154c07db 100644 --- a/crates/debugger_ui/src/tests/module_list.rs +++ b/crates/debugger_ui/src/tests/module_list.rs @@ -225,7 +225,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index b824a37babee75..9413b8f6966c4c 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -181,7 +181,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -444,7 +444,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -667,7 +667,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo }); let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 5267dfdcd5fbcc..9173b6c5864719 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -235,7 +235,7 @@ async fn test_basic_fetch_initial_scope_and_variables( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -514,7 +514,7 @@ async fn test_fetch_variables_for_multiple_scopes( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -1129,7 +1129,7 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -1375,7 +1375,7 @@ async fn test_it_only_fetches_scopes_and_variables_for_the_first_stack_frame( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); @@ -1743,7 +1743,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( let shutdown_session = project.update(cx, |project, cx| { project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_client(&session.read(cx).id(), cx) + dap_store.shutdown_session(&session.read(cx).id(), cx) }) }); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index afef6e751ce8f7..4578a8c67ea263 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -94,12 +94,12 @@ impl LocalDapStore { } pub fn respond_to_start_debugging( &mut self, - client: &Entity, + session: &Entity, seq: u64, args: Option, cx: &mut Context, ) -> Task> { - let config = client.read(cx).configuration(); + let config = session.read(cx).configuration(); let request_args = args.unwrap_or_else(|| StartDebuggingRequestArguments { configuration: config.initialize_args.clone().unwrap_or_default(), @@ -208,7 +208,7 @@ pub struct DapStore { downstream_client: Option<(AnyProtoClient, u64)>, breakpoint_store: Entity, active_debug_line: Option<(SessionId, ProjectPath, u32)>, - clients: BTreeMap>, + sessions: BTreeMap>, } impl EventEmitter for DapStore {} @@ -248,7 +248,7 @@ impl DapStore { breakpoint_store: Entity, cx: &mut Context, ) -> Self { - cx.on_app_quit(Self::shutdown_clients).detach(); + cx.on_app_quit(Self::shutdown_sessions).detach(); Self { mode: DapStoreMode::Local(LocalDapStore { @@ -263,7 +263,7 @@ impl DapStore { downstream_client: None, active_debug_line: None, breakpoint_store, - clients: Default::default(), + sessions: Default::default(), } } @@ -281,7 +281,7 @@ impl DapStore { downstream_client: None, active_debug_line: None, breakpoint_store, - clients: Default::default(), + sessions: Default::default(), } } @@ -337,7 +337,7 @@ impl DapStore { cx: &mut Context, ) { if let DapStoreMode::Remote(remote) = &self.mode { - self.clients.insert( + self.sessions.insert( session_id, cx.new(|_| { debugger::session::Session::remote( @@ -358,12 +358,12 @@ impl DapStore { session_id: impl Borrow, ) -> Option> { let session_id = session_id.borrow(); - let client = self.clients.get(session_id).cloned(); + let client = self.sessions.get(session_id).cloned(); client } pub fn clients(&self) -> impl Iterator> { - self.clients.values() + self.sessions.values() } pub fn capabilities_by_id( @@ -372,7 +372,7 @@ impl DapStore { cx: &App, ) -> Option { let session_id = session_id.borrow(); - self.clients + self.sessions .get(session_id) .map(|client| client.read(cx).capabilities.clone()) } @@ -903,10 +903,10 @@ impl DapStore { // client.wait_for_request(request::Modules); // This ensures that the request that we've fired off runs to completions // let returned_value = client.modules(); // this is a cheap getter. - pub fn shutdown_clients(&mut self, cx: &mut Context) -> Task<()> { + pub fn shutdown_sessions(&mut self, cx: &mut Context) -> Task<()> { let mut tasks = vec![]; - for session_id in self.clients.keys().cloned().collect::>() { - tasks.push(self.shutdown_client(&session_id, cx)); + for session_id in self.sessions.keys().cloned().collect::>() { + tasks.push(self.shutdown_session(&session_id, cx)); } cx.background_executor().spawn(async move { @@ -914,7 +914,7 @@ impl DapStore { }) } - pub fn shutdown_client( + pub fn shutdown_session( &mut self, session_id: &SessionId, cx: &mut Context, @@ -933,7 +933,7 @@ impl DapStore { return Task::ready(Err(anyhow!("Cannot shutdown session on remote side"))); }; - let Some(client) = self.clients.remove(session_id) else { + let Some(client) = self.sessions.remove(session_id) else { return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); }; @@ -944,6 +944,55 @@ impl DapStore { Task::ready(Ok(())) } + // async fn _handle_dap_command_2( + // this: Entity, + // envelope: TypedEnvelope, + // mut cx: AsyncApp, + // ) -> Result<::Response> + // where + // ::Arguments: Send, + // ::Response: Send, + // { + // let request = T::from_proto(&envelope.payload); + // let session_id = T::session_id_from_proto(&envelope.payload); + + // let _state = this + // .update(&mut cx, |this, cx| { + // this.client_by_id(session_id)? + // .read(cx) + // ._wait_for_request(request) + // }) + // .ok() + // .flatten(); + // if let Some(_state) = _state { + // let _ = _state.await; + // } + + // todo!() + // } + + // async fn handle_dap_command( + // this: Entity, + // envelope: TypedEnvelope, + // mut cx: AsyncApp, + // ) -> Result<::Response> + // where + // ::Arguments: Send, + // ::Response: Send, + // { + // let _sender_id = envelope.original_sender_id().unwrap_or_default(); + // let session_id = T::session_id_from_proto(&envelope.payload); + + // let request = T::from_proto(&envelope.payload); + // let response = this + // .update(&mut cx, |this, cx| { + // this.request_dap::(&session_id, request, cx) + // })? + // .await?; + + // Ok(T::response_to_proto(&session_id, response)) + // } + async fn handle_update_debug_adapter( this: Entity, envelope: TypedEnvelope, @@ -1099,7 +1148,7 @@ impl DapStore { let mut tasks = Vec::new(); for (session_id, client) in self - .clients + .sessions .iter() .filter(|(_, client)| client.read(cx).adapter_client().is_some()) { From a8f59b8a1daca0ee6ff8caacd7fbd9b662dfb5b3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 02:09:23 +0100 Subject: [PATCH 589/650] Renames --- crates/debugger_tools/src/dap_log.rs | 22 +++++-------------- crates/debugger_ui/src/attach_modal.rs | 2 +- crates/project/src/debugger/dap_store.rs | 28 ++++++++++++------------ 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 115f1a5f52dab3..b34e812876bb6a 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -113,8 +113,7 @@ impl LogStore { }) .detach_and_log_err(cx); - let (adapter_log_tx, mut adapter_log_rx) = - unbounded::<(SessionId, IoKind, String)>(); + let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>(); cx.spawn(|this, mut cx| async move { while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await { if let Some(this) = this.upgrade() { @@ -170,7 +169,7 @@ impl LogStore { let client = project.update(cx, |project, cx| { project.dap_store().update(cx, |store, cx| { store - .client_by_id(client_id) + .session_by_id(client_id) .and_then(|client| Some(client)) }) }); @@ -189,10 +188,7 @@ impl LogStore { ); } - fn get_debug_adapter_state( - &mut self, - id: SessionId, - ) -> Option<&mut DebugAdapterState> { + fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> { self.debug_clients.get_mut(&id) } @@ -328,17 +324,11 @@ impl LogStore { cx.notify(); } - fn log_messages_for_client( - &mut self, - client_id: SessionId, - ) -> Option<&mut VecDeque> { + fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque> { Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages) } - fn rpc_messages_for_client( - &mut self, - client_id: SessionId, - ) -> Option<&mut VecDeque> { + fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque> { Some( &mut self .debug_clients @@ -568,7 +558,7 @@ impl DapLogView { .read(cx) .dap_store() .read(cx) - .clients() + .sessions() .filter_map(|client| { let client = client.read(cx).adapter_client()?; Some(DapMenuItem { diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 328e1b1e7685f0..37d2d04b11c61b 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -128,7 +128,7 @@ impl PickerDelegate for AttachModalDelegate { } else { let Some(client) = this.delegate.dap_store.update(cx, |store, cx| { store - .client_by_id(&this.delegate.client_id) + .session_by_id(&this.delegate.client_id) .and_then(|client| client.read(cx).adapter_client()) }) else { return Vec::new(); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 4578a8c67ea263..24da8c6f382d6c 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -353,7 +353,7 @@ impl DapStore { } } - pub fn client_by_id( + pub fn session_by_id( &self, session_id: impl Borrow, ) -> Option> { @@ -362,7 +362,7 @@ impl DapStore { client } - pub fn clients(&self) -> impl Iterator> { + pub fn sessions(&self) -> impl Iterator> { self.sessions.values() } @@ -383,7 +383,7 @@ impl DapStore { capabilities: &Capabilities, cx: &mut Context, ) { - if let Some(client) = self.client_by_id(session_id) { + if let Some(client) = self.session_by_id(session_id) { client.update(cx, |this, cx| { this.capabilities = this.capabilities.merge(capabilities.clone()); }); @@ -450,7 +450,7 @@ impl DapStore { let session_id = SessionId::from_proto(envelope.payload.session_id); this.update(&mut cx, |this, cx| { - if let Some(client) = this.client_by_id(&session_id) { + if let Some(client) = this.session_by_id(&session_id) { client.update(cx, |client, cx| { client.set_ignore_breakpoints(envelope.payload.ignore) }); @@ -466,7 +466,7 @@ impl DapStore { ignore: bool, cx: &mut Context, ) { - if let Some(session) = self.client_by_id(session_id) { + if let Some(session) = self.session_by_id(session_id) { session.update(cx, |session, _| { session.set_ignore_breakpoints(ignore); }); @@ -474,13 +474,13 @@ impl DapStore { } pub fn ignore_breakpoints(&self, session_id: &SessionId, cx: &App) -> bool { - self.client_by_id(session_id) + self.session_by_id(session_id) .map(|client| client.read(cx).breakpoints_enabled()) .unwrap_or_default() } pub fn toggle_ignore_breakpoints(&mut self, session_id: &SessionId, cx: &mut Context) { - if let Some(client) = self.client_by_id(session_id) { + if let Some(client) = self.session_by_id(session_id) { client.update(cx, |client, _| { client.set_ignore_breakpoints(!client.breakpoints_enabled()); }); @@ -659,7 +659,7 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(session_id) + .session_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); @@ -768,7 +768,7 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(session_id) + .session_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!( @@ -800,7 +800,7 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(session_id) + .session_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); @@ -830,7 +830,7 @@ impl DapStore { cx: &mut Context, ) -> Task>> { let Some(client) = self - .client_by_id(session_id) + .session_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); @@ -861,7 +861,7 @@ impl DapStore { cx: &mut Context, ) -> Task> { let Some(client) = self - .client_by_id(session_id) + .session_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); @@ -1035,7 +1035,7 @@ impl DapStore { this.update(&mut cx, |dap_store, cx| { let session_id = SessionId::from_proto(envelope.payload.session_id); - dap_store.client_by_id(session_id).map(|state| { + dap_store.session_by_id(session_id).map(|state| { state.update(cx, |state, cx| { state.shutdown(cx); }) @@ -1093,7 +1093,7 @@ impl DapStore { cx: &App, ) -> Task> { let Some(client) = self - .client_by_id(session_id) + .session_by_id(session_id) .and_then(|client| client.read(cx).adapter_client()) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); From c308a8cc2dc406df69dde958af9b81e5a22d593f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 02:11:31 +0100 Subject: [PATCH 590/650] Reduce visibility of modes --- crates/project/src/debugger/session.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index e1ca4a657bce80..b680e206711cbe 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -117,7 +117,7 @@ impl From for Thread { type UpstreamProjectId = u64; -pub struct RemoteConnection { +struct RemoteConnection { client: AnyProtoClient, upstream_project_id: UpstreamProjectId, } @@ -150,7 +150,7 @@ impl RemoteConnection { } } -pub enum Mode { +enum Mode { Local(LocalMode), Remote(RemoteConnection), } From 04a1e569c27a5f8328453e4011087e6a6e81ea90 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 02:23:30 +0100 Subject: [PATCH 591/650] fmt --- .../debugger_ui/src/session/running/loaded_source_list.rs | 6 +----- crates/debugger_ui/src/session/running/module_list.rs | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index 589e1f2653d66e..db88bd359d9c7f 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -13,11 +13,7 @@ pub struct LoadedSourceList { } impl LoadedSourceList { - pub fn new( - session: Entity, - client_id: SessionId, - cx: &mut Context, - ) -> Self { + pub fn new(session: Entity, client_id: SessionId, cx: &mut Context) -> Self { let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 198fb0454f4e05..76958234bd67e0 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -12,11 +12,7 @@ pub struct ModuleList { } impl ModuleList { - pub fn new( - session: Entity, - client_id: SessionId, - cx: &mut Context, - ) -> Self { + pub fn new(session: Entity, client_id: SessionId, cx: &mut Context) -> Self { let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); From 1d5fc3771f5deee6f2e428b1208355a73a22b793 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:15:44 +0100 Subject: [PATCH 592/650] Change dap name on click in inert state --- crates/debugger_ui/src/session/inert.rs | 42 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 12738320b4fe3c..565f2df232e78a 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,46 +1,66 @@ use gpui::{App, FocusHandle, Focusable}; use ui::{ div, h_flex, v_flex, Button, ButtonCommon, ButtonStyle, Context, ContextMenu, DropdownMenu, - Element, InteractiveElement, ParentElement, Render, Styled, + Element, InteractiveElement, ParentElement, Render, SharedString, Styled, Window, }; pub(super) struct InertState { focus_handle: FocusHandle, + selected_debugger: Option, } impl InertState { pub(super) fn new(cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), + selected_debugger: None, } } } impl Focusable for InertState { - fn focus_handle(&self, cx: &App) -> FocusHandle { + fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } +static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger"); impl Render for InertState { fn render( &mut self, window: &mut ui::Window, cx: &mut ui::Context<'_, Self>, ) -> impl ui::IntoElement { + let weak = cx.weak_entity(); v_flex() .track_focus(&self.focus_handle) .size_full() .gap_1() .p_1() - .child(h_flex().child(DropdownMenu::new( - "dap-adapter-picker", - "Select Debug Adapter", - ContextMenu::build(window, cx, |this, _, _| { - this.entry("GDB", None, |_, _| {}) - .entry("Delve", None, |_, _| {}) - .entry("LLDB", None, |_, _| {}) - }), - ))) + .child( + h_flex().child(DropdownMenu::new( + "dap-adapter-picker", + self.selected_debugger + .as_ref() + .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL) + .clone(), + ContextMenu::build(window, cx, move |this, _, _| { + let setter_for_name = |name: &'static str| { + let weak = weak.clone(); + move |_: &mut Window, cx: &mut App| { + let name = name; + (&weak) + .update(cx, move |this, _| { + this.selected_debugger = Some(name.into()); + }) + .ok(); + } + }; + this.entry("GDB", None, setter_for_name("GDB")) + .entry("Delve", None, setter_for_name("Delve")) + .entry("LLDB", None, setter_for_name("LLDB")) + }), + )), + ) .child( h_flex() .gap_1() From e82520c6cf14d5baec9bc7d8b96035b24c2d4830 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:58:05 +0100 Subject: [PATCH 593/650] Move from inert to starting state --- crates/debugger_ui/src/session.rs | 29 +++++++++++++++++++--- crates/debugger_ui/src/session/inert.rs | 20 ++++++++++++--- crates/debugger_ui/src/session/starting.rs | 26 +++++++++++++++++-- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 5bb0d3f76e5593..2403a81e1d4886 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -11,7 +11,7 @@ use dap::{ use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use inert::InertState; +use inert::{InertEvent, InertState}; use project::debugger::session::Session; use project::debugger::session::{ThreadId, ThreadStatus}; @@ -33,6 +33,7 @@ enum DebugSessionState { pub struct DebugSession { remote_id: Option, mode: DebugSessionState, + _subscriptions: [Subscription; 1], } #[derive(Debug)] pub enum DebugPanelItemEvent { @@ -70,9 +71,15 @@ impl ThreadItem { impl DebugSession { pub(super) fn inert(cx: &mut App) -> Entity { - cx.new(|cx| Self { - remote_id: None, - mode: DebugSessionState::Inert(cx.new(|cx| InertState::new(cx))), + let inert = cx.new(|cx| InertState::new(cx)); + + cx.new(|cx| { + let _subscriptions = [cx.subscribe(&inert, Self::on_inert_event)]; + Self { + remote_id: None, + mode: DebugSessionState::Inert(inert), + _subscriptions, + } }) } pub(crate) fn session_id(&self, cx: &App) -> Option { @@ -82,6 +89,20 @@ impl DebugSession { DebugSessionState::Running(entity) => Some(entity.read(cx).client_id()), } } + fn on_inert_event( + &mut self, + _: Entity, + _: &InertEvent, + cx: &mut Context<'_, Self>, + ) { + self.mode = DebugSessionState::Starting(cx.new(|cx| { + let task = cx.background_executor().spawn(async move { + std::future::pending::<()>().await; + Ok(()) + }); + StartingState::new(task, cx) + })); + } } impl EventEmitter for DebugSession {} diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 565f2df232e78a..1ec1f5f58fa803 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,7 +1,7 @@ -use gpui::{App, FocusHandle, Focusable}; +use gpui::{App, EventEmitter, FocusHandle, Focusable}; use ui::{ - div, h_flex, v_flex, Button, ButtonCommon, ButtonStyle, Context, ContextMenu, DropdownMenu, - Element, InteractiveElement, ParentElement, Render, SharedString, Styled, Window, + div, h_flex, v_flex, Button, ButtonCommon, ButtonStyle, Clickable, Context, ContextMenu, + DropdownMenu, Element, InteractiveElement, ParentElement, Render, SharedString, Styled, Window, }; pub(super) struct InertState { @@ -23,6 +23,12 @@ impl Focusable for InertState { } } +pub(crate) enum InertEvent { + Spawned { config: () }, +} + +impl EventEmitter for InertState {} + static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger"); impl Render for InertState { fn render( @@ -64,7 +70,13 @@ impl Render for InertState { .child( h_flex() .gap_1() - .child(Button::new("launch-dap", "Launch").style(ButtonStyle::Filled)) + .child( + Button::new("launch-dap", "Launch") + .style(ButtonStyle::Filled) + .on_click(cx.listener(|_, _, _, cx| { + cx.emit(InertEvent::Spawned { config: () }); + })), + ) .child(Button::new("attach-dap", "Attach").style(ButtonStyle::Filled)), ) } diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs index d928a2b3f05e8a..c1b97bd564d407 100644 --- a/crates/debugger_ui/src/session/starting.rs +++ b/crates/debugger_ui/src/session/starting.rs @@ -1,8 +1,30 @@ -use gpui::{FocusHandle, Focusable}; -use ui::{div, Element, ParentElement, Render, Styled}; +use anyhow::Result; + +use gpui::{EventEmitter, FocusHandle, Focusable, Subscription, Task}; +use ui::{div, Context, Element, ParentElement, Render, Styled}; pub(super) struct StartingState { focus_handle: FocusHandle, + _notify_parent: Task>, +} + +pub(crate) enum StartingEvent { + Finished(()), +} + +impl EventEmitter for StartingState {} +impl StartingState { + pub(crate) fn new(task: Task>, cx: &mut Context) -> Self { + let _notify_parent = cx.spawn(move |this, mut cx| async move { + task.await?; + this.update(&mut cx, |_, cx| cx.emit(StartingEvent::Finished(()))); + Ok(()) + }); + Self { + focus_handle: cx.focus_handle(), + _notify_parent, + } + } } impl Focusable for StartingState { From 083fca426ec4607d5354aacf30dd838dac81ed85 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:01:47 +0100 Subject: [PATCH 594/650] Style starting state a bit --- crates/debugger_ui/src/session/starting.rs | 28 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs index c1b97bd564d407..dc96660b60d6ba 100644 --- a/crates/debugger_ui/src/session/starting.rs +++ b/crates/debugger_ui/src/session/starting.rs @@ -1,7 +1,15 @@ +use std::time::Duration; + use anyhow::Result; -use gpui::{EventEmitter, FocusHandle, Focusable, Subscription, Task}; -use ui::{div, Context, Element, ParentElement, Render, Styled}; +use gpui::{ + percentage, Animation, AnimationExt, EventEmitter, FocusHandle, Focusable, Subscription, Task, + Transformation, +}; +use ui::{ + div, v_flex, Color, Context, Element, Icon, IconName, IconSize, IntoElement, ParentElement, + Render, Styled, +}; pub(super) struct StartingState { focus_handle: FocusHandle, @@ -39,6 +47,20 @@ impl Render for StartingState { window: &mut ui::Window, cx: &mut ui::Context<'_, Self>, ) -> impl ui::IntoElement { - div().size_full().child("Starting a debug adapter") + v_flex() + .size_full() + .gap_1() + .items_center() + .child("Starting a debug adapter") + .child( + Icon::new(IconName::ArrowCircle) + .color(Color::Info) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ) + .into_any_element(), + ) } } From 9ea14ec74d8934e6c74fcf804dd722620ce11332 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:07:19 +0100 Subject: [PATCH 595/650] Touchups --- crates/debugger_ui/src/session/running.rs | 6 +++--- crates/debugger_ui/src/session/starting.rs | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index e08321365295e7..4d6f4ccaa8e88e 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -41,7 +41,7 @@ pub struct RunningState { module_list: Entity, active_thread_item: ThreadItem, _workspace: WeakEntity, - client_id: SessionId, + session_id: SessionId, variable_list: Entity, _subscriptions: Vec, stack_frame_list: Entity, @@ -380,7 +380,7 @@ impl RunningState { remote_id: None, stack_frame_list, loaded_source_list, - client_id, + session_id: client_id, show_console_indicator: false, active_thread_item: ThreadItem::Variables, } @@ -425,7 +425,7 @@ impl RunningState { } pub fn client_id(&self) -> SessionId { - self.client_id + self.session_id } pub fn thread_id(&self) -> ThreadId { diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs index dc96660b60d6ba..d47ec6285ceec5 100644 --- a/crates/debugger_ui/src/session/starting.rs +++ b/crates/debugger_ui/src/session/starting.rs @@ -21,11 +21,13 @@ pub(crate) enum StartingEvent { } impl EventEmitter for StartingState {} + impl StartingState { pub(crate) fn new(task: Task>, cx: &mut Context) -> Self { let _notify_parent = cx.spawn(move |this, mut cx| async move { task.await?; - this.update(&mut cx, |_, cx| cx.emit(StartingEvent::Finished(()))); + this.update(&mut cx, |_, cx| cx.emit(StartingEvent::Finished(()))) + .ok(); Ok(()) }); Self { From b8911725c21bcce2b00c228d54a171dd5980406c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:10:51 +0100 Subject: [PATCH 596/650] client_id -> session_id some more --- crates/debugger_ui/src/session.rs | 2 +- crates/debugger_ui/src/session/running.rs | 23 +++++++------------ .../src/session/running/loaded_source_list.rs | 4 +--- .../src/session/running/module_list.rs | 8 +++---- .../src/session/running/stack_frame_list.rs | 1 - .../src/session/running/variable_list.rs | 3 --- 6 files changed, 14 insertions(+), 27 deletions(-) diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 2403a81e1d4886..c93f3bf95b7e3b 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -86,7 +86,7 @@ impl DebugSession { match &self.mode { DebugSessionState::Inert(_) => None, DebugSessionState::Starting(_entity) => unimplemented!(), - DebugSessionState::Running(entity) => Some(entity.read(cx).client_id()), + DebugSessionState::Running(entity) => Some(entity.read(cx).session_id()), } } fn on_inert_event( diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 4d6f4ccaa8e88e..598d9cd43b3975 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -319,7 +319,7 @@ impl RunningState { #[allow(clippy::too_many_arguments)] pub fn new( session: Entity, - client_id: SessionId, + session_id: SessionId, thread_id: ThreadId, debug_panel: &Entity, workspace: WeakEntity, @@ -332,24 +332,17 @@ impl RunningState { StackFrameList::new(workspace.clone(), session.clone(), thread_id, window, cx) }); - let variable_list = cx.new(|cx| { - VariableList::new( - session.clone(), - client_id, - stack_frame_list.clone(), - window, - cx, - ) - }); + let variable_list = + cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); - let module_list = cx.new(|cx| ModuleList::new(session.clone(), client_id, cx)); + let module_list = cx.new(|cx| ModuleList::new(session.clone(), session_id, cx)); - let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), client_id, cx)); + let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx)); let console = cx.new(|cx| { Console::new( session.clone(), - client_id, + session_id, stack_frame_list.clone(), variable_list.clone(), window, @@ -380,7 +373,7 @@ impl RunningState { remote_id: None, stack_frame_list, loaded_source_list, - session_id: client_id, + session_id, show_console_indicator: false, active_thread_item: ThreadItem::Variables, } @@ -424,7 +417,7 @@ impl RunningState { &self.session } - pub fn client_id(&self) -> SessionId { + pub fn session_id(&self) -> SessionId { self.session_id } diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index db88bd359d9c7f..2d0eeb26270104 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -9,11 +9,10 @@ pub struct LoadedSourceList { focus_handle: FocusHandle, _subscription: Subscription, session: Entity, - client_id: SessionId, } impl LoadedSourceList { - pub fn new(session: Entity, client_id: SessionId, cx: &mut Context) -> Self { + pub fn new(session: Entity, cx: &mut Context) -> Self { let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); @@ -43,7 +42,6 @@ impl LoadedSourceList { session, focus_handle, _subscription, - client_id, } } diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 76958234bd67e0..6e01285bc8725c 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -8,11 +8,11 @@ pub struct ModuleList { focus_handle: FocusHandle, _subscription: Subscription, session: Entity, - client_id: SessionId, + session_id: SessionId, } impl ModuleList { - pub fn new(session: Entity, client_id: SessionId, cx: &mut Context) -> Self { + pub fn new(session: Entity, session_id: SessionId, cx: &mut Context) -> Self { let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); @@ -40,7 +40,7 @@ impl ModuleList { session, focus_handle, _subscription, - client_id, + session_id, } } @@ -101,7 +101,7 @@ use util::maybe; #[cfg(any(test, feature = "test-support"))] impl ModuleList { pub fn modules(&self, cx: &mut Context) -> Vec { - let Some(state) = self.session.read(cx).client_state(self.client_id) else { + let Some(state) = self.session.read(cx).client_state(self.session_id) else { return vec![]; }; diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 790009bc7d69d5..e3f49eb09230fd 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -1,7 +1,6 @@ use std::path::Path; use anyhow::{anyhow, Result}; -use dap::client::SessionId; use gpui::{ list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task, WeakEntity, diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index 56c1174abc2336..db0ac3fb9554d8 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -330,7 +330,6 @@ pub struct VariableList { focus_handle: FocusHandle, open_entries: Vec, session: Entity, - client_id: SessionId, _subscriptions: Vec, set_variable_editor: Entity, selection: Option, @@ -346,7 +345,6 @@ pub struct VariableList { impl VariableList { pub fn new( session: Entity, - client_id: SessionId, stack_frame_list: Entity, window: &mut Window, cx: &mut Context, @@ -389,7 +387,6 @@ impl VariableList { selection: None, stack_frame_list, set_variable_editor, - client_id, open_context_menu: None, set_variable_state: None, fetch_variables_task: None, From 1c10133f1e0ba467f43ca6908c7e6b33747981c3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:11:21 +0100 Subject: [PATCH 597/650] ^ --- crates/debugger_ui/src/session/running.rs | 2 +- crates/debugger_ui/src/session/running/module_list.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 598d9cd43b3975..b65ee2352cee40 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -335,7 +335,7 @@ impl RunningState { let variable_list = cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); - let module_list = cx.new(|cx| ModuleList::new(session.clone(), session_id, cx)); + let module_list = cx.new(|cx| ModuleList::new(session.clone(), cx)); let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx)); diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 6e01285bc8725c..d45904707bd170 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -8,11 +8,10 @@ pub struct ModuleList { focus_handle: FocusHandle, _subscription: Subscription, session: Entity, - session_id: SessionId, } impl ModuleList { - pub fn new(session: Entity, session_id: SessionId, cx: &mut Context) -> Self { + pub fn new(session: Entity, cx: &mut Context) -> Self { let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); From b4c2de0838b0b190ac9efdbea4cd81e77252205f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:12:37 +0100 Subject: [PATCH 598/650] Oops --- crates/debugger_ui/src/session/running/module_list.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index d45904707bd170..7f5cd2b0b199fd 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -39,7 +39,6 @@ impl ModuleList { session, focus_handle, _subscription, - session_id, } } From 27bda501a19a2c0a05cbfe6ee41ef5b0031ede70 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:22:04 +0100 Subject: [PATCH 599/650] Remove another session id --- crates/debugger_ui/src/session/running.rs | 1 - crates/debugger_ui/src/session/running/console.rs | 3 --- 2 files changed, 4 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index b65ee2352cee40..0d0ee4c15b1001 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -342,7 +342,6 @@ impl RunningState { let console = cx.new(|cx| { Console::new( session.clone(), - session_id, stack_frame_list.clone(), variable_list.clone(), window, diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index e06745bbaefa2c..02b573510bb5ce 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -34,7 +34,6 @@ pub struct Console { console: Entity, query_bar: Entity, session: Entity, - client_id: SessionId, _subscriptions: Vec, variable_list: Entity, stack_frame_list: Entity, @@ -43,7 +42,6 @@ pub struct Console { impl Console { pub fn new( session: Entity, - client_id: SessionId, stack_frame_list: Entity, variable_list: Entity, window: &mut Window, @@ -90,7 +88,6 @@ impl Console { variable_list, _subscriptions, stack_frame_list, - client_id, groups: Vec::default(), } } From 2ca2942ffaf5aea91ebcf5d359876c23d686d10e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:46:16 +0100 Subject: [PATCH 600/650] Adjust DAP store to return session from start methods --- crates/dap/src/client.rs | 20 ++++----- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/debugger/dap_store.rs | 44 +++++-------------- crates/project/src/debugger/session.rs | 17 +++---- crates/project/src/project.rs | 3 +- 5 files changed, 32 insertions(+), 54 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 4cf340c2d46df2..6adb2fd7d441dd 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -48,16 +48,15 @@ pub struct DebugAdapterClient { transport_delegate: TransportDelegate, } +pub type DapMessageHandler = Box; + impl DebugAdapterClient { - pub async fn start( + pub async fn start( id: SessionId, binary: DebugAdapterBinary, - message_handler: F, + message_handler: DapMessageHandler, cx: AsyncApp, - ) -> Result - where - F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, - { + ) -> Result { let ((server_rx, server_tx), transport_delegate) = TransportDelegate::start(&binary, cx.clone()).await?; let this = Self { @@ -92,16 +91,13 @@ impl DebugAdapterClient { }) } - async fn handle_receive_messages( + async fn handle_receive_messages( client_id: SessionId, server_rx: Receiver, client_tx: Sender, - mut event_handler: F, + mut event_handler: DapMessageHandler, cx: &mut AsyncApp, - ) -> Result<()> - where - F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, - { + ) -> Result<()> { let result = loop { let message = match server_rx.recv().await { Ok(message) => message, diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index ec7db5e7470d1c..d24c1533e00f56 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -504,7 +504,7 @@ impl BreakpointStore { .as_path(), )) } else { - Some(project_path.clone().path) + Some(project_path.path.clone()) } }) else { continue; diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 24da8c6f382d6c..d039aa0082f45a 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -555,9 +555,7 @@ impl DapStore { cx.spawn(|this, mut cx| async move { let adapter = build_adapter(&config.kind).await?; - if !unimplemented!("adapter.supports_attach()") - && matches!(config.request, DebugRequestType::Attach(_)) - { + if !config.supports_attach && matches!(config.request, DebugRequestType::Attach(_)) { bail!("Debug adapter does not support `attach` request"); } @@ -596,7 +594,8 @@ impl DapStore { } }; - let mut client = DebugAdapterClient::start(session_id, binary, |_, _| {}, cx).await?; + let mut client = + DebugAdapterClient::start(session_id, binary, Box::new(|_, _| {}), cx).await?; Ok(Arc::new(client)) }) @@ -607,9 +606,9 @@ impl DapStore { config: DebugAdapterConfig, worktree: &Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { let Some(local_store) = self.as_local() else { - return Task::ready(Err(anyhow!("cannot start session on remote side"))); + unimplemented!("Starting session on remote side"); }; let delegate = DapAdapterDelegate::new( @@ -625,32 +624,13 @@ impl DapStore { }), ); - let start_client_task = self.start_client_internal(delegate, config.clone(), cx); - - cx.spawn(|this, mut cx| async move { - let client = match start_client_task.await { - Ok(client) => client, - Err(error) => { - this.update(&mut cx, |_, cx| { - cx.emit(DapStoreEvent::Notification(error.to_string())); - }) - .log_err(); - - return Err(error); - } - }; - - this.update(&mut cx, |store, cx| { - let session_id = client.id(); - - unimplemented!("store.clients.insert(session_id, client);"); - - cx.emit(DapStoreEvent::DebugClientStarted(session_id)); - cx.notify(); - - client - }) - }) + Session::local( + self.breakpoint_store.clone(), + local_store.next_session_id(), + delegate, + config, + cx, + ) } pub fn configuration_done( diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index b680e206711cbe..4eaaf3d04db375 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -11,7 +11,7 @@ use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; use collections::{HashMap, IndexMap}; use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; -use dap::client::{DebugAdapterClient, SessionId}; +use dap::client::{DapMessageHandler, DebugAdapterClient, SessionId}; use dap::{ messages::Message, Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, SteppingGranularity, @@ -160,17 +160,14 @@ struct LocalMode { } impl LocalMode { - fn new( + fn new( session_id: SessionId, breakpoint_store: Entity, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, - message_handler: F, + message_handler: DapMessageHandler, cx: AsyncApp, - ) -> Task> - where - F: FnMut(Message, &mut App) + 'static + Send + Sync + Clone, - { + ) -> Task> { cx.spawn(move |mut cx| async move { let adapter = build_adapter(&disposition.kind).await?; @@ -221,6 +218,10 @@ impl LocalMode { cx.background_executor().clone(), ) .await?; + let breakpoints = + breakpoint_store.update(&mut cx, |this, cx| this.all_breakpoints(true, cx))?; + + for (path, breakpoints) in breakpoints {} Ok((this, capabilities)) }) } @@ -377,7 +378,7 @@ impl Session { breakpoints, config.clone(), delegate, - |_, _| {}, + Box::new(|_, _| {}), cx.clone(), ) .await?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7211e83d2a4bb4..46934203dd4464 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -50,6 +50,7 @@ use debounced_delay::DebouncedDelay; use debugger::{ breakpoint_store::{BreakpointStore, BreakpointStoreEvent, SerializedBreakpoint}, dap_store::{DapStore, DapStoreEvent}, + session::Session, }; pub use environment::ProjectEnvironment; use futures::{ @@ -1343,7 +1344,7 @@ impl Project { &mut self, config: DebugAdapterConfig, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { let worktree = maybe!({ if let Some(cwd) = &config.cwd { Some(self.find_worktree(cwd.as_path(), cx)?.0) From b159e3c1636fe000dcdb8b712eb229e3d4dbebaf Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:51:09 +0100 Subject: [PATCH 601/650] Remove start_client_internal --- crates/project/src/debugger/dap_store.rs | 61 ------------------------ 1 file changed, 61 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index d039aa0082f45a..c50e5b3b79faa9 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -540,67 +540,6 @@ impl DapStore { // }) // } - fn start_client_internal( - &mut self, - delegate: DapAdapterDelegate, - config: DebugAdapterConfig, - cx: &mut Context, - ) -> Task>> { - let Some(local_store) = self.as_local_mut() else { - return Task::ready(Err(anyhow!("cannot start client on remote side"))); - }; - - let session_id = local_store.next_session_id(); - - cx.spawn(|this, mut cx| async move { - let adapter = build_adapter(&config.kind).await?; - - if !config.supports_attach && matches!(config.request, DebugRequestType::Attach(_)) { - bail!("Debug adapter does not support `attach` request"); - } - - let binary = cx.update(|cx| { - let name = DebugAdapterName::from(adapter.name().as_ref()); - - ProjectSettings::get_global(cx) - .dap - .get(&name) - .and_then(|s| s.binary.as_ref().map(PathBuf::from)) - })?; - - let (adapter, binary) = match adapter - .get_binary(&delegate, &config, binary, &mut cx) - .await - { - Err(error) => { - delegate.update_status( - adapter.name(), - DapStatus::Failed { - error: error.to_string(), - }, - ); - - return Err(error); - } - Ok(mut binary) => { - delegate.update_status(adapter.name(), DapStatus::None); - - let shell_env = delegate.shell_env().await; - let mut envs = binary.envs.unwrap_or_default(); - envs.extend(shell_env); - binary.envs = Some(envs); - - (adapter, binary) - } - }; - - let mut client = - DebugAdapterClient::start(session_id, binary, Box::new(|_, _| {}), cx).await?; - - Ok(Arc::new(client)) - }) - } - pub fn start_debug_session( &mut self, config: DebugAdapterConfig, From 4d2ecb2cdfde12e9b00e78f24901638498a4458e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:52:48 +0100 Subject: [PATCH 602/650] Remove some more code --- crates/project/src/debugger/dap_store.rs | 53 ------------------------ 1 file changed, 53 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index c50e5b3b79faa9..48f1fab392c349 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -487,59 +487,6 @@ impl DapStore { } } - // fn reconnect_client( - // &mut self, - // adapter: Arc, - // binary: DebugAdapterBinary, - // config: DebugAdapterConfig, - // cx: &mut Context, - // ) -> Task> { - // if !config.supports_attach && matches!(config.request, DebugRequestType::Attach(_)) { - // return Task::ready(Err(anyhow!( - // "Debug adapter does not support `attach` request" - // ))); - // } - - // let session_id = self.as_local().unwrap().next_session_id(); - - // cx.spawn(|dap_store, mut cx| async move { - // let mut client = DebugAdapterClient::new(session_id, adapter, binary, &cx); - - // client - // .reconnect( - // { - // let dap_store = dap_store.clone(); - // move |message, cx| { - // dap_store - // .update(cx, |_, cx| { - // cx.emit(DapStoreEvent::DebugClientEvent { session_id, message }) - // }) - // .log_err(); - // } - // }, - // &mut cx, - // ) - // .await?; - - // dap_store.update(&mut cx, |store, cx| { - // cx.new(|cx| { - // let client_state = - // debugger::client::Client::local(Arc::new(client), capabilities); - // }); - - // store.clients.insert(Arc::new(client), session_id); - - // // don't emit this event ourself in tests, so we can add request, - // // response and event handlers for this client - // if !cfg!(any(test, feature = "test-support")) { - // cx.emit(DapStoreEvent::DebugClientStarted(session_id)); - // } - - // cx.notify(); - // }) - // }) - // } - pub fn start_debug_session( &mut self, config: DebugAdapterConfig, From 5908d5d5fd548c3c59e4481e95ee4c83c01d2734 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 18 Feb 2025 00:57:06 -0500 Subject: [PATCH 603/650] Cherry pick from remove-dap-session --- crates/collab/src/rpc.rs | 5 - crates/collab/src/tests/debug_panel_tests.rs | 96 ++----------- crates/dap/src/adapters.rs | 4 + crates/dap/src/client.rs | 132 ++++++++---------- crates/dap/src/lib.rs | 13 ++ crates/dap/src/transport.rs | 32 ++--- crates/dap_adapters/src/custom.rs | 2 + crates/dap_adapters/src/go.rs | 2 + crates/dap_adapters/src/javascript.rs | 2 + crates/dap_adapters/src/lldb.rs | 2 + crates/dap_adapters/src/php.rs | 2 + crates/dap_adapters/src/python.rs | 2 + crates/debugger_ui/src/debugger_panel.rs | 3 +- .../src/session/running/module_list.rs | 8 +- crates/debugger_ui/src/tests/attach_modal.rs | 38 +---- crates/debugger_ui/src/tests/console.rs | 36 +---- .../debugger_ui/src/tests/debugger_panel.rs | 120 ++-------------- crates/debugger_ui/src/tests/module_list.rs | 20 +-- .../debugger_ui/src/tests/stack_frame_list.rs | 36 +---- crates/debugger_ui/src/tests/variable_list.rs | 60 +------- crates/project/src/debugger/dap_store.rs | 68 +++++---- crates/project/src/debugger/session.rs | 6 +- crates/task/src/task_template.rs | 4 +- 23 files changed, 188 insertions(+), 505 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 19221ae5733f72..530d0150b21892 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -446,7 +446,6 @@ impl Server { ) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) - .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) .add_request_handler(forward_mutating_project_request::) .add_message_handler( @@ -457,10 +456,6 @@ impl Server { ) .add_message_handler( broadcast_project_message_from_host::, - ) - .add_message_handler(broadcast_project_message_from_host::) - .add_request_handler( - forward_mutating_project_request::, ); Arc::new(server) diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 5b54ef14a26cf7..a6d286d343d54b 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -206,17 +206,7 @@ async fn test_debug_panel_item_opens_on_remote( remote_cx.run_until_parked(); let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -299,17 +289,7 @@ async fn test_active_debug_panel_item_set_on_join_project( host_cx.run_until_parked(); let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -410,17 +390,7 @@ async fn test_debug_panel_remote_button_presses( let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (_, client) = task.await.unwrap(); @@ -747,17 +717,7 @@ async fn test_restart_stack_frame(host_cx: &mut TestAppContext, remote_cx: &mut let called_restart_frame = Arc::new(AtomicBool::new(false)); let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -899,17 +859,7 @@ async fn test_updated_breakpoints_send_to_dap( }; let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -1124,17 +1074,7 @@ async fn test_module_list( let (_client_remote, remote_workspace, _remote_project, remote_cx) = remote_zed.expand().await; let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -1827,17 +1767,7 @@ async fn test_ignore_breakpoints( remote_cx.run_until_parked(); let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -2253,17 +2183,7 @@ async fn test_debug_panel_console(host_cx: &mut TestAppContext, remote_cx: &mut remote_cx.run_until_parked(); let task = host_project.update(host_cx, |project, cx| { - project.start_debug_session( - dap::DebugAdapterConfig { - label: "test config".into(), - kind: dap::DebugAdapterKind::Fake, - request: dap::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 4758640698ede9..82ba49ce877945 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -103,6 +103,9 @@ pub struct DebugAdapterBinary { pub envs: Option>, pub cwd: Option, pub connection: Option, + #[cfg(any(test, feature = "test-support"))] + // todo(debugger) Find a way to remove this. It's a hack for FakeTransport + pub is_fake: bool, } pub struct AdapterVersion { @@ -337,6 +340,7 @@ impl DebugAdapter for FakeAdapter { port: None, timeout: None, }), + is_fake: true, envs: None, cwd: None, }) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 6adb2fd7d441dd..28371ad9112008 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,5 +1,5 @@ use crate::{ - adapters::{DebugAdapter, DebugAdapterBinary}, + adapters::{DebugAdapter, DebugAdapterBinary, TcpArguments}, transport::{IoKind, LogKind, TransportDelegate}, }; use anyhow::{anyhow, Result}; @@ -12,6 +12,7 @@ use gpui::{App, AsyncApp, BackgroundExecutor}; use smol::channel::{Receiver, Sender}; use std::{ hash::Hash, + net::Ipv4Addr, sync::{ atomic::{AtomicU64, Ordering}, Arc, @@ -226,7 +227,7 @@ impl DebugAdapterClient { { let transport = self.transport_delegate.transport(); - transport.as_fake().on_request::(handler).await; + transport.on_request::(handler).await; } #[cfg(any(test, feature = "test-support"))] @@ -247,7 +248,7 @@ impl DebugAdapterClient { { let transport = self.transport_delegate.transport(); - transport.as_fake().on_response::(handler).await; + transport.on_response::(handler).await; } #[cfg(any(test, feature = "test-support"))] @@ -291,21 +292,25 @@ mod tests { pub async fn test_initialize_client(cx: &mut TestAppContext) { init_test(cx); - let mut client = DebugAdapterClient::new( + let mut client = DebugAdapterClient::start( crate::client::SessionId(1), DebugAdapterBinary { command: "command".into(), arguments: Default::default(), envs: Default::default(), - connection: Some(TcpArguments { + connection: Some(crate::adapters::TcpArguments { host: Ipv4Addr::LOCALHOST, port: None, timeout: None, }), + is_fake: true, cwd: None, }, - &mut cx.to_async(), - ); + |_, _| panic!("Did not expect to hit this code path"), + cx.to_async(), + ) + .await + .unwrap(); client .on_request::(move |_, _| { @@ -316,14 +321,6 @@ mod tests { }) .await; - client - .start( - |_, _| panic!("Did not expect to hit this code path"), - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.run_until_parked(); let response = client @@ -365,44 +362,39 @@ mod tests { pub async fn test_calls_event_handler(cx: &mut TestAppContext) { init_test(cx); - let adapter = Arc::new(FakeAdapter::new()); let called_event_handler = Arc::new(AtomicBool::new(false)); - let mut client = DebugAdapterClient::new( + let client = DebugAdapterClient::start( crate::client::SessionId(1), DebugAdapterBinary { command: "command".into(), arguments: Default::default(), envs: Default::default(), - connection: Some(TCPArguments { + connection: Some(TcpArguments { host: Ipv4Addr::LOCALHOST, port: None, - path: None, + timeout: None, }), + is_fake: true, cwd: None, }, - &mut cx.to_async(), - ); - - client - .start( - { - let called_event_handler = called_event_handler.clone(); - move |event, _| { - called_event_handler.store(true, Ordering::SeqCst); - - assert_eq!( - Message::Event(Box::new(Events::Initialized(Some( - Capabilities::default() - )))), - event - ); - } - }, - &mut cx.to_async(), - ) - .await - .unwrap(); + { + let called_event_handler = called_event_handler.clone(); + move |event, _| { + called_event_handler.store(true, Ordering::SeqCst); + + assert_eq!( + Message::Event(Box::new(Events::Initialized( + Some(Capabilities::default()) + ))), + event + ); + } + }, + cx.to_async(), + ) + .await + .unwrap(); cx.run_until_parked(); @@ -424,50 +416,44 @@ mod tests { pub async fn test_calls_event_handler_for_reverse_request(cx: &mut TestAppContext) { init_test(cx); - let adapter = Arc::new(FakeAdapter::new()); let called_event_handler = Arc::new(AtomicBool::new(false)); - let mut client = DebugAdapterClient::new( + let client = DebugAdapterClient::start( crate::client::SessionId(1), DebugAdapterBinary { command: "command".into(), arguments: Default::default(), envs: Default::default(), - - connection: Some(TCPArguments { + connection: Some(TcpArguments { host: Ipv4Addr::LOCALHOST, port: None, - path: None, + timeout: None, }), + is_fake: true, cwd: None, }, - &mut cx.to_async(), - ); - - client - .start( - { - let called_event_handler = called_event_handler.clone(); - move |event, _| { - called_event_handler.store(true, Ordering::SeqCst); - - assert_eq!( - Message::Request(dap_types::messages::Request { - seq: 1, - command: RunInTerminal::COMMAND.into(), - arguments: Some(json!({ - "cwd": "/project/path/src", - "args": ["node", "test.js"], - })) - }), - event - ); - } - }, - &mut cx.to_async(), - ) - .await - .unwrap(); + { + let called_event_handler = called_event_handler.clone(); + move |event, _| { + called_event_handler.store(true, Ordering::SeqCst); + + assert_eq!( + Message::Request(dap_types::messages::Request { + seq: 1, + command: RunInTerminal::COMMAND.into(), + arguments: Some(json!({ + "cwd": "/project/path/src", + "args": ["node", "test.js"], + })) + }), + event + ); + } + }, + cx.to_async(), + ) + .await + .unwrap(); cx.run_until_parked(); diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs index fc458dc8763c88..d83c0931cb0efc 100644 --- a/crates/dap/src/lib.rs +++ b/crates/dap/src/lib.rs @@ -9,3 +9,16 @@ pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType}; #[cfg(any(test, feature = "test-support"))] pub use adapters::FakeAdapter; + +#[cfg(any(test, feature = "test-support"))] +pub fn test_config() -> DebugAdapterConfig { + DebugAdapterConfig { + label: "test config".into(), + kind: DebugAdapterKind::Fake, + request: DebugRequestType::Launch, + program: None, + supports_attach: false, + cwd: None, + initialize_args: None, + } +} diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 96f6d1c3bf912a..dceca0d2e3e133 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -78,7 +78,7 @@ enum Transport { impl Transport { async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> { #[cfg(any(test, feature = "test-support"))] - if binary.kind == DebugAdapterKind::Fake { + if binary.is_fake { return FakeTransport::start(cx) .await .map(|(transports, fake)| (transports, Self::Fake(fake))); @@ -133,7 +133,7 @@ pub(crate) struct TransportDelegate { impl Transport { #[cfg(any(test, feature = "test-support"))] fn fake(args: DebugAdapterBinary) -> Self { - let this = Self::Fake(FakeTransport::new()); + todo!() } } @@ -523,8 +523,8 @@ impl TransportDelegate { } #[cfg(any(test, feature = "test-support"))] - pub fn transport(&self) -> &Arc { - &self.transport + pub fn transport(&self) -> Arc<&FakeTransport> { + Arc::new(self.transport.as_fake()) } pub fn add_log_handler(&self, f: F, kind: LogKind) @@ -810,21 +810,11 @@ impl FakeTransport { .insert(R::COMMAND, Box::new(handler)); } - async fn reconnect(&self, cx: AsyncApp) -> Result { - self.start( - &DebugAdapterBinary { - command: "command".into(), - arguments: None, - envs: None, - cwd: None, - connection: TcpTransport::new(None, None), - }, - cx, - ) - .await + async fn reconnect(&self, cx: AsyncApp) -> Result<(TransportPipe, Self)> { + FakeTransport::start(cx).await } - async fn start(cx: AsyncApp) -> Result { + async fn start(cx: AsyncApp) -> Result<(TransportPipe, Self)> { let this = Self { request_handlers: Arc::new(Mutex::new(HashMap::default())), response_handlers: Arc::new(Mutex::new(HashMap::default())), @@ -925,11 +915,9 @@ impl FakeTransport { }) .detach(); - Ok(TransportPipe::new( - Box::new(stdin_writer), - Box::new(stdout_reader), - None, - None, + Ok(( + TransportPipe::new(Box::new(stdin_writer), Box::new(stdout_reader), None, None), + this, )) } diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index a761dfb541081b..5eaec5530d445f 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -51,6 +51,8 @@ impl DebugAdapter for CustomDebugAdapter { .map(|args| args.iter().map(OsString::from).collect()), cwd: config.cwd.clone(), envs: self.custom_args.envs.clone(), + #[cfg(any(test, feature = "test-support"))] + is_fake: false, connection, }; Ok(ret) diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index 95e7d453b2759c..000dcc60aa1be3 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -83,6 +83,8 @@ impl DebugAdapter for GoDebugAdapter { cwd: config.cwd.clone(), envs: None, connection: None, + #[cfg(any(test, feature = "test-support"))] + is_fake: false, }) } diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 61e1f8bfe14f8e..2f56ff1260a7c4 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -94,6 +94,8 @@ impl DebugAdapter for JsDebugAdapter { ]), cwd: config.cwd.clone(), envs: None, + #[cfg(any(test, feature = "test-support"))] + is_fake: false, connection: Some(adapters::TcpArguments { host: self.host, port: Some(self.port), diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index 0015bf0b64704d..ad46db4fdfea9f 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -56,6 +56,8 @@ impl DebugAdapter for LldbDebugAdapter { envs: None, cwd: config.cwd.clone(), connection: None, + #[cfg(any(test, feature = "test-support"))] + is_fake: false, }) } diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 98d7474e6f6d87..a959a296f2238f 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -95,6 +95,8 @@ impl DebugAdapter for PhpDebugAdapter { }), cwd: config.cwd.clone(), envs: None, + #[cfg(any(test, feature = "test-support"))] + is_fake: false, }) } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index bce937a6c36ea6..27735cdcf2e97b 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -128,6 +128,8 @@ impl DebugAdapter for PythonDebugAdapter { }), cwd: config.cwd.clone(), envs: None, + #[cfg(any(test, feature = "test-support"))] + is_fake: false, }) } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 52f5d6b834b8df..849d5a682ef5b2 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -190,7 +190,8 @@ impl DebugPanel { #[cfg(any(test, feature = "test-support"))] pub fn message_queue(&self) -> &HashMap> { - &self.message_queue + // &self.message_queue + unimplemented!("Should chekc session for console messagse") } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 7f5cd2b0b199fd..f228ab90252357 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -99,10 +99,8 @@ use util::maybe; #[cfg(any(test, feature = "test-support"))] impl ModuleList { pub fn modules(&self, cx: &mut Context) -> Vec { - let Some(state) = self.session.read(cx).client_state(self.session_id) else { - return vec![]; - }; - - state.update(cx, |state, cx| state.modules(cx).iter().cloned().collect()) + self.session.update(cx, |session, cx| { + session.modules(cx).iter().cloned().collect() + }) } } diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index c5161c76a8fa20..3ba3bbaad88f4f 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -33,19 +33,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Attach(AttachConfig { - process_id: Some(10), - }), - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -121,17 +109,7 @@ async fn test_show_attach_modal_and_select_process( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Attach(AttachConfig { process_id: None }), - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -232,17 +210,7 @@ async fn test_shutdown_session_when_modal_is_dismissed( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Attach(AttachConfig { process_id: None }), - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index 85db5619dbb0a7..fa6aaf58742b92 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -33,17 +33,7 @@ async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestApp let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -200,17 +190,7 @@ async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppConte let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -501,17 +481,7 @@ async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestApp let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index e1477a8c466a15..8785c9a9f1495a 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -49,17 +49,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -172,17 +162,7 @@ async fn test_we_can_only_have_one_panel_per_debug_thread( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -326,17 +306,7 @@ async fn test_client_can_open_multiple_thread_panels( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -482,17 +452,7 @@ async fn test_handle_successful_run_in_terminal_reverse_request( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -594,17 +554,7 @@ async fn test_handle_error_run_in_terminal_reverse_request( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -697,17 +647,7 @@ async fn test_handle_start_debugging_reverse_request( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -833,17 +773,7 @@ async fn test_debug_panel_item_thread_status_reset_on_failure( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -1038,17 +968,7 @@ async fn test_send_breakpoints_when_editor_has_been_saved( .unwrap(); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -1227,17 +1147,7 @@ async fn test_it_send_breakpoint_request_if_breakpoint_buffer_is_unopened( .unwrap(); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -1363,17 +1273,7 @@ async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs index 13da9e154c07db..44e887e678ed84 100644 --- a/crates/debugger_ui/src/tests/module_list.rs +++ b/crates/debugger_ui/src/tests/module_list.rs @@ -24,17 +24,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -143,7 +133,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) active_debug_panel_item(workspace, cx).update(cx, |item, cx| { item.set_thread_item(ThreadItem::Modules, cx); - let actual_modules = item.module_list().update(cx, |list, cx| list.modules(cx)); + let actual_modules = item.modules().update(cx, |list, cx| list.modules(cx)); assert_eq!(modules, actual_modules); }); @@ -175,7 +165,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) cx.run_until_parked(); active_debug_panel_item(workspace, cx).update(cx, |item, cx| { - let actual_modules = item.module_list().update(cx, |list, cx| list.modules(cx)); + let actual_modules = item.modules().update(cx, |list, cx| list.modules(cx)); assert_eq!(actual_modules.len(), 3); assert!(actual_modules.contains(&new_module)); }); @@ -203,7 +193,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) cx.run_until_parked(); active_debug_panel_item(workspace, cx).update(cx, |item, cx| { - let actual_modules = item.module_list().update(cx, |list, cx| list.modules(cx)); + let actual_modules = item.modules().update(cx, |list, cx| list.modules(cx)); assert_eq!(actual_modules.len(), 3); assert!(actual_modules.contains(&changed_module)); }); @@ -218,7 +208,7 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) cx.run_until_parked(); active_debug_panel_item(workspace, cx).update(cx, |item, cx| { - let actual_modules = item.module_list().update(cx, |list, cx| list.modules(cx)); + let actual_modules = item.modules().update(cx, |list, cx| list.modules(cx)); assert_eq!(actual_modules.len(), 2); assert!(!actual_modules.contains(&changed_module)); }); diff --git a/crates/debugger_ui/src/tests/stack_frame_list.rs b/crates/debugger_ui/src/tests/stack_frame_list.rs index 9413b8f6966c4c..83ba416a38ed1a 100644 --- a/crates/debugger_ui/src/tests/stack_frame_list.rs +++ b/crates/debugger_ui/src/tests/stack_frame_list.rs @@ -51,17 +51,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -222,17 +212,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -485,17 +465,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 9173b6c5864719..396e66ac1bfadf 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -46,17 +46,7 @@ async fn test_basic_fetch_initial_scope_and_variables( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -277,17 +267,7 @@ async fn test_fetch_variables_for_multiple_scopes( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -553,17 +533,7 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -1173,17 +1143,7 @@ async fn test_it_only_fetches_scopes_and_variables_for_the_first_stack_frame( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); @@ -1419,17 +1379,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( let cx = &mut VisualTestContext::from_window(*workspace, cx); let task = project.update(cx, |project, cx| { - project.start_debug_session( - task::DebugAdapterConfig { - label: "test config".into(), - kind: task::DebugAdapterKind::Fake, - request: task::DebugRequestType::Launch, - program: None, - cwd: None, - initialize_args: None, - }, - cx, - ) + project.start_debug_session(dap::test_config(), cx) }); let (session, client) = task.await.unwrap(); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 48f1fab392c349..7b536a89cb9eec 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -509,42 +509,54 @@ impl DapStore { env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx) }), ); + let session_id = local_store.next_session_id(); - Session::local( + let start_client_task = Session::local( self.breakpoint_store.clone(), - local_store.next_session_id(), + session_id, delegate, config, + { + let weak_store = cx.weak_entity(); + + Box::new(move |message, cx| { + weak_store + .update(cx, |store, cx| { + if let Some(session) = store.sessions.get(&session_id) { + session.update(cx, |session, cx| { + session.handle_dap_message(message); + }); + } + }) + .with_context(|| "Failed to process message from DAP server") + .log_err(); + }) + }, cx, - ) - } + ); - pub fn configuration_done( - &self, - session_id: SessionId, - cx: &mut Context, - ) -> Task> { - let Some(client) = self - .session_by_id(session_id) - .and_then(|client| client.read(cx).adapter_client()) - else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); - }; + cx.spawn(|this, mut cx| async move { + let session = match start_client_task.await { + Ok(session) => session, + Err(error) => { + this.update(&mut cx, |_, cx| { + cx.emit(DapStoreEvent::Notification(error.to_string())); + }) + .log_err(); - if self - .capabilities_by_id(session_id, cx) - .map(|caps| caps.supports_configuration_done_request) - .flatten() - .unwrap_or_default() - { - cx.background_executor().spawn(async move { - client - .request::(dap::ConfigurationDoneArguments) - .await + return Err(error); + } + }; + + this.update(&mut cx, |store, cx| { + store.sessions.insert(session_id, session.clone()); + + cx.emit(DapStoreEvent::DebugClientStarted(session_id)); + cx.notify(); + + session }) - } else { - Task::ready(Ok(())) - } + }) } pub fn new_session( diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 4eaaf3d04db375..a695657249c098 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -370,6 +370,7 @@ impl Session { session_id: SessionId, delegate: DapAdapterDelegate, config: DebugAdapterConfig, + message_handler: DapMessageHandler, cx: &mut App, ) -> Task>> { cx.spawn(move |mut cx| async move { @@ -378,10 +379,11 @@ impl Session { breakpoints, config.clone(), delegate, - Box::new(|_, _| {}), + message_handler, cx.clone(), ) .await?; + cx.new(|_| Self { mode: Mode::Local(mode), id: session_id, @@ -425,6 +427,8 @@ impl Session { self.config.clone() } + pub(crate) fn handle_dap_message(&mut self, message: Message) {} + pub(crate) fn _wait_for_request( &self, request: R, diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 7a5969c75a8fa2..baa3e59e2f592e 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -109,6 +109,7 @@ mod deserialization_tests { kind: DebugAdapterKind::Python(TCPHost::default()), request: crate::DebugRequestType::Launch, program: Some("main".to_string()), + supports_attach: false, cwd: None, initialize_args: None, }; @@ -116,7 +117,8 @@ mod deserialization_tests { "label": "test config", "type": "debug", "adapter": "python", - "program": "main" + "program": "main", + "supports_attach": false, }); let task_type: TaskType = From 4e0408ab34e93d943247a2ee2562f89da0e8bc3e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:52:14 +0100 Subject: [PATCH 604/650] Start wiring in running state Co-authored-by: Anthony --- crates/debugger_ui/src/attach_modal.rs | 8 ++- crates/debugger_ui/src/debugger_panel.rs | 65 ++++++++++-------- crates/debugger_ui/src/session.rs | 56 ++++++++++++--- crates/debugger_ui/src/session/inert.rs | 21 +++++- crates/debugger_ui/src/session/starting.rs | 18 ++--- crates/project/src/debugger/dap_store.rs | 80 +--------------------- crates/project/src/debugger/session.rs | 2 +- crates/project/src/project.rs | 2 +- 8 files changed, 123 insertions(+), 129 deletions(-) diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index 37d2d04b11c61b..7b3834ba72b0d4 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -215,13 +215,15 @@ impl PickerDelegate for AttachModalDelegate { return cx.emit(DismissEvent); }; - self.dap_store.update(cx, |store, cx| { + unimplemented!( + r#"self.dap_store.update(cx, |store, cx| {{ store .attach(self.client_id, candidate.pid, cx) .detach_and_log_err(cx); - }); + }})"# + ); - cx.emit(DismissEvent); + // cx.emit(DismissEvent); } fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 849d5a682ef5b2..cf8db8cf61dea3 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -21,6 +21,7 @@ use project::{ session::ThreadId, }, terminals::TerminalKind, + Project, }; use rpc::proto::{self, UpdateDebugAdapter}; use serde_json::Value; @@ -57,7 +58,7 @@ actions!(debug_panel, [ToggleFocus]); pub struct DebugPanel { size: Pixels, pane: Entity, - + project: WeakEntity, workspace: WeakEntity, _subscriptions: Vec, } @@ -69,6 +70,7 @@ impl DebugPanel { cx: &mut Context, ) -> Entity { cx.new(|cx| { + let project = workspace.project().clone(); let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.weak_handle(), @@ -84,31 +86,38 @@ impl DebugPanel { pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_window, _cx| true); pane.set_close_pane_if_empty(true, cx); - pane.set_render_tab_bar_buttons(cx, |_, _, cx| { - ( - None, - Some( - h_flex() - .child( - IconButton::new("new-debug-session", IconName::Plus) - .icon_size(IconSize::Small) - .on_click(cx.listener(|pane, _, window, cx| { - pane.add_item( - Box::new(DebugSession::inert(cx)), - false, - false, - None, - window, - cx, - ); - })), - ) - .into_any_element(), - ), - ) + pane.set_render_tab_bar_buttons(cx, { + let project = project.clone(); + move |_, _, cx| { + let project = project.clone(); + ( + None, + Some( + h_flex() + .child( + IconButton::new("new-debug-session", IconName::Plus) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |pane, _, window, cx| { + pane.add_item( + Box::new(DebugSession::inert( + project.clone(), + cx, + )), + false, + false, + None, + window, + cx, + ); + })), + ) + .into_any_element(), + ), + ) + } }); pane.add_item( - Box::new(DebugSession::inert(cx)), + Box::new(DebugSession::inert(project.clone(), cx)), false, false, None, @@ -118,8 +127,6 @@ impl DebugPanel { pane }); - let project = workspace.project().clone(); - let _subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe_in(&pane, window, Self::handle_pane_event), @@ -129,6 +136,7 @@ impl DebugPanel { pane, size: px(300.), _subscriptions, + project: project.downgrade(), workspace: workspace.weak_handle(), }; @@ -327,10 +335,13 @@ impl Panel for DebugPanel { } fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { if active && self.pane.read(cx).items_len() == 0 { + let Some(project) = self.project.clone().upgrade() else { + return; + }; // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items). self.pane.update(cx, |this, cx| { this.add_item( - Box::new(DebugSession::inert(cx)), + Box::new(DebugSession::inert(project, cx)), false, false, None, diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index c93f3bf95b7e3b..0896105b2e0960 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -2,8 +2,11 @@ mod inert; mod running; mod starting; +use std::time::Duration; + use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; +use anyhow::anyhow; use dap::{ client::SessionId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, ThreadEvent, @@ -15,9 +18,12 @@ use inert::{InertEvent, InertState}; use project::debugger::session::Session; use project::debugger::session::{ThreadId, ThreadStatus}; +use project::debugger::dap_store::DapStore; +use project::worktree_store::WorktreeStore; +use project::Project; use rpc::proto::{self, PeerId}; use settings::Settings; -use starting::StartingState; +use starting::{StartingEvent, StartingState}; use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, Tooltip}; use workspace::{ item::{self, Item, ItemEvent}, @@ -33,6 +39,8 @@ enum DebugSessionState { pub struct DebugSession { remote_id: Option, mode: DebugSessionState, + dap_store: WeakEntity, + worktree_store: WeakEntity, _subscriptions: [Subscription; 1], } #[derive(Debug)] @@ -70,14 +78,18 @@ impl ThreadItem { } impl DebugSession { - pub(super) fn inert(cx: &mut App) -> Entity { + pub(super) fn inert(project: Entity, cx: &mut App) -> Entity { let inert = cx.new(|cx| InertState::new(cx)); - + let project = project.read(cx); + let dap_store = project.dap_store().downgrade(); + let worktree_store = project.worktree_store().downgrade(); cx.new(|cx| { let _subscriptions = [cx.subscribe(&inert, Self::on_inert_event)]; Self { remote_id: None, mode: DebugSessionState::Inert(inert), + dap_store, + worktree_store, _subscriptions, } }) @@ -92,16 +104,44 @@ impl DebugSession { fn on_inert_event( &mut self, _: Entity, - _: &InertEvent, + event: &InertEvent, + cx: &mut Context<'_, Self>, + ) { + let dap_store = self.dap_store.clone(); + let InertEvent::Spawned { config } = event; + let config = config.clone(); + let worktree = self + .worktree_store + .update(cx, |this, _| this.worktrees().next()) + .ok() + .flatten() + .expect("worktree-less project"); + let Ok(task) = dap_store.update(cx, |store, cx| store.new_session(config, &worktree, cx)) + else { + return; + }; + let starting = cx.new(|cx| StartingState::new(task, cx)); + self._subscriptions = [cx.subscribe(&starting, Self::on_starting_event)]; + self.mode = DebugSessionState::Starting(starting); + } + fn on_starting_event( + &mut self, + _: Entity, + event: &StartingEvent, cx: &mut Context<'_, Self>, ) { - self.mode = DebugSessionState::Starting(cx.new(|cx| { + let StartingEvent::Finished(session) = event; + let session = session.as_ref().unwrap(); + let starting = cx.new(|cx| { + let timer = cx.background_executor().timer(Duration::from_secs(3)); let task = cx.background_executor().spawn(async move { - std::future::pending::<()>().await; - Ok(()) + timer.await; + Err(anyhow!("Foo")) }); StartingState::new(task, cx) - })); + }); + //self._subscriptions = [cx.subscribe(Self::on_starting_event)]; + //self.mode = DebugSessionState::Running(starting); } } impl EventEmitter for DebugSession {} diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 1ec1f5f58fa803..35e9d911b46a08 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,3 +1,4 @@ +use dap::{DebugAdapterConfig, DebugRequestType}; use gpui::{App, EventEmitter, FocusHandle, Focusable}; use ui::{ div, h_flex, v_flex, Button, ButtonCommon, ButtonStyle, Clickable, Context, ContextMenu, @@ -24,7 +25,7 @@ impl Focusable for InertState { } pub(crate) enum InertEvent { - Spawned { config: () }, + Spawned { config: DebugAdapterConfig }, } impl EventEmitter for InertState {} @@ -74,7 +75,23 @@ impl Render for InertState { Button::new("launch-dap", "Launch") .style(ButtonStyle::Filled) .on_click(cx.listener(|_, _, _, cx| { - cx.emit(InertEvent::Spawned { config: () }); + cx.emit(InertEvent::Spawned { + config: DebugAdapterConfig { + label: "hard coded".into(), + kind: dap::DebugAdapterKind::Python(task::TCPHost { + port: None, + host: None, + timeout: None, + }), + request: DebugRequestType::Launch, + program: Some( + "/Users/hiro/Projects/zed/test_debug_file.py".into(), + ), + cwd: None, + initialize_args: None, + supports_attach: false, + }, + }); })), ) .child(Button::new("attach-dap", "Attach").style(ButtonStyle::Filled)), diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs index d47ec6285ceec5..5bdde4ce31fec7 100644 --- a/crates/debugger_ui/src/session/starting.rs +++ b/crates/debugger_ui/src/session/starting.rs @@ -3,9 +3,10 @@ use std::time::Duration; use anyhow::Result; use gpui::{ - percentage, Animation, AnimationExt, EventEmitter, FocusHandle, Focusable, Subscription, Task, - Transformation, + percentage, Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, + Subscription, Task, Transformation, }; +use project::debugger::session::Session; use ui::{ div, v_flex, Color, Context, Element, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled, @@ -13,22 +14,23 @@ use ui::{ pub(super) struct StartingState { focus_handle: FocusHandle, - _notify_parent: Task>, + _notify_parent: Task<()>, } pub(crate) enum StartingEvent { - Finished(()), + Finished(Result>), } impl EventEmitter for StartingState {} impl StartingState { - pub(crate) fn new(task: Task>, cx: &mut Context) -> Self { + pub(crate) fn new(task: Task>>, cx: &mut Context) -> Self { let _notify_parent = cx.spawn(move |this, mut cx| async move { - task.await?; - this.update(&mut cx, |_, cx| cx.emit(StartingEvent::Finished(()))) + dbg!("Waiting for session to start"); + let entity = task.await; + dbg!(entity.is_err()); + this.update(&mut cx, |_, cx| cx.emit(StartingEvent::Finished(entity))) .ok(); - Ok(()) }); Self { focus_handle: cx.focus_handle(), diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 7b536a89cb9eec..5a4ccff4d03de9 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -487,7 +487,7 @@ impl DapStore { } } - pub fn start_debug_session( + pub fn new_session( &mut self, config: DebugAdapterConfig, worktree: &Entity, @@ -559,84 +559,6 @@ impl DapStore { }) } - pub fn new_session( - &mut self, - config: DebugAdapterConfig, - cx: &mut Context, - ) -> Task> { - // let config = session.read(cx).as_local().unwrap().configuration(); - // let mut adapter_args = client.adapter().request_args(&config); - // if let Some(args) = config.initialize_args.clone() { - // merge_json_value_into(args, &mut adapter_args); - // } - - // // TODO(debugger): GDB starts the debuggee program on launch instead of configurationDone - // // causing our sent breakpoints to not be valid. This delay should eventually be taken out - // let delay = if &client.adapter_id() == "gdb" { - // Some( - // cx.background_executor() - // .timer(std::time::Duration::from_millis(20u64)), - // ) - // } else { - // None - // }; - - // cx.background_executor().spawn(async move { - // if let Some(delay) = delay { - // delay.await; - // } - - // client - // .request::(LaunchRequestArguments { raw: adapter_args }) - // .await - // }) - Task::ready(Ok(())) - } - - pub fn attach( - &mut self, - session_id: SessionId, - process_id: u32, - cx: &mut Context, - ) -> Task> { - unimplemented!(); - // let Some(client) = self - // .client_by_id(session_id) - // .and_then(|client| Some(client.read(cx).adapter_client()?)) - // else { - // return Task::ready(Err( - // anyhow!("Could not find debug client: {:?}", session_id,), - // )); - // }; - - // // update the process id on the config, so when the `startDebugging` reverse request - // // comes in we send another `attach` request with the already selected PID - // // If we don't do this the user has to select the process twice if the adapter sends a `startDebugging` request - // session.update(cx, |session, cx| { - // session.as_local_mut().unwrap().update_configuration( - // |config| { - // config.request = DebugRequestType::Attach(task::AttachConfig { - // process_id: Some(process_id), - // }); - // }, - // cx, - // ); - // }); - - // let config = session.read(cx).as_local().unwrap().configuration(); - // let mut adapter_args = client.adapter().request_args(&config); - - // if let Some(args) = config.initialize_args.clone() { - // merge_json_value_into(args, &mut adapter_args); - // } - - // cx.background_executor().spawn(async move { - // client - // .request::(AttachRequestArguments { raw: adapter_args }) - // .await - // }) - } - pub fn respond_to_run_in_terminal( &self, session_id: SessionId, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index a695657249c098..3e5220a91cf7df 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -336,7 +336,7 @@ impl Eq for RequestSlot {} impl Hash for RequestSlot { fn hash(&self, state: &mut H) { self.0.dyn_hash(state); - self.0.as_any().type_id().hash(state); + self.0.as_any().type_id().hash(state) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 92afbbee8f63b2..f83b04f9cf0f30 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1357,7 +1357,7 @@ impl Project { }; self.dap_store.update(cx, |dap_store, cx| { - dap_store.start_debug_session(config, worktree, cx) + dap_store.new_session(config, worktree, cx) }) } From 286532e4391a562037b49775566ec1a168e8f9e3 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 18 Feb 2025 01:45:09 -0500 Subject: [PATCH 605/650] Handle initialize event --- crates/project/src/debugger/dap_store.rs | 124 ++++++---------- crates/project/src/debugger/session.rs | 174 +++++++++++++++++++++-- crates/project/src/project.rs | 34 +++-- 3 files changed, 224 insertions(+), 108 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 5a4ccff4d03de9..4f3b80fff3d7a5 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -344,6 +344,7 @@ impl DapStore { session_id, remote.upstream_client.clone(), remote.upstream_project_id, + self.breakpoint_store.clone(), ignore.unwrap_or(false), ) }), @@ -524,7 +525,7 @@ impl DapStore { .update(cx, |store, cx| { if let Some(session) = store.sessions.get(&session_id) { session.update(cx, |session, cx| { - session.handle_dap_message(message); + session.handle_dap_message(message, cx); }); } }) @@ -883,50 +884,6 @@ impl DapStore { }) } - pub fn send_breakpoints( - &self, - session_id: SessionId, - absolute_file_path: Arc, - mut breakpoints: Vec, - ignore: bool, - source_changed: bool, - cx: &App, - ) -> Task> { - let Some(client) = self - .session_by_id(session_id) - .and_then(|client| client.read(cx).adapter_client()) - else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); - }; - - // Adjust breakpoints as our client declares that indices start at one. - breakpoints.iter_mut().for_each(|bp| bp.line += 1u64); - - cx.background_executor().spawn(async move { - client - .request::(SetBreakpointsArguments { - source: Source { - path: Some(String::from(absolute_file_path.to_string_lossy())), - name: absolute_file_path - .file_name() - .map(|name| name.to_string_lossy().to_string()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - }, - breakpoints: Some(if ignore { Vec::default() } else { breakpoints }), - source_modified: Some(source_changed), - lines: None, - }) - .await?; - - Ok(()) - }) - } - pub fn send_changed_breakpoints( &self, project_path: &ProjectPath, @@ -935,44 +892,45 @@ impl DapStore { source_changed: bool, cx: &App, ) -> Task> { - let source_breakpoints = self - .breakpoint_store - .read(cx) - .breakpoints - .get(project_path) - .cloned() - .unwrap_or_default() - .iter() - .map(|breakpoint| breakpoint.source_for_snapshot(buffer_snapshot.as_ref())) - .collect::>(); - - let mut tasks = Vec::new(); - for (session_id, client) in self - .sessions - .iter() - .filter(|(_, client)| client.read(cx).adapter_client().is_some()) - { - let client = client.read(cx); - let ignore_breakpoints = !client.breakpoints_enabled(); - - tasks.push(self.send_breakpoints( - *session_id, - Arc::from(absolute_path.clone()), - source_breakpoints.clone(), - ignore_breakpoints, - source_changed, - cx, - )); - } - - if tasks.is_empty() { - return Task::ready(Ok(())); - } - - cx.background_executor().spawn(async move { - futures::future::join_all(tasks).await; - Ok(()) - }) + todo!() + // let source_breakpoints = self + // .breakpoint_store + // .read(cx) + // .breakpoints + // .get(project_path) + // .cloned() + // .unwrap_or_default() + // .iter() + // .map(|breakpoint| breakpoint.source_for_snapshot(buffer_snapshot.as_ref())) + // .collect::>(); + + // let mut tasks = Vec::new(); + // for (client_id, client) in self + // .sessions + // .iter() + // .filter(|(_, client)| client.read(cx).adapter_client().is_some()) + // { + // let client = client.read(cx); + // let ignore_breakpoints = !client.breakpoints_enabled(); + + // tasks.push(self.send_breakpoints( + // *client_id, + // Arc::from(absolute_path.clone()), + // source_breakpoints.clone(), + // ignore_breakpoints, + // source_changed, + // cx, + // )); + // } + + // if tasks.is_empty() { + // return Task::ready(Ok(())); + // } + + // cx.background_executor().spawn(async move { + // futures::future::join_all(tasks).await; + // Ok(()) + // }) } pub fn shared( diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 3e5220a91cf7df..74f96269763118 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1,6 +1,6 @@ use crate::project_settings::ProjectSettings; -use super::breakpoint_store::BreakpointStore; +use super::breakpoint_store::{self, BreakpointStore}; use super::dap_command::{ self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, Initialize, LocalDapCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, @@ -12,12 +12,15 @@ use anyhow::{anyhow, Result}; use collections::{HashMap, IndexMap}; use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; use dap::client::{DapMessageHandler, DebugAdapterClient, SessionId}; +use dap::requests::Request; use dap::{ - messages::Message, Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, - SteppingGranularity, + messages::{self, Events, Message}, + requests::{RunInTerminal, SetBreakpoints, StartDebugging}, + Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, SetBreakpointsArguments, + Source, SourceBreakpoint, SteppingGranularity, }; use dap_adapters::build_adapter; -use futures::{future::Shared, FutureExt}; +use futures::{future::join_all, future::Shared, FutureExt}; use gpui::{App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Task}; use rpc::AnyProtoClient; use serde_json::Value; @@ -28,6 +31,7 @@ use std::{ any::Any, collections::hash_map::Entry, hash::{Hash, Hasher}, + path::Path, sync::Arc, }; use task::DebugAdapterConfig; @@ -160,9 +164,8 @@ struct LocalMode { } impl LocalMode { - fn new( - session_id: SessionId, - breakpoint_store: Entity, + fn new( + client_id: DebugAdapterClientId, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, message_handler: DapMessageHandler, @@ -282,6 +285,7 @@ pub struct Session { config: DebugAdapterConfig, pub(super) capabilities: Capabilities, id: SessionId, + breakpoint_store: Entity, ignore_breakpoints: bool, modules: Vec, loaded_sources: Vec, @@ -363,7 +367,9 @@ impl CompletionsQuery { } } } - +// local session will send breakpoint updates to DAP for all new breakpoints +// remote side will only send breakpoint updates when it is a breakpoint created by that peer +// BreakpointStore notifies session on breakpoint changes impl Session { pub(crate) fn local( breakpoints: Entity, @@ -376,7 +382,6 @@ impl Session { cx.spawn(move |mut cx| async move { let (mode, capabilities) = LocalMode::new( session_id, - breakpoints, config.clone(), delegate, message_handler, @@ -387,6 +392,7 @@ impl Session { cx.new(|_| Self { mode: Mode::Local(mode), id: session_id, + breakpoint_store: breakpoints, config, capabilities, ignore_breakpoints: false, @@ -402,6 +408,7 @@ impl Session { session_id: SessionId, client: AnyProtoClient, upstream_project_id: u64, + breakpoint_store: Entity, ignore_breakpoints: bool, ) -> Self { Self { @@ -411,6 +418,7 @@ impl Session { }), id: session_id, capabilities: Capabilities::default(), + breakpoint_store, ignore_breakpoints, requests: HashMap::default(), modules: Vec::default(), @@ -427,7 +435,153 @@ impl Session { self.config.clone() } - pub(crate) fn handle_dap_message(&mut self, message: Message) {} + fn send_initial_breakpoints(&self, cx: &App) -> Task<()> { + let mut tasks = Vec::new(); + + for (abs_path, serialized_breakpoints) in self + .breakpoint_store + .read_with(cx, |store, cx| store.all_breakpoints(true, cx)) + .into_iter() + .filter(|(_, bps)| !bps.is_empty()) + { + let source_breakpoints = serialized_breakpoints + .iter() + .map(|bp| bp.to_source_breakpoint()) + .collect::>(); + + tasks.push(self.send_breakpoints( + abs_path, + source_breakpoints, + self.ignore_breakpoints, + false, + cx, + )); + } + + cx.background_executor().spawn(async move { + join_all(tasks).await; + }) + } + + pub fn send_breakpoints( + &self, + absolute_file_path: Arc, + mut breakpoints: Vec, + ignore: bool, + source_changed: bool, + cx: &App, + ) -> Task> { + let Some(client) = self.adapter_client() else { + return Task::ready(Err(anyhow!( + "Could not get client in remote session to send breakpoints" + ))); + }; + + // Adjust breakpoints as our client declares that indices start at one. + breakpoints.iter_mut().for_each(|bp| bp.line += 1u64); + + cx.background_executor().spawn(async move { + client + .request::(SetBreakpointsArguments { + source: Source { + path: Some(String::from(absolute_file_path.to_string_lossy())), + name: absolute_file_path + .file_name() + .map(|name| name.to_string_lossy().to_string()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + }, + breakpoints: Some(if ignore { Vec::default() } else { breakpoints }), + source_modified: Some(source_changed), + lines: None, + }) + .await?; + + Ok(()) + }) + } + + fn handle_initialized_event(&mut self, caps: Option, cx: &mut Context) { + if let Some(caps) = caps { + self.capabilities = caps; + } + + let send_breakpoints = self.send_initial_breakpoints(cx); + + cx.spawn(|this, mut cx| async move { + send_breakpoints.await; + + this.update(&mut cx, |this, cx| { + if this + .capabilities + .supports_configuration_done_request + .unwrap_or_default() + { + if let Some(adapter) = this.adapter_client() { + cx.background_spawn(async move { + adapter.request::( + dap::ConfigurationDoneArguments, + ); + }) + } else { + Task::ready(()) + } + } else { + Task::ready(()) + } + }) + }) + .detach_and_log_err(cx); + } + + fn handle_dap_event(&mut self, event: Box, cx: &mut Context) { + match *event { + Events::Initialized(event) => self.handle_initialized_event(event, cx), + Events::Stopped(event) => {} + Events::Continued(event) => {} + Events::Exited(event) => {} + Events::Terminated(event) => {} + Events::Thread(event) => {} + Events::Output(event) => {} + Events::Breakpoint(_) => {} + Events::Module(event) => {} + Events::LoadedSource(event) => {} + Events::Capabilities(event) => {} + Events::Memory(_) => {} + Events::Process(_) => {} + Events::ProgressEnd(_) => {} + Events::ProgressStart(_) => {} + Events::ProgressUpdate(_) => {} + Events::Invalidated(_) => {} + Events::Other(_) => {} + } + } + + fn handle_start_debugging_request(&mut self, request: messages::Request) {} + + fn handle_run_in_terminal_request(&mut self, request: messages::Request) {} + + pub(crate) fn handle_dap_message(&mut self, message: Message, cx: &mut Context) { + match message { + Message::Event(event) => { + self.handle_dap_event(event, cx); + } + Message::Request(request) => { + if StartDebugging::COMMAND == request.command { + self.handle_start_debugging_request(request); + } else if RunInTerminal::COMMAND == request.command { + self.handle_run_in_terminal_request(request); + } else { + debug_assert!(false, "Encountered unexpected command type"); + } + } + _ => unreachable!(), + } + } pub(crate) fn _wait_for_request( &self, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f83b04f9cf0f30..901afbee4df3dc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1304,6 +1304,7 @@ impl Project { } } +<<<<<<< HEAD pub fn initial_send_breakpoints( &self, session_id: SessionId, @@ -1339,6 +1340,8 @@ impl Project { }) } +======= +>>>>>>> e1c7e1088b (Handle initialize event) pub fn start_debug_session( &mut self, config: DebugAdapterConfig, @@ -1416,7 +1419,7 @@ impl Project { .log_err(); } - let mut tasks = Vec::new(); + let mut tasks: Vec> = Vec::new(); for (project_path, breakpoints) in &self.breakpoint_store.read(cx).breakpoints { let Some((buffer, buffer_path)) = maybe!({ @@ -1435,26 +1438,27 @@ impl Project { continue; }; - tasks.push( - store.send_breakpoints( - session_id, - Arc::from(buffer_path), - breakpoints - .into_iter() - .map(|breakpoint| breakpoint.to_source_breakpoint(buffer)) - .collect::>(), - store.ignore_breakpoints(&session_id, cx), - false, - cx, - ), - ); + // todo (debugger): Fix breakpoint stuff + // tasks.push( + // store.send_breakpoints( + // client_id, + // Arc::from(buffer_path), + // breakpoints + // .into_iter() + // .map(|breakpoint| breakpoint.to_source_breakpoint(buffer)) + // .collect::>(), + // store.ignore_breakpoints(&client_id, cx), + // false, + // cx, + // ), + // ); } tasks }); cx.background_executor().spawn(async move { - try_join_all(tasks).await?; + // try_join_all(tasks).await?; Ok(()) }) From df6c3649df4f89cae69a8f5dba021f5230fbdd4e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 18 Feb 2025 01:47:44 -0500 Subject: [PATCH 606/650] Clean up handle initialized event return func --- crates/project/src/debugger/session.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 74f96269763118..05d027f0c0b503 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -522,17 +522,18 @@ impl Session { .unwrap_or_default() { if let Some(adapter) = this.adapter_client() { - cx.background_spawn(async move { - adapter.request::( - dap::ConfigurationDoneArguments, - ); - }) - } else { - Task::ready(()) + return cx.background_spawn(async move { + adapter + .request::( + dap::ConfigurationDoneArguments, + ) + .await + .log_err(); + }); } - } else { - Task::ready(()) } + + Task::ready(()) }) }) .detach_and_log_err(cx); From c1ad9873db30c93e9d832a1b666beb9dd4cf500c Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 18 Feb 2025 01:48:41 -0500 Subject: [PATCH 607/650] Rename event handler to message handle in debug client func --- crates/dap/src/client.rs | 6 ++-- crates/project/src/debugger/session.rs | 7 ++--- crates/project/src/project.rs | 38 -------------------------- 3 files changed, 5 insertions(+), 46 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 28371ad9112008..eee5bd2345e727 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -96,7 +96,7 @@ impl DebugAdapterClient { client_id: SessionId, server_rx: Receiver, client_tx: Sender, - mut event_handler: DapMessageHandler, + mut message_handler: DapMessageHandler, cx: &mut AsyncApp, ) -> Result<()> { let result = loop { @@ -109,9 +109,9 @@ impl DebugAdapterClient { Message::Event(ev) => { log::debug!("Client {} received event `{}`", client_id.0, &ev); - cx.update(|cx| event_handler(Message::Event(ev), cx)) + cx.update(|cx| message_handler(Message::Event(ev), cx)) } - Message::Request(req) => cx.update(|cx| event_handler(Message::Request(req), cx)), + Message::Request(req) => cx.update(|cx| message_handler(Message::Request(req), cx)), Message::Response(response) => { log::debug!("Received response after request timeout: {:#?}", response); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 05d027f0c0b503..208bf0b94b5e47 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -164,8 +164,8 @@ struct LocalMode { } impl LocalMode { - fn new( - client_id: DebugAdapterClientId, + fn new( + session_id: SessionId, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, message_handler: DapMessageHandler, @@ -221,10 +221,7 @@ impl LocalMode { cx.background_executor().clone(), ) .await?; - let breakpoints = - breakpoint_store.update(&mut cx, |this, cx| this.all_breakpoints(true, cx))?; - for (path, breakpoints) in breakpoints {} Ok((this, capabilities)) }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 901afbee4df3dc..f1dcf192f39faa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1304,44 +1304,6 @@ impl Project { } } -<<<<<<< HEAD - pub fn initial_send_breakpoints( - &self, - session_id: SessionId, - cx: &mut Context, - ) -> Task<()> { - let mut tasks = Vec::new(); - - for (abs_path, serialized_breakpoints) in self - .breakpoint_store() - .read_with(cx, |store, cx| store.all_breakpoints(true, cx)) - .into_iter() - .filter(|(_, bps)| !bps.is_empty()) - { - let source_breakpoints = serialized_breakpoints - .iter() - .map(|bp| bp.to_source_breakpoint()) - .collect::>(); - - tasks.push(self.dap_store.update(cx, |store, cx| { - store.send_breakpoints( - session_id, - abs_path, - source_breakpoints, - store.ignore_breakpoints(&session_id, cx), - false, - cx, - ) - })); - } - - cx.background_executor().spawn(async move { - join_all(tasks).await; - }) - } - -======= ->>>>>>> e1c7e1088b (Handle initialize event) pub fn start_debug_session( &mut self, config: DebugAdapterConfig, From 16448713364ba56ce83519d34688b680ad83634e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Tue, 18 Feb 2025 12:59:22 -0500 Subject: [PATCH 608/650] Start work on handling stopped event I didn't have time to finish this beacuse I have a pairing session that I have to go too. Will resume soon --- crates/project/src/debugger/session.rs | 51 +++++++++++++++++++------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 208bf0b94b5e47..060574e8629c46 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -17,7 +17,7 @@ use dap::{ messages::{self, Events, Message}, requests::{RunInTerminal, SetBreakpoints, StartDebugging}, Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, SetBreakpointsArguments, - Source, SourceBreakpoint, SteppingGranularity, + Source, SourceBreakpoint, SteppingGranularity, StoppedEvent, }; use dap_adapters::build_adapter; use futures::{future::join_all, future::Shared, FutureExt}; @@ -104,8 +104,8 @@ pub enum ThreadStatus { pub struct Thread { dap: dap::Thread, stack_frames: Vec, - _status: ThreadStatus, - _has_stopped: bool, + status: ThreadStatus, + has_stopped: bool, } impl From for Thread { @@ -113,8 +113,8 @@ impl From for Thread { Self { dap, stack_frames: vec![], - _status: ThreadStatus::default(), - _has_stopped: false, + status: ThreadStatus::default(), + has_stopped: false, } } } @@ -536,19 +536,42 @@ impl Session { .detach_and_log_err(cx); } + fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context) { + // todo(debugger): We should see if we could only invalidate the thread that stopped + // instead of everything right now + self.invalidate(cx); + + // todo(debugger): We should query for all threads here if we don't get a thread id + // maybe in both cases too? + if let Some(thread_id) = event.thread_id { + self.threads.insert( + ThreadId(thread_id), + Thread { + dap: dap::Thread { + id: thread_id, + name: "".into(), + }, + stack_frames: vec![], + status: ThreadStatus::Stopped, + has_stopped: true, + }, + ); + } + } + fn handle_dap_event(&mut self, event: Box, cx: &mut Context) { match *event { Events::Initialized(event) => self.handle_initialized_event(event, cx), - Events::Stopped(event) => {} - Events::Continued(event) => {} - Events::Exited(event) => {} - Events::Terminated(event) => {} - Events::Thread(event) => {} - Events::Output(event) => {} + Events::Stopped(event) => self.handle_stopped_event(event, cx), + Events::Continued(_event) => {} + Events::Exited(_event) => {} + Events::Terminated(_event) => {} + Events::Thread(_event) => {} + Events::Output(_event) => {} Events::Breakpoint(_) => {} - Events::Module(event) => {} - Events::LoadedSource(event) => {} - Events::Capabilities(event) => {} + Events::Module(_event) => {} + Events::LoadedSource(_event) => {} + Events::Capabilities(_event) => {} Events::Memory(_) => {} Events::Process(_) => {} Events::ProgressEnd(_) => {} From 060c8df7e8caaa2f6e4f0d4ac6bbc344641909c2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 00:13:02 +0100 Subject: [PATCH 609/650] Remove code --- crates/project/src/debugger/dap_store.rs | 49 ------------------------ crates/project/src/project.rs | 42 -------------------- 2 files changed, 91 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 4f3b80fff3d7a5..d91e3ac2b9c849 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -884,55 +884,6 @@ impl DapStore { }) } - pub fn send_changed_breakpoints( - &self, - project_path: &ProjectPath, - absolute_path: PathBuf, - buffer_snapshot: Option, - source_changed: bool, - cx: &App, - ) -> Task> { - todo!() - // let source_breakpoints = self - // .breakpoint_store - // .read(cx) - // .breakpoints - // .get(project_path) - // .cloned() - // .unwrap_or_default() - // .iter() - // .map(|breakpoint| breakpoint.source_for_snapshot(buffer_snapshot.as_ref())) - // .collect::>(); - - // let mut tasks = Vec::new(); - // for (client_id, client) in self - // .sessions - // .iter() - // .filter(|(_, client)| client.read(cx).adapter_client().is_some()) - // { - // let client = client.read(cx); - // let ignore_breakpoints = !client.breakpoints_enabled(); - - // tasks.push(self.send_breakpoints( - // *client_id, - // Arc::from(absolute_path.clone()), - // source_breakpoints.clone(), - // ignore_breakpoints, - // source_changed, - // cx, - // )); - // } - - // if tasks.is_empty() { - // return Task::ready(Ok(())); - // } - - // cx.background_executor().spawn(async move { - // futures::future::join_all(tasks).await; - // Ok(()) - // }) - } - pub fn shared( &mut self, project_id: u64, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f1dcf192f39faa..578f6a44bd324e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -705,8 +705,6 @@ impl Project { ) }); cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); - cx.subscribe(&breakpoint_store, Self::on_breakpoint_store_event) - .detach(); let image_store = cx.new(|cx| ImageStore::local(worktree_store.clone(), cx)); cx.subscribe(&image_store, Self::on_image_store_event) @@ -1174,8 +1172,6 @@ impl Project { .detach(); cx.subscribe(&dap_store, Self::on_dap_store_event).detach(); - cx.subscribe(&breakpoint_store, Self::on_breakpoint_store_event) - .detach(); let mut this = Self { buffer_ordered_messages_tx: tx, @@ -2576,44 +2572,6 @@ impl Project { } } - fn on_breakpoint_store_event( - &mut self, - _: Entity, - event: &BreakpointStoreEvent, - cx: &mut Context, - ) { - match event { - BreakpointStoreEvent::BreakpointsChanged { - project_path, - source_changed, - } => { - cx.notify(); // so the UI updates - - let buffer_snapshot = self - .buffer_store - .read(cx) - .get_by_path(&project_path, cx) - .map(|buffer| buffer.read(cx).snapshot()); - - let Some(absolute_path) = self.absolute_path(project_path, cx) else { - return; - }; - - self.dap_store.read_with(cx, |dap_store, cx| { - dap_store - .send_changed_breakpoints( - project_path, - absolute_path, - buffer_snapshot, - *source_changed, - cx, - ) - .detach_and_log_err(cx) - }); - } - } - } - fn on_dap_store_event( &mut self, _: Entity, From 859cc67fa137a55471c70277be58c8b44b4f22af Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 01:19:47 +0100 Subject: [PATCH 610/650] WIP --- crates/debugger_ui/src/session/inert.rs | 4 +- crates/debugger_ui/src/session/starting.rs | 2 - crates/project/src/debugger/dap_command.rs | 43 ++++++++++++++++++++++ crates/project/src/debugger/session.rs | 37 ++++++++++++++++--- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 35e9d911b46a08..67ddcf4b53d254 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -85,9 +85,9 @@ impl Render for InertState { }), request: DebugRequestType::Launch, program: Some( - "/Users/hiro/Projects/zed/test_debug_file.py".into(), + "/Users/hiro/Projects/repros/python-funsies/nested/file.py".into(), ), - cwd: None, + cwd: Some("/Users/hiro/Projects/repros/python-funsies/nested".into()), initialize_args: None, supports_attach: false, }, diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs index 5bdde4ce31fec7..a4330d04a0ca41 100644 --- a/crates/debugger_ui/src/session/starting.rs +++ b/crates/debugger_ui/src/session/starting.rs @@ -26,9 +26,7 @@ impl EventEmitter for StartingState {} impl StartingState { pub(crate) fn new(task: Task>>, cx: &mut Context) -> Self { let _notify_parent = cx.spawn(move |this, mut cx| async move { - dbg!("Waiting for session to start"); let entity = task.await; - dbg!(entity.is_err()); this.update(&mut cx, |_, cx| cx.emit(StartingEvent::Finished(entity))) .ok(); }); diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index e069e370369b16..cb50e9952b91d2 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -10,6 +10,7 @@ use dap::{ StepOutArguments, SteppingGranularity, ValueFormat, Variable, VariablesArgumentsFilter, }; use rpc::proto; +use serde_json::Value; use util::ResultExt; pub(crate) trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; @@ -1598,3 +1599,45 @@ impl LocalDapCommand for Initialize { Ok(message) } } + +#[derive(Clone, Debug, Hash, PartialEq)] +pub(super) struct ConfigurationDone; + +impl LocalDapCommand for ConfigurationDone { + type Response = (); + type DapRequest = dap::requests::ConfigurationDone; + + fn to_dap(&self) -> ::Arguments { + dap::ConfigurationDoneArguments + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message) + } +} + +#[derive(Clone, Debug, Hash, PartialEq)] +pub(super) struct Launch { + pub(super) raw: Value, +} + +impl LocalDapCommand for Launch { + type Response = (); + type DapRequest = dap::requests::Launch; + + fn to_dap(&self) -> ::Arguments { + dap::LaunchRequestArguments { + raw: self.raw.clone(), + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message) + } +} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 060574e8629c46..f58dfc7b94f1c6 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -2,10 +2,10 @@ use crate::project_settings::ProjectSettings; use super::breakpoint_store::{self, BreakpointStore}; use super::dap_command::{ - self, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, Initialize, - LocalDapCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, - ScopesCommand, SetVariableValueCommand, StepBackCommand, StepCommand, StepInCommand, - StepOutCommand, TerminateCommand, TerminateThreadsCommand, VariablesCommand, + self, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, + Initialize, Launch, LocalDapCommand, NextCommand, PauseCommand, RestartCommand, + RestartStackFrameCommand, ScopesCommand, SetVariableValueCommand, StepBackCommand, StepCommand, + StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, VariablesCommand, }; use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; @@ -20,6 +20,7 @@ use dap::{ Source, SourceBreakpoint, SteppingGranularity, StoppedEvent, }; use dap_adapters::build_adapter; +use futures::channel::oneshot; use futures::{future::join_all, future::Shared, FutureExt}; use gpui::{App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Task}; use rpc::AnyProtoClient; @@ -159,6 +160,7 @@ enum Mode { Remote(RemoteConnection), } +#[derive(Clone)] struct LocalMode { client: Arc, } @@ -168,7 +170,6 @@ impl LocalMode { session_id: SessionId, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, - message_handler: DapMessageHandler, cx: AsyncApp, ) -> Task> { cx.spawn(move |mut cx| async move { @@ -209,6 +210,21 @@ impl LocalMode { } }; + let (initialized_tx, initialized_rx) = oneshot::channel(); + let mut initialized_tx = Some(initialized_tx); + let message_handler = Box::new(move |message, cx: &mut App| match message { + Message::Event(events) => { + if let Events::Initialized(_) = *events { + initialized_tx + .take() + .expect("To receive just one Initialized event") + .send(()) + .ok(); + } + } + Message::Response(response) => {} + Message::Request(request) => todo!(), + }); let client = Arc::new( DebugAdapterClient::start(session_id, binary, message_handler, cx.clone()).await?, ); @@ -222,6 +238,15 @@ impl LocalMode { ) .await?; + let raw = adapter.request_args(&disposition); + + let launch = this + .request(Launch { raw }, cx.background_executor().clone()) + .await?; + let _ = initialized_rx.await?; + this.request(ConfigurationDone, cx.background_executor().clone()) + .await?; + Ok((this, capabilities)) }) } @@ -381,7 +406,7 @@ impl Session { session_id, config.clone(), delegate, - message_handler, + // message_handler, cx.clone(), ) .await?; From 1af8c11ef4a682f4c4338aedf8fd5d8bb21153a2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 01:31:23 +0100 Subject: [PATCH 611/650] Initialization sequence --- crates/project/src/debugger/session.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index f58dfc7b94f1c6..83f89c3793c534 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -222,8 +222,9 @@ impl LocalMode { .ok(); } } - Message::Response(response) => {} + Message::Request(request) => todo!(), + _ => {} }); let client = Arc::new( DebugAdapterClient::start(session_id, binary, message_handler, cx.clone()).await?, @@ -240,12 +241,22 @@ impl LocalMode { let raw = adapter.request_args(&disposition); - let launch = this - .request(Launch { raw }, cx.background_executor().clone()) - .await?; - let _ = initialized_rx.await?; - this.request(ConfigurationDone, cx.background_executor().clone()) - .await?; + // Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 + let launch = this.request(Launch { raw }, cx.background_executor().clone()); + let that = this.clone(); + let configuration_sequence = async move { + let _ = initialized_rx.await?; + if capabilities + .supports_configuration_done_request + .unwrap_or_default() + { + that.request(ConfigurationDone, cx.background_executor().clone()) + .await?; + } + + anyhow::Result::<_, anyhow::Error>::Ok(()) + }; + let _ = futures::future::join(configuration_sequence, launch).await; Ok((this, capabilities)) }) From 95eca602c42c8d69c66bceff2515eb74e04f4758 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 02:02:07 +0100 Subject: [PATCH 612/650] Bring back old UI --- crates/debugger_ui/src/debugger_panel.rs | 19 +++++++- crates/debugger_ui/src/session.rs | 47 ++++++++++++------- crates/debugger_ui/src/session/running.rs | 13 ++--- .../src/session/running/stack_frame_list.rs | 1 - crates/project/src/debugger/session.rs | 7 +++ 5 files changed, 58 insertions(+), 29 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index cf8db8cf61dea3..a9cdf52dcbb2c7 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -71,6 +71,7 @@ impl DebugPanel { ) -> Entity { cx.new(|cx| { let project = workspace.project().clone(); + let weak_workspace = workspace.weak_handle(); let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.weak_handle(), @@ -88,8 +89,10 @@ impl DebugPanel { pane.set_close_pane_if_empty(true, cx); pane.set_render_tab_bar_buttons(cx, { let project = project.clone(); + let weak_workspace = weak_workspace.clone(); move |_, _, cx| { let project = project.clone(); + let weak_workspace = weak_workspace.clone(); ( None, Some( @@ -101,6 +104,8 @@ impl DebugPanel { pane.add_item( Box::new(DebugSession::inert( project.clone(), + weak_workspace.clone(), + window, cx, )), false, @@ -117,7 +122,12 @@ impl DebugPanel { } }); pane.add_item( - Box::new(DebugSession::inert(project.clone(), cx)), + Box::new(DebugSession::inert( + project.clone(), + weak_workspace.clone(), + window, + cx, + )), false, false, None, @@ -341,7 +351,12 @@ impl Panel for DebugPanel { // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items). self.pane.update(cx, |this, cx| { this.add_item( - Box::new(DebugSession::inert(project, cx)), + Box::new(DebugSession::inert( + project, + self.workspace.clone(), + window, + cx, + )), false, false, None, diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 0896105b2e0960..bd6f15e3ae0213 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -22,6 +22,7 @@ use project::debugger::dap_store::DapStore; use project::worktree_store::WorktreeStore; use project::Project; use rpc::proto::{self, PeerId}; +use running::RunningState; use settings::Settings; use starting::{StartingEvent, StartingState}; use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, Tooltip}; @@ -41,6 +42,7 @@ pub struct DebugSession { mode: DebugSessionState, dap_store: WeakEntity, worktree_store: WeakEntity, + workspace: WeakEntity, _subscriptions: [Subscription; 1], } #[derive(Debug)] @@ -78,18 +80,25 @@ impl ThreadItem { } impl DebugSession { - pub(super) fn inert(project: Entity, cx: &mut App) -> Entity { + pub(super) fn inert( + project: Entity, + workspace: WeakEntity, + window: &Window, + cx: &mut App, + ) -> Entity { let inert = cx.new(|cx| InertState::new(cx)); + let project = project.read(cx); let dap_store = project.dap_store().downgrade(); let worktree_store = project.worktree_store().downgrade(); cx.new(|cx| { - let _subscriptions = [cx.subscribe(&inert, Self::on_inert_event)]; + let _subscriptions = [cx.subscribe_in(&inert, window, Self::on_inert_event)]; Self { remote_id: None, mode: DebugSessionState::Inert(inert), dap_store, worktree_store, + workspace, _subscriptions, } }) @@ -103,8 +112,9 @@ impl DebugSession { } fn on_inert_event( &mut self, - _: Entity, + _: &Entity, event: &InertEvent, + window: &mut Window, cx: &mut Context<'_, Self>, ) { let dap_store = self.dap_store.clone(); @@ -121,27 +131,32 @@ impl DebugSession { return; }; let starting = cx.new(|cx| StartingState::new(task, cx)); - self._subscriptions = [cx.subscribe(&starting, Self::on_starting_event)]; + + self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)]; self.mode = DebugSessionState::Starting(starting); } fn on_starting_event( &mut self, - _: Entity, + _: &Entity, event: &StartingEvent, + window: &mut Window, cx: &mut Context<'_, Self>, ) { - let StartingEvent::Finished(session) = event; - let session = session.as_ref().unwrap(); - let starting = cx.new(|cx| { - let timer = cx.background_executor().timer(Duration::from_secs(3)); - let task = cx.background_executor().spawn(async move { - timer.await; - Err(anyhow!("Foo")) - }); - StartingState::new(task, cx) + let StartingEvent::Finished(Ok(session)) = event else { + return; + }; + + let mode = cx.new(|cx| { + RunningState::new( + session.clone(), + ThreadId(1), + self.workspace.clone(), + window, + cx, + ) }); - //self._subscriptions = [cx.subscribe(Self::on_starting_event)]; - //self.mode = DebugSessionState::Running(starting); + + self.mode = DebugSessionState::Running(mode); } } impl EventEmitter for DebugSession {} diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 0d0ee4c15b1001..c26befa656010b 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -319,18 +319,15 @@ impl RunningState { #[allow(clippy::too_many_arguments)] pub fn new( session: Entity, - session_id: SessionId, thread_id: ThreadId, - debug_panel: &Entity, workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); - - let stack_frame_list = cx.new(|cx| { - StackFrameList::new(workspace.clone(), session.clone(), thread_id, window, cx) - }); + let session_id = session.read(cx).session_id(); + let stack_frame_list = + cx.new(|cx| StackFrameList::new(workspace.clone(), session.clone(), thread_id, cx)); let variable_list = cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); @@ -420,10 +417,6 @@ impl RunningState { self.session_id } - pub fn thread_id(&self) -> ThreadId { - self.thread_id - } - #[cfg(any(test, feature = "test-support"))] pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context) { self.active_thread_item = thread_item; diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index e3f49eb09230fd..3c1c8c6dc77a52 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -42,7 +42,6 @@ impl StackFrameList { workspace: WeakEntity, session: Entity, thread_id: ThreadId, - _window: &Window, cx: &mut Context, ) -> Self { let weak_entity = cx.weak_entity(); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 83f89c3793c534..820742c934b1a0 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -165,6 +165,9 @@ struct LocalMode { client: Arc, } +enum ReverseRequest { + RunInTerminal(), +} impl LocalMode { fn new( session_id: SessionId, @@ -461,6 +464,10 @@ impl Session { } } + pub fn session_id(&self) -> SessionId { + self.id + } + pub fn capabilities(&self) -> &Capabilities { &self.capabilities } From ac68f31550f149c56c593fae3d34145a15a553e4 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 19 Feb 2025 01:58:05 -0500 Subject: [PATCH 613/650] Allow session to handle toggling ignore breakpoint --- crates/debugger_ui/src/session/running.rs | 5 +- crates/project/src/debugger/dap_store.rs | 33 +------ crates/project/src/debugger/session.rs | 91 ++++++++++++++---- crates/project/src/project.rs | 110 ---------------------- crates/proto/proto/zed.proto | 2 +- crates/workspace/src/workspace.rs | 9 +- 6 files changed, 83 insertions(+), 167 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index c26befa656010b..2d5b20531655e1 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -586,9 +586,8 @@ impl RunningState { } pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context) { - self.session.update(cx, |session, cx| { - session.set_ignore_breakpoints(!session.breakpoints_enabled()); - }); + self.session + .update(cx, |session, cx| session.toggle_ignore_breakpoints(cx)); } } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index d91e3ac2b9c849..d2f89328223472 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -451,9 +451,9 @@ impl DapStore { let session_id = SessionId::from_proto(envelope.payload.session_id); this.update(&mut cx, |this, cx| { - if let Some(client) = this.session_by_id(&session_id) { - client.update(cx, |client, cx| { - client.set_ignore_breakpoints(envelope.payload.ignore) + if let Some(session) = this.session_by_id(&session_id) { + session.update(cx, |session, cx| { + session.set_ignore_breakpoints(envelope.payload.ignore, cx) }); } })?; @@ -461,33 +461,6 @@ impl DapStore { Ok(()) } - pub fn set_ignore_breakpoints( - &mut self, - session_id: &SessionId, - ignore: bool, - cx: &mut Context, - ) { - if let Some(session) = self.session_by_id(session_id) { - session.update(cx, |session, _| { - session.set_ignore_breakpoints(ignore); - }); - } - } - - pub fn ignore_breakpoints(&self, session_id: &SessionId, cx: &App) -> bool { - self.session_by_id(session_id) - .map(|client| client.read(cx).breakpoints_enabled()) - .unwrap_or_default() - } - - pub fn toggle_ignore_breakpoints(&mut self, session_id: &SessionId, cx: &mut Context) { - if let Some(client) = self.session_by_id(session_id) { - client.update(cx, |client, _| { - client.set_ignore_breakpoints(!client.breakpoints_enabled()); - }); - } - } - pub fn new_session( &mut self, config: DebugAdapterConfig, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 820742c934b1a0..ab77d64c34ec84 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -23,7 +23,7 @@ use dap_adapters::build_adapter; use futures::channel::oneshot; use futures::{future::join_all, future::Shared, FutureExt}; use gpui::{App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Task}; -use rpc::AnyProtoClient; +use rpc::{proto, AnyProtoClient}; use serde_json::Value; use settings::Settings; use std::path::PathBuf; @@ -761,12 +761,55 @@ impl Session { &self.modules } - pub fn set_ignore_breakpoints(&mut self, ignore: bool) { + pub fn toggle_ignore_breakpoints(&mut self, cx: &App) -> Task> { + self.set_ignore_breakpoints(!self.ignore_breakpoints, cx) + } + + pub(crate) fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &App) -> Task> { + if self.ignore_breakpoints == ignore { + return Task::ready(Err(anyhow!( + "Can't set ignore breakpoint to state it's already at" + ))); + } + + // todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions + self.ignore_breakpoints = ignore; + let mut tasks = Vec::new(); + + for (abs_path, serialized_breakpoints) in self + .breakpoint_store + .read_with(cx, |store, cx| store.all_breakpoints(true, cx)) + .into_iter() + { + let source_breakpoints = if self.ignore_breakpoints { + serialized_breakpoints + .iter() + .map(|bp| bp.to_source_breakpoint()) + .collect::>() + } else { + vec![] + }; + + tasks.push(self.send_breakpoints( + abs_path, + source_breakpoints, + self.ignore_breakpoints, + false, + cx, + )); + } + + cx.background_executor().spawn(async move { + join_all(tasks).await; + Ok(()) + }) } + pub fn breakpoints_enabled(&self) -> bool { self.ignore_breakpoints } + pub fn handle_module_event(&mut self, event: &dap::ModuleEvent, cx: &mut Context) { match event.reason { dap::ModuleEventReason::New => self.modules.push(event.module.clone()), @@ -1034,25 +1077,33 @@ impl Session { } pub fn stack_frames(&mut self, thread_id: ThreadId, cx: &mut Context) -> Vec { - self.fetch( - super::dap_command::StackTraceCommand { - thread_id: thread_id.0, - start_frame: None, - levels: None, - }, - move |this, stack_frames, cx| { - let entry = this.threads.entry(thread_id).and_modify(|thread| { - thread.stack_frames = stack_frames.iter().cloned().map(From::from).collect(); - }); - debug_assert!( - matches!(entry, indexmap::map::Entry::Occupied(_)), - "Sent request for thread_id that doesn't exist" - ); + if self + .threads + .get(&thread_id) + .map(|thread| thread.status == ThreadStatus::Stopped) + .unwrap_or(false) + { + self.fetch( + super::dap_command::StackTraceCommand { + thread_id: thread_id.0, + start_frame: None, + levels: None, + }, + move |this, stack_frames, cx| { + let entry = this.threads.entry(thread_id).and_modify(|thread| { + thread.stack_frames = + stack_frames.iter().cloned().map(From::from).collect(); + }); + debug_assert!( + matches!(entry, indexmap::map::Entry::Occupied(_)), + "Sent request for thread_id that doesn't exist" + ); - cx.notify(); - }, - cx, - ); + cx.notify(); + }, + cx, + ); + } self.threads .get(&thread_id) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 578f6a44bd324e..8cc6da5c97c73a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -644,8 +644,6 @@ impl Project { client.add_entity_request_handler(Self::handle_open_new_buffer); client.add_entity_message_handler(Self::handle_create_buffer_for_peer); - client.add_entity_message_handler(Self::handle_toggle_ignore_breakpoints); - WorktreeStore::init(&client); BufferStore::init(&client); LspStore::init(&client); @@ -1322,114 +1320,6 @@ impl Project { }) } - pub fn serialize_breakpoints( - &self, - cx: &Context, - ) -> HashMap, Vec> { - self.breakpoint_store.read(cx).serialize_breakpoints(cx) - } - - async fn handle_toggle_ignore_breakpoints( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |project, cx| { - // Only the host should handle this message because the host - // handles direct communication with the debugger servers. - if let Some((_, _)) = project.dap_store.read(cx).downstream_client() { - project - .toggle_ignore_breakpoints( - SessionId::from_proto(envelope.payload.client_id), - cx, - ) - .detach_and_log_err(cx); - } - }) - } - - pub fn toggle_ignore_breakpoints( - &self, - session_id: SessionId, - cx: &mut Context, - ) -> Task> { - let tasks = self.dap_store.update(cx, |store, cx| { - if let Some((upstream_client, project_id)) = store.upstream_client() { - upstream_client - .send(proto::ToggleIgnoreBreakpoints { - client_id: session_id.to_proto(), - project_id, - }) - .log_err(); - - return Vec::new(); - } - - store.toggle_ignore_breakpoints(&session_id, cx); - - if let Some((downstream_client, project_id)) = store.downstream_client() { - downstream_client - .send(proto::IgnoreBreakpointState { - session_id: session_id.to_proto(), - project_id: *project_id, - ignore: store.ignore_breakpoints(&session_id, cx), - }) - .log_err(); - } - - let mut tasks: Vec> = Vec::new(); - - for (project_path, breakpoints) in &self.breakpoint_store.read(cx).breakpoints { - let Some((buffer, buffer_path)) = maybe!({ - let buffer = self - .buffer_store - .read_with(cx, |store, cx| store.get_by_path(&project_path, cx))?; - - let buffer = buffer.read(cx); - let project_path = buffer.project_path(cx)?; - let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; - Some(( - buffer, - worktree.read(cx).absolutize(&project_path.path).ok()?, - )) - }) else { - continue; - }; - - // todo (debugger): Fix breakpoint stuff - // tasks.push( - // store.send_breakpoints( - // client_id, - // Arc::from(buffer_path), - // breakpoints - // .into_iter() - // .map(|breakpoint| breakpoint.to_source_breakpoint(buffer)) - // .collect::>(), - // store.ignore_breakpoints(&client_id, cx), - // false, - // cx, - // ), - // ); - } - - tasks - }); - - cx.background_executor().spawn(async move { - // try_join_all(tasks).await?; - - Ok(()) - }) - } - - /// Toggles a breakpoint - /// - /// Also sends updated breakpoint information of one file to all active debug adapters - /// - /// This function is called whenever a breakpoint is toggled, and it doesn't need - /// to send breakpoints from closed files because those breakpoints can't change - /// without opening a buffer. - #[cfg(any(test, feature = "test-support"))] pub async fn example( root_paths: impl IntoIterator, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 017b79bb331480..2a458b61d82eee 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2890,7 +2890,7 @@ message DapRestartStackFrameRequest { message ToggleIgnoreBreakpoints { uint64 project_id = 1; - uint64 client_id = 2; + uint32 session_id = 2; } message IgnoreBreakpointState { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d7222b4fac0c57..2bb07a1cc34b8c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4602,9 +4602,12 @@ impl Workspace { }; if let Some(location) = location { - let breakpoint_lines = self - .project - .update(cx, |project, cx| project.serialize_breakpoints(cx)); + let breakpoint_lines = self.project.update(cx, |project, cx| { + project + .breakpoint_store() + .read(cx) + .serialize_breakpoints(cx) + }); let center_group = build_serialized_pane_group(&self.center.root, window, cx); let docks = build_serialized_docks(self, window, cx); From 13dd3aa2b5012355e91d7e384cea144d331faed7 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 19 Feb 2025 02:26:00 -0500 Subject: [PATCH 614/650] Enable session to send changed breakpoints to active dap servers --- .../project/src/debugger/breakpoint_store.rs | 2 +- crates/project/src/debugger/session.rs | 64 +++++++++++++++---- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index d24c1533e00f56..39edab41cb60c0 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -424,7 +424,7 @@ impl BreakpointStore { }) } - fn serialize_breakpoints_for_project_path( + pub(crate) fn serialize_breakpoints_for_project_path( &self, project_path: &ProjectPath, cx: &App, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index ab77d64c34ec84..70f6eaa390425a 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1,6 +1,6 @@ use crate::project_settings::ProjectSettings; -use super::breakpoint_store::{self, BreakpointStore}; +use super::breakpoint_store::{self, BreakpointStore, BreakpointStoreEvent}; use super::dap_command::{ self, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, Initialize, Launch, LocalDapCommand, NextCommand, PauseCommand, RestartCommand, @@ -425,17 +425,22 @@ impl Session { ) .await?; - cx.new(|_| Self { - mode: Mode::Local(mode), - id: session_id, - breakpoint_store: breakpoints, - config, - capabilities, - ignore_breakpoints: false, - requests: HashMap::default(), - modules: Vec::default(), - loaded_sources: Vec::default(), - threads: IndexMap::default(), + cx.new(|cx| { + cx.subscribe(&breakpoints, Self::send_changed_breakpoints) + .detach(); + + Self { + mode: Mode::Local(mode), + id: session_id, + breakpoint_store: breakpoints, + config, + capabilities, + ignore_breakpoints: false, + requests: HashMap::default(), + modules: Vec::default(), + loaded_sources: Vec::default(), + threads: IndexMap::default(), + } }) }) } @@ -475,6 +480,41 @@ impl Session { self.config.clone() } + fn send_changed_breakpoints( + &mut self, + _breakpoint_store: Entity, + event: &BreakpointStoreEvent, + cx: &mut Context, + ) { + let BreakpointStoreEvent::BreakpointsChanged { + project_path, + source_changed, + } = event; + + // We still want to send an empty list of breakpoints to tell the dap server that there are no breakpoints + let Some((abs_path, breakpoints)) = self + .breakpoint_store + .read(cx) + .serialize_breakpoints_for_project_path(project_path, cx) + else { + return; + }; + + let source_breakpoints = breakpoints + .iter() + .map(|bp| bp.to_source_breakpoint()) + .collect::>(); + + self.send_breakpoints( + abs_path, + source_breakpoints, + self.ignore_breakpoints, + *source_changed, + cx, + ) + .detach_and_log_err(cx); + } + fn send_initial_breakpoints(&self, cx: &App) -> Task<()> { let mut tasks = Vec::new(); From 650984bfe5b944273201af995ffd6d3a1bc6b81c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:12:37 +0100 Subject: [PATCH 615/650] Send out breakpoints in the configuration sequence Co-authored-by: Remco Smits --- crates/project/src/debugger/dap_command.rs | 34 ++++++++++++++++-- crates/project/src/debugger/session.rs | 42 ++++++++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index cb50e9952b91d2..73b338b2c58606 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -6,12 +6,15 @@ use dap::{ proto_conversions::ProtoConversion, requests::{Continue, Next}, Capabilities, ContinueArguments, InitializeRequestArguments, - InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, StepInArguments, - StepOutArguments, SteppingGranularity, ValueFormat, Variable, VariablesArgumentsFilter, + InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint, + StepInArguments, StepOutArguments, SteppingGranularity, ValueFormat, Variable, + VariablesArgumentsFilter, }; use rpc::proto; use serde_json::Value; use util::ResultExt; + +use super::breakpoint_store::SerializedBreakpoint; pub(crate) trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; @@ -1641,3 +1644,30 @@ impl LocalDapCommand for Launch { Ok(message) } } + +#[derive(Clone, Debug, Hash, PartialEq)] +pub(super) struct SetBreakpoints { + pub(super) source: dap::Source, + pub(super) breakpoints: Vec, +} + +impl LocalDapCommand for SetBreakpoints { + type Response = Vec; + type DapRequest = dap::requests::SetBreakpoints; + + fn to_dap(&self) -> ::Arguments { + dap::SetBreakpointsArguments { + lines: None, + source_modified: None, + source: self.source.clone(), + breakpoints: Some(self.breakpoints.clone()), + } + } + + fn response_from_dap( + &self, + message: ::Response, + ) -> Result { + Ok(message.breakpoints) + } +} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 70f6eaa390425a..79b58bc77e9573 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -168,12 +168,29 @@ struct LocalMode { enum ReverseRequest { RunInTerminal(), } + +fn client_source(abs_path: &Path) -> dap::Source { + dap::Source { + name: abs_path + .file_name() + .map(|filename| filename.to_string_lossy().to_string()), + path: Some(abs_path.to_string_lossy().to_string()), + source_reference: None, + presentation_hint: None, + origin: None, + sources: None, + adapter_data: None, + checksums: None, + } +} + impl LocalMode { fn new( session_id: SessionId, + breakpoints: Entity, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, - cx: AsyncApp, + mut cx: AsyncApp, ) -> Task> { cx.spawn(move |mut cx| async move { let adapter = build_adapter(&disposition.kind).await?; @@ -247,8 +264,29 @@ impl LocalMode { // Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 let launch = this.request(Launch { raw }, cx.background_executor().clone()); let that = this.clone(); + let breakpoints = + breakpoints.update(&mut cx, |this, cx| this.all_breakpoints(true, cx))?; + let configuration_sequence = async move { let _ = initialized_rx.await?; + + let mut breakpoint_tasks = Vec::new(); + for (path, breakpoints) in breakpoints { + breakpoint_tasks.push( + that.request( + dap_command::SetBreakpoints { + source: client_source(&path), + breakpoints: breakpoints + .iter() + .map(|breakpoint| breakpoint.to_source_breakpoint()) + .collect(), + }, + cx.background_executor().clone(), + ), + ); + } + let _ = futures::future::join_all(breakpoint_tasks).await; + if capabilities .supports_configuration_done_request .unwrap_or_default() @@ -418,9 +456,9 @@ impl Session { cx.spawn(move |mut cx| async move { let (mode, capabilities) = LocalMode::new( session_id, + breakpoints.clone(), config.clone(), delegate, - // message_handler, cx.clone(), ) .await?; From b43a2d1a8a50b3ae59ea7baa842f260e0d47752f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:56:52 +0100 Subject: [PATCH 616/650] Bubble up event handling to Session and Dap Store Co-authored-by: Remco Smits --- crates/project/src/debugger/dap_store.rs | 46 +++++++---- crates/project/src/debugger/session.rs | 101 +++++++++++------------ 2 files changed, 79 insertions(+), 68 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index d2f89328223472..1d83cbe0a471f5 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -44,7 +44,7 @@ use rpc::{ }; use serde_json::Value; use settings::{Settings as _, WorktreeId}; -use smol::lock::Mutex; +use smol::{lock::Mutex, stream::StreamExt}; use std::{ borrow::Borrow, collections::{BTreeMap, HashSet}, @@ -86,6 +86,8 @@ pub struct LocalDapStore { environment: Entity, language_registry: Arc, toolchain_store: Arc, + start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>, + _start_debugging_task: Task<()>, } impl LocalDapStore { @@ -250,6 +252,29 @@ impl DapStore { ) -> Self { cx.on_app_quit(Self::shutdown_sessions).detach(); + let (start_debugging_tx, mut message_rx) = + futures::channel::mpsc::unbounded::<(SessionId, Message)>(); + + let _start_debugging_task = cx.spawn(move |this, mut cx| async move { + while let Some((session_id, message)) = message_rx.next().await { + match message { + Message::Request(request) => { + this.update(&mut cx, |this, cx| { + if request.command == StartDebugging::COMMAND { + // this.sessions.get(1).update(|session, cx| { + // session.child(session_id, cx); + // }); + + // this.new_session(config, worktree, cx) + } else if request.command == RunInTerminal::COMMAND { + // spawn terminal + } + }); + } + _ => {} + } + } + }); Self { mode: DapStoreMode::Local(LocalDapStore { fs, @@ -259,6 +284,8 @@ impl DapStore { toolchain_store, language_registry, next_session_id: Default::default(), + start_debugging_tx, + _start_debugging_task, }), downstream_client: None, active_debug_line: None, @@ -490,22 +517,7 @@ impl DapStore { session_id, delegate, config, - { - let weak_store = cx.weak_entity(); - - Box::new(move |message, cx| { - weak_store - .update(cx, |store, cx| { - if let Some(session) = store.sessions.get(&session_id) { - session.update(cx, |session, cx| { - session.handle_dap_message(message, cx); - }); - } - }) - .with_context(|| "Failed to process message from DAP server") - .log_err(); - }) - }, + local_store.start_debugging_tx.clone(), cx, ); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 79b58bc77e9573..35433b6db65cec 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -21,11 +21,13 @@ use dap::{ }; use dap_adapters::build_adapter; use futures::channel::oneshot; +use futures::SinkExt; use futures::{future::join_all, future::Shared, FutureExt}; -use gpui::{App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Task}; +use gpui::{App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Task, WeakEntity}; use rpc::{proto, AnyProtoClient}; use serde_json::Value; use settings::Settings; +use smol::stream::StreamExt; use std::path::PathBuf; use std::u64; use std::{ @@ -190,7 +192,8 @@ impl LocalMode { breakpoints: Entity, disposition: DebugAdapterConfig, delegate: DapAdapterDelegate, - mut cx: AsyncApp, + messages_tx: futures::channel::mpsc::UnboundedSender, + cx: AsyncApp, ) -> Task> { cx.spawn(move |mut cx| async move { let adapter = build_adapter(&disposition.kind).await?; @@ -232,19 +235,20 @@ impl LocalMode { let (initialized_tx, initialized_rx) = oneshot::channel(); let mut initialized_tx = Some(initialized_tx); - let message_handler = Box::new(move |message, cx: &mut App| match message { - Message::Event(events) => { - if let Events::Initialized(_) = *events { - initialized_tx - .take() - .expect("To receive just one Initialized event") - .send(()) - .ok(); - } + let message_handler = Box::new(move |message, _cx: &mut App| { + let Message::Event(event) = &message else { + messages_tx.unbounded_send(message).ok(); + return; + }; + if let Events::Initialized(_) = **event { + initialized_tx + .take() + .expect("To receive just one Initialized event") + .send(()) + .ok(); + } else { + messages_tx.unbounded_send(message).ok(); } - - Message::Request(request) => todo!(), - _ => {} }); let client = Arc::new( DebugAdapterClient::start(session_id, binary, message_handler, cx.clone()).await?, @@ -365,6 +369,7 @@ pub struct Session { loaded_sources: Vec, threads: IndexMap, requests: HashMap>>>, + _background_tasks: Vec>, } trait CacheableCommand: 'static + Send + Sync { @@ -450,20 +455,41 @@ impl Session { session_id: SessionId, delegate: DapAdapterDelegate, config: DebugAdapterConfig, - message_handler: DapMessageHandler, + start_debugging_requests_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>, cx: &mut App, ) -> Task>> { + let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded(); + cx.spawn(move |mut cx| async move { let (mode, capabilities) = LocalMode::new( session_id, breakpoints.clone(), config.clone(), delegate, + message_tx, cx.clone(), ) .await?; cx.new(|cx| { + let _background_tasks = + vec![cx.spawn(move |this: WeakEntity, mut cx| async move { + while let Some(message) = message_rx.next().await { + if let Message::Event(event) = message { + let Ok(_) = this.update(&mut cx, |session, cx| { + session.handle_dap_event(event, cx); + }) else { + break; + }; + } else { + let Ok(_) = start_debugging_requests_tx + .unbounded_send((session_id, message)) + else { + break; + }; + } + } + })]; cx.subscribe(&breakpoints, Self::send_changed_breakpoints) .detach(); @@ -478,6 +504,7 @@ impl Session { modules: Vec::default(), loaded_sources: Vec::default(), threads: IndexMap::default(), + _background_tasks, } }) }) @@ -504,6 +531,7 @@ impl Session { loaded_sources: Vec::default(), threads: IndexMap::default(), config: todo!(), + _background_tasks: Vec::default(), } } @@ -623,40 +651,6 @@ impl Session { }) } - fn handle_initialized_event(&mut self, caps: Option, cx: &mut Context) { - if let Some(caps) = caps { - self.capabilities = caps; - } - - let send_breakpoints = self.send_initial_breakpoints(cx); - - cx.spawn(|this, mut cx| async move { - send_breakpoints.await; - - this.update(&mut cx, |this, cx| { - if this - .capabilities - .supports_configuration_done_request - .unwrap_or_default() - { - if let Some(adapter) = this.adapter_client() { - return cx.background_spawn(async move { - adapter - .request::( - dap::ConfigurationDoneArguments, - ) - .await - .log_err(); - }); - } - } - - Task::ready(()) - }) - }) - .detach_and_log_err(cx); - } - fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context) { // todo(debugger): We should see if we could only invalidate the thread that stopped // instead of everything right now @@ -680,9 +674,14 @@ impl Session { } } - fn handle_dap_event(&mut self, event: Box, cx: &mut Context) { + pub(crate) fn handle_dap_event(&mut self, event: Box, cx: &mut Context) { match *event { - Events::Initialized(event) => self.handle_initialized_event(event, cx), + Events::Initialized(_) => { + debug_assert!( + false, + "Initialized event should have been handled in LocalMode" + ); + } Events::Stopped(event) => self.handle_stopped_event(event, cx), Events::Continued(_event) => {} Events::Exited(_event) => {} From c711168ad41eaf15ad18c6c4783fed9bcb4ca57d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:41:38 +0100 Subject: [PATCH 617/650] Start wiring through thread state handling Co-authored-by: Remco Smits --- crates/project/src/debugger/session.rs | 144 ++++++++++++++----------- 1 file changed, 79 insertions(+), 65 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 35433b6db65cec..4b08803c693693 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -3,9 +3,10 @@ use crate::project_settings::ProjectSettings; use super::breakpoint_store::{self, BreakpointStore, BreakpointStoreEvent}; use super::dap_command::{ self, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, - Initialize, Launch, LocalDapCommand, NextCommand, PauseCommand, RestartCommand, - RestartStackFrameCommand, ScopesCommand, SetVariableValueCommand, StepBackCommand, StepCommand, - StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, VariablesCommand, + Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, ModulesCommand, NextCommand, + PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, SetVariableValueCommand, + StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, + TerminateThreadsCommand, ThreadsCommand, VariablesCommand, }; use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; @@ -107,7 +108,6 @@ pub enum ThreadStatus { pub struct Thread { dap: dap::Thread, stack_frames: Vec, - status: ThreadStatus, has_stopped: bool, } @@ -116,7 +116,6 @@ impl From for Thread { Self { dap, stack_frames: vec![], - status: ThreadStatus::default(), has_stopped: false, } } @@ -357,6 +356,45 @@ impl Mode { } } +#[derive(Default)] +struct ThreadStates { + global_state: Option, + known_thread_states: IndexMap, +} + +impl ThreadStates { + fn all_threads_stopped(&mut self) { + self.global_state = Some(ThreadStatus::Stopped); + self.known_thread_states.clear(); + } + fn all_threads_continued(&mut self) { + self.global_state = Some(ThreadStatus::Running); + self.known_thread_states.clear(); + } + fn thread_stopped(&mut self, thread_id: ThreadId) { + self.known_thread_states + .insert(thread_id, ThreadStatus::Stopped); + } + fn thread_continued(&mut self, thread_id: ThreadId) { + self.known_thread_states + .insert(thread_id, ThreadStatus::Running); + } + + fn thread_status(&self, thread_id: ThreadId) -> ThreadStatus { + self.thread_state(thread_id) + .unwrap_or(ThreadStatus::Running) + } + + // stopped event (all_threads: true) + // thread event (reason: started) <- true state is running, but we'll think that it's stopped + fn thread_state(&self, thread_id: ThreadId) -> Option { + self.known_thread_states + .get(&thread_id) + .copied() + .or(self.global_state) + } +} + /// Represents a current state of a single debug adapter and provides ways to mutate it. pub struct Session { mode: Mode, @@ -369,6 +407,7 @@ pub struct Session { loaded_sources: Vec, threads: IndexMap, requests: HashMap>>>, + thread_states: ThreadStates, _background_tasks: Vec>, } @@ -499,6 +538,7 @@ impl Session { breakpoint_store: breakpoints, config, capabilities, + thread_states: ThreadStates::default(), ignore_breakpoints: false, requests: HashMap::default(), modules: Vec::default(), @@ -526,6 +566,7 @@ impl Session { capabilities: Capabilities::default(), breakpoint_store, ignore_breakpoints, + thread_states: ThreadStates::default(), requests: HashMap::default(), modules: Vec::default(), loaded_sources: Vec::default(), @@ -658,19 +699,12 @@ impl Session { // todo(debugger): We should query for all threads here if we don't get a thread id // maybe in both cases too? - if let Some(thread_id) = event.thread_id { - self.threads.insert( - ThreadId(thread_id), - Thread { - dap: dap::Thread { - id: thread_id, - name: "".into(), - }, - stack_frames: vec![], - status: ThreadStatus::Stopped, - has_stopped: true, - }, - ); + if event.all_threads_stopped.unwrap_or_default() { + self.thread_states.all_threads_stopped(); + } else if let Some(thread_id) = event.thread_id { + self.thread_states.thread_stopped(ThreadId(thread_id)); + } else { + // TODO(debugger): all threads should be stopped } } @@ -686,7 +720,16 @@ impl Session { Events::Continued(_event) => {} Events::Exited(_event) => {} Events::Terminated(_event) => {} - Events::Thread(_event) => {} + Events::Thread(event) => { + match event.reason { + dap::ThreadEventReason::Started => { + self.thread_states + .thread_continued(ThreadId(event.thread_id)); + } + _ => {} + } + self.invalidate_state(&ThreadsCommand.into()); + } Events::Output(_event) => {} Events::Breakpoint(_) => {} Events::Module(_event) => {} @@ -800,14 +843,18 @@ impl Session { ) } - pub fn invalidate(&mut self, cx: &mut Context) { + fn invalidate_state(&mut self, key: &RequestSlot) { + self.requests.remove(&key); + } + + fn invalidate(&mut self, cx: &mut Context) { self.requests.clear(); self.modules.clear(); self.loaded_sources.clear(); cx.notify(); } - pub fn threads(&mut self, cx: &mut Context) -> Vec { + pub fn threads(&mut self, cx: &mut Context) -> Vec<(dap::Thread, ThreadStatus)> { self.fetch( dap_command::ThreadsCommand, |this, result, cx| { @@ -822,7 +869,12 @@ impl Session { ); self.threads .values() - .map(|thread| thread.dap.clone()) + .map(|thread| { + ( + thread.dap.clone(), + self.thread_states.thread_status(ThreadId(thread.dap.id)), + ) + }) .collect() } @@ -887,16 +939,8 @@ impl Session { self.ignore_breakpoints } - pub fn handle_module_event(&mut self, event: &dap::ModuleEvent, cx: &mut Context) { - match event.reason { - dap::ModuleEventReason::New => self.modules.push(event.module.clone()), - dap::ModuleEventReason::Changed => { - if let Some(module) = self.modules.iter_mut().find(|m| m.id == event.module.id) { - *module = event.module.clone(); - } - } - dap::ModuleEventReason::Removed => self.modules.retain(|m| m.id != event.module.id), - } + pub fn handle_module_event(&mut self, _event: &dap::ModuleEvent, cx: &mut Context) { + self.invalidate_state(&ModulesCommand.into()); cx.notify(); } @@ -1121,45 +1165,15 @@ impl Session { pub fn handle_loaded_source_event( &mut self, - event: &dap::LoadedSourceEvent, + _: &dap::LoadedSourceEvent, cx: &mut Context, ) { - match event.reason { - dap::LoadedSourceEventReason::New => self.loaded_sources.push(event.source.clone()), - dap::LoadedSourceEventReason::Changed => { - let updated_source = - if let Some(ref_id) = event.source.source_reference.filter(|&r| r != 0) { - self.loaded_sources - .iter_mut() - .find(|s| s.source_reference == Some(ref_id)) - } else if let Some(path) = &event.source.path { - self.loaded_sources - .iter_mut() - .find(|s| s.path.as_ref() == Some(path)) - } else { - self.loaded_sources - .iter_mut() - .find(|s| s.name == event.source.name) - }; - - if let Some(loaded_source) = updated_source { - *loaded_source = event.source.clone(); - } - } - dap::LoadedSourceEventReason::Removed => { - self.loaded_sources.retain(|source| *source != event.source) - } - } + self.invalidate_state(&LoadedSourcesCommand.into()); cx.notify(); } pub fn stack_frames(&mut self, thread_id: ThreadId, cx: &mut Context) -> Vec { - if self - .threads - .get(&thread_id) - .map(|thread| thread.status == ThreadStatus::Stopped) - .unwrap_or(false) - { + if self.thread_states.thread_status(thread_id) == ThreadStatus::Stopped { self.fetch( super::dap_command::StackTraceCommand { thread_id: thread_id.0, From 14eab34e618d5e5a07dddd36f46efaff3b1b9cc5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:54:36 +0100 Subject: [PATCH 618/650] Show threads in the dropdown Co-authored-by: Remco Smits --- crates/debugger_ui/src/session/running.rs | 21 ++++++++++++++------- crates/project/src/debugger/session.rs | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 2d5b20531655e1..300751d6964192 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -53,8 +53,9 @@ impl Render for RunningState { let thread_status = ThreadStatus::Running; let active_thread_item = &self.active_thread_item; + let threads = self.session.update(cx, |this, cx| this.threads(cx)); let capabilities = self.capabilities(cx); - + let state = cx.entity(); h_flex() .key_context("DebugPanelItem") .track_focus(&self.focus_handle(cx)) @@ -236,12 +237,18 @@ impl Render for RunningState { DropdownMenu::new( "thread-list", "Threads", - ContextMenu::build(window, cx, |this, _, _| { - this.entry("Thread 1", None, |_, _| {}).entry( - "Thread 2", - None, - |_, _| {}, - ) + ContextMenu::build(window, cx, move |mut this, _, _| { + for (thread, status) in threads { + let state = state.clone(); + let thread_id = thread.id; + this = this.entry(thread.name, None, move |_, cx| { + state.update(cx, |state, cx| { + state.thread_id = ThreadId(thread_id); + cx.notify(); + }); + }); + } + this }), ), )), diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 4b08803c693693..3964d811b77871 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -96,7 +96,7 @@ impl From for StackFrame { } } -#[derive(Copy, Clone, Default, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum ThreadStatus { #[default] Running, From 2e83b375a24dd9df42dbf4933a4b93ec1f44e844 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 19 Feb 2025 14:08:44 +0100 Subject: [PATCH 619/650] Fix didn't do anything with returned task --- crates/debugger_ui/src/session/running.rs | 3 ++- crates/project/src/debugger/dap_store.rs | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 300751d6964192..52e52166ebe0ca 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -594,7 +594,8 @@ impl RunningState { pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context) { self.session - .update(cx, |session, cx| session.toggle_ignore_breakpoints(cx)); + .update(cx, |session, cx| session.toggle_ignore_breakpoints(cx)) + .detach_and_log_err(cx); } } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 1d83cbe0a471f5..ad050c99579d10 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -481,11 +481,12 @@ impl DapStore { if let Some(session) = this.session_by_id(&session_id) { session.update(cx, |session, cx| { session.set_ignore_breakpoints(envelope.payload.ignore, cx) - }); + }) + } else { + Task::ready(Ok(())) } - })?; - - Ok(()) + })? + .await } pub fn new_session( From 8a5e75408bc7c2abd9dc0792fe3dfe0ce48e67dd Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 19 Feb 2025 14:10:17 +0100 Subject: [PATCH 620/650] Impl continued event --- crates/project/src/debugger/session.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 3964d811b77871..335e332f8e45f2 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -717,7 +717,15 @@ impl Session { ); } Events::Stopped(event) => self.handle_stopped_event(event, cx), - Events::Continued(_event) => {} + Events::Continued(event) => { + if event.all_threads_continued.unwrap_or_default() { + self.thread_states.all_threads_continued(); + } else { + self.thread_states + .thread_continued(ThreadId(event.thread_id)); + } + self.invalidate(cx); + } Events::Exited(_event) => {} Events::Terminated(_event) => {} Events::Thread(event) => { From 03144d77921dd6a8417bd3188f23f0ed4c8dcf8e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:25:22 +0100 Subject: [PATCH 621/650] Mark thread list as disabled when there are no threads. --- crates/debugger_ui/src/session/running.rs | 41 +++++++++++++---------- crates/ui/src/components/dropdown_menu.rs | 6 +++- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 52e52166ebe0ca..c9e22f9c621fef 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -54,6 +54,7 @@ impl Render for RunningState { let active_thread_item = &self.active_thread_item; let threads = self.session.update(cx, |this, cx| this.threads(cx)); + let has_no_threads = threads.is_empty(); let capabilities = self.capabilities(cx); let state = cx.entity(); h_flex() @@ -233,25 +234,29 @@ impl Render for RunningState { ), ) //.child(h_flex()) - .child(h_flex().p_1().mx_2().w_3_4().justify_end().child( - DropdownMenu::new( - "thread-list", - "Threads", - ContextMenu::build(window, cx, move |mut this, _, _| { - for (thread, status) in threads { - let state = state.clone(); - let thread_id = thread.id; - this = this.entry(thread.name, None, move |_, cx| { - state.update(cx, |state, cx| { - state.thread_id = ThreadId(thread_id); - cx.notify(); - }); - }); - } - this - }), + .child( + h_flex().p_1().mx_2().w_3_4().justify_end().child( + DropdownMenu::new( + "thread-list", + "Threads", + ContextMenu::build(window, cx, move |mut this, _, _| { + for (thread, status) in threads { + let state = state.clone(); + let thread_id = thread.id; + this = + this.entry(thread.name, None, move |_, cx| { + state.update(cx, |state, cx| { + state.thread_id = ThreadId(thread_id); + cx.notify(); + }); + }); + } + this + }), + ) + .disabled(has_no_threads), ), - )), + ), ) .child( h_flex() diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 1f2e0473af4fe1..00698912c19e47 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -45,7 +45,11 @@ impl RenderOnce for DropdownMenu { PopoverMenu::new(self.id) .full_width(self.full_width) .menu(move |_window, _cx| Some(self.menu.clone())) - .trigger(DropdownMenuTrigger::new(self.label).full_width(self.full_width)) + .trigger( + DropdownMenuTrigger::new(self.label) + .full_width(self.full_width) + .disabled(self.disabled), + ) .attach(Corner::BottomLeft) } } From f4cb78f5c6103712896b03c1346145786a5f5ba0 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 19 Feb 2025 14:41:11 +0100 Subject: [PATCH 622/650] Fix a few clippy errors --- crates/dap/src/adapters.rs | 1 - crates/dap/src/client.rs | 8 ++--- crates/dap/src/transport.rs | 4 +-- crates/dap_adapters/src/custom.rs | 4 +-- crates/dap_adapters/src/go.rs | 2 +- crates/dap_adapters/src/javascript.rs | 2 +- crates/dap_adapters/src/lldb.rs | 3 +- crates/dap_adapters/src/php.rs | 2 +- crates/dap_adapters/src/python.rs | 2 +- crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 33 +++++-------------- crates/debugger_ui/src/lib.rs | 5 +-- crates/debugger_ui/src/session.rs | 19 +++-------- crates/debugger_ui/src/session/inert.rs | 10 +++--- crates/debugger_ui/src/session/running.rs | 24 +++++--------- .../src/session/running/console.rs | 3 +- .../src/session/running/loaded_source_list.rs | 1 - .../src/session/running/module_list.rs | 2 +- .../src/session/running/variable_list.rs | 4 +-- crates/debugger_ui/src/session/starting.rs | 9 ++--- crates/project/src/debugger/dap_command.rs | 1 - crates/project/src/debugger/dap_store.rs | 32 +++++++----------- crates/project/src/debugger/session.rs | 7 ++-- crates/project/src/project.rs | 5 ++- 24 files changed, 59 insertions(+), 126 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 82ba49ce877945..a2f1304e254465 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -22,7 +22,6 @@ use std::{ ops::Deref, path::{Path, PathBuf}, sync::Arc, - time::Duration, }; use sysinfo::{Pid, Process}; use task::DebugAdapterConfig; diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index eee5bd2345e727..29c18711e7a641 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -1,5 +1,5 @@ use crate::{ - adapters::{DebugAdapter, DebugAdapterBinary, TcpArguments}, + adapters::DebugAdapterBinary, transport::{IoKind, LogKind, TransportDelegate}, }; use anyhow::{anyhow, Result}; @@ -12,11 +12,7 @@ use gpui::{App, AsyncApp, BackgroundExecutor}; use smol::channel::{Receiver, Sender}; use std::{ hash::Hash, - net::Ipv4Addr, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, - }, + sync::atomic::{AtomicU64, Ordering}, time::Duration, }; diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index dceca0d2e3e133..3200491757d187 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, bail, Context, Result}; -use async_trait::async_trait; use dap_types::{ messages::{Message, Response}, ErrorResponse, @@ -16,14 +15,13 @@ use smol::{ process::Child, }; use std::{ - any::Any, collections::HashMap, net::{Ipv4Addr, SocketAddrV4}, process::Stdio, sync::Arc, time::Duration, }; -use task::{DebugAdapterKind, TCPHost}; +use task::TCPHost; use util::ResultExt as _; use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings}; diff --git a/crates/dap_adapters/src/custom.rs b/crates/dap_adapters/src/custom.rs index 5eaec5530d445f..b1d3ff60bb8300 100644 --- a/crates/dap_adapters/src/custom.rs +++ b/crates/dap_adapters/src/custom.rs @@ -1,8 +1,6 @@ -use std::{ffi::OsString, path::PathBuf, sync::Arc}; - -use dap::transport::{StdioTransport, TcpTransport}; use gpui::AsyncApp; use serde_json::Value; +use std::{ffi::OsString, path::PathBuf}; use task::DebugAdapterConfig; use crate::*; diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index 000dcc60aa1be3..d367bbeebbd23e 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -1,6 +1,6 @@ use dap::transport::TcpTransport; use gpui::AsyncApp; -use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf, sync::Arc}; +use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; use crate::*; diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 2f56ff1260a7c4..4209a3b731f1d5 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -2,7 +2,7 @@ use adapters::latest_github_release; use dap::transport::TcpTransport; use gpui::AsyncApp; use regex::Regex; -use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf}; use sysinfo::{Pid, Process}; use task::DebugRequestType; diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index ad46db4fdfea9f..52a0c5adc1f983 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -1,8 +1,7 @@ -use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::Arc}; +use std::{collections::HashMap, ffi::OsStr, path::PathBuf}; use anyhow::Result; use async_trait::async_trait; -use dap::transport::StdioTransport; use gpui::AsyncApp; use sysinfo::{Pid, Process}; use task::{DebugAdapterConfig, DebugRequestType}; diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index a959a296f2238f..e7634d34f0537b 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -1,7 +1,7 @@ use adapters::latest_github_release; use dap::{adapters::TcpArguments, transport::TcpTransport}; use gpui::AsyncApp; -use std::{net::Ipv4Addr, path::PathBuf, sync::Arc}; +use std::{net::Ipv4Addr, path::PathBuf}; use crate::*; diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 27735cdcf2e97b..b5f4f5c9db55ef 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,7 +1,7 @@ use crate::*; use dap::transport::TcpTransport; use gpui::AsyncApp; -use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf, sync::Arc}; +use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf}; pub(crate) struct PythonDebugAdapter { port: u16, diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index b34e812876bb6a..e2728f784981e0 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -1,5 +1,5 @@ use dap::{ - client::{DebugAdapterClient, SessionId}, + client::SessionId, debugger_settings::DebuggerSettings, transport::{IoKind, LogKind}, }; diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index a9cdf52dcbb2c7..b0c0ca809caca5 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,40 +1,23 @@ -use crate::{attach_modal::AttachModal, session::DebugSession}; +use crate::session::DebugSession; use anyhow::Result; -use collections::{BTreeMap, HashMap}; use command_palette_hooks::CommandPaletteFilter; use dap::{ - client::SessionId, - debugger_settings::DebuggerSettings, - messages::{Events, Message}, - requests::{Request, RunInTerminal, StartDebugging}, - Capabilities, CapabilitiesEvent, ContinuedEvent, ErrorResponse, ExitedEvent, LoadedSourceEvent, - ModuleEvent, OutputEvent, RunInTerminalRequestArguments, RunInTerminalResponse, StoppedEvent, - TerminatedEvent, ThreadEvent, ThreadEventReason, + client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent, + ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, }; use gpui::{ actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::{ - debugger::{ - dap_store::{DapStore, DapStoreEvent}, - session::ThreadId, - }, - terminals::TerminalKind, - Project, -}; -use rpc::proto::{self, UpdateDebugAdapter}; -use serde_json::Value; +use project::Project; +use rpc::proto::{self}; use settings::Settings; -use std::{any::TypeId, collections::VecDeque, path::PathBuf, u64}; -use task::DebugRequestType; -use terminal_view::terminal_panel::TerminalPanel; +use std::any::TypeId; use ui::prelude::*; -use util::ResultExt as _; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - pane, Continue, Disconnect, Pane, Pause, Restart, Start, StepBack, StepInto, StepOut, StepOver, - Stop, ToggleIgnoreBreakpoints, Workspace, + pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop, + ToggleIgnoreBreakpoints, Workspace, }; pub enum DebugPanelEvent { diff --git a/crates/debugger_ui/src/lib.rs b/crates/debugger_ui/src/lib.rs index 7d4dc52538e968..49c64edf502951 100644 --- a/crates/debugger_ui/src/lib.rs +++ b/crates/debugger_ui/src/lib.rs @@ -3,10 +3,7 @@ use debugger_panel::{DebugPanel, ToggleFocus}; use gpui::App; use session::DebugSession; use settings::Settings; -use workspace::{ - Continue, Pause, Restart, ShutdownDebugAdapters, Start, StepBack, StepInto, StepOut, StepOver, - Stop, ToggleIgnoreBreakpoints, Workspace, -}; +use workspace::{ShutdownDebugAdapters, Start, Workspace}; pub mod attach_modal; pub mod debugger_panel; diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index bd6f15e3ae0213..99aec3a21bc9c2 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -2,32 +2,21 @@ mod inert; mod running; mod starting; -use std::time::Duration; - -use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; - -use anyhow::anyhow; -use dap::{ - client::SessionId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent, - LoadedSourceEvent, ModuleEvent, OutputEvent, OutputEventCategory, StoppedEvent, ThreadEvent, -}; +use dap::client::SessionId; use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; use inert::{InertEvent, InertState}; -use project::debugger::session::Session; -use project::debugger::session::{ThreadId, ThreadStatus}; - use project::debugger::dap_store::DapStore; +use project::debugger::session::ThreadId; use project::worktree_store::WorktreeStore; use project::Project; use rpc::proto::{self, PeerId}; use running::RunningState; -use settings::Settings; use starting::{StartingEvent, StartingState}; -use ui::{prelude::*, ContextMenu, DropdownMenu, Indicator, Tooltip}; +use ui::prelude::*; use workspace::{ - item::{self, Item, ItemEvent}, + item::{self, Item}, FollowableItem, ViewId, Workspace, }; diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 67ddcf4b53d254..8b495d6a9e49a3 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,8 +1,8 @@ use dap::{DebugAdapterConfig, DebugRequestType}; use gpui::{App, EventEmitter, FocusHandle, Focusable}; use ui::{ - div, h_flex, v_flex, Button, ButtonCommon, ButtonStyle, Clickable, Context, ContextMenu, - DropdownMenu, Element, InteractiveElement, ParentElement, Render, SharedString, Styled, Window, + h_flex, v_flex, Button, ButtonCommon, ButtonStyle, Clickable, Context, ContextMenu, + DropdownMenu, InteractiveElement, ParentElement, Render, SharedString, Styled, Window, }; pub(super) struct InertState { @@ -78,16 +78,16 @@ impl Render for InertState { cx.emit(InertEvent::Spawned { config: DebugAdapterConfig { label: "hard coded".into(), - kind: dap::DebugAdapterKind::Python(task::TCPHost { + kind: dap::DebugAdapterKind::Javascript(task::TCPHost { port: None, host: None, timeout: None, }), request: DebugRequestType::Launch, program: Some( - "/Users/hiro/Projects/repros/python-funsies/nested/file.py".into(), + "/Users/remcosmits/Documents/code/prettier-test/test.js".into(), ), - cwd: Some("/Users/hiro/Projects/repros/python-funsies/nested".into()), + cwd: Some("/Users/remcosmits/Documents/code/prettier-test".into()), initialize_args: None, supports_attach: false, }, diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index c9e22f9c621fef..bfb168fdac44d8 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -4,32 +4,24 @@ mod module_list; mod stack_frame_list; mod variable_list; +use super::{DebugPanelItemEvent, ThreadItem}; use console::Console; -use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities, ContinuedEvent}; -use gpui::{ - AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, -}; +use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities}; +use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity}; use loaded_source_list::LoadedSourceList; use module_list::ModuleList; use project::debugger::session::{Session, ThreadId, ThreadStatus}; -use rpc::proto::{self, ViewId}; +use rpc::proto::ViewId; use settings::Settings; use stack_frame_list::{StackFrameList, StackFrameListEvent}; use ui::{ div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Color, - Context, ContextMenu, Disableable, DropdownMenu, Element, FluentBuilder, IconButton, IconName, - IconSize, Indicator, InteractiveElement, IntoElement, Label, LabelCommon, ParentElement, - Render, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window, + Context, ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, + Indicator, InteractiveElement, IntoElement, Label, LabelCommon, ParentElement, Render, + SharedString, StatefulInteractiveElement, Styled, Tooltip, Window, }; use variable_list::VariableList; -use workspace::{ - item::{self, ItemEvent}, - FollowableItem, Item, Workspace, -}; - -use crate::debugger_panel::{DebugPanel, DebugPanelEvent}; - -use super::{DebugPanelItemEvent, ThreadItem}; +use workspace::{item::ItemEvent, Item, Workspace}; pub struct RunningState { session: Entity, diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 02b573510bb5ce..5e46d69f2a334d 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -2,8 +2,7 @@ use super::{ stack_frame_list::{StackFrameList, StackFrameListEvent}, variable_list::VariableList, }; -use anyhow::anyhow; -use dap::{client::SessionId, OutputEvent, OutputEventGroup}; +use dap::{OutputEvent, OutputEventGroup}; use editor::{ display_map::{Crease, CreaseId}, Anchor, CompletionProvider, Editor, EditorElement, EditorStyle, FoldPlaceholder, diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index 2d0eeb26270104..fd1fbdc98199f6 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -1,4 +1,3 @@ -use dap::client::SessionId; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; use project::debugger::session::Session; use ui::prelude::*; diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index f228ab90252357..aad4aaf9a5a331 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -1,4 +1,4 @@ -use dap::{client::SessionId, ModuleEvent}; +use dap::ModuleEvent; use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription}; use project::debugger::session::Session; use ui::prelude::*; diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index db0ac3fb9554d8..f5c032b5d9d42b 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -1,8 +1,6 @@ use super::stack_frame_list::{StackFrameId, StackFrameList, StackFrameListEvent}; use anyhow::{anyhow, Result}; -use dap::{ - client::SessionId, proto_conversions::ProtoConversion, Scope, ScopePresentationHint, Variable, -}; +use dap::{proto_conversions::ProtoConversion, Scope, ScopePresentationHint, Variable}; use editor::{actions::SelectAll, Editor, EditorEvent}; use gpui::{ actions, anchored, deferred, list, AnyElement, ClipboardItem, Context, DismissEvent, Entity, diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs index a4330d04a0ca41..cf3ff928c46216 100644 --- a/crates/debugger_ui/src/session/starting.rs +++ b/crates/debugger_ui/src/session/starting.rs @@ -3,14 +3,11 @@ use std::time::Duration; use anyhow::Result; use gpui::{ - percentage, Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, - Subscription, Task, Transformation, + percentage, Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task, + Transformation, }; use project::debugger::session::Session; -use ui::{ - div, v_flex, Color, Context, Element, Icon, IconName, IconSize, IntoElement, ParentElement, - Render, Styled, -}; +use ui::{v_flex, Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled}; pub(super) struct StartingState { focus_handle: FocusHandle, diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 73b338b2c58606..053049c350eb19 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -14,7 +14,6 @@ use rpc::proto; use serde_json::Value; use util::ResultExt; -use super::breakpoint_store::SerializedBreakpoint; pub(crate) trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index ad050c99579d10..6ebc5a822dfead 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -6,36 +6,29 @@ use super::{ // RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, // TerminateCommand, TerminateThreadsCommand, VariablesCommand, // }, - dap_command::DapCommand, session::{self, Session}, }; -use crate::{ - debugger, project_settings::ProjectSettings, DebugAdapterClientState, ProjectEnvironment, - ProjectPath, -}; -use anyhow::{anyhow, bail, Context as _, Result}; +use crate::{debugger, ProjectEnvironment, ProjectPath}; +use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; use collections::HashMap; use dap::{ - adapters::{DapDelegate, DapStatus, DebugAdapter, DebugAdapterBinary, DebugAdapterName}, - client::{DebugAdapterClient, SessionId}, + adapters::{DapStatus, DebugAdapterName}, + client::SessionId, messages::{Message, Response}, requests::{ - Attach, Completions, Evaluate, Initialize, Launch, Request as _, RunInTerminal, - SetBreakpoints, SetExpression, SetVariable, StartDebugging, + Completions, Evaluate, Request as _, RunInTerminal, SetExpression, SetVariable, + StartDebugging, }, - AttachRequestArguments, Capabilities, CompletionItem, CompletionsArguments, ErrorResponse, - EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, InitializeRequestArguments, - InitializeRequestArgumentsPathFormat, LaunchRequestArguments, SetBreakpointsArguments, - SetExpressionArguments, SetVariableArguments, Source, SourceBreakpoint, - StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, + Capabilities, CompletionItem, CompletionsArguments, ErrorResponse, EvaluateArguments, + EvaluateArgumentsContext, EvaluateResponse, SetExpressionArguments, SetVariableArguments, + Source, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, }; -use dap_adapters::build_adapter; use fs::Fs; use futures::future::Shared; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; use http_client::HttpClient; -use language::{BinaryStatus, BufferSnapshot, LanguageRegistry, LanguageToolchainStore}; +use language::{BinaryStatus, LanguageRegistry, LanguageToolchainStore}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; use rpc::{ @@ -43,14 +36,13 @@ use rpc::{ AnyProtoClient, TypedEnvelope, }; use serde_json::Value; -use settings::{Settings as _, WorktreeId}; +use settings::WorktreeId; use smol::{lock::Mutex, stream::StreamExt}; use std::{ borrow::Borrow, collections::{BTreeMap, HashSet}, ffi::OsStr, - hash::Hash, - path::{Path, PathBuf}, + path::PathBuf, sync::{atomic::Ordering::SeqCst, Arc}, }; use std::{collections::VecDeque, sync::atomic::AtomicU32}; diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 335e332f8e45f2..57009b4f731da9 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1,6 +1,6 @@ use crate::project_settings::ProjectSettings; -use super::breakpoint_store::{self, BreakpointStore, BreakpointStoreEvent}; +use super::breakpoint_store::{BreakpointStore, BreakpointStoreEvent}; use super::dap_command::{ self, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, ModulesCommand, NextCommand, @@ -12,7 +12,7 @@ use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; use collections::{HashMap, IndexMap}; use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; -use dap::client::{DapMessageHandler, DebugAdapterClient, SessionId}; +use dap::client::{DebugAdapterClient, SessionId}; use dap::requests::Request; use dap::{ messages::{self, Events, Message}, @@ -22,10 +22,9 @@ use dap::{ }; use dap_adapters::build_adapter; use futures::channel::oneshot; -use futures::SinkExt; use futures::{future::join_all, future::Shared, FutureExt}; use gpui::{App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Task, WeakEntity}; -use rpc::{proto, AnyProtoClient}; +use rpc::AnyProtoClient; use serde_json::Value; use settings::Settings; use smol::stream::StreamExt; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9072571c2be56c..826d8e534cef27 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -40,7 +40,6 @@ use clock::ReplicaId; use dap::{ client::{DebugAdapterClient, SessionId}, - debugger_settings::DebuggerSettings, messages::Message, DebugAdapterConfig, }; @@ -48,14 +47,14 @@ use dap::{ use collections::{BTreeSet, HashMap, HashSet}; use debounced_delay::DebouncedDelay; use debugger::{ - breakpoint_store::{BreakpointStore, BreakpointStoreEvent, SerializedBreakpoint}, + breakpoint_store::BreakpointStore, dap_store::{DapStore, DapStoreEvent}, session::Session, }; pub use environment::ProjectEnvironment; use futures::{ channel::mpsc::{self, UnboundedReceiver}, - future::{join_all, try_join_all}, + future::try_join_all, StreamExt, }; pub use image_store::{ImageItem, ImageStore}; From c8632a332315166f7bb123f1df850af36ac37441 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 19 Feb 2025 15:53:01 +0100 Subject: [PATCH 623/650] Show stack frames when you a select thread --- crates/debugger_ui/src/session.rs | 11 +-- crates/debugger_ui/src/session/inert.rs | 4 +- crates/debugger_ui/src/session/running.rs | 90 ++++++++++++------- .../src/session/running/stack_frame_list.rs | 23 +++-- 4 files changed, 73 insertions(+), 55 deletions(-) diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 99aec3a21bc9c2..59845651e01e25 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -135,15 +135,8 @@ impl DebugSession { return; }; - let mode = cx.new(|cx| { - RunningState::new( - session.clone(), - ThreadId(1), - self.workspace.clone(), - window, - cx, - ) - }); + let mode = + cx.new(|cx| RunningState::new(session.clone(), self.workspace.clone(), window, cx)); self.mode = DebugSessionState::Running(mode); } diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 8b495d6a9e49a3..186e2b2e52c08d 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -78,14 +78,14 @@ impl Render for InertState { cx.emit(InertEvent::Spawned { config: DebugAdapterConfig { label: "hard coded".into(), - kind: dap::DebugAdapterKind::Javascript(task::TCPHost { + kind: dap::DebugAdapterKind::Python(task::TCPHost { port: None, host: None, timeout: None, }), request: DebugRequestType::Launch, program: Some( - "/Users/remcosmits/Documents/code/prettier-test/test.js".into(), + "/Users/remcosmits/Documents/code/prettier-test/test.py".into(), ), cwd: Some("/Users/remcosmits/Documents/code/prettier-test".into()), initialize_args: None, diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index bfb168fdac44d8..fcf83d69d868a2 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -15,17 +15,17 @@ use rpc::proto::ViewId; use settings::Settings; use stack_frame_list::{StackFrameList, StackFrameListEvent}; use ui::{ - div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Color, - Context, ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, - Indicator, InteractiveElement, IntoElement, Label, LabelCommon, ParentElement, Render, - SharedString, StatefulInteractiveElement, Styled, Tooltip, Window, + div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context, + ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, + Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString, + StatefulInteractiveElement, Styled, Tooltip, Window, }; use variable_list::VariableList; use workspace::{item::ItemEvent, Item, Workspace}; pub struct RunningState { session: Entity, - thread_id: ThreadId, + thread_id: Option, console: Entity, focus_handle: FocusHandle, remote_id: Option, @@ -238,8 +238,10 @@ impl Render for RunningState { this = this.entry(thread.name, None, move |_, cx| { state.update(cx, |state, cx| { - state.thread_id = ThreadId(thread_id); - cx.notify(); + state.select_thread( + ThreadId(thread_id), + cx, + ); }); }); } @@ -320,10 +322,8 @@ impl Render for RunningState { } impl RunningState { - #[allow(clippy::too_many_arguments)] pub fn new( session: Entity, - thread_id: ThreadId, workspace: WeakEntity, window: &mut Window, cx: &mut Context, @@ -331,7 +331,7 @@ impl RunningState { let focus_handle = cx.focus_handle(); let session_id = session.read(cx).session_id(); let stack_frame_list = - cx.new(|cx| StackFrameList::new(workspace.clone(), session.clone(), thread_id, cx)); + cx.new(|cx| StackFrameList::new(workspace.clone(), session.clone(), cx)); let variable_list = cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); @@ -363,13 +363,12 @@ impl RunningState { Self { session, console, - thread_id, _workspace: workspace, module_list, - focus_handle, variable_list, _subscriptions, + thread_id: None, remote_id: None, stack_frame_list, loaded_source_list, @@ -456,6 +455,16 @@ impl RunningState { self.session().read(cx).capabilities().clone() } + fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { + self.thread_id = Some(thread_id); + + self.stack_frame_list.update(cx, |stack_frame_list, cx| { + stack_frame_list.set_thread_id(self.thread_id, cx); + }); + + cx.notify(); + } + fn clear_highlights(&self, _cx: &mut Context) { // TODO(debugger): make this work again // if let Some((_, project_path, _)) = self.dap_store.read(cx).active_debug_line() { @@ -528,40 +537,60 @@ impl RunningState { } pub fn continue_thread(&mut self, cx: &mut Context) { + let Some(thread_id) = self.thread_id else { + return; + }; + self.session().update(cx, |state, cx| { - state.continue_thread(self.thread_id, cx); + state.continue_thread(thread_id, cx); }); } pub fn step_over(&mut self, cx: &mut Context) { + let Some(thread_id) = self.thread_id else { + return; + }; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.session().update(cx, |state, cx| { - state.step_over(self.thread_id, granularity, cx); + state.step_over(thread_id, granularity, cx); }); } pub fn step_in(&mut self, cx: &mut Context) { + let Some(thread_id) = self.thread_id else { + return; + }; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.session().update(cx, |state, cx| { - state.step_in(self.thread_id, granularity, cx); + state.step_in(thread_id, granularity, cx); }); } pub fn step_out(&mut self, cx: &mut Context) { + let Some(thread_id) = self.thread_id else { + return; + }; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.session().update(cx, |state, cx| { - state.step_out(self.thread_id, granularity, cx); + state.step_out(thread_id, granularity, cx); }); } pub fn step_back(&mut self, cx: &mut Context) { + let Some(thread_id) = self.thread_id else { + return; + }; + let granularity = DebuggerSettings::get_global(cx).stepping_granularity; self.session().update(cx, |state, cx| { - state.step_back(self.thread_id, granularity, cx); + state.step_back(thread_id, granularity, cx); }); } @@ -572,14 +601,22 @@ impl RunningState { } pub fn pause_thread(&self, cx: &mut Context) { + let Some(thread_id) = self.thread_id else { + return; + }; + self.session().update(cx, |state, cx| { - state.pause_thread(self.thread_id, cx); + state.pause_thread(thread_id, cx); }); } pub fn stop_thread(&self, cx: &mut Context) { + let Some(thread_id) = self.thread_id else { + return; + }; + self.session().update(cx, |state, cx| { - state.terminate_threads(Some(vec![self.thread_id; 1]), cx); + state.terminate_threads(Some(vec![thread_id; 1]), cx); }); } @@ -613,22 +650,11 @@ impl Item for RunningState { _window: &Window, cx: &App, ) -> AnyElement { - Label::new(format!("{} - Thread {}", todo!(), self.thread_id.0)) - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() + todo!() } fn tab_tooltip_text(&self, cx: &App) -> Option { - Some(SharedString::from(format!( - "{} Thread {} - {:?}", - todo!(), - self.thread_id.0, - todo!("thread state"), - ))) + todo!() } fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 3c1c8c6dc77a52..551f77883bb068 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -21,7 +21,7 @@ pub enum StackFrameListEvent { pub struct StackFrameList { list: ListState, - thread_id: ThreadId, + thread_id: Option, focus_handle: FocusHandle, _subscription: Subscription, session: Entity, @@ -41,7 +41,6 @@ impl StackFrameList { pub fn new( workspace: WeakEntity, session: Entity, - thread_id: ThreadId, cx: &mut Context, ) -> Self { let weak_entity = cx.weak_entity(); @@ -62,10 +61,6 @@ impl StackFrameList { ); let _subscription = cx.observe(&session, |stack_frame_list, state, cx| { - let _frame_len = state.update(cx, |state, cx| { - state.stack_frames(stack_frame_list.thread_id, cx).len() - }); - stack_frame_list.build_entries(cx); }); @@ -73,18 +68,18 @@ impl StackFrameList { list, session, workspace, - thread_id, - focus_handle, _subscription, + thread_id: None, entries: Default::default(), _fetch_stack_frames_task: None, current_stack_frame_id: Default::default(), } } - pub(crate) fn thread_id(&self) -> ThreadId { - self.thread_id + pub(crate) fn set_thread_id(&mut self, thread_id: Option, cx: &mut Context) { + self.thread_id = thread_id; + self.build_entries(cx); } #[cfg(any(test, feature = "test-support"))] @@ -93,8 +88,12 @@ impl StackFrameList { } pub fn stack_frames(&self, cx: &mut App) -> Vec { - self.session - .update(cx, |this, cx| this.stack_frames(self.thread_id, cx)) + self.thread_id + .map(|thread_id| { + self.session + .update(cx, |this, cx| this.stack_frames(thread_id, cx)) + }) + .unwrap_or_default() } #[cfg(any(test, feature = "test-support"))] From 26502cdd9e31af3a21d1731fc87c8caa33e6d073 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 19 Feb 2025 15:58:40 +0100 Subject: [PATCH 624/650] Read thread status from session Make exited the ultimate fallback so we disable all the UI buttons. --- crates/debugger_ui/src/session/running.rs | 6 +++++- crates/project/src/debugger/session.rs | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index fcf83d69d868a2..44a3a5dda6e2a8 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -42,7 +42,11 @@ pub struct RunningState { impl Render for RunningState { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let thread_status = ThreadStatus::Running; + let thread_status = self + .thread_id + .map(|thread_id| self.session.read(cx).thread_status(thread_id)) + .unwrap_or(ThreadStatus::Exited); + let active_thread_item = &self.active_thread_item; let threads = self.session.update(cx, |this, cx| this.threads(cx)); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 57009b4f731da9..b5a7ae579dbb2d 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -861,6 +861,10 @@ impl Session { cx.notify(); } + pub fn thread_status(&self, thread_id: ThreadId) -> ThreadStatus { + self.thread_states.thread_status(thread_id) + } + pub fn threads(&mut self, cx: &mut Context) -> Vec<(dap::Thread, ThreadStatus)> { self.fetch( dap_command::ThreadsCommand, From 55a39d5358963e4269da2655a869432031bbf4f2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:09:50 +0100 Subject: [PATCH 625/650] Use LocalDapCommand::is_supported for ConfigurationDone --- crates/project/src/debugger/dap_command.rs | 6 ++++++ crates/project/src/debugger/session.rs | 7 ++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 053049c350eb19..f6386c46f5f8ee 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -1609,6 +1609,12 @@ impl LocalDapCommand for ConfigurationDone { type Response = (); type DapRequest = dap::requests::ConfigurationDone; + fn is_supported(capabilities: &Capabilities) -> bool { + capabilities + .supports_configuration_done_request + .unwrap_or_default() + } + fn to_dap(&self) -> ::Arguments { dap::ConfigurationDoneArguments } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index b5a7ae579dbb2d..70d73a624d506d 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -268,7 +268,7 @@ impl LocalMode { let that = this.clone(); let breakpoints = breakpoints.update(&mut cx, |this, cx| this.all_breakpoints(true, cx))?; - + let caps = &capabilities; let configuration_sequence = async move { let _ = initialized_rx.await?; @@ -289,10 +289,7 @@ impl LocalMode { } let _ = futures::future::join_all(breakpoint_tasks).await; - if capabilities - .supports_configuration_done_request - .unwrap_or_default() - { + if ConfigurationDone::is_supported(caps) { that.request(ConfigurationDone, cx.background_executor().clone()) .await?; } From 0358cdc5620d286a471bfddd1a01446b6f55deea Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:39:51 +0100 Subject: [PATCH 626/650] Start sketching out config view Co-authored-by: Anthony Eid Co-authored-by: Remco Smits --- crates/debugger_ui/src/session.rs | 4 +- crates/debugger_ui/src/session/inert.rs | 93 +++++++++++++++++++++---- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 59845651e01e25..721881f3260243 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -72,10 +72,10 @@ impl DebugSession { pub(super) fn inert( project: Entity, workspace: WeakEntity, - window: &Window, + window: &mut Window, cx: &mut App, ) -> Entity { - let inert = cx.new(|cx| InertState::new(cx)); + let inert = cx.new(|cx| InertState::new(window, cx)); let project = project.read(cx); let dap_store = project.dap_store().downgrade(); diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 186e2b2e52c08d..6772c1c59e730b 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -1,20 +1,31 @@ -use dap::{DebugAdapterConfig, DebugRequestType}; -use gpui::{App, EventEmitter, FocusHandle, Focusable}; +use std::path::PathBuf; + +use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType}; +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle}; +use settings::Settings as _; +use task::TCPHost; +use theme::ThemeSettings; use ui::{ - h_flex, v_flex, Button, ButtonCommon, ButtonStyle, Clickable, Context, ContextMenu, - DropdownMenu, InteractiveElement, ParentElement, Render, SharedString, Styled, Window, + h_flex, relative, v_flex, ActiveTheme as _, Button, ButtonCommon, ButtonStyle, Clickable, + Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, Label, ParentElement, + Render, SharedString, Styled, Window, }; pub(super) struct InertState { focus_handle: FocusHandle, selected_debugger: Option, + program_editor: Entity, + cwd_editor: Entity, } impl InertState { - pub(super) fn new(cx: &mut Context) -> Self { + pub(super) fn new(window: &mut Window, cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle(), selected_debugger: None, + program_editor: cx.new(|cx| Editor::single_line(window, cx)), + cwd_editor: cx.new(|cx| Editor::single_line(window, cx)), } } } @@ -31,6 +42,7 @@ pub(crate) enum InertEvent { impl EventEmitter for InertState {} static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger"); + impl Render for InertState { fn render( &mut self, @@ -65,29 +77,44 @@ impl Render for InertState { this.entry("GDB", None, setter_for_name("GDB")) .entry("Delve", None, setter_for_name("Delve")) .entry("LLDB", None, setter_for_name("LLDB")) + .entry("PHP", None, setter_for_name("PHP")) + .entry("Debugpy", None, setter_for_name("Debugpy")) }), )), ) + .child( + v_flex() + .child( + h_flex() + .w_4_5() + .gap_2() + .child(Label::new("Program path")) + .child(Self::render_editor(&self.program_editor, cx)), + ) + .child( + h_flex() + .gap_2() + .child(Label::new("Working directory")) + .child(Self::render_editor(&self.cwd_editor, cx)), + ), + ) .child( h_flex() .gap_1() .child( Button::new("launch-dap", "Launch") .style(ButtonStyle::Filled) - .on_click(cx.listener(|_, _, _, cx| { + .on_click(cx.listener(|this, _, _, cx| { + let program = this.program_editor.read(cx).text(cx); + let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx)); + let kind = kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| unimplemented!("Automatic selection of a debugger based on users project"))); cx.emit(InertEvent::Spawned { config: DebugAdapterConfig { label: "hard coded".into(), - kind: dap::DebugAdapterKind::Python(task::TCPHost { - port: None, - host: None, - timeout: None, - }), + kind, request: DebugRequestType::Launch, - program: Some( - "/Users/remcosmits/Documents/code/prettier-test/test.py".into(), - ), - cwd: Some("/Users/remcosmits/Documents/code/prettier-test".into()), + program: Some(program), + cwd: Some(cwd), initialize_args: None, supports_attach: false, }, @@ -98,3 +125,39 @@ impl Render for InertState { ) } } + +fn kind_for_label(label: &str) -> DebugAdapterKind { + match label { + "LLDB" => DebugAdapterKind::Lldb, + "Debugpy" => DebugAdapterKind::Python(TCPHost::default()), + "PHP" => DebugAdapterKind::Php(TCPHost::default()), + "Delve" => DebugAdapterKind::Go(TCPHost::default()), + _ => { + unimplemented!() + } // Maybe we should set a toast notification here + } +} +impl InertState { + fn render_editor(editor: &Entity, cx: &Context) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: cx.theme().colors().text, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_size: settings.buffer_font_size.into(), + font_weight: settings.buffer_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }; + + EditorElement::new( + editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } +} From 9815a8f4b60ffb9f46b48709031a37882af7bd9e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 19 Feb 2025 11:54:51 -0500 Subject: [PATCH 627/650] Clean up errors and only send modules requests when thread is stopped --- crates/dap/src/adapters.rs | 2 - crates/dap/src/client.rs | 4 +- crates/dap/src/transport.rs | 6 +-- crates/project/src/debugger/session.rs | 52 ++++++++++++-------------- crates/project/src/project.rs | 8 +--- 5 files changed, 29 insertions(+), 43 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index a2f1304e254465..40b076b2919a73 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -1,5 +1,3 @@ -#[cfg(any(test, feature = "test-support"))] -use crate::transport::FakeTransport; use ::fs::Fs; use anyhow::{anyhow, Context as _, Ok, Result}; use async_compression::futures::bufread::GzipDecoder; diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 29c18711e7a641..5aad4935e90a0c 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -258,9 +258,7 @@ impl DebugAdapterClient { #[cfg(test)] mod tests { use super::*; - use crate::{ - adapters::FakeAdapter, client::DebugAdapterClient, debugger_settings::DebuggerSettings, - }; + use crate::{client::DebugAdapterClient, debugger_settings::DebuggerSettings}; use dap_types::{ messages::Events, requests::{Initialize, Request, RunInTerminal}, diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 3200491757d187..162ce2ed462271 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -130,16 +130,16 @@ pub(crate) struct TransportDelegate { impl Transport { #[cfg(any(test, feature = "test-support"))] - fn fake(args: DebugAdapterBinary) -> Self { + fn _fake(_args: DebugAdapterBinary) -> Self { todo!() } } impl TransportDelegate { #[cfg(any(test, feature = "test-support"))] - pub fn fake(args: DebugAdapterBinary) -> Self { + pub fn _fake(args: DebugAdapterBinary) -> Self { Self { - transport: Transport::fake(args), + transport: Transport::_fake(args), server_tx: Default::default(), log_handlers: Default::default(), current_requests: Default::default(), diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 70d73a624d506d..194e0dea159f9f 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -389,6 +389,15 @@ impl ThreadStates { .copied() .or(self.global_state) } + + fn any_thread_running(&self) -> bool { + self.global_state + .is_some_and(|state| state == ThreadStatus::Running) + || self + .known_thread_states + .values() + .any(|status| *status == ThreadStatus::Running) + } } /// Represents a current state of a single debug adapter and provides ways to mutate it. @@ -567,8 +576,8 @@ impl Session { modules: Vec::default(), loaded_sources: Vec::default(), threads: IndexMap::default(), - config: todo!(), _background_tasks: Vec::default(), + config: todo!(), } } @@ -749,27 +758,9 @@ impl Session { } } - fn handle_start_debugging_request(&mut self, request: messages::Request) {} - - fn handle_run_in_terminal_request(&mut self, request: messages::Request) {} + fn _handle_start_debugging_request(&mut self, _request: messages::Request) {} - pub(crate) fn handle_dap_message(&mut self, message: Message, cx: &mut Context) { - match message { - Message::Event(event) => { - self.handle_dap_event(event, cx); - } - Message::Request(request) => { - if StartDebugging::COMMAND == request.command { - self.handle_start_debugging_request(request); - } else if RunInTerminal::COMMAND == request.command { - self.handle_run_in_terminal_request(request); - } else { - debug_assert!(false, "Encountered unexpected command type"); - } - } - _ => unreachable!(), - } - } + fn _handle_run_in_terminal_request(&mut self, _request: messages::Request) {} pub(crate) fn _wait_for_request( &self, @@ -887,14 +878,17 @@ impl Session { } pub fn modules(&mut self, cx: &mut Context) -> &[Module] { - self.fetch( - dap_command::ModulesCommand, - |this, result, cx| { - this.modules = result.clone(); - cx.notify(); - }, - cx, - ); + if self.thread_states.any_thread_running() { + self.fetch( + dap_command::ModulesCommand, + |this, result, cx| { + this.modules = result.clone(); + cx.notify(); + }, + cx, + ); + } + &self.modules } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 826d8e534cef27..3d7cc67d6d19ac 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1090,12 +1090,8 @@ impl Project { bp_store })?; - let dap_store = cx.new(|cx| { - let mut dap_store = - DapStore::new_remote(remote_id, client.clone().into(), breakpoint_store.clone()); - - unimplemented!("dap_store.request_active_debug_sessions(cx)"); - dap_store + let dap_store = cx.new(|_cx| { + DapStore::new_remote(remote_id, client.clone().into(), breakpoint_store.clone()) })?; let lsp_store = cx.new(|cx| { From 6e17f91bb63bdee7efc43447eeb386d89fd14991 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:58:49 +0100 Subject: [PATCH 628/650] send out breakpoints in the initial batch Co-authored-by: Remco < --- crates/debugger_ui/src/session/running.rs | 3 ++- crates/debugger_ui/src/session/starting.rs | 2 +- crates/project/src/debugger/session.rs | 15 ++++++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 44a3a5dda6e2a8..66c92bfc2bb7a0 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -46,7 +46,7 @@ impl Render for RunningState { .thread_id .map(|thread_id| self.session.read(cx).thread_status(thread_id)) .unwrap_or(ThreadStatus::Exited); - + let is_terminated = self.session.read(cx).is_terminated(); let active_thread_item = &self.active_thread_item; let threads = self.session.update(cx, |this, cx| this.threads(cx)); @@ -54,6 +54,7 @@ impl Render for RunningState { let capabilities = self.capabilities(cx); let state = cx.entity(); h_flex() + .when(is_terminated, |this| this.bg(gpui::red())) .key_context("DebugPanelItem") .track_focus(&self.focus_handle(cx)) .size_full() diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs index cf3ff928c46216..c00d920e030347 100644 --- a/crates/debugger_ui/src/session/starting.rs +++ b/crates/debugger_ui/src/session/starting.rs @@ -35,7 +35,7 @@ impl StartingState { } impl Focusable for StartingState { - fn focus_handle(&self, cx: &ui::App) -> FocusHandle { + fn focus_handle(&self, _: &ui::App) -> FocusHandle { self.focus_handle.clone() } } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 194e0dea159f9f..4ee72f0104e894 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -273,6 +273,7 @@ impl LocalMode { let _ = initialized_rx.await?; let mut breakpoint_tasks = Vec::new(); + for (path, breakpoints) in breakpoints { breakpoint_tasks.push( that.request( @@ -381,8 +382,6 @@ impl ThreadStates { .unwrap_or(ThreadStatus::Running) } - // stopped event (all_threads: true) - // thread event (reason: started) <- true state is running, but we'll think that it's stopped fn thread_state(&self, thread_id: ThreadId) -> Option { self.known_thread_states .get(&thread_id) @@ -413,6 +412,7 @@ pub struct Session { threads: IndexMap, requests: HashMap>>>, thread_states: ThreadStates, + is_session_terminated: bool, _background_tasks: Vec>, } @@ -550,6 +550,7 @@ impl Session { loaded_sources: Vec::default(), threads: IndexMap::default(), _background_tasks, + is_session_terminated: false, } }) }) @@ -578,6 +579,7 @@ impl Session { threads: IndexMap::default(), _background_tasks: Vec::default(), config: todo!(), + is_session_terminated: false, } } @@ -588,10 +590,15 @@ impl Session { pub fn capabilities(&self) -> &Capabilities { &self.capabilities } + pub fn configuration(&self) -> DebugAdapterConfig { self.config.clone() } + pub fn is_terminated(&self) -> bool { + self.is_session_terminated + } + fn send_changed_breakpoints( &mut self, _breakpoint_store: Entity, @@ -732,7 +739,9 @@ impl Session { self.invalidate(cx); } Events::Exited(_event) => {} - Events::Terminated(_event) => {} + Events::Terminated(_) => { + self.is_session_terminated = true; + } Events::Thread(event) => { match event.reason { dap::ThreadEventReason::Started => { From 9bac1ff9a28201f2202ec49c6a6d7f631904320f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 19 Feb 2025 18:13:59 +0100 Subject: [PATCH 629/650] Display selected thread & auto-select first thread if we don't have a selected thread --- crates/debugger_ui/src/session/running.rs | 44 ++++++++++++++--------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 66c92bfc2bb7a0..07e7e094b7fcfd 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -25,7 +25,7 @@ use workspace::{item::ItemEvent, Item, Workspace}; pub struct RunningState { session: Entity, - thread_id: Option, + thread: Option<(ThreadId, String)>, console: Entity, focus_handle: FocusHandle, remote_id: Option, @@ -42,14 +42,19 @@ pub struct RunningState { impl Render for RunningState { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let threads = self.session.update(cx, |this, cx| this.threads(cx)); + if let Some((thread, _)) = threads.first().filter(|_| self.thread.is_none()) { + self.select_thread(ThreadId(thread.id), thread.name.clone(), cx); + } + let thread_status = self - .thread_id - .map(|thread_id| self.session.read(cx).thread_status(thread_id)) + .thread + .as_ref() + .map(|(thread_id, _)| self.session.read(cx).thread_status(*thread_id)) .unwrap_or(ThreadStatus::Exited); let is_terminated = self.session.read(cx).is_terminated(); let active_thread_item = &self.active_thread_item; - let threads = self.session.update(cx, |this, cx| this.threads(cx)); let has_no_threads = threads.is_empty(); let capabilities = self.capabilities(cx); let state = cx.entity(); @@ -235,16 +240,21 @@ impl Render for RunningState { h_flex().p_1().mx_2().w_3_4().justify_end().child( DropdownMenu::new( "thread-list", - "Threads", + self.thread + .as_ref() + .map(|(_, name)| format!("Thread {name}")) + .unwrap_or_else(|| "Threads".into()), ContextMenu::build(window, cx, move |mut this, _, _| { - for (thread, status) in threads { + for (thread, _) in threads { let state = state.clone(); let thread_id = thread.id; + let thread_name = SharedString::from(&thread.name); this = this.entry(thread.name, None, move |_, cx| { state.update(cx, |state, cx| { state.select_thread( ThreadId(thread_id), + String::from(thread_name.as_ref()), cx, ); }); @@ -373,7 +383,7 @@ impl RunningState { focus_handle, variable_list, _subscriptions, - thread_id: None, + thread: None, remote_id: None, stack_frame_list, loaded_source_list, @@ -460,11 +470,11 @@ impl RunningState { self.session().read(cx).capabilities().clone() } - fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { - self.thread_id = Some(thread_id); + fn select_thread(&mut self, thread_id: ThreadId, thread_name: String, cx: &mut Context) { + self.thread = Some((thread_id, thread_name)); self.stack_frame_list.update(cx, |stack_frame_list, cx| { - stack_frame_list.set_thread_id(self.thread_id, cx); + stack_frame_list.set_thread_id(self.thread.as_ref().map(|id| id.0), cx); }); cx.notify(); @@ -542,7 +552,7 @@ impl RunningState { } pub fn continue_thread(&mut self, cx: &mut Context) { - let Some(thread_id) = self.thread_id else { + let Some((thread_id, _)) = self.thread else { return; }; @@ -552,7 +562,7 @@ impl RunningState { } pub fn step_over(&mut self, cx: &mut Context) { - let Some(thread_id) = self.thread_id else { + let Some((thread_id, _)) = self.thread else { return; }; @@ -564,7 +574,7 @@ impl RunningState { } pub fn step_in(&mut self, cx: &mut Context) { - let Some(thread_id) = self.thread_id else { + let Some((thread_id, _)) = self.thread else { return; }; @@ -576,7 +586,7 @@ impl RunningState { } pub fn step_out(&mut self, cx: &mut Context) { - let Some(thread_id) = self.thread_id else { + let Some((thread_id, _)) = self.thread else { return; }; @@ -588,7 +598,7 @@ impl RunningState { } pub fn step_back(&mut self, cx: &mut Context) { - let Some(thread_id) = self.thread_id else { + let Some((thread_id, _)) = self.thread else { return; }; @@ -606,7 +616,7 @@ impl RunningState { } pub fn pause_thread(&self, cx: &mut Context) { - let Some(thread_id) = self.thread_id else { + let Some((thread_id, _)) = self.thread else { return; }; @@ -616,7 +626,7 @@ impl RunningState { } pub fn stop_thread(&self, cx: &mut Context) { - let Some(thread_id) = self.thread_id else { + let Some((thread_id, _)) = self.thread else { return; }; From eeded3d0a4d55c07aeeefef321543dc26c7c69fd Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 19 Feb 2025 14:07:13 -0500 Subject: [PATCH 630/650] Allow starting session from action and add new session as item to debug panel --- Cargo.lock | 1 + crates/debugger_tools/src/dap_log.rs | 2 +- crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/debugger_panel.rs | 36 +++++++++++++++++++++++- crates/debugger_ui/src/session.rs | 26 +++++++++++++++-- 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9dc428614b1563..5de41bd245e1bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3845,6 +3845,7 @@ dependencies = [ "fuzzy", "gpui", "language", + "log", "menu", "picker", "pretty_assertions", diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index e2728f784981e0..13c6520c04ecb4 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -563,7 +563,7 @@ impl DapLogView { let client = client.read(cx).adapter_client()?; Some(DapMenuItem { client_id: client.id(), - client_name: unimplemented!(), + client_name: "debygpy (hard coded)".into(), // todo(debugger) Fix this hard coded has_adapter_logs: client.has_adapter_logs(), selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind), }) diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 076af6e56485c2..ad4a013d7a6249 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -28,6 +28,7 @@ editor.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true +log.workspace = true menu.workspace = true picker.workspace = true pretty_assertions.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b0c0ca809caca5..be7eced67f3e65 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -9,7 +9,10 @@ use gpui::{ actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::Project; +use project::{ + debugger::dap_store::{self, DapStore}, + Project, +}; use rpc::proto::{self}; use settings::Settings; use std::any::TypeId; @@ -54,6 +57,7 @@ impl DebugPanel { ) -> Entity { cx.new(|cx| { let project = workspace.project().clone(); + let dap_store = project.read(cx).dap_store(); let weak_workspace = workspace.weak_handle(); let pane = cx.new(|cx| { let mut pane = Pane::new( @@ -123,6 +127,7 @@ impl DebugPanel { let _subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe_in(&pane, window, Self::handle_pane_event), + cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event), ]; let debug_panel = Self { @@ -237,6 +242,35 @@ impl DebugPanel { }) } + fn handle_dap_store_event( + &mut self, + dap_store: &Entity, + event: &dap_store::DapStoreEvent, + window: &mut Window, + cx: &mut Context, + ) { + match event { + dap_store::DapStoreEvent::DebugClientStarted(session_id) => { + let Some(session) = dap_store.read(cx).session_by_id(session_id) else { + return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event"); + }; + + let Some(project) = self.project.upgrade() else { + return log::error!("Debug Panel out lived it's weak reference to Project"); + }; + + let session_item = + DebugSession::running(project, self.workspace.clone(), session, window, cx); + + self.pane.update(cx, |pane, cx| { + pane.add_item(Box::new(session_item), true, true, None, window, cx); + window.focus(&pane.focus_handle(cx)); + }); + } + _ => {} + } + } + fn handle_pane_event( &mut self, _: &Entity, diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 721881f3260243..a1065463c2cb80 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -7,8 +7,7 @@ use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; use inert::{InertEvent, InertState}; -use project::debugger::dap_store::DapStore; -use project::debugger::session::ThreadId; +use project::debugger::{self, dap_store::DapStore, session::Session}; use project::worktree_store::WorktreeStore; use project::Project; use rpc::proto::{self, PeerId}; @@ -92,6 +91,28 @@ impl DebugSession { } }) } + + pub(crate) fn running( + project: Entity, + workspace: WeakEntity, + session: Entity, + window: &mut Window, + cx: &mut App, + ) -> Entity { + let mode = DebugSessionState::Running( + cx.new(|cx| RunningState::new(session.clone(), workspace.clone(), window, cx)), + ); + + cx.new(|cx| Self { + remote_id: None, + mode, + dap_store: project.read(cx).dap_store().downgrade(), + worktree_store: project.read(cx).worktree_store().downgrade(), + workspace, + _subscriptions: [cx.subscribe(&project, |_, _, _, _| {})], // todo(debugger) We don't need this subscription + }) + } + pub(crate) fn session_id(&self, cx: &App) -> Option { match &self.mode { DebugSessionState::Inert(_) => None, @@ -124,6 +145,7 @@ impl DebugSession { self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)]; self.mode = DebugSessionState::Starting(starting); } + fn on_starting_event( &mut self, _: &Entity, From 3378b7a8590d846d8c28ab28ff66908e480af186 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 19 Feb 2025 14:56:10 -0500 Subject: [PATCH 631/650] Get module list to render correctly --- crates/debugger_ui/src/debugger_panel.rs | 1 + crates/project/src/debugger/session.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index be7eced67f3e65..ae6ed3f45fae28 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -265,6 +265,7 @@ impl DebugPanel { self.pane.update(cx, |pane, cx| { pane.add_item(Box::new(session_item), true, true, None, window, cx); window.focus(&pane.focus_handle(cx)); + cx.notify(); }); } _ => {} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 4ee72f0104e894..2b83d3b792cbe4 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -705,10 +705,6 @@ impl Session { } fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context) { - // todo(debugger): We should see if we could only invalidate the thread that stopped - // instead of everything right now - self.invalidate(cx); - // todo(debugger): We should query for all threads here if we don't get a thread id // maybe in both cases too? if event.all_threads_stopped.unwrap_or_default() { @@ -718,6 +714,10 @@ impl Session { } else { // TODO(debugger): all threads should be stopped } + + // todo(debugger): We should see if we could only invalidate the thread that stopped + // instead of everything right now. + self.invalidate(cx); } pub(crate) fn handle_dap_event(&mut self, event: Box, cx: &mut Context) { @@ -851,6 +851,7 @@ impl Session { self.requests.remove(&key); } + /// This function should be called after changing state not before fn invalidate(&mut self, cx: &mut Context) { self.requests.clear(); self.modules.clear(); @@ -887,7 +888,7 @@ impl Session { } pub fn modules(&mut self, cx: &mut Context) -> &[Module] { - if self.thread_states.any_thread_running() { + if !self.thread_states.any_thread_running() { self.fetch( dap_command::ModulesCommand, |this, result, cx| { From 597371960f63aa903f93ca1383d42b7caa096ed2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 20 Feb 2025 01:07:33 +0100 Subject: [PATCH 632/650] Invalidate all stack frames on thread event --- crates/project/src/debugger/session.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 2b83d3b792cbe4..0538de36c2e32c 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -5,8 +5,8 @@ use super::dap_command::{ self, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand, EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, SetVariableValueCommand, - StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, - TerminateThreadsCommand, ThreadsCommand, VariablesCommand, + StackTraceCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, + TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand, }; use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; @@ -50,7 +50,7 @@ impl ThreadId { pub const MAX: ThreadId = ThreadId(u64::MAX); } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Variable { _dap: dap::Variable, _variables: Vec, @@ -65,7 +65,7 @@ impl From for Variable { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Scope { pub dap: dap::Scope, pub variables: Vec, @@ -80,7 +80,7 @@ impl From for Scope { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct StackFrame { pub dap: dap::StackFrame, pub scopes: Vec, @@ -104,6 +104,7 @@ pub enum ThreadStatus { Ended, } +#[derive(Debug)] pub struct Thread { dap: dap::Thread, stack_frames: Vec, @@ -867,6 +868,17 @@ impl Session { self.fetch( dap_command::ThreadsCommand, |this, result, cx| { + let v = this.threads.keys().copied().collect::>(); + for thread_id in v { + this.invalidate_state( + &StackTraceCommand { + thread_id: thread_id.0, + start_frame: None, + levels: None, + } + .into(), + ); + } this.threads.extend( result .iter() From 58640da34bfdbbc5c322d084e7abd69f36bc1f71 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 20 Feb 2025 01:21:15 +0100 Subject: [PATCH 633/650] Annotate cacheable requests on LocalDapCommand level --- crates/project/src/debugger/dap_command.rs | 9 +++++++++ crates/project/src/debugger/session.rs | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index f6386c46f5f8ee..fba9414c56d5dc 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -17,6 +17,7 @@ use util::ResultExt; pub(crate) trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; + const CACHEABLE: bool = false; fn is_supported(_capabilities: &Capabilities) -> bool { true @@ -843,6 +844,7 @@ pub struct VariablesCommand { impl LocalDapCommand for VariablesCommand { type Response = Vec; type DapRequest = dap::requests::Variables; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::VariablesArguments { @@ -1069,6 +1071,7 @@ pub(crate) struct ModulesCommand; impl LocalDapCommand for ModulesCommand { type Response = Vec; type DapRequest = dap::requests::Modules; + const CACHEABLE: bool = true; fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_modules_request.unwrap_or_default() @@ -1142,6 +1145,8 @@ pub(crate) struct LoadedSourcesCommand; impl LocalDapCommand for LoadedSourcesCommand { type Response = Vec; type DapRequest = dap::requests::LoadedSources; + const CACHEABLE: bool = true; + fn is_supported(capabilities: &Capabilities) -> bool { capabilities .supports_loaded_sources_request @@ -1216,6 +1221,7 @@ pub(crate) struct StackTraceCommand { impl LocalDapCommand for StackTraceCommand { type Response = Vec; type DapRequest = dap::requests::StackTrace; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::StackTraceArguments { @@ -1289,6 +1295,7 @@ pub(crate) struct ScopesCommand { impl LocalDapCommand for ScopesCommand { type Response = Vec; type DapRequest = dap::requests::Scopes; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::ScopesArguments { @@ -1347,6 +1354,7 @@ impl DapCommand for ScopesCommand { impl LocalDapCommand for super::session::CompletionsQuery { type Response = dap::CompletionsResponse; type DapRequest = dap::requests::Completions; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::CompletionsArguments { @@ -1512,6 +1520,7 @@ pub(crate) struct ThreadsCommand; impl LocalDapCommand for ThreadsCommand { type Response = Vec; type DapRequest = dap::requests::Threads; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { () diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 0538de36c2e32c..43897dbb6fe628 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -787,6 +787,12 @@ impl Session { process_result: impl FnOnce(&mut Self, &T::Response, &mut Context) + 'static, cx: &mut Context, ) { + const { + assert!( + T::CACHEABLE, + "Only requests marked as cacheable should invoke `fetch`" + ); + } if let Entry::Vacant(vacant) = self.requests.entry(request.into()) { let command = vacant.key().0.clone().as_any_arc().downcast::().unwrap(); From 6375d34f50b88c51654f973fefbd10547e29b749 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 20 Feb 2025 00:43:38 -0500 Subject: [PATCH 634/650] Start work on enabling getting active debug line to show in editor --- .../src/session/running/stack_frame_list.rs | 27 +++++++++++++------ crates/project/src/debugger/dap_store.rs | 4 +-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 551f77883bb068..a1af6fedb2769f 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -206,7 +206,7 @@ impl StackFrameList { return Task::ready(Ok(())); }; - let _row = (stack_frame.line.saturating_sub(1)) as u32; + let row = (stack_frame.line.saturating_sub(1)) as u32; let Some(project_path) = self.project_path_from_stack_frame(&stack_frame, cx) else { return Task::ready(Err(anyhow!("Project path not found"))); @@ -229,14 +229,25 @@ impl StackFrameList { })?? .await?; - Ok(()) - // TODO(debugger): make this work again - // this.update(&mut cx, |this, cx| { - // this.dap_store.update(cx, |store, cx| { - // store.set_active_debug_line(client_id, &project_path, row, cx); - // }) - // }) + this.update(&mut cx, |this, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .dap_store() + .update(cx, |store, cx| { + store.set_active_debug_line( + this.session.read(cx).session_id(), + &project_path, + row, + cx, + ); + }); + }); + }); + + Ok(()) } }) } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 6ebc5a822dfead..0c1062154e8276 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -428,12 +428,12 @@ impl DapStore { pub fn set_active_debug_line( &mut self, - session_id: &SessionId, + session_id: SessionId, project_path: &ProjectPath, row: u32, cx: &mut Context, ) { - self.active_debug_line = Some((*session_id, project_path.clone(), row)); + self.active_debug_line = Some((session_id, project_path.clone(), row)); cx.emit(DapStoreEvent::ActiveDebugLineChanged); cx.notify(); } From d327e6015d136674fc76e558d16e401839af02a9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:35:53 +0100 Subject: [PATCH 635/650] Remove unused import --- crates/editor/src/editor.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e1ee8de90a0f41..c375ccbf801175 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -82,12 +82,12 @@ use git::blame::GitBlame; use gpui::{ div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds, - ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, Entity, - EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, - Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton, - MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled, - StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, - UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, + ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Entity, EntityInputHandler, + EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, + HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, + ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task, + TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, + WeakEntity, WeakFocusHandle, Window, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; From 0c7cd6f848ca32383db4e27626f05e50558afe1b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:20:43 +0100 Subject: [PATCH 636/650] Make one-based rows explicit by use of NonZeroU32 --- crates/editor/src/editor.rs | 15 ++-- crates/editor/src/element.rs | 28 +++++-- .../project/src/debugger/breakpoint_store.rs | 84 ++++++------------- crates/workspace/src/persistence.rs | 11 ++- 4 files changed, 62 insertions(+), 76 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c375ccbf801175..4145328ae44675 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5672,7 +5672,7 @@ impl Editor { if let Some(project_path) = buffer.project_path(cx) { if let Some(breakpoints) = breakpoints.get(&project_path) { for breakpoint in breakpoints { - let point = breakpoint.point_for_buffer(&buffer); + let point = breakpoint.point_for_buffer(&buffer.text_snapshot()); breakpoint_display_points .insert(point.to_display_point(&snapshot).row(), breakpoint.clone()); @@ -7565,11 +7565,14 @@ impl Editor { }; let Some(cache_position) = self.buffer.read_with(cx, |buffer, cx| { - buffer.buffer(buffer_id).map(|buffer| { - buffer - .read(cx) - .summary_for_anchor::(&breakpoint_position) - .row + buffer.buffer(buffer_id).and_then(|buffer| { + NonZeroU32::new( + buffer + .read(cx) + .summary_for_anchor::(&breakpoint_position) + .row + + 1, + ) }) }) else { return; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f41e95f27fe431..d248222f73b337 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -64,6 +64,7 @@ use std::{ cmp::{self, Ordering}, fmt::{self, Write}, iter, mem, + num::NonZeroU32, ops::{Deref, Range}, rc::Rc, sync::Arc, @@ -7116,14 +7117,25 @@ impl Element for EditorElement { if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator { breakpoint_rows .entry(gutter_breakpoint_point.row()) - .or_insert(Breakpoint { - active_position: Some( - snapshot - .display_point_to_breakpoint_anchor(gutter_breakpoint_point) - .text_anchor, - ), - cached_position: 0, - kind: BreakpointKind::Standard, + .or_insert_with(|| { + let position = snapshot + .display_point_to_breakpoint_anchor(gutter_breakpoint_point); + let mut breakpoint = Breakpoint { + active_position: Some(position.text_anchor), + cached_position: NonZeroU32::new(u32::MAX).unwrap(), + kind: BreakpointKind::Standard, + }; + let buffer = snapshot + .buffer_snapshot + .buffer_for_excerpt(position.excerpt_id); + if let Some(buffer) = buffer { + breakpoint.cached_position = NonZeroU32::new( + breakpoint.point_for_buffer(buffer).row + 1, + ) + .unwrap(); + } + + breakpoint }); } diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 39edab41cb60c0..188523fd3b5559 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -15,6 +15,7 @@ use settings::Settings; use settings::WorktreeId; use std::{ hash::{Hash, Hasher}, + num::NonZeroU32, path::Path, sync::Arc, }; @@ -285,10 +286,14 @@ impl BreakpointStore { }; if let Some(breakpoint_set) = self.breakpoints.remove(&project_path) { - let breakpoint_iter = breakpoint_set.into_iter().map(|mut breakpoint| { - breakpoint.cached_position = breakpoint.point_for_buffer(buffer.read(cx)).row; + let breakpoint_iter = breakpoint_set.into_iter().filter_map(|mut breakpoint| { + let position = NonZeroU32::new( + breakpoint.point_for_buffer(&buffer.read(cx).snapshot()).row + 1, + ); + debug_assert!(position.is_some()); + breakpoint.cached_position = position?; breakpoint.active_position = None; - breakpoint + Some(breakpoint) }); self.breakpoints.insert( @@ -573,7 +578,7 @@ impl Hash for BreakpointKind { #[derive(Clone, Debug)] pub struct Breakpoint { pub active_position: Option, - pub cached_position: u32, + pub cached_position: NonZeroU32, pub kind: BreakpointKind, } @@ -605,68 +610,23 @@ impl Hash for Breakpoint { } impl Breakpoint { - pub fn to_source_breakpoint(&self, buffer: &Buffer) -> SourceBreakpoint { - let line = self - .active_position - .map(|position| buffer.summary_for_anchor::(&position).row) - .unwrap_or(self.cached_position) as u64; - - let log_message = match &self.kind { - BreakpointKind::Standard => None, - BreakpointKind::Log(message) => Some(message.clone().to_string()), - }; - - SourceBreakpoint { - line, - condition: None, - hit_condition: None, - log_message, - column: None, - mode: None, - } - } - pub fn set_active_position(&mut self, buffer: &Buffer) { if self.active_position.is_none() { self.active_position = - Some(buffer.breakpoint_anchor(Point::new(self.cached_position, 0))); + Some(buffer.breakpoint_anchor(Point::new(self.cached_position.get() - 1, 0))); } } - pub fn point_for_buffer(&self, buffer: &Buffer) -> Point { + pub fn point_for_buffer(&self, buffer: &text::BufferSnapshot) -> Point { self.active_position .map(|position| buffer.summary_for_anchor::(&position)) - .unwrap_or(Point::new(self.cached_position, 0)) + .unwrap_or(Point::new(self.cached_position.get() - 1, 0)) } pub fn point_for_buffer_snapshot(&self, buffer_snapshot: &BufferSnapshot) -> Point { self.active_position .map(|position| buffer_snapshot.summary_for_anchor::(&position)) - .unwrap_or(Point::new(self.cached_position, 0)) - } - - pub fn source_for_snapshot(&self, snapshot: Option<&BufferSnapshot>) -> SourceBreakpoint { - let line = match snapshot { - Some(snapshot) => self - .active_position - .map(|position| snapshot.summary_for_anchor::(&position).row) - .unwrap_or(self.cached_position) as u64, - None => self.cached_position as u64, - }; - - let log_message = match &self.kind { - BreakpointKind::Standard => None, - BreakpointKind::Log(log_message) => Some(log_message.clone().to_string()), - }; - - SourceBreakpoint { - line, - condition: None, - hit_condition: None, - log_message, - column: None, - mode: None, - } + .unwrap_or(Point::new(self.cached_position.get() - 1, 0)) } pub fn to_serialized(&self, buffer: Option<&Buffer>, path: Arc) -> SerializedBreakpoint { @@ -674,7 +634,15 @@ impl Breakpoint { Some(buffer) => SerializedBreakpoint { position: self .active_position - .map(|position| buffer.summary_for_anchor::(&position).row) + .and_then(|position| { + let ret = + NonZeroU32::new(buffer.summary_for_anchor::(&position).row + 1); + debug_assert!( + ret.is_some(), + "Serializing breakpoint close to u32::MAX position failed" + ); + ret + }) .unwrap_or(self.cached_position), path, kind: self.kind.clone(), @@ -694,7 +662,7 @@ impl Breakpoint { } else { None }, - cached_position: self.cached_position, + cached_position: self.cached_position.get(), kind: match self.kind { BreakpointKind::Standard => proto::BreakpointKind::Standard.into(), BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(), @@ -714,7 +682,7 @@ impl Breakpoint { } else { None }, - cached_position: breakpoint.cached_position, + cached_position: NonZeroU32::new(breakpoint.cached_position)?, kind: match proto::BreakpointKind::from_i32(breakpoint.kind) { Some(proto::BreakpointKind::Log) => { BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into()) @@ -727,7 +695,7 @@ impl Breakpoint { #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct SerializedBreakpoint { - pub position: u32, + pub position: NonZeroU32, pub path: Arc, pub kind: BreakpointKind, } @@ -740,7 +708,7 @@ impl SerializedBreakpoint { }; SourceBreakpoint { - line: self.position as u64, + line: self.position.get() as u64, condition: None, hit_condition: None, log_message, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 575ff40beb6736..52f9ac948e0805 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -2,6 +2,7 @@ pub mod model; use std::{ borrow::Cow, + num::NonZeroU32, path::{Path, PathBuf}, str::FromStr, sync::Arc, @@ -145,7 +146,7 @@ impl Column for SerializedWindowBounds { #[derive(Debug)] pub struct Breakpoint { - pub position: u32, + pub position: NonZeroU32, pub kind: BreakpointKind, } @@ -208,7 +209,7 @@ impl sqlez::bindable::Bind for Breakpoint { statement: &sqlez::statement::Statement, start_index: i32, ) -> anyhow::Result { - let next_index = statement.bind(&self.position, start_index)?; + let next_index = statement.bind(&self.position.get(), start_index)?; statement.bind( &BreakpointKindWrapper(Cow::Borrowed(&self.kind)), next_index, @@ -222,7 +223,8 @@ impl Column for Breakpoint { .column_int(start_index) .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? as u32; - + let position = + NonZeroU32::new(position).ok_or_else(|| anyhow!("Position must be non-zero"))?; let (kind, next_index) = BreakpointKindWrapper::column(statement, start_index + 1)?; Ok(( @@ -248,7 +250,8 @@ impl Column for Breakpoints { .column_int(index) .with_context(|| format!("Failed to read BreakPoint at index {index}"))? as u32; - + let position = NonZeroU32::new(position) + .ok_or_else(|| anyhow!("Position must be non-zero"))?; let (kind, next_index) = BreakpointKindWrapper::column(statement, index + 1)?; breakpoints.push(Breakpoint { From 0e998cde8456aafa4f3102db6305f7616c6c8d30 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 21 Feb 2025 00:30:53 -0500 Subject: [PATCH 637/650] Change stack frame data structure to index map instead of vec --- crates/project/src/debugger/session.rs | 51 ++++++++++++++++---------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 43897dbb6fe628..1a289899616743 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -104,10 +104,12 @@ pub enum ThreadStatus { Ended, } +type StackFrameId = u64; + #[derive(Debug)] pub struct Thread { dap: dap::Thread, - stack_frames: Vec, + stack_frames: IndexMap, has_stopped: bool, } @@ -115,7 +117,7 @@ impl From for Thread { fn from(dap: dap::Thread) -> Self { Self { dap, - stack_frames: vec![], + stack_frames: IndexMap::new(), has_stopped: false, } } @@ -1212,8 +1214,11 @@ impl Session { }, move |this, stack_frames, cx| { let entry = this.threads.entry(thread_id).and_modify(|thread| { - thread.stack_frames = - stack_frames.iter().cloned().map(From::from).collect(); + thread.stack_frames = stack_frames + .iter() + .cloned() + .map(|frame| (frame.id, frame.into())) + .collect(); }); debug_assert!( matches!(entry, indexmap::map::Entry::Occupied(_)), @@ -1228,7 +1233,7 @@ impl Session { self.threads .get(&thread_id) - .map(|thread| thread.stack_frames.clone()) + .map(|thread| thread.stack_frames.values().cloned().collect()) .unwrap_or_default() } @@ -1245,11 +1250,7 @@ impl Session { }, move |this, scopes, cx| { this.threads.entry(thread_id).and_modify(|thread| { - if let Some(stack_frame) = thread - .stack_frames - .iter_mut() - .find(|frame| frame.dap.id == stack_frame_id) - { + if let Some(stack_frame) = thread.stack_frames.get_mut(&stack_frame_id) { stack_frame.scopes = scopes.iter().cloned().map(From::from).collect(); cx.notify(); } @@ -1260,9 +1261,10 @@ impl Session { self.threads .get(&thread_id) .and_then(|thread| { - thread.stack_frames.iter().find_map(|stack_frame| { - (stack_frame.dap.id == stack_frame_id).then(|| stack_frame.scopes.clone()) - }) + thread + .stack_frames + .get(&stack_frame_id) + .map(|stack_frame| stack_frame.scopes.clone()) }) .unwrap_or_default() } @@ -1274,14 +1276,15 @@ impl Session { variables_reference: u64, ) -> Option<&mut Scope> { self.threads.get_mut(&thread_id).and_then(|thread| { - let stack_frame = thread + thread .stack_frames - .iter_mut() - .find(|stack_frame| (stack_frame.dap.id == stack_frame_id))?; - stack_frame - .scopes - .iter_mut() - .find(|scope| scope.dap.variables_reference == variables_reference) + .get_mut(&stack_frame_id) + .and_then(|frame| { + frame + .scopes + .iter_mut() + .find(|scope| scope.dap.variables_reference == variables_reference) + }) }) } @@ -1395,4 +1398,12 @@ impl Session { .detach(); } } + + pub fn variable_list(&mut self, selected_thread_id: ThreadId, stack_frame_id: u64) -> &[Scope] { + let Some(thread) = self.threads.get_mut(&selected_thread_id) else { + return &[]; + }; + + todo!() + } } From d34726f285aadc0f3ab3898bc83771dd43f85172 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 13:52:31 +0100 Subject: [PATCH 638/650] Fix compile error --- crates/debugger_ui/src/session/running/console.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 5e46d69f2a334d..b3ab2c845fae2d 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -201,7 +201,7 @@ impl Console { FoldPlaceholder { render: Arc::new({ let placeholder = placeholder.clone(); - move |_id, _range, _window, _cx| { + move |_id, _range, _cx| { ButtonLike::new("output-group-placeholder") .style(ButtonStyle::Transparent) .layer(ElevationIndex::ElevatedSurface) From f957e79e5d50dc592e26910a9194756bad8d73fa Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 13:59:24 +0100 Subject: [PATCH 639/650] Fix incorrect if statement logic for when to fetch stack frames --- crates/project/src/debugger/session.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 1a289899616743..0f844e0e75ee6a 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -367,14 +367,17 @@ impl ThreadStates { self.global_state = Some(ThreadStatus::Stopped); self.known_thread_states.clear(); } + fn all_threads_continued(&mut self) { self.global_state = Some(ThreadStatus::Running); self.known_thread_states.clear(); } + fn thread_stopped(&mut self, thread_id: ThreadId) { self.known_thread_states .insert(thread_id, ThreadStatus::Stopped); } + fn thread_continued(&mut self, thread_id: ThreadId) { self.known_thread_states .insert(thread_id, ThreadStatus::Running); @@ -908,11 +911,11 @@ impl Session { } pub fn modules(&mut self, cx: &mut Context) -> &[Module] { - if !self.thread_states.any_thread_running() { + if self.thread_states.any_thread_running() { self.fetch( dap_command::ModulesCommand, |this, result, cx| { - this.modules = result.clone(); + this.modules = result.iter().cloned().collect(); cx.notify(); }, cx, @@ -977,14 +980,17 @@ impl Session { } pub fn loaded_sources(&mut self, cx: &mut Context) -> &[Source] { - self.fetch( - dap_command::LoadedSourcesCommand, - |this, result, cx| { - this.loaded_sources = result.clone(); - cx.notify(); - }, - cx, - ); + if self.thread_states.any_thread_running() { + self.fetch( + dap_command::LoadedSourcesCommand, + |this, result, cx| { + this.loaded_sources = result.clone(); + cx.notify(); + }, + cx, + ); + } + &self.loaded_sources } From 76b2004489c988a663552478ea54f581d803a484 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 14:02:44 +0100 Subject: [PATCH 640/650] Fix some clippy errors --- crates/debugger_tools/src/dap_log.rs | 16 +++++++--------- crates/debugger_ui/src/session/inert.rs | 2 ++ .../src/session/running/stack_frame_list.rs | 8 +++----- crates/project/src/debugger/session.rs | 3 +-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 13c6520c04ecb4..6ed048ca19febb 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -166,15 +166,13 @@ impl LogStore { }), cx.subscribe(project, |this, project, event, cx| match event { project::Event::DebugClientStarted(client_id) => { - let client = project.update(cx, |project, cx| { - project.dap_store().update(cx, |store, cx| { - store - .session_by_id(client_id) - .and_then(|client| Some(client)) - }) - }); - if let Some(client) = client { - this.add_debug_client(*client_id, client, cx); + let session = project + .read(cx) + .dap_store() + .read(cx) + .session_by_id(client_id); + if let Some(session) = session { + this.add_debug_client(*client_id, session, cx); } } project::Event::DebugClientShutdown(client_id) => { diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs index 6772c1c59e730b..b14c1257e44586 100644 --- a/crates/debugger_ui/src/session/inert.rs +++ b/crates/debugger_ui/src/session/inert.rs @@ -78,6 +78,7 @@ impl Render for InertState { .entry("Delve", None, setter_for_name("Delve")) .entry("LLDB", None, setter_for_name("LLDB")) .entry("PHP", None, setter_for_name("PHP")) + .entry("JavaScript", None, setter_for_name("JavaScript")) .entry("Debugpy", None, setter_for_name("Debugpy")) }), )), @@ -130,6 +131,7 @@ fn kind_for_label(label: &str) -> DebugAdapterKind { match label { "LLDB" => DebugAdapterKind::Lldb, "Debugpy" => DebugAdapterKind::Python(TCPHost::default()), + "JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()), "PHP" => DebugAdapterKind::Php(TCPHost::default()), "Delve" => DebugAdapterKind::Go(TCPHost::default()), _ => { diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index a1af6fedb2769f..0c34b46aa7c628 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -243,11 +243,9 @@ impl StackFrameList { row, cx, ); - }); - }); - }); - - Ok(()) + }) + }) + })? } }) } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 0f844e0e75ee6a..4e83773a228f90 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -13,10 +13,9 @@ use anyhow::{anyhow, Result}; use collections::{HashMap, IndexMap}; use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; use dap::client::{DebugAdapterClient, SessionId}; -use dap::requests::Request; use dap::{ messages::{self, Events, Message}, - requests::{RunInTerminal, SetBreakpoints, StartDebugging}, + requests::SetBreakpoints, Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, SetBreakpointsArguments, Source, SourceBreakpoint, SteppingGranularity, StoppedEvent, }; From 7b0dd6827c23c6e6a053abb8220aa91bc33de05c Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 14:15:06 +0100 Subject: [PATCH 641/650] Update if statement for fetching modules and loaded sources This is more implicit, and the previous variant we would also request the dap when the status was exited --- crates/project/src/debugger/session.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 4e83773a228f90..a8133b9f7340a7 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -394,13 +394,13 @@ impl ThreadStates { .or(self.global_state) } - fn any_thread_running(&self) -> bool { + fn any_stopped_thread(&self) -> bool { self.global_state - .is_some_and(|state| state == ThreadStatus::Running) + .is_some_and(|state| state == ThreadStatus::Stopped) || self .known_thread_states .values() - .any(|status| *status == ThreadStatus::Running) + .any(|status| *status == ThreadStatus::Stopped) } } @@ -910,7 +910,7 @@ impl Session { } pub fn modules(&mut self, cx: &mut Context) -> &[Module] { - if self.thread_states.any_thread_running() { + if self.thread_states.any_stopped_thread() { self.fetch( dap_command::ModulesCommand, |this, result, cx| { @@ -979,7 +979,7 @@ impl Session { } pub fn loaded_sources(&mut self, cx: &mut Context) -> &[Source] { - if self.thread_states.any_thread_running() { + if self.thread_states.any_stopped_thread() { self.fetch( dap_command::LoadedSourcesCommand, |this, result, cx| { From c391ff8742ee5a4f37e163bc6ba40533f5618005 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 14:15:12 +0100 Subject: [PATCH 642/650] Remove unused method --- crates/debugger_ui/src/session/running/module_list.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index aad4aaf9a5a331..821b91519fda0b 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -42,11 +42,6 @@ impl ModuleList { } } - pub fn on_module_event(&mut self, event: &ModuleEvent, cx: &mut Context) { - self.session - .update(cx, |state, cx| state.handle_module_event(event, cx)); - } - fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { let Some(module) = maybe!({ self.session From 14472db13c561614f3816e1497ba60e405a5a092 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 14:23:09 +0100 Subject: [PATCH 643/650] Invalidate modules and loaded source when receiving events --- crates/project/src/debugger/session.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index a8133b9f7340a7..c92b51d30b7c18 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -759,8 +759,12 @@ impl Session { } Events::Output(_event) => {} Events::Breakpoint(_) => {} - Events::Module(_event) => {} - Events::LoadedSource(_event) => {} + Events::Module(_) => { + self.invalidate_state(&ModulesCommand.into()); + } + Events::LoadedSource(_) => { + self.invalidate_state(&ModulesCommand.into()); + } Events::Capabilities(_event) => {} Events::Memory(_) => {} Events::Process(_) => {} @@ -983,7 +987,7 @@ impl Session { self.fetch( dap_command::LoadedSourcesCommand, |this, result, cx| { - this.loaded_sources = result.clone(); + this.loaded_sources = result.iter().cloned().collect(); cx.notify(); }, cx, From 104980ac151fef9d9d184f7240ccecd38acdec43 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 14:27:29 +0100 Subject: [PATCH 644/650] Impl capabilities event --- crates/project/src/debugger/session.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index c92b51d30b7c18..634bb2f2a3b039 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -765,7 +765,10 @@ impl Session { Events::LoadedSource(_) => { self.invalidate_state(&ModulesCommand.into()); } - Events::Capabilities(_event) => {} + Events::Capabilities(event) => { + self.capabilities = self.capabilities.merge(event.capabilities); + cx.notify(); + } Events::Memory(_) => {} Events::Process(_) => {} Events::ProgressEnd(_) => {} From 7b49c7aad6cc84cecb246aff25273812c0dbef6d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:16:51 +0100 Subject: [PATCH 645/650] Remove unused function --- crates/project/src/debugger/session.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 634bb2f2a3b039..f5b0c9303e3af8 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -980,11 +980,6 @@ impl Session { self.ignore_breakpoints } - pub fn handle_module_event(&mut self, _event: &dap::ModuleEvent, cx: &mut Context) { - self.invalidate_state(&ModulesCommand.into()); - cx.notify(); - } - pub fn loaded_sources(&mut self, cx: &mut Context) -> &[Source] { if self.thread_states.any_stopped_thread() { self.fetch( From a8c79cce9944a72fb2379501b592ef1f9936f46b Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 17:31:35 +0100 Subject: [PATCH 646/650] Oopps --- crates/project/src/debugger/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index f5b0c9303e3af8..ee84ec61866f3f 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -763,7 +763,7 @@ impl Session { self.invalidate_state(&ModulesCommand.into()); } Events::LoadedSource(_) => { - self.invalidate_state(&ModulesCommand.into()); + self.invalidate_state(&LoadedSourcesCommand.into()); } Events::Capabilities(event) => { self.capabilities = self.capabilities.merge(event.capabilities); From 417e1d9907702068ba873e09476f4a1ef8e775c4 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 21 Feb 2025 01:33:54 -0500 Subject: [PATCH 647/650] Get scopes to render when selecting a stack frame --- .../src/session/running/stack_frame_list.rs | 4 + .../src/session/running/variable_list.rs | 163 +++++------------- crates/project/src/debugger/session.rs | 54 +++++- 3 files changed, 91 insertions(+), 130 deletions(-) diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 0c34b46aa7c628..0b775ea740b710 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -115,6 +115,10 @@ impl StackFrameList { self.current_stack_frame_id } + pub fn current_thread_id(&self) -> Option { + self.thread_id + } + fn build_entries(&mut self, cx: &mut Context) { let mut entries = Vec::new(); let mut collapsed_entries = Vec::new(); diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index f5c032b5d9d42b..a6751443f69ea8 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -7,7 +7,7 @@ use gpui::{ FocusHandle, Focusable, Hsla, ListOffset, ListState, MouseDownEvent, Point, Subscription, Task, }; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}; -use project::debugger::session::Session; +use project::debugger::session::{self, Session}; use rpc::proto::{ self, DebuggerScopeVariableIndex, DebuggerVariableContainer, VariableListScopes, VariableListVariables, @@ -18,7 +18,7 @@ use std::{ }; use sum_tree::{Dimension, Item, SumTree, Summary}; use ui::{prelude::*, ContextMenu, ListItem}; -use util::ResultExt; +use util::{debug_panic, ResultExt}; actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]); @@ -237,31 +237,6 @@ impl Summary for ScopeVariableSummary { } } -impl ProtoConversion for ScopeVariableIndex { - type ProtoType = DebuggerScopeVariableIndex; - type Output = Self; - - fn to_proto(&self) -> Self::ProtoType { - DebuggerScopeVariableIndex { - fetched_ids: self.fetched_ids.iter().copied().collect(), - variables: self.variables.iter().map(|var| var.to_proto()).collect(), - } - } - - fn from_proto(payload: Self::ProtoType) -> Self { - Self { - fetched_ids: payload.fetched_ids.iter().copied().collect(), - variables: SumTree::from_iter( - payload - .variables - .iter() - .filter_map(|var| VariableContainer::from_proto(var.clone()).log_err()), - &(), - ), - } - } -} - impl ScopeVariableIndex { pub fn new() -> Self { Self { @@ -395,67 +370,6 @@ impl VariableList { } } - pub(crate) fn _to_proto(&self) -> proto::DebuggerVariableList { - let variables = self - .variables - .iter() - .map( - |((stack_frame_id, scope_id), scope_variable_index)| VariableListVariables { - scope_id: *scope_id, - stack_frame_id: *stack_frame_id, - variables: Some(scope_variable_index.to_proto()), - }, - ) - .collect(); - - let scopes = self - .scopes - .iter() - .map(|(key, scopes)| VariableListScopes { - stack_frame_id: *key, - scopes: scopes.to_proto(), - }) - .collect(); - - proto::DebuggerVariableList { scopes, variables } - } - - pub(crate) fn set_from_proto( - &mut self, - state: &proto::DebuggerVariableList, - cx: &mut Context, - ) { - self.variables = state - .variables - .iter() - .filter_map(|variable| { - Some(( - (variable.stack_frame_id, variable.scope_id), - ScopeVariableIndex::from_proto(variable.variables.clone()?), - )) - }) - .collect(); - - self.scopes = state - .scopes - .iter() - .map(|scope| { - ( - scope.stack_frame_id, - scope - .scopes - .clone() - .into_iter() - .map(Scope::from_proto) - .collect(), - ) - }) - .collect(); - - self.build_entries(true, cx); - cx.notify(); - } - fn handle_stack_frame_list_events( &mut self, _: Entity, @@ -559,34 +473,23 @@ impl VariableList { fn render_entry(&mut self, ix: usize, cx: &mut Context) -> AnyElement { let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id(); + let Some(thread_id) = self.stack_frame_list.read(cx).current_thread_id() else { + return div().into_any_element(); + }; + + let entries = self.session.update(cx, |session, cx| { + session.variable_list(thread_id, stack_frame_id, cx) + }); - let Some(entries) = self.entries.get(&stack_frame_id) else { + let Some(entry) = entries.get(ix) else { + debug_panic!("Trying to render entry in variable list that has an out of bounds index"); return div().into_any_element(); }; let entry = &entries[ix]; match entry { - VariableListEntry::Scope(scope) => { - self.render_scope(scope, Some(entry) == self.selection.as_ref(), cx) - } - VariableListEntry::SetVariableEditor { depth, state } => { - self.render_set_variable_editor(*depth, state, cx) - } - VariableListEntry::Variable { - depth, - scope, - variable, - has_children, - container_reference, - } => self.render_variable( - *container_reference, - variable, - scope, - *depth, - *has_children, - Some(entry) == self.selection.as_ref(), - cx, - ), + session::VariableListContainer::Scope(scope) => self.render_scope(scope, false, cx), // todo(debugger) pass a valid value for is selected + _ => div().into_any_element(), } } @@ -1291,13 +1194,19 @@ impl VariableList { .into_any() } - fn render_scope(&self, scope: &Scope, is_selected: bool, cx: &mut Context) -> AnyElement { - let element_id = scope.variables_reference; + fn render_scope( + &self, + scope: &session::Scope, + is_selected: bool, + cx: &mut Context, + ) -> AnyElement { + let element_id = scope.dap.variables_reference; let entry_id = OpenEntry::Scope { - name: scope.name.clone(), + name: scope.dap.name.clone(), }; - let disclosed = self.open_entries.binary_search(&entry_id).is_ok(); + + let disclosed = scope.is_toggled; let colors = get_entry_color(cx); let bg_hover_color = if !is_selected { @@ -1322,26 +1231,21 @@ impl VariableList { .h_full() .hover(|style| style.bg(bg_hover_color)) .on_click(cx.listener({ - let scope = scope.clone(); move |this, _, _window, cx| { - this.selection = Some(VariableListEntry::Scope(scope.clone())); cx.notify(); } })) .child( ListItem::new(SharedString::from(format!( "scope-{}", - scope.variables_reference + scope.dap.variables_reference ))) .selectable(false) .indent_level(1) .indent_step_size(px(20.)) .always_show_disclosure_icon(true) .toggle(disclosed) - .on_toggle( - cx.listener(move |this, _, _window, cx| this.toggle_entry(&entry_id, cx)), - ) - .child(div().text_ui(cx).w_full().child(scope.name.clone())), + .child(div().text_ui(cx).w_full().child(scope.dap.name.clone())), ) .into_any() } @@ -1355,6 +1259,23 @@ impl Focusable for VariableList { impl Render for VariableList { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + // todo(debugger): We are reconstructing the variable list list state every frame + // which is very bad!! We should only reconstruct the variable list state when necessary. + // Will fix soon + let (stack_frame_id, thread_id) = self.stack_frame_list.read_with(cx, |list, cx| { + (list.current_stack_frame_id(), list.current_thread_id()) + }); + let len = if let Some(thread_id) = thread_id { + self.session + .update(cx, |session, cx| { + session.variable_list(thread_id, stack_frame_id, cx) + }) + .len() + } else { + 0 + }; + self.list.reset(len); + div() .key_context("VariableList") .id("variable-list") diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index ee84ec61866f3f..b749fd32c6211a 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -49,17 +49,35 @@ impl ThreadId { pub const MAX: ThreadId = ThreadId(u64::MAX); } +pub enum VariableListContainer { + Scope(Scope), + Variable(Variable), +} + +#[derive(Clone, Debug)] +enum ToggledState { + Toggled, + UnToggled, + Leaf, +} + #[derive(Clone, Debug)] pub struct Variable { - _dap: dap::Variable, - _variables: Vec, + dap: dap::Variable, + children: Vec, + is_toggled: ToggledState, } impl From for Variable { fn from(dap: dap::Variable) -> Self { Self { - _dap: dap, - _variables: vec![], + children: vec![], + is_toggled: if dap.variables_reference == 0 { + ToggledState::Leaf + } else { + ToggledState::UnToggled + }, + dap, } } } @@ -68,6 +86,7 @@ impl From for Variable { pub struct Scope { pub dap: dap::Scope, pub variables: Vec, + pub is_toggled: bool, } impl From for Scope { @@ -75,6 +94,7 @@ impl From for Scope { Self { dap: scope, variables: vec![], + is_toggled: true, } } } @@ -1406,11 +1426,27 @@ impl Session { } } - pub fn variable_list(&mut self, selected_thread_id: ThreadId, stack_frame_id: u64) -> &[Scope] { - let Some(thread) = self.threads.get_mut(&selected_thread_id) else { - return &[]; - }; + pub fn variable_list( + &mut self, + selected_thread_id: ThreadId, + stack_frame_id: u64, + cx: &mut Context, + ) -> Vec { + self.scopes(selected_thread_id, stack_frame_id, cx) + .iter() + .cloned() + .map(|scope| { + if scope.is_toggled { + self.variables( + selected_thread_id, + stack_frame_id, + scope.dap.variables_reference, + cx, + ); + } - todo!() + VariableListContainer::Scope(scope) + }) + .collect() } } From 255c86c6f0140a1f71563f4d38053c98adfdc58a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 21 Feb 2025 23:22:07 +0100 Subject: [PATCH 648/650] Make output console work again (#116) This uses a last processed index for adding new message that we didn't add yet, so we can keep the folded output items. --- .../src/session/running/console.rs | 20 +++++++++++++++-- crates/project/src/debugger/session.rs | 22 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index b3ab2c845fae2d..ba46d3af21c840 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -77,8 +77,24 @@ impl Console { editor }); - let _subscriptions = - vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)]; + let _subscriptions = vec![ + cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events), + cx.observe_in(&session, window, |console, session, window, cx| { + let (output, last_processed_ix) = session.update(cx, |session, cx| { + (session.output(), session.last_processed_output()) + }); + + if output.len() > last_processed_ix { + for event in &output[last_processed_ix..] { + console.add_message(event.clone(), window, cx); + } + + session.update(cx, |session, cx| { + session.set_last_processed_output(output.len()); + }); + } + }), + ]; Self { session, diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index b749fd32c6211a..f158344acecba7 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -434,6 +434,8 @@ pub struct Session { ignore_breakpoints: bool, modules: Vec, loaded_sources: Vec, + last_processed_output: usize, + output: Vec, threads: IndexMap, requests: HashMap>>>, thread_states: ThreadStates, @@ -570,6 +572,8 @@ impl Session { capabilities, thread_states: ThreadStates::default(), ignore_breakpoints: false, + last_processed_output: 0, + output: Vec::default(), requests: HashMap::default(), modules: Vec::default(), loaded_sources: Vec::default(), @@ -598,6 +602,8 @@ impl Session { breakpoint_store, ignore_breakpoints, thread_states: ThreadStates::default(), + last_processed_output: 0, + output: Vec::default(), requests: HashMap::default(), modules: Vec::default(), loaded_sources: Vec::default(), @@ -729,6 +735,18 @@ impl Session { }) } + pub fn output(&self) -> Vec { + self.output.iter().cloned().collect() + } + + pub fn last_processed_output(&self) -> usize { + self.last_processed_output + } + + pub fn set_last_processed_output(&mut self, last_processed_output: usize) { + self.last_processed_output = last_processed_output; + } + fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context) { // todo(debugger): We should query for all threads here if we don't get a thread id // maybe in both cases too? @@ -777,7 +795,9 @@ impl Session { } self.invalidate_state(&ThreadsCommand.into()); } - Events::Output(_event) => {} + Events::Output(event) => { + self.output.push(event); + } Events::Breakpoint(_) => {} Events::Module(_) => { self.invalidate_state(&ModulesCommand.into()); From bd3ffd7150de2a2a3cf2d22cce85f26987eadf7f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 22 Feb 2025 14:52:53 +0100 Subject: [PATCH 649/650] Add evaluate value to output console again --- .../src/session/running/console.rs | 30 ------------------- crates/project/src/debugger/session.rs | 20 ++++++++++--- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index ba46d3af21c840..5ff9b0d0f1ce8a 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -255,36 +255,6 @@ impl Console { cx, ); }); - - // TODO(debugger): make this work again - // let weak_console = cx.weak_entity(); - - // window - // .spawn(cx, |mut cx| async move { - // let response = evaluate_task.await?; - - // weak_console.update_in(&mut cx, |console, window, cx| { - // console.add_message( - // OutputEvent { - // category: None, - // output: response.result, - // group: None, - // variables_reference: Some(response.variables_reference), - // source: None, - // line: None, - // column: None, - // data: None, - // }, - // window, - // cx, - // ); - - // console.variable_list.update(cx, |variable_list, cx| { - // variable_list.invalidate(window, cx); - // }) - // }) - // }) - // .detach_and_log_err(cx); } fn render_console(&self, cx: &Context) -> impl IntoElement { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index f158344acecba7..765172f64bbe5f 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -11,9 +11,9 @@ use super::dap_command::{ use super::dap_store::DapAdapterDelegate; use anyhow::{anyhow, Result}; use collections::{HashMap, IndexMap}; -use dap::adapters::{DapDelegate, DapStatus, DebugAdapterName}; -use dap::client::{DebugAdapterClient, SessionId}; use dap::{ + adapters::{DapDelegate, DapStatus, DebugAdapterName}, + client::{DebugAdapterClient, SessionId}, messages::{self, Events, Message}, requests::SetBreakpoints, Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, SetBreakpointsArguments, @@ -1411,12 +1411,24 @@ impl Session { frame_id, source, }, - |this, _response, cx| { + |this, response, cx| { + this.output.push(dap::OutputEvent { + category: None, + output: response.result.clone(), + group: None, + variables_reference: Some(response.variables_reference), + source: None, + line: None, + column: None, + data: None, + }); + + // TODO(debugger): only invalidate variables & scopes this.invalidate(cx); }, cx, ) - .detach() + .detach(); } pub fn disconnect_client(&mut self, cx: &mut Context) { From 6a339a119366b64bfe7b8163194507cf8f1811ef Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 22 Feb 2025 14:55:00 +0100 Subject: [PATCH 650/650] Fix one more todo --- crates/debugger_ui/src/session/running/console.rs | 5 ++--- crates/project/src/debugger/session.rs | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 5ff9b0d0f1ce8a..c63a64ad79c08b 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -117,9 +117,8 @@ impl Console { &self.query_bar } - fn is_local(&self, _cx: &Context) -> bool { - // todo(debugger): Fix this function - true + fn is_local(&self, cx: &Context) -> bool { + self.session.read(cx).is_local() } fn handle_stack_frame_list_events( diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 765172f64bbe5f..6dfb94651b6682 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -630,6 +630,10 @@ impl Session { self.is_session_terminated } + pub fn is_local(&self) -> bool { + matches!(self.mode, Mode::Local(_)) + } + fn send_changed_breakpoints( &mut self, _breakpoint_store: Entity,