From 0068fe7632a59216efb336edf741c72708f9e3e7 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 27 Sep 2024 18:09:50 -0400 Subject: [PATCH 1/6] Create basic breakpoint prompt editor structure --- crates/editor/src/editor.rs | 77 +++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 029c9196ceac7d..c1e9af743a83ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14127,3 +14127,80 @@ fn check_multiline_range(buffer: &Buffer, range: Range) -> Range { range.start..range.start } } + +struct BreakpointPromptEditor { + editor: View, + _subscriptions: Vec, +} + +impl BreakpointPromptEditor { + const MAX_LINES: u8 = 4; + + fn new(cx: &mut ViewContext) -> Self { + let buffer = cx.new_model(|cx| Buffer::local(String::new(), cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let editor = cx.new_view(|cx| { + let mut editor = Editor::new( + EditorMode::AutoHeight { + max_lines: Self::MAX_LINES as usize, + }, + buffer, + None, + false, + cx, + ); + editor.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. + editor.set_show_cursor_when_unfocused(true, cx); + editor.set_placeholder_text("Add a prompt…", cx); + editor + }); + + Self { + editor, + _subscriptions: vec![], + } + } + + fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.editor.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.editor, + 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 { + 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) + .child(self.render_prompt_editor(cx)) + } +} From 9f2cf5233eaba747bd272e41d41bf62e6e8eaf8c Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Fri, 27 Sep 2024 22:13:20 -0400 Subject: [PATCH 2/6] Get breakpoint prompt to properly render --- crates/editor/src/editor.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c1e9af743a83ac..c00f43bcbf2717 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5479,6 +5479,35 @@ impl Editor { .entry("Toggle Log Breakpoint", None, move |cx| { if let Some(editor) = second_weak.clone().upgrade() { editor.update(cx, |this, cx| { + let breakpoint_prompt = + cx.new_view(BreakpointPromptEditor::new); + + let position = this.snapshot(cx).display_point_to_anchor( + DisplayPoint::new(row, 0), + Bias::Left, + ); + + let height = breakpoint_prompt.update(cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + editor.max_point(cx).row().0 + 1 + 2 + }) + }); + + let prompt_editor = breakpoint_prompt.clone(); + + let blocks = vec![BlockProperties { + style: BlockStyle::Sticky, + position, + height, + render: Box::new(move |cx| { + prompt_editor.clone().into_any_element() + }), + disposition: BlockDisposition::Above, + priority: 0, + }]; + + this.insert_blocks(blocks, None, cx); + this.toggle_breakpoint_at_anchor( anchor, BreakpointKind::Log("Log breakpoint".into()), @@ -14129,7 +14158,7 @@ fn check_multiline_range(buffer: &Buffer, range: Range) -> Range { } struct BreakpointPromptEditor { - editor: View, + pub(crate) editor: View, _subscriptions: Vec, } @@ -14201,6 +14230,6 @@ impl Render for BreakpointPromptEditor { .border_color(cx.theme().status().info_border) .size_full() .py(cx.line_height() / 2.5) - .child(self.render_prompt_editor(cx)) + .child(div().flex_1().child(self.render_prompt_editor(cx))) } } From d3b585b5e6a04af05c6c2514001de15700e60fdf Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 2 Oct 2024 18:01:12 -0400 Subject: [PATCH 3/6] 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. --- crates/editor/src/display_map.rs | 11 ++++++ crates/editor/src/editor.rs | 49 +++++++++++++++++-------- crates/editor/src/element.rs | 38 +++++++++++-------- crates/multi_buffer/src/multi_buffer.rs | 10 +++++ crates/project/src/dap_store.rs | 43 +++++++++++++++++----- crates/text/src/anchor.rs | 8 ---- crates/text/src/text.rs | 12 ++++++ 7 files changed, 121 insertions(+), 50 deletions(-) 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 c00f43bcbf2717..c6a775519dfe5f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5332,7 +5332,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 { @@ -5352,7 +5352,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), breakpoint.clone()); } }; }; @@ -5408,7 +5408,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, breakpoint.clone()); } } }; @@ -5484,7 +5484,7 @@ impl Editor { let position = this.snapshot(cx).display_point_to_anchor( DisplayPoint::new(row, 0), - Bias::Left, + Bias::Right, ); let height = breakpoint_prompt.update(cx, |this, cx| { @@ -5499,7 +5499,7 @@ impl Editor { style: BlockStyle::Sticky, position, height, - render: Box::new(move |cx| { + render: Box::new(move |_cx| { prompt_editor.clone().into_any_element() }), disposition: BlockDisposition::Above, @@ -6372,15 +6372,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 @@ -6389,10 +6380,36 @@ 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)) + }); + + if let Some((anchor, kind)) = found_bp { + self.toggle_breakpoint_at_anchor(anchor, kind, cx); + } else { + self.toggle_breakpoint_at_anchor(breakpoint_position, BreakpointKind::Standard, cx); + } } pub fn toggle_breakpoint_at_anchor( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 076bfb88bfcf5b..e58696c7264594 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -51,7 +51,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, }; @@ -1592,7 +1592,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| { @@ -1602,7 +1602,7 @@ impl EditorElement { breakpoints .iter() - .filter_map(|(point, kind)| { + .filter_map(|(point, bp)| { let row = MultiBufferRow { 0: point.row().0 }; if range.start > point.row() || range.end < point.row() { @@ -1613,18 +1613,16 @@ 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(*point) .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.row(), + &bp.kind, + cx, + ); let button = prepaint_gutter_button( button, @@ -1652,7 +1650,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| { @@ -1721,7 +1719,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; @@ -5248,7 +5246,15 @@ impl Element for EditorElement { if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator { breakpoint_lines .entry(gutter_breakpoint_point) - .or_insert(BreakpointKind::Standard); + .or_insert(Breakpoint { + active_position: Some( + snapshot + .display_point_to_breakpoint_anchor(gutter_breakpoint_point) + .text_anchor, + ), + cache_position: 0, + kind: BreakpointKind::Standard, + }); } let line_numbers = self.layout_line_numbers( diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index d406f9bfaf6ac2..d8993aa1be051d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -3078,6 +3078,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 f187fb94ca043e..7095546a337049 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -139,6 +139,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, @@ -827,7 +841,16 @@ 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 + if self.kind != other.kind { + return false; + } + + 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, + } } } @@ -835,8 +858,13 @@ 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); + } + + self.kind.hash(state); } } @@ -859,13 +887,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/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 7e247a90be4ee2..53a41dce756b10 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 70a00c8e1cbb4c6a450e3d4a0ba24e404b622167 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 2 Oct 2024 18:34:59 -0400 Subject: [PATCH 4/6] 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 --- crates/editor/src/editor.rs | 6 +-- crates/editor/src/element.rs | 80 +++++++++++++++--------------------- 2 files changed, 37 insertions(+), 49 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c6a775519dfe5f..15825e92023d52 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5332,7 +5332,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 { @@ -5352,7 +5352,7 @@ impl Editor { let point = breakpoint.point_for_buffer(&buffer); breakpoint_display_points - .insert(point.to_display_point(&snapshot), breakpoint.clone()); + .insert(point.to_display_point(&snapshot).row(), breakpoint.clone()); } }; }; @@ -5408,7 +5408,7 @@ impl Editor { let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0); - breakpoint_display_points.insert(position, breakpoint.clone()); + breakpoint_display_points.insert(position.row(), breakpoint.clone()); } } }; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e58696c7264594..d3ff807e2c499a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -27,7 +27,7 @@ use crate::{ ToPoint, CURSORS_VISIBLE_FOR, 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::{ @@ -1592,7 +1592,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| { @@ -1603,9 +1603,9 @@ impl EditorElement { breakpoints .iter() .filter_map(|(point, bp)| { - let row = MultiBufferRow { 0: point.row().0 }; + let row = MultiBufferRow { 0: point.0 }; - if range.start > point.row() || range.end < point.row() { + if range.start > *point || range.end < *point { return None; } @@ -1614,19 +1614,19 @@ impl EditorElement { } let backup_position = snapshot - .display_point_to_breakpoint_anchor(*point) + .display_point_to_breakpoint_anchor(DisplayPoint::new(*point, 0)) .text_anchor; let button = editor.render_breakpoint( bp.active_position.unwrap_or(backup_position), - point.row(), + *point, &bp.kind, cx, ); let button = prepaint_gutter_button( button, - point.row(), + *point, line_height, gutter_dimensions, scroll_pixel_position, @@ -1650,7 +1650,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| { @@ -1685,12 +1685,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, ); @@ -1719,7 +1718,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; @@ -1737,10 +1736,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 @@ -1829,7 +1825,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(|| { @@ -1869,7 +1865,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 @@ -5045,7 +5041,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)); @@ -5230,22 +5226,24 @@ impl Element for EditorElement { cx, ); + let line_numbers = self.layout_line_numbers( + start_row..end_row, + buffer_rows.iter().copied(), + &active_rows, + newest_selection_head, + &snapshot, + &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; - - 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) + breakpoint_rows + .entry(gutter_breakpoint_point.row()) .or_insert(Breakpoint { active_position: Some( snapshot @@ -5257,16 +5255,6 @@ impl Element for EditorElement { }); } - let line_numbers = self.layout_line_numbers( - start_row..end_row, - buffer_rows.iter().copied(), - &active_rows, - newest_selection_head, - &snapshot, - breakpoint_rows, - cx, - ); - let mut gutter_fold_toggles = cx.with_element_namespace("gutter_fold_toggles", |cx| { self.layout_gutter_fold_toggles( @@ -5590,7 +5578,7 @@ impl Element for EditorElement { &gutter_dimensions, &gutter_hitbox, &rows_with_hunk_bounds, - &mut breakpoint_lines, + &mut breakpoint_rows, cx, ); } @@ -5609,7 +5597,7 @@ impl Element for EditorElement { &gutter_hitbox, &rows_with_hunk_bounds, &snapshot, - &mut breakpoint_lines, + &mut breakpoint_rows, cx, ) } else { @@ -5624,7 +5612,7 @@ impl Element for EditorElement { &gutter_hitbox, &rows_with_hunk_bounds, &snapshot, - breakpoint_lines, + breakpoint_rows, cx, ); @@ -6492,7 +6480,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| { @@ -6502,7 +6490,7 @@ mod tests { &Default::default(), Some(DisplayPoint::new(DisplayRow(0), 0)), &snapshot, - breakpoint_rows, + &breakpoint_rows, cx, ) }) From 876870e487ccaf27e4fca0ce8b82b7355f66c154 Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Wed, 2 Oct 2024 20:04:19 -0400 Subject: [PATCH 5/6] Get breakpoint prompt to add log breakpoint's correctly --- Cargo.lock | 1 + crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 102 ++++++++++++++++++++++++++---------- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8f71f10ed2d8a..7938205c9e3fa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3777,6 +3777,7 @@ dependencies = [ "log", "lsp", "markdown", + "menu", "multi_buffer", "ordered-float 2.10.1", "parking_lot", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index b6b22ef64d33f6..d8e33f4092da1c 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -51,6 +51,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/editor.rs b/crates/editor/src/editor.rs index 15825e92023d52..b7a53ff51f7e1c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5479,41 +5479,37 @@ impl Editor { .entry("Toggle Log Breakpoint", None, move |cx| { if let Some(editor) = second_weak.clone().upgrade() { editor.update(cx, |this, cx| { - let breakpoint_prompt = - cx.new_view(BreakpointPromptEditor::new); - 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, cx) + }); - let height = breakpoint_prompt.update(cx, |this, cx| { - this.editor.update(cx, |editor, cx| { - editor.max_point(cx).row().0 + 1 + 2 + let height = bp_prompt.update(cx, |this, cx| { + this.prompt.update(cx, |prompt, cx| { + prompt.max_point(cx).row().0 + 1 + 2 }) }); - - let prompt_editor = breakpoint_prompt.clone(); - + let cloned_prompt = bp_prompt.clone(); let blocks = vec![BlockProperties { style: BlockStyle::Sticky, position, height, render: Box::new(move |_cx| { - prompt_editor.clone().into_any_element() + cloned_prompt.clone().into_any_element() }), disposition: BlockDisposition::Above, priority: 0, }]; - this.insert_blocks(blocks, None, cx); - - this.toggle_breakpoint_at_anchor( - anchor, - BreakpointKind::Log("Log breakpoint".into()), - cx, - ); - }) + let block_ids = this.insert_blocks(blocks, None, cx); + bp_prompt.update(cx, |prompt, _| { + prompt.add_block_ids(block_ids); + }); + }); } }) }); @@ -14175,19 +14171,26 @@ fn check_multiline_range(buffer: &Buffer, range: Range) -> Range { } struct BreakpointPromptEditor { - pub(crate) editor: View, + pub(crate) prompt: View, + editor: WeakView, + breakpoint_anchor: text::Anchor, + block_ids: HashSet, _subscriptions: Vec, } impl BreakpointPromptEditor { const MAX_LINES: u8 = 4; - fn new(cx: &mut ViewContext) -> Self { + fn new( + editor: WeakView, + breakpoint_anchor: text::Anchor, + cx: &mut ViewContext, + ) -> Self { let buffer = cx.new_model(|cx| Buffer::local(String::new(), cx)); let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.new_view(|cx| { - let mut editor = Editor::new( + let prompt = cx.new_view(|cx| { + let mut prompt = Editor::new( EditorMode::AutoHeight { max_lines: Self::MAX_LINES as usize, }, @@ -14196,25 +14199,66 @@ impl BreakpointPromptEditor { false, cx, ); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, 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. - editor.set_show_cursor_when_unfocused(true, cx); - editor.set_placeholder_text("Add a prompt…", cx); - editor + prompt.set_show_cursor_when_unfocused(true, cx); + prompt.set_placeholder_text("Add a prompt…", cx); + + prompt }); Self { + prompt, editor, + breakpoint_anchor, + 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.toggle_breakpoint_at_anchor( + self.breakpoint_anchor, + BreakpointKind::Log(log_message.into()), + 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.editor.read(cx).read_only(cx) { + color: if self.prompt.read(cx).read_only(cx) { cx.theme().colors().text_disabled } else { cx.theme().colors().text @@ -14227,7 +14271,7 @@ impl BreakpointPromptEditor { ..Default::default() }; EditorElement::new( - &self.editor, + &self.prompt, EditorStyle { background: cx.theme().colors().editor_background, local_player: cx.theme().players().local(), @@ -14247,6 +14291,8 @@ impl Render for BreakpointPromptEditor { .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(div().flex_1().child(self.render_prompt_editor(cx))) } } From d6d7ceb71219a2d06953bd86fe418f0ae1ec921e Mon Sep 17 00:00:00 2001 From: Anthony Eid Date: Thu, 3 Oct 2024 00:09:59 -0400 Subject: [PATCH 6/6] Clean up breakpoint prompt UI & allow editting of log messages --- crates/editor/src/editor.rs | 79 ++++++++++++++++++++++++++++----- crates/project/src/dap_store.rs | 49 +++++++++++++++----- crates/project/src/project.rs | 4 +- 3 files changed, 109 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b7a53ff51f7e1c..0ff80b21775b86 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -100,6 +100,7 @@ use language::{ }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; +use project::dap_store::BreakpointEditAction; use proposed_changes_editor::{ProposedChangesBuffer, ProposedChangesEditor}; use similar::{ChangeTag, TextDiff}; use task::{ResolvedTask, TaskTemplate, TaskVariables}; @@ -5439,6 +5440,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) @@ -5447,7 +5449,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 @@ -5461,6 +5468,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(|| {})) @@ -5468,24 +5483,32 @@ 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| { 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, cx) + BreakpointPromptEditor::new( + weak_editor, + anchor, + log_message, + cx, + ) }); let height = bp_prompt.update(cx, |this, cx| { @@ -5498,13 +5521,18 @@ impl Editor { style: BlockStyle::Sticky, position, height, - render: Box::new(move |_cx| { + 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); @@ -6401,17 +6429,25 @@ impl Editor { Some((bp.active_position?, bp.kind)) }); + let edit_action = BreakpointEditAction::Toggle; + if let Some((anchor, kind)) = found_bp { - self.toggle_breakpoint_at_anchor(anchor, kind, cx); + self.edit_breakpoint_at_anchor(anchor, kind, edit_action, cx); } else { - self.toggle_breakpoint_at_anchor(breakpoint_position, BreakpointKind::Standard, cx); + 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 { @@ -6445,6 +6481,7 @@ impl Editor { active_position: Some(breakpoint_position), kind, }, + edit_action, cx, ); }); @@ -14175,6 +14212,7 @@ struct BreakpointPromptEditor { editor: WeakView, breakpoint_anchor: text::Anchor, block_ids: HashSet, + gutter_dimensions: Arc>, _subscriptions: Vec, } @@ -14184,9 +14222,15 @@ impl BreakpointPromptEditor { fn new( editor: WeakView, breakpoint_anchor: text::Anchor, + log_message: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.new_model(|cx| Buffer::local(String::new(), cx)); + 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| { @@ -14204,7 +14248,10 @@ impl BreakpointPromptEditor { // 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("Add a prompt…", cx); + prompt.set_placeholder_text( + "Message to log when breakpoint is hit. Expressions within {} are interpolated.", + cx, + ); prompt }); @@ -14213,6 +14260,7 @@ impl BreakpointPromptEditor { prompt, editor, breakpoint_anchor, + gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), block_ids: Default::default(), _subscriptions: vec![], } @@ -14236,9 +14284,10 @@ impl BreakpointPromptEditor { .to_string(); editor.update(cx, |editor, cx| { - editor.toggle_breakpoint_at_anchor( + editor.edit_breakpoint_at_anchor( self.breakpoint_anchor, BreakpointKind::Log(log_message.into()), + BreakpointEditAction::EditLogMessage, cx, ); @@ -14284,6 +14333,7 @@ impl BreakpointPromptEditor { 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) @@ -14293,6 +14343,13 @@ impl Render for BreakpointPromptEditor { .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/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 7095546a337049..771a8a44639932 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -724,16 +724,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); } self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) @@ -811,9 +816,16 @@ 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), @@ -826,6 +838,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)] @@ -841,10 +874,6 @@ pub struct Breakpoint { // overlapping breakpoint's with them being aware. impl PartialEq for Breakpoint { fn eq(&self, other: &Self) -> bool { - if self.kind != other.kind { - return false; - } - match (&self.active_position, &other.active_position) { (None, None) => self.cache_position == other.cache_position, (None, Some(_)) => false, @@ -863,8 +892,6 @@ impl Hash for Breakpoint { } else { self.cache_position.hash(state); } - - self.kind.hash(state); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3ca4e4c083b891..5354f19747ae2f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -34,7 +34,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::{ @@ -1280,6 +1280,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 { @@ -1303,6 +1304,7 @@ impl Project { breakpoint, buffer_path, buffer.read(cx).snapshot(), + edit_action, cx, ); });