diff --git a/helix-dap/src/types.rs b/helix-dap/src/types.rs index bbaf53a602c17..96bf6327b0622 100644 --- a/helix-dap/src/types.rs +++ b/helix-dap/src/types.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; +use std::convert::TryFrom; use std::path::PathBuf; #[derive( @@ -22,6 +23,64 @@ pub trait Request { const COMMAND: &'static str; } +#[derive(Copy, Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub struct Line(u32); + +impl TryFrom for usize { + type Error = (); + + fn try_from(value: Line) -> Result { + (value.0 as usize).checked_sub(1).ok_or(()) + } +} + +impl TryFrom for Line { + type Error = (); + + fn try_from(value: usize) -> Result { + if let Some(value) = (value as u32).checked_add(1) { + Ok(Line(value)) + } else { + Err(()) + } + } +} + +impl TryFrom for u32 { + type Error = (); + + fn try_from(value: Line) -> Result { + value.0.checked_sub(1).ok_or(()) + } +} + +#[derive(Copy, Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub struct Column(u32); + +impl Column { + pub fn saturating_sub(self, rhs: u32) -> Self { + Self(self.0.saturating_sub(rhs)) + } +} + +impl From for Column { + fn from(column: usize) -> Self { + Column(column as u32) + } +} + +impl From for usize { + fn from(column: Column) -> Self { + column.0 as usize + } +} + +impl From for u32 { + fn from(column: Column) -> Self { + column.0 + } +} + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ColumnDescriptor { @@ -162,9 +221,9 @@ pub struct Source { #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SourceBreakpoint { - pub line: usize, + pub line: Line, #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, + pub column: Option, #[serde(skip_serializing_if = "Option::is_none")] pub condition: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -184,13 +243,13 @@ pub struct Breakpoint { #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub line: Option, + pub line: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, + pub column: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub end_line: Option, + pub end_line: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub end_column: Option, + pub end_column: Option, #[serde(skip_serializing_if = "Option::is_none")] pub instruction_reference: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -223,12 +282,12 @@ pub struct StackFrame { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, - pub line: usize, - pub column: usize, + pub line: Line, + pub column: Column, #[serde(skip_serializing_if = "Option::is_none")] - pub end_line: Option, + pub end_line: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub end_column: Option, + pub end_column: Option, #[serde(skip_serializing_if = "Option::is_none")] pub can_restart: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -261,13 +320,13 @@ pub struct Scope { #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub line: Option, + pub line: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, + pub column: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub end_line: Option, + pub end_line: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub end_column: Option, + pub end_column: Option, } #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] @@ -820,9 +879,9 @@ pub mod events { #[serde(skip_serializing_if = "Option::is_none")] pub group: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub line: Option, + pub line: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, + pub column: Option, #[serde(skip_serializing_if = "Option::is_none")] pub variables_reference: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c7e939959ca42..f248e6aff9336 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -99,7 +99,7 @@ fn setup_integration_logging() { )) }) .level(level) - .chain(std::io::stdout()) + .chain(stdout()) .apply(); } diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index dac1e9d5258d3..dd31aba88dfc4 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -8,7 +8,7 @@ use dap::{StackFrame, Thread, ThreadStates}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; use helix_dap::{self as dap, Client}; use helix_lsp::block_on; -use helix_view::editor::Breakpoint; +use helix_view::{editor::Breakpoint, handlers::dap::pos_to_dap_pos}; use serde_json::{to_value, Value}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -82,8 +82,8 @@ fn thread_picker( let frame = frames.get(0)?; let path = frame.source.as_ref()?.path.clone()?; let pos = Some(( - frame.line.saturating_sub(1), - frame.end_line.unwrap_or(frame.line).saturating_sub(1), + frame.line.try_into().unwrap_or(0), + frame.end_line.unwrap_or(frame.line).try_into().unwrap_or(0), )); Some((path.into(), pos)) }, @@ -100,7 +100,9 @@ fn get_breakpoint_at_current_line(editor: &mut Editor) -> Option<(usize, Breakpo let line = doc.selection(view.id).primary().cursor_line(text); let path = doc.path()?; editor.breakpoints.get(path).and_then(|breakpoints| { - let i = breakpoints.iter().position(|b| b.line == line); + let i = breakpoints + .iter() + .position(|b| b.line.try_into().unwrap_or(0) == line); i.map(|i| (i, breakpoints[i].clone())) }) } @@ -396,25 +398,37 @@ pub fn dap_toggle_breakpoint(cx: &mut Context) { return; } }; - let text = doc.text().slice(..); - let line = doc.selection(view.id).primary().cursor_line(text); - dap_toggle_breakpoint_impl(cx, path, line); + let dap_pos = pos_to_dap_pos(doc.text(), doc.selection(view.id).primary().head); + dap_toggle_breakpoint_impl( + cx, + path, + dap_pos.line as usize, + Some(dap_pos.character as usize), + ); } -pub fn dap_toggle_breakpoint_impl(cx: &mut Context, path: PathBuf, line: usize) { +pub fn dap_toggle_breakpoint_impl( + cx: &mut Context, + path: PathBuf, + line: usize, + column: Option, +) { // TODO: need to map breakpoints over edits and update them? // we shouldn't really allow editing while debug is running though let breakpoints = cx.editor.breakpoints.entry(path.clone()).or_default(); // TODO: always keep breakpoints sorted and use binary search to determine insertion point - if let Some(pos) = breakpoints - .iter() - .position(|breakpoint| breakpoint.line == line) - { + if let Some(pos) = breakpoints.iter().position(|breakpoint| { + breakpoint.line.try_into().unwrap_or(0) == line + && breakpoint.column.map(|column| column.into()) == column + }) { breakpoints.remove(pos); } else { + log::error!("line is {line:?}"); + log::error!("column is {column:?}"); breakpoints.push(Breakpoint { - line, + line: line.try_into().unwrap_or_default(), + column: column.map(|column| column.into()), ..Default::default() }); } @@ -755,8 +769,12 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { ( path.into(), Some(( - frame.line.saturating_sub(1), - frame.end_line.unwrap_or(frame.line).saturating_sub(1), + frame.line.try_into().unwrap_or_default(), + frame + .end_line + .unwrap_or(frame.line) + .try_into() + .unwrap_or_default(), )), ) }) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 57b0b2b5f22f1..ad08eca7f5c52 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -24,6 +24,7 @@ use helix_view::{ document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, + handlers::dap::dap_pos_to_pos, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, @@ -102,10 +103,9 @@ impl EditorView { // Set DAP highlights, if needed. if let Some(frame) = editor.current_stack_frame() { - let dap_line = frame.line.saturating_sub(1) as usize; let style = theme.get("ui.highlight.frameline"); let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { - if pos.doc_line != dap_line { + if pos.doc_line != usize::try_from(frame.line).unwrap_or_default() { return; } renderer.surface.set_style( @@ -138,6 +138,34 @@ impl EditorView { highlights = Box::new(syntax::merge(highlights, diagnostic)); } + // Set DAP highlights, with possibility to overwrite syntax highlights, if fg set. + let highlights: Box> = + if let Some(frame) = editor.current_stack_frame() { + log::error!("Frame: {frame:?}"); + let dap_line_start = frame.line; + let dap_line_end = frame.end_line.unwrap_or(dap_line_start); + let dap_col_start = frame.column; + let dap_col_end = frame.end_column.unwrap_or_default(); + + let dap_start = dap_pos_to_pos(doc.text(), dap_line_start, dap_col_start); + let dap_end = dap_pos_to_pos(doc.text(), dap_line_end, dap_col_end); + log::error!("Dap: {dap_start:?}->{dap_end:?}"); + let dap_start = + dap_start.unwrap_or_else(|| usize::try_from(dap_line_start).unwrap_or(0)); + let dap_end = dap_end.unwrap_or_else(|| usize::try_from(dap_line_end).unwrap_or(0)); + + if let Some(dap_current_index) = theme.find_scope_index("ui.highlight.frameline") { + Box::new(syntax::merge( + highlights, + vec![(dap_current_index, dap_start..dap_end)], + )) + } else { + highlights + } + } else { + highlights + }; + let highlights: Box> = if is_focused { let highlights = syntax::merge( highlights, @@ -418,7 +446,6 @@ impl EditorView { Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"), } .unwrap_or(base_cursor_scope); - let primary_cursor_scope = match mode { Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"), Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"), @@ -1055,7 +1082,7 @@ impl EditorView { view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true) { let line = doc.text().char_to_line(char_idx); - commands::dap_toggle_breakpoint_impl(cxt, path, line); + commands::dap_toggle_breakpoint_impl(cxt, path, line, None); return EventResult::Consumed(None); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 68102994f2e0f..a2bb1d38c554d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -10,7 +10,7 @@ use crate::{ view::ViewPosition, Align, Document, DocumentId, View, ViewId, }; -use dap::StackFrame; +use dap::{Column, Line, StackFrame}; use helix_vcs::DiffProviderRegistry; use futures_util::stream::select_all::SelectAll; @@ -770,8 +770,8 @@ pub struct Breakpoint { pub verified: bool, pub message: Option, - pub line: usize, - pub column: Option, + pub line: Line, + pub column: Option, pub condition: Option, pub hit_condition: Option, pub log_message: Option, diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index e3b199c0b44bc..4a1116675194e 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -263,7 +263,7 @@ pub fn breakpoints<'doc>( } let breakpoint = breakpoints .iter() - .find(|breakpoint| breakpoint.line == line)?; + .find(|breakpoint| breakpoint.line.try_into().unwrap_or(0) == line)?; let style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { error.underline_style(UnderlineStyle::Line) @@ -297,7 +297,7 @@ fn execution_pause_indicator<'doc>( } let current_stack_frame = editor.current_stack_frame()?; - if line != current_stack_frame.line - 1 + if line != current_stack_frame.line.try_into().unwrap_or(0) || doc.path().is_none() || current_stack_frame .source diff --git a/helix-view/src/handlers/dap.rs b/helix-view/src/handlers/dap.rs index 107c29be578cf..80916f65619d7 100644 --- a/helix-view/src/handlers/dap.rs +++ b/helix-view/src/handlers/dap.rs @@ -1,9 +1,11 @@ use crate::editor::{Action, Breakpoint}; use crate::{align_view, Align, Editor}; use dap::requests::DisconnectArguments; +use dap::{Column, Line}; use helix_core::Selection; use helix_dap::{self as dap, Client, ConnectionType, Payload, Request, ThreadId}; -use helix_lsp::block_on; +use helix_lsp::util::{lsp_pos_to_pos, pos_to_lsp_pos}; +use helix_lsp::{block_on, OffsetEncoding, Position}; use log::warn; use std::fmt::Write; use std::path::PathBuf; @@ -19,12 +21,22 @@ macro_rules! debugger { } // general utils: -pub fn dap_pos_to_pos(doc: &helix_core::Rope, line: usize, column: usize) -> Option { - // 1-indexing to 0 indexing - let line = doc.try_line_to_char(line - 1).ok()?; - let pos = line + column.saturating_sub(1); - // TODO: this is probably utf-16 offsets - Some(pos) +pub fn dap_pos_to_pos(doc: &helix_core::Rope, line: Line, column: Column) -> Option { + if let Ok(line) = line.try_into() { + lsp_pos_to_pos( + doc, + Position::new(line, column.into()), + OffsetEncoding::Utf16, + ) + } else { + None + } +} + +pub fn pos_to_dap_pos(doc: &helix_core::Rope, pos: usize) -> Position { + let mut dap_pos = pos_to_lsp_pos(doc, pos, OffsetEncoding::Utf16); + dap_pos.line += 1; + dap_pos } pub async fn select_thread_id(editor: &mut Editor, thread_id: ThreadId, force: bool) { @@ -74,7 +86,9 @@ pub fn jump_to_stack_frame(editor: &mut Editor, frame: &helix_dap::StackFrame) { let start = dap_pos_to_pos(doc.text(), frame.line, frame.column).unwrap_or(0); let end = frame .end_line - .and_then(|end_line| dap_pos_to_pos(doc.text(), end_line, frame.end_column.unwrap_or(0))) + .and_then(|end_line| { + dap_pos_to_pos(doc.text(), end_line, frame.end_column.unwrap_or_default()) + }) .unwrap_or(start); let selection = Selection::single(start.min(text_end), end.min(text_end)); @@ -113,7 +127,8 @@ pub fn breakpoints_changed( let source_breakpoints = breakpoints .iter() .map(|breakpoint| helix_dap::SourceBreakpoint { - line: breakpoint.line + 1, // convert from 0-indexing to 1-indexing (TODO: could set debugger to 0-indexing on init) + line: breakpoint.line, + column: breakpoint.column, ..Default::default() }) .collect::>(); @@ -127,8 +142,7 @@ pub fn breakpoints_changed( breakpoint.message = dap_breakpoint.message; // TODO: handle breakpoint.message // TODO: verify source matches - breakpoint.line = dap_breakpoint.line.unwrap_or(0).saturating_sub(1); // convert to 0-indexing - // TODO: no unwrap + breakpoint.line = dap_breakpoint.line.unwrap(); breakpoint.column = dap_breakpoint.column; // TODO: verify end_linef/col instruction reference, offset } @@ -214,7 +228,7 @@ impl Editor { id: breakpoint.id, verified: breakpoint.verified, message: breakpoint.message, - line: breakpoint.line.unwrap().saturating_sub(1), // TODO: no unwrap + line: breakpoint.line.unwrap(), // TODO: no unwrap column: breakpoint.column, ..Default::default() }); @@ -227,8 +241,7 @@ impl Editor { { breakpoints[i].verified = breakpoint.verified; breakpoints[i].message = breakpoint.message.clone(); - breakpoints[i].line = - breakpoint.line.unwrap().saturating_sub(1); // TODO: no unwrap + breakpoints[i].line = breakpoint.line.unwrap_or_default(); // TODO: no unwrap breakpoints[i].column = breakpoint.column; } }