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:
parent
2ff9b01551
commit
506b321d47
9 changed files with 800 additions and 148 deletions
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(¶ms.text_document_position.text_document.uri);
|
.normalize_url(¶ms.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) = ¶ms.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) = ¶ms.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(¶ms);
|
||||||
|
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>,
|
||||||
|
|
649
cli/lsp/tsc.rs
649
cli/lsp/tsc.rs
|
@ -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(¶m.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": []
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
18
cli/tests/lsp/completion_request.json
Normal file
18
cli/tests/lsp/completion_request.json
Normal 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": "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
cli/tests/lsp/completion_resolve_request.json
Normal file
17
cli/tests/lsp/completion_resolve_request.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
cli/tests/lsp/did_open_notification_completions.json
Normal file
12
cli/tests/lsp/did_open_notification_completions.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "textDocument/didOpen",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file:///a/file.ts",
|
||||||
|
"languageId": "typescript",
|
||||||
|
"version": 1,
|
||||||
|
"text": "Deno."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
14
cli/tsc/compiler.d.ts
vendored
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue