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

refactor(lsp): refactor completions and add tests (#9789)

This commit is contained in:
Kitson Kelly 2021-03-16 09:01:41 +11:00 committed by GitHub
parent 2ff9b01551
commit 506b321d47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 800 additions and 148 deletions

View file

@ -55,7 +55,12 @@ pub fn server_capabilities(
)), )),
hover_provider: Some(HoverProviderCapability::Simple(true)), hover_provider: Some(HoverProviderCapability::Simple(true)),
completion_provider: Some(CompletionOptions { 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![ trigger_characters: Some(vec![
".".to_string(), ".".to_string(),
"\"".to_string(), "\"".to_string(),
@ -66,7 +71,7 @@ pub fn server_capabilities(
"<".to_string(), "<".to_string(),
"#".to_string(), "#".to_string(),
]), ]),
resolve_provider: None, resolve_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions { work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None, work_done_progress: None,
}, },
@ -77,7 +82,7 @@ pub fn server_capabilities(
"(".to_string(), "(".to_string(),
"<".to_string(), "<".to_string(),
]), ]),
retrigger_characters: None, retrigger_characters: Some(vec![")".to_string()]),
work_done_progress_options: WorkDoneProgressOptions { work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None, work_done_progress: None,
}, },

View file

@ -15,7 +15,7 @@ pub struct ClientCapabilities {
pub workspace_did_change_watched_files: bool, pub workspace_did_change_watched_files: bool,
} }
#[derive(Debug, Default, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CodeLensSettings { pub struct CodeLensSettings {
/// Flag for providing implementation code lenses. /// Flag for providing implementation code lenses.
@ -30,13 +30,50 @@ pub struct CodeLensSettings {
pub references_all_functions: bool, 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)] #[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct WorkspaceSettings { pub struct WorkspaceSettings {
pub enable: bool, pub enable: bool,
pub config: Option<String>, pub config: Option<String>,
pub import_map: 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)] #[serde(default)]
pub lint: bool, pub lint: bool,
@ -48,36 +85,7 @@ impl WorkspaceSettings {
/// Determine if any code lenses are enabled at all. This allows short /// Determine if any code lenses are enabled at all. This allows short
/// circuiting when there are no code lenses enabled. /// circuiting when there are no code lenses enabled.
pub fn enabled_code_lens(&self) -> bool { pub fn enabled_code_lens(&self) -> bool {
if let Some(code_lens) = &self.code_lens { self.code_lens.implementations || self.code_lens.references
// 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
}
} }
} }

View file

