summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2021-03-16 09:01:41 +1100
committerGitHub <noreply@github.com>2021-03-16 09:01:41 +1100
commit506b321d472005d0cf916823dfa8ea37fa0b064a (patch)
tree6e4da0350772c1143a6efcfaabf99155623fe724
parent2ff9b01551d4bdb3a820774252706d4e58bceaba (diff)
refactor(lsp): refactor completions and add tests (#9789)
-rw-r--r--cli/lsp/capabilities.rs11
-rw-r--r--cli/lsp/config.rs72
-rw-r--r--cli/lsp/language_server.rs139
-rw-r--r--cli/lsp/tsc.rs623
-rw-r--r--cli/tests/lsp/completion_request.json18
-rw-r--r--cli/tests/lsp/completion_resolve_request.json17
-rw-r--r--cli/tests/lsp/did_open_notification_completions.json12
-rw-r--r--cli/tsc/99_main_compiler.js16
-rw-r--r--cli/tsc/compiler.d.ts14
9 files changed, 787 insertions, 135 deletions
diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs
index be318e7f3..82bb910bb 100644
--- a/cli/lsp/capabilities.rs
+++ b/cli/lsp/capabilities.rs
@@ -55,7 +55,12 @@ pub fn server_capabilities(
)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
completion_provider: Some(CompletionOptions {
- all_commit_characters: None,
+ all_commit_characters: Some(vec![
+ ".".to_string(),
+ ",".to_string(),
+ ";".to_string(),
+ "(".to_string(),
+ ]),
trigger_characters: Some(vec![
".".to_string(),
"\"".to_string(),
@@ -66,7 +71,7 @@ pub fn server_capabilities(
"<".to_string(),
"#".to_string(),
]),
- resolve_provider: None,
+ resolve_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
@@ -77,7 +82,7 @@ pub fn server_capabilities(
"(".to_string(),
"<".to_string(),
]),
- retrigger_characters: None,
+ retrigger_characters: Some(vec![")".to_string()]),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs
index 8d31e3d54..201e5f23c 100644
--- a/cli/lsp/config.rs
+++ b/cli/lsp/config.rs
@@ -15,7 +15,7 @@ pub struct ClientCapabilities {
pub workspace_did_change_watched_files: bool,
}
-#[derive(Debug, Default, Clone, Deserialize)]
+#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeLensSettings {
/// Flag for providing implementation code lenses.
@@ -30,13 +30,50 @@ pub struct CodeLensSettings {
pub references_all_functions: bool,
}
+impl Default for CodeLensSettings {
+ fn default() -> Self {
+ Self {
+ implementations: false,
+ references: false,
+ references_all_functions: false,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CompletionSettings {
+ #[serde(default)]
+ pub complete_function_calls: bool,
+ #[serde(default)]
+ pub names: bool,
+ #[serde(default)]
+ pub paths: bool,
+ #[serde(default)]
+ pub auto_imports: bool,
+}
+
+impl Default for CompletionSettings {
+ fn default() -> Self {
+ Self {
+ complete_function_calls: false,
+ names: true,
+ paths: true,
+ auto_imports: true,
+ }
+ }
+}
+
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceSettings {
pub enable: bool,
pub config: Option<String>,
pub import_map: Option<String>,
- pub code_lens: Option<CodeLensSettings>,
+ #[serde(default)]
+ pub code_lens: CodeLensSettings,
+ #[serde(default)]
+ pub suggest: CompletionSettings,
#[serde(default)]
pub lint: bool,
@@ -48,36 +85,7 @@ impl WorkspaceSettings {
/// Determine if any code lenses are enabled at all. This allows short
/// circuiting when there are no code lenses enabled.
pub fn enabled_code_lens(&self) -> bool {
- if let Some(code_lens) = &self.code_lens {
- // This should contain all the "top level" code lens references
- code_lens.implementations || code_lens.references
- } else {
- false
- }
- }
-
- pub fn enabled_code_lens_implementations(&self) -> bool {
- if let Some(code_lens) = &self.code_lens {
- code_lens.implementations
- } else {
- false
- }
- }
-
- pub fn enabled_code_lens_references(&self) -> bool {
- if let Some(code_lens) = &self.code_lens {
- code_lens.references
- } else {
- false
- }
- }
-
- pub fn enabled_code_lens_references_all_functions(&self) -> bool {
- if let Some(code_lens) = &self.code_lens {
- code_lens.references_all_functions
- } else {
- false
- }
+ self.code_lens.implementations || self.code_lens.references
}
}
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index 96983dc52..3c3d82b3b 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -917,7 +917,7 @@ impl Inner {
let mut code_lenses = cl.borrow_mut();
// TSC Implementations Code Lens
- if self.config.settings.enabled_code_lens_implementations() {
+ if self.config.settings.code_lens.implementations {
let source = CodeLensSource::Implementations;
match i.kind {
tsc::ScriptElementKind::InterfaceElement => {
@@ -941,7 +941,7 @@ impl Inner {
}
// TSC References Code Lens
- if self.config.settings.enabled_code_lens_references() {
+ if self.config.settings.code_lens.references {
let source = CodeLensSource::References;
if let Some(parent) = &mp {
if parent.kind == tsc::ScriptElementKind::EnumElement {
@@ -950,11 +950,7 @@ impl Inner {
}
match i.kind {
tsc::ScriptElementKind::FunctionElement => {
- if self
- .config
- .settings
- .enabled_code_lens_references_all_functions()
- {
+ if self.config.settings.code_lens.references_all_functions {
code_lenses.push(i.to_code_lens(
&line_index,
&specifier,
@@ -1358,7 +1354,6 @@ impl Inner {
let specifier = self
.url_map
.normalize_url(&params.text_document_position.text_document.uri);
- // TODO(lucacasonato): handle error correctly
let line_index =
if let Some(line_index) = self.get_line_index_sync(&specifier) {
line_index
@@ -1368,13 +1363,22 @@ impl Inner {
specifier
)));
};
+ let trigger_character = if let Some(context) = &params.context {
+ context.trigger_character.clone()
+ } else {
+ None
+ };
+ let position =
+ line_index.offset_tsc(params.text_document_position.position)?;
let req = tsc::RequestMethod::GetCompletions((
- specifier,
- line_index.offset_tsc(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()
+ specifier.clone(),
+ position,
+ tsc::GetCompletionsAtPositionOptions {
+ user_preferences: tsc::UserPreferences {
+ include_completions_with_insert_text: Some(true),
+ ..Default::default()
+ },
+ trigger_character,
},
));
let maybe_completion_info: Option<tsc::CompletionInfo> = self
@@ -1387,7 +1391,12 @@ impl Inner {
})?;
if let Some(completions) = maybe_completion_info {
- let results = completions.into_completion_response(&line_index);
+ let results = completions.as_completion_response(
+ &line_index,
+ &self.config.settings.suggest,
+ &specifier,
+ position,
+ );
self.performance.measure(mark);
Ok(Some(results))
} else {
@@ -1396,6 +1405,47 @@ impl Inner {
}
}
+ async fn completion_resolve(
+ &mut self,
+ params: CompletionItem,
+ ) -> LspResult<CompletionItem> {
+ let mark = self.performance.mark("completion_resolve");
+ if let Some(data) = &params.data {
+ let data: tsc::CompletionItemData = serde_json::from_value(data.clone())
+ .map_err(|err| {
+ error!("{}", err);
+ LspError::invalid_params(
+ "Could not decode data field of completion item.",
+ )
+ })?;
+ let req = tsc::RequestMethod::GetCompletionDetails(data.into());
+ let maybe_completion_info: Option<tsc::CompletionEntryDetails> = self
+ .ts_server
+ .request(self.snapshot(), req)
+ .await
+ .map_err(|err| {
+ error!("Unable to get completion info from TypeScript: {}", err);
+ LspError::internal_error()
+ })?;
+ if let Some(completion_info) = maybe_completion_info {
+ let completion_item = completion_info.as_completion_item(&params);
+ self.performance.measure(mark);
+ Ok(completion_item)
+ } else {
+ error!(
+ "Received an undefined response from tsc for completion details."
+ );
+ self.performance.measure(mark);
+ Ok(params)
+ }
+ } else {
+ self.performance.measure(mark);
+ Err(LspError::invalid_params(
+ "The completion item is missing the data field.",
+ ))
+ }
+ }
+
async fn goto_implementation(
&mut self,
params: GotoImplementationParams,
@@ -1715,6 +1765,13 @@ impl lspower::LanguageServer for LanguageServer {
self.0.lock().await.completion(params).await
}
+ async fn completion_resolve(
+ &self,
+ params: CompletionItem,
+ ) -> LspResult<CompletionItem> {
+ self.0.lock().await.completion_resolve(params).await
+ }
+
async fn goto_implementation(
&self,
params: GotoImplementationParams,
@@ -2741,6 +2798,58 @@ mod tests {
}
#[derive(Deserialize)]
+ struct CompletionResult {
+ pub result: Option<CompletionResponse>,
+ }
+
+ #[tokio::test]
+ async fn test_completions() {
+ let mut harness = LspTestHarness::new(vec![
+ ("initialize_request.json", LspResponse::RequestAny),
+ ("initialized_notification.json", LspResponse::None),
+ ("did_open_notification_completions.json", LspResponse::None),
+ (
+ "completion_request.json",
+ LspResponse::RequestAssert(|value| {
+ let response: CompletionResult =
+ serde_json::from_value(value).unwrap();
+ let result = response.result.unwrap();
+ match result {
+ CompletionResponse::List(list) => {
+ // there should be at least 90 completions for `Deno.`
+ assert!(list.items.len() > 90);
+ }
+ _ => panic!("unexpected result"),
+ }
+ }),
+ ),
+ (
+ "completion_resolve_request.json",
+ LspResponse::Request(
+ 4,
+ json!({
+ "label": "build",
+ "kind": 6,
+ "detail": "const Deno.build: {\n target: string;\n arch: \"x86_64\";\n os: \"darwin\" | \"linux\" | \"windows\";\n vendor: string;\n env?: string | undefined;\n}",
+ "documentation": {
+ "kind": "markdown",
+ "value": "Build related information."
+ },
+ "sortText": "1",
+ "insertTextFormat": 1,
+ }),
+ ),
+ ),
+ (
+ "shutdown_request.json",
+ LspResponse::Request(3, json!(null)),
+ ),
+ ("exit_notification.json", LspResponse::None),
+ ]);
+ harness.run().await;
+ }
+
+ #[derive(Deserialize)]
struct PerformanceAverages {
averages: Vec<PerformanceAverage>,
}
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index 31434f529..a60f15eb8 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -3,6 +3,7 @@
use super::analysis::CodeLensSource;
use super::analysis::ResolvedDependency;
use super::analysis::ResolvedDependencyErr;
+use super::config;
use super::language_server;
use super::language_server::StateSnapshot;
use super::text;
@@ -35,11 +36,15 @@ use regex::Captures;
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
+use std::collections::HashSet;
use std::thread;
use text_size::TextSize;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
+const FILE_EXTENSION_KIND_MODIFIERS: &[&str] =
+ &[".d.ts", ".ts", ".tsx", ".js", ".jsx", ".json"];
+
type Request = (
RequestMethod,
StateSnapshot,
@@ -170,10 +175,10 @@ pub async fn get_asset(
}
}
-fn display_parts_to_string(parts: Vec<SymbolDisplayPart>) -> String {
+fn display_parts_to_string(parts: &[SymbolDisplayPart]) -> String {
parts
- .into_iter()
- .map(|p| p.text)
+ .iter()
+ .map(|p| p.text.to_string())
.collect::<Vec<String>>()
.join("")
}
@@ -276,7 +281,12 @@ fn replace_links(text: &str) -> String {
.to_string()
}
-#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+fn parse_kind_modifier(kind_modifiers: &str) -> HashSet<&str> {
+ let re = Regex::new(r",|\s+").unwrap();
+ re.split(kind_modifiers).collect()
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub enum ScriptElementKind {
#[serde(rename = "")]
Unknown,
@@ -348,42 +358,58 @@ pub enum ScriptElementKind {
String,
}
+impl Default for ScriptElementKind {
+ fn default() -> Self {
+ Self::Unknown
+ }
+}
+
impl From<ScriptElementKind> for lsp::CompletionItemKind {
fn from(kind: ScriptElementKind) -> Self {
- use lspower::lsp::CompletionItemKind;
-
match kind {
ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => {
- CompletionItemKind::Keyword
+ lsp::CompletionItemKind::Keyword
}
- ScriptElementKind::ConstElement => CompletionItemKind::Constant,
- ScriptElementKind::LetElement
+ ScriptElementKind::ConstElement
+ | ScriptElementKind::LetElement
| ScriptElementKind::VariableElement
| ScriptElementKind::LocalVariableElement
- | ScriptElementKind::Alias => CompletionItemKind::Variable,
+ | ScriptElementKind::Alias
+ | ScriptElementKind::ParameterElement => {
+ lsp::CompletionItemKind::Variable
+ }
ScriptElementKind::MemberVariableElement
| ScriptElementKind::MemberGetAccessorElement
| ScriptElementKind::MemberSetAccessorElement => {
- CompletionItemKind::Field
+ lsp::CompletionItemKind::Field
+ }
+ ScriptElementKind::FunctionElement
+ | ScriptElementKind::LocalFunctionElement => {
+ lsp::CompletionItemKind::Function
}
- ScriptElementKind::FunctionElement => CompletionItemKind::Function,
ScriptElementKind::MemberFunctionElement
| ScriptElementKind::ConstructSignatureElement
| ScriptElementKind::CallSignatureElement
- | ScriptElementKind::IndexSignatureElement => CompletionItemKind::Method,
- ScriptElementKind::EnumElement => CompletionItemKind::Enum,
+ | ScriptElementKind::IndexSignatureElement => {
+ lsp::CompletionItemKind::Method
+ }
+ ScriptElementKind::EnumElement => lsp::CompletionItemKind::Enum,
+ ScriptElementKind::EnumMemberElement => {
+ lsp::CompletionItemKind::EnumMember
+ }
ScriptElementKind::ModuleElement
- | ScriptElementKind::ExternalModuleName => CompletionItemKind::Module,
- ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => {
- CompletionItemKind::Class
+ | ScriptElementKind::ExternalModuleName => {
+ lsp::CompletionItemKind::Module
}
- ScriptElementKind::InterfaceElement => CompletionItemKind::Interface,
- ScriptElementKind::Warning | ScriptElementKind::ScriptElement => {
- CompletionItemKind::File
+ ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => {
+ lsp::CompletionItemKind::Class
}
- ScriptElementKind::Directory => CompletionItemKind::Folder,
- ScriptElementKind::String => CompletionItemKind::Constant,
- _ => CompletionItemKind::Property,
+ ScriptElementKind::InterfaceElement => lsp::CompletionItemKind::Interface,
+ ScriptElementKind::Warning => lsp::CompletionItemKind::Text,
+ ScriptElementKind::ScriptElement => lsp::CompletionItemKind::File,
+ ScriptElementKind::Directory => lsp::CompletionItemKind::Folder,
+ ScriptElementKind::String => lsp::CompletionItemKind::Constant,
+ _ => lsp::CompletionItemKind::Property,
}
}
}
@@ -432,16 +458,20 @@ pub struct QuickInfo {
impl QuickInfo {
pub fn to_hover(&self, line_index: &LineIndex) -> lsp::Hover {
let mut contents = Vec::<lsp::MarkedString>::new();
- if let Some(display_string) =
- self.display_parts.clone().map(display_parts_to_string)
+ if let Some(display_string) = self
+ .display_parts
+ .clone()
+ .map(|p| display_parts_to_string(&p))
{
contents.push(lsp::MarkedString::from_language_code(
"typescript".to_string(),
display_string,
));
}
- if let Some(documentation) =
- self.documentation.clone().map(display_parts_to_string)
+ if let Some(documentation) = self
+ .documentation
+ .clone()
+ .map(|p| display_parts_to_string(&p))
{
contents.push(lsp::MarkedString::from_markdown(documentation));
}
@@ -824,6 +854,15 @@ impl FileTextChanges {
}
}
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CodeAction {
+ description: String,
+ changes: Vec<FileTextChanges>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ commands: Option<Vec<Value>>,
+}
+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodeFixAction {
@@ -882,99 +921,308 @@ impl ReferenceEntry {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
+pub struct CompletionEntryDetails {
+ name: String,
+ kind: ScriptElementKind,
+ kind_modifiers: String,
+ display_parts: Vec<SymbolDisplayPart>,
+ documentation: Option<Vec<SymbolDisplayPart>>,
+ tags: Option<Vec<JSDocTagInfo>>,
+ code_actions: Option<Vec<CodeAction>>,
+ source: Option<Vec<SymbolDisplayPart>>,
+}
+
+impl CompletionEntryDetails {
+ pub fn as_completion_item(
+ &self,
+ original_item: &lsp::CompletionItem,
+ ) -> lsp::CompletionItem {
+ let detail = if original_item.detail.is_some() {
+ original_item.detail.clone()
+ } else if !self.display_parts.is_empty() {
+ Some(replace_links(&display_parts_to_string(&self.display_parts)))
+ } else {
+ None
+ };
+ let documentation = if let Some(parts) = &self.documentation {
+ let mut value = display_parts_to_string(parts);
+ if let Some(tags) = &self.tags {
+ let tag_documentation = tags
+ .iter()
+ .map(get_tag_documentation)
+ .collect::<Vec<String>>()
+ .join("");
+ value = format!("{}\n\n{}", value, tag_documentation);
+ }
+ Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value,
+ }))
+ } else {
+ None
+ };
+ // TODO(@kitsonk) add `self.code_actions`
+ // TODO(@kitsonk) add `use_code_snippet`
+
+ lsp::CompletionItem {
+ data: None,
+ detail,
+ documentation,
+ ..original_item.clone()
+ }
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
pub struct CompletionInfo {
entries: Vec<CompletionEntry>,
+ is_global_completion: bool,
is_member_completion: bool,
+ is_new_identifier_location: bool,
+ metadata: Option<Value>,
+ optional_replacement_span: Option<TextSpan>,
}
impl CompletionInfo {
- pub fn into_completion_response(
- self,
+ pub fn as_completion_response(
+ &self,
line_index: &LineIndex,
+ settings: &config::CompletionSettings,
+ specifier: &ModuleSpecifier,
+ position: u32,
) -> lsp::CompletionResponse {
let items = self
.entries
- .into_iter()
- .map(|entry| entry.into_completion_item(line_index))
+ .iter()
+ .map(|entry| {
+ entry
+ .as_completion_item(line_index, self, settings, specifier, position)
+ })
.collect();
- lsp::CompletionResponse::Array(items)
+ let is_incomplete = self
+ .metadata
+ .clone()
+ .map(|v| {
+ v.as_object()
+ .unwrap()
+ .get("isIncomplete")
+ .unwrap_or(&json!(false))
+ .as_bool()
+ .unwrap()
+ })
+ .unwrap_or(false);
+ lsp::CompletionResponse::List(lsp::CompletionList {
+ is_incomplete,
+ items,
+ })
}
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CompletionItemData {
+ pub specifier: ModuleSpecifier,
+ pub position: u32,
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub source: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub data: Option<Value>,
+ pub use_code_snippet: bool,
+}
+
+#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionEntry {
+ name: String,
kind: ScriptElementKind,
+ #[serde(skip_serializing_if = "Option::is_none")]
kind_modifiers: Option<String>,
- name: String,
sort_text: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
insert_text: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
replacement_span: Option<TextSpan>,
+ #[serde(skip_serializing_if = "Option::is_none")]
has_action: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
source: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
is_recommended: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ is_from_unchecked_file: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ data: Option<Value>,
}
impl CompletionEntry {
- pub fn into_completion_item(
- self,
- line_index: &LineIndex,
- ) -> lsp::CompletionItem {
- let mut item = lsp::CompletionItem {
- label: self.name,
- kind: Some(self.kind.into()),
- sort_text: Some(self.sort_text.clone()),
- // TODO(lucacasonato): missing commit_characters
- ..Default::default()
- };
+ fn get_commit_characters(
+ &self,
+ info: &CompletionInfo,
+ settings: &config::CompletionSettings,
+ ) -> Option<Vec<String>> {
+ if info.is_new_identifier_location {
+ return None;
+ }
- 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)
+ let mut commit_characters = vec![];
+ match self.kind {
+ ScriptElementKind::MemberGetAccessorElement
+ | ScriptElementKind::MemberSetAccessorElement
+ | ScriptElementKind::ConstructSignatureElement
+ | ScriptElementKind::CallSignatureElement
+ | ScriptElementKind::IndexSignatureElement
+ | ScriptElementKind::EnumElement
+ | ScriptElementKind::InterfaceElement => {
+ commit_characters.push(".");
+ commit_characters.push(";");
+ }
+ ScriptElementKind::ModuleElement
+ | ScriptElementKind::Alias
+ | ScriptElementKind::ConstElement
+ | ScriptElementKind::LetElement
+ | ScriptElementKind::VariableElement
+ | ScriptElementKind::LocalVariableElement
+ | ScriptElementKind::MemberVariableElement
+ | ScriptElementKind::ClassElement
+ | ScriptElementKind::FunctionElement
+ | ScriptElementKind::MemberFunctionElement
+ | ScriptElementKind::Keyword
+ | ScriptElementKind::ParameterElement => {
+ commit_characters.push(".");
+ commit_characters.push(",");
+ commit_characters.push(";");
+ if !settings.complete_function_calls {
+ commit_characters.push("(");
+ }
+ }
+ _ => (),
}
- match item.kind {
- Some(lsp::CompletionItemKind::Function)
- | Some(lsp::CompletionItemKind::Method) => {
- item.insert_text_format = Some(lsp::InsertTextFormat::Snippet);
+ if commit_characters.is_empty() {
+ None
+ } else {
+ Some(commit_characters.into_iter().map(String::from).collect())
+ }
+ }
+
+ fn get_filter_text(&self) -> Option<String> {
+ // TODO(@kitsonk) this is actually quite a bit more complex.
+ // See `MyCompletionItem.getFilterText` in vscode completion.ts.
+ if self.name.starts_with('#') && self.insert_text.is_none() {
+ return Some(self.name.clone());
+ }
+
+ if let Some(insert_text) = &self.insert_text {
+ if insert_text.starts_with("this.") {
+ return None;
+ }
+ if insert_text.starts_with('[') {
+ let re = Regex::new(r#"^\[['"](.+)['"]\]$"#).unwrap();
+ let insert_text = re.replace(insert_text, ".$1").to_string();
+ return Some(insert_text);
}
- _ => {}
}
- let mut insert_text = self.insert_text;
- let replacement_range: Option<lsp::Range> =
- self.replacement_span.map(|span| span.to_range(line_index));
+ self.insert_text.clone()
+ }
- // TODO(lucacasonato): port other special cases from https://github.com/theia-ide/typescript-language-server/blob/fdf28313833cd6216d00eb4e04dc7f00f4c04f09/server/src/completion.ts#L49-L55
+ pub fn as_completion_item(
+ &self,
+ line_index: &LineIndex,
+ info: &CompletionInfo,
+ settings: &config::CompletionSettings,
+ specifier: &ModuleSpecifier,
+ position: u32,
+ ) -> lsp::CompletionItem {
+ let mut label = self.name.clone();
+ let mut kind: Option<lsp::CompletionItemKind> =
+ Some(self.kind.clone().into());
- if let Some(kind_modifiers) = self.kind_modifiers {
- if kind_modifiers.contains("\\optional\\") {
+ let sort_text = if self.source.is_some() {
+ Some(format!("\u{ffff}{}", self.sort_text))
+ } else {
+ Some(self.sort_text.clone())
+ };
+
+ let preselect = self.is_recommended;
+ 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 mut insert_text = self.insert_text.clone();
+ let range = self.replacement_span.clone();
+ let mut filter_text = self.get_filter_text();
+ let mut tags = None;
+ let mut detail = None;
+
+ if let Some(kind_modifiers) = &self.kind_modifiers {
+ let kind_modifiers = parse_kind_modifier(kind_modifiers);
+ if kind_modifiers.contains("optional") {
if insert_text.is_none() {
- insert_text = Some(item.label.clone());
+ insert_text = Some(label.clone());
}
- if item.filter_text.is_none() {
- item.filter_text = Some(item.label.clone());
+ if filter_text.is_none() {
+ filter_text = Some(label.clone());
+ }
+ label += "?";
+ }
+ if kind_modifiers.contains("deprecated") {
+ tags = Some(vec![lsp::CompletionItemTag::Deprecated]);
+ }
+ if kind_modifiers.contains("color") {
+ kind = Some(lsp::CompletionItemKind::Color);
+ }
+ if self.kind == ScriptElementKind::ScriptElement {
+ for ext_modifier in FILE_EXTENSION_KIND_MODIFIERS {
+ if kind_modifiers.contains(ext_modifier) {
+ detail = if self.name.to_lowercase().ends_with(ext_modifier) {
+ Some(self.name.clone())
+ } else {
+ Some(format!("{}{}", self.name, ext_modifier))
+ };
+ break;
+ }
}
- item.label += "?";
}
}
- if let Some(insert_text) = insert_text {
- if let Some(replacement_range) = replacement_range {
- item.text_edit = Some(lsp::CompletionTextEdit::Edit(
- lsp::TextEdit::new(replacement_range, insert_text),
- ));
+ let text_edit =
+ if let (Some(text_span), Some(new_text)) = (range, insert_text) {
+ let range = text_span.to_range(line_index);
+ let insert_replace_edit = lsp::InsertReplaceEdit {
+ new_text,
+ insert: range,
+ replace: range,
+ };
+ Some(insert_replace_edit.into())
} else {
- item.insert_text = Some(insert_text);
- }
- }
+ None
+ };
+
+ let data = CompletionItemData {
+ specifier: specifier.clone(),
+ position,
+ name: self.name.clone(),
+ source: self.source.clone(),
+ data: self.data.clone(),
+ use_code_snippet,
+ };
- item
+ lsp::CompletionItem {
+ label,
+ kind,
+ sort_text,
+ preselect,
+ text_edit,
+ filter_text,
+ detail,
+ tags,
+ data: Some(serde_json::to_value(data).unwrap()),
+ ..Default::default()
+ }
}
}
@@ -1016,18 +1264,18 @@ pub struct SignatureHelpItem {
impl SignatureHelpItem {
pub fn into_signature_information(self) -> lsp::SignatureInformation {
- let prefix_text = display_parts_to_string(self.prefix_display_parts);
+ let prefix_text = display_parts_to_string(&self.prefix_display_parts);
let params_text = self
.parameters
.iter()
- .map(|param| display_parts_to_string(param.display_parts.clone()))
+ .map(|param| display_parts_to_string(&param.display_parts))
.collect::<Vec<String>>()
.join(", ");
- let suffix_text = display_parts_to_string(self.suffix_display_parts);
+ let suffix_text = display_parts_to_string(&self.suffix_display_parts);
lsp::SignatureInformation {
label: format!("{}{}{}", prefix_text, params_text, suffix_text),
documentation: Some(lsp::Documentation::String(display_parts_to_string(
- self.documentation,
+ &self.documentation,
))),
parameters: Some(
self
@@ -1054,10 +1302,10 @@ impl SignatureHelpParameter {
pub fn into_parameter_information(self) -> lsp::ParameterInformation {
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple(display_parts_to_string(
- self.display_parts,
+ &self.display_parts,
)),
documentation: Some(lsp::Documentation::String(display_parts_to_string(
- self.documentation,
+ &self.documentation,
))),
}
}
@@ -1481,6 +1729,15 @@ pub enum IncludePackageJsonAutoImports {
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsAtPositionOptions {
+ #[serde(flatten)]
+ pub user_preferences: UserPreferences,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub trigger_character: Option<String>,
+}
+
+#[derive(Debug, Default, Serialize)]
+#[serde(rename_all = "camelCase")]
pub struct UserPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_suggestions: Option<bool>,
@@ -1542,6 +1799,30 @@ pub struct SignatureHelpTriggerReason {
pub trigger_character: Option<String>,
}
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionDetailsArgs {
+ pub specifier: ModuleSpecifier,
+ pub position: u32,
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub source: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub data: Option<Value>,
+}
+
+impl From<CompletionItemData> for GetCompletionDetailsArgs {
+ fn from(item_data: CompletionItemData) -> Self {
+ Self {
+ specifier: item_data.specifier,
+ position: item_data.position,
+ name: item_data.name,
+ source: item_data.source,
+ data: item_data.data,
+ }
+ }
+}
+
/// Methods that are supported by the Language Service in the compiler isolate.
#[derive(Debug)]
pub enum RequestMethod {
@@ -1554,7 +1835,9 @@ pub enum RequestMethod {
/// Retrieve code fixes for a range of a file with the provided error codes.
GetCodeFixes((ModuleSpecifier, u32, u32, Vec<String>)),
/// Get completion information at a given position (IntelliSense).
- GetCompletions((ModuleSpecifier, u32, UserPreferences)),
+ GetCompletions((ModuleSpecifier, u32, GetCompletionsAtPositionOptions)),
+ /// Get details about a specific completion entry.
+ GetCompletionDetails(GetCompletionDetailsArgs),
/// Retrieve the combined code fixes for a fix id for a module.
GetCombinedCodeFix((ModuleSpecifier, Value)),
/// Get declaration information for a specific position.
@@ -1626,6 +1909,11 @@ impl RequestMethod {
"specifier": specifier,
"fixId": fix_id,
}),
+ RequestMethod::GetCompletionDetails(args) => json!({
+ "id": id,
+ "method": "getCompletionDetails",
+ "args": args
+ }),
RequestMethod::GetCompletions((specifier, position, preferences)) => {
json!({
"id": id,
@@ -1738,6 +2026,7 @@ mod tests {
use crate::lsp::analysis;
use crate::lsp::documents::DocumentCache;
use crate::lsp::sources::Sources;
+ use crate::lsp::text::LineIndex;
use std::path::Path;
use std::path::PathBuf;
use tempfile::TempDir;
@@ -2228,4 +2517,170 @@ mod tests {
})
);
}
+
+ #[test]
+ fn test_completion_entry_filter_text() {
+ let fixture = CompletionEntry {
+ kind: ScriptElementKind::MemberVariableElement,
+ name: "['foo']".to_string(),
+ insert_text: Some("['foo']".to_string()),
+ ..Default::default()
+ };
+ let actual = fixture.get_filter_text();
+ assert_eq!(actual, Some(".foo".to_string()));
+ }
+
+ #[test]
+ fn test_completions() {
+ let fixture = r#"
+ import { B } from "https://deno.land/x/b/mod.ts";
+
+ const b = new B();
+
+ console.
+ "#;
+ let line_index = LineIndex::new(fixture);
+ let position = line_index
+ .offset_tsc(lsp::Position {
+ line: 5,
+ character: 16,
+ })
+ .unwrap();
+ let (mut runtime, state_snapshot, _) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ &[("file:///a.ts", fixture, 1)],
+ );
+ let specifier = resolve_url("file:///a.ts").expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ state_snapshot.clone(),
+ RequestMethod::GetDiagnostics(vec![specifier.clone()]),
+ );
+ assert!(result.is_ok());
+ let result = request(
+ &mut runtime,
+ state_snapshot.clone(),
+ RequestMethod::GetCompletions((
+ specifier.clone(),
+ position,
+ GetCompletionsAtPositionOptions {
+ user_preferences: UserPreferences {
+ include_completions_with_insert_text: Some(true),
+ ..Default::default()
+ },
+ trigger_character: Some(".".to_string()),
+ },
+ )),
+ );
+ assert!(result.is_ok());
+ let response: CompletionInfo =
+ serde_json::from_value(result.unwrap()).unwrap();
+ assert_eq!(response.entries.len(), 20);
+ let result = request(
+ &mut runtime,
+ state_snapshot,
+ RequestMethod::GetCompletionDetails(GetCompletionDetailsArgs {
+ specifier,
+ position,
+ name: "log".to_string(),
+ source: None,
+ data: None,
+ }),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(
+ response,
+ json!({
+ "name": "log",
+ "kindModifiers": "declare",
+ "kind": "method",
+ "displayParts": [
+ {
+ "text": "(",
+ "kind": "punctuation"
+ },
+ {
+ "text": "method",
+ "kind": "text"
+ },
+ {
+ "text": ")",
+ "kind": "punctuation"
+ },
+ {
+ "text": " ",
+ "kind": "space"
+ },
+ {
+ "text": "Console",
+ "kind": "interfaceName"
+ },
+ {
+ "text": ".",
+ "kind": "punctuation"
+ },
+ {
+ "text": "log",
+ "kind": "methodName"
+ },
+ {
+ "text": "(",
+ "kind": "punctuation"
+ },
+ {
+ "text": "...",
+ "kind": "punctuation"
+ },
+ {
+ "text": "data",
+ "kind": "parameterName"
+ },
+ {
+ "text": ":",
+ "kind": "punctuation"
+ },
+ {
+ "text": " ",
+ "kind": "space"
+ },
+ {
+ "text": "any",
+ "kind": "keyword"
+ },
+ {
+ "text": "[",
+ "kind": "punctuation"
+ },
+ {
+ "text": "]",
+ "kind": "punctuation"
+ },
+ {
+ "text": ")",
+ "kind": "punctuation"
+ },
+ {
+ "text": ":",
+ "kind": "punctuation"
+ },
+ {
+ "text": " ",
+ "kind": "space"
+ },
+ {
+ "text": "void",
+ "kind": "keyword"
+ }
+ ],
+ "documentation": []
+ })
+ );
+ }
}
diff --git a/cli/tests/lsp/completion_request.json b/cli/tests/lsp/completion_request.json
new file mode 100644
index 000000000..81bf719a9
--- /dev/null
+++ b/cli/tests/lsp/completion_request.json
@@ -0,0 +1,18 @@
+{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "method": "textDocument/completion",
+ "params": {
+ "textDocument": {
+ "uri": "file:///a/file.ts"
+ },
+ "position": {
+ "line": 0,
+ "character": 5
+ },
+ "context": {
+ "triggerKind": 2,
+ "triggerCharacter": "."
+ }
+ }
+}
diff --git a/cli/tests/lsp/completion_resolve_request.json b/cli/tests/lsp/completion_resolve_request.json
new file mode 100644
index 000000000..c176a431e
--- /dev/null
+++ b/cli/tests/lsp/completion_resolve_request.json
@@ -0,0 +1,17 @@
+{
+ "jsonrpc": "2.0",
+ "id": 4,
+ "method": "completionItem/resolve",
+ "params": {
+ "label": "build",
+ "kind": 6,
+ "sortText": "1",
+ "insertTextFormat": 1,
+ "data": {
+ "specifier": "file:///a/file.ts",
+ "position": 5,
+ "name": "build",
+ "useCodeSnippet": false
+ }
+ }
+}
diff --git a/cli/tests/lsp/did_open_notification_completions.json b/cli/tests/lsp/did_open_notification_completions.json
new file mode 100644
index 000000000..edcdc9373
--- /dev/null
+++ b/cli/tests/lsp/did_open_notification_completions.json
@@ -0,0 +1,12 @@
+{
+ "jsonrpc": "2.0",
+ "method": "textDocument/didOpen",
+ "params": {
+ "textDocument": {
+ "uri": "file:///a/file.ts",
+ "languageId": "typescript",
+ "version": 1,
+ "text": "Deno."
+ }
+ }
+}
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js
index f8eabc890..c84c2365c 100644
--- a/cli/tsc/99_main_compiler.js
+++ b/cli/tsc/99_main_compiler.js
@@ -594,6 +594,22 @@ delete Object.prototype.__proto__;
),
);
}
+ case "getCompletionDetails": {
+ debug("request", request);
+ return respond(
+ id,
+ languageService.getCompletionEntryDetails(
+ 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.data,
+ ),
+ );
+ }
case "getCompletions": {
return respond(
id,
diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts
index d37b56c06..a3200469c 100644
--- a/cli/tsc/compiler.d.ts
+++ b/cli/tsc/compiler.d.ts
@@ -51,6 +51,7 @@ declare global {
| GetAsset
| GetCodeFixes
| GetCombinedCodeFix
+ | GetCompletionDetails
| GetCompletionsRequest
| GetDefinitionRequest
| GetDiagnosticsRequest
@@ -102,11 +103,22 @@ declare global {
fixId: {};
}
+ interface GetCompletionDetails extends BaseLanguageServerRequest {
+ method: "getCompletionDetails";
+ args: {
+ specifier: string;
+ position: number;
+ name: string;
+ source?: string;
+ data?: unknown;
+ };
+ }
+
interface GetCompletionsRequest extends BaseLanguageServerRequest {
method: "getCompletions";
specifier: string;
position: number;
- preferences: ts.UserPreferences;
+ preferences: ts.GetCompletionsAtPositionOptions;
}
interface GetDiagnosticsRequest extends BaseLanguageServerRequest {