Skip to content

Commit 81a61b2

Browse files
committed
feat(debug): scope-precise breakpoints
Continuing work done in helix-editor#5957, these changes add column precision to breakpoints for those set using the keymap command. For those set with the mouse, preserve the current behaviour, concretely, add a line breakpoint. This allows for more fine grained control, allowing, for debuggers which support it, to add breakpoints to scopes such as lambda functions, callbacks and more. Change how the stack frame line is highlighted, using a full line for breakpoints which apply to a line and highlight only the scope for those that operate only on those scopes. Closes: helix-editor#6238 Signed-off-by: Filip Dutescu <filip.dutescu@gmail.com>
1 parent 9ec135d commit 81a61b2

7 files changed

Lines changed: 172 additions & 55 deletions

File tree

helix-dap/src/types.rs

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use serde::{Deserialize, Serialize};
22
use serde_json::Value;
33
use std::collections::HashMap;
4+
use std::convert::TryFrom;
45
use std::path::PathBuf;
56

67
#[derive(
@@ -22,6 +23,64 @@ pub trait Request {
2223
const COMMAND: &'static str;
2324
}
2425

26+
#[derive(Copy, Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
27+
pub struct Line(u32);
28+
29+
impl TryFrom<Line> for usize {
30+
type Error = ();
31+
32+
fn try_from(value: Line) -> Result<Self, Self::Error> {
33+
(value.0 as usize).checked_sub(1).ok_or(())
34+
}
35+
}
36+
37+
impl TryFrom<usize> for Line {
38+
type Error = ();
39+
40+
fn try_from(value: usize) -> Result<Self, Self::Error> {
41+
if let Some(value) = (value as u32).checked_add(1) {
42+
Ok(Line(value))
43+
} else {
44+
Err(())
45+
}
46+
}
47+
}
48+
49+
impl TryFrom<Line> for u32 {
50+
type Error = ();
51+
52+
fn try_from(value: Line) -> Result<Self, Self::Error> {
53+
value.0.checked_sub(1).ok_or(())
54+
}
55+
}
56+
57+
#[derive(Copy, Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
58+
pub struct Column(u32);
59+
60+
impl Column {
61+
pub fn saturating_sub(self, rhs: u32) -> Self {
62+
Self(self.0.saturating_sub(rhs))
63+
}
64+
}
65+
66+
impl From<usize> for Column {
67+
fn from(column: usize) -> Self {
68+
Column(column as u32)
69+
}
70+
}
71+
72+
impl From<Column> for usize {
73+
fn from(column: Column) -> Self {
74+
column.0 as usize
75+
}
76+
}
77+
78+
impl From<Column> for u32 {
79+
fn from(column: Column) -> Self {
80+
column.0
81+
}
82+
}
83+
2584
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
2685
#[serde(rename_all = "camelCase")]
2786
pub struct ColumnDescriptor {
@@ -162,9 +221,9 @@ pub struct Source {
162221
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
163222
#[serde(rename_all = "camelCase")]
164223
pub struct SourceBreakpoint {
165-
pub line: usize,
224+
pub line: Line,
166225
#[serde(skip_serializing_if = "Option::is_none")]
167-
pub column: Option<usize>,
226+
pub column: Option<Column>,
168227
#[serde(skip_serializing_if = "Option::is_none")]
169228
pub condition: Option<String>,
170229
#[serde(skip_serializing_if = "Option::is_none")]
@@ -184,13 +243,13 @@ pub struct Breakpoint {
184243
#[serde(skip_serializing_if = "Option::is_none")]
185244
pub source: Option<Source>,
186245
#[serde(skip_serializing_if = "Option::is_none")]
187-
pub line: Option<usize>,
246+
pub line: Option<Line>,
188247
#[serde(skip_serializing_if = "Option::is_none")]
189-
pub column: Option<usize>,
248+
pub column: Option<Column>,
190249
#[serde(skip_serializing_if = "Option::is_none")]
191-
pub end_line: Option<usize>,
250+
pub end_line: Option<Line>,
192251
#[serde(skip_serializing_if = "Option::is_none")]
193-
pub end_column: Option<usize>,
252+
pub end_column: Option<Column>,
194253
#[serde(skip_serializing_if = "Option::is_none")]
195254
pub instruction_reference: Option<String>,
196255
#[serde(skip_serializing_if = "Option::is_none")]
@@ -223,12 +282,12 @@ pub struct StackFrame {
223282
pub name: String,
224283
#[serde(skip_serializing_if = "Option::is_none")]
225284
pub source: Option<Source>,
226-
pub line: usize,
227-
pub column: usize,
285+
pub line: Line,
286+
pub column: Column,
228287
#[serde(skip_serializing_if = "Option::is_none")]
229-
pub end_line: Option<usize>,
288+
pub end_line: Option<Line>,
230289
#[serde(skip_serializing_if = "Option::is_none")]
231-
pub end_column: Option<usize>,
290+
pub end_column: Option<Column>,
232291
#[serde(skip_serializing_if = "Option::is_none")]
233292
pub can_restart: Option<bool>,
234293
#[serde(skip_serializing_if = "Option::is_none")]
@@ -261,13 +320,13 @@ pub struct Scope {
261320
#[serde(skip_serializing_if = "Option::is_none")]
262321
pub source: Option<Source>,
263322
#[serde(skip_serializing_if = "Option::is_none")]
264-
pub line: Option<usize>,
323+
pub line: Option<Line>,
265324
#[serde(skip_serializing_if = "Option::is_none")]
266-
pub column: Option<usize>,
325+
pub column: Option<Column>,
267326
#[serde(skip_serializing_if = "Option::is_none")]
268-
pub end_line: Option<usize>,
327+
pub end_line: Option<Line>,
269328
#[serde(skip_serializing_if = "Option::is_none")]
270-
pub end_column: Option<usize>,
329+
pub end_column: Option<Column>,
271330
}
272331

273332
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
@@ -820,9 +879,9 @@ pub mod events {
820879
#[serde(skip_serializing_if = "Option::is_none")]
821880
pub group: Option<String>,
822881
#[serde(skip_serializing_if = "Option::is_none")]
823-
pub line: Option<usize>,
882+
pub line: Option<Line>,
824883
#[serde(skip_serializing_if = "Option::is_none")]
825-
pub column: Option<usize>,
884+
pub column: Option<Column>,
826885
#[serde(skip_serializing_if = "Option::is_none")]
827886
pub variables_reference: Option<usize>,
828887
#[serde(skip_serializing_if = "Option::is_none")]

helix-term/src/application.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ fn setup_integration_logging() {
9999
))
100100
})
101101
.level(level)
102-
.chain(std::io::stdout())
102+
.chain(stdout())
103103
.apply();
104104
}
105105

helix-term/src/commands/dap.rs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates};
88
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
99
use helix_dap::{self as dap, Client};
1010
use helix_lsp::block_on;
11-
use helix_view::editor::Breakpoint;
11+
use helix_view::{editor::Breakpoint, handlers::dap::pos_to_dap_pos};
1212

1313
use serde_json::{to_value, Value};
1414
use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -82,8 +82,8 @@ fn thread_picker(
8282
let frame = frames.get(0)?;
8383
let path = frame.source.as_ref()?.path.clone()?;
8484
let pos = Some((
85-
frame.line.saturating_sub(1),
86-
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
85+
frame.line.try_into().unwrap_or(0),
86+
frame.end_line.unwrap_or(frame.line).try_into().unwrap_or(0),
8787
));
8888
Some((path.into(), pos))
8989
},
@@ -100,7 +100,9 @@ fn get_breakpoint_at_current_line(editor: &mut Editor) -> Option<(usize, Breakpo
100100
let line = doc.selection(view.id).primary().cursor_line(text);
101101
let path = doc.path()?;
102102
editor.breakpoints.get(path).and_then(|breakpoints| {
103-
let i = breakpoints.iter().position(|b| b.line == line);
103+
let i = breakpoints
104+
.iter()
105+
.position(|b| b.line.try_into().unwrap_or(0) == line);
104106
i.map(|i| (i, breakpoints[i].clone()))
105107
})
106108
}
@@ -396,25 +398,37 @@ pub fn dap_toggle_breakpoint(cx: &mut Context) {
396398
return;
397399
}
398400
};
399-
let text = doc.text().slice(..);
400-
let line = doc.selection(view.id).primary().cursor_line(text);
401-
dap_toggle_breakpoint_impl(cx, path, line);
401+
let dap_pos = pos_to_dap_pos(doc.text(), doc.selection(view.id).primary().head);
402+
dap_toggle_breakpoint_impl(
403+
cx,
404+
path,
405+
dap_pos.line as usize,
406+
Some(dap_pos.character as usize),
407+
);
402408
}
403409

