1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 15:24:46 -05:00

fix(lsp): enable auto imports (#15145)

Fixes: #15111
This commit is contained in:
Kitson Kelly 2022-07-12 09:35:18 +10:00 committed by GitHub
parent 82431062fa
commit 5db16d1229
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 506 additions and 56 deletions

View file

@ -31,6 +31,7 @@ static FILE_PROTO_RE: Lazy<Regex> =
const CURRENT_PATH: &str = "."; const CURRENT_PATH: &str = ".";
const PARENT_PATH: &str = ".."; const PARENT_PATH: &str = "..";
const LOCAL_PATHS: &[&str] = &[CURRENT_PATH, PARENT_PATH]; const LOCAL_PATHS: &[&str] = &[CURRENT_PATH, PARENT_PATH];
const IMPORT_COMMIT_CHARS: &[&str] = &["\"", "'", "/"];
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -182,6 +183,9 @@ pub async fn get_import_completions(
detail: Some("(local)".to_string()), detail: Some("(local)".to_string()),
sort_text: Some("1".to_string()), sort_text: Some("1".to_string()),
insert_text: Some(s.to_string()), insert_text: Some(s.to_string()),
commit_characters: Some(
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
),
..Default::default() ..Default::default()
}) })
.collect(); .collect();
@ -231,6 +235,9 @@ fn get_base_import_map_completions(
detail: Some("(import map)".to_string()), detail: Some("(import map)".to_string()),
sort_text: Some(label.clone()), sort_text: Some(label.clone()),
insert_text: Some(label), insert_text: Some(label),
commit_characters: Some(
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
),
..Default::default() ..Default::default()
} }
}) })
@ -284,6 +291,9 @@ fn get_import_map_completions(
sort_text: Some("1".to_string()), sort_text: Some("1".to_string()),
filter_text: Some(new_text), filter_text: Some(new_text),
text_edit, text_edit,
commit_characters: Some(
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
),
..Default::default() ..Default::default()
}) })
} else { } else {
@ -311,6 +321,9 @@ fn get_import_map_completions(
detail: Some("(import map)".to_string()), detail: Some("(import map)".to_string()),
sort_text: Some("1".to_string()), sort_text: Some("1".to_string()),
text_edit, text_edit,
commit_characters: Some(
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
),
..Default::default() ..Default::default()
}); });
} }
@ -382,6 +395,9 @@ fn get_local_completions(
filter_text, filter_text,
sort_text: Some("1".to_string()), sort_text: Some("1".to_string()),
text_edit, text_edit,
commit_characters: Some(
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
),
..Default::default() ..Default::default()
}), }),
Ok(file_type) if file_type.is_file() => { Ok(file_type) if file_type.is_file() => {
@ -393,6 +409,9 @@ fn get_local_completions(
filter_text, filter_text,
sort_text: Some("1".to_string()), sort_text: Some("1".to_string()),
text_edit, text_edit,
commit_characters: Some(
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
),
..Default::default() ..Default::default()
}) })
} else { } else {
@ -463,6 +482,9 @@ fn get_workspace_completions(
detail, detail,
sort_text: Some("1".to_string()), sort_text: Some("1".to_string()),
text_edit, text_edit,
commit_characters: Some(
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
),
..Default::default() ..Default::default()
}) })
} else { } else {
@ -484,7 +506,7 @@ fn get_workspace_completions(
/// assert_eq!(relative_specifier(&specifier, &base), "../b.ts"); /// assert_eq!(relative_specifier(&specifier, &base), "../b.ts");
/// ``` /// ```
/// ///
fn relative_specifier( pub fn relative_specifier(
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
base: &ModuleSpecifier, base: &ModuleSpecifier,
) -> String { ) -> String {
@ -812,6 +834,9 @@ mod tests {
}, },
new_text: "https://deno.land/x/a/b/c.ts".to_string(), new_text: "https://deno.land/x/a/b/c.ts".to_string(),
})), })),
commit_characters: Some(
IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect()
),
..Default::default() ..Default::default()
}] }]
); );

View file

