diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index 1b3de6ea08bd..de30876efa2b 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -1,7 +1,7 @@ //! When typing the opening character of one of the possible pairs defined below, //! this module provides the functionality to insert the paired closing character. -use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction}; +use crate::{movement::Direction, Change, Range, Rope, Selection, Tendril, Transaction}; use log::debug; use smallvec::SmallVec; @@ -34,9 +34,7 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20 // middle of triple quotes, and more exotic pairs like Jinja's {% %} #[must_use] -pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option { - debug!("autopairs hook selection: {:#?}", selection); - +pub fn hook_insert(doc: &Rope, selection: &Selection, ch: char) -> Option { for &(open, close) in PAIRS { if open == ch { if open == close { @@ -214,6 +212,23 @@ fn handle_same( t } +pub fn hook_delete(doc: &Rope, range: &Range) -> Option { + let cursor = range.cursor(doc.slice(..)); + let next_char = doc.get_char(cursor)?; + let prev_char = prev_char(doc, cursor)?; + let mut change = None; + + for (open, close) in PAIRS { + if prev_char == *open && next_char == *close { + change = Some((cursor - open.len_utf8(), cursor + close.len_utf8(), None)); + break; + } + } + + debug!("hook delete change: {:#?}", change); + change +} + #[cfg(test)] mod test { use super::*; @@ -234,7 +249,7 @@ mod test { expected_doc: &Rope, expected_sel: &Selection, ) { - let trans = hook(&in_doc, &in_sel, ch).unwrap(); + let trans = hook_insert(&in_doc, &in_sel, ch).unwrap(); let mut actual_doc = in_doc.clone(); assert!(trans.apply(&mut actual_doc)); assert_eq!(expected_doc, &actual_doc); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ee6a59894abb..110ad80db28f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4263,13 +4263,13 @@ pub mod insert { Some(transaction) } - use helix_core::auto_pairs; + use helix_core::{auto_pairs, Change}; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current!(cx.editor); let hooks: &[Hook] = match cx.editor.config.auto_pairs { - true => &[auto_pairs::hook, insert], + true => &[auto_pairs::hook_insert, insert], false => &[insert], }; @@ -4369,79 +4369,97 @@ pub mod insert { }); transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); - // - doc.apply(&transaction, view.id); } pub fn delete_char_backward(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let indent_unit = doc.indent_unit(); - let tab_size = doc.tab_width(); + let text = doc.text(); let transaction = Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { - let pos = range.cursor(text); - let line_start_pos = text.line_to_char(range.cursor_line(text)); - // considier to delete by indent level if all characters before `pos` are indent units. - let fragment = Cow::from(text.slice(line_start_pos..pos)); - if !fragment.is_empty() && fragment.chars().all(|ch| ch.is_whitespace()) { - if text.get_char(pos.saturating_sub(1)) == Some('\t') { - // fast path, delete one char + handle_backspace_dedent(doc, range) + .or_else(|| { + if !cx.editor.config.auto_pairs { + None + } else { + auto_pairs::hook_delete(text, range) + } + }) + .unwrap_or_else(|| { + let text = text.slice(..); + let pos = range.cursor(text); + ( - graphemes::nth_prev_grapheme_boundary(text, pos, 1), + graphemes::nth_prev_grapheme_boundary(text, pos, count), pos, None, ) - } else { - let unit_len = indent_unit.chars().count(); - // NOTE: indent_unit always contains 'only spaces' or 'only tab' according to `IndentStyle` definition. - let unit_size = if indent_unit.starts_with('\t') { - tab_size * unit_len - } else { - unit_len - }; - let width: usize = fragment - .chars() - .map(|ch| { - if ch == '\t' { - tab_size - } else { - // it can be none if it still meet control characters other than '\t' - // here just set the width to 1 (or some value better?). - ch.width().unwrap_or(1) - } - }) - .sum(); - let mut drop = width % unit_size; // round down to nearest unit - if drop == 0 { - drop = unit_size - }; // if it's already at a unit, consume a whole unit - let mut chars = fragment.chars().rev(); - let mut start = pos; - for _ in 0..drop { - // delete up to `drop` spaces - match chars.next() { - Some(' ') => start -= 1, - _ => break, - } - } - (start, pos, None) // delete! - } - } else { - // delete char - ( - graphemes::nth_prev_grapheme_boundary(text, pos, count), - pos, - None, - ) - } + }) }); + doc.apply(&transaction, view.id); } + fn handle_backspace_dedent(doc: &Document, range: &Range) -> Option { + let text = doc.text().slice(..); + let indent_unit = doc.indent_unit(); + let tab_size = doc.tab_width(); + let pos = range.cursor(text); + + let line_start_pos = text.line_to_char(range.cursor_line(text)); + // considier to delete by indent level if all characters before `pos` are indent units. + let fragment = Cow::from(text.slice(line_start_pos..pos)); + + if fragment.is_empty() || !fragment.chars().all(|ch| ch.is_whitespace()) { + return None; + } + + // fast path, return None to delete one char with default handling + if text.get_char(pos.saturating_sub(1)) == Some('\t') { + return None; + } + + let unit_len = indent_unit.chars().count(); + + // NOTE: indent_unit always contains 'only spaces' or 'only tab' according to `IndentStyle` definition. + let unit_size = if indent_unit.starts_with('\t') { + tab_size * unit_len + } else { + unit_len + }; + + let width: usize = fragment + .chars() + .map(|ch| { + if ch == '\t' { + tab_size + } else { + // it can be none if it still meet control characters other than '\t' + // here just set the width to 1 (or some value better?). + ch.width().unwrap_or(1) + } + }) + .sum(); + + let mut drop = width % unit_size; // round down to nearest unit + if drop == 0 { + drop = unit_size + }; // if it's already at a unit, consume a whole unit + let mut chars = fragment.chars().rev(); + let mut start = pos; + for _ in 0..drop { + // delete up to `drop` spaces + match chars.next() { + Some(' ') => start -= 1, + _ => break, + } + } + + Some((start, pos, None)) // delete! + } + pub fn delete_char_forward(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor);