Files
vamp/src/snippet.rs
T
2026-10-16 18:53:02 +02:00

367 lines
12 KiB
Rust

use std::{collections::BTreeMap, ops::Range};
use unicode_segmentation::UnicodeSegmentation;
use crate::plugin::{CompletionSnippet, SnippetTabstop};
const MAX_BYTES: usize = 256 * 1024;
const MAX_NODES: usize = 4096;
const MAX_STOPS: usize = 1024;
const MAX_DEPTH: usize = 32;
const MAX_WORK: usize = 1024 * 1024;
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Error {
Invalid,
Unsupported,
Limit,
}
enum Node<'a> {
Text(&'a str),
Value(String),
Stop(u32, Option<Vec<usize>>),
Default(Vec<usize>),
}
struct Parser<'a, F> {
text: &'a str,
cursor: usize,
nodes: Vec<Node<'a>>,
value_bytes: usize,
variable: F,
}
/// Parse normalized snippet text before assigning any byte offsets. Unsupported
/// constructs omit the entire candidate, never leaking template syntax into text.
pub(crate) fn expand(
text: &str,
variable: impl Fn(&str) -> Option<String>,
) -> Result<(String, CompletionSnippet), Error> {
if text.len() > MAX_BYTES {
return Err(Error::Limit);
}
let normalized = if text.contains('\r') {
std::borrow::Cow::Owned(text.replace("\r\n", "\n").replace('\r', "\n"))
} else {
std::borrow::Cow::Borrowed(text)
};
let mut parser = Parser {
text: &normalized,
cursor: 0,
nodes: Vec::new(),
value_bytes: 0,
variable,
};
let roots = parser.sequence(false, 0)?;
let mut definitions = BTreeMap::new();
let mut pending: Vec<_> = roots.iter().rev().copied().collect();
while let Some(id) = pending.pop() {
match &parser.nodes[id] {
Node::Stop(index, Some(children)) => {
definitions.entry(*index).or_insert(id);
pending.extend(children.iter().rev().copied());
}
Node::Default(children) => pending.extend(children.iter().rev().copied()),
_ => {}
}
}
let mut rendered = Render {
nodes: &parser.nodes,
definitions: &definitions,
text: String::new(),
stops: BTreeMap::new(),
ancestors: Vec::new(),
occurrences: 0,
work: 0,
};
rendered.sequence(&roots, 0)?;
if !rendered.stops.contains_key(&0) {
if rendered.occurrences == MAX_STOPS {
return Err(Error::Limit);
}
rendered.stops.insert(
0,
SnippetTabstop {
index: 0,
ranges: std::iter::once(rendered.text.len()..rendered.text.len()).collect(),
ancestors: vec![Vec::new()],
},
);
}
let final_stop = rendered.stops.get(&0).unwrap();
if final_stop.ranges.len() != 1
|| !final_stop.ranges[0].is_empty()
|| !final_stop.ancestors[0].is_empty()
|| rendered.stops.values().any(|stop| {
stop.ancestors
.iter()
.any(|ancestors| ancestors.contains(&0))
})
{
return Err(Error::Unsupported);
}
let boundaries: Vec<_> = rendered
.text
.grapheme_indices(true)
.map(|(offset, _)| offset)
.chain(std::iter::once(rendered.text.len()))
.collect();
for stop in rendered.stops.values() {
let mut previous: Option<&Range<usize>> = None;
for range in &stop.ranges {
if boundaries.binary_search(&range.start).is_err()
|| boundaries.binary_search(&range.end).is_err()
|| previous.is_some_and(|old| {
old.end > range.start
|| (old.start == range.start && !(old.is_empty() && range.is_empty()))
})
{
return Err(Error::Invalid);
}
previous = Some(range);
}
}
let final_stop = rendered.stops.remove(&0).unwrap();
let mut tabstops: Vec<_> = rendered.stops.into_values().collect();
tabstops.push(final_stop);
Ok((rendered.text, CompletionSnippet { tabstops }))
}
impl<'a, F: Fn(&str) -> Option<String>> Parser<'a, F> {
fn node(&mut self, mut node: Node<'a>) -> Result<usize, Error> {
if self.nodes.len() == MAX_NODES {
return Err(Error::Limit);
}
if let Node::Value(value) = &mut node {
if value.len() > MAX_BYTES {
return Err(Error::Limit);
}
if value.contains('\r') {
*value = value.replace("\r\n", "\n").replace('\r', "\n");
}
self.value_bytes = self
.value_bytes
.checked_add(value.len())
.filter(|size| *size <= MAX_BYTES)
.ok_or(Error::Limit)?;
}
let id = self.nodes.len();
self.nodes.push(node);
Ok(id)
}
fn byte(&self) -> Option<u8> {
self.text.as_bytes().get(self.cursor).copied()
}
fn sequence(&mut self, nested: bool, depth: usize) -> Result<Vec<usize>, Error> {
if depth > MAX_DEPTH {
return Err(Error::Limit);
}
let mut nodes = Vec::new();
while let Some(byte) = self.byte() {
if nested && byte == b'}' {
self.cursor += 1;
return Ok(nodes);
}
if byte == b'$' {
nodes.push(self.construct(depth)?);
} else if byte == b'\\' {
self.cursor += 1;
let start = self.cursor;
if !matches!(self.byte(), Some(b'$' | b'}' | b'\\')) {
return Err(Error::Invalid);
}
self.cursor += 1;
nodes.push(self.node(Node::Text(&self.text[start..self.cursor]))?);
} else {
let start = self.cursor;
while let Some(next) = self.byte() {
if next == b'$' || next == b'\\' || (nested && next == b'}') {
break;
}
self.cursor += 1;
}
nodes.push(self.node(Node::Text(&self.text[start..self.cursor]))?);
}
}
if nested {
Err(Error::Invalid)
} else {
Ok(nodes)
}
}
fn construct(&mut self, depth: usize) -> Result<usize, Error> {
self.cursor += 1;
let braced = self.byte() == Some(b'{');
self.cursor += usize::from(braced);
let start = self.cursor;
let numbered = self.byte().is_some_and(|byte| byte.is_ascii_digit());
if numbered {
while self.byte().is_some_and(|byte| byte.is_ascii_digit()) {
self.cursor += 1;
}
} else {
if !self
.byte()
.is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
{
return Err(Error::Invalid);
}
self.cursor += 1;
while self
.byte()
.is_some_and(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
{
self.cursor += 1;
}
}
let name = &self.text[start..self.cursor];
let mut children = None;
if braced {
match self.byte() {
Some(b'}') => self.cursor += 1,
Some(b':') => {
self.cursor += 1;
children = Some(self.sequence(true, depth + 1)?);
}
Some(b'|') if numbered => {
self.cursor += 1;
children = Some(self.choice()?);
}
Some(b'/') => return Err(Error::Unsupported),
_ => return Err(Error::Invalid),
}
}
if numbered {
let index = name.parse::<u32>().map_err(|_| Error::Limit)?;
self.node(Node::Stop(index, children))
} else {
match (self.variable)(name) {
Some(value) if !value.is_empty() => self.node(Node::Value(value)),
value => match children {
Some(children) => self.node(Node::Default(children)),
None if value.is_some() => self.node(Node::Text("")),
None => Err(Error::Unsupported),
},
}
}
}
fn choice(&mut self) -> Result<Vec<usize>, Error> {
let mut first = String::new();
let mut keep = true;
loop {
match self.byte().ok_or(Error::Invalid)? {
b'|' => {
self.cursor += 1;
if self.byte() != Some(b'}') {
return Err(Error::Invalid);
}
self.cursor += 1;
return Ok(vec![self.node(Node::Value(first))?]);
}
b',' => {
keep = false;
self.cursor += 1;
}
b'\\' => {
self.cursor += 1;
let byte = self.byte().ok_or(Error::Invalid)?;
if !matches!(byte, b'$' | b'}' | b'\\' | b',' | b'|') {
return Err(Error::Invalid);
}
if keep {
first.push(byte as char);
}
self.cursor += 1;
}
_ => {
let ch = self.text[self.cursor..].chars().next().unwrap();
if keep {
first.push(ch);
}
self.cursor += ch.len_utf8();
}
}
}
}
}
struct Render<'a> {
nodes: &'a [Node<'a>],
definitions: &'a BTreeMap<u32, usize>,
text: String,
stops: BTreeMap<u32, SnippetTabstop>,
ancestors: Vec<u32>,
occurrences: usize,
work: usize,
}
impl Render<'_> {
fn sequence(&mut self, nodes: &[usize], depth: usize) -> Result<(), Error> {
if depth > MAX_DEPTH {
return Err(Error::Limit);
}
let arena = self.nodes;
for &id in nodes {
self.work += 1;
if self.work > MAX_WORK {
return Err(Error::Limit);
}
match &arena[id] {
Node::Text(text) => self.append(text)?,
Node::Value(text) => self.append(text)?,
Node::Default(children) => self.sequence(children, depth + 1)?,
Node::Stop(index, _) => {
if self.occurrences == MAX_STOPS {
return Err(Error::Limit);
}
self.occurrences += 1;
let start = self.text.len();
let ancestors = self.ancestors.clone();
if let Some(&definition) = self.definitions.get(index) {
if self.ancestors.contains(index) {
return Err(Error::Invalid);
}
self.ancestors.push(*index);
if let Node::Stop(_, Some(children)) = &arena[definition] {
self.sequence(children, depth + 1)?;
}
self.ancestors.pop();
}
let stop = self.stops.entry(*index).or_insert_with(|| SnippetTabstop {
index: *index,
ranges: Vec::new(),
ancestors: Vec::new(),
});
stop.ranges.push(start..self.text.len());
stop.ancestors.push(ancestors);
}
}
}
Ok(())
}
fn append(&mut self, text: &str) -> Result<(), Error> {
if self
.text
.len()
.checked_add(text.len())
.is_none_or(|size| size > MAX_BYTES)
{
return Err(Error::Limit);
}
self.text.push_str(text);
Ok(())
}
}
#[cfg(test)]
// These fixtures intentionally contain ranges, not lists of indices.
#[allow(clippy::single_range_in_vec_init)]
#[path = "../tests/unit/snippet.rs"]
mod tests;