@ -917,7 +917,7 @@ impl Inner {
let mut code_lenses = cl.borrow_mut(); let mut code_lenses = cl.borrow_mut();
// TSC Implementations Code Lens // TSC Implementations Code Lens
if self.config.settings.enabled_code_lens_implementations() { if self.config.settings.code_lens.implementations {
let source = CodeLensSource::Implementations; let source = CodeLensSource::Implementations;
match i.kind { match i.kind {
tsc::ScriptElementKind::InterfaceElement => { tsc::ScriptElementKind::InterfaceElement => {
@ -941,7 +941,7 @@ impl Inner {
} }
// TSC References Code Lens // TSC References Code Lens
if self.config.settings.enabled_code_lens_references() { if self.config.settings.code_lens.references {
let source = CodeLensSource::References; let source = CodeLensSource::References;
if let Some(parent) = &mp { if let Some(parent) = &mp {
if parent.kind == tsc::ScriptElementKind::EnumElement { if parent.kind == tsc::ScriptElementKind::EnumElement {
@ -950,11 +950,7 @@ impl Inner {
} }
match i.kind { match i.kind {
tsc::ScriptElementKind::FunctionElement => { tsc::ScriptElementKind::FunctionElement => {
if self if self.config.settings.code_lens.references_all_functions {
.config
.settings
.enabled_code_lens_references_all_functions()
{
code_lenses.push(i.to_code_lens( code_lenses.push(i.to_code_lens(
&line_index, &line_index,
&specifier, &specifier,
@ -1358,7 +1354,6 @@ impl Inner {
let specifier = self let specifier = self
.url_map .url_map
.normalize_url(&params.text_document_position.text_document.uri); .normalize_url(&params.text_document_position.text_document.uri);
// TODO(lucacasonato): handle error correctly
let line_index = let line_index =
if let Some(line_index) = self.get_line_index_sync(&specifier) { if let Some(line_index) = self.get_line_index_sync(&specifier) {
line_index line_index
@ -1368,13 +1363,22 @@ impl Inner {
specifier 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(( let req = tsc::RequestMethod::GetCompletions((
specifier, specifier.clone(),
line_index.offset_tsc(params.text_document_position.position)?, position,
tsc::UserPreferences { tsc::GetCompletionsAtPositionOptions {
// TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651 user_preferences: tsc::UserPreferences {
include_completions_with_insert_text: Some(false), include_completions_with_insert_text: Some(true),
..Default::default() ..Default::default()
},
trigger_character,
}, },
)); ));
let maybe_completion_info: Option<tsc::CompletionInfo> = self let maybe_completion_info: Option<tsc::CompletionInfo> = self
@ -1387,7 +1391,12 @@ impl Inner {
})?; })?;
if let Some(completions) = maybe_completion_info { 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); self.performance.measure(mark);
Ok(Some(results)) Ok(Some(results))
} else { } 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( async fn goto_implementation(
&mut self, &mut self,
params: GotoImplementationParams, params: GotoImplementationParams,
@ -1715,6 +1765,13 @@ impl lspower::LanguageServer for LanguageServer {
self.0.lock().await.completion(params).await 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( async fn goto_implementation(
&self, &self,
params: GotoImplementationParams, params: GotoImplementationParams,
@ -2740,6 +2797,58 @@ mod tests {
harness.run().await; harness.run().await;
} }
#[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)] #[derive(Deserialize)]
struct PerformanceAverages { struct PerformanceAverages {
averages: Vec<PerformanceAverage>, averages: Vec<PerformanceAverage>,

View file

@ -3,6 +3,7 @@
use super::analysis::CodeLensSource; use super::analysis::CodeLensSource;
use super::analysis::ResolvedDependency; use super::analysis::ResolvedDependency;
use super::analysis::ResolvedDependencyErr; use super::analysis::ResolvedDependencyErr;
use super::config;
use super::language_server; use super::language_server;
use super::language_server::StateSnapshot; use super::language_server::StateSnapshot;
use super::text; use super::text;
@ -35,11 +36,15 @@ use regex::Captures;
use regex::Regex; use regex::Regex;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet;
use std::thread; use std::thread;
use text_size::TextSize; use text_size::TextSize;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::oneshot; use tokio::sync::oneshot;
const FILE_EXTENSION_KIND_MODIFIERS: &[&str] =
&[".d.ts", ".ts", ".tsx", ".js", ".jsx", ".json"];
type Request = ( type Request = (
RequestMethod, RequestMethod,
StateSnapshot, 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 parts
.into_iter() .iter()
.map(|p| p.text) .map(|p| p.text.to_string())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("") .join("")
} }
@ -276,7 +281,12 @@ fn replace_links(text: &str) -> String {
.to_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 { pub enum ScriptElementKind {
#[serde(rename = "")] #[serde(rename = "")]
Unknown, Unknown,
@ -348,42 +358,58 @@ pub enum ScriptElementKind {
String, String,
} }
impl Default for ScriptElementKind {
fn default() -> Self {
Self::Unknown
}
}
impl From<ScriptElementKind> for lsp::CompletionItemKind { impl From<ScriptElementKind> for lsp::CompletionItemKind {
fn from(kind: ScriptElementKind) -> Self { fn from(kind: ScriptElementKind) -> Self {
use lspower::lsp::CompletionItemKind;
match kind { match kind {
ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => { ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => {
CompletionItemKind::Keyword lsp::CompletionItemKind::Keyword
} }
ScriptElementKind::ConstElement => CompletionItemKind::Constant, ScriptElementKind::ConstElement
ScriptElementKind::LetElement | ScriptElementKind::LetElement
| ScriptElementKind::VariableElement | ScriptElementKind::VariableElement
| ScriptElementKind::LocalVariableElement | ScriptElementKind::LocalVariableElement
| ScriptElementKind::Alias => CompletionItemKind::Variable, | ScriptElementKind::Alias
| ScriptElementKind::ParameterElement => {
lsp::CompletionItemKind::Variable
}
ScriptElementKind::MemberVariableElement ScriptElementKind::MemberVariableElement
| ScriptElementKind::MemberGetAccessorElement | ScriptElementKind::MemberGetAccessorElement
| ScriptElementKind::MemberSetAccessorElement => { | ScriptElementKind::MemberSetAccessorElement => {
CompletionItemKind::Field lsp::CompletionItemKind::Field
}
ScriptElementKind::FunctionElement
| ScriptElementKind::LocalFunctionElement => {
lsp::CompletionItemKind::Function
} }
ScriptElementKind::FunctionElement => CompletionItemKind::Function,
ScriptElementKind::MemberFunctionElement ScriptElementKind::MemberFunctionElement
| ScriptElementKind::ConstructSignatureElement | ScriptElementKind::ConstructSignatureElement
| ScriptElementKind::CallSignatureElement | ScriptElementKind::CallSignatureElement
| ScriptElementKind::IndexSignatureElement => CompletionItemKind::Method, | ScriptElementKind::IndexSignatureElement => {
ScriptElementKind::EnumElement => CompletionItemKind::Enum, lsp::CompletionItemKind::Method
}
ScriptElementKind::EnumElement => lsp::CompletionItemKind::Enum,
ScriptElementKind::EnumMemberElement => {
lsp::CompletionItemKind::EnumMember
}
ScriptElementKind::ModuleElement ScriptElementKind::ModuleElement
| ScriptElementKind::ExternalModuleName => CompletionItemKind::Module, | ScriptElementKind::ExternalModuleName => {
lsp::CompletionItemKind::Module
}
ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => { ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => {
CompletionItemKind::Class lsp::CompletionItemKind::Class
} }
ScriptElementKind::InterfaceElement => CompletionItemKind::Interface, ScriptElementKind::InterfaceElement => lsp::CompletionItemKind::Interface,
ScriptElementKind::Warning | ScriptElementKind::ScriptElement => { ScriptElementKind::Warning => lsp::CompletionItemKind::Text,
CompletionItemKind::File ScriptElementKind::ScriptElement => lsp::CompletionItemKind::File,
} ScriptElementKind::Directory => lsp::CompletionItemKind::Folder,
ScriptElementKind::Directory => CompletionItemKind::Folder, ScriptElementKind::String => lsp::CompletionItemKind::Constant,
ScriptElementKind::String => CompletionItemKind::Constant, _ => lsp::CompletionItemKind::Property,
_ => CompletionItemKind::Property,
} }
} }
} }
@ -432,16 +458,20 @@ pub struct QuickInfo {
impl QuickInfo { impl QuickInfo {
pub fn to_hover(&self, line_index: &LineIndex) -> lsp::Hover { pub fn to_hover(&self, line_index: &LineIndex) -> lsp::Hover {
let mut contents = Vec::<lsp::MarkedString>::new(); let mut contents = Vec::<lsp::MarkedString>::new();
if let Some(display_string) = if let Some(display_string) = self
self.display_parts.clone().map(display_parts_to_string) .display_parts
.clone()
.map(|p| display_parts_to_string(&p))
{ {
contents.push(lsp::MarkedString::from_language_code( contents.push(lsp::MarkedString::from_language_code(
"typescript".to_string(), "typescript".to_string(),
display_string, display_string,
)); ));
} }
if let Some(documentation) = if let Some(documentation) = self
self.documentation.clone().map(display_parts_to_string) .documentation
.clone()
.map(|p| display_parts_to_string(&p))
{ {
contents.push(lsp::MarkedString::from_markdown(documentation)); 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)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CodeFixAction { pub struct CodeFixAction {
@ -882,99 +921,308 @@ impl ReferenceEntry {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CompletionInfo { pub struct CompletionEntryDetails {
entries: Vec<CompletionEntry>, name: String,
is_member_completion: bool, 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 CompletionInfo { impl CompletionEntryDetails {
pub fn into_completion_response( pub fn as_completion_item(
self, &self,
line_index: &LineIndex, original_item: &lsp::CompletionItem,
) -> lsp::CompletionResponse { ) -> lsp::CompletionItem {
let items = self let detail = if original_item.detail.is_some() {
.entries original_item.detail.clone()
.into_iter() } else if !self.display_parts.is_empty() {
.map(|entry| entry.into_completion_item(line_index)) Some(replace_links(&display_parts_to_string(&self.display_parts)))
.collect(); } else {
lsp::CompletionResponse::Array(items) 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)] #[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 as_completion_response(
&self,
line_index: &LineIndex,
settings: &config::CompletionSettings,
specifier: &ModuleSpecifier,
position: u32,
) -> lsp::CompletionResponse {
let items = self
.entries
.iter()
.map(|entry| {
entry
.as_completion_item(line_index, self, settings, specifier, position)
})
.collect();
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, 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")] #[serde(rename_all = "camelCase")]
pub struct CompletionEntry { pub struct CompletionEntry {
kind: ScriptElementKind,
kind_modifiers: Option<String>,
name: String, name: String,
kind: ScriptElementKind,
#[serde(skip_serializing_if = "Option::is_none")]
kind_modifiers: Option<String>,
sort_text: String, sort_text: String,
#[serde(skip_serializing_if = "Option::is_none")]
insert_text: Option<String>, insert_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
replacement_span: Option<TextSpan>, replacement_span: Option<TextSpan>,
#[serde(skip_serializing_if = "Option::is_none")]
has_action: Option<bool>, has_action: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<String>, source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
is_recommended: Option<bool>, 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 { impl CompletionEntry {
pub fn into_completion_item( fn get_commit_characters(
self, &self,
info: &CompletionInfo,
settings: &config::CompletionSettings,
) -> Option<Vec<String>> {
if info.is_new_identifier_location {
return None;
}
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("(");
}
}
_ => (),
}
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);
}
}
self.insert_text.clone()
}
pub fn as_completion_item(
&self,
line_index: &LineIndex, line_index: &LineIndex,
info: &CompletionInfo,
settings: &config::CompletionSettings,
specifier: &ModuleSpecifier,
position: u32,
) -> lsp::CompletionItem { ) -> lsp::CompletionItem {
let mut item = lsp::CompletionItem { let mut label = self.name.clone();
label: self.name, let mut kind: Option<lsp::CompletionItemKind> =
kind: Some(self.kind.into()), Some(self.kind.clone().into());
sort_text: Some(self.sort_text.clone()),
// TODO(lucacasonato): missing commit_characters let sort_text = if self.source.is_some() {
..Default::default() Some(format!("\u{ffff}{}", self.sort_text))
} else {
Some(self.sort_text.clone())
}; };
if let Some(true) = self.is_recommended { let preselect = self.is_recommended;
// Make sure isRecommended property always comes first let use_code_snippet = settings.complete_function_calls
// https://github.com/Microsoft/vscode/issues/40325 && (kind == Some(lsp::CompletionItemKind::Function)
item.preselect = Some(true); || kind == Some(lsp::CompletionItemKind::Method));
} else if self.source.is_some() { // TODO(@kitsonk) missing from types: https://github.com/gluon-lang/lsp-types/issues/204
// De-prioritze auto-imports let _commit_characters = self.get_commit_characters(info, settings);
// https://github.com/Microsoft/vscode/issues/40311 let mut insert_text = self.insert_text.clone();
item.sort_text = Some("\u{ffff}".to_string() + &self.sort_text) let range = self.replacement_span.clone();
} let mut filter_text = self.get_filter_text();
let mut tags = None;
let mut detail = None;
match item.kind { if let Some(kind_modifiers) = &self.kind_modifiers {
Some(lsp::CompletionItemKind::Function) let kind_modifiers = parse_kind_modifier(kind_modifiers);
| Some(lsp::CompletionItemKind::Method) => { if kind_modifiers.contains("optional") {
item.insert_text_format = Some(lsp::InsertTextFormat::Snippet);
}
_ => {}
}
let mut insert_text = self.insert_text;
let replacement_range: Option<lsp::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() { if insert_text.is_none() {
insert_text = Some(item.label.clone()); insert_text = Some(label.clone());
} }
if item.filter_text.is_none() { if filter_text.is_none() {
item.filter_text = Some(item.label.clone()); 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 { let text_edit =
if let Some(replacement_range) = replacement_range { if let (Some(text_span), Some(new_text)) = (range, insert_text) {
item.text_edit = Some(lsp::CompletionTextEdit::Edit( let range = text_span.to_range(line_index);
lsp::TextEdit::new(replacement_range, insert_text), let insert_replace_edit = lsp::InsertReplaceEdit {
)); new_text,
insert: range,
replace: range,
};
Some(insert_replace_edit.into())
} else { } else {
item.insert_text = Some(insert_text); None
} };
}
item let data = CompletionItemData {
specifier: specifier.clone(),
position,
name: self.name.clone(),
source: self.source.clone(),
data: self.data.clone(),
use_code_snippet,
};
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 { impl SignatureHelpItem {
pub fn into_signature_information(self) -> lsp::SignatureInformation { 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 let params_text = self
.parameters .parameters
.iter() .iter()
.map(|param| display_parts_to_string(param.display_parts.clone())) .map(|param| display_parts_to_string(&param.display_parts))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", "); .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 { lsp::SignatureInformation {
label: format!("{}{}{}", prefix_text, params_text, suffix_text), label: format!("{}{}{}", prefix_text, params_text, suffix_text),
documentation: Some(lsp::Documentation::String(display_parts_to_string( documentation: Some(lsp::Documentation::String(display_parts_to_string(
self.documentation, &self.documentation,
))), ))),
parameters: Some( parameters: Some(
self self
@ -1054,10 +1302,10 @@ impl SignatureHelpParameter {
pub fn into_parameter_information(self) -> lsp::ParameterInformation { pub fn into_parameter_information(self) -> lsp::ParameterInformation {
lsp::ParameterInformation { lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple(display_parts_to_string( label: lsp::ParameterLabel::Simple(display_parts_to_string(
self.display_parts, &self.display_parts,
)), )),
documentation: Some(lsp::Documentation::String(display_parts_to_string( documentation: Some(lsp::Documentation::String(display_parts_to_string(
self.documentation, &self.documentation,
))), ))),
} }
} }
@ -1479,6 +1727,15 @@ pub enum IncludePackageJsonAutoImports {
Off, Off,
} }
#[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)] #[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UserPreferences { pub struct UserPreferences {
@ -1542,6 +1799,30 @@ pub struct SignatureHelpTriggerReason {
pub trigger_character: Option<String>, 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. /// Methods that are supported by the Language Service in the compiler isolate.
#[derive(Debug)] #[derive(Debug)]
pub enum RequestMethod { pub enum RequestMethod {
@ -1554,7 +1835,9 @@ pub enum RequestMethod {
/// Retrieve code fixes for a range of a file with the provided error codes. /// Retrieve code fixes for a range of a file with the provided error codes.
GetCodeFixes((ModuleSpecifier, u32, u32, Vec<String>)), GetCodeFixes((ModuleSpecifier, u32, u32, Vec<String>)),
/// Get completion information at a given position (IntelliSense). /// 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. /// Retrieve the combined code fixes for a fix id for a module.
GetCombinedCodeFix((ModuleSpecifier, Value)), GetCombinedCodeFix((ModuleSpecifier, Value)),
/// Get declaration information for a specific position. /// Get declaration information for a specific position.
@ -1626,6 +1909,11 @@ impl RequestMethod {
"specifier": specifier, "specifier": specifier,
"fixId": fix_id, "fixId": fix_id,
}), }),
RequestMethod::GetCompletionDetails(args) => json!({
"id": id,
"method": "getCompletionDetails",
"args": args
}),
RequestMethod::GetCompletions((specifier, position, preferences)) => { RequestMethod::GetCompletions((specifier, position, preferences)) => {
json!({ json!({
"id": id, "id": id,
@ -1738,6 +2026,7 @@ mod tests {
use crate::lsp::analysis; use crate::lsp::analysis;
use crate::lsp::documents::DocumentCache; use crate::lsp::documents::DocumentCache;
use crate::lsp::sources::Sources; use crate::lsp::sources::Sources;
use crate::lsp::text::LineIndex;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use tempfile::TempDir; 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": []
})
);
}
} }

View file

@ -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": "."
}
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,12 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "Deno."
}
}
}

View file

@ -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": { case "getCompletions": {
return respond( return respond(
id, id,

14
cli/tsc/compiler.d.ts vendored
View file

@ -51,6 +51,7 @@ declare global {
| GetAsset | GetAsset
| GetCodeFixes | GetCodeFixes
| GetCombinedCodeFix | GetCombinedCodeFix
| GetCompletionDetails
| GetCompletionsRequest | GetCompletionsRequest
| GetDefinitionRequest | GetDefinitionRequest
| GetDiagnosticsRequest | GetDiagnosticsRequest
@ -102,11 +103,22 @@ declare global {
fixId: {}; fixId: {};
} }
interface GetCompletionDetails extends BaseLanguageServerRequest {
method: "getCompletionDetails";
args: {
specifier: string;
position: number;
name: string;
source?: string;
data?: unknown;
};
}
interface GetCompletionsRequest extends BaseLanguageServerRequest { interface GetCompletionsRequest extends BaseLanguageServerRequest {
method: "getCompletions"; method: "getCompletions";
specifier: string; specifier: string;
position: number; position: number;
preferences: ts.UserPreferences; preferences: ts.GetCompletionsAtPositionOptions;
} }
interface GetDiagnosticsRequest extends BaseLanguageServerRequest { interface GetDiagnosticsRequest extends BaseLanguageServerRequest {