diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 0b75d2a586f1..52d77a9aace5 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -39,6 +39,10 @@ pub enum DiagnosticTag { #[derive(Debug, Clone)] pub struct Diagnostic { pub range: Range, + // whether this diagnostic ends at the end of(or inside) a word + pub ends_at_word: bool, + pub starts_at_word: bool, + pub zero_width: bool, pub line: usize, pub message: String, pub severity: Option, diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 9d2a3e5c43a5..263ba9ccc770 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -1,6 +1,6 @@ use smallvec::SmallVec; -use crate::{Range, Rope, Selection, Tendril}; +use crate::{chars::char_is_word, Range, Rope, Selection, Tendril}; use std::{borrow::Cow, iter::once}; /// (from, to, replacement) @@ -22,6 +22,30 @@ pub enum Operation { pub enum Assoc { Before, After, + /// Acts like `After` if a word character is inserted + /// after the position, otherwise acts like `Before` + AfterWord, + /// Acts like `Before` if a word character is inserted + /// before the position, otherwise acts like `After` + BeforeWord, +} + +impl Assoc { + /// Whether to stick to gaps + fn stay_at_gaps(self) -> bool { + !matches!(self, Self::BeforeWord | Self::AfterWord) + } + + fn insert_offset(self, s: &str) -> usize { + let chars = s.chars().count(); + match self { + Assoc::After => chars, + Assoc::AfterWord => s.chars().take_while(|&c| char_is_word(c)).count(), + // return position before inserted text + Assoc::Before => 0, + Assoc::BeforeWord => chars - s.chars().rev().take_while(|&c| char_is_word(c)).count(), + } + } } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -411,8 +435,6 @@ impl ChangeSet { map!(|pos, _| (old_end > pos).then_some(new_pos), i); } Insert(s) => { - let ins = s.chars().count(); - // a subsequent delete means a replace, consume it if let Some((_, Delete(len))) = iter.peek() { iter.next(); @@ -420,13 +442,13 @@ impl ChangeSet { old_end = old_pos + len; // in range of replaced text map!( - |pos, assoc| (old_end > pos).then(|| { + |pos, assoc: Assoc| (old_end > pos).then(|| { // at point or tracking before - if pos == old_pos || assoc == Assoc::Before { + if pos == old_pos && assoc.stay_at_gaps() { new_pos } else { // place to end of insert - new_pos + ins + new_pos + assoc.insert_offset(s) } }), i @@ -434,20 +456,15 @@ impl ChangeSet { } else { // at insert point map!( - |pos, assoc| (old_pos == pos).then(|| { + |pos, assoc: Assoc| (old_pos == pos).then(|| { // return position before inserted text - if assoc == Assoc::Before { - new_pos - } else { - // after text - new_pos + ins - } + new_pos + assoc.insert_offset(s) }), i ); } - new_pos += ins; + new_pos += s.chars().count(); } } old_pos = old_end; @@ -880,6 +897,48 @@ mod test { let mut positions = [4, 2]; cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After))); assert_eq!(positions, [4, 2]); + // stays at word boundary + let cs = ChangeSet { + changes: vec![ + Retain(2), // + Insert(" ab".into()), + Retain(2), // cd + Insert("de ".into()), + ], + len: 4, + len_after: 10, + }; + assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 3); + assert_eq!(cs.map_pos(4, Assoc::AfterWord), 9); + let cs = ChangeSet { + changes: vec![ + Retain(1), // + Insert(" b".into()), + Delete(1), // c + Retain(1), // d + Insert("e ".into()), + Delete(1), // + ], + len: 5, + len_after: 7, + }; + assert_eq!(cs.map_pos(1, Assoc::BeforeWord), 2); + assert_eq!(cs.map_pos(3, Assoc::AfterWord), 5); + let cs = ChangeSet { + changes: vec![ + Retain(1), // + Insert("a".into()), + Delete(2), // b + Retain(1), // d + Insert("e".into()), + Delete(1), // f + Retain(1), // + ], + len: 5, + len_after: 7, + }; + assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 1); + assert_eq!(cs.map_pos(4, Assoc::AfterWord), 4); } #[test] diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3baf1ce4b7c3..f8140f986cdd 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,6 +1,7 @@ use arc_swap::{access::Map, ArcSwap}; use futures_util::Stream; use helix_core::{ + chars::char_is_word, diagnostic::{DiagnosticTag, NumberOrString}, path::get_relative_path, pos_at_coords, syntax, Selection, @@ -832,7 +833,6 @@ impl Application { log::warn!("lsp position out of bounds - {:?}", diagnostic); return None; }; - let severity = diagnostic.severity.map(|severity| match severity { DiagnosticSeverity::ERROR => Error, @@ -884,8 +884,17 @@ impl Application { Vec::new() }; + let ends_at_word = start != end + && end != 0 + && text.get_char(end - 1).map_or(false, char_is_word); + let starts_at_word = start != end + && text.get_char(start).map_or(false, char_is_word); + Some(Diagnostic { range: Range { start, end }, + ends_at_word, + starts_at_word, + zero_width: start == end, line: diagnostic.range.start.line as usize, message: diagnostic.message.clone(), severity, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index ebd621d2a3b7..654e726d3161 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1174,20 +1174,32 @@ impl Document { let changes = transaction.changes(); - changes.update_positions( - self.diagnostics - .iter_mut() - .map(|diagnostic| (&mut diagnostic.range.start, Assoc::After)), - ); - changes.update_positions( - self.diagnostics - .iter_mut() - .map(|diagnostic| (&mut diagnostic.range.end, Assoc::After)), - ); - // map state.diagnostics over changes::map_pos too - for diagnostic in &mut self.diagnostics { + // map diagnostics over changes too + changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| { + let assoc = if diagnostic.starts_at_word { + Assoc::BeforeWord + } else { + Assoc::After + }; + (&mut diagnostic.range.start, assoc) + })); + changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| { + let assoc = if diagnostic.ends_at_word { + Assoc::AfterWord + } else { + Assoc::Before + }; + (&mut diagnostic.range.end, assoc) + })); + self.diagnostics.retain_mut(|diagnostic| { + if diagnostic.range.start > diagnostic.range.end + || (!diagnostic.zero_width && diagnostic.range.start == diagnostic.range.end) + { + return false; + } diagnostic.line = self.text.char_to_line(diagnostic.range.start); - } + true + }); self.diagnostics .sort_unstable_by_key(|diagnostic| diagnostic.range);