Files
ruff/crates/ty_server/src/server/api/requests/completion.rs
Andrew Gallant 196a68e4c8 [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]: 5d905576d4/crates/rust-analyzer/src/lsp/to_proto.rs (L391-L404)
2025-10-29 08:50:41 -04:00

168 lines
6.2 KiB
Rust

use std::borrow::Cow;
use std::time::Instant;
use lsp_types::request::Completion;
use lsp_types::{
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList,
CompletionParams, CompletionResponse, Documentation, TextEdit, Url,
};
use ruff_db::source::{line_index, source_text};
use ruff_source_file::OneIndexed;
use ruff_text_size::Ranged;
use ty_ide::{CompletionKind, CompletionSettings, completion};
use ty_project::ProjectDatabase;
use crate::document::{PositionExt, ToRangeExt};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::DocumentSnapshot;
use crate::session::client::Client;
pub(crate) struct CompletionRequestHandler;
impl RequestHandler for CompletionRequestHandler {
type RequestType = Completion;
}
impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
fn document_url(params: &CompletionParams) -> Cow<'_, Url> {
Cow::Borrowed(&params.text_document_position.text_document.uri)
}
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: &DocumentSnapshot,
_client: &Client,
params: CompletionParams,
) -> crate::server::Result<Option<CompletionResponse>> {
let start = Instant::now();
if snapshot
.workspace_settings()
.is_language_services_disabled()
{
return Ok(None);
}
let Some(file) = snapshot.file(db) else {
return Ok(None);
};
let source = source_text(db, file);
let line_index = line_index(db, file);
let offset = params.text_document_position.position.to_text_size(
&source,
&line_index,
snapshot.encoding(),
);
let settings = CompletionSettings {
auto_import: snapshot.global_settings().is_auto_import_enabled(),
};
let completions = completion(db, &settings, file, offset);
if completions.is_empty() {
return Ok(None);
}
// Safety: we just checked that completions is not empty.
let max_index_len = OneIndexed::new(completions.len()).unwrap().digits().get();
let items: Vec<CompletionItem> = completions
.into_iter()
.enumerate()
.map(|(i, comp)| {
let kind = comp.kind(db).map(ty_kind_to_lsp_kind);
let type_display = comp.ty.map(|ty| ty.display(db).to_string());
let import_edit = comp.import.as_ref().map(|edit| {
let range =
edit.range()
.to_lsp_range(&source, &line_index, snapshot.encoding());
TextEdit {
range,
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,
kind,
sort_text: Some(format!("{i:-max_index_len$}")),
detail: type_display,
label_details,
insert_text: comp.insert.map(String::from),
additional_text_edits: import_edit.map(|edit| vec![edit]),
documentation: comp
.documentation
.map(|docstring| Documentation::String(docstring.render_plaintext())),
..Default::default()
}
})
.collect();
let len = items.len();
let response = CompletionResponse::List(CompletionList {
is_incomplete: true,
items,
});
tracing::debug!(
"Completions request returned {len} suggestions in {elapsed:?}",
elapsed = Instant::now().duration_since(start)
);
Ok(Some(response))
}
}
impl RetriableRequestHandler for CompletionRequestHandler {
const RETRY_ON_CANCELLATION: bool = true;
}
fn ty_kind_to_lsp_kind(kind: CompletionKind) -> CompletionItemKind {
// Gimme my dang globs in tight scopes!
#[allow(clippy::enum_glob_use)]
use self::CompletionKind::*;
// ref https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind
match kind {
Text => CompletionItemKind::TEXT,
Method => CompletionItemKind::METHOD,
Function => CompletionItemKind::FUNCTION,
Constructor => CompletionItemKind::CONSTRUCTOR,
Field => CompletionItemKind::FIELD,
Variable => CompletionItemKind::VARIABLE,
Class => CompletionItemKind::CLASS,
Interface => CompletionItemKind::INTERFACE,
Module => CompletionItemKind::MODULE,
Property => CompletionItemKind::PROPERTY,
Unit => CompletionItemKind::UNIT,
Value => CompletionItemKind::VALUE,
Enum => CompletionItemKind::ENUM,
Keyword => CompletionItemKind::KEYWORD,
Snippet => CompletionItemKind::SNIPPET,
Color => CompletionItemKind::COLOR,
File => CompletionItemKind::FILE,
Reference => CompletionItemKind::REFERENCE,
Folder => CompletionItemKind::FOLDER,
EnumMember => CompletionItemKind::ENUM_MEMBER,
Constant => CompletionItemKind::CONSTANT,
Struct => CompletionItemKind::STRUCT,
Event => CompletionItemKind::EVENT,
Operator => CompletionItemKind::OPERATOR,
TypeParameter => CompletionItemKind::TYPE_PARAMETER,
}
}