1use super::TextBuffer;
7use super::selection::VisualModeKind;
8use crate::buffer::util::is_word_char;
9use crate::buffer::{Pos, Selection};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TextObjectScope {
13 Inner,
14 Around,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DelimiterKind {
19 Parentheses,
20 Brackets,
21 Braces,
22 SingleQuotes,
23 DoubleQuotes,
24 Backticks,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum TextObjectKind {
29 Word,
30 BigWord,
31 Paragraph,
32 Delimiter(DelimiterKind),
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct TextObjectSpec {
37 pub scope: TextObjectScope,
38 pub kind: TextObjectKind,
39 pub count: usize,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct TextObjectEditPlan {
44 pub delete_ranges: Vec<(Pos, Pos)>,
45 pub text: String,
46 pub mode: VisualModeKind,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum RangeMode {
51 Char,
52 Line,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56struct TextObjectRange {
57 start: usize,
58 end: usize,
59 mode: RangeMode,
60}
61
62impl TextBuffer {
63 pub fn text_object_selection(
64 &self,
65 cursor: Pos,
66 spec: TextObjectSpec,
67 ) -> Option<(Selection, VisualModeKind)> {
68 let range = self.text_object_range(cursor, spec)?;
69
70 match range.mode {
71 RangeMode::Char => {
72 if range.start >= range.end {
73 return None;
74 }
75
76 Some((
77 Selection::new(
78 self.char_to_pos(range.start),
79 self.char_to_pos(range.end.saturating_sub(1)),
80 ),
81 VisualModeKind::Char,
82 ))
83 }
84 RangeMode::Line => {
85 let start_line = self.char_to_line(range.start);
86 let end_line = self.char_to_line(range.end.saturating_sub(1));
87 Some((
88 Selection::new(Pos::new(start_line, 0), Pos::new(end_line, 0)),
89 VisualModeKind::Line,
90 ))
91 }
92 }
93 }
94
95 pub fn text_object_edit_plan(
96 &self,
97 cursor: Pos,
98 spec: TextObjectSpec,
99 ) -> Option<TextObjectEditPlan> {
100 let range = self.text_object_range(cursor, spec)?;
101
102 Some(match range.mode {
103 RangeMode::Char => TextObjectEditPlan {
104 delete_ranges: vec![(self.char_to_pos(range.start), self.char_to_pos(range.end))],
105 text: self.slice_chars(range.start, range.end),
106 mode: VisualModeKind::Char,
107 },
108 RangeMode::Line => {
109 let start_line = self.char_to_line(range.start);
110 let end_line = self.char_to_line(range.end.saturating_sub(1));
111 let (start, end) = self.line_span_pos_range(start_line, end_line);
112 TextObjectEditPlan {
113 delete_ranges: vec![(start, end)],
114 text: self.line_span_text_linewise_register(start_line, end_line),
115 mode: VisualModeKind::Line,
116 }
117 }
118 })
119 }
120
121 fn text_object_range(&self, cursor: Pos, spec: TextObjectSpec) -> Option<TextObjectRange> {
122 match spec.kind {
123 TextObjectKind::Word => self.word_text_object_range(cursor, spec.scope, spec.count),
124 TextObjectKind::BigWord => {
125 self.big_word_text_object_range(cursor, spec.scope, spec.count)
126 }
127 TextObjectKind::Paragraph => {
128 self.paragraph_text_object_range(cursor, spec.scope, spec.count)
129 }
130 TextObjectKind::Delimiter(kind) => {
131 self.delimiter_text_object_range(cursor, kind, spec.scope, spec.count)
132 }
133 }
134 }
135
136 fn word_text_object_range(
137 &self,
138 cursor: Pos,
139 scope: TextObjectScope,
140 count: usize,
141 ) -> Option<TextObjectRange> {
142 self.run_text_object_range(cursor, scope, count, is_word_char)
143 }
144
145 fn big_word_text_object_range(
146 &self,
147 cursor: Pos,
148 scope: TextObjectScope,
149 count: usize,
150 ) -> Option<TextObjectRange> {
151 self.run_text_object_range(cursor, scope, count, |ch| !ch.is_whitespace())
152 }
153
154 fn run_text_object_range(
155 &self,
156 cursor: Pos,
157 scope: TextObjectScope,
158 count: usize,
159 predicate: impl Fn(char) -> bool + Copy,
160 ) -> Option<TextObjectRange> {
161 let count = count.max(1);
162 let mut start = self.find_seed_char(self.pos_to_char(cursor), predicate)?;
163 let mut end = self.run_end(start, predicate);
164 start = self.run_start(start, predicate);
165
166 for _ in 1..count {
167 if let Some(next_start) = self.find_next_run_start(end, predicate) {
168 end = self.run_end(next_start, predicate);
169 } else {
170 break;
171 }
172 }
173
174 if scope == TextObjectScope::Around {
175 let trailing_end = self.scan_whitespace_forward(end);
176 if trailing_end > end {
177 end = trailing_end;
178 } else {
179 start = self.scan_whitespace_backward(start);
180 }
181 }
182
183 Some(TextObjectRange {
184 start,
185 end,
186 mode: RangeMode::Char,
187 })
188 }
189
190 fn paragraph_text_object_range(
191 &self,
192 cursor: Pos,
193 scope: TextObjectScope,
194 count: usize,
195 ) -> Option<TextObjectRange> {
196 let count = count.max(1);
197 let mut start_line = self.clamp_line(cursor.line);
198
199 if self.line_is_blank(start_line) {
200 return Some(TextObjectRange {
201 start: self.line_to_char(start_line),
202 end: self.line_full_end_char(start_line),
203 mode: RangeMode::Line,
204 });
205 }
206
207 while start_line > 0 && !self.line_is_blank(start_line - 1) {
208 start_line -= 1;
209 }
210
211 let mut end_line = self.clamp_line(cursor.line);
212 while end_line + 1 < self.len_lines() && !self.line_is_blank(end_line + 1) {
213 end_line += 1;
214 }
215
216 for _ in 1..count {
217 let mut next_line = end_line.saturating_add(1);
218 while next_line < self.len_lines() && self.line_is_blank(next_line) {
219 next_line += 1;
220 }
221 if next_line >= self.len_lines() || self.line_is_blank(next_line) {
222 break;
223 }
224 end_line = next_line;
225 while end_line + 1 < self.len_lines() && !self.line_is_blank(end_line + 1) {
226 end_line += 1;
227 }
228 }
229
230 if scope == TextObjectScope::Around {
231 let mut trailing = end_line.saturating_add(1);
232 let mut extended = false;
233 while trailing < self.len_lines() && self.line_is_blank(trailing) {
234 end_line = trailing;
235 trailing += 1;
236 extended = true;
237 }
238 if !extended {
239 while start_line > 0 && self.line_is_blank(start_line - 1) {
240 start_line -= 1;
241 }
242 }
243 }
244
245 Some(TextObjectRange {
246 start: self.line_to_char(start_line),
247 end: self.line_full_end_char(end_line),
248 mode: RangeMode::Line,
249 })
250 }
251
252 fn delimiter_text_object_range(
253 &self,
254 cursor: Pos,
255 kind: DelimiterKind,
256 scope: TextObjectScope,
257 count: usize,
258 ) -> Option<TextObjectRange> {
259 let count = count.max(1);
260 let cursor_char = self.pos_to_char(cursor);
261 let anchor_before = cursor_char.saturating_sub(1);
262 let (open, close) = delimiter_chars(kind);
263 if open == close {
264 return self.symmetric_delimiter_text_object_range(cursor, open, scope, count);
265 }
266
267 let cursor_line = self.clamp_line(cursor.line);
268
269 let mut containing_pairs = Vec::new();
270 let mut same_line_pairs = Vec::new();
271 let mut stack = Vec::new();
272 for char_idx in 0..self.len_chars() {
273 let ch = self.rope().char(char_idx);
274 if ch == open {
275 stack.push(char_idx);
276 } else if ch == close
277 && let Some(start) = stack.pop()
278 {
279 if pair_contains_cursor(start, char_idx, cursor_char, anchor_before) {
280 containing_pairs.push((start, char_idx));
281 } else if pair_is_on_line(self, start, char_idx, cursor_line) {
282 same_line_pairs.push((start, char_idx));
283 }
284 }
285 }
286
287 let (start, end_inclusive) = if !containing_pairs.is_empty() {
288 containing_pairs.sort_by_key(|(start, end)| end.saturating_sub(*start));
289 *containing_pairs.get(count.saturating_sub(1))?
290 } else {
291 same_line_pairs.sort_by_key(|(start, end)| {
292 (
293 delimiter_pair_distance(*start, *end, cursor_char),
294 end.saturating_sub(*start),
295 *start,
296 )
297 });
298 *same_line_pairs.get(count.saturating_sub(1))?
299 };
300
301 let (range_start, range_end) = match scope {
302 TextObjectScope::Inner => (start.saturating_add(1), end_inclusive),
303 TextObjectScope::Around => (start, end_inclusive.saturating_add(1)),
304 };
305
306 Some(TextObjectRange {
307 start: range_start.min(self.len_chars()),
308 end: range_end.min(self.len_chars()),
309 mode: RangeMode::Char,
310 })
311 }
312
313 fn symmetric_delimiter_text_object_range(
314 &self,
315 cursor: Pos,
316 delimiter: char,
317 scope: TextObjectScope,
318 count: usize,
319 ) -> Option<TextObjectRange> {
320 let cursor_line = self.clamp_line(cursor.line);
321 let cursor_char = self.pos_to_char(cursor);
322 let anchor_before = cursor_char.saturating_sub(1);
323 let line_range = self.line_char_range(cursor_line);
324 let mut quote_chars = Vec::new();
325
326 for char_idx in line_range.clone() {
327 if self.rope().char(char_idx) == delimiter && !self.char_is_escaped(char_idx) {
328 quote_chars.push(char_idx);
329 }
330 }
331
332 let mut containing_pairs = Vec::new();
333 let mut same_line_pairs = Vec::new();
334 for pair in quote_chars.chunks_exact(2) {
335 let start = pair[0];
336 let end_inclusive = pair[1];
337 if pair_contains_cursor(start, end_inclusive, cursor_char, anchor_before) {
338 containing_pairs.push((start, end_inclusive));
339 } else {
340 same_line_pairs.push((start, end_inclusive));
341 }
342 }
343
344 let (start, end_inclusive) = if !containing_pairs.is_empty() {
345 containing_pairs.sort_by_key(|(start, end)| end.saturating_sub(*start));
346 *containing_pairs.get(count.saturating_sub(1))?
347 } else {
348 same_line_pairs.sort_by_key(|(start, end)| {
349 (
350 delimiter_pair_distance(*start, *end, cursor_char),
351 end.saturating_sub(*start),
352 *start,
353 )
354 });
355 *same_line_pairs.get(count.saturating_sub(1))?
356 };
357
358 let (range_start, range_end) = match scope {
359 TextObjectScope::Inner => (start.saturating_add(1), end_inclusive),
360 TextObjectScope::Around => (start, end_inclusive.saturating_add(1)),
361 };
362
363 Some(TextObjectRange {
364 start: range_start.min(self.len_chars()),
365 end: range_end.min(self.len_chars()),
366 mode: RangeMode::Char,
367 })
368 }
369
370 fn find_seed_char(
371 &self,
372 cursor_char: usize,
373 predicate: impl Fn(char) -> bool + Copy,
374 ) -> Option<usize> {
375 let maxc = self.len_chars();
376 if maxc == 0 {
377 return None;
378 }
379
380 let clamped = cursor_char.min(maxc.saturating_sub(1));
381 if predicate(self.rope().char(clamped)) {
382 return Some(clamped);
383 }
384
385 if let Some(next) = self.find_next_run_start(clamped, predicate) {
386 return Some(next);
387 }
388
389 if clamped > 0 && predicate(self.rope().char(clamped - 1)) {
390 return Some(clamped - 1);
391 }
392
393 self.find_prev_run_start(clamped, predicate)
394 }
395
396 fn run_start(&self, mut char_idx: usize, predicate: impl Fn(char) -> bool + Copy) -> usize {
397 while char_idx > 0 && predicate(self.rope().char(char_idx - 1)) {
398 char_idx -= 1;
399 }
400 char_idx
401 }
402
403 fn run_end(&self, mut char_idx: usize, predicate: impl Fn(char) -> bool + Copy) -> usize {
404 while char_idx < self.len_chars() && predicate(self.rope().char(char_idx)) {
405 char_idx += 1;
406 }
407 char_idx
408 }
409
410 fn find_next_run_start(
411 &self,
412 mut char_idx: usize,
413 predicate: impl Fn(char) -> bool + Copy,
414 ) -> Option<usize> {
415 while char_idx < self.len_chars() {
416 if predicate(self.rope().char(char_idx)) {
417 return Some(char_idx);
418 }
419 char_idx += 1;
420 }
421 None
422 }
423
424 fn find_prev_run_start(
425 &self,
426 mut char_idx: usize,
427 predicate: impl Fn(char) -> bool + Copy,
428 ) -> Option<usize> {
429 char_idx = char_idx.min(self.len_chars());
430 while char_idx > 0 {
431 char_idx -= 1;
432 if predicate(self.rope().char(char_idx)) {
433 return Some(self.run_start(char_idx, predicate));
434 }
435 }
436 None
437 }
438
439 fn scan_whitespace_forward(&self, mut char_idx: usize) -> usize {
440 while char_idx < self.len_chars() && self.rope().char(char_idx).is_whitespace() {
441 char_idx += 1;
442 }
443 char_idx
444 }
445
446 fn scan_whitespace_backward(&self, mut char_idx: usize) -> usize {
447 while char_idx > 0 && self.rope().char(char_idx - 1).is_whitespace() {
448 char_idx -= 1;
449 }
450 char_idx
451 }
452
453 fn line_is_blank(&self, line_idx: usize) -> bool {
454 self.line_string(line_idx).trim().is_empty()
455 }
456
457 fn char_is_escaped(&self, char_idx: usize) -> bool {
458 let mut backslashes = 0;
459 let mut idx = char_idx;
460 while idx > 0 {
461 idx -= 1;
462 if self.rope().char(idx) != '\\' {
463 break;
464 }
465 backslashes += 1;
466 }
467 backslashes % 2 == 1
468 }
469}
470
471fn delimiter_chars(kind: DelimiterKind) -> (char, char) {
472 match kind {
473 DelimiterKind::Parentheses => ('(', ')'),
474 DelimiterKind::Brackets => ('[', ']'),
475 DelimiterKind::Braces => ('{', '}'),
476 DelimiterKind::SingleQuotes => ('\'', '\''),
477 DelimiterKind::DoubleQuotes => ('"', '"'),
478 DelimiterKind::Backticks => ('`', '`'),
479 }
480}
481
482fn pair_contains_cursor(
483 start: usize,
484 end_inclusive: usize,
485 cursor_char: usize,
486 before: usize,
487) -> bool {
488 (start <= cursor_char && cursor_char <= end_inclusive)
489 || (cursor_char > 0 && start <= before && before <= end_inclusive)
490}
491
492fn pair_is_on_line(buf: &TextBuffer, start: usize, end_inclusive: usize, line: usize) -> bool {
493 buf.char_to_line(start) == line && buf.char_to_line(end_inclusive) == line
494}
495
496fn delimiter_pair_distance(start: usize, end_inclusive: usize, cursor_char: usize) -> usize {
497 if cursor_char < start {
498 start - cursor_char
499 } else if cursor_char > end_inclusive {
500 cursor_char - end_inclusive
501 } else {
502 0
503 }
504}