feat: expand completion snippets and navigate placeholders
This commit is contained in:
+485
@@ -0,0 +1,485 @@
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn grammar_expands_nested_mirrors_choices_variables_and_normalized_offsets() {
|
||||
let (text, snippet) = expand(
|
||||
"call($1, ${1:λ${2:x}}, ${3|a\\,b,c|}, ${UNKNOWN:${TM_LINE_NUMBER}})\r\n$0\\$",
|
||||
|name| (name == "TM_LINE_NUMBER").then(|| "4".to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(text, "call(λx, λx, a,b, 4)\n$");
|
||||
assert_eq!(
|
||||
snippet.tabstops,
|
||||
vec![
|
||||
SnippetTabstop {
|
||||
index: 1,
|
||||
ranges: vec![5..8, 10..13],
|
||||
ancestors: vec![vec![], vec![]]
|
||||
},
|
||||
SnippetTabstop {
|
||||
index: 2,
|
||||
ranges: vec![7..8, 12..13],
|
||||
ancestors: vec![vec![1], vec![1]]
|
||||
},
|
||||
SnippetTabstop {
|
||||
index: 3,
|
||||
ranges: vec![15..18],
|
||||
ancestors: vec![vec![]]
|
||||
},
|
||||
SnippetTabstop {
|
||||
index: 0,
|
||||
ranges: vec![23..23],
|
||||
ancestors: vec![vec![]]
|
||||
},
|
||||
]
|
||||
);
|
||||
let (text, snippet) = expand("call($1, ${2})", |_| None).unwrap();
|
||||
assert_eq!(text, "call(, )");
|
||||
assert_eq!(
|
||||
snippet.tabstops,
|
||||
vec![
|
||||
SnippetTabstop {
|
||||
index: 1,
|
||||
ranges: vec![5..5],
|
||||
ancestors: vec![vec![]]
|
||||
},
|
||||
SnippetTabstop {
|
||||
index: 2,
|
||||
ranges: vec![7..7],
|
||||
ancestors: vec![vec![]]
|
||||
},
|
||||
SnippetTabstop {
|
||||
index: 0,
|
||||
ranges: vec![8..8],
|
||||
ancestors: vec![vec![]]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
expand("${KNOWN:${1:hidden}}$1", |name| {
|
||||
(name == "KNOWN").then(|| "shown".to_owned())
|
||||
})
|
||||
.unwrap()
|
||||
.0,
|
||||
"shown"
|
||||
);
|
||||
let (text, snippet) = expand("call(${CTX})$1", |name| {
|
||||
(name == "CTX").then(|| "a\r\nb\rc".to_owned())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(text, "call(a\nb\nc)");
|
||||
assert_eq!(snippet.tabstops[0].ranges, [11..11]);
|
||||
assert_eq!(
|
||||
expand("${1|a\\|b,c|}\\}\\\\", |_| None).unwrap().0,
|
||||
"a|b}\\"
|
||||
);
|
||||
for input in ["${1", "${1:$1}", "${1:a}${1:b}$1", "${1:e}\u{301}"] {
|
||||
if input == "${1:a}${1:b}$1" {
|
||||
assert_eq!(expand(input, |_| None).unwrap().0, "aaa");
|
||||
} else {
|
||||
assert_eq!(expand(input, |_| None), Err(Error::Invalid));
|
||||
}
|
||||
}
|
||||
let (text, snippet) = expand("${1}$1", |_| None).unwrap();
|
||||
assert!(text.is_empty());
|
||||
assert_eq!(
|
||||
snippet.tabstops[0],
|
||||
SnippetTabstop {
|
||||
index: 1,
|
||||
ranges: vec![0..0, 0..0],
|
||||
ancestors: vec![vec![], vec![]],
|
||||
}
|
||||
);
|
||||
for input in [
|
||||
"$UNKNOWN",
|
||||
"${TM_FILENAME/(.*)/$1/}",
|
||||
"${1/a/b/}",
|
||||
"${1:a$0b}",
|
||||
"$0$0",
|
||||
"${0:arg}",
|
||||
"${0:$1}",
|
||||
] {
|
||||
assert_eq!(expand(input, |_| None), Err(Error::Unsupported));
|
||||
}
|
||||
assert_eq!(
|
||||
expand(&"$1".repeat(MAX_STOPS + 1), |_| None),
|
||||
Err(Error::Limit)
|
||||
);
|
||||
assert_eq!(
|
||||
expand(
|
||||
&format!(
|
||||
"{}x{}",
|
||||
"${1:".repeat(MAX_DEPTH + 1),
|
||||
"}".repeat(MAX_DEPTH + 1)
|
||||
),
|
||||
|_| None
|
||||
),
|
||||
Err(Error::Limit)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user