@ -1764,10 +1764,14 @@ impl Inner {
Some(response) Some(response)
} else { } else {
let line_index = asset_or_doc.line_index(); let line_index = asset_or_doc.line_index();
let trigger_character = if let Some(context) = &params.context { let (trigger_character, trigger_kind) =
context.trigger_character.clone() if let Some(context) = &params.context {
(
context.trigger_character.clone(),
Some(context.trigger_kind.into()),
)
} else { } else {
None (None, None)
}; };
let position = let position =
line_index.offset_tsc(params.text_document_position.position)?; line_index.offset_tsc(params.text_document_position.position)?;
@ -1776,14 +1780,30 @@ impl Inner {
position, position,
tsc::GetCompletionsAtPositionOptions { tsc::GetCompletionsAtPositionOptions {
user_preferences: tsc::UserPreferences { user_preferences: tsc::UserPreferences {
allow_text_changes_in_new_files: Some(specifier.scheme() == "file"),
include_automatic_optional_chain_completions: Some(true),
provide_refactor_not_applicable_reason: Some(true),
include_completions_with_insert_text: Some(true),
allow_incomplete_completions: Some(true), allow_incomplete_completions: Some(true),
allow_text_changes_in_new_files: Some(specifier.scheme() == "file"),
import_module_specifier_ending: Some(
tsc::ImportModuleSpecifierEnding::Index,
),
include_automatic_optional_chain_completions: Some(true),
include_completions_for_import_statements: Some(
self.config.get_workspace_settings().suggest.auto_imports,
),
include_completions_for_module_exports: Some(true),
include_completions_with_object_literal_method_snippets: Some(true),
include_completions_with_class_member_snippets: Some(true),
include_completions_with_insert_text: Some(true),
include_completions_with_snippet_text: Some(true),
jsx_attribute_completion_style: Some(
tsc::JsxAttributeCompletionStyle::Auto,
),
provide_prefix_and_suffix_text_for_rename: Some(true),
provide_refactor_not_applicable_reason: Some(true),
use_label_details_in_completion_entries: Some(true),
..Default::default() ..Default::default()
}, },
trigger_character, trigger_character,
trigger_kind,
}, },
)); ));
let snapshot = self.snapshot(); let snapshot = self.snapshot();
@ -1822,7 +1842,8 @@ impl Inner {
"Could not decode data field of completion item.", "Could not decode data field of completion item.",
) )
})?; })?;
if let Some(data) = data.tsc { if let Some(data) = &data.tsc {
let specifier = data.specifier.clone();
let req = tsc::RequestMethod::GetCompletionDetails(data.into()); let req = tsc::RequestMethod::GetCompletionDetails(data.into());
let maybe_completion_info: Option<tsc::CompletionEntryDetails> = let maybe_completion_info: Option<tsc::CompletionEntryDetails> =
self.ts_server.request(self.snapshot(), req).await.map_err( self.ts_server.request(self.snapshot(), req).await.map_err(
@ -1832,7 +1853,15 @@ impl Inner {
}, },
)?; )?;
if let Some(completion_info) = maybe_completion_info { if let Some(completion_info) = maybe_completion_info {
completion_info.as_completion_item(&params, self) completion_info
.as_completion_item(&params, data, &specifier, self)
.map_err(|err| {
error!(
"Failed to serialize virtual_text_document response: {}",
err
);
LspError::internal_error()
})?
} else { } else {
error!( error!(
"Received an undefined response from tsc for completion details." "Received an undefined response from tsc for completion details."

View file

@ -1,6 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use super::code_lens; use super::code_lens;
use super::completions::relative_specifier;
use super::config; use super::config;
use super::documents::AssetOrDocument; use super::documents::AssetOrDocument;
use super::language_server; use super::language_server;
@ -511,7 +512,7 @@ pub enum ScriptElementKind {
Link, Link,
#[serde(rename = "link name")] #[serde(rename = "link name")]
LinkName, LinkName,
#[serde(rename = "link test")] #[serde(rename = "link text")]
LinkText, LinkText,
} }
@ -636,7 +637,7 @@ pub struct SymbolDisplayPart {
target: Option<DocumentSpan>, target: Option<DocumentSpan>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct JsDocTagInfo { pub struct JsDocTagInfo {
name: String, name: String,
@ -1285,7 +1286,14 @@ pub struct TextChange {
} }
impl TextChange { impl TextChange {
pub fn as_text_edit( pub fn as_text_edit(&self, line_index: Arc<LineIndex>) -> lsp::TextEdit {
lsp::TextEdit {
range: self.span.to_range(line_index),
new_text: self.new_text.clone(),
}
}
pub fn as_text_or_annotated_text_edit(
&self, &self,
line_index: Arc<LineIndex>, line_index: Arc<LineIndex>,
) -> lsp::OneOf<lsp::TextEdit, lsp::AnnotatedTextEdit> { ) -> lsp::OneOf<lsp::TextEdit, lsp::AnnotatedTextEdit> {
@ -1315,7 +1323,7 @@ impl FileTextChanges {
let edits = self let edits = self
.text_changes .text_changes
.iter() .iter()
.map(|tc| tc.as_text_edit(asset_or_doc.line_index())) .map(|tc| tc.as_text_or_annotated_text_edit(asset_or_doc.line_index()))
.collect(); .collect();
Ok(lsp::TextDocumentEdit { Ok(lsp::TextDocumentEdit {
text_document: lsp::OptionalVersionedTextDocumentIdentifier { text_document: lsp::OptionalVersionedTextDocumentIdentifier {
@ -1359,7 +1367,7 @@ impl FileTextChanges {
let edits = self let edits = self
.text_changes .text_changes
.iter() .iter()
.map(|tc| tc.as_text_edit(line_index.clone())) .map(|tc| tc.as_text_or_annotated_text_edit(line_index.clone()))
.collect(); .collect();
ops.push(lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { ops.push(lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit {
text_document: lsp::OptionalVersionedTextDocumentIdentifier { text_document: lsp::OptionalVersionedTextDocumentIdentifier {
@ -1579,13 +1587,13 @@ impl RefactorEditInfo {
} }
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CodeAction { pub struct CodeAction {
// description: String, description: String,
// changes: Vec<FileTextChanges>, changes: Vec<FileTextChanges>,
// #[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
// commands: Option<Vec<Value>>, commands: Option<Vec<Value>>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
@ -1816,25 +1824,97 @@ impl CallHierarchyOutgoingCall {
} }
} }
#[derive(Debug, Deserialize)] /// Used to convert completion code actions into a command and additional text
/// edits to pass in the completion item.
fn parse_code_actions(
maybe_code_actions: Option<&Vec<CodeAction>>,
data: &CompletionItemData,
specifier: &ModuleSpecifier,
language_server: &language_server::Inner,
) -> Result<(Option<lsp::Command>, Option<Vec<lsp::TextEdit>>), AnyError> {
if let Some(code_actions) = maybe_code_actions {
let mut additional_text_edits: Vec<lsp::TextEdit> = Vec::new();
let mut has_remaining_commands_or_edits = false;
for ts_action in code_actions {
if ts_action.commands.is_some() {
has_remaining_commands_or_edits = true;
}
let asset_or_doc =
language_server.get_asset_or_document(&data.specifier)?;
for change in &ts_action.changes {
let change_specifier = normalize_specifier(&change.file_name)?;
if data.specifier == change_specifier {
additional_text_edits.extend(change.text_changes.iter().map(|tc| {
update_import_statement(
tc.as_text_edit(asset_or_doc.line_index()),
data,
)
}));
} else {
has_remaining_commands_or_edits = true;
}
}
}
let mut command: Option<lsp::Command> = None;
if has_remaining_commands_or_edits {
let actions: Vec<Value> = code_actions
.iter()
.map(|ca| {
let changes: Vec<FileTextChanges> = ca
.changes
.clone()
.into_iter()
.filter(|ch| {
normalize_specifier(&ch.file_name).unwrap() == data.specifier
})
.collect();
json!({
"commands": ca.commands,
"description": ca.description,
"changes": changes,
})
})
.collect();
command = Some(lsp::Command {
title: "".to_string(),
command: "_typescript.applyCompletionCodeAction".to_string(),
arguments: Some(vec![json!(specifier.to_string()), json!(actions)]),
});
}
if additional_text_edits.is_empty() {
Ok((command, None))
} else {
Ok((command, Some(additional_text_edits)))
}
} else {
Ok((None, None))
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CompletionEntryDetails { pub struct CompletionEntryDetails {
display_parts: Vec<SymbolDisplayPart>, display_parts: Vec<SymbolDisplayPart>,
documentation: Option<Vec<SymbolDisplayPart>>, documentation: Option<Vec<SymbolDisplayPart>>,
tags: Option<Vec<JsDocTagInfo>>, tags: Option<Vec<JsDocTagInfo>>,
// name: String, name: String,
// kind: ScriptElementKind, kind: ScriptElementKind,
// kind_modifiers: String, kind_modifiers: String,
// code_actions: Option<Vec<CodeAction>>, code_actions: Option<Vec<CodeAction>>,
// source_display: Option<Vec<SymbolDisplayPart>>, source_display: Option<Vec<SymbolDisplayPart>>,
} }
impl CompletionEntryDetails { impl CompletionEntryDetails {
pub fn as_completion_item( pub fn as_completion_item(
&self, &self,
original_item: &lsp::CompletionItem, original_item: &lsp::CompletionItem,
data: &CompletionItemData,
specifier: &ModuleSpecifier,
language_server: &language_server::Inner, language_server: &language_server::Inner,
) -> lsp::CompletionItem { ) -> Result<lsp::CompletionItem, AnyError> {
let detail = if original_item.detail.is_some() { let detail = if original_item.detail.is_some() {
original_item.detail.clone() original_item.detail.clone()
} else if !self.display_parts.is_empty() { } else if !self.display_parts.is_empty() {
@ -1862,15 +1942,22 @@ impl CompletionEntryDetails {
} else { } else {
None None
}; };
// TODO(@kitsonk) add `self.code_actions` let (command, additional_text_edits) = parse_code_actions(
self.code_actions.as_ref(),
data,
specifier,
language_server,
)?;
// TODO(@kitsonk) add `use_code_snippet` // TODO(@kitsonk) add `use_code_snippet`
lsp::CompletionItem { Ok(lsp::CompletionItem {
data: None, data: None,
detail, detail,
documentation, documentation,
command,
additional_text_edits,
..original_item.clone() ..original_item.clone()
} })
} }
} }
@ -1951,6 +2038,36 @@ pub struct CompletionItemData {
pub use_code_snippet: bool, pub use_code_snippet: bool,
} }
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CompletionEntryDataImport {
module_specifier: String,
file_name: String,
}
/// Modify an import statement text replacement to have the correct import
/// specifier to work with Deno module resolution.
fn update_import_statement(
mut text_edit: lsp::TextEdit,
item_data: &CompletionItemData,
) -> lsp::TextEdit {
if let Some(data) = &item_data.data {
if let Ok(import_data) =
serde_json::from_value::<CompletionEntryDataImport>(data.clone())
{
if let Ok(import_specifier) = normalize_specifier(&import_data.file_name)
{
let new_module_specifier =
relative_specifier(&import_specifier, &item_data.specifier);
text_edit.new_text = text_edit
.new_text
.replace(&import_data.module_specifier, &new_module_specifier);
}
}
}
text_edit
}
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CompletionEntry { pub struct CompletionEntry {
@ -2087,8 +2204,7 @@ impl CompletionEntry {
let use_code_snippet = settings.complete_function_calls let use_code_snippet = settings.complete_function_calls
&& (kind == Some(lsp::CompletionItemKind::FUNCTION) && (kind == Some(lsp::CompletionItemKind::FUNCTION)
|| kind == Some(lsp::CompletionItemKind::METHOD)); || kind == Some(lsp::CompletionItemKind::METHOD));
// TODO(@kitsonk) missing from types: https://github.com/gluon-lang/lsp-types/issues/204 let commit_characters = self.get_commit_characters(info, settings);
let _commit_characters = self.get_commit_characters(info, settings);
let mut insert_text = self.insert_text.clone(); let mut insert_text = self.insert_text.clone();
let range = self.replacement_span.clone(); let range = self.replacement_span.clone();
let mut filter_text = self.get_filter_text(); let mut filter_text = self.get_filter_text();
@ -2158,9 +2274,8 @@ impl CompletionEntry {
insert_text, insert_text,
detail, detail,
tags, tags,
data: Some(json!({ commit_characters,
"tsc": tsc, data: Some(json!({ "tsc": tsc })),
})),
..Default::default() ..Default::default()
} }
} }
@ -2662,6 +2777,27 @@ fn start(
Ok(()) Ok(())
} }
#[derive(Debug, Deserialize_repr, Serialize_repr)]
#[repr(u32)]
pub enum CompletionTriggerKind {
Invoked = 1,
TriggerCharacter = 2,
TriggerForIncompleteCompletions = 3,
}
impl From<lsp::CompletionTriggerKind> for CompletionTriggerKind {
fn from(kind: lsp::CompletionTriggerKind) -> Self {
match kind {
lsp::CompletionTriggerKind::INVOKED => Self::Invoked,
lsp::CompletionTriggerKind::TRIGGER_CHARACTER => Self::TriggerCharacter,
lsp::CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS => {
Self::TriggerForIncompleteCompletions
}
_ => Self::Invoked,
}
}
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[allow(dead_code)] #[allow(dead_code)]
@ -2690,6 +2826,15 @@ pub enum ImportModuleSpecifierEnding {
Js, Js,
} }
#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub enum IncludeInlayParameterNameHints {
None,
Literals,
All,
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[allow(dead_code)] #[allow(dead_code)]
@ -2699,6 +2844,15 @@ pub enum IncludePackageJsonAutoImports {
Off, Off,
} }
#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub enum JsxAttributeCompletionStyle {
Auto,
Braces,
None,
}
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GetCompletionsAtPositionOptions { pub struct GetCompletionsAtPositionOptions {
@ -2706,6 +2860,8 @@ pub struct GetCompletionsAtPositionOptions {
pub user_preferences: UserPreferences, pub user_preferences: UserPreferences,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub trigger_character: Option<String>, pub trigger_character: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_kind: Option<CompletionTriggerKind>,
} }
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
@ -2726,6 +2882,12 @@ pub struct UserPreferences {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub include_completions_with_insert_text: Option<bool>, pub include_completions_with_insert_text: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub include_completions_with_class_member_snippets: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_completions_with_object_literal_method_snippets: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_label_details_in_completion_entries: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_incomplete_completions: Option<bool>, pub allow_incomplete_completions: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub import_module_specifier_preference: pub import_module_specifier_preference:
@ -2740,6 +2902,24 @@ pub struct UserPreferences {
pub include_package_json_auto_imports: Option<IncludePackageJsonAutoImports>, pub include_package_json_auto_imports: Option<IncludePackageJsonAutoImports>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub provide_refactor_not_applicable_reason: Option<bool>, pub provide_refactor_not_applicable_reason: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jsx_attribute_completion_style: Option<JsxAttributeCompletionStyle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_parameter_name_hints:
Option<IncludeInlayParameterNameHints>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_parameter_name_hints_when_argument_matches_name:
Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_function_parameter_type_hints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_variable_type_hints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_property_declaration_type_hints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_function_like_return_type_hints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_enum_member_value_hints: Option<bool>,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -2789,17 +2969,20 @@ pub struct GetCompletionDetailsArgs {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>, pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub preferences: Option<UserPreferences>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>, pub data: Option<Value>,
} }
impl From<CompletionItemData> for GetCompletionDetailsArgs { impl From<&CompletionItemData> for GetCompletionDetailsArgs {
fn from(item_data: CompletionItemData) -> Self { fn from(item_data: &CompletionItemData) -> Self {
Self { Self {
specifier: item_data.specifier, specifier: item_data.specifier.clone(),
position: item_data.position, position: item_data.position,
name: item_data.name, name: item_data.name.clone(),
source: item_data.source, source: item_data.source.clone(),
data: item_data.data, preferences: None,
data: item_data.data.clone(),
} }
} }
} }
@ -3809,6 +3992,7 @@ mod tests {
..Default::default() ..Default::default()
}, },
trigger_character: Some(".".to_string()), trigger_character: Some(".".to_string()),
trigger_kind: None,
}, },
)), )),
Default::default(), Default::default(),
@ -3825,6 +4009,7 @@ mod tests {
position, position,
name: "log".to_string(), name: "log".to_string(),
source: None, source: None,
preferences: None,
data: None, data: None,
}), }),
Default::default(), Default::default(),
@ -3919,4 +4104,79 @@ mod tests {
}) })
); );
} }
#[test]
fn test_update_import_statement() {
let fixtures = vec![
(
"file:///a/a.ts",
"./b",
"file:///a/b.ts",
"import { b } from \"./b\";\n\n",
"import { b } from \"./b.ts\";\n\n",
),
(
"file:///a/a.ts",
"../b/b",
"file:///b/b.ts",
"import { b } from \"../b/b\";\n\n",
"import { b } from \"../b/b.ts\";\n\n",
),
("file:///a/a.ts", "./b", "file:///a/b.ts", ", b", ", b"),
];
for (
specifier_text,
module_specifier,
file_name,
orig_text,
expected_text,
) in fixtures
{
let specifier = ModuleSpecifier::parse(specifier_text).unwrap();
let item_data = CompletionItemData {
specifier: specifier.clone(),
position: 0,
name: "b".to_string(),
source: None,
data: Some(json!({
"moduleSpecifier": module_specifier,
"fileName": file_name,
})),
use_code_snippet: false,
};
let actual = update_import_statement(
lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 0,
},
end: lsp::Position {
line: 0,
character: 0,
},
},
new_text: orig_text.to_string(),
},
&item_data,
);
assert_eq!(
actual,
lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 0,
},
end: lsp::Position {
line: 0,
character: 0,
},
},
new_text: expected_text.to_string(),
}
);
}
}
} }

View file

@ -774,35 +774,40 @@ fn lsp_import_map_import_completions() {
"kind": 19, "kind": 19,
"detail": "(local)", "detail": "(local)",
"sortText": "1", "sortText": "1",
"insertText": "." "insertText": ".",
"commitCharacters": ["\"", "'", "/"],
}, },
{ {
"label": "..", "label": "..",
"kind": 19, "kind": 19,
"detail": "(local)", "detail": "(local)",
"sortText": "1", "sortText": "1",
"insertText": ".." "insertText": "..",
"commitCharacters": ["\"", "'", "/"],
}, },
{ {
"label": "std", "label": "std",
"kind": 19, "kind": 19,
"detail": "(import map)", "detail": "(import map)",
"sortText": "std", "sortText": "std",
"insertText": "std" "insertText": "std",
"commitCharacters": ["\"", "'", "/"],
}, },
{ {
"label": "fs", "label": "fs",
"kind": 17, "kind": 17,
"detail": "(import map)", "detail": "(import map)",
"sortText": "fs", "sortText": "fs",
"insertText": "fs" "insertText": "fs",
"commitCharacters": ["\"", "'", "/"],
}, },
{ {
"label": "/~", "label": "/~",
"kind": 19, "kind": 19,
"detail": "(import map)", "detail": "(import map)",
"sortText": "/~", "sortText": "/~",
"insertText": "/~" "insertText": "/~",
"commitCharacters": ["\"", "'", "/"],
} }
] ]
})) }))
@ -883,7 +888,8 @@ fn lsp_import_map_import_completions() {
} }
}, },
"newText": "/~/b.ts" "newText": "/~/b.ts"
} },
"commitCharacters": ["\"", "'", "/"],
} }
] ]
})) }))
@ -3490,6 +3496,7 @@ fn lsp_completions_optional() {
"sortText": "11", "sortText": "11",
"filterText": "b", "filterText": "b",
"insertText": "b", "insertText": "b",
"commitCharacters": [".", ",", ";", "("],
"data": { "data": {
"tsc": { "tsc": {
"specifier": "file:///a/file.ts", "specifier": "file:///a/file.ts",
@ -3527,6 +3534,124 @@ fn lsp_completions_optional() {
shutdown(&mut client); shutdown(&mut client);
} }
#[test]
fn lsp_completions_auto_import() {
let mut client = init("initialize_params.json");
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/b.ts",
"languageId": "typescript",
"version": 1,
"text": "export const foo = \"foo\";\n",
}
}),
);
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "export {};\n\n",
}
}),
);
let (maybe_res, maybe_err) = client
.write_request(
"textDocument/completion",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": {
"line": 2,
"character": 0,
},
"context": {
"triggerKind": 1,
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
if let Some(lsp::CompletionResponse::List(list)) = maybe_res {
assert!(!list.is_incomplete);
if !list.items.iter().any(|item| item.label == "foo") {
panic!("completions items missing 'foo' symbol");
}
} else {
panic!("unexpected completion response");
}
let (maybe_res, maybe_err) = client
.write_request(
"completionItem/resolve",
json!({
"label": "foo",
"kind": 6,
"sortText": "￿16",
"commitCharacters": [
".",
",",
";",
"("
],
"data": {
"tsc": {
"specifier": "file:///a/file.ts",
"position": 12,
"name": "foo",
"source": "./b",
"data": {
"exportName": "foo",
"moduleSpecifier": "./b",
"fileName": "file:///a/b.ts"
},
"useCodeSnippet": false
}
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
maybe_res,
Some(json!({
"label": "foo",
"kind": 6,
"detail": "const foo: \"foo\"",
"documentation": {
"kind": "markdown",
"value": ""
},
"sortText": "￿16",
"additionalTextEdits": [
{
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 0,
"character": 0
}
},
"newText": "import { foo } from \"./b.ts\";\n\n"
}
],
"commitCharacters": [
".",
",",
";",
"("
]
}))
);
}
#[test] #[test]
fn lsp_completions_registry() { fn lsp_completions_registry() {
let _g = http_server(); let _g = http_server();

View file

@ -6,14 +6,24 @@
"kind": 19, "kind": 19,
"detail": "(local)", "detail": "(local)",
"sortText": "1", "sortText": "1",
"insertText": "." "insertText": ".",
"commitCharacters": [
"\"",
"'",
"/"
]
}, },
{ {
"label": "..", "label": "..",
"kind": 19, "kind": 19,
"detail": "(local)", "detail": "(local)",
"sortText": "1", "sortText": "1",
"insertText": ".." "insertText": "..",
"commitCharacters": [
"\"",
"'",
"/"
]
}, },
{ {
"label": "http://localhost:4545", "label": "http://localhost:4545",

View file

@ -718,10 +718,9 @@ delete Object.prototype.__proto__;
request.args.specifier, request.args.specifier,
request.args.position, request.args.position,
request.args.name, request.args.name,
undefined, {},
request.args.source, request.args.source,
undefined, request.args.preferences,
// @ts-expect-error this exists in 4.3 but not part of the d.ts
request.args.data, request.args.data,
), ),
); );

View file

@ -50,6 +50,7 @@ declare global {
} }
type LanguageServerRequest = type LanguageServerRequest =
| Restart
| ConfigureRequest | ConfigureRequest
| FindRenameLocationsRequest | FindRenameLocationsRequest
| GetAssets | GetAssets
@ -138,7 +139,8 @@ declare global {
position: number; position: number;
name: string; name: string;
source?: string; source?: string;
data?: unknown; preferences?: ts.UserPreferences;
data?: ts.CompletionEntryData;
}; };
} }
@ -252,7 +254,7 @@ declare global {
position: number; position: number;
} }
interface Restart { interface Restart extends BaseLanguageServerRequest {
method: "restart"; method: "restart";
} }
} }