on master help sign in
Implement indent-aware delete (#1120)
5959356WindSoilder
helix-term/src/commands.rs+61−9
@@ -10,10 +10,11 @@ use helix_core::{
10 object, pos_at_coords,
10 object, pos_at_coords,
11 regex::{self, Regex, RegexBuilder},
11 regex::{self, Regex, RegexBuilder},
12 register::Register,
12 register::Register,
13− search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
13+ search, selection, surround, textobject,
14+ unicode::width::UnicodeWidthChar,
14− RopeSlice, Selection, SmallVec, Tendril, Transaction,
15+ LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
16+ Transaction,
15};
17};
16−
17use helix_view::{
18use helix_view::{
18 clipboard::ClipboardType,
19 clipboard::ClipboardType,
19 document::{Mode, SCRATCH_BUFFER_NAME},
20 document::{Mode, SCRATCH_BUFFER_NAME},
@@ -4014,19 +4015,70 @@ pub mod insert {
4014 doc.apply(&transaction, view.id);
4015 doc.apply(&transaction, view.id);
4015 }
4016 }
4016
4017
4017− // TODO: handle indent-aware delete
4018 pub fn delete_char_backward(cx: &mut Context) {
4018 pub fn delete_char_backward(cx: &mut Context) {
4019 let count = cx.count();
4019 let count = cx.count();
4020 let (view, doc) = current!(cx.editor);
4020 let (view, doc) = current!(cx.editor);
4021 let text = doc.text().slice(..);
4021 let text = doc.text().slice(..);
4022+ let indent_unit = doc.indent_unit();
4023+ let tab_size = doc.tab_width();
4024+
4022 let transaction =
4025 let transaction =
4023 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
4026 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
4024 let pos = range.cursor(text);
4027 let pos = range.cursor(text);
4025− (
4028+ let line_start_pos = text.line_to_char(range.cursor_line(text));
4029+ // considier to delete by indent level if all characters before `pos` are indent units.
4030+ let fragment = Cow::from(text.slice(line_start_pos..pos));
4031+ if !fragment.is_empty() && fragment.chars().all(|ch| ch.is_whitespace()) {
4032+ if text.get_char(pos.saturating_sub(1)) == Some('\t') {
4033+ // fast path, delete one char
4034+ (
4026− graphemes::nth_prev_grapheme_boundary(text, pos, count),
4035+ graphemes::nth_prev_grapheme_boundary(text, pos, 1),
4027− pos,
4036+ pos,
4037+ None,
4038+ )
4039+ } else {
4040+ let unit_len = indent_unit.chars().count();
4041+ // NOTE: indent_unit always contains 'only spaces' or 'only tab' according to `IndentStyle` definition.
4042+ let unit_size = if indent_unit.starts_with('\t') {
4043+ tab_size * unit_len
4044+ } else {
4045+ unit_len
4046+ };
4047+ let width: usize = fragment
4048+ .chars()
4049+ .map(|ch| {
4050+ if ch == '\t' {
4051+ tab_size
4052+ } else {
4053+ // it can be none if it still meet control characters other than '\t'
4054+ // here just set the width to 1 (or some value better?).
4055+ ch.width().unwrap_or(1)
4056+ }
4057+ })
4058+ .sum();
4059+ let mut drop = width % unit_size; // round down to nearest unit
4060+ if drop == 0 {
4061+ drop = unit_size
4062+ }; // if it's already at a unit, consume a whole unit
4063+ let mut chars = fragment.chars().rev();
4064+ let mut start = pos;
4065+ for _ in 0..drop {
4066+ // delete up to `drop` spaces
4067+ match chars.next() {
4068+ Some(' ') => start -= 1,
4069+ _ => break,
4070+ }
4071+ }
4072+ (start, pos, None) // delete!
4073+ }
4074+ } else {
4075+ // delete char
4076+ (
4077+ graphemes::nth_prev_grapheme_boundary(text, pos, count),
4078+ pos,
4028− None,
4079+ None,
4029− )
4080+ )
4081+ }
4030 });
4082 });
4031 doc.apply(&transaction, view.id);
4083 doc.apply(&transaction, view.id);
4032 }
4084 }