404-
pub fn dap_toggle_breakpoint_impl(cx: &mut Context, path: PathBuf, line: usize) {
410+
pub fn dap_toggle_breakpoint_impl(
411+
cx: &mut Context,
412+
path: PathBuf,
413+
line: usize,
414+
column: Option<usize>,
415+
) {
405416
// TODO: need to map breakpoints over edits and update them?
406417
// we shouldn't really allow editing while debug is running though
407418

408419
let breakpoints = cx.editor.breakpoints.entry(path.clone()).or_default();
409420
// TODO: always keep breakpoints sorted and use binary search to determine insertion point
410-
if let Some(pos) = breakpoints
411-
.iter()
412-
.position(|breakpoint| breakpoint.line == line)
413-
{
421+
if let Some(pos) = breakpoints.iter().position(|breakpoint| {
422+
breakpoint.line.try_into().unwrap_or(0) == line
423+
&& breakpoint.column.map(|column| column.into()) == column
424+
}) {
414425
breakpoints.remove(pos);
415426
} else {
427+
log::error!("line is {line:?}");
428+
log::error!("column is {column:?}");
416429
breakpoints.push(Breakpoint {
417-
line,
430+
line: line.try_into().unwrap_or_default(),
431+
column: column.map(|column| column.into()),
418432
..Default::default()
419433
});
420434
}
@@ -755,8 +769,12 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
755769
(
756770
path.into(),
757771
Some((
758-
frame.line.saturating_sub(1),
759-
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
772+
frame.line.try_into().unwrap_or_default(),
773+
frame
774+
.end_line
775+
.unwrap_or(frame.line)
776+
.try_into()
777+
.unwrap_or_default(),
760778
)),
761779
)
762780
})

helix-term/src/ui/editor.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use helix_view::{
2424
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
2525
editor::{CompleteAction, CursorShapeConfig},
2626
graphics::{Color, CursorKind, Modifier, Rect, Style},
27+
handlers::dap::dap_pos_to_pos,
2728
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
2829
keyboard::{KeyCode, KeyModifiers},
2930
Document, Editor, Theme, View,
@@ -102,10 +103,9 @@ impl EditorView {
102103

103104
// Set DAP highlights, if needed.
104105
if let Some(frame) = editor.current_stack_frame() {
105-
let dap_line = frame.line.saturating_sub(1) as usize;
106106
let style = theme.get("ui.highlight.frameline");
107107
let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
108-
if pos.doc_line != dap_line {
108+
if pos.doc_line != usize::try_from(frame.line).unwrap_or_default() {
109109
return;
110110
}
111111
renderer.surface.set_style(
@@ -138,6 +138,34 @@ impl EditorView {
138138
highlights = Box::new(syntax::merge(highlights, diagnostic));
139139
}
140140

141+
// Set DAP highlights, with possibility to overwrite syntax highlights, if fg set.
142+
let highlights: Box<dyn Iterator<Item = HighlightEvent>> =
143+
if let Some(frame) = editor.current_stack_frame() {
144+
log::error!("Frame: {frame:?}");
145+
let dap_line_start = frame.line;
146+
let dap_line_end = frame.end_line.unwrap_or(dap_line_start);
147+
let dap_col_start = frame.column;
148+
let dap_col_end = frame.end_column.unwrap_or_default();
149+
150+
let dap_start = dap_pos_to_pos(doc.text(), dap_line_start, dap_col_start);
151+
let dap_end = dap_pos_to_pos(doc.text(), dap_line_end, dap_col_end);
152+
log::error!("Dap: {dap_start:?}->{dap_end:?}");
153+
let dap_start =
154+
dap_start.unwrap_or_else(|| usize::try_from(dap_line_start).unwrap_or(0));
155+
let dap_end = dap_end.unwrap_or_else(|| usize::try_from(dap_line_end).unwrap_or(0));
156+
157+
if let Some(dap_current_index) = theme.find_scope_index("ui.highlight.frameline") {
158+
Box::new(syntax::merge(
159+
highlights,
160+
vec![(dap_current_index, dap_start..dap_end)],
161+
))
162+
} else {
163+
highlights
164+
}
165+
} else {
166+
highlights
167+
};
168+
141169
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
142170
let highlights = syntax::merge(
143171
highlights,
@@ -418,7 +446,6 @@ impl EditorView {
418446
Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"),
419447
}
420448
.unwrap_or(base_cursor_scope);
421-
422449
let primary_cursor_scope = match mode {
423450
Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"),
424451
Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"),
@@ -1055,7 +1082,7 @@ impl EditorView {
10551082
view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
10561083
{
10571084
let line = doc.text().char_to_line(char_idx);
1058-
commands::dap_toggle_breakpoint_impl(cxt, path, line);
1085+
commands::dap_toggle_breakpoint_impl(cxt, path, line, None);
10591086
return EventResult::Consumed(None);
10601087
}
10611088
}

helix-view/src/editor.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::{
1010
view::ViewPosition,
1111
Align, Document, DocumentId, View, ViewId,
1212
};
13-
use dap::StackFrame;
13+
use dap::{Column, Line, StackFrame};
1414
use helix_vcs::DiffProviderRegistry;
1515

1616
use futures_util::stream::select_all::SelectAll;
@@ -770,8 +770,8 @@ pub struct Breakpoint {
770770
pub verified: bool,
771771
pub message: Option<String>,
772772

773-
pub line: usize,
774-
pub column: Option<usize>,
773+
pub line: Line,
774+
pub column: Option<Column>,
775775
pub condition: Option<String>,
776776
pub hit_condition: Option<String>,
777777
pub log_message: Option<String>,

helix-view/src/gutter.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ pub fn breakpoints<'doc>(
263263
}
264264
let breakpoint = breakpoints
265265
.iter()
266-
.find(|breakpoint| breakpoint.line == line)?;
266+
.find(|breakpoint| breakpoint.line.try_into().unwrap_or(0) == line)?;
267267

268268
let style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
269269
error.underline_style(UnderlineStyle::Line)
@@ -297,7 +297,7 @@ fn execution_pause_indicator<'doc>(
297297
}
298298

299299
let current_stack_frame = editor.current_stack_frame()?;
300-
if line != current_stack_frame.line - 1
300+
if line != current_stack_frame.line.try_into().unwrap_or(0)
301301
|| doc.path().is_none()
302302
|| current_stack_frame
303303
.source

0 commit comments

Comments
 (0)