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)),
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,
},

View file

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

View file

@ -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,
@ -2740,6 +2797,58 @@ mod tests {
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)]
struct PerformanceAverages {
averages: Vec<PerformanceAverage>,

View file

@ -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::ExternalModuleName => {
lsp::CompletionItemKind::Module
}
ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => {
CompletionItemKind::Class
lsp::CompletionItemKind::Class
}
ScriptElementKind::InterfaceElement => CompletionItemKind::Interface,
ScriptElementKind::Warning | ScriptElementKind::ScriptElement => {
CompletionItemKind::File
}
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 CompletionInfo {
entries: Vec<CompletionEntry>,
is_member_completion: bool,
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 CompletionInfo {
pub fn into_completion_response(
self,
line_index: &LineIndex,
) -> lsp::CompletionResponse {
let items = self
.entries
.into_iter()
.map(|entry| entry.into_completion_item(line_index))
.collect();
lsp::CompletionResponse::Array(items)
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)]
#[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")]
pub struct CompletionEntry {
kind: ScriptElementKind,
kind_modifiers: Option<String>,
name: String,
kind: ScriptElementKind,
#[serde(skip_serializing_if = "Option::is_none")]
kind_modifiers: Option<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,
fn get_commit_characters(
&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,
info: &CompletionInfo,
settings: &config::CompletionSettings,
specifier: &ModuleSpecifier,
position: u32,
) -> 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()
let mut label = self.name.clone();
let mut kind: Option<lsp::CompletionItemKind> =
Some(self.kind.clone().into());
let sort_text = if self.source.is_some() {
Some(format!("\u{ffff}{}", self.sort_text))
} else {
Some(self.sort_text.clone())
};
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 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;
match item.kind {
Some(lsp::CompletionItemKind::Function)
| Some(lsp::CompletionItemKind::Method) => {
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 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
};
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 {
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,
))),
}
}
@ -1479,6 +1727,15 @@ pub enum IncludePackageJsonAutoImports {
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)]
#[serde(rename_all = "camelCase")]
pub struct UserPreferences {
@ -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": []
})
);
}
}

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

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

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