From 196a68e4c8f03142573dceeeba2c8485f59e132a Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 28 Oct 2025 11:23:15 -0400 Subject: [PATCH] [ty] Render `import <...>` in completions when "label details" isn't supported This fixes a bug where the `import module` part of a completion for unimported candidates would be missing. This makes it especially confusing because the user can't tell where the symbol is coming from, and there is no hint that an `import` statement will be inserted. Previously, we were using [`CompletionItemLabelDetails`] to render the `import module` part of the suggestion. But this is only supported in clients that support version 3.17 (or newer) of the LSP specification. It turns out that this support isn't widespread yet. In particular, Heliex doesn't seem to support "label details." To fix this, we take a [cue from rust-analyzer][rust-analyzer-details]. We detect if the client supports "label details," and if so, use it. Otherwise, we push the `import module` text into the completion label itself. Fixes https://github.com/astral-sh/ruff/pull/20439#issuecomment-3313689568 [`CompletionItemLabelDetails`]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails [rust-analyzer-details]: https://github.com/rust-lang/rust-analyzer/blob/5d905576d49233ed843bb40df4ed57e5534558f5/crates/rust-analyzer/src/lsp/to_proto.rs#L391-L404 --- crates/ty_server/src/capabilities.rs | 15 +++++++++++ .../src/server/api/requests/completion.rs | 27 ++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index fed7ac811e..bda8546b3d 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -36,6 +36,7 @@ bitflags::bitflags! { const DIAGNOSTIC_DYNAMIC_REGISTRATION = 1 << 14; const WORKSPACE_CONFIGURATION = 1 << 15; const RENAME_DYNAMIC_REGISTRATION = 1 << 16; + const COMPLETION_ITEM_LABEL_DETAILS_SUPPORT = 1 << 17; } } @@ -158,6 +159,11 @@ impl ResolvedClientCapabilities { self.contains(Self::RENAME_DYNAMIC_REGISTRATION) } + /// Returns `true` if the client supports "label details" in completion items. + pub(crate) const fn supports_completion_item_label_details(self) -> bool { + self.contains(Self::COMPLETION_ITEM_LABEL_DETAILS_SUPPORT) + } + pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self { let mut flags = Self::empty(); @@ -314,6 +320,15 @@ impl ResolvedClientCapabilities { flags |= Self::WORK_DONE_PROGRESS; } + if text_document + .and_then(|text_document| text_document.completion.as_ref()) + .and_then(|completion| completion.completion_item.as_ref()) + .and_then(|completion_item| completion_item.label_details_support) + .unwrap_or_default() + { + flags |= Self::COMPLETION_ITEM_LABEL_DETAILS_SUPPORT; + } + flags } } diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index 35e1c3fd25..a3e7d91f94 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -81,15 +81,30 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { new_text: edit.content().map(ToString::to_string).unwrap_or_default(), } }); + + let name = comp.name.to_string(); + let import_suffix = comp.module_name.map(|name| format!(" (import {name})")); + let (label, label_details) = if snapshot + .resolved_client_capabilities() + .supports_completion_item_label_details() + { + let label_details = CompletionItemLabelDetails { + detail: import_suffix, + description: type_display.clone(), + }; + (name, Some(label_details)) + } else { + let label = import_suffix + .map(|suffix| format!("{name}{suffix}")) + .unwrap_or_else(|| name); + (label, None) + }; CompletionItem { - label: comp.name.into(), + label, kind, sort_text: Some(format!("{i:-max_index_len$}")), - detail: type_display.clone(), - label_details: Some(CompletionItemLabelDetails { - detail: comp.module_name.map(|name| format!(" (import {name})")), - description: type_display, - }), + detail: type_display, + label_details, insert_text: comp.insert.map(String::from), additional_text_edits: import_edit.map(|edit| vec![edit]), documentation: comp