Skip to content

Commit

Permalink
feat: basic completions in lsp
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacasonato committed Dec 8, 2020
1 parent b77d6cb commit c9f570b
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 4 deletions.
19 changes: 18 additions & 1 deletion cli/lsp/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
///! client.
///!
use lsp_types::ClientCapabilities;
use lsp_types::CompletionOptions;
use lsp_types::HoverProviderCapability;
use lsp_types::OneOf;
use lsp_types::SaveOptions;
use lsp_types::ServerCapabilities;
use lsp_types::TextDocumentSyncCapability;
use lsp_types::TextDocumentSyncKind;
use lsp_types::TextDocumentSyncOptions;
use lsp_types::WorkDoneProgressOptions;

pub fn server_capabilities(
_client_capabilities: &ClientCapabilities,
Expand All @@ -28,7 +30,22 @@ pub fn server_capabilities(
},
)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
completion_provider: None,
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![
".".to_string(),
"\"".to_string(),
"'".to_string(),
"`".to_string(),
"/".to_string(),
"@".to_string(),
"<".to_string(),
"#".to_string(),
]),
resolve_provider: None,
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
}),
signature_help_provider: None,
declaration_provider: None,
definition_provider: Some(OneOf::Left(true)),
Expand Down
32 changes: 32 additions & 0 deletions cli/lsp/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::ModuleSpecifier;
use dprint_plugin_typescript as dprint;
use lsp_types::CompletionParams;
use lsp_types::CompletionResponse;
use lsp_types::DocumentFormattingParams;
use lsp_types::DocumentHighlight;
use lsp_types::DocumentHighlightParams;
Expand Down Expand Up @@ -187,6 +189,36 @@ pub fn handle_hover(
}
}

pub fn handle_completion(
state: &mut ServerState,
params: CompletionParams,
) -> Result<Option<CompletionResponse>, AnyError> {
let specifier =
utils::normalize_url(params.text_document_position.text_document.uri);
let line_index = get_line_index(state, &specifier)?;
let server_state = state.snapshot();
let maybe_completion_info: Option<tsc::CompletionInfo> =
serde_json::from_value(tsc::request(
&mut state.ts_runtime,
&server_state,
tsc::RequestMethod::GetCompletions((
specifier,
text::to_char_pos(&line_index, params.text_document_position.position),
tsc::UserPreferences {
// TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651
include_completions_with_insert_text: Some(false),
..Default::default()
},
)),
)?)?;

if let Some(completions) = maybe_completion_info {
Ok(Some(completions.to_completion_response(&line_index)))
} else {
Ok(None)
}
}

pub fn handle_references(
state: &mut ServerState,
params: ReferenceParams,
Expand Down
1 change: 1 addition & 0 deletions cli/lsp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ impl ServerState {
handlers::handle_goto_definition,
)?
.on_sync::<lsp_types::request::HoverRequest>(handlers::handle_hover)?
.on_sync::<lsp_types::request::Completion>(handlers::handle_completion)?
.on_sync::<lsp_types::request::References>(handlers::handle_references)?
.on::<lsp_types::request::Formatting>(handlers::handle_formatting)
.on::<lsp_extensions::VirtualTextDocument>(
Expand Down
225 changes: 223 additions & 2 deletions cli/lsp/tsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::json_op_sync;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
Expand Down Expand Up @@ -229,7 +230,7 @@ fn replace_links(text: &str) -> String {
.to_string()
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub enum ScriptElementKind {
#[serde(rename = "")]
Unknown,
Expand Down Expand Up @@ -301,7 +302,47 @@ pub enum ScriptElementKind {
String,
}

#[derive(Debug, Deserialize)]
impl From<ScriptElementKind> for lsp_types::CompletionItemKind {
fn from(kind: ScriptElementKind) -> Self {
use lsp_types::CompletionItemKind;

match kind {
ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => {
CompletionItemKind::Keyword
}
ScriptElementKind::ConstElement => CompletionItemKind::Constant,
ScriptElementKind::LetElement
| ScriptElementKind::VariableElement
| ScriptElementKind::LocalVariableElement
| ScriptElementKind::Alias => CompletionItemKind::Variable,
ScriptElementKind::MemberVariableElement
| ScriptElementKind::MemberGetAccessorElement
| ScriptElementKind::MemberSetAccessorElement => {
CompletionItemKind::Field
}
ScriptElementKind::FunctionElement => CompletionItemKind::Function,
ScriptElementKind::MemberFunctionElement
| ScriptElementKind::ConstructSignatureElement
| ScriptElementKind::CallSignatureElement
| ScriptElementKind::IndexSignatureElement => CompletionItemKind::Method,
ScriptElementKind::EnumElement => CompletionItemKind::Enum,
ScriptElementKind::ModuleElement
| ScriptElementKind::ExternalModuleName => CompletionItemKind::Module,
ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => {
CompletionItemKind::Class
}
ScriptElementKind::InterfaceElement => CompletionItemKind::Interface,
ScriptElementKind::Warning | ScriptElementKind::ScriptElement => {
CompletionItemKind::File
}
ScriptElementKind::Directory => CompletionItemKind::Folder,
ScriptElementKind::String => CompletionItemKind::Constant,
_ => CompletionItemKind::Property,
}
}
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextSpan {
start: u32,
Expand Down Expand Up @@ -519,6 +560,110 @@ impl ReferenceEntry {
}
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionInfo {
entries: Vec<CompletionEntry>,
is_member_completion: bool,
}

impl CompletionInfo {
pub fn to_completion_response(
self,
line_index: &[u32],
) -> lsp_types::CompletionResponse {
let items = self
.entries
.into_iter()
.map(|entry| entry.to_completion_item(line_index))
.collect();
lsp_types::CompletionResponse::Array(items)
}
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionEntry {
kind: ScriptElementKind,
kind_modifiers: Option<String>,
name: String,
sort_text: String,
insert_text: Option<String>,
replacement_span: Option<TextSpan>,
has_action: Option<bool>,
source: Option<String>,
is_recommended: Option<bool>,
}

impl CompletionEntry {
pub fn to_completion_item(
self,
line_index: &[u32],
) -> lsp_types::CompletionItem {
let mut item = lsp_types::CompletionItem {
label: self.name,
kind: Some(self.kind.into()),
sort_text: Some(self.sort_text.clone()),
// TODO(lucacasonato): missing commit_characters
..Default::default()
};

if let Some(true) = self.is_recommended {
// Make sure isRecommended property always comes first
// https://github.com/Microsoft/vscode/issues/40325
item.preselect = Some(true);
} else if self.source.is_some() {
// De-prioritze auto-imports
// https://github.com/Microsoft/vscode/issues/40311
item.sort_text = Some("\u{ffff}".to_string() + &self.sort_text)
}

match item.kind {
Some(lsp_types::CompletionItemKind::Function)
| Some(lsp_types::CompletionItemKind::Method) => {
item.insert_text_format = Some(lsp_types::InsertTextFormat::Snippet);
}
_ => {}
}

let mut insert_text = self.insert_text;
let replacement_range: Option<lsp_types::Range> =
self.replacement_span.map(|span| span.to_range(line_index));

// // Make sure we only replace a single line at most
// if (replacementRange && replacementRange.start.line !== replacementRange.end.line) {
// replacementRange = lsp.Range.create(replacementRange.start, document.getLineEnd(replacementRange.start.line));
// }
// if (insertText && replacementRange && insertText[0] === '[') { // o.x -> o['x']
// item.filterText = '.' + item.label;
// }

if let Some(kind_modifiers) = self.kind_modifiers {
if kind_modifiers.contains("\\optional\\") {
if insert_text.is_none() {
insert_text = Some(item.label.clone());
}
if item.filter_text.is_none() {
item.filter_text = Some(item.label.clone());
}
item.label += "?";
}
}

if let Some(insert_text) = insert_text {
if let Some(replacement_range) = replacement_range {
item.text_edit = Some(lsp_types::CompletionTextEdit::Edit(
lsp_types::TextEdit::new(replacement_range, insert_text.clone()),
));
} else {
item.insert_text = Some(insert_text);
}
}

item
}
}

#[derive(Debug, Clone, Deserialize)]
struct Response {
id: usize,
Expand Down Expand Up @@ -815,6 +960,71 @@ pub fn start(debug: bool) -> Result<JsRuntime, AnyError> {
Ok(runtime)
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub enum QuotePreference {
Auto,
Double,
Single,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub enum ImportModuleSpecifierPreference {
Auto,
Relative,
NonRelative,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub enum ImportModuleSpecifierEnding {
Auto,
Minimal,
Index,
Js,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub enum IncludePackageJsonAutoImports {
Auto,
On,
Off,
}

#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_suggestions: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quote_preference: Option<QuotePreference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_completions_for_module_exports: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_automatic_optional_chain_completions: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_completions_with_insert_text: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub import_module_specifier_preference:
Option<ImportModuleSpecifierPreference>,
#[serde(skip_serializing_if = "Option::is_none")]
pub import_module_specifier_ending: Option<ImportModuleSpecifierEnding>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_text_changes_in_new_files: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provide_prefix_and_suffix_text_for_rename: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_package_json_auto_imports: Option<IncludePackageJsonAutoImports>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provide_refactor_not_applicable_reason: Option<bool>,
}

/// Methods that are supported by the Language Service in the compiler isolate.
pub enum RequestMethod {
/// Configure the compilation settings for the server.
Expand All @@ -833,6 +1043,8 @@ pub enum RequestMethod {
GetReferences((ModuleSpecifier, u32)),
/// Get declaration information for a specific position.
GetDefinition((ModuleSpecifier, u32)),
/// Get completion information at a given position (IntelliSense).
GetCompletions((ModuleSpecifier, u32, UserPreferences)),
}

impl RequestMethod {
Expand Down Expand Up @@ -887,6 +1099,15 @@ impl RequestMethod {
"specifier": specifier,
"position": position,
}),
RequestMethod::GetCompletions((specifier, position, preferences)) => {
json!({
"id": id,
"method": "getCompletions",
"specifier": specifier,
"position": position,
"preferences": preferences,
})
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions cli/tsc/99_main_compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,16 @@ delete Object.prototype.__proto__;
),
);
}
case "getCompletions": {
return respond(
id,
languageService.getCompletionsAtPosition(
request.specifier,
request.position,
request.preferences,
),
);
}
case "getDocumentHighlights": {
return respond(
id,
Expand Down
Loading

0 comments on commit c9f570b

Please sign in to comment.