1use std::collections::{BTreeMap, HashMap};
17
18use crate::{AnchorBias, TextAnchor, TextDelta};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct SnippetRange {
23 pub start: usize,
25 pub end: usize,
27}
28
29impl SnippetRange {
30 fn offset_by(&self, base: usize) -> Self {
31 Self {
32 start: self.start.saturating_add(base),
33 end: self.end.saturating_add(base),
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SnippetTabstop {
46 pub index: u32,
48 pub ranges: Vec<SnippetRange>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct SnippetTemplate {
55 pub text: String,
57 pub tabstops: Vec<SnippetTabstop>,
59 pub final_offset: usize,
61}
62
63#[derive(Debug, Default, Clone)]
64struct SnippetParts {
65 text: String,
66 text_char_len: usize,
67 tabstops: BTreeMap<u32, Vec<SnippetRange>>,
68 final_offset: Option<usize>,
71}
72
73impl SnippetParts {
74 fn push_char(&mut self, ch: char) {
75 self.text.push(ch);
76 self.text_char_len = self.text_char_len.saturating_add(1);
77 }
78
79 fn push_str(&mut self, s: &str) {
80 self.text.push_str(s);
81 self.text_char_len = self.text_char_len.saturating_add(s.chars().count());
82 }
83
84 fn record_tabstop_range(&mut self, idx: u32, range: SnippetRange) {
85 if idx == 0 {
86 return;
87 }
88 self.tabstops.entry(idx).or_default().push(range);
89 }
90
91 fn merge_with_offset(&mut self, other: SnippetParts, base: usize) {
92 self.push_str(&other.text);
93
94 for (idx, mut ranges) in other.tabstops {
95 for r in &mut ranges {
96 *r = r.offset_by(base);
97 }
98 self.tabstops.entry(idx).or_default().extend(ranges);
99 }
100
101 if self.final_offset.is_none()
102 && let Some(off) = other.final_offset
103 {
104 self.final_offset = Some(base.saturating_add(off));
105 }
106 }
107
108 fn finish(mut self) -> SnippetTemplate {
109 let mut tabstops: Vec<SnippetTabstop> = self
110 .tabstops
111 .into_iter()
112 .filter(|(idx, _)| *idx != 0)
113 .map(|(index, mut ranges)| {
114 ranges.sort_by_key(|r| (r.start, r.end));
115 SnippetTabstop { index, ranges }
116 })
117 .collect();
118 tabstops.sort_by_key(|t| t.index);
119
120 let final_offset = self.final_offset.unwrap_or(self.text_char_len);
121 if final_offset > self.text_char_len {
122 self.final_offset = Some(self.text_char_len);
123 }
124
125 SnippetTemplate {
126 text: self.text,
127 tabstops,
128 final_offset: final_offset.min(self.text_char_len),
129 }
130 }
131}
132
133const MAX_SNIPPET_PARSE_DEPTH: usize = 32;
134
135pub fn parse_snippet(snippet: &str) -> SnippetTemplate {
146 let mut chars = snippet.chars().peekable();
147 let mut ctx = ParseCtx::default();
148 parse_until(&mut chars, None, 0, &mut ctx).finish()
149}
150
151fn parse_until<I>(
152 chars: &mut std::iter::Peekable<I>,
153 terminator: Option<char>,
154 depth: usize,
155 ctx: &mut ParseCtx,
156) -> SnippetParts
157where
158 I: Iterator<Item = char>,
159{
160 if depth > MAX_SNIPPET_PARSE_DEPTH {
161 let mut parts = SnippetParts::default();
163 for ch in chars.by_ref() {
164 if terminator == Some(ch) {
165 break;
166 }
167 parts.push_char(ch);
168 }
169 return parts;
170 }
171
172 let mut parts = SnippetParts::default();
173
174 while let Some(ch) = chars.next() {
175 if terminator == Some(ch) {
176 break;
177 }
178
179 match ch {
180 '\\' => {
181 if let Some(next) = chars.next() {
183 parts.push_char(next);
184 }
185 }
186 '$' => match chars.peek().copied() {
187 Some('{') => {
188 chars.next(); parse_braced_expression(&mut parts, chars, depth + 1, ctx);
190 }
191 Some(d) if d.is_ascii_digit() => {
192 let idx = parse_number(chars);
193 insert_tabstop_reference(&mut parts, ctx, idx);
194 }
195 Some(c) if c == '_' || c.is_ascii_alphabetic() => {
196 parse_identifier(chars);
198 }
199 _ => parts.push_char('$'),
200 },
201 other => parts.push_char(other),
202 }
203 }
204
205 parts
206}
207
208#[derive(Debug, Default)]
209struct ParseCtx {
210 tabstop_defaults: HashMap<u32, String>,
211}
212
213fn parse_number<I>(chars: &mut std::iter::Peekable<I>) -> u32
214where
215 I: Iterator<Item = char>,
216{
217 let mut num: u32 = 0;
218 let mut saw_any = false;
219 while let Some(ch) = chars.peek().copied() {
220 if !ch.is_ascii_digit() {
221 break;
222 }
223 saw_any = true;
224 chars.next();
225 num = num
226 .saturating_mul(10)
227 .saturating_add((ch as u8 - b'0') as u32);
228 }
229 if saw_any { num } else { 0 }
230}
231
232fn parse_identifier<I>(chars: &mut std::iter::Peekable<I>) -> String
233where
234 I: Iterator<Item = char>,
235{
236 let mut out = String::new();
237 while let Some(ch) = chars.peek().copied() {
238 if ch == '_' || ch.is_ascii_alphanumeric() {
239 chars.next();
240 out.push(ch);
241 } else {
242 break;
243 }
244 }
245 out
246}
247
248fn parse_braced_expression<I>(
249 parts: &mut SnippetParts,
250 chars: &mut std::iter::Peekable<I>,
251 depth: usize,
252 ctx: &mut ParseCtx,
253) where
254 I: Iterator<Item = char>,
255{
256 let ident_first = chars.peek().copied();
258 let is_number = matches!(ident_first, Some(c) if c.is_ascii_digit());
259
260 if is_number {
261 let idx = parse_number(chars);
262 match chars.peek().copied() {
263 Some('}') => {
264 chars.next();
265 insert_tabstop_reference(parts, ctx, idx);
266 }
267 Some(':') => {
268 chars.next();
269 parse_tabstop_default(parts, chars, depth, idx, ctx);
270 }
271 Some('|') => {
272 chars.next();
273 parse_tabstop_choice(parts, chars, idx, ctx);
274 }
275 _ => {
276 consume_until_closing_brace(chars);
278 }
279 }
280 return;
281 }
282
283 let name = parse_identifier(chars);
284 match chars.peek().copied() {
285 Some('}') => {
286 chars.next();
287 }
289 Some(':') => {
290 chars.next();
291 let start = parts.text_char_len;
292 let inner = parse_until(chars, Some('}'), depth, ctx);
293 parts.merge_with_offset(inner, start);
294 }
295 _ => {
296 consume_until_closing_brace(chars);
298 }
299 }
300
301 let _ = name; }
303
304fn parse_tabstop_default<I>(
305 parts: &mut SnippetParts,
306 chars: &mut std::iter::Peekable<I>,
307 depth: usize,
308 idx: u32,
309 ctx: &mut ParseCtx,
310) where
311 I: Iterator<Item = char>,
312{
313 let start = parts.text_char_len;
317 let inner = parse_until(chars, Some('}'), depth, ctx);
318
319 if idx == 0 {
320 if parts.final_offset.is_none() {
322 parts.final_offset = Some(start);
323 }
324 return;
325 }
326
327 let placeholder_start = parts.text_char_len;
328 if let std::collections::hash_map::Entry::Vacant(e) = ctx.tabstop_defaults.entry(idx) {
329 e.insert(inner.text.clone());
330 }
331 parts.merge_with_offset(inner, placeholder_start);
332 let placeholder_end = parts.text_char_len;
333 parts.record_tabstop_range(
334 idx,
335 SnippetRange {
336 start: placeholder_start,
337 end: placeholder_end,
338 },
339 );
340}
341
342fn parse_tabstop_choice<I>(
343 parts: &mut SnippetParts,
344 chars: &mut std::iter::Peekable<I>,
345 idx: u32,
346 ctx: &mut ParseCtx,
347) where
348 I: Iterator<Item = char>,
349{
350 let mut options: Vec<String> = Vec::new();
352 let mut current = String::new();
353
354 while let Some(ch) = chars.next() {
355 match ch {
356 '\\' => {
357 if let Some(next) = chars.next() {
358 current.push(next);
359 }
360 }
361 ',' => {
362 options.push(current);
363 current = String::new();
364 }
365 '|' => {
366 if chars.peek().copied() == Some('}') {
367 chars.next(); options.push(current);
369 break;
370 }
371 current.push('|');
372 }
373 other => current.push(other),
374 }
375 }
376
377 if idx == 0 {
378 if parts.final_offset.is_none() {
380 parts.final_offset = Some(parts.text_char_len);
381 }
382 return;
383 }
384
385 let insert_text = options.first().map(String::as_str).unwrap_or("");
386 ctx.tabstop_defaults
387 .entry(idx)
388 .or_insert_with(|| insert_text.to_string());
389 let start = parts.text_char_len;
390 parts.push_str(insert_text);
391 let end = parts.text_char_len;
392 parts.record_tabstop_range(idx, SnippetRange { start, end });
393}
394
395fn insert_tabstop_reference(parts: &mut SnippetParts, ctx: &mut ParseCtx, idx: u32) {
396 if idx == 0 {
397 if parts.final_offset.is_none() {
398 parts.final_offset = Some(parts.text_char_len);
399 }
400 return;
401 }
402
403 if let Some(default) = ctx.tabstop_defaults.get(&idx) {
404 let start = parts.text_char_len;
405 parts.push_str(default);
406 let end = parts.text_char_len;
407 parts.record_tabstop_range(idx, SnippetRange { start, end });
408 } else {
409 let at = parts.text_char_len;
410 parts.record_tabstop_range(idx, SnippetRange { start: at, end: at });
411 }
412}
413
414fn consume_until_closing_brace<I>(chars: &mut std::iter::Peekable<I>)
415where
416 I: Iterator<Item = char>,
417{
418 while let Some(ch) = chars.next() {
419 match ch {
420 '\\' => {
421 let _ = chars.next();
422 }
423 '}' => break,
424 _ => {}
425 }
426 }
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
430struct AnchoredRange {
431 start: TextAnchor,
432 end: TextAnchor,
433}
434
435impl AnchoredRange {
436 fn from_offsets(start: usize, end: usize) -> Self {
437 Self {
438 start: TextAnchor::new(start, AnchorBias::Left),
439 end: TextAnchor::new(end, AnchorBias::Right),
440 }
441 }
442
443 fn offsets(&self) -> (usize, usize) {
444 let a = self.start.offset;
445 let b = self.end.offset;
446 (a.min(b), a.max(b))
447 }
448
449 fn apply_delta(&mut self, delta: &TextDelta) {
450 self.start.apply_delta(delta);
451 self.end.apply_delta(delta);
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq)]
456struct AnchoredTabstop {
457 index: u32,
458 ranges: Vec<AnchoredRange>,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
466pub struct SnippetSession {
467 tabstops: Vec<AnchoredTabstop>,
468 final_anchor: TextAnchor,
469 active_tabstop_index: usize,
470}
471
472#[derive(Debug, Clone, PartialEq, Eq)]
474pub(crate) enum SnippetNavigation {
475 SelectRanges(Vec<(usize, usize)>),
477 Finish(usize),
479 Noop,
481}
482
483impl SnippetSession {
484 pub fn new(insert_start: usize, template: &SnippetTemplate) -> Option<Self> {
489 if template.tabstops.is_empty() {
490 return None;
491 }
492
493 let mut tabstops: Vec<AnchoredTabstop> = Vec::with_capacity(template.tabstops.len());
494 for t in &template.tabstops {
495 let mut ranges: Vec<AnchoredRange> = Vec::with_capacity(t.ranges.len());
496 for r in &t.ranges {
497 let start = insert_start.saturating_add(r.start);
498 let end = insert_start.saturating_add(r.end);
499 ranges.push(AnchoredRange::from_offsets(start, end));
500 }
501
502 tabstops.push(AnchoredTabstop {
503 index: t.index,
504 ranges,
505 });
506 }
507
508 Some(Self {
509 tabstops,
510 final_anchor: TextAnchor::new(
511 insert_start.saturating_add(template.final_offset),
512 AnchorBias::Right,
513 ),
514 active_tabstop_index: 0,
515 })
516 }
517
518 pub fn is_active(&self) -> bool {
520 !self.tabstops.is_empty()
521 }
522
523 pub fn apply_delta(&mut self, delta: &TextDelta) {
525 for t in &mut self.tabstops {
526 for r in &mut t.ranges {
527 r.apply_delta(delta);
528 }
529 }
530 self.final_anchor.apply_delta(delta);
531 }
532
533 pub(crate) fn current_ranges(&self) -> Vec<(usize, usize)> {
534 self.tabstops
535 .get(self.active_tabstop_index)
536 .map(|t| {
537 let mut ranges: Vec<(usize, usize)> =
538 t.ranges.iter().map(|r| r.offsets()).collect();
539 ranges.sort_by_key(|r| (r.0, r.1));
540 ranges
541 })
542 .unwrap_or_default()
543 }
544
545 pub(crate) fn next(&mut self) -> SnippetNavigation {
546 if self.tabstops.is_empty() {
547 return SnippetNavigation::Noop;
548 }
549
550 let next_index = self.active_tabstop_index.saturating_add(1);
551 if next_index >= self.tabstops.len() {
552 return SnippetNavigation::Finish(self.final_anchor.offset);
553 }
554
555 self.active_tabstop_index = next_index;
556 SnippetNavigation::SelectRanges(self.current_ranges())
557 }
558
559 pub(crate) fn prev(&mut self) -> SnippetNavigation {
560 if self.tabstops.is_empty() {
561 return SnippetNavigation::Noop;
562 }
563
564 if self.active_tabstop_index == 0 {
565 return SnippetNavigation::SelectRanges(self.current_ranges());
566 }
567
568 self.active_tabstop_index = self.active_tabstop_index.saturating_sub(1);
569 SnippetNavigation::SelectRanges(self.current_ranges())
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn parses_basic_placeholders_and_final_offset() {
579 let t = parse_snippet("println!(${1:msg})$0");
580 assert_eq!(t.text, "println!(msg)");
581 assert_eq!(t.final_offset, "println!(msg)".chars().count());
582 assert_eq!(t.tabstops.len(), 1);
583 assert_eq!(t.tabstops[0].index, 1);
584 assert_eq!(
585 t.tabstops[0].ranges,
586 vec![SnippetRange {
587 start: "println!(".chars().count(),
588 end: "println!(msg".chars().count()
589 }]
590 );
591 }
592
593 #[test]
594 fn parses_choice_placeholder_picks_first() {
595 let t = parse_snippet("${1|a,b,c|} $0");
596 assert_eq!(t.text, "a ");
597 assert_eq!(t.tabstops.len(), 1);
598 assert_eq!(t.tabstops[0].ranges[0], SnippetRange { start: 0, end: 1 });
599 assert_eq!(t.final_offset, 2);
600 }
601
602 #[test]
603 fn parses_variable_with_default_inserts_default() {
604 let t = parse_snippet("${TM_FILENAME:main.rs} $0");
605 assert_eq!(t.text, "main.rs ");
606 assert!(t.tabstops.is_empty());
607 assert_eq!(t.final_offset, "main.rs ".chars().count());
608 }
609
610 #[test]
611 fn mirrored_tabstops_insert_default_text() {
612 let t = parse_snippet("${1:foo} = $1; $0");
613 assert_eq!(t.text, "foo = foo; ");
614 assert_eq!(t.tabstops.len(), 1);
615 assert_eq!(t.tabstops[0].index, 1);
616 assert_eq!(t.tabstops[0].ranges.len(), 2);
617 }
618}