1use std::future::Future;
17use std::pin::Pin;
18use std::sync::Arc;
19
20use lattice_completion::{
21 AsyncCompletionSource, CandidateSink, CompletionSourceContribution, CompletionSourceKind,
22 InsertContextSnapshot, SourceId,
23};
24use lattice_config::OptionOverrideSet;
25use lattice_mode::{
26 CapabilitySet, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind, ModeRegistry,
27};
28use lattice_protocol::CancellationToken;
29
30use crate::supervisor::LspSupervisorHandle;
31
32pub const LSP_COMPLETION_KIND_ID: u32 = 1;
37
38#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
50pub struct LspCompletionMeta {
51 pub label: String,
52 pub insert_text: String,
53 pub filter_text: Option<String>,
54 pub sort_text: Option<String>,
55 pub detail: Option<String>,
56 pub documentation: Option<String>,
57 pub kind: Option<lsp_types::CompletionItemKind>,
58 pub deprecated: bool,
59 pub preselect: bool,
60 pub commit_characters: Vec<char>,
61 pub additional_text_edits: Vec<lsp_types::TextEdit>,
62 pub command: Option<lsp_types::Command>,
63 pub insert_text_format: lsp_types::InsertTextFormat,
64 pub replace_range: Option<lsp_types::Range>,
68 pub server_id: String,
73 pub original_item: lsp_types::CompletionItem,
78 pub resolved: bool,
82}
83
84pub fn encode_meta(meta: &LspCompletionMeta) -> Vec<u8> {
91 serde_json::to_vec(meta).expect("LspCompletionMeta must serialise (all fields serde-derived)")
92}
93
94pub fn decode_meta(payload: &[u8]) -> Option<LspCompletionMeta> {
101 serde_json::from_slice(payload).ok()
102}
103
104#[derive(Debug, Clone)]
108pub struct LspCompletionSource {
109 pub lsp: LspSupervisorHandle,
110}
111
112impl AsyncCompletionSource for LspCompletionSource {
113 fn produce_async(
114 &self,
115 ctx: InsertContextSnapshot,
116 sink: Arc<dyn CandidateSink>,
117 token: CancellationToken,
118 ) -> Pin<Box<dyn Future<Output = ()> + Send>> {
119 let lsp = self.lsp.clone();
120 Box::pin(async move {
121 const MAX_LSP_ITEMS: usize = 500;
130 use std::str::FromStr;
131 let Some(uri_string) = ctx.uri.as_deref() else {
132 return;
133 };
134 let Ok(uri) = lsp_types::Uri::from_str(uri_string) else {
135 return;
136 };
137 let Some((line, character)) = ctx.lsp_position else {
138 return;
139 };
140 let lsp_position = lsp_types::Position { line, character };
141 let (lsp_trigger_kind, lsp_trigger_char) = match ctx.trigger {
142 lattice_completion::CompletionTrigger::TriggerChar(c) => (
143 lsp_types::CompletionTriggerKind::TRIGGER_CHARACTER,
144 Some(c.to_string()),
145 ),
146 lattice_completion::CompletionTrigger::IncompleteRefresh => (
147 lsp_types::CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS,
148 None,
149 ),
150 _ => (lsp_types::CompletionTriggerKind::INVOKED, None),
151 };
152 let handles: Vec<crate::ServerHandle> = lsp.servers_for(&uri);
153 if handles.is_empty() {
154 return;
155 }
156 let mut emitted = 0usize;
157 let mut any_incomplete = false;
158 let mut seen_keys: std::collections::HashSet<(String, String)> =
159 std::collections::HashSet::new();
160 let lsp_source_id =
161 lattice_completion::SourceId::new(lattice_completion::LSP_COMPLETION_SOURCE_ID);
162 for handle in handles {
163 if token.is_cancelled() {
164 return;
165 }
166 if !handle.capabilities().supports_completion() {
167 continue;
168 }
169 let params = lsp_types::CompletionParams {
170 text_document_position: lsp_types::TextDocumentPositionParams {
171 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
172 position: lsp_position,
173 },
174 work_done_progress_params: Default::default(),
175 partial_result_params: Default::default(),
176 context: Some(lsp_types::CompletionContext {
177 trigger_kind: lsp_trigger_kind,
178 trigger_character: lsp_trigger_char.clone(),
179 }),
180 };
181 let Ok(Some(resp)) = handle.completion(params, token.clone()).await else {
182 continue;
183 };
184 let (items, is_incomplete) = match resp {
185 lsp_types::CompletionResponse::Array(items) => (items, false),
186 lsp_types::CompletionResponse::List(list) => (list.items, list.is_incomplete),
187 };
188 if is_incomplete {
189 any_incomplete = true;
190 }
191 for ci in items {
192 let kind = ci.kind;
193 let label = ci.label.clone();
194 let kind_tag = kind
195 .map(|k| format!("{k:?}"))
196 .unwrap_or_else(|| "none".to_string());
197 let key = (label.clone(), kind_tag);
198 if !seen_keys.insert(key) {
199 continue;
200 }
201 let deprecated = ci
202 .tags
203 .as_ref()
204 .map(|t| t.contains(&lsp_types::CompletionItemTag::DEPRECATED))
205 .unwrap_or(false)
206 || ci.deprecated.unwrap_or(false);
207 let (insert_text, replace_range) = match ci.text_edit.as_ref() {
208 Some(lsp_types::CompletionTextEdit::Edit(te)) => {
209 (te.new_text.clone(), Some(te.range))
210 }
211 Some(lsp_types::CompletionTextEdit::InsertAndReplace(ir)) => {
212 (ir.new_text.clone(), Some(ir.replace))
213 }
214 None => (
215 ci.insert_text.clone().unwrap_or_else(|| label.clone()),
216 None,
217 ),
218 };
219 let documentation = ci.documentation.as_ref().map(|d| match d {
220 lsp_types::Documentation::String(s) => s.clone(),
221 lsp_types::Documentation::MarkupContent(mc) => mc.value.clone(),
222 });
223 let commit_characters = ci
224 .commit_characters
225 .as_ref()
226 .map(|chars| chars.iter().filter_map(|s| s.chars().next()).collect())
227 .unwrap_or_default();
228 let meta = LspCompletionMeta {
229 label: label.clone(),
230 insert_text,
231 filter_text: ci.filter_text.clone(),
232 sort_text: ci.sort_text.clone(),
233 detail: ci.detail.clone(),
234 documentation,
235 kind,
236 deprecated,
237 preselect: ci.preselect.unwrap_or(false),
238 commit_characters,
239 additional_text_edits: ci.additional_text_edits.clone().unwrap_or_default(),
240 command: ci.command.clone(),
241 insert_text_format: ci
242 .insert_text_format
243 .unwrap_or(lsp_types::InsertTextFormat::PLAIN_TEXT),
244 replace_range,
245 server_id: handle.server_id().to_string(),
246 original_item: ci,
247 resolved: false,
248 };
249 let display = match meta.detail.as_ref() {
250 Some(d) => format!("{} {}", meta.label, d),
251 None => meta.label.clone(),
252 };
253 let match_text = meta
254 .filter_text
255 .clone()
256 .unwrap_or_else(|| meta.label.clone());
257 let payload = encode_meta(&meta);
258 let mut raw = lattice_completion::RawCandidate::plain(
259 match_text,
260 lattice_completion::CandidateKind::Plain,
261 )
262 .with_source(lsp_source_id.clone());
263 raw.display = display;
264 raw.data = lattice_completion::CandidateData::Extension {
265 kind_id: LSP_COMPLETION_KIND_ID,
266 payload,
267 };
268 sink.push(raw);
269 emitted += 1;
270 if emitted >= MAX_LSP_ITEMS {
271 break;
272 }
273 }
274 if emitted >= MAX_LSP_ITEMS {
275 break;
276 }
277 }
278 if any_incomplete {
279 sink.mark_incomplete();
280 }
281 })
282 }
283}
284
285#[derive(Debug, Clone)]
293pub struct LspCompletionMode {
294 pub lsp: LspSupervisorHandle,
295}
296
297impl LspCompletionMode {
298 pub fn mode_id() -> ModeId {
299 ModeId::new("lsp-completion-mode")
300 }
301}
302
303impl Mode for LspCompletionMode {
304 type Guard = ();
305 fn id(&self) -> ModeId {
306 Self::mode_id()
307 }
308 fn kind(&self) -> ModeKind {
309 ModeKind::Minor
310 }
311 fn options(&self) -> OptionOverrideSet {
312 OptionOverrideSet::default()
313 }
314 fn required_capabilities(&self) -> CapabilitySet {
315 CapabilitySet::empty()
316 }
317 fn completion_sources(&self) -> Vec<CompletionSourceContribution> {
318 vec![CompletionSourceContribution {
319 accepts_non_word_query: false,
320 id: SourceId::new(lattice_completion::LSP_COMPLETION_SOURCE_ID),
321 default_priority: 200,
324 auto_trigger: true,
325 trigger_chars: Vec::new(),
329 popup_filter_chord: Some('o'),
330 kind: CompletionSourceKind::Async(Arc::new(LspCompletionSource {
331 lsp: self.lsp.clone(),
332 })),
333 }]
334 }
335 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, ()> {
336 Box::pin(async { Ok(()) })
337 }
338}
339
340pub fn register_lsp_completion_mode(registry: &mut ModeRegistry, lsp: LspSupervisorHandle) {
346 registry
347 .register(LspCompletionMode { lsp })
348 .expect("lsp-completion-mode must register without conflict");
349}
350
351#[cfg(test)]
352mod tests {
353 #![allow(clippy::unwrap_used, clippy::panic)]
354 use super::*;
355
356 fn sample_meta() -> LspCompletionMeta {
357 LspCompletionMeta {
358 label: "println!".into(),
359 insert_text: "println!(\"$1\")$0".into(),
360 filter_text: Some("println".into()),
361 sort_text: Some("00println".into()),
362 detail: Some("macro_rules! println".into()),
363 documentation: Some("Prints to the standard output.".into()),
364 kind: Some(lsp_types::CompletionItemKind::SNIPPET),
365 deprecated: false,
366 preselect: true,
367 commit_characters: vec!['(', '!'],
368 additional_text_edits: vec![lsp_types::TextEdit {
369 range: lsp_types::Range {
370 start: lsp_types::Position {
371 line: 0,
372 character: 0,
373 },
374 end: lsp_types::Position {
375 line: 0,
376 character: 0,
377 },
378 },
379 new_text: "use std::println;\n".into(),
380 }],
381 command: None,
382 insert_text_format: lsp_types::InsertTextFormat::SNIPPET,
383 replace_range: Some(lsp_types::Range {
384 start: lsp_types::Position {
385 line: 1,
386 character: 4,
387 },
388 end: lsp_types::Position {
389 line: 1,
390 character: 11,
391 },
392 }),
393 server_id: "rust-analyzer".to_string(),
394 original_item: lsp_types::CompletionItem::default(),
395 resolved: false,
396 }
397 }
398
399 #[test]
404 fn encode_decode_round_trip_preserves_all_fields() {
405 let meta = sample_meta();
406 let bytes = encode_meta(&meta);
407 let decoded = decode_meta(&bytes).expect("decode");
408 assert_eq!(decoded.label, meta.label);
409 assert_eq!(decoded.insert_text, meta.insert_text);
410 assert_eq!(decoded.filter_text, meta.filter_text);
411 assert_eq!(decoded.sort_text, meta.sort_text);
412 assert_eq!(decoded.detail, meta.detail);
413 assert_eq!(decoded.documentation, meta.documentation);
414 assert_eq!(decoded.kind, meta.kind);
415 assert_eq!(decoded.deprecated, meta.deprecated);
416 assert_eq!(decoded.preselect, meta.preselect);
417 assert_eq!(decoded.commit_characters, meta.commit_characters);
418 assert_eq!(decoded.additional_text_edits, meta.additional_text_edits);
419 assert_eq!(decoded.insert_text_format, meta.insert_text_format);
420 assert_eq!(decoded.replace_range, meta.replace_range);
421 assert_eq!(decoded.server_id, meta.server_id);
422 assert_eq!(decoded.resolved, meta.resolved);
423 }
424
425 #[test]
430 fn decode_meta_returns_none_for_garbage() {
431 assert!(decode_meta(b"not json").is_none());
432 assert!(decode_meta(b"").is_none());
433 }
434}