feat: expand completion snippets and navigate placeholders
This commit is contained in:
+71
-8
@@ -22,6 +22,7 @@ use crate::{
|
||||
DocumentEventKind, DocumentSnapshot, PluginUpdate, SemanticToken, SemanticTokenKind,
|
||||
SemanticTokenModifier, TextPosition, UpdateEffect,
|
||||
},
|
||||
snippet,
|
||||
};
|
||||
|
||||
const MAX_BODY: usize = 4 * 1024 * 1024;
|
||||
@@ -683,7 +684,7 @@ impl Runtime {
|
||||
"dynamicRegistration": false, "contextSupport": true,
|
||||
"insertTextMode": 1,
|
||||
"completionItem": {
|
||||
"snippetSupport": false, "commitCharactersSupport": false,
|
||||
"snippetSupport": true, "commitCharactersSupport": false,
|
||||
"insertReplaceSupport": true,
|
||||
"insertTextModeSupport": {"valueSet": [1]},
|
||||
"resolveSupport": {"properties": []}
|
||||
@@ -1030,7 +1031,9 @@ impl Runtime {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
match result.and_then(|value| decode_completion(&snapshot.text, value, &source)) {
|
||||
match result.and_then(|value| {
|
||||
decode_completion(&snapshot.text, value, &source, snapshot.path.as_deref())
|
||||
}) {
|
||||
Ok(items) => self.emit(PluginUpdate {
|
||||
request_id: Some(source.request_id),
|
||||
effects: vec![UpdateEffect::Completion {
|
||||
@@ -2538,6 +2541,7 @@ fn decode_completion(
|
||||
text: &str,
|
||||
result: &Value,
|
||||
source: &Source,
|
||||
path: Option<&str>,
|
||||
) -> Result<Vec<CompletionItem>, String> {
|
||||
if result.is_null() {
|
||||
return Ok(Vec::new());
|
||||
@@ -2611,6 +2615,17 @@ fn decode_completion(
|
||||
}
|
||||
}
|
||||
}
|
||||
let word_end = caret_byte
|
||||
+ line[caret_byte..]
|
||||
.graphemes(true)
|
||||
.take_while(|cluster| {
|
||||
cluster
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|ch| ch.is_alphanumeric() || ch == '_')
|
||||
})
|
||||
.map(str::len)
|
||||
.sum::<usize>();
|
||||
let mut decoded = Vec::with_capacity(items.len());
|
||||
let mut bytes = 0usize;
|
||||
for item in items {
|
||||
@@ -2653,8 +2668,8 @@ fn decode_completion(
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(1);
|
||||
// No snippets, indentation adjustment, server commands or lazy edit resolution.
|
||||
if format == 2 || mode == 2 || item.get("command").is_some() {
|
||||
// No indentation adjustment, server commands or lazy edit resolution.
|
||||
if mode == 2 || item.get("command").is_some() {
|
||||
continue;
|
||||
}
|
||||
let explicit = item.get("textEdit");
|
||||
@@ -2669,7 +2684,7 @@ fn decode_completion(
|
||||
(start, end, new_text)
|
||||
} else if let Some(range) = default_range {
|
||||
let (start, end) = completion_replacement(&lines, range, source)?;
|
||||
(start, end, edit_text.unwrap_or(label))
|
||||
(start, end, edit_text.or(insert).unwrap_or(label))
|
||||
} else {
|
||||
(
|
||||
TextPosition {
|
||||
@@ -2693,10 +2708,25 @@ fn decode_completion(
|
||||
prefix
|
||||
};
|
||||
completion_text(new_text, true)?;
|
||||
let (new_text, snippet) = if format == 2 {
|
||||
match snippet::expand(new_text, |name| {
|
||||
completion_variable(name, line, &line[prefix_byte..word_end], source.row, path)
|
||||
}) {
|
||||
Ok((text, snippet)) => (text, Some(snippet)),
|
||||
Err(snippet::Error::Unsupported) => continue,
|
||||
Err(snippet::Error::Invalid) => return Err("invalid completion snippet".into()),
|
||||
Err(snippet::Error::Limit) => {
|
||||
return Err("completion snippet exceeds expansion limits".into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(normalized(new_text).into_owned(), None)
|
||||
};
|
||||
completion_text(&new_text, true)?;
|
||||
let mut edits = vec![CompletionEdit {
|
||||
start,
|
||||
end,
|
||||
text: normalized(new_text).into_owned(),
|
||||
text: new_text,
|
||||
}];
|
||||
if let Some(additional) = item.get("additionalTextEdits") {
|
||||
for edit in additional
|
||||
@@ -2749,6 +2779,7 @@ fn decode_completion(
|
||||
detail: detail.map(str::to_owned),
|
||||
primary_edit,
|
||||
edits,
|
||||
snippet,
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -2756,6 +2787,38 @@ fn decode_completion(
|
||||
Ok(decoded.into_iter().map(|(_, item)| item).collect())
|
||||
}
|
||||
|
||||
fn completion_variable(
|
||||
name: &str,
|
||||
line: &str,
|
||||
word: &str,
|
||||
row: usize,
|
||||
path: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let value = match name {
|
||||
"TM_SELECTED_TEXT" => "",
|
||||
"TM_CURRENT_LINE" => line,
|
||||
"TM_CURRENT_WORD" => word,
|
||||
"TM_LINE_INDEX" => return Some(row.to_string()),
|
||||
"TM_LINE_NUMBER" => return Some((row + 1).to_string()),
|
||||
"TM_FILEPATH" => path.unwrap_or(""),
|
||||
"TM_FILENAME" => path
|
||||
.and_then(|path| Path::new(path).file_name())
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(""),
|
||||
"TM_FILENAME_BASE" => path
|
||||
.and_then(|path| Path::new(path).file_stem())
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(""),
|
||||
"TM_DIRECTORY" => path
|
||||
.and_then(|path| Path::new(path).parent())
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or(""),
|
||||
_ => return None,
|
||||
};
|
||||
// Context values have the same per-expansion bound as template text.
|
||||
(value.len() <= 256 * 1024).then(|| value.to_owned())
|
||||
}
|
||||
|
||||
fn coordinates(value: &Value) -> Result<(usize, usize), String> {
|
||||
let line = value["line"]
|
||||
.as_u64()
|
||||
@@ -3077,7 +3140,7 @@ mod tests {
|
||||
};
|
||||
let at_limit = json!([{"label": "x", "insertText": "x".repeat(MAX_BODY - 1)}]);
|
||||
assert_eq!(
|
||||
decode_completion("", &at_limit, &source).unwrap()[0].edits[0]
|
||||
decode_completion("", &at_limit, &source, None).unwrap()[0].edits[0]
|
||||
.text
|
||||
.len(),
|
||||
MAX_BODY - 1
|
||||
@@ -3087,7 +3150,7 @@ mod tests {
|
||||
{"label": "y"}
|
||||
]);
|
||||
assert!(
|
||||
decode_completion("", &over_limit, &source)
|
||||
decode_completion("", &over_limit, &source, None)
|
||||
.unwrap_err()
|
||||
.contains("4 MiB")
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user