Skip to content

Commit 47eac07

Browse files
authored
Add snippet sanitization for Zed completion items (#237)
Replace `$TM_SELECTED_TEXT` variable in completion snippets since Zed doesn't support VS Code-specific snippet variables. Address #235
1 parent 93771eb commit 47eac07

2 files changed

Lines changed: 61 additions & 30 deletions

File tree

proxy/src/completions.rs

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
use serde_json::Value;
22

3-
pub fn should_sort_completions(msg: &Value) -> bool {
3+
/// Returns true if the message contains a completion response with items.
4+
pub fn is_completion_response(msg: &Value) -> bool {
45
msg.get("result").is_some_and(|result| {
56
result.get("items").is_some_and(|v| v.is_array()) || result.is_array()
67
})
78
}
89

9-
pub fn sort_completions_by_param_count(msg: &mut Value) {
10-
let items = if let Some(result) = msg.get_mut("result") {
11-
if result.is_array() {
12-
result.as_array_mut()
13-
} else {
14-
result.get_mut("items").and_then(|v| v.as_array_mut())
15-
}
16-
} else {
17-
None
10+
/// Single-pass processing of completion items:
11+
/// - Sorts methods/functions by parameter count (prepends count to sortText)
12+
/// - Strips unsupported VS Code snippet variables ($TM_SELECTED_TEXT) from snippets
13+
pub fn process_completions(msg: &mut Value) {
14+
let items = match msg.get_mut("result") {
15+
Some(result) if result.is_array() => result.as_array_mut(),
16+
Some(result) => result.get_mut("items").and_then(|v| v.as_array_mut()),
17+
None => None,
1818
};
1919

20-
if let Some(items) = items {
21-
for item in items.iter_mut() {
22-
let kind = item.get("kind").and_then(|v| v.as_u64()).unwrap_or(0);
23-
if kind == 2 || kind == 3 {
20+
let Some(items) = items else { return };
21+
22+
for item in items.iter_mut() {
23+
let kind = item.get("kind").and_then(|v| v.as_u64()).unwrap_or(0);
24+
25+
match kind {
26+
// Method (2) or Function (3): prepend param count to sortText
27+
2 | 3 => {
2428
let detail = item
2529
.pointer("/labelDetails/detail")
2630
.and_then(|v| v.as_str())
@@ -29,29 +33,55 @@ pub fn sort_completions_by_param_count(msg: &mut Value) {
2933
let existing = item.get("sortText").and_then(|v| v.as_str()).unwrap_or("");
3034
item["sortText"] = Value::String(format!("{count:02}{existing}"));
3135
}
36+
// Snippet (15): strip $TM_SELECTED_TEXT
37+
15 => {
38+
strip_tm_selected_text(item, "textEditText");
39+
strip_tm_selected_text(item, "insertText");
40+
}
41+
_ => {}
3242
}
3343
}
3444
}
3545

36-
fn count_params(detail: &str) -> usize {
37-
if detail.is_empty() || detail == "()" {
38-
return 0;
46+
fn strip_tm_selected_text(item: &mut Value, key: &str) {
47+
if let Some(text) = item.get(key).and_then(|v| v.as_str()) {
48+
if text.contains("$TM_SELECTED_TEXT") {
49+
item[key] = Value::String(text.replace("$TM_SELECTED_TEXT", ""));
50+
}
3951
}
40-
let inner = detail
41-
.strip_prefix('(')
42-
.and_then(|s| s.strip_suffix(')'))
43-
.unwrap_or(detail)
44-
.trim();
52+
}
53+
54+
/// Sanitize a single resolved completion item (completionItem/resolve response).
55+
pub fn sanitize_resolved_completion(msg: &mut Value) {
56+
let Some(result) = msg.get_mut("result") else {
57+
return;
58+
};
59+
strip_tm_selected_text(result, "textEditText");
60+
strip_tm_selected_text(result, "insertText");
61+
// Also check inside textEdit.newText
62+
if let Some(new_text) = result.pointer("/textEdit/newText").and_then(|v| v.as_str()) {
63+
if new_text.contains("$TM_SELECTED_TEXT") {
64+
result["textEdit"]["newText"] =
65+
Value::String(new_text.replace("$TM_SELECTED_TEXT", ""));
66+
}
67+
}
68+
}
69+
70+
fn count_params(detail: &str) -> usize {
71+
let inner = match detail.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
72+
Some(s) => s.trim(),
73+
None => return 0,
74+
};
4575
if inner.is_empty() {
4676
return 0;
4777
}
4878
let mut count = 1usize;
4979
let mut depth = 0i32;
50-
for ch in inner.chars() {
80+
for ch in inner.bytes() {
5181
match ch {
52-
'<' => depth += 1,
53-
'>' => depth -= 1,
54-
',' if depth == 0 => count += 1,
82+
b'<' => depth += 1,
83+
b'>' => depth -= 1,
84+
b',' if depth == 0 => count += 1,
5585
_ => {}
5686
}
5787
}

proxy/src/main.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ mod log;
55
mod lsp;
66
mod platform;
77

8-
use completions::{should_sort_completions, sort_completions_by_param_count};
8+
use completions::{is_completion_response, process_completions, sanitize_resolved_completion};
99
use decompile::{rewrite_jdt_in_strings, rewrite_jdt_locations};
1010
use http::handle_http;
1111
use lsp::{parse_lsp_content, raw_has_id, write_raw, write_to_stdout, LspReader};
@@ -209,6 +209,7 @@ fn main() {
209209
&pending,
210210
&mut next_id,
211211
);
212+
sanitize_resolved_completion(&mut msg);
212213
}
213214
}
214215
write_to_stdout(&msg);
@@ -217,9 +218,9 @@ fn main() {
217218
}
218219
}
219220

220-
// Sort completion responses by param count
221-
if should_sort_completions(&msg) {
222-
sort_completions_by_param_count(&mut msg);
221+
// Process completion responses (sort + sanitize) in a single pass
222+
if is_completion_response(&msg) {
223+
process_completions(&mut msg);
223224
write_to_stdout(&msg);
224225
continue;
225226
}

0 commit comments

Comments
 (0)