1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//! Insert mode: text in, Esc out. Every session is recorded so
//! dot-repeat can replay both the command and the inserted text.
use super::{Editor, Key, Mode};
impl Editor {
/// One-level dedent when the line so far is whitespace-only (called
/// before inserting a closer). No-op when there is real text or no
/// indent to give back.
fn dedent_for_closer(&mut self) {
let line = self.buf().line_of(self.cursor);
let start = self.buf().line_start(line);
let col = self.buf().col_of(self.cursor);
let text = self.buf().line_text(line);
let before: String = text.chars().take(col).collect();
if !before.chars().all(|c| c == ' ' || c == '\t') {
return; // real text before the cursor — never reindent
}
let width = self.config.tab_size;
let mut strip = 0;
while strip < width && start + strip < self.cursor && self.buf().byte(start + strip) == b' '
{
strip += 1;
}
if strip == 0 {
return;
}
self.buf_mut()
.delete(strop_core::Range::charwise(start, start + strip));
self.cursor = self.cursor.saturating_sub(strip);
// dot-repeat: the dedent belongs to the same change; the recorded
// insert text keeps literal content, replay re-derives via the
// same smartindent path
}
/// Indent for a new line at the cursor (0001 daily-driver): copy the
/// current line's leading whitespace, plus one level after an opener
/// (`{[`(` c-like, `:` python-ish). Configurable width (config.toml).
pub(crate) fn auto_indent(&self) -> String {
let line = self.buf().line_of(self.cursor);
let text = self.buf().line_text(line);
let before_cursor = &text[..self.buf().col_of(self.cursor).min(text.len())];
let base: String = before_cursor
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect();
let trimmed = before_cursor.trim_end();
let deeper = trimmed.ends_with('{')
|| trimmed.ends_with('[')
|| trimmed.ends_with('(')
|| trimmed.ends_with(':');
let mut indent = base;
if deeper {
indent.push_str(&self.config.indent());
}
indent
}
/// Indent for o/O: the current line's full leading whitespace,
/// deepened after an opener even mid-line.
pub(crate) fn auto_indent_full_line(&self) -> String {
let line = self.buf().line_of(self.cursor);
let text = self.buf().line_text(line);
let base: String = text
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect();
let trimmed = text.trim_end();
let deeper = trimmed.ends_with('{')
|| trimmed.ends_with('[')
|| trimmed.ends_with('(')
|| trimmed.ends_with(':');
let mut indent = base;
if deeper {
indent.push_str(&self.config.indent());
}
indent
}
/// Enter insert mode and start recording (dot-repeat, 0001 §2.1).
/// `keys` is what got us here (`i`, `o`, `ci[`, …).
pub(crate) fn enter_insert_from(&mut self, keys: &str) {
// an insert session is one undo unit (with the change op, if any, that
// opened it); plain entries open a fresh transaction
self.tx_begin();
self.mode = Mode::Insert;
self.recording_insert = Some(String::new());
if self.last_cmd_keys.is_empty()
|| !matches!(keys.chars().next(), Some('c') if keys.len() > 1)
{
// plain entries remember their own key; change ops already set
// last_cmd_keys in execute()
if matches!(keys, "i" | "a" | "A" | "o" | "O" | "v..." | "V...") {
self.last_cmd_keys = keys.trim_end_matches("...").into();
}
}
}
pub(crate) fn feed_insert(&mut self, key: Key) {
match key {
Key::CtrlW | Key::CtrlX => {}
Key::Esc => {
self.tx_commit(); // the insert session is one undo unit
self.mode = Mode::Normal;
self.cursor = self.cursor.saturating_sub(1);
for c in &mut self.extra_cursors {
*c = c.saturating_sub(1);
}
self.clamp_cursor();
self.normalize_cursors();
if let Some(rec) = self.recording_insert.take() {
self.last_insert = Some(rec);
}
}
Key::Backspace => {
// cascade: every cursor deletes one char back, bottom-up
// so positions never shift mid-batch (0013 §3)
let mut positions = self.all_cursors();
positions.sort_unstable();
positions.dedup();
positions.retain(|&p| p > 0); // cursors at 0 can't delete
for &pos in positions.iter().rev() {
self.buf_mut()
.delete(strop_core::Range::charwise(pos - 1, pos));
}
if !positions.is_empty() {
self.remap_after_mirrored_edit(&positions, -1);
if let Some(rec) = &mut self.recording_insert {
rec.pop();
}
}
}
Key::Enter => {
let indent = self.auto_indent();
let text = format!("\n{indent}");
let mut positions = self.all_cursors();
positions.sort_unstable();
positions.dedup(); // stacked cursors edit once
for &pos in positions.iter().rev() {
self.buf_mut().insert(pos, &text);
}
self.remap_after_mirrored_edit(&positions, text.len() as isize);
if let Some(rec) = &mut self.recording_insert {
rec.push('\n');
rec.push_str(&indent);
}
}
Key::CtrlR | Key::Up | Key::Down | Key::Tab | Key::Backtab => {}
Key::Char(c) => {
// smartindent (vim/helix behavior): a closer typed on an
// indent-only line dedents one level first — typing `}`
// after the auto-indent deepens never strands you
if matches!(c, '}' | ']' | ')') {
self.dedent_for_closer();
}
let mut tmp = [0u8; 4];
let encoded = c.encode_utf8(&mut tmp).to_string();
let mut positions = self.all_cursors();
positions.sort_unstable();
positions.dedup(); // stacked cursors edit once
for &pos in positions.iter().rev() {
self.buf_mut().insert(pos, &encoded);
}
self.remap_after_mirrored_edit(&positions, c.len_utf8() as isize);
if let Some(rec) = &mut self.recording_insert {
rec.push(c);
}
}
}
}
}