Skip to content

Commit 0f6c82c

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 0f6c82c

8 files changed

Lines changed: 216 additions & 70 deletions

File tree

helix-dap/src/types.rs

Lines changed: 87 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,19 +243,31 @@ 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")]
197256
pub offset: Option<usize>,
198257
}
199258

259+
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
260+
#[serde(rename_all = "camelCase")]
261+
pub struct BreakpointLocation {
262+
pub line: Line,
263+
#[serde(skip_serializing_if = "Option::is_none")]
264+
pub column: Option<Column>,
265+
#[serde(skip_serializing_if = "Option::is_none")]
266+
pub end_line: Option<Line>,
267+
#[serde(skip_serializing_if = "Option::is_none")]
268+
pub end_column: Option<Column>,
269+
}
270+
200271
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
201272
#[serde(rename_all = "camelCase")]
202273
pub struct StackFrameFormat {
@@ -223,12 +294,12 @@ pub struct StackFrame {
223294
pub name: String,
224295
#[serde(skip_serializing_if = "Option::is_none")]
225296
pub source: Option<Source>,
226-
pub line: usize,
227-
pub column: usize,
297+
pub line: Line,
298+
pub column: Column,
228299
#[serde(skip_serializing_if = "Option::is_none")]
229-
pub end_line: Option<usize>,
300+
pub end_line: Option<Line>,
230301
#[serde(skip_serializing_if = "Option::is_none")]
231-
pub end_column: Option<usize>,
302+
pub end_column: Option<Column>,
232303
#[serde(skip_serializing_if = "Option::is_none")]
233304
pub can_restart: Option<bool>,
234305
#[serde(skip_serializing_if = "Option::is_none")]
@@ -261,13 +332,13 @@ pub struct Scope {
261332
#[serde(skip_serializing_if = "Option::is_none")]
262333
pub source: Option<Source>,
263334
#[serde(skip_serializing_if = "Option::is_none")]
264-
pub line: Option<usize>,
335+
pub line: Option<Line>,
265336
#[serde(skip_serializing_if = "Option::is_none")]
266-
pub column: Option<usize>,
337+
pub column: Option<Column>,
267338
#[serde(skip_serializing_if = "Option::is_none")]
268-
pub end_line: Option<usize>,
339+
pub end_line: Option<Line>,
269340
#[serde(skip_serializing_if = "Option::is_none")]
270-
pub end_column: Option<usize>,
341+
pub end_column: Option<Column>,
271342
}
272343

273344
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
@@ -820,9 +891,9 @@ pub mod events {
820891
#[serde(skip_serializing_if = "Option::is_none")]
821892
pub group: Option<String>,
822893
#[serde(skip_serializing_if = "Option::is_none")]
823-
pub line: Option<usize>,
894+
pub line: Option<Line>,
824895
#[serde(skip_serializing_if = "Option::is_none")]
825-
pub column: Option<usize>,
896+
pub column: Option<Column>,
826897
#[serde(skip_serializing_if = "Option::is_none")]
827898
pub variables_reference: Option<usize>,
828899
#[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: 39 additions & 18 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,
@@ -138,6 +139,34 @@ impl EditorView {
138139
highlights = Box::new(syntax::merge(highlights, diagnostic));
139140
}
140141

142+
// Set DAP highlights, with possibility to overwrite syntax highlights, if fg set.
143+
let highlights: Box<dyn Iterator<Item = HighlightEvent>> =
144+
if let Some(frame) = editor.current_stack_frame() {
145+
log::error!("Frame: {frame:?}");
146+
let dap_line_start = frame.line;
147+
let dap_line_end = frame.end_line.unwrap_or(dap_line_start);
148+
let dap_col_start = frame.column;
149+
let dap_col_end = frame.end_column.unwrap_or_default();
150+
151+
let dap_start = dap_pos_to_pos(doc.text(), dap_line_start, dap_col_start);
152+
let dap_end = dap_pos_to_pos(doc.text(), dap_line_end, dap_col_end);
153+
log::error!("Dap: {dap_start:?}->{dap_end:?}");
154+
let dap_start =
155+
dap_start.unwrap_or_else(|| usize::try_from(dap_line_start).unwrap_or(0));
156+
let dap_end = dap_end.unwrap_or_else(|| usize::try_from(dap_line_end).unwrap_or(0));
157+
158+
if let Some(dap_current_index) = theme.find_scope_index("ui.highlight.frameline") {
159+
Box::new(syntax::merge(
160+
highlights,
161+
vec![(dap_current_index, dap_start..dap_end)],
162+
))
163+
} else {
164+
highlights
165+
}
166+
} else {
167+
highlights
168+
};
169+
141170
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
142171
let highlights = syntax::merge(
143172
highlights,
@@ -411,20 +440,10 @@ impl EditorView {
411440
let base_primary_cursor_scope = theme
412441
.find_scope_index("ui.cursor.primary")
413442
.unwrap_or(base_cursor_scope);
414-
415-
let cursor_scope = match mode {
416-
Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"),
417-
Mode::Select => theme.find_scope_index_exact("ui.cursor.select"),
418-
Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"),
419-
}
420-
.unwrap_or(base_cursor_scope);
421-
422-
let primary_cursor_scope = match mode {
423-
Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"),
424-
Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"),
425-
Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"),
426-
}
427-
.unwrap_or(base_primary_cursor_scope);
443+
let cursor_scope = mode.cursor_scope(theme).unwrap_or(base_cursor_scope);
444+
let primary_cursor_scope = mode
445+
.primary_cursor_scope(theme)
446+
.unwrap_or(base_primary_cursor_scope);
428447

429448
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
430449
for (i, range) in selection.iter().enumerate() {
@@ -459,16 +478,14 @@ impl EditorView {
459478
} else {
460479
cursor_start
461480
};
481+
462482
spans.push((selection_scope, range.anchor..selection_end));
463483
if !selection_is_primary || cursor_is_block {
464484
spans.push((cursor_scope, cursor_start..range.head));
465485
}
466486
} else {
467487
// Reverse case.
468488
let cursor_end = next_grapheme_boundary(text, range.head);
469-
if !selection_is_primary || cursor_is_block {
470-
spans.push((cursor_scope, range.head..cursor_end));
471-
}
472489
// non block cursors look like they exclude the cursor
473490
let selection_start = if selection_is_primary
474491
&& !cursor_is_block
@@ -478,6 +495,10 @@ impl EditorView {
478495
} else {
479496
cursor_end
480497
};
498+
499+
if !selection_is_primary || cursor_is_block {
500+
spans.push((cursor_scope, range.head..cursor_end));
501+
}
481502
spans.push((selection_scope, selection_start..range.anchor));
482503
}
483504
}
@@ -1055,7 +1076,7 @@ impl EditorView {
10551076
view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
10561077
{
10571078
let line = doc.text().char_to_line(char_idx);
1058-
commands::dap_toggle_breakpoint_impl(cxt, path, line);
1079+
commands::dap_toggle_breakpoint_impl(cxt, path, line, None);
10591080
return EventResult::Consumed(None);
10601081
}
10611082
}

0 commit comments

Comments
 (0)