summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2022-07-12 09:35:18 +1000
committerGitHub <noreply@github.com>2022-07-12 09:35:18 +1000
commit5db16d122914336124620a5152655917e58f05a6 (patch)
tree41c5124e3fbe7ba13a54ab710c2bdbabbbea1d95
parent82431062fa6dd82679e1903dda4e33103a0299da (diff)
fix(lsp): enable auto imports (#15145)
Fixes: #15111
-rw-r--r--cli/lsp/completions.rs27
-rw-r--r--cli/lsp/language_server.rs47
-rw-r--r--cli/lsp/tsc.rs322
-rw-r--r--cli/tests/integration/lsp_tests.rs137
-rw-r--r--cli/tests/testdata/lsp/completion_request_response_empty.json14
-rw-r--r--cli/tsc/99_main_compiler.js5
-rw-r--r--cli/tsc/compiler.d.ts6
7 files changed, 504 insertions, 54 deletions
diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs
index a41e26bf5..e69df8079 100644
--- a/cli/lsp/completions.rs
+++ b/cli/lsp/completions.rs
@@ -31,6 +31,7 @@ static FILE_PROTO_RE: Lazy<Regex> =
const CURRENT_PATH: &str = ".";
const PARENT_PATH: &str = "..";
const LOCAL_PATHS: &[&str] = &[CURRENT_PATH, PARENT_PATH];
+const IMPORT_COMMIT_CHARS: &[&str] = &["\"", "'", "/"];
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -182,6 +183,9 @@ pub async fn get_import_completions(
detail: Some("(local)".to_string()),
sort_text: Some("1".to_string()),
insert_text: Some(s.to_string()),
+ commit_characters: Some(
+ IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
+ ),
..Default::default()
})
.collect();
@@ -231,6 +235,9 @@ fn get_base_import_map_completions(
detail: Some("(import map)".to_string()),
sort_text: Some(label.clone()),
insert_text: Some(label),
+ commit_characters: Some(
+ IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
+ ),
..Default::default()
}
})
@@ -284,6 +291,9 @@ fn get_import_map_completions(
sort_text: Some("1".to_string()),
filter_text: Some(new_text),
text_edit,
+ commit_characters: Some(
+ IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
+ ),
..Default::default()
})
} else {
@@ -311,6 +321,9 @@ fn get_import_map_completions(
detail: Some("(import map)".to_string()),
sort_text: Some("1".to_string()),
text_edit,
+ commit_characters: Some(
+ IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
+ ),
..Default::default()
});
}
@@ -382,6 +395,9 @@ fn get_local_completions(
filter_text,
sort_text: Some("1".to_string()),
text_edit,
+ commit_characters: Some(
+ IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
+ ),
..Default::default()
}),
Ok(file_type) if file_type.is_file() => {
@@ -393,6 +409,9 @@ fn get_local_completions(
filter_text,
sort_text: Some("1".to_string()),
text_edit,
+ commit_characters: Some(
+ IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
+ ),
..Default::default()
})
} else {
@@ -463,6 +482,9 @@ fn get_workspace_completions(
detail,
sort_text: Some("1".to_string()),
text_edit,
+ commit_characters: Some(
+ IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
+ ),
..Default::default()
})
} else {
@@ -484,7 +506,7 @@ fn get_workspace_completions(
/// assert_eq!(relative_specifier(&specifier, &base), "../b.ts");
/// ```
///
-fn relative_specifier(
+pub fn relative_specifier(
specifier: &ModuleSpecifier,
base: &ModuleSpecifier,
) -> String {
@@ -812,6 +834,9 @@ mod tests {
},
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()
}]
);
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index d5fa03e24..05d23bf00 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -1764,11 +1764,15 @@ impl Inner {
Some(response)
} else {
let line_index = asset_or_doc.line_index();
- let trigger_character = if let Some(context) = &params.context {
- context.trigger_character.clone()
- } else {
- None
- };
+ let (trigger_character, trigger_kind) =
+ if let Some(context) = &params.context {
+ (
+ context.trigger_character.clone(),
+ Some(context.trigger_kind.into()),
+ )
+ } else {
+ (None, None)
+ };
let position =
line_index.offset_tsc(params.text_document_position.position)?;
let req = tsc::RequestMethod::GetCompletions((
@@ -1776,14 +1780,30 @@ impl Inner {
position,
tsc::GetCompletionsAtPositionOptions {
user_preferences: tsc::UserPreferences {
+ 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),
- provide_refactor_not_applicable_reason: 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),
- allow_incomplete_completions: 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()
},
trigger_character,
+ trigger_kind,
},
));
let snapshot = self.snapshot();
@@ -1822,7 +1842,8 @@ impl Inner {
"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 maybe_completion_info: Option<tsc::CompletionEntryDetails> =
self.ts_server.request(self.snapshot(), req).await.map_err(
@@ -1832,7 +1853,15 @@ impl Inner {
},
)?;
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 {
error!(
"Received an undefined response from tsc for completion details."
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index c362d4ce3..b701893df 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -1,6 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use super::code_lens;
+use super::completions::relative_specifier;
use super::config;
use super::documents::AssetOrDocument;
use super::language_server;
@@ -511,7 +512,7 @@ pub enum ScriptElementKind {
Link,
#[serde(rename = "link name")]
LinkName,
- #[serde(rename = "link test")]
+ #[serde(rename = "link text")]
LinkText,
}
@@ -636,7 +637,7 @@ pub struct SymbolDisplayPart {
target: Option<DocumentSpan>,
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsDocTagInfo {
name: String,
@@ -1285,7 +1286,14 @@ pub struct 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,
line_index: Arc<LineIndex>,
) -> lsp::OneOf<lsp::TextEdit, lsp::AnnotatedTextEdit> {
@@ -1315,7 +1323,7 @@ impl FileTextChanges {
let edits = self
.text_changes
.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();
Ok(lsp::TextDocumentEdit {
text_document: lsp::OptionalVersionedTextDocumentIdentifier {
@@ -1359,7 +1367,7 @@ impl FileTextChanges {
let edits = self
.text_changes
.iter()
- .map(|tc| tc.as_text_edit(line_index.clone()))
+ .map(|tc| tc.as_text_or_annotated_text_edit(line_index.clone()))
.collect();
ops.push(lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit {
text_document: lsp::OptionalVersionedTextDocumentIdentifier {
@@ -1579,13 +1587,13 @@ impl RefactorEditInfo {
}
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeAction {
- // description: String,
- // changes: Vec<FileTextChanges>,
- // #[serde(skip_serializing_if = "Option::is_none")]
- // commands: Option<Vec<Value>>,
+ description: String,
+ changes: Vec<FileTextChanges>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ commands: Option<Vec<Value>>,
}
#[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")]
pub struct CompletionEntryDetails {
display_parts: Vec<SymbolDisplayPart>,
documentation: Option<Vec<SymbolDisplayPart>>,
tags: Option<Vec<JsDocTagInfo>>,
- // name: String,
- // kind: ScriptElementKind,
- // kind_modifiers: String,
- // code_actions: Option<Vec<CodeAction>>,
- // source_display: Option<Vec<SymbolDisplayPart>>,
+ name: String,
+ kind: ScriptElementKind,
+ kind_modifiers: String,
+ code_actions: Option<Vec<CodeAction>>,
+ source_display: Option<Vec<SymbolDisplayPart>>,
}
impl CompletionEntryDetails {
pub fn as_completion_item(
&self,
original_item: &lsp::CompletionItem,
+ data: &CompletionItemData,
+ specifier: &ModuleSpecifier,
language_server: &language_server::Inner,
- ) -> lsp::CompletionItem {
+ ) -> Result<lsp::CompletionItem, AnyError> {
let detail = if original_item.detail.is_some() {
original_item.detail.clone()
} else if !self.display_parts.is_empty() {
@@ -1862,15 +1942,22 @@ impl CompletionEntryDetails {
} else {
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`
- lsp::CompletionItem {
+ Ok(lsp::CompletionItem {
data: None,
detail,
documentation,
+ command,
+ additional_text_edits,
..original_item.clone()
- }
+ })
}
}
@@ -1951,6 +2038,36 @@ pub struct CompletionItemData {
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)]
#[serde(rename_all = "camelCase")]
pub struct CompletionEntry {
@@ -2087,8 +2204,7 @@ impl CompletionEntry {
let use_code_snippet = settings.complete_function_calls
&& (kind == Some(lsp::CompletionItemKind::FUNCTION)
|| 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 range = self.replacement_span.clone();
let mut filter_text = self.get_filter_text();
@@ -2158,9 +2274,8 @@ impl CompletionEntry {
insert_text,
detail,
tags,
- data: Some(json!({
- "tsc": tsc,
- })),
+ commit_characters,
+ data: Some(json!({ "tsc": tsc })),
..Default::default()
}
}
@@ -2662,6 +2777,27 @@ fn start(
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)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
@@ -2693,12 +2829,30 @@ pub enum ImportModuleSpecifierEnding {
#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
+pub enum IncludeInlayParameterNameHints {
+ None,
+ Literals,
+ All,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "kebab-case")]
+#[allow(dead_code)]
pub enum IncludePackageJsonAutoImports {
Auto,
On,
Off,
}
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "kebab-case")]
+#[allow(dead_code)]
+pub enum JsxAttributeCompletionStyle {
+ Auto,
+ Braces,
+ None,
+}
+
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsAtPositionOptions {
@@ -2706,6 +2860,8 @@ pub struct GetCompletionsAtPositionOptions {
pub user_preferences: UserPreferences,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_character: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub trigger_kind: Option<CompletionTriggerKind>,
}
#[derive(Debug, Default, Serialize)]
@@ -2726,6 +2882,12 @@ pub struct UserPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub include_completions_with_insert_text: Option<bool>,
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub import_module_specifier_preference:
@@ -2740,6 +2902,24 @@ pub struct UserPreferences {
pub include_package_json_auto_imports: Option<IncludePackageJsonAutoImports>,
#[serde(skip_serializing_if = "Option::is_none")]
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)]
@@ -2789,17 +2969,20 @@ pub struct GetCompletionDetailsArgs {
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
+ pub preferences: Option<UserPreferences>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
-impl From<CompletionItemData> for GetCompletionDetailsArgs {
- fn from(item_data: CompletionItemData) -> Self {
+impl From<&CompletionItemData> for GetCompletionDetailsArgs {
+ fn from(item_data: &CompletionItemData) -> Self {
Self {
- specifier: item_data.specifier,
+ specifier: item_data.specifier.clone(),
position: item_data.position,
- name: item_data.name,
- source: item_data.source,
- data: item_data.data,
+ name: item_data.name.clone(),
+ source: item_data.source.clone(),
+ preferences: None,
+ data: item_data.data.clone(),
}
}
}
@@ -3809,6 +3992,7 @@ mod tests {
..Default::default()
},
trigger_character: Some(".".to_string()),
+ trigger_kind: None,
},
)),
Default::default(),
@@ -3825,6 +4009,7 @@ mod tests {
position,
name: "log".to_string(),
source: None,
+ preferences: None,
data: None,
}),
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(),
+ }
+ );
+ }
+ }
}
diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs
index 213827057..568ac6e7c 100644
--- a/cli/tests/integration/lsp_tests.rs
+++ b/cli/tests/integration/lsp_tests.rs
@@ -774,35 +774,40 @@ fn lsp_import_map_import_completions() {
"kind": 19,
"detail": "(local)",
"sortText": "1",
- "insertText": "."
+ "insertText": ".",
+ "commitCharacters": ["\"", "'", "/"],
},
{
"label": "..",
"kind": 19,
"detail": "(local)",
"sortText": "1",
- "insertText": ".."
+ "insertText": "..",
+ "commitCharacters": ["\"", "'", "/"],
},
{
"label": "std",
"kind": 19,
"detail": "(import map)",
"sortText": "std",
- "insertText": "std"
+ "insertText": "std",
+ "commitCharacters": ["\"", "'", "/"],
},
{
"label": "fs",
"kind": 17,
"detail": "(import map)",
"sortText": "fs",
- "insertText": "fs"
+ "insertText": "fs",
+ "commitCharacters": ["\"", "'", "/"],
},
{
"label": "/~",
"kind": 19,
"detail": "(import map)",
"sortText": "/~",
- "insertText": "/~"
+ "insertText": "/~",
+ "commitCharacters": ["\"", "'", "/"],
}
]
}))
@@ -883,7 +888,8 @@ fn lsp_import_map_import_completions() {
}
},
"newText": "/~/b.ts"
- }
+ },
+ "commitCharacters": ["\"", "'", "/"],
}
]
}))
@@ -3490,6 +3496,7 @@ fn lsp_completions_optional() {
"sortText": "11",
"filterText": "b",
"insertText": "b",
+ "commitCharacters": [".", ",", ";", "("],
"data": {
"tsc": {
"specifier": "file:///a/file.ts",
@@ -3528,6 +3535,124 @@ fn lsp_completions_optional() {
}
#[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]
fn lsp_completions_registry() {
let _g = http_server();
let mut client = init("initialize_params_registry.json");
diff --git a/cli/tests/testdata/lsp/completion_request_response_empty.json b/cli/tests/testdata/lsp/completion_request_response_empty.json
index 272dfb475..c2218aaa7 100644
--- a/cli/tests/testdata/lsp/completion_request_response_empty.json
+++ b/cli/tests/testdata/lsp/completion_request_response_empty.json
@@ -6,14 +6,24 @@
"kind": 19,
"detail": "(local)",
"sortText": "1",
- "insertText": "."
+ "insertText": ".",
+ "commitCharacters": [
+ "\"",
+ "'",
+ "/"
+ ]
},
{
"label": "..",
"kind": 19,
"detail": "(local)",
"sortText": "1",
- "insertText": ".."
+ "insertText": "..",
+ "commitCharacters": [
+ "\"",
+ "'",
+ "/"
+ ]
},
{
"label": "http://localhost:4545",
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js
index c2b50ba16..85ab38ccc 100644
--- a/cli/tsc/99_main_compiler.js
+++ b/cli/tsc/99_main_compiler.js
@@ -718,10 +718,9 @@ delete Object.prototype.__proto__;
request.args.specifier,
request.args.position,
request.args.name,
- undefined,
+ {},
request.args.source,
- undefined,
- // @ts-expect-error this exists in 4.3 but not part of the d.ts
+ request.args.preferences,
request.args.data,
),
);
diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts
index 1ba116170..bf0660470 100644
--- a/cli/tsc/compiler.d.ts
+++ b/cli/tsc/compiler.d.ts
@@ -50,6 +50,7 @@ declare global {
}
type LanguageServerRequest =
+ | Restart
| ConfigureRequest
| FindRenameLocationsRequest
| GetAssets
@@ -138,7 +139,8 @@ declare global {
position: number;
name: string;
source?: string;
- data?: unknown;
+ preferences?: ts.UserPreferences;
+ data?: ts.CompletionEntryData;
};
}
@@ -252,7 +254,7 @@ declare global {
position: number;
}
- interface Restart {
+ interface Restart extends BaseLanguageServerRequest {
method: "restart";
}
}