summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuca Casonato <lucacasonato@yahoo.com>2020-12-08 11:36:13 +0100
committerGitHub <noreply@github.com>2020-12-08 11:36:13 +0100
commite94a18240e5b6312358f787c19dffd3006300a4b (patch)
treebe99df02a2851beea0ccec60321deefc46b0d465
parent8bf3e0f4c6980c0f3ed8b618062657b017a0f609 (diff)
feat(lsp): basic support for textDocument/completion (#8651)
-rw-r--r--cli/lsp/capabilities.rs19
-rw-r--r--cli/lsp/handlers.rs32
-rw-r--r--cli/lsp/mod.rs1
-rw-r--r--cli/lsp/tsc.rs219
-rw-r--r--cli/tsc/99_main_compiler.js10
-rw-r--r--cli/tsc/compiler.d.ts10
6 files changed, 287 insertions, 4 deletions
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs
index cf8f150ca..954baaf51 100644
--- a/cli/lsp/capabilities.rs
+++ b/cli/lsp/capabilities.rs
@@ -6,6 +6,7 @@
///! client.
///!
use lsp_types::ClientCapabilities;
+use lsp_types::CompletionOptions;
use lsp_types::HoverProviderCapability;
use lsp_types::OneOf;
use lsp_types::SaveOptions;
@@ -13,6 +14,7 @@ use lsp_types::ServerCapabilities;
use lsp_types::TextDocumentSyncCapability;
use lsp_types::TextDocumentSyncKind;
use lsp_types::TextDocumentSyncOptions;
+use lsp_types::WorkDoneProgressOptions;
pub fn server_capabilities(
_client_capabilities: &ClientCapabilities,
@@ -28,7 +30,22 @@ pub fn server_capabilities(
},
)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
- completion_provider: None,
+ completion_provider: Some(CompletionOptions {
+ trigger_characters: Some(vec![
+ ".".to_string(),
+ "\"".to_string(),
+ "'".to_string(),
+ "`".to_string(),
+ "/".to_string(),
+ "@".to_string(),
+ "<".to_string(),
+ "#".to_string(),
+ ]),
+ resolve_provider: None,
+ work_done_progress_options: WorkDoneProgressOptions {
+ work_done_progress: None,
+ },
+ }),
signature_help_provider: None,
declaration_provider: None,
definition_provider: Some(OneOf::Left(true)),
diff --git a/cli/lsp/handlers.rs b/cli/lsp/handlers.rs
index 6dd7321c7..ccda69f7d 100644
--- a/cli/lsp/handlers.rs
+++ b/cli/lsp/handlers.rs
@@ -12,6 +12,8 @@ use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::ModuleSpecifier;
use dprint_plugin_typescript as dprint;
+use lsp_types::CompletionParams;
+use lsp_types::CompletionResponse;
use lsp_types::DocumentFormattingParams;
use lsp_types::DocumentHighlight;
use lsp_types::DocumentHighlightParams;
@@ -187,6 +189,36 @@ pub fn handle_hover(
}
}
+pub fn handle_completion(
+ state: &mut ServerState,
+ params: CompletionParams,
+) -> Result<Option<CompletionResponse>, AnyError> {
+ let specifier =
+ utils::normalize_url(params.text_document_position.text_document.uri);
+ let line_index = get_line_index(state, &specifier)?;
+ let server_state = state.snapshot();
+ let maybe_completion_info: Option<tsc::CompletionInfo> =
+ serde_json::from_value(tsc::request(
+ &mut state.ts_runtime,
+ &server_state,
+ tsc::RequestMethod::GetCompletions((
+ specifier,
+ text::to_char_pos(&line_index, params.text_document_position.position),
+ tsc::UserPreferences {
+ // TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651
+ include_completions_with_insert_text: Some(false),
+ ..Default::default()
+ },
+ )),
+ )?)?;
+
+ if let Some(completions) = maybe_completion_info {
+ Ok(Some(completions.into_completion_response(&line_index)))
+ } else {
+ Ok(None)
+ }
+}
+
pub fn handle_references(
state: &mut ServerState,
params: ReferenceParams,
diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs
index c26c5d89e..e3092d815 100644
--- a/cli/lsp/mod.rs
+++ b/cli/lsp/mod.rs
@@ -382,6 +382,7 @@ impl ServerState {
handlers::handle_goto_definition,
)?
.on_sync::<lsp_types::request::HoverRequest>(handlers::handle_hover)?
+ .on_sync::<lsp_types::request::Completion>(handlers::handle_completion)?
.on_sync::<lsp_types::request::References>(handlers::handle_references)?
.on::<lsp_types::request::Formatting>(handlers::handle_formatting)
.on::<lsp_extensions::VirtualTextDocument>(
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index 65f6ebbdb..6ed727d9f 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -14,6 +14,7 @@ use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::json_op_sync;
use deno_core::serde::Deserialize;
+use deno_core::serde::Serialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
@@ -229,7 +230,7 @@ fn replace_links(text: &str) -> String {
.to_string()
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Clone, Deserialize)]
pub enum ScriptElementKind {
#[serde(rename = "")]
Unknown,
@@ -301,7 +302,47 @@ pub enum ScriptElementKind {
String,
}
-#[derive(Debug, Deserialize)]
+impl From<ScriptElementKind> for lsp_types::CompletionItemKind {
+ fn from(kind: ScriptElementKind) -> Self {
+ use lsp_types::CompletionItemKind;
+
+ match kind {
+ ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => {
+ CompletionItemKind::Keyword
+ }
+ ScriptElementKind::ConstElement => CompletionItemKind::Constant,
+ ScriptElementKind::LetElement
+ | ScriptElementKind::VariableElement
+ | ScriptElementKind::LocalVariableElement
+ | ScriptElementKind::Alias => CompletionItemKind::Variable,
+ ScriptElementKind::MemberVariableElement
+ | ScriptElementKind::MemberGetAccessorElement
+ | ScriptElementKind::MemberSetAccessorElement => {
+ CompletionItemKind::Field
+ }
+ ScriptElementKind::FunctionElement => CompletionItemKind::Function,
+ ScriptElementKind::MemberFunctionElement
+ | ScriptElementKind::ConstructSignatureElement
+ | ScriptElementKind::CallSignatureElement
+ | ScriptElementKind::IndexSignatureElement => CompletionItemKind::Method,
+ ScriptElementKind::EnumElement => CompletionItemKind::Enum,
+ ScriptElementKind::ModuleElement
+ | ScriptElementKind::ExternalModuleName => CompletionItemKind::Module,
+ ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => {
+ CompletionItemKind::Class
+ }
+ ScriptElementKind::InterfaceElement => CompletionItemKind::Interface,
+ ScriptElementKind::Warning | ScriptElementKind::ScriptElement => {
+ CompletionItemKind::File
+ }
+ ScriptElementKind::Directory => CompletionItemKind::Folder,
+ ScriptElementKind::String => CompletionItemKind::Constant,
+ _ => CompletionItemKind::Property,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextSpan {
start: u32,
@@ -519,6 +560,104 @@ impl ReferenceEntry {
}
}
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CompletionInfo {
+ entries: Vec<CompletionEntry>,
+ is_member_completion: bool,
+}
+
+impl CompletionInfo {
+ pub fn into_completion_response(
+ self,
+ line_index: &[u32],
+ ) -> lsp_types::CompletionResponse {
+ let items = self
+ .entries
+ .into_iter()
+ .map(|entry| entry.into_completion_item(line_index))
+ .collect();
+ lsp_types::CompletionResponse::Array(items)
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CompletionEntry {
+ kind: ScriptElementKind,
+ kind_modifiers: Option<String>,
+ name: String,
+ sort_text: String,
+ insert_text: Option<String>,
+ replacement_span: Option<TextSpan>,
+ has_action: Option<bool>,
+ source: Option<String>,
+ is_recommended: Option<bool>,
+}
+
+impl CompletionEntry {
+ pub fn into_completion_item(
+ self,
+ line_index: &[u32],
+ ) -> lsp_types::CompletionItem {
+ let mut item = lsp_types::CompletionItem {
+ label: self.name,
+ kind: Some(self.kind.into()),
+ sort_text: Some(self.sort_text.clone()),
+ // TODO(lucacasonato): missing commit_characters
+ ..Default::default()
+ };
+
+ if let Some(true) = self.is_recommended {
+ // Make sure isRecommended property always comes first
+ // https://github.com/Microsoft/vscode/issues/40325
+ item.preselect = Some(true);
+ } else if self.source.is_some() {
+ // De-prioritze auto-imports
+ // https://github.com/Microsoft/vscode/issues/40311
+ item.sort_text = Some("\u{ffff}".to_string() + &self.sort_text)
+ }
+
+ match item.kind {
+ Some(lsp_types::CompletionItemKind::Function)
+ | Some(lsp_types::CompletionItemKind::Method) => {
+ item.insert_text_format = Some(lsp_types::InsertTextFormat::Snippet);
+ }
+ _ => {}
+ }
+
+ let mut insert_text = self.insert_text;
+ let replacement_range: Option<lsp_types::Range> =
+ self.replacement_span.map(|span| span.to_range(line_index));
+
+ // TODO(lucacasonato): port other special cases from https://github.com/theia-ide/typescript-language-server/blob/fdf28313833cd6216d00eb4e04dc7f00f4c04f09/server/src/completion.ts#L49-L55
+
+ if let Some(kind_modifiers) = self.kind_modifiers {
+ if kind_modifiers.contains("\\optional\\") {
+ if insert_text.is_none() {
+ insert_text = Some(item.label.clone());
+ }
+ if item.filter_text.is_none() {
+ item.filter_text = Some(item.label.clone());
+ }
+ item.label += "?";
+ }
+ }
+
+ if let Some(insert_text) = insert_text {
+ if let Some(replacement_range) = replacement_range {
+ item.text_edit = Some(lsp_types::CompletionTextEdit::Edit(
+ lsp_types::TextEdit::new(replacement_range, insert_text),
+ ));
+ } else {
+ item.insert_text = Some(insert_text);
+ }
+ }
+
+ item
+ }
+}
+
#[derive(Debug, Clone, Deserialize)]
struct Response {
id: usize,
@@ -815,6 +954,71 @@ pub fn start(debug: bool) -> Result<JsRuntime, AnyError> {
Ok(runtime)
}
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "kebab-case")]
+#[allow(dead_code)]
+pub enum QuotePreference {
+ Auto,
+ Double,
+ Single,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "kebab-case")]
+#[allow(dead_code)]
+pub enum ImportModuleSpecifierPreference {
+ Auto,
+ Relative,
+ NonRelative,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "kebab-case")]
+#[allow(dead_code)]
+pub enum ImportModuleSpecifierEnding {
+ Auto,
+ Minimal,
+ Index,
+ Js,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "kebab-case")]
+#[allow(dead_code)]
+pub enum IncludePackageJsonAutoImports {
+ Auto,
+ On,
+ Off,
+}
+
+#[derive(Debug, Default, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UserPreferences {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub disable_suggestions: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub quote_preference: Option<QuotePreference>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub include_completions_for_module_exports: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub include_automatic_optional_chain_completions: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub include_completions_with_insert_text: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub import_module_specifier_preference:
+ Option<ImportModuleSpecifierPreference>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub import_module_specifier_ending: Option<ImportModuleSpecifierEnding>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub allow_text_changes_in_new_files: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub provide_prefix_and_suffix_text_for_rename: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub include_package_json_auto_imports: Option<IncludePackageJsonAutoImports>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub provide_refactor_not_applicable_reason: Option<bool>,
+}
+
/// Methods that are supported by the Language Service in the compiler isolate.
pub enum RequestMethod {
/// Configure the compilation settings for the server.
@@ -833,6 +1037,8 @@ pub enum RequestMethod {
GetReferences((ModuleSpecifier, u32)),
/// Get declaration information for a specific position.
GetDefinition((ModuleSpecifier, u32)),
+ /// Get completion information at a given position (IntelliSense).
+ GetCompletions((ModuleSpecifier, u32, UserPreferences)),
}
impl RequestMethod {
@@ -887,6 +1093,15 @@ impl RequestMethod {
"specifier": specifier,
"position": position,
}),
+ RequestMethod::GetCompletions((specifier, position, preferences)) => {
+ json!({
+ "id": id,
+ "method": "getCompletions",
+ "specifier": specifier,
+ "position": position,
+ "preferences": preferences,
+ })
+ }
}
}
}
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js
index f379d6bae..a78b85203 100644
--- a/cli/tsc/99_main_compiler.js
+++ b/cli/tsc/99_main_compiler.js
@@ -514,6 +514,16 @@ delete Object.prototype.__proto__;
),
);
}
+ case "getCompletions": {
+ return respond(
+ id,
+ languageService.getCompletionsAtPosition(
+ request.specifier,
+ request.position,
+ request.preferences,
+ ),
+ );
+ }
case "getDocumentHighlights": {
return respond(
id,
diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts
index 1a899c291..a1f4e851c 100644
--- a/cli/tsc/compiler.d.ts
+++ b/cli/tsc/compiler.d.ts
@@ -48,7 +48,8 @@ declare global {
| GetQuickInfoRequest
| GetDocumentHighlightsRequest
| GetReferencesRequest
- | GetDefinitionRequest;
+ | GetDefinitionRequest
+ | GetCompletionsRequest;
interface BaseLanguageServerRequest {
id: number;
@@ -100,4 +101,11 @@ declare global {
specifier: string;
position: number;
}
+
+ interface GetCompletionsRequest extends BaseLanguageServerRequest {
+ method: "getCompletions";
+ specifier: string;
+ position: number;
+ preferences: ts.UserPreferences;
+ }
}