1use std::sync::Arc;
12
13use lattice_grammar::CommandError;
14
15use lattice_core::Document;
16use lattice_core::buffer::Buffer;
17use lattice_grammar::args::{ArgKind, ArgSpec, ArgValue, Args};
18use lattice_grammar::effect::{Effect, YankKind};
19use lattice_grammar::registry::{CommandRegistry, OperatorId, OperatorSpec};
20use lattice_grammar::source::SourceLocation;
21use lattice_keymap::contribution::Keymap;
22use lattice_protocol::ChordPattern;
23use lattice_protocol::chord::KeyChord;
24use lattice_protocol::edit::Edit;
25use lattice_protocol::position::Position;
26
27use crate::mode::ActivationPolicy;
28use crate::{CapabilitySet, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind};
29
30pub fn open_close_pair(ch: char) -> Option<(char, char)> {
38 Some(match ch {
39 '(' | ')' => ('(', ')'),
40 '[' | ']' => ('[', ']'),
41 '{' | '}' => ('{', '}'),
42 '<' | '>' => ('<', '>'),
43 '"' => ('"', '"'),
44 '\'' => ('\'', '\''),
45 '`' => ('`', '`'),
46 _ => return None,
47 })
48}
49
50fn is_opener(ch: char) -> bool {
51 matches!(ch, '(' | '[' | '{' | '<')
52}
53
54fn is_closer(ch: char) -> bool {
55 matches!(ch, ')' | ']' | '}' | '>')
56}
57
58fn is_symmetric(ch: char) -> bool {
59 matches!(ch, '"' | '\'' | '`')
60}
61
62fn matching_closer(opener: char) -> Option<char> {
63 open_close_pair(opener).map(|(_, close)| close)
64}
65
66fn matching_opener(closer: char) -> Option<char> {
67 open_close_pair(closer).map(|(open, _)| open)
68}
69
70fn pads_inside(ch: char) -> bool {
81 matches!(open_close_pair(ch), Some((open, close)) if open != close && ch == open)
82}
83
84fn padding_for(ch: char) -> &'static str {
87 if pads_inside(ch) { " " } else { "" }
88}
89
90fn space_run_forward(line: &str, from: usize, limit: usize) -> usize {
93 line.get(from..limit)
94 .unwrap_or("")
95 .bytes()
96 .take_while(|b| *b == b' ' || *b == b'\t')
97 .count()
98}
99
100fn space_run_backward(line: &str, until: usize, floor: usize) -> usize {
103 line.get(floor..until)
104 .unwrap_or("")
105 .bytes()
106 .rev()
107 .take_while(|b| *b == b' ' || *b == b'\t')
108 .count()
109}
110
111fn delimiter_spans(
123 buffer: &Buffer,
124 open_pos: Position,
125 open_len: u32,
126 close_pos: Position,
127 close_len: u32,
128 absorb_padding: bool,
129) -> (
130 lattice_protocol::position::Range,
131 lattice_protocol::position::Range,
132) {
133 let open_end = open_pos.byte + open_len;
134 let close_end = close_pos.byte + close_len;
135
136 let (pad_after_open, pad_before_close) = if !absorb_padding {
137 (0, 0)
138 } else if open_pos.line == close_pos.line {
139 let line = buffer.line(open_pos.line).unwrap_or_default().to_string();
140 let inner_end = close_pos.byte as usize;
141 let forward = space_run_forward(&line, open_end as usize, inner_end);
142 let backward = space_run_backward(
143 &line,
144 inner_end,
145 (open_end as usize + forward).min(inner_end),
146 );
147 (forward as u32, backward as u32)
148 } else {
149 let open_line = buffer.line(open_pos.line).unwrap_or_default().to_string();
150 let close_line = buffer.line(close_pos.line).unwrap_or_default().to_string();
151 let forward = space_run_forward(&open_line, open_end as usize, open_line.len());
152 let backward = space_run_backward(&close_line, close_pos.byte as usize, 0);
153 (forward as u32, backward as u32)
154 };
155
156 (
157 lattice_protocol::position::Range::new(
158 open_pos,
159 Position::new(open_pos.line, open_end + pad_after_open),
160 ),
161 lattice_protocol::position::Range::new(
162 Position::new(close_pos.line, close_pos.byte - pad_before_close),
163 Position::new(close_pos.line, close_end),
164 ),
165 )
166}
167
168pub fn find_surround_pair(
188 buffer: &Buffer,
189 cursor: Position,
190 target: char,
191) -> Option<(usize, usize)> {
192 let text = buffer.as_string();
193 let cursor_byte = buffer.position_to_byte(cursor).ok()?;
194
195 let (target_open, target_close) = open_close_pair(target)?;
197
198 let cursor_byte = match text.get(cursor_byte..).and_then(|s| s.chars().next()) {
199 Some(ch) if ch == target_open && target_open == target_close => {
207 let line = buffer.line(cursor.line).unwrap_or_default().to_string();
208 let preceding = line
209 .get(..cursor.byte as usize)
210 .unwrap_or("")
211 .chars()
212 .filter(|c| *c == target_open)
213 .count();
214 if preceding % 2 == 0 {
215 cursor_byte + ch.len_utf8()
216 } else {
217 cursor_byte
218 }
219 }
220 Some(ch) if ch == target_open => cursor_byte + ch.len_utf8(),
224 _ => cursor_byte,
225 };
226
227 let mut closer_stack: Vec<char> = Vec::new();
229 let mut opener_byte: Option<usize> = None;
230 let char_indices: Vec<(usize, char)> = text.char_indices().collect();
231
232 for (byte, ch) in char_indices.iter().rev() {
233 let byte = *byte;
234 let ch = *ch;
235 if byte >= cursor_byte {
236 continue;
237 }
238
239 if is_symmetric(ch) && ch == target_close {
240 if closer_stack.last() == Some(&ch) {
242 closer_stack.pop();
244 } else if closer_stack.is_empty() {
245 opener_byte = Some(byte);
247 break;
248 } else {
249 closer_stack.push(ch);
251 }
252 } else if is_closer(ch) {
253 let complement = matching_opener(ch);
254 if complement == Some(ch) {
255 closer_stack.push(ch);
257 } else {
258 closer_stack.push(ch);
259 }
260 } else if is_opener(ch) {
261 let complement = matching_closer(ch);
262 if let Some(comp) = complement {
263 if closer_stack.last() == Some(&comp) {
264 closer_stack.pop();
265 }
266 if closer_stack.is_empty() && ch == target_open {
268 opener_byte = Some(byte);
269 break;
270 }
271 }
272 } else if is_symmetric(ch) {
273 if closer_stack.last() == Some(&ch) {
275 closer_stack.pop();
276 } else {
277 closer_stack.push(ch);
278 }
279 }
280 }
281
282 let opener_byte = opener_byte?;
283
284 let mut opener_stack: Vec<char> = Vec::new();
286 let mut closer_byte: Option<usize> = None;
287
288 for (byte, ch) in char_indices.iter() {
289 let byte = *byte;
290 let ch = *ch;
291 if byte < cursor_byte {
292 continue;
293 }
294
295 if is_symmetric(ch) && ch == target_open {
296 if opener_stack.last() == Some(&ch) {
297 opener_stack.pop();
298 } else if opener_stack.is_empty() {
299 closer_byte = Some(byte);
301 break;
302 } else {
303 opener_stack.push(ch);
304 }
305 } else if is_opener(ch) {
306 opener_stack.push(ch);
307 } else if is_closer(ch) {
308 if let Some(open) = opener_stack.last() {
309 let comp = matching_closer(*open);
310 if comp == Some(ch) || (is_symmetric(*open) && *open == ch) {
311 opener_stack.pop();
312 }
313 }
314 if opener_stack.is_empty() && ch == target_close {
315 closer_byte = Some(byte);
316 break;
317 }
318 } else if is_symmetric(ch) {
319 if opener_stack.last() == Some(&ch) {
320 opener_stack.pop();
321 } else {
322 opener_stack.push(ch);
323 }
324 }
325 }
326
327 let closer_byte = closer_byte?;
328 Some((opener_byte, closer_byte))
329}
330
331fn operator_surround_delete(
334 ctx: &mut lattice_grammar::registry::OperatorContext,
335) -> Result<Effect, CommandError> {
336 let target = match &ctx.args {
337 Args::Char(c) => *c,
338 _ => return Err(CommandError::InvalidArgs("ds requires Args::Char")),
339 };
340
341 let (open_pos, close_pos, open_text, close_text) = {
344 let buffer = ctx.document.buffer();
345 let cursor = get_cursor_from_document(ctx.document);
346 let (open_byte, close_byte) = match find_surround_pair(buffer, cursor, target) {
347 Some(pair) => pair,
348 None => return Ok(Effect::None),
349 };
350 let open_pos = buffer.byte_to_position(open_byte)?;
351 let close_pos = buffer.byte_to_position(close_byte)?;
352
353 let yanked_open = buffer.slice(lattice_protocol::position::Range::new(
354 open_pos,
355 Position::new(open_pos.line, open_pos.byte + target.len_utf8() as u32),
356 ))?;
357
358 let target_close = open_close_pair(target).map(|(_, c)| c).unwrap_or(target);
359 let yanked_close = buffer.slice(lattice_protocol::position::Range::new(
360 close_pos,
361 Position::new(
362 close_pos.line,
363 close_pos.byte + target_close.len_utf8() as u32,
364 ),
365 ))?;
366
367 (open_pos, close_pos, yanked_open, yanked_close)
368 };
369
370 let target_close = open_close_pair(target).map(|(_, c)| c).unwrap_or(target);
371
372 let (open_span, close_span) = {
376 let buffer = ctx.document.buffer();
377 delimiter_spans(
378 buffer,
379 open_pos,
380 target.len_utf8() as u32,
381 close_pos,
382 target_close.len_utf8() as u32,
383 pads_inside(target),
384 )
385 };
386
387 let edit_close = Edit::delete(close_span);
389 let edit_open = Edit::delete(open_span);
390
391 let applied = ctx.document.apply_edit_batch(vec![edit_close, edit_open])?;
392
393 let new_cursor = if open_pos.line == close_pos.line {
394 Position::new(open_pos.line, open_pos.byte)
395 } else {
396 open_pos
397 };
398
399 Ok(Effect::Many(vec![
400 Effect::Edits(applied),
401 Effect::Yank {
402 register: ctx.register,
403 content: format!("{}{}", open_text, close_text),
404 kind: YankKind::Charwise,
405 explicit_yank: false,
406 },
407 Effect::CursorMove(new_cursor),
408 ]))
409}
410
411fn operator_surround_change(
414 ctx: &mut lattice_grammar::registry::OperatorContext,
415) -> Result<Effect, CommandError> {
416 let (target, replacement) = match &ctx.args {
417 Args::List(values) if values.len() == 2 => {
418 let t = match &values[0] {
419 ArgValue::Char(c) => *c,
420 _ => return Err(CommandError::InvalidArgs("cs arg[0] must be Char")),
421 };
422 let r = match &values[1] {
423 ArgValue::Char(c) => *c,
424 _ => return Err(CommandError::InvalidArgs("cs arg[1] must be Char")),
425 };
426 (t, r)
427 }
428 _ => {
429 return Err(CommandError::InvalidArgs(
430 "cs requires Args::List([Char, Char])",
431 ));
432 }
433 };
434
435 let (new_open, new_close) = match open_close_pair(replacement) {
436 Some(pair) => pair,
437 None => return Ok(Effect::None),
438 };
439 let target_close = open_close_pair(target).map(|(_, c)| c).unwrap_or(target);
440
441 let (open_pos, close_pos, open_text, close_text) = {
442 let buffer = ctx.document.buffer();
443 let cursor = get_cursor_from_document(ctx.document);
444 let (open_byte, close_byte) = match find_surround_pair(buffer, cursor, target) {
445 Some(pair) => pair,
446 None => return Ok(Effect::None),
447 };
448 let open_pos = buffer.byte_to_position(open_byte)?;
449 let close_pos = buffer.byte_to_position(close_byte)?;
450
451 let yanked_open = buffer.slice(lattice_protocol::position::Range::new(
452 open_pos,
453 Position::new(open_pos.line, open_pos.byte + target.len_utf8() as u32),
454 ))?;
455 let yanked_close = buffer.slice(lattice_protocol::position::Range::new(
456 close_pos,
457 Position::new(
458 close_pos.line,
459 close_pos.byte + target_close.len_utf8() as u32,
460 ),
461 ))?;
462
463 (open_pos, close_pos, yanked_open, yanked_close)
464 };
465
466 let pad = padding_for(replacement);
472 let open_text_repl = format!("{new_open}{pad}");
473 let close_text_repl = format!("{pad}{new_close}");
474
475 let (open_span, close_span) = {
476 let buffer = ctx.document.buffer();
477 delimiter_spans(
478 buffer,
479 open_pos,
480 target.len_utf8() as u32,
481 close_pos,
482 target_close.len_utf8() as u32,
483 pads_inside(target),
484 )
485 };
486
487 let edit_close = Edit::replace(close_span, close_text_repl.clone());
488 let edit_open = Edit::replace(open_span, open_text_repl);
489
490 let applied = ctx.document.apply_edit_batch(vec![edit_close, edit_open])?;
491
492 let new_cursor = if open_pos.line == close_pos.line {
493 Position::new(open_pos.line, open_pos.byte + 1)
494 } else {
495 Position::new(open_pos.line, open_pos.byte + new_open.len_utf8() as u32)
496 };
497 Ok(Effect::Many(vec![
498 Effect::Edits(applied),
499 Effect::Yank {
500 register: ctx.register,
501 content: format!("{}{}", open_text, close_text),
502 kind: YankKind::Charwise,
503 explicit_yank: false,
504 },
505 Effect::CursorMove(new_cursor),
506 ]))
507}
508
509fn operator_surround_add(
512 ctx: &mut lattice_grammar::registry::OperatorContext,
513) -> Result<Effect, CommandError> {
514 let wrapper = match &ctx.args {
515 Args::Char(c) => *c,
516 _ => {
517 return Err(CommandError::InvalidArgs(
518 "surround-add requires Args::Char",
519 ));
520 }
521 };
522
523 let (open, close) = match open_close_pair(wrapper) {
524 Some(pair) => pair,
525 None => return Ok(Effect::None),
526 };
527
528 if ctx.range.is_empty() {
529 return Ok(Effect::None);
530 }
531
532 let (wrap_start, wrap_end, wrap_text, new_cursor) = {
535 let buffer = ctx.document.buffer();
536
537 let (wrap_start, wrap_end, wrap_text) = if ctx.linewise {
538 let line = buffer.line(ctx.range.start.line).unwrap_or_default();
539 let start = Position::new(ctx.range.start.line, 0);
540 let line_byte_len = buffer.line_byte_len(ctx.range.start.line);
541 let end = Position::new(ctx.range.start.line, line_byte_len);
542 let text = line.to_string();
543 (start, end, text)
544 } else {
545 let text = buffer.slice(ctx.range)?;
546 (ctx.range.start, ctx.range.end, text)
547 };
548
549 let inside = open.len_utf8() + padding_for(wrapper).len();
552
553 let new_cursor = if ctx.linewise {
554 Position::new(wrap_start.line, inside as u32)
555 } else {
556 let new_byte = buffer.position_to_byte(wrap_start)? + inside;
557 buffer.byte_to_position(new_byte)?
558 };
559
560 (wrap_start, wrap_end, wrap_text, new_cursor)
561 };
562
563 let pad = padding_for(wrapper);
565 let wrapped = format!("{open}{pad}{wrap_text}{pad}{close}");
566
567 let wrap_range = lattice_protocol::position::Range::new(wrap_start, wrap_end);
568 let edit = Edit::replace(wrap_range, wrapped);
569 let applied = ctx.document.apply_edit(edit)?;
570
571 Ok(Effect::Many(vec![
572 Effect::Edits(vec![applied]),
573 Effect::CursorMove(new_cursor),
574 ]))
575}
576
577fn get_cursor_from_document(doc: &Document) -> Position {
580 doc.selections().primary().head
581}
582
583#[derive(Debug, Clone)]
587pub struct SurroundOperators {
588 pub delete: OperatorId,
590 pub change: OperatorId,
592 pub add: OperatorId,
594}
595
596pub fn register_surround_operators(registry: &mut CommandRegistry) -> SurroundOperators {
599 let delete = registry.register_operator(
600 "operator:surround-delete",
601 "Delete the nearest surrounding pair (vim's `ds{char}`).",
602 OperatorSpec {
603 repeatable: true,
604 apply: Arc::new(operator_surround_delete),
605 args_schema: vec![ArgSpec::required(
606 "target",
607 ArgKind::Char,
608 "The surrounding pair character to delete (e.g. `\"`, `(`, `[`)",
609 )],
610 blockwise_per_row: false,
611 post_motion_char: false,
612 },
613 );
614
615 let change = registry.register_operator(
616 "operator:surround-change",
617 "Change the nearest surrounding pair to a different one (vim's `cs{old}{new}`).",
618 OperatorSpec {
619 repeatable: true,
620 apply: Arc::new(operator_surround_change),
621 args_schema: vec![
622 ArgSpec::required(
623 "target",
624 ArgKind::Char,
625 "The current surrounding pair character",
626 ),
627 ArgSpec::required(
628 "replacement",
629 ArgKind::Char,
630 "The replacement surrounding pair character",
631 ),
632 ],
633 blockwise_per_row: false,
634 post_motion_char: false,
635 },
636 );
637
638 let add = registry.register_operator(
639 "operator:surround-add",
640 "Wrap the target range in a surrounding pair (vim's `yss{char}` / visual `S{char}` / `ys{motion}{char}`).",
641 OperatorSpec {
642 repeatable: true,
643 apply: Arc::new(operator_surround_add),
644 args_schema: vec![
645 ArgSpec::required("wrapper", ArgKind::Char, "The pair character to wrap with"),
646 ],
647 blockwise_per_row: false,
648 post_motion_char: true,
649 },
650 );
651
652 SurroundOperators {
653 delete,
654 change,
655 add,
656 }
657}
658
659pub struct SurroundMode {
668 operators: SurroundOperators,
669}
670
671impl SurroundMode {
672 pub fn mode_id() -> ModeId {
676 ModeId::new("surround-mode")
677 }
678}
679
680impl Mode for SurroundMode {
681 type Guard = ();
682
683 fn id(&self) -> ModeId {
684 Self::mode_id()
685 }
686
687 fn kind(&self) -> ModeKind {
688 ModeKind::Minor
689 }
690
691 fn activation_policy(&self) -> ActivationPolicy {
692 ActivationPolicy::Global
693 }
694
695 fn keymap(&self) -> Keymap {
696 use lattice_grammar::command::CommandInvocation;
697 use lattice_keymap::binding_mode::BindingMode;
698 use lattice_keymap::contribution::KeymapBinding;
699
700 let lit_ch = |c: char| ChordPattern::Literal(KeyChord::char(c));
701
702 Keymap::new()
727 .bind(
728 KeymapBinding::new(
729 BindingMode::Normal,
730 vec![lit_ch('d'), lit_ch('s'), ChordPattern::CharLiteral],
731 CommandInvocation::of(self.operators.delete.0)
732 .with_range(lattice_grammar::range::Range::CurrentLine),
733 SourceLocation::builtin_file(file!(), line!()),
734 )
735 .with_doc("Delete the nearest surrounding pair (vim's `ds{char}`)."),
736 )
737 .bind(
738 KeymapBinding::new(
739 BindingMode::Normal,
740 vec![
741 lit_ch('c'),
742 lit_ch('s'),
743 ChordPattern::CharLiteral,
744 ChordPattern::CharLiteral,
745 ],
746 CommandInvocation::of(self.operators.change.0)
747 .with_range(lattice_grammar::range::Range::CurrentLine),
748 SourceLocation::builtin_file(file!(), line!()),
749 )
750 .with_doc("Change the nearest surrounding pair (vim's `cs{old}{new}`)."),
751 )
752 .bind(
753 KeymapBinding::new(
754 BindingMode::Normal,
755 vec![
756 lit_ch('y'),
757 lit_ch('s'),
758 lit_ch('s'),
759 ChordPattern::CharLiteral,
760 ],
761 CommandInvocation::of(self.operators.add.0)
762 .with_range(lattice_grammar::range::Range::CurrentLine),
763 SourceLocation::builtin_file(file!(), line!()),
764 )
765 .with_doc("Wrap the current line in a surrounding pair (vim's `yss{char}`)."),
766 )
767 .bind(
768 KeymapBinding::new(
769 BindingMode::Visual,
770 vec![lit_ch('S'), ChordPattern::CharLiteral],
771 CommandInvocation::of(self.operators.add.0)
772 .with_range(lattice_grammar::range::Range::Selection),
773 SourceLocation::builtin_file(file!(), line!()),
774 )
775 .with_doc(
776 "Wrap the visual selection in a surrounding pair (vim's visual `S{char}`).",
777 ),
778 )
779 }
780
781 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
782 Box::pin(async { Ok(()) })
783 }
784
785 fn required_capabilities(&self) -> CapabilitySet {
786 CapabilitySet::empty()
787 }
788}
789
790pub fn register_surround_modes(
796 registry: &mut crate::registry::ModeRegistry,
797 operators: SurroundOperators,
798) {
799 registry
800 .register(SurroundMode { operators })
801 .expect("surround-mode must register without conflict");
802}
803
804#[cfg(test)]
807mod tests {
808 use super::*;
809 use lattice_core::buffer::Buffer;
810 use lattice_protocol::position::Position;
811
812 #[test]
813 fn open_close_pair_maps_brackets() {
814 assert_eq!(open_close_pair('('), Some(('(', ')')));
815 assert_eq!(open_close_pair(')'), Some(('(', ')')));
816 assert_eq!(open_close_pair('['), Some(('[', ']')));
817 assert_eq!(open_close_pair(']'), Some(('[', ']')));
818 assert_eq!(open_close_pair('{'), Some(('{', '}')));
819 assert_eq!(open_close_pair('}'), Some(('{', '}')));
820 assert_eq!(open_close_pair('<'), Some(('<', '>')));
821 assert_eq!(open_close_pair('>'), Some(('<', '>')));
822 }
823
824 #[test]
825 fn open_close_pair_maps_symmetric() {
826 assert_eq!(open_close_pair('"'), Some(('"', '"')));
827 assert_eq!(open_close_pair('\''), Some(('\'', '\'')));
828 assert_eq!(open_close_pair('`'), Some(('`', '`')));
829 }
830
831 #[test]
832 fn open_close_pair_unknown_returns_none() {
833 assert_eq!(open_close_pair('x'), None);
834 assert_eq!(open_close_pair(' '), None);
835 }
836
837 #[test]
838 fn find_surround_pair_simple_quotes() {
839 let buf = Buffer::from_text("hello \"world\" foo");
840 let cursor = buf.position_to_byte(Position::new(0, 8)).unwrap();
842 let cursor_pos = buf.byte_to_position(cursor).unwrap();
843 let pair = find_surround_pair(&buf, cursor_pos, '"').unwrap();
844 let open_pos = buf.byte_to_position(pair.0).unwrap();
845 let close_pos = buf.byte_to_position(pair.1).unwrap();
846 assert_eq!(open_pos, Position::new(0, 6)); assert_eq!(close_pos, Position::new(0, 12)); }
849
850 #[test]
851 fn find_surround_pair_brackets() {
852 let buf = Buffer::from_text("fn foo(x: i32) {}");
853 let cursor = buf.position_to_byte(Position::new(0, 10)).unwrap();
855 let cursor_pos = buf.byte_to_position(cursor).unwrap();
856 let pair = find_surround_pair(&buf, cursor_pos, '(').unwrap();
857 let open_pos = buf.byte_to_position(pair.0).unwrap();
858 let close_pos = buf.byte_to_position(pair.1).unwrap();
859 assert_eq!(open_pos, Position::new(0, 6)); assert_eq!(close_pos, Position::new(0, 13)); }
863
864 #[test]
865 fn find_surround_pair_nested() {
866 let buf = Buffer::from_text("a (b (c) d) e");
867 let cursor = buf.position_to_byte(Position::new(0, 7)).unwrap();
869 let cursor_pos = buf.byte_to_position(cursor).unwrap();
870 let pair = find_surround_pair(&buf, cursor_pos, '(').unwrap();
871 let open_pos = buf.byte_to_position(pair.0).unwrap();
872 let close_pos = buf.byte_to_position(pair.1).unwrap();
873 assert_eq!(open_pos, Position::new(0, 5)); assert_eq!(close_pos, Position::new(0, 7)); }
878
879 #[test]
880 fn find_surround_pair_no_match() {
881 let buf = Buffer::from_text("hello world");
882 let pair = find_surround_pair(&buf, Position::ZERO, '"');
883 assert!(pair.is_none());
884 }
885
886 fn pair_cols(buf: &Buffer, cursor_col: u32, target: char) -> Option<(u32, u32)> {
897 let pair = find_surround_pair(buf, Position::new(0, cursor_col), target)?;
898 let open = buf.byte_to_position(pair.0).ok()?;
899 let close = buf.byte_to_position(pair.1).ok()?;
900 Some((open.byte, close.byte))
901 }
902
903 #[test]
904 fn a_cursor_on_the_opening_quote_finds_its_pair() {
905 let buf = Buffer::from_text("\"hello\"");
906 assert_eq!(pair_cols(&buf, 0, '"'), Some((0, 6)));
907 }
908
909 #[test]
910 fn a_cursor_on_the_closing_quote_finds_its_pair() {
911 let buf = Buffer::from_text("\"hello\"");
912 assert_eq!(pair_cols(&buf, 6, '"'), Some((0, 6)));
913 }
914
915 #[test]
916 fn a_cursor_on_the_opening_bracket_finds_its_pair() {
917 let buf = Buffer::from_text("(hello)");
918 assert_eq!(pair_cols(&buf, 0, '('), Some((0, 6)));
919 }
920
921 #[test]
922 fn a_cursor_on_the_closing_bracket_finds_its_pair() {
923 let buf = Buffer::from_text("(hello)");
924 assert_eq!(pair_cols(&buf, 6, '('), Some((0, 6)));
925 }
926
927 #[test]
935 fn a_cursor_on_a_later_opening_quote_takes_its_own_pair() {
936 let buf = Buffer::from_text("\"a\" \"b\"");
937 assert_eq!(pair_cols(&buf, 4, '"'), Some((4, 6)));
939 }
940
941 #[test]
942 fn a_cursor_on_a_later_closing_quote_takes_its_own_pair() {
943 let buf = Buffer::from_text("\"a\" \"b\"");
944 assert_eq!(pair_cols(&buf, 6, '"'), Some((4, 6)));
945 }
946
947 #[test]
950 fn a_cursor_on_an_inner_bracket_takes_the_inner_pair() {
951 let buf = Buffer::from_text("a (b (c) d) e");
952 assert_eq!(pair_cols(&buf, 5, '('), Some((5, 7)));
954 }
955
956 #[test]
959 fn a_lone_delimiter_under_the_cursor_finds_nothing() {
960 let buf = Buffer::from_text("(hello");
961 assert_eq!(pair_cols(&buf, 0, '('), None);
962 }
963
964 #[test]
965 fn find_surround_pair_close_char_target() {
966 let buf = Buffer::from_text("(hello)");
967 let cursor = buf.position_to_byte(Position::new(0, 3)).unwrap(); let cursor_pos = buf.byte_to_position(cursor).unwrap();
969 let pair = find_surround_pair(&buf, cursor_pos, ')').unwrap();
971 let open_pos = buf.byte_to_position(pair.0).unwrap();
972 let close_pos = buf.byte_to_position(pair.1).unwrap();
973 assert_eq!(open_pos, Position::new(0, 0)); assert_eq!(close_pos, Position::new(0, 6)); }
976
977 #[test]
978 fn find_surround_pair_large_line_performance() {
979 let line = "x".repeat(10_000);
981 let buf = Buffer::from_text(&line);
982 let cursor = buf.position_to_byte(Position::new(0, 5000)).unwrap();
983 let cursor_pos = buf.byte_to_position(cursor).unwrap();
984 let start = std::time::Instant::now();
985 let _ = find_surround_pair(&buf, cursor_pos, '"');
986 let elapsed = start.elapsed();
987 assert!(
992 elapsed.as_micros() < 5000,
993 "find_surround_pair on 10k chars took {:?}",
994 elapsed
995 );
996 }
997}
998
999#[cfg(test)]
1000mod operator_tests {
1001 use super::*;
1002 use lattice_core::BufferId;
1003 use lattice_grammar::CancellationToken;
1004 use lattice_grammar::args::{ArgValue, Args};
1005 use lattice_grammar::builtins::populate as grammar_builtins_populate;
1006 use lattice_grammar::command::CommandInvocation;
1007 use lattice_grammar::dispatcher::execute as grammar_execute;
1008
1009 use lattice_grammar::builtins::Builtins;
1010
1011 fn fixture(text: &str) -> (CommandRegistry, Builtins, SurroundOperators, Document) {
1012 let mut r = CommandRegistry::new();
1013 let builtins = grammar_builtins_populate(&mut r);
1014 let ops = register_surround_operators(&mut r);
1015 let d = Document::from_text(text);
1016 (r, builtins, ops, d)
1017 }
1018
1019 fn doc_text(doc: &Document) -> String {
1020 doc.buffer().as_string()
1021 }
1022
1023 fn set_cursor(doc: &mut Document, pos: Position) {
1024 use lattice_protocol::selection::{Selection, SelectionSet};
1025 doc.set_selections(SelectionSet::single(Selection::cursor(pos)));
1026 }
1027
1028 #[test]
1029 fn surround_delete_removes_double_quotes() {
1030 let (registry, _builtins, ops, mut doc) = fixture("hello \"world\" foo");
1031 let cursor = Position::new(0, 8); set_cursor(&mut doc, cursor);
1033 let inv = CommandInvocation::of(ops.delete.0)
1034 .with_range(lattice_grammar::range::Range::CurrentLine)
1035 .with_args(Args::Char('"'));
1036 grammar_execute(
1037 ®istry,
1038 &mut doc,
1039 BufferId(0),
1040 cursor,
1041 inv,
1042 &CancellationToken::never(),
1043 )
1044 .unwrap();
1045 assert_eq!(doc_text(&doc), "hello world foo");
1046 }
1047
1048 #[test]
1049 fn surround_delete_no_match_is_noop() {
1050 let (registry, _builtins, ops, mut doc) = fixture("hello world");
1051 let cursor = Position::ZERO;
1052 set_cursor(&mut doc, cursor);
1053 let inv = CommandInvocation::of(ops.delete.0)
1054 .with_range(lattice_grammar::range::Range::CurrentLine)
1055 .with_args(Args::Char('"'));
1056 let eff = grammar_execute(
1057 ®istry,
1058 &mut doc,
1059 BufferId(0),
1060 cursor,
1061 inv,
1062 &CancellationToken::never(),
1063 )
1064 .unwrap();
1065 assert!(matches!(eff, lattice_grammar::effect::Effect::None));
1067 assert_eq!(doc_text(&doc), "hello world");
1068 }
1069
1070 #[test]
1071 fn surround_delete_removes_parens() {
1072 let (registry, _builtins, ops, mut doc) = fixture("fn foo(x: i32) {}");
1073 let cursor = Position::new(0, 10); set_cursor(&mut doc, cursor);
1075 let inv = CommandInvocation::of(ops.delete.0)
1076 .with_range(lattice_grammar::range::Range::CurrentLine)
1077 .with_args(Args::Char('('));
1078 grammar_execute(
1079 ®istry,
1080 &mut doc,
1081 BufferId(0),
1082 cursor,
1083 inv,
1084 &CancellationToken::never(),
1085 )
1086 .unwrap();
1087 assert_eq!(doc_text(&doc), "fn foox: i32 {}");
1088 }
1089
1090 #[test]
1091 fn surround_delete_with_close_char_target() {
1092 let (registry, _builtins, ops, mut doc) = fixture("(hello world)");
1093 let cursor = Position::new(0, 3); set_cursor(&mut doc, cursor);
1095 let inv = CommandInvocation::of(ops.delete.0)
1096 .with_range(lattice_grammar::range::Range::CurrentLine)
1097 .with_args(Args::Char(')'));
1098 grammar_execute(
1099 ®istry,
1100 &mut doc,
1101 BufferId(0),
1102 cursor,
1103 inv,
1104 &CancellationToken::never(),
1105 )
1106 .unwrap();
1107 assert_eq!(doc_text(&doc), "hello world");
1108 }
1109
1110 #[test]
1111 fn surround_change_double_to_single_quotes() {
1112 let (registry, _builtins, ops, mut doc) = fixture("hello \"world\" foo");
1113 let cursor = Position::new(0, 8); set_cursor(&mut doc, cursor);
1115 let inv = CommandInvocation::of(ops.change.0)
1116 .with_range(lattice_grammar::range::Range::CurrentLine)
1117 .with_args(Args::List(vec![ArgValue::Char('"'), ArgValue::Char('\'')]));
1118 grammar_execute(
1119 ®istry,
1120 &mut doc,
1121 BufferId(0),
1122 cursor,
1123 inv,
1124 &CancellationToken::never(),
1125 )
1126 .unwrap();
1127 assert_eq!(doc_text(&doc), "hello 'world' foo");
1128 }
1129
1130 #[test]
1135 fn surround_change_parens_to_brackets() {
1136 let (registry, _builtins, ops, mut doc) = fixture("(hello)");
1137 let cursor = Position::new(0, 2); set_cursor(&mut doc, cursor);
1139 let inv = CommandInvocation::of(ops.change.0)
1140 .with_range(lattice_grammar::range::Range::CurrentLine)
1141 .with_args(Args::List(vec![ArgValue::Char('('), ArgValue::Char('[')]));
1142 grammar_execute(
1143 ®istry,
1144 &mut doc,
1145 BufferId(0),
1146 cursor,
1147 inv,
1148 &CancellationToken::never(),
1149 )
1150 .unwrap();
1151 assert_eq!(doc_text(&doc), "[ hello ]");
1152 }
1153
1154 #[test]
1155 fn surround_add_linewise_wraps_line() {
1156 let (registry, _builtins, ops, mut doc) = fixture("hello world\n");
1157 let cursor = Position::new(0, 0);
1158 let inv = CommandInvocation::of(ops.add.0)
1159 .with_range(lattice_grammar::range::Range::CurrentLine)
1160 .with_args(Args::Char('"'));
1161 grammar_execute(
1162 ®istry,
1163 &mut doc,
1164 BufferId(0),
1165 cursor,
1166 inv,
1167 &CancellationToken::never(),
1168 )
1169 .unwrap();
1170 assert_eq!(doc_text(&doc), "\"hello world\"\n");
1171 }
1172
1173 #[test]
1177 fn surround_add_linewise_wraps_line_with_brackets() {
1178 let (registry, _builtins, ops, mut doc) = fixture("hello\n");
1179 let cursor = Position::new(0, 0);
1180 let inv = CommandInvocation::of(ops.add.0)
1181 .with_range(lattice_grammar::range::Range::CurrentLine)
1182 .with_args(Args::Char('('));
1183 grammar_execute(
1184 ®istry,
1185 &mut doc,
1186 BufferId(0),
1187 cursor,
1188 inv,
1189 &CancellationToken::never(),
1190 )
1191 .unwrap();
1192 assert_eq!(doc_text(&doc), "( hello )\n");
1193 }
1194
1195 fn add_linewise(text: &str, wrapper: char) -> String {
1199 let (registry, _builtins, ops, mut doc) = fixture(text);
1200 let cursor = Position::new(0, 0);
1201 let inv = CommandInvocation::of(ops.add.0)
1202 .with_range(lattice_grammar::range::Range::CurrentLine)
1203 .with_args(Args::Char(wrapper));
1204 grammar_execute(
1205 ®istry,
1206 &mut doc,
1207 BufferId(0),
1208 cursor,
1209 inv,
1210 &CancellationToken::never(),
1211 )
1212 .unwrap();
1213 doc_text(&doc)
1214 }
1215
1216 fn change_at(text: &str, col: u32, target: char, replacement: char) -> String {
1218 let (registry, _builtins, ops, mut doc) = fixture(text);
1219 let cursor = Position::new(0, col);
1220 set_cursor(&mut doc, cursor);
1221 let inv = CommandInvocation::of(ops.change.0)
1222 .with_range(lattice_grammar::range::Range::CurrentLine)
1223 .with_args(Args::List(vec![
1224 ArgValue::Char(target),
1225 ArgValue::Char(replacement),
1226 ]));
1227 grammar_execute(
1228 ®istry,
1229 &mut doc,
1230 BufferId(0),
1231 cursor,
1232 inv,
1233 &CancellationToken::never(),
1234 )
1235 .unwrap();
1236 doc_text(&doc)
1237 }
1238
1239 fn delete_at(text: &str, col: u32, target: char) -> String {
1241 let (registry, _builtins, ops, mut doc) = fixture(text);
1242 let cursor = Position::new(0, col);
1243 set_cursor(&mut doc, cursor);
1244 let inv = CommandInvocation::of(ops.delete.0)
1245 .with_range(lattice_grammar::range::Range::CurrentLine)
1246 .with_args(Args::Char(target));
1247 grammar_execute(
1248 ®istry,
1249 &mut doc,
1250 BufferId(0),
1251 cursor,
1252 inv,
1253 &CancellationToken::never(),
1254 )
1255 .unwrap();
1256 doc_text(&doc)
1257 }
1258
1259 #[test]
1260 fn surround_add_with_the_opening_form_pads() {
1261 assert_eq!(add_linewise("hello\n", '('), "( hello )\n");
1262 assert_eq!(add_linewise("hello\n", '['), "[ hello ]\n");
1263 assert_eq!(add_linewise("hello\n", '{'), "{ hello }\n");
1264 assert_eq!(add_linewise("hello\n", '<'), "< hello >\n");
1265 }
1266
1267 #[test]
1268 fn surround_add_with_the_closing_form_does_not_pad() {
1269 assert_eq!(add_linewise("hello\n", ')'), "(hello)\n");
1270 assert_eq!(add_linewise("hello\n", ']'), "[hello]\n");
1271 assert_eq!(add_linewise("hello\n", '}'), "{hello}\n");
1272 assert_eq!(add_linewise("hello\n", '>'), "<hello>\n");
1273 }
1274
1275 #[test]
1278 fn surround_add_never_pads_a_symmetric_wrapper() {
1279 assert_eq!(add_linewise("hello\n", '"'), "\"hello\"\n");
1280 assert_eq!(add_linewise("hello\n", '\''), "'hello'\n");
1281 }
1282
1283 #[test]
1284 fn surround_change_pads_when_the_replacement_is_an_opening_form() {
1285 assert_eq!(change_at("\"hello\"", 3, '"', '('), "( hello )");
1286 assert_eq!(change_at("\"hello\"", 3, '"', ')'), "(hello)");
1287 }
1288
1289 #[test]
1293 fn surround_delete_with_the_opening_form_takes_the_padding_too() {
1294 assert_eq!(delete_at("( hello )", 3, '('), "hello");
1295 assert_eq!(delete_at("(hello)", 3, '('), "hello");
1297 }
1298
1299 #[test]
1303 fn surround_delete_with_the_closing_form_leaves_the_padding() {
1304 assert_eq!(delete_at("( hello )", 3, ')'), " hello ");
1305 }
1306
1307 #[test]
1308 fn surround_change_from_a_padded_pair_drops_the_padding() {
1309 assert_eq!(change_at("( hello )", 3, '(', '"'), "\"hello\"");
1310 assert_eq!(change_at("( hello )", 3, ')', '"'), "\" hello \"");
1312 }
1313
1314 #[test]
1318 fn the_padded_forms_round_trip() {
1319 for ch in ['(', '[', '{', '<'] {
1320 let added = add_linewise("hello\n", ch);
1321 let back = delete_at(added.trim_end_matches('\n'), 3, ch);
1322 assert_eq!(back, "hello", "round trip failed for {ch:?}");
1323 }
1324 }
1325
1326 #[test]
1327 fn surround_add_on_selection_wraps_text() {
1328 let (registry, _builtins, ops, mut doc) = fixture("hello world");
1329 let cursor = Position::new(0, 0);
1330 use lattice_protocol::selection::{Selection, SelectionSet};
1332 doc.set_selections(SelectionSet::from_parts(
1333 vec![Selection {
1334 anchor: Position::ZERO,
1335 head: Position::new(0, 5),
1336 visual: None,
1337 }],
1338 0,
1339 ));
1340 let inv = CommandInvocation::of(ops.add.0)
1341 .with_range(lattice_grammar::range::Range::Selection)
1342 .with_args(Args::Char('"'));
1343 let _eff = grammar_execute(
1344 ®istry,
1345 &mut doc,
1346 BufferId(0),
1347 cursor,
1348 inv,
1349 &CancellationToken::never(),
1350 )
1351 .unwrap();
1352 let text = doc_text(&doc);
1353 assert!(text.starts_with("\"hello"));
1356 assert!(text.ends_with("\" world") || text.ends_with("\"world"));
1357 }
1358
1359 #[test]
1360 fn surround_change_no_match_is_noop() {
1361 let (registry, _builtins, ops, mut doc) = fixture("hello world");
1362 let cursor = Position::ZERO;
1363 set_cursor(&mut doc, cursor);
1364 let inv = CommandInvocation::of(ops.change.0)
1365 .with_range(lattice_grammar::range::Range::CurrentLine)
1366 .with_args(Args::List(vec![ArgValue::Char('"'), ArgValue::Char('\'')]));
1367 let eff = grammar_execute(
1368 ®istry,
1369 &mut doc,
1370 BufferId(0),
1371 cursor,
1372 inv,
1373 &CancellationToken::never(),
1374 )
1375 .unwrap();
1376 assert!(matches!(eff, lattice_grammar::effect::Effect::None));
1377 assert_eq!(doc_text(&doc), "hello world");
1378 }
1379
1380 #[test]
1381 fn surround_add_with_unknown_wrapper_is_noop() {
1382 let (registry, _builtins, ops, mut doc) = fixture("hello\n");
1383 let cursor = Position::ZERO;
1384 let inv = CommandInvocation::of(ops.add.0)
1385 .with_range(lattice_grammar::range::Range::CurrentLine)
1386 .with_args(Args::Char('x')); let eff = grammar_execute(
1388 ®istry,
1389 &mut doc,
1390 BufferId(0),
1391 cursor,
1392 inv,
1393 &CancellationToken::never(),
1394 )
1395 .unwrap();
1396 assert!(matches!(eff, lattice_grammar::effect::Effect::None));
1397 }
1398
1399 #[test]
1402 fn surround_add_via_motion_word_forward() {
1403 let (registry, builtins, ops, mut doc) = fixture("hello world");
1405 let cursor = Position::new(0, 0); set_cursor(&mut doc, cursor);
1407 let inv = CommandInvocation::of(ops.add.0)
1408 .with_target(lattice_grammar::Target::Motion(
1409 builtins.word_forward,
1410 Args::None,
1411 ))
1412 .with_args(Args::Char('"'));
1413 grammar_execute(
1414 ®istry,
1415 &mut doc,
1416 BufferId(0),
1417 cursor,
1418 inv,
1419 &CancellationToken::never(),
1420 )
1421 .unwrap();
1422 assert!(
1425 doc_text(&doc).starts_with("\"hello"),
1426 "expected wrapped text to start with \"hello, got: {}",
1427 doc_text(&doc)
1428 );
1429 }
1430
1431 #[test]
1432 fn surround_add_via_inner_word_text_object() {
1433 let (registry, builtins, ops, mut doc) = fixture("hello world");
1435 let cursor = Position::new(0, 2); set_cursor(&mut doc, cursor);
1437 let inv = CommandInvocation::of(ops.add.0)
1438 .with_target(lattice_grammar::Target::TextObject(
1439 builtins.inner_word,
1440 Args::None,
1441 ))
1442 .with_args(Args::Char('"'));
1443 grammar_execute(
1444 ®istry,
1445 &mut doc,
1446 BufferId(0),
1447 cursor,
1448 inv,
1449 &CancellationToken::never(),
1450 )
1451 .unwrap();
1452 assert_eq!(doc_text(&doc), "\"hello\" world");
1453 }
1454}