ts_bridge/protocol/text_document/
completion_resolve.rs1use anyhow::{Context, Result};
11use lsp_types::{
12 CompletionItem, CompletionItemKind, CompletionTextEdit, Documentation, InsertTextFormat,
13 MarkupContent, MarkupKind, Position, TextEdit,
14};
15use serde::Deserialize;
16use serde_json::{Value, json};
17
18use crate::protocol::{AdapterResult, RequestSpec};
19use crate::rpc::{Priority, Route};
20use crate::utils::tsserver_range_from_value_lsp;
21
22#[derive(Debug, Deserialize)]
23#[serde(rename_all = "camelCase")]
24struct CompletionResolveData {
25 file: String,
26 position: Position,
27 #[serde(default)]
28 entry_names: Vec<Value>,
29}
30
31pub fn handle(mut item: CompletionItem) -> Option<RequestSpec> {
32 let data = item.data.take()?;
33 let data: CompletionResolveData = serde_json::from_value(data).ok()?;
34 let request = json!({
35 "command": "completionEntryDetails",
36 "arguments": {
37 "file": data.file,
38 "line": data.position.line + 1,
39 "offset": data.position.character + 1,
40 "entryNames": data.entry_names,
41 }
42 });
43
44 let context = serde_json::to_value(item).ok()?;
45
46 Some(RequestSpec {
47 route: Route::Syntax,
48 payload: request,
49 priority: Priority::Normal,
50 on_response: Some(adapt_completion_resolve),
51 response_context: Some(context),
52 })
53}
54
55fn adapt_completion_resolve(payload: &Value, context: Option<&Value>) -> Result<AdapterResult> {
56 let mut item: CompletionItem =
57 serde_json::from_value(context.cloned().context("missing completion item")?)?;
58 let details = payload
59 .get("body")
60 .and_then(|value| value.as_array())
61 .and_then(|array| array.first())
62 .context("tsserver completion details missing body")?;
63
64 if let Some(display) = render_display_parts(details.get("displayParts")) {
65 item.detail = Some(display);
66 }
67
68 if let Some(documentation) = render_documentation(details) {
69 item.documentation = Some(Documentation::MarkupContent(MarkupContent {
70 kind: MarkupKind::Markdown,
71 value: documentation,
72 }));
73 }
74
75 if let Some(edits) = build_additional_text_edits(details.get("codeActions")) {
76 item.additional_text_edits = Some(edits);
77 }
78
79 if should_create_function_snippet(&item, details) {
80 inject_snippet(&mut item, details);
81 }
82
83 Ok(AdapterResult::ready(serde_json::to_value(item)?))
84}
85
86fn render_display_parts(parts: Option<&Value>) -> Option<String> {
87 let parts = parts?.as_array()?;
88 let mut buffer = String::new();
89 for part in parts {
90 if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
91 buffer.push_str(text);
92 }
93 }
94 if buffer.is_empty() {
95 None
96 } else {
97 Some(buffer)
98 }
99}
100
101fn render_documentation(details: &Value) -> Option<String> {
102 let mut sections = Vec::new();
103 if let Some(docs) = details
104 .get("documentation")
105 .and_then(|value| value.as_array())
106 {
107 let mut buffer = String::new();
108 for entry in docs {
109 if let Some(text) = entry.get("text").and_then(|v| v.as_str()) {
110 if !buffer.is_empty() {
111 buffer.push('\n');
112 }
113 buffer.push_str(text);
114 }
115 }
116 if !buffer.is_empty() {
117 sections.push(buffer);
118 }
119 }
120 if let Some(tags) = details.get("tags").and_then(|value| value.as_array()) {
121 for tag in tags {
122 if let Some(name) = tag.get("name").and_then(|v| v.as_str()) {
123 let text = tag
124 .get("text")
125 .and_then(|t| render_display_parts(Some(t)))
126 .unwrap_or_default();
127 if text.is_empty() {
128 sections.push(format!("_@{}_", name));
129 } else {
130 sections.push(format!("_@{}_ — {}", name, text));
131 }
132 }
133 }
134 }
135 if sections.is_empty() {
136 None
137 } else {
138 Some(sections.join("\n\n"))
139 }
140}
141
142fn build_additional_text_edits(actions_value: Option<&Value>) -> Option<Vec<TextEdit>> {
143 let actions = actions_value?.as_array()?;
144 let mut edits = Vec::new();
145 for action in actions {
146 let changes = action.get("changes").and_then(|value| value.as_array());
147 if let Some(changes) = changes {
148 for change in changes {
149 if let Some(text_changes) = change.get("textChanges").and_then(|v| v.as_array()) {
150 for text_change in text_changes {
151 if let Some(range) = tsserver_range_from_value_lsp(text_change) {
152 if let Some(new_text) =
153 text_change.get("newText").and_then(|v| v.as_str())
154 {
155 edits.push(TextEdit {
156 range,
157 new_text: new_text.to_string(),
158 });
159 }
160 }
161 }
162 }
163 }
164 }
165 }
166 if edits.is_empty() { None } else { Some(edits) }
167}
168
169fn should_create_function_snippet(item: &CompletionItem, details: &Value) -> bool {
170 matches!(
171 item.kind.unwrap_or(CompletionItemKind::TEXT),
172 CompletionItemKind::FUNCTION | CompletionItemKind::METHOD | CompletionItemKind::CONSTRUCTOR
173 ) && details.get("displayParts").is_some()
174}
175
176fn inject_snippet(item: &mut CompletionItem, details: &Value) {
177 if let Some(parts) = details
178 .get("displayParts")
179 .and_then(|value| value.as_array())
180 {
181 let mut snippet = String::new();
182 snippet.push_str(
183 item.insert_text
184 .as_deref()
185 .or_else(|| {
186 item.text_edit.as_ref().map(|edit| match edit {
187 CompletionTextEdit::Edit(edit) => edit.new_text.as_str(),
188 CompletionTextEdit::InsertAndReplace(edit) => edit.new_text.as_str(),
189 })
190 })
191 .unwrap_or(item.label.as_str()),
192 );
193 snippet.push('(');
194
195 let mut param_index = 1;
196 let mut first = true;
197 for part in parts {
198 let kind = part.get("kind").and_then(|v| v.as_str());
199 if kind == Some("parameterName") {
200 if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
201 if !first {
202 snippet.push_str(", ");
203 }
204 snippet.push_str(&format!("${{{}:{}}}", param_index, escape_snippet(text)));
205 param_index += 1;
206 first = false;
207 }
208 } else if kind == Some("punctuation") {
209 if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
210 if text == ")" {
211 break;
212 }
213 }
214 }
215 }
216
217 if first {
218 snippet.push_str("$0");
219 }
220 snippet.push(')');
221
222 item.insert_text = Some(snippet.clone());
223 item.insert_text_format = Some(InsertTextFormat::SNIPPET);
224 if let Some(CompletionTextEdit::Edit(edit)) = item.text_edit.as_mut() {
225 edit.new_text = snippet;
226 }
227 }
228}
229
230fn escape_snippet(value: &str) -> String {
231 value
232 .replace('\\', "\\\\")
233 .replace('$', "\\$")
234 .replace('}', "\\}")
235}