summaryrefslogtreecommitdiff
path: root/cli/lsp/repl.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2021-12-15 13:23:43 -0500
committerGitHub <noreply@github.com>2021-12-15 13:23:43 -0500
commit6c324acf2363e88293ab94cf3de6c9d7a264b55d (patch)
treeb0d7c8752bf7e7b471be4a50e65572d501bb8b5a /cli/lsp/repl.rs
parenta1f0796fccfafee19b2fe06155efe746da2e9654 (diff)
feat: REPL import specifier auto-completions (#13078)
Diffstat (limited to 'cli/lsp/repl.rs')
-rw-r--r--cli/lsp/repl.rs297
1 files changed, 297 insertions, 0 deletions
diff --git a/cli/lsp/repl.rs b/cli/lsp/repl.rs
new file mode 100644
index 000000000..458841c19
--- /dev/null
+++ b/cli/lsp/repl.rs
@@ -0,0 +1,297 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+use std::collections::HashMap;
+use std::future::Future;
+
+use deno_ast::swc::common::BytePos;
+use deno_ast::swc::common::Span;
+use deno_ast::LineAndColumnIndex;
+use deno_ast::ModuleSpecifier;
+use deno_ast::SourceTextInfo;
+use deno_core::anyhow::anyhow;
+use deno_core::error::AnyError;
+use deno_core::serde_json;
+use lspower::lsp::ClientCapabilities;
+use lspower::lsp::ClientInfo;
+use lspower::lsp::CompletionContext;
+use lspower::lsp::CompletionParams;
+use lspower::lsp::CompletionResponse;
+use lspower::lsp::CompletionTextEdit;
+use lspower::lsp::CompletionTriggerKind;
+use lspower::lsp::DidChangeTextDocumentParams;
+use lspower::lsp::DidCloseTextDocumentParams;
+use lspower::lsp::DidOpenTextDocumentParams;
+use lspower::lsp::InitializeParams;
+use lspower::lsp::InitializedParams;
+use lspower::lsp::PartialResultParams;
+use lspower::lsp::Position;
+use lspower::lsp::Range;
+use lspower::lsp::TextDocumentContentChangeEvent;
+use lspower::lsp::TextDocumentIdentifier;
+use lspower::lsp::TextDocumentItem;
+use lspower::lsp::TextDocumentPositionParams;
+use lspower::lsp::VersionedTextDocumentIdentifier;
+use lspower::lsp::WorkDoneProgressParams;
+use lspower::LanguageServer;
+
+use crate::logger;
+
+use super::client::Client;
+use super::config::CompletionSettings;
+use super::config::ImportCompletionSettings;
+use super::config::WorkspaceSettings;
+
+#[derive(Debug)]
+pub struct ReplCompletionItem {
+ pub new_text: String,
+ pub span: Span,
+}
+
+pub struct ReplLanguageServer {
+ language_server: super::language_server::LanguageServer,
+ document_version: i32,
+ document_text: String,
+ pending_text: String,
+ cwd_uri: ModuleSpecifier,
+}
+
+impl ReplLanguageServer {
+ pub async fn new_initialized() -> Result<ReplLanguageServer, AnyError> {
+ super::logging::set_lsp_log_level(log::Level::Debug);
+ let language_server =
+ super::language_server::LanguageServer::new(Client::new_for_repl());
+
+ let cwd_uri = get_cwd_uri()?;
+
+ #[allow(deprecated)]
+ language_server
+ .initialize(InitializeParams {
+ process_id: None,
+ root_path: None,
+ root_uri: Some(cwd_uri.clone()),
+ initialization_options: Some(
+ serde_json::to_value(get_repl_workspace_settings()).unwrap(),
+ ),
+ capabilities: ClientCapabilities {
+ workspace: None,
+ text_document: None,
+ window: None,
+ general: None,
+ experimental: None,
+ },
+ trace: None,
+ workspace_folders: None,
+ client_info: Some(ClientInfo {
+ name: "Deno REPL".to_string(),
+ version: None,
+ }),
+ locale: None,
+ })
+ .await?;
+
+ language_server.initialized(InitializedParams {}).await;
+
+ let server = ReplLanguageServer {
+ language_server,
+ document_version: 0,
+ document_text: String::new(),
+ pending_text: String::new(),
+ cwd_uri,
+ };
+ server.open_current_document().await;
+
+ Ok(server)
+ }
+
+ pub async fn commit_text(&mut self, line_text: &str) {
+ self.did_change(line_text).await;
+ self.document_text.push_str(&self.pending_text);
+ self.pending_text = String::new();
+ }
+
+ pub async fn completions(
+ &mut self,
+ line_text: &str,
+ position: usize,
+ ) -> Vec<ReplCompletionItem> {
+ self.did_change(line_text).await;
+ let before_line_len = BytePos(self.document_text.len() as u32);
+ let position = before_line_len + BytePos(position as u32);
+ let text_info = deno_ast::SourceTextInfo::from_string(format!(
+ "{}{}",
+ self.document_text, self.pending_text
+ ));
+ let line_and_column = text_info.line_and_column_index(position);
+ let response = self
+ .language_server
+ .completion(CompletionParams {
+ text_document_position: TextDocumentPositionParams {
+ text_document: TextDocumentIdentifier {
+ uri: self.get_document_specifier(),
+ },
+ position: Position {
+ line: line_and_column.line_index as u32,
+ character: line_and_column.column_index as u32,
+ },
+ },
+ work_done_progress_params: WorkDoneProgressParams {
+ work_done_token: None,
+ },
+ partial_result_params: PartialResultParams {
+ partial_result_token: None,
+ },
+ context: Some(CompletionContext {
+ trigger_kind: CompletionTriggerKind::INVOKED,
+ trigger_character: None,
+ }),
+ })
+ .await
+ .ok()
+ .unwrap_or_default();
+
+ let items = match response {
+ Some(CompletionResponse::Array(items)) => items,
+ Some(CompletionResponse::List(list)) => list.items,
+ None => Vec::new(),
+ };
+ items
+ .into_iter()
+ .filter_map(|item| {
+ item.text_edit.and_then(|edit| match edit {
+ CompletionTextEdit::Edit(edit) => Some(ReplCompletionItem {
+ new_text: edit.new_text,
+ span: lsp_range_to_span(&text_info, &edit.range),
+ }),
+ CompletionTextEdit::InsertAndReplace(_) => None,
+ })
+ })
+ .filter(|item| {
+ // filter the results to only exact matches
+ let text = &text_info.text_str()
+ [item.span.lo.0 as usize..item.span.hi.0 as usize];
+ item.new_text.starts_with(text)
+ })
+ .map(|mut item| {
+ // convert back to a line position
+ item.span = Span::new(
+ item.span.lo - before_line_len,
+ item.span.hi - before_line_len,
+ Default::default(),
+ );
+ item
+ })
+ .collect()
+ }
+
+ async fn did_change(&mut self, new_text: &str) {
+ self.check_cwd_change().await;
+ let new_text = if new_text.ends_with('\n') {
+ new_text.to_string()
+ } else {
+ format!("{}\n", new_text)
+ };
+ self.document_version += 1;
+ let current_line_count =
+ self.document_text.chars().filter(|c| *c == '\n').count() as u32;
+ let pending_line_count =
+ self.pending_text.chars().filter(|c| *c == '\n').count() as u32;
+ self
+ .language_server
+ .did_change(DidChangeTextDocumentParams {
+ text_document: VersionedTextDocumentIdentifier {
+ uri: self.get_document_specifier(),
+ version: self.document_version,
+ },
+ content_changes: vec![TextDocumentContentChangeEvent {
+ range: Some(Range {
+ start: Position::new(current_line_count, 0),
+ end: Position::new(current_line_count + pending_line_count, 0),
+ }),
+ range_length: None,
+ text: new_text.to_string(),
+ }],
+ })
+ .await;
+ self.pending_text = new_text;
+ }
+
+ async fn check_cwd_change(&mut self) {
+ // handle if the cwd changes, if the cwd is deleted in the case of
+ // get_cwd_uri() erroring, then keep using it as the base
+ let cwd_uri = get_cwd_uri().unwrap_or_else(|_| self.cwd_uri.clone());
+ if self.cwd_uri != cwd_uri {
+ self
+ .language_server
+ .did_close(DidCloseTextDocumentParams {
+ text_document: TextDocumentIdentifier {
+ uri: self.get_document_specifier(),
+ },
+ })
+ .await;
+ self.cwd_uri = cwd_uri;
+ self.document_version = 0;
+ self.open_current_document().await;
+ }
+ }
+
+ async fn open_current_document(&self) {
+ self
+ .language_server
+ .did_open(DidOpenTextDocumentParams {
+ text_document: TextDocumentItem {
+ uri: self.get_document_specifier(),
+ language_id: "typescript".to_string(),
+ version: self.document_version,
+ text: format!("{}{}", self.document_text, self.pending_text),
+ },
+ })
+ .await;
+ }
+
+ fn get_document_specifier(&self) -> ModuleSpecifier {
+ self.cwd_uri.join("$deno$repl.ts").unwrap()
+ }
+}
+
+fn lsp_range_to_span(text_info: &SourceTextInfo, range: &Range) -> Span {
+ Span::new(
+ text_info.byte_index(LineAndColumnIndex {
+ line_index: range.start.line as usize,
+ column_index: range.start.character as usize,
+ }),
+ text_info.byte_index(LineAndColumnIndex {
+ line_index: range.end.line as usize,
+ column_index: range.end.character as usize,
+ }),
+ Default::default(),
+ )
+}
+
+fn get_cwd_uri() -> Result<ModuleSpecifier, AnyError> {
+ let cwd = std::env::current_dir()?;
+ ModuleSpecifier::from_directory_path(&cwd)
+ .map_err(|_| anyhow!("Could not get URI from {}", cwd.display()))
+}
+
+pub fn get_repl_workspace_settings() -> WorkspaceSettings {
+ WorkspaceSettings {
+ enable: true,
+ config: None,
+ cache: None,
+ import_map: None,
+ code_lens: Default::default(),
+ internal_debug: false,
+ lint: false,
+ unstable: false,
+ suggest: CompletionSettings {
+ complete_function_calls: false,
+ names: false,
+ paths: false,
+ auto_imports: false,
+ imports: ImportCompletionSettings {
+ auto_discover: false,
+ hosts: HashMap::from([("https://deno.land".to_string(), true)]),
+ },
+ },
+ }
+}