on master help sign in
Implement indent-aware delete (#1120)

* delete character backward can make undent behavior * improve to handle mixed indentation

commit5959356a2404a8c317d07934ee388d6637c2888a authorWindSoilder <WindSoilder@outlook.com> dateThu 18 Nov 2021, 23:19 (5 years ago) parentbd56dde
1 file changed+61−9
helix-term/src/commands.rs+61−9
helix-term/src/commands.rs+61−9
@@ -10,10 +10,11 @@ use helix_core::{
1010 object, pos_at_coords,
1111 regex::{self, Regex, RegexBuilder},
1212 register::Register,
13− search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
14− RopeSlice, Selection, SmallVec, Tendril, Transaction,
13+ search, selection, surround, textobject,
14+ unicode::width::UnicodeWidthChar,
15+ LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
16+ Transaction,
1517};
16−
1718use helix_view::{
1819 clipboard::ClipboardType,
1920 document::{Mode, SCRATCH_BUFFER_NAME},
@@ -4014,19 +4015,70 @@ pub mod insert {
40144015 doc.apply(&transaction, view.id);
40154016 }
40164017
4017− // TODO: handle indent-aware delete
40184018 pub fn delete_char_backward(cx: &mut Context) {
40194019 let count = cx.count();
40204020 let (view, doc) = current!(cx.editor);
40214021 let text = doc.text().slice(..);
4022+ let indent_unit = doc.indent_unit();
4023+ let tab_size = doc.tab_width();
4024+
40224025 let transaction =
40234026 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
40244027 let pos = range.cursor(text);
4025− (
4026− graphemes::nth_prev_grapheme_boundary(text, pos, count),
4027− pos,
4028− None,
4029− )
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+ (
4035+ graphemes::nth_prev_grapheme_boundary(text, pos, 1),
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,
4079+ None,
4080+ )
4081+ }
40304082 });
40314083 doc.apply(&transaction, view.id);
40324084 }