1use core::ops::Range;
15
16use crate::decorations::{DecorationId, DecorationKind, DecorationStore, Stickiness};
17
18#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Snippet {
22 pub text: String,
26 pub stops: Vec<TabStop>,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct TabStop {
33 pub index: u16,
35 pub range: Range<u32>,
38 pub choices: Vec<String>,
40}
41
42impl TabStop {
43 #[must_use]
46 pub fn is_final(&self) -> bool {
47 self.index == u16::MAX
48 }
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
56pub enum SnippetError {
57 #[error("duplicate placeholder index {0}")]
60 DuplicateIndex(u16),
61 #[error("nested placeholder inside a default")]
63 Nesting,
64 #[error("malformed placeholder: {0}")]
66 Malformed(&'static str),
67}
68
69impl Snippet {
70 #[must_use]
82 pub fn for_insertion(&self, indent: &str, indent_size: usize) -> Snippet {
83 use std::collections::BTreeMap;
84 let tab = " ".repeat(indent_size);
85 let mut out = String::with_capacity(self.text.len());
86 let mut map: BTreeMap<u32, u32> = BTreeMap::new();
88 let mut it = self.text.char_indices().peekable();
89 while let Some((byte_i, c)) = it.next() {
90 map.insert(byte_i as u32, out.len() as u32);
91 match c {
92 '\r' => {
93 if it.peek().map(|&(_, c)| c) == Some('\n') {
94 it.next(); }
96 out.push('\n');
97 out.push_str(indent);
98 }
99 '\n' => {
100 out.push('\n');
101 out.push_str(indent);
102 }
103 '\t' => out.push_str(&tab),
104 _ => out.push(c),
105 }
106 }
107 map.insert(self.text.len() as u32, out.len() as u32);
108
109 let remap = |off: u32| map.get(&off).copied().unwrap_or(off);
110 let stops = self
111 .stops
112 .iter()
113 .map(|s| TabStop {
114 index: s.index,
115 range: remap(s.range.start)..remap(s.range.end),
116 choices: s.choices.clone(),
117 })
118 .collect();
119 Snippet { text: out, stops }
120 }
121
122 pub fn parse(body: &str) -> Result<Self, SnippetError> {
128 let chars: Vec<char> = body.chars().collect();
129 let mut text = String::new();
130 let mut stops: Vec<TabStop> = Vec::new();
131 let mut seen: Vec<u16> = Vec::new();
132 let mut i = 0;
133
134 while i < chars.len() {
135 match chars[i] {
136 '\\' if matches!(chars.get(i + 1), Some('$' | '}' | '\\')) => {
138 text.push(chars[i + 1]);
139 i += 2;
140 }
141 '$' if matches!(chars.get(i + 1), Some('{')) => {
142 i += 2; let index = read_index(&chars, &mut i)?;
144 match chars.get(i) {
145 Some('}') => {
146 i += 1;
147 let pos = text.len() as u32;
148 push_stop(&mut stops, &mut seen, index, pos..pos, Vec::new())?;
149 }
150 Some(':') => {
151 i += 1;
152 let start = text.len() as u32;
153 read_default(&chars, &mut i, &mut text)?;
154 let end = text.len() as u32;
155 push_stop(&mut stops, &mut seen, index, start..end, Vec::new())?;
156 }
157 Some('|') => {
158 i += 1;
159 let choices = read_choices(&chars, &mut i)?;
160 let start = text.len() as u32;
161 text.push_str(choices.first().map_or("", String::as_str));
162 let end = text.len() as u32;
163 push_stop(&mut stops, &mut seen, index, start..end, choices)?;
164 }
165 _ => return Err(SnippetError::Malformed("expected } : or | after ${N")),
166 }
167 }
168 '$' if matches!(chars.get(i + 1), Some(c) if c.is_ascii_digit()) => {
169 i += 1; let index = read_index(&chars, &mut i)?;
171 let pos = text.len() as u32;
172 push_stop(&mut stops, &mut seen, index, pos..pos, Vec::new())?;
173 }
174 c => {
176 text.push(c);
177 i += 1;
178 }
179 }
180 }
181
182 if !seen.contains(&u16::MAX) {
184 let pos = text.len() as u32;
185 stops.push(TabStop { index: u16::MAX, range: pos..pos, choices: Vec::new() });
186 }
187 stops.sort_by_key(|s| s.index);
189 Ok(Snippet { text, stops })
190 }
191}
192
193fn read_index(chars: &[char], i: &mut usize) -> Result<u16, SnippetError> {
196 let start = *i;
197 while matches!(chars.get(*i), Some(c) if c.is_ascii_digit()) {
198 *i += 1;
199 }
200 if *i == start {
201 return Err(SnippetError::Malformed("placeholder without an index"));
202 }
203 let n: u32 = chars[start..*i]
204 .iter()
205 .collect::<String>()
206 .parse()
207 .map_err(|_| SnippetError::Malformed("placeholder index out of range"))?;
208 if n == 0 {
209 Ok(u16::MAX)
210 } else {
211 u16::try_from(n).map_err(|_| SnippetError::Malformed("placeholder index out of range"))
212 }
213}
214
215fn read_default(chars: &[char], i: &mut usize, text: &mut String) -> Result<(), SnippetError> {
218 loop {
219 match chars.get(*i) {
220 None => return Err(SnippetError::Malformed("unterminated ${N:default}")),
221 Some('\\') if matches!(chars.get(*i + 1), Some('$' | '}' | '\\')) => {
222 text.push(chars[*i + 1]);
223 *i += 2;
224 }
225 Some('}') => {
226 *i += 1;
227 return Ok(());
228 }
229 Some('$') => return Err(SnippetError::Nesting),
230 Some(&c) => {
231 text.push(c);
232 *i += 1;
233 }
234 }
235 }
236}
237
238fn read_choices(chars: &[char], i: &mut usize) -> Result<Vec<String>, SnippetError> {
241 let mut choices = Vec::new();
242 let mut cur = String::new();
243 loop {
244 match chars.get(*i) {
245 None => return Err(SnippetError::Malformed("unterminated ${N|choices|}")),
246 Some('\\') if matches!(chars.get(*i + 1), Some('$' | '}' | '\\')) => {
247 cur.push(chars[*i + 1]);
248 *i += 2;
249 }
250 Some(',') => {
251 choices.push(core::mem::take(&mut cur));
252 *i += 1;
253 }
254 Some('|') => {
255 *i += 1;
256 if chars.get(*i) != Some(&'}') {
257 return Err(SnippetError::Malformed("choice not closed with |}"));
258 }
259 *i += 1;
260 choices.push(cur);
261 return Ok(choices);
262 }
263 Some(&c) => {
264 cur.push(c);
265 *i += 1;
266 }
267 }
268 }
269}
270
271#[derive(Clone, Debug, PartialEq, Eq)]
273pub enum TabOutcome {
274 Move(Range<u32>),
276 Finish(u32),
279 Stay,
281}
282
283#[derive(Clone, Debug, PartialEq, Eq)]
285pub enum CaretOutcome {
286 Move(Range<u32>),
288 Stay,
290 Escaped,
292}
293
294pub struct SnippetSession {
301 stops: Vec<DecorationId>,
303 active: usize,
306}
307
308impl SnippetSession {
309 pub fn start(snippet: &Snippet, base: u32, store: &mut DecorationStore) -> Option<(Self, Range<u32>)> {
315 if snippet.stops.len() < 2 {
316 return None; }
318 let stops: Vec<DecorationId> = snippet
319 .stops
320 .iter()
321 .enumerate()
322 .map(|(i, s)| {
323 let range = (base + s.range.start)..(base + s.range.end);
324 let stickiness = if i == 0 { Stickiness::AlwaysGrows } else { Stickiness::NeverGrows };
325 store.add_decoration(range, DecorationKind::SnippetStop { index: i as u8 }, stickiness)
326 })
327 .collect();
328 let session = Self { stops, active: 0 };
329 let first = store.decoration_range(session.stops[0]).expect("just registered");
330 Some((session, first))
331 }
332
333 #[must_use]
335 pub fn active_index(&self) -> usize {
336 self.active
337 }
338
339 #[must_use]
342 pub fn active_range(&self, store: &DecorationStore) -> Option<Range<u32>> {
343 store.decoration_range(self.stops[self.active])
344 }
345
346 pub fn tab(&mut self, forward: bool, store: &mut DecorationStore) -> TabOutcome {
350 let last = self.stops.len() - 1; if forward {
352 let next = self.active + 1;
353 if next == last {
354 let pos = store.decoration_range(self.stops[last]).map_or(0, |r| r.start);
355 self.cancel(store);
356 return TabOutcome::Finish(pos);
357 }
358 self.activate(next, store);
359 TabOutcome::Move(self.active_range(store).expect("active stop live"))
360 } else if self.active == 0 {
361 TabOutcome::Stay
362 } else {
363 self.activate(self.active - 1, store);
364 TabOutcome::Move(self.active_range(store).expect("active stop live"))
365 }
366 }
367
368 pub fn on_caret(&mut self, offset: u32, store: &mut DecorationStore) -> CaretOutcome {
372 let last = self.stops.len() - 1;
373 for i in 0..last {
374 if let Some(r) = store.decoration_range(self.stops[i]) {
375 if offset >= r.start && offset <= r.end {
376 if i == self.active {
377 return CaretOutcome::Stay;
378 }
379 self.activate(i, store);
380 return CaretOutcome::Move(self.active_range(store).expect("active stop live"));
381 }
382 }
383 }
384 CaretOutcome::Escaped
385 }
386
387 #[must_use]
390 pub fn edit_escapes(&self, range: &Range<u32>, store: &DecorationStore) -> bool {
391 !self.stops.iter().any(|&id| {
392 store.decoration_range(id).is_some_and(|r| range.start <= r.end && r.start <= range.end)
393 })
394 }
395
396 pub fn cancel(&mut self, store: &mut DecorationStore) {
398 for id in self.stops.drain(..) {
399 store.take_decoration(id);
400 }
401 }
402
403 fn activate(&mut self, i: usize, store: &mut DecorationStore) {
405 store.set_decoration_stickiness(self.stops[self.active], Stickiness::NeverGrows);
406 store.set_decoration_stickiness(self.stops[i], Stickiness::AlwaysGrows);
407 self.active = i;
408 }
409}
410
411fn push_stop(
413 stops: &mut Vec<TabStop>,
414 seen: &mut Vec<u16>,
415 index: u16,
416 range: Range<u32>,
417 choices: Vec<String>,
418) -> Result<(), SnippetError> {
419 if seen.contains(&index) {
420 return Err(SnippetError::DuplicateIndex(if index == u16::MAX { 0 } else { index }));
421 }
422 seen.push(index);
423 stops.push(TabStop { index, range, choices });
424 Ok(())
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 fn parse(body: &str) -> Snippet {
432 Snippet::parse(body).expect("valid snippet")
433 }
434
435 fn stop(index: u16, range: Range<u32>, choices: &[&str]) -> TabStop {
436 TabStop { index, range, choices: choices.iter().map(|s| s.to_string()).collect() }
437 }
438
439 #[test]
440 fn plain_body_has_only_an_implicit_final_stop_at_the_end() {
441 let s = parse("hello world");
442 assert_eq!(s.text, "hello world");
443 assert_eq!(s.stops, vec![stop(u16::MAX, 11..11, &[])]);
444 }
445
446 #[test]
447 fn default_placeholder_expands_and_ranges_cover_it() {
448 let s = parse("${1:name}");
449 assert_eq!(s.text, "name");
450 assert_eq!(s.stops, vec![stop(1, 0..4, &[]), stop(u16::MAX, 4..4, &[])]);
451 }
452
453 #[test]
454 fn empty_and_multiple_placeholders_track_positions() {
455 let s = parse("$1 and $2");
456 assert_eq!(s.text, " and ");
457 assert_eq!(s.stops, vec![stop(1, 0..0, &[]), stop(2, 5..5, &[]), stop(u16::MAX, 5..5, &[])]);
458 }
459
460 #[test]
461 fn explicit_final_stop_is_placed_and_not_duplicated_at_end() {
462 let s = parse("foo($0)bar");
463 assert_eq!(s.text, "foo()bar");
464 assert_eq!(s.stops, vec![stop(u16::MAX, 4..4, &[])]);
465 }
466
467 #[test]
468 fn choice_inserts_the_first_and_keeps_all() {
469 let s = parse("${1|hs_can,ms_can,kline|}");
470 assert_eq!(s.text, "hs_can");
471 assert_eq!(s.stops, vec![stop(1, 0..6, &["hs_can", "ms_can", "kline"]), stop(u16::MAX, 6..6, &[])]);
472 }
473
474 #[test]
475 fn escapes_are_literal_and_do_not_start_placeholders() {
476 let s = parse(r"\${1\} and \\");
477 assert_eq!(s.text, r"${1} and \");
478 assert_eq!(s.stops, vec![stop(u16::MAX, 10..10, &[])]);
479 }
480
481 #[test]
482 fn stops_sort_by_index_regardless_of_textual_order() {
483 let s = parse("${2:b}${1:a}");
484 assert_eq!(s.text, "ba");
485 assert_eq!(
487 s.stops,
488 vec![stop(1, 1..2, &[]), stop(2, 0..1, &[]), stop(u16::MAX, 2..2, &[])]
489 );
490 }
491
492 #[test]
493 fn duplicate_index_and_nesting_and_malformed_are_errors() {
494 assert_eq!(Snippet::parse("$1 $1"), Err(SnippetError::DuplicateIndex(1)));
495 assert_eq!(Snippet::parse("$0$0"), Err(SnippetError::DuplicateIndex(0)));
496 assert_eq!(Snippet::parse("${1:${2:x}}"), Err(SnippetError::Nesting));
497 assert!(matches!(Snippet::parse("${x}"), Err(SnippetError::Malformed(_))));
498 assert!(matches!(Snippet::parse("${1:oops"), Err(SnippetError::Malformed(_))));
499 }
500
501 #[test]
502 fn for_insertion_is_identity_on_a_flat_single_line() {
503 let s = parse("${1:name}").for_insertion(" ", 4);
504 assert_eq!(s.text, "name");
505 assert_eq!(s.stops, vec![stop(1, 0..4, &[]), stop(u16::MAX, 4..4, &[])]);
506 }
507
508 #[test]
509 fn for_insertion_reindents_continuation_lines_and_expands_tabs() {
510 let s = parse("fn ${1:id} {\n\t$0\n}").for_insertion(" ", 4);
512 assert_eq!(s.text, "fn id {\n \n }");
514 assert_eq!(s.stops[0], stop(1, 3..5, &[]));
516 assert!(s.stops[1].is_final());
517 assert_eq!(s.stops[1].range, 16..16);
519 }
520
521 #[test]
522 fn for_insertion_remaps_stops_across_the_rewrite() {
523 let s = parse("${1:a}\n${2:b}").for_insertion(" ", 4);
524 assert_eq!(s.text, "a\n b");
525 assert_eq!(
526 s.stops,
527 vec![stop(1, 0..1, &[]), stop(2, 4..5, &[]), stop(u16::MAX, 5..5, &[])]
528 );
529 }
530
531 #[test]
532 fn for_insertion_normalizes_crlf() {
533 let s = parse("a\r\nb").for_insertion("", 4);
534 assert_eq!(s.text, "a\nb");
535 }
536
537 #[test]
538 fn a_bare_dollar_not_starting_a_placeholder_is_literal() {
539 let s = parse("a $ b and c$d");
542 assert_eq!(s.text, "a $ b and c$d");
543 assert_eq!(s.stops, vec![stop(u16::MAX, 13..13, &[])]);
544 }
545
546 fn live_stops(store: &DecorationStore) -> Vec<(Range<u32>, Stickiness)> {
550 let mut v: Vec<_> = store
551 .iter()
552 .filter(|r| matches!(r.kind, DecorationKind::SnippetStop { .. }))
553 .map(|r| (r.range.clone(), r.stickiness))
554 .collect();
555 v.sort_by_key(|(r, _)| r.start);
556 v
557 }
558
559 #[test]
560 fn session_needs_a_stop_besides_the_final() {
561 let mut store = DecorationStore::new();
562 assert!(SnippetSession::start(&parse("$0"), 0, &mut store).is_none());
564 assert!(SnippetSession::start(&parse("plain"), 0, &mut store).is_none());
565 let (_s, range) = SnippetSession::start(&parse("${1:x}"), 0, &mut store).expect("session");
567 assert_eq!(range, 0..1);
568 }
569
570 #[test]
571 fn start_registers_stops_with_only_the_first_active() {
572 let mut store = DecorationStore::new();
573 let (_s, first) = SnippetSession::start(&parse("${1:a}${2:b}"), 0, &mut store).expect("session");
574 assert_eq!(first, 0..1);
575 assert_eq!(
576 live_stops(&store),
577 vec![
578 (0..1, Stickiness::AlwaysGrows), (1..2, Stickiness::NeverGrows), (2..2, Stickiness::NeverGrows), ]
582 );
583 }
584
585 #[test]
586 fn tab_moves_active_swaps_stickiness_then_finishes_at_the_final() {
587 let mut store = DecorationStore::new();
588 let (mut s, _) = SnippetSession::start(&parse("${1:a}${2:b}"), 0, &mut store).expect("session");
589 assert_eq!(s.tab(true, &mut store), TabOutcome::Move(1..2));
591 assert_eq!(
592 live_stops(&store),
593 vec![(0..1, Stickiness::NeverGrows), (1..2, Stickiness::AlwaysGrows), (2..2, Stickiness::NeverGrows)]
594 );
595 assert_eq!(s.tab(true, &mut store), TabOutcome::Finish(2));
597 assert!(live_stops(&store).is_empty());
598 }
599
600 #[test]
601 fn shift_tab_at_the_first_stop_is_a_no_op() {
602 let mut store = DecorationStore::new();
603 let (mut s, _) = SnippetSession::start(&parse("${1:a}${2:b}"), 0, &mut store).expect("session");
604 assert_eq!(s.tab(false, &mut store), TabOutcome::Stay);
605 assert_eq!(s.active_index(), 0);
606 assert_eq!(s.tab(true, &mut store), TabOutcome::Move(1..2));
608 assert_eq!(s.tab(false, &mut store), TabOutcome::Move(0..1));
609 assert_eq!(s.active_index(), 0);
610 }
611
612 #[test]
613 fn on_caret_reactivates_inside_a_stop_and_escapes_outside() {
614 let mut store = DecorationStore::new();
615 let (mut s, _) = SnippetSession::start(&parse("${1:aa} ${2:bb}"), 0, &mut store).expect("session");
616 assert_eq!(s.on_caret(4, &mut store), CaretOutcome::Move(3..5));
618 assert_eq!(s.active_index(), 1);
619 assert_eq!(s.on_caret(0, &mut store), CaretOutcome::Move(0..2));
621 assert_eq!(s.on_caret(1, &mut store), CaretOutcome::Stay);
623 assert_eq!(s.on_caret(99, &mut store), CaretOutcome::Escaped);
624 }
625
626 #[test]
627 fn edit_escapes_only_when_wholly_outside_every_stop() {
628 let mut store = DecorationStore::new();
629 let (s, _) = SnippetSession::start(&parse("${1:aa} ${2:bb}"), 0, &mut store).expect("session");
630 assert!(!s.edit_escapes(&(1..2), &store), "inside stop 1");
631 assert!(!s.edit_escapes(&(4..4), &store), "inside stop 2");
632 assert!(s.edit_escapes(&(10..12), &store), "past every stop");
633 }
634
635 #[test]
636 fn cancel_unregisters_all_ranges() {
637 let mut store = DecorationStore::new();
638 let (mut s, _) = SnippetSession::start(&parse("${1:a}${2:b}"), 0, &mut store).expect("session");
639 assert_eq!(live_stops(&store).len(), 3);
640 s.cancel(&mut store);
641 assert!(live_stops(&store).is_empty());
642 }
643}