diff --git a/.zed/tasks.json b/.zed/tasks.json index 259ab07f3e065..ca84769a0716d 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/Cargo.lock b/Cargo.lock index 5d22c2dbf47af..4bb714aa3b970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3233,7 +3233,9 @@ dependencies = [ "dap-types", "futures 0.3.28", "gpui", + "language", "log", + "multi_buffer", "parking_lot", "postage", "release_channel", @@ -3243,6 +3245,7 @@ dependencies = [ "serde_json_lenient", "smol", "task", + "text", "util", ] @@ -3593,6 +3596,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 462cf97cc972c..c6a3aa0894bca 100644 --- a/crates/dap/Cargo.toml +++ b/crates/dap/Cargo.toml @@ -14,6 +14,8 @@ 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 +language.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true @@ -24,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 b6d5ec94676d5..8b2e7fcae2956 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)] @@ -331,7 +333,9 @@ impl DebugAdapterClient { { while let Ok(payload) = client_rx.recv().await { match payload { - Payload::Event(event) => cx.update(|cx| event_handler(*event, cx))?, + 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?; @@ -395,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( @@ -416,10 +419,7 @@ impl DebugAdapterClient { ) .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); @@ -455,6 +455,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())?), @@ -644,3 +645,24 @@ impl DebugAdapterClient { .await } } + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +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/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 34b3425c949be..c0cef38564562 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), diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 8fb4e3be32559..b0761667509cf 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 9ca552937bcb8..264c484b1c029 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,12 +451,6 @@ struct ResolvedTasks { position: Anchor, } -#[derive(Clone, Debug)] -struct Breakpoint { - row: MultiBufferRow, - _line: BufferRow, -} - #[derive(Copy, Clone, Debug)] struct MultiBufferOffset(usize); #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] @@ -575,7 +570,13 @@ pub struct Editor { expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, - breakpoints: BTreeMap<(BufferId, BufferRow), Breakpoint>, + /// 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, @@ -1789,6 +1790,12 @@ impl Editor { None }; + 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, show_cursor_when_unfocused: false, @@ -1891,7 +1898,8 @@ impl Editor { blame_subscription: None, file_header_size, tasks: Default::default(), - breakpoints: Default::default(), + breakpoints, + gutter_breakpoint_indicator: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), @@ -5141,14 +5149,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,33 +5985,58 @@ 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; }; + + let Some(breakpoints) = &self.breakpoints else { + return; + }; + let Some(buffer) = self.buffer.read(cx).as_singleton() else { return; }; let buffer_id = buffer.read(cx).remote_id(); - let key = (buffer_id, row); - - if self.breakpoints.remove(&key).is_none() { - self.breakpoints.insert( - key, - Breakpoint { - row: MultiBufferRow(row), - _line: row, - }, - ); + + 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| { - project.update_breakpoint(buffer, row + 1, cx); + if project.has_active_debugger() { + project.update_file_breakpoints(buffer_id, cx); + } }); + cx.notify(); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 73ae70abdb13b..ecbced834d47d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -702,6 +702,16 @@ impl EditorElement { let gutter_hovered = gutter_hitbox.is_hovered(cx); editor.set_gutter_hovered(gutter_hovered, cx); + if gutter_hovered { + 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; + } + // Don't trigger hover popover if mouse is hovering over context menu if text_hitbox.is_hovered(cx) { let point_for_position = @@ -1566,34 +1576,89 @@ impl EditorElement { 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 Some(breakpoints) = &editor.breakpoints else { + return vec![]; + }; - let button = prepaint_gutter_button( - button, - display_row, - line_height, - gutter_dimensions, - scroll_pixel_position, - gutter_hitbox, - rows_with_hunk_bounds, - cx, - ); - Some(button) - }) - .collect_vec() + let Some(active_buffer) = editor.buffer().read(cx).as_singleton() else { + return vec![]; + }; + + let active_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(&active_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; + } + + 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 + // 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.row(), + line_height, + gutter_dimensions, + scroll_pixel_position, + gutter_hitbox, + rows_with_hunk_bounds, + cx, + ); + + breakpoints_to_render.push(button); + } + + breakpoints_to_render }) } + #[allow(clippy::too_many_arguments)] fn layout_run_indicators( &self, line_height: Pixels, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 575b7be08e696..6c7f5b1f017c5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -25,9 +25,8 @@ 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, }; 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, @@ -182,7 +181,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, @@ -315,10 +314,6 @@ impl PartialEq for LanguageServerPromptRequest { } } -struct Breakpoint { - row: BufferRow, -} - #[derive(Clone, Debug, PartialEq)] pub enum Event { LanguageServerAdded(LanguageServerId), @@ -1160,11 +1155,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()?; @@ -1174,18 +1177,11 @@ impl Project { if let Some((path, breakpoints)) = res { tasks.push( client.set_breakpoints( - path, + path.clone(), 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| b.to_source_breakpoint(buffer)) .collect::>(), ), ), @@ -1202,6 +1198,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, @@ -1263,26 +1263,7 @@ 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 }); - } - + pub fn update_file_breakpoints(&self, buffer_id: BufferId, cx: &mut ModelContext) { let clients = self .debug_adapters .iter() @@ -1292,18 +1273,49 @@ impl Project { }) .collect::>(); - let mut tasks = Vec::new(); - for client in clients { - tasks.push(self.send_breakpoints(client, cx)); - } + let Some(buffer) = self.buffer_for_id(buffer_id, cx) else { + return; + }; - cx.background_executor() - .spawn(async move { - try_join_all(tasks).await?; + let buffer = buffer.read(cx); - anyhow::Ok(()) - }) - .detach_and_log_err(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( diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index a70d1b769238f..a40c0dedf28ee 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, {