Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto pairs delete #1379

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions helix-core/src/auto_pairs.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<Transaction> {
debug!("autopairs hook selection: {:#?}", selection);

pub fn hook_insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
for &(open, close) in PAIRS {
if open == ch {
if open == close {
Expand Down Expand Up @@ -214,6 +212,23 @@ fn handle_same(
t
}

pub fn hook_delete(doc: &Rope, range: &Range) -> Option<Change> {
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::*;
Expand All @@ -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);
Expand Down
134 changes: 76 additions & 58 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
};

Expand Down Expand Up @@ -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<Change> {
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);
Expand Down