From 6c324acf2363e88293ab94cf3de6c9d7a264b55d Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 15 Dec 2021 13:23:43 -0500 Subject: [PATCH] feat: REPL import specifier auto-completions (#13078) --- cli/file_fetcher.rs | 15 +- cli/logger.rs | 15 +- cli/lsp/client.rs | 228 +++++++++++++++++++++ cli/lsp/completions.rs | 7 +- cli/lsp/config.rs | 48 +++-- cli/lsp/diagnostics.rs | 7 +- cli/lsp/language_server.rs | 38 ++-- cli/lsp/logging.rs | 49 +++++ cli/lsp/mod.rs | 11 +- cli/lsp/performance.rs | 7 +- cli/lsp/registries.rs | 20 +- cli/lsp/repl.rs | 297 ++++++++++++++++++++++++++++ cli/tests/integration/repl_tests.rs | 38 ++++ cli/tools/repl/channel.rs | 53 ++++- cli/tools/repl/mod.rs | 66 +++++-- 15 files changed, 798 insertions(+), 101 deletions(-) create mode 100644 cli/lsp/client.rs create mode 100644 cli/lsp/logging.rs create mode 100644 cli/lsp/repl.rs diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index e8ad2ccb2d..c10c72e531 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -26,7 +26,6 @@ use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_web::BlobStore; use deno_runtime::permissions::Permissions; use log::debug; -use log::info; use std::borrow::Borrow; use std::collections::HashMap; use std::env; @@ -254,6 +253,7 @@ pub struct FileFetcher { pub(crate) http_cache: HttpCache, http_client: reqwest::Client, blob_store: BlobStore, + download_log_level: log::Level, } impl FileFetcher { @@ -280,9 +280,15 @@ impl FileFetcher { None, )?, blob_store, + download_log_level: log::Level::Info, }) } + /// Sets the log level to use when outputting the download message. + pub fn set_download_log_level(&mut self, level: log::Level) { + self.download_log_level = level; + } + /// Creates a `File` structure for a remote file. fn build_remote_file( &self, @@ -512,7 +518,12 @@ impl FileFetcher { .boxed(); } - info!("{} {}", colors::green("Download"), specifier); + log::log!( + self.download_log_level, + "{} {}", + colors::green("Download"), + specifier + ); let maybe_etag = match self.http_cache.get(specifier) { Ok((_, headers, _)) => headers.get("etag").cloned(), diff --git a/cli/logger.rs b/cli/logger.rs index f3a12eec2e..ff3c8f066f 100644 --- a/cli/logger.rs +++ b/cli/logger.rs @@ -1,13 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use std::io::Write; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; -use std::sync::Arc; - -lazy_static::lazy_static! { - pub static ref LSP_DEBUG_FLAG: Arc = Arc::new(AtomicBool::new(false)); -} struct CliLogger(env_logger::Logger); @@ -23,13 +16,7 @@ impl CliLogger { impl log::Log for CliLogger { fn enabled(&self, metadata: &log::Metadata) -> bool { - if metadata.target() == "deno::lsp::performance" - && metadata.level() == log::Level::Debug - { - LSP_DEBUG_FLAG.load(Ordering::Relaxed) - } else { - self.0.enabled(metadata) - } + self.0.enabled(metadata) } fn log(&self, record: &log::Record) { diff --git a/cli/lsp/client.rs b/cli/lsp/client.rs new file mode 100644 index 0000000000..1023048799 --- /dev/null +++ b/cli/lsp/client.rs @@ -0,0 +1,228 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use deno_core::anyhow::anyhow; +use deno_core::error::AnyError; +use deno_core::futures::future; +use deno_core::serde_json; +use deno_core::serde_json::json; +use lspower::lsp; + +use crate::lsp::config::SETTINGS_SECTION; +use crate::lsp::repl::get_repl_workspace_settings; + +use super::lsp_custom; + +#[derive(Clone)] +pub struct Client(Arc); + +impl std::fmt::Debug for Client { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Client").finish() + } +} + +impl Client { + pub fn from_lspower(client: lspower::Client) -> Self { + Self(Arc::new(LspowerClient(client))) + } + + pub fn new_for_repl() -> Self { + Self(Arc::new(ReplClient)) + } + + pub async fn publish_diagnostics( + &self, + uri: lsp::Url, + diags: Vec, + version: Option, + ) { + self.0.publish_diagnostics(uri, diags, version).await; + } + + pub async fn send_registry_state_notification( + &self, + params: lsp_custom::RegistryStateNotificationParams, + ) { + self.0.send_registry_state_notification(params).await; + } + + pub async fn configuration( + &self, + items: Vec, + ) -> Result, AnyError> { + self.0.configuration(items).await + } + + pub async fn show_message( + &self, + message_type: lsp::MessageType, + message: impl std::fmt::Display, + ) { + self + .0 + .show_message(message_type, format!("{}", message)) + .await + } + + pub async fn register_capability( + &self, + registrations: Vec, + ) -> Result<(), AnyError> { + self.0.register_capability(registrations).await + } +} + +type AsyncReturn = Pin + 'static + Send>>; + +trait ClientTrait: Send + Sync { + fn publish_diagnostics( + &self, + uri: lsp::Url, + diagnostics: Vec, + version: Option, + ) -> AsyncReturn<()>; + fn send_registry_state_notification( + &self, + params: lsp_custom::RegistryStateNotificationParams, + ) -> AsyncReturn<()>; + fn configuration( + &self, + items: Vec, + ) -> AsyncReturn, AnyError>>; + fn show_message( + &self, + message_type: lsp::MessageType, + text: String, + ) -> AsyncReturn<()>; + fn register_capability( + &self, + registrations: Vec, + ) -> AsyncReturn>; +} + +#[derive(Clone)] +struct LspowerClient(lspower::Client); + +impl ClientTrait for LspowerClient { + fn publish_diagnostics( + &self, + uri: lsp::Url, + diagnostics: Vec, + version: Option, + ) -> AsyncReturn<()> { + let client = self.0.clone(); + Box::pin(async move { + client.publish_diagnostics(uri, diagnostics, version).await + }) + } + + fn send_registry_state_notification( + &self, + params: lsp_custom::RegistryStateNotificationParams, + ) -> AsyncReturn<()> { + let client = self.0.clone(); + Box::pin(async move { + client + .send_custom_notification::( + params, + ) + .await + }) + } + + fn configuration( + &self, + items: Vec, + ) -> AsyncReturn, AnyError>> { + let client = self.0.clone(); + Box::pin(async move { + client + .configuration(items) + .await + .map_err(|err| anyhow!("{}", err)) + }) + } + + fn show_message( + &self, + message_type: lsp::MessageType, + message: String, + ) -> AsyncReturn<()> { + let client = self.0.clone(); + Box::pin(async move { client.show_message(message_type, message).await }) + } + + fn register_capability( + &self, + registrations: Vec, + ) -> AsyncReturn> { + let client = self.0.clone(); + Box::pin(async move { + client + .register_capability(registrations) + .await + .map_err(|err| anyhow!("{}", err)) + }) + } +} + +#[derive(Clone)] +struct ReplClient; + +impl ClientTrait for ReplClient { + fn publish_diagnostics( + &self, + _uri: lsp::Url, + _diagnostics: Vec, + _version: Option, + ) -> AsyncReturn<()> { + Box::pin(future::ready(())) + } + + fn send_registry_state_notification( + &self, + _params: lsp_custom::RegistryStateNotificationParams, + ) -> AsyncReturn<()> { + Box::pin(future::ready(())) + } + + fn configuration( + &self, + items: Vec, + ) -> AsyncReturn, AnyError>> { + let is_global_config_request = items.len() == 1 + && items[0].scope_uri.is_none() + && items[0].section.as_deref() == Some(SETTINGS_SECTION); + let response = if is_global_config_request { + vec![serde_json::to_value(get_repl_workspace_settings()).unwrap()] + } else { + // all specifiers are enabled for the REPL + items + .into_iter() + .map(|_| { + json!({ + "enable": true, + }) + }) + .collect() + }; + Box::pin(future::ready(Ok(response))) + } + + fn show_message( + &self, + _message_type: lsp::MessageType, + _message: String, + ) -> AsyncReturn<()> { + Box::pin(future::ready(())) + } + + fn register_capability( + &self, + _registrations: Vec, + ) -> AsyncReturn> { + Box::pin(future::ready(Ok(()))) + } +} diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 0dafa6e85f..ec9467a1f2 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -1,5 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +use super::client::Client; use super::language_server; use super::lsp_custom; use super::tsc; @@ -37,7 +38,7 @@ pub struct CompletionItemData { async fn check_auto_config_registry( url_str: &str, snapshot: &language_server::StateSnapshot, - client: lspower::Client, + client: Client, ) { // check to see if auto discovery is enabled if snapshot @@ -82,7 +83,7 @@ async fn check_auto_config_registry( // TODO(@kitsonk) clean up protocol when doing v2 of suggestions if suggestions { client - .send_custom_notification::( + .send_registry_state_notification( lsp_custom::RegistryStateNotificationParams { origin, suggestions, @@ -133,7 +134,7 @@ pub(crate) async fn get_import_completions( specifier: &ModuleSpecifier, position: &lsp::Position, state_snapshot: &language_server::StateSnapshot, - client: lspower::Client, + client: Client, ) -> Option { let document = state_snapshot.documents.get(specifier)?; let (text, _, range) = document.get_maybe_dependency(position)?; diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 27efaf4970..1c707e68f5 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -19,6 +19,8 @@ use std::sync::Arc; use std::thread; use tokio::sync::mpsc; +use super::client::Client; + pub const SETTINGS_SECTION: &str = "deno"; #[derive(Debug, Clone, Default)] @@ -229,7 +231,7 @@ pub struct Config { } impl Config { - pub fn new(client: lspower::Client) -> Self { + pub fn new(client: Client) -> Self { let (tx, mut rx) = mpsc::channel::(100); let settings = Arc::new(RwLock::new(Settings::default())); let settings_ref = settings.clone(); @@ -284,31 +286,37 @@ impl Config { if settings_ref.read().specifiers.contains_key(&specifier) { continue; } - if let Ok(value) = client + match client .configuration(vec![lsp::ConfigurationItem { scope_uri: Some(uri.clone()), section: Some(SETTINGS_SECTION.to_string()), }]) .await { - match serde_json::from_value::( - value[0].clone(), - ) { - Ok(specifier_settings) => { - settings_ref - .write() - .specifiers - .insert(specifier, (uri, specifier_settings)); - } - Err(err) => { - error!("Error converting specifier settings: {}", err); + Ok(values) => { + if let Some(value) = values.first() { + match serde_json::from_value::(value.clone()) { + Ok(specifier_settings) => { + settings_ref + .write() + .specifiers + .insert(specifier, (uri, specifier_settings)); + } + Err(err) => { + error!("Error converting specifier settings ({}): {}", specifier, err); + } + } + } else { + error!("Expected the client to return a configuration item for specifier: {}", specifier); } + }, + Err(err) => { + error!( + "Error retrieving settings for specifier ({}): {}", + specifier, + err, + ); } - } else { - error!( - "Error retrieving settings for specifier: {}", - specifier - ); } } } @@ -453,9 +461,9 @@ mod tests { } fn setup() -> Config { - let mut maybe_client: Option = None; + let mut maybe_client: Option = None; let (_service, _) = lspower::LspService::new(|client| { - maybe_client = Some(client); + maybe_client = Some(Client::from_lspower(client)); MockLanguageServer::default() }); Config::new(maybe_client.unwrap()) diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 64c696adad..82a08c8f90 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -1,6 +1,7 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use super::analysis; +use super::client::Client; use super::documents; use super::documents::Documents; use super::language_server; @@ -125,7 +126,7 @@ impl DiagnosticsServer { pub(crate) fn start( &mut self, language_server: Arc>, - client: lspower::Client, + client: Client, ts_server: Arc, ) { let (tx, mut rx) = mpsc::unbounded_channel::<()>(); @@ -530,7 +531,7 @@ async fn generate_deps_diagnostics( /// Publishes diagnostics to the client. async fn publish_diagnostics( - client: &lspower::Client, + client: &Client, collection: &mut DiagnosticCollection, snapshot: &language_server::StateSnapshot, ) { @@ -569,7 +570,7 @@ async fn publish_diagnostics( /// Updates diagnostics for any specifiers that don't have the correct version /// generated and publishes the diagnostics to the client. async fn update_diagnostics( - client: &lspower::Client, + client: &Client, collection: Arc>, snapshot: Arc, ts_server: &tsc::TsServer, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index e9e4469183..660ef8d90d 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -16,7 +16,6 @@ use lspower::jsonrpc::Error as LspError; use lspower::jsonrpc::Result as LspResult; use lspower::lsp::request::*; use lspower::lsp::*; -use lspower::Client; use serde_json::from_value; use std::env; use std::path::PathBuf; @@ -30,6 +29,7 @@ use super::analysis::CodeActionCollection; use super::analysis::CodeActionData; use super::cache::CacheServer; use super::capabilities; +use super::client::Client; use super::code_lens; use super::completions; use super::config::Config; @@ -61,6 +61,7 @@ use crate::deno_dir; use crate::file_fetcher::get_source_from_data_url; use crate::fs_util; use crate::logger; +use crate::lsp::logging::lsp_log; use crate::tools::fmt::format_file; use crate::tools::fmt::format_parsed_source; @@ -299,7 +300,7 @@ impl Inner { let maybe_config = workspace_settings.config; if let Some(config_str) = &maybe_config { if !config_str.is_empty() { - info!("Setting TypeScript configuration from: \"{}\"", config_str); + lsp_log!("Setting TypeScript configuration from: \"{}\"", config_str); let config_url = if let Ok(url) = Url::from_file_path(config_str) { Ok(url) } else if let Some(root_uri) = maybe_root_uri { @@ -312,7 +313,7 @@ impl Inner { config_str )) }?; - info!(" Resolved configuration file: \"{}\"", config_url); + lsp_log!(" Resolved configuration file: \"{}\"", config_url); let config_file = ConfigFile::from_specifier(&config_url)?; return Ok(Some((config_file, config_url))); @@ -393,7 +394,7 @@ impl Inner { ) }; let maybe_cache_path = if let Some(cache_str) = &maybe_cache { - info!("Setting cache path from: \"{}\"", cache_str); + lsp_log!("Setting cache path from: \"{}\"", cache_str); let cache_url = if let Ok(url) = Url::from_file_path(cache_str) { Ok(url) } else if let Some(root_uri) = &maybe_root_uri { @@ -409,7 +410,7 @@ impl Inner { )) }?; let cache_path = fs_util::specifier_to_file_path(&cache_url)?; - info!( + lsp_log!( " Resolved cache path: \"{}\"", cache_path.to_string_lossy() ); @@ -444,7 +445,7 @@ impl Inner { ) }; if let Some(import_map_str) = &maybe_import_map { - info!("Setting import map from: \"{}\"", import_map_str); + lsp_log!("Setting import map from: \"{}\"", import_map_str); let import_map_url = if let Ok(url) = Url::from_file_path(import_map_str) { Ok(url) @@ -469,7 +470,7 @@ impl Inner { get_source_from_data_url(&import_map_url)?.0 } else { let import_map_path = fs_util::specifier_to_file_path(&import_map_url)?; - info!( + lsp_log!( " Resolved import map: \"{}\"", import_map_path.to_string_lossy() ); @@ -494,16 +495,9 @@ impl Inner { Ok(()) } - pub fn update_debug_flag(&self) -> bool { + pub fn update_debug_flag(&self) { let internal_debug = self.config.get_workspace_settings().internal_debug; - logger::LSP_DEBUG_FLAG - .compare_exchange( - !internal_debug, - internal_debug, - Ordering::Acquire, - Ordering::Relaxed, - ) - .is_ok() + super::logging::set_lsp_debug_flag(internal_debug) } async fn update_registries(&mut self) -> Result<(), AnyError> { @@ -517,7 +511,7 @@ impl Inner { .iter() { if *enabled { - info!("Enabling import suggestions for: {}", registry); + lsp_log!("Enabling import suggestions for: {}", registry); self.module_registries.enable(registry).await?; } else { self.module_registries.disable(registry).await?; @@ -621,7 +615,7 @@ impl Inner { &mut self, params: InitializeParams, ) -> LspResult { - info!("Starting Deno language server..."); + lsp_log!("Starting Deno language server..."); let mark = self.performance.mark("initialize", Some(¶ms)); // exit this process when the parent is lost @@ -637,9 +631,9 @@ impl Inner { env!("PROFILE"), env!("TARGET") ); - info!(" version: {}", version); + lsp_log!(" version: {}", version); if let Ok(path) = std::env::current_exe() { - info!(" executable: {}", path.to_string_lossy()); + lsp_log!(" executable: {}", path.to_string_lossy()); } let server_info = ServerInfo { @@ -648,7 +642,7 @@ impl Inner { }; if let Some(client_info) = params.client_info { - info!( + lsp_log!( "Connected to \"{}\" {}", client_info.name, client_info.version.unwrap_or_default(), @@ -746,7 +740,7 @@ impl Inner { } } - info!("Server ready."); + lsp_log!("Server ready."); } async fn shutdown(&self) -> LspResult<()> { diff --git a/cli/lsp/logging.rs b/cli/lsp/logging.rs new file mode 100644 index 0000000000..0bfc2fbc84 --- /dev/null +++ b/cli/lsp/logging.rs @@ -0,0 +1,49 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use std::sync::atomic::AtomicBool; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +static LSP_DEBUG_FLAG: AtomicBool = AtomicBool::new(false); +static LSP_LOG_LEVEL: AtomicUsize = AtomicUsize::new(log::Level::Info as usize); + +pub fn set_lsp_debug_flag(value: bool) { + LSP_DEBUG_FLAG.store(value, Ordering::SeqCst) +} + +pub fn lsp_debug_enabled() -> bool { + LSP_DEBUG_FLAG.load(Ordering::SeqCst) +} + +pub fn set_lsp_log_level(level: log::Level) { + LSP_LOG_LEVEL.store(level as usize, Ordering::SeqCst) +} + +pub fn lsp_log_level() -> log::Level { + let level = LSP_LOG_LEVEL.load(Ordering::SeqCst); + unsafe { std::mem::transmute(level) } +} + +/// Use this macro to do "info" logs in the lsp code. This allows +/// for downgrading these logs to another log level in the REPL. +macro_rules! lsp_log { + ($($arg:tt)+) => ( + let lsp_log_level = crate::lsp::logging::lsp_log_level(); + if lsp_log_level == log::Level::Debug { + crate::lsp::logging::lsp_debug!($($arg)+) + } else { + log::log!(lsp_log_level, $($arg)+) + } + ) +} + +macro_rules! lsp_debug { + ($($arg:tt)+) => ( + if crate::lsp::logging::lsp_debug_enabled() { + log::debug!($($arg)+) + } + ) +} + +pub(super) use lsp_debug; +pub(super) use lsp_log; diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index bbfb757dc7..ac4dfc7e9d 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -7,21 +7,27 @@ use deno_core::error::AnyError; use lspower::LspService; use lspower::Server; +pub use repl::ReplCompletionItem; +pub use repl::ReplLanguageServer; + mod analysis; mod cache; mod capabilities; +mod client; mod code_lens; mod completions; mod config; mod diagnostics; mod documents; pub(crate) mod language_server; +mod logging; mod lsp_custom; mod parent_process_checker; mod path_to_regex; mod performance; mod refactor; mod registries; +mod repl; mod semantic_tokens; mod text; mod tsc; @@ -31,8 +37,9 @@ pub async fn start() -> Result<(), AnyError> { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); - let (service, messages) = - LspService::new(language_server::LanguageServer::new); + let (service, messages) = LspService::new(|client| { + language_server::LanguageServer::new(client::Client::from_lspower(client)) + }); Server::new(stdin, stdout) .interleave(messages) .serve(service) diff --git a/cli/lsp/performance.rs b/cli/lsp/performance.rs index 74af38ec1e..2c58ab9c93 100644 --- a/cli/lsp/performance.rs +++ b/cli/lsp/performance.rs @@ -4,7 +4,6 @@ use deno_core::parking_lot::Mutex; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::serde_json::json; -use log::debug; use std::cmp; use std::collections::HashMap; use std::collections::VecDeque; @@ -13,6 +12,8 @@ use std::sync::Arc; use std::time::Duration; use std::time::Instant; +use super::logging::lsp_debug; + #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct PerformanceAverage { @@ -156,7 +157,7 @@ impl Performance { "name": name, }) }; - debug!("{},", msg); + lsp_debug!("{},", msg); PerformanceMark { name: name.to_string(), count: *count, @@ -169,7 +170,7 @@ impl Performance { /// measurement to the internal buffer. pub fn measure(&self, mark: PerformanceMark) -> Duration { let measure = PerformanceMeasure::from(mark); - debug!( + lsp_debug!( "{},", json!({ "type": "measure", diff --git a/cli/lsp/registries.rs b/cli/lsp/registries.rs index 62a6aa9874..afaefffd87 100644 --- a/cli/lsp/registries.rs +++ b/cli/lsp/registries.rs @@ -395,29 +395,14 @@ impl Default for ModuleRegistry { // custom root. let dir = deno_dir::DenoDir::new(None).unwrap(); let location = dir.root.join("registries"); - let http_cache = HttpCache::new(&location); - let cache_setting = CacheSetting::RespectHeaders; - let file_fetcher = FileFetcher::new( - http_cache, - cache_setting, - true, - None, - BlobStore::default(), - None, - ) - .unwrap(); - - Self { - origins: HashMap::new(), - file_fetcher, - } + Self::new(&location) } } impl ModuleRegistry { pub fn new(location: &Path) -> Self { let http_cache = HttpCache::new(location); - let file_fetcher = FileFetcher::new( + let mut file_fetcher = FileFetcher::new( http_cache, CacheSetting::RespectHeaders, true, @@ -427,6 +412,7 @@ impl ModuleRegistry { ) .context("Error creating file fetcher in module registry.") .unwrap(); + file_fetcher.set_download_log_level(super::logging::lsp_log_level()); Self { origins: HashMap::new(), diff --git a/cli/lsp/repl.rs b/cli/lsp/repl.rs new file mode 100644 index 0000000000..458841c194 --- /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 { + 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 { + 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 { + 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)]), + }, + }, + } +} diff --git a/cli/tests/integration/repl_tests.rs b/cli/tests/integration/repl_tests.rs index 18e022cfe6..c98afd6d58 100644 --- a/cli/tests/integration/repl_tests.rs +++ b/cli/tests/integration/repl_tests.rs @@ -122,6 +122,44 @@ fn pty_complete_primitives() { }); } +#[test] +fn pty_complete_imports() { + util::with_pty(&["repl"], |mut console| { + // single quotes + console.write_line("import './001_hel\t'"); + // double quotes + console.write_line("import { output } from \"./045_out\t\""); + console.write_line("output('testing output');"); + console.write_line("close();"); + + let output = console.read_all_output(); + assert!(output.contains("Hello World")); + assert!(output.contains("\ntesting output")); + }); + + // ensure when the directory changes that the suggestions come from the cwd + util::with_pty(&["repl"], |mut console| { + console.write_line("Deno.chdir('./subdir');"); + console.write_line("import '../001_hel\t'"); + console.write_line("close();"); + + let output = console.read_all_output(); + assert!(output.contains("Hello World")); + }); + + // ensure nothing too bad happens when deleting the cwd + util::with_pty(&["repl"], |mut console| { + console.write_line("Deno.mkdirSync('./temp-repl-lsp-dir');"); + console.write_line("Deno.chdir('./temp-repl-lsp-dir');"); + console.write_line("Deno.removeSync('../temp-repl-lsp-dir');"); + console.write_line("import '../001_hello\t'"); + console.write_line("close();"); + + let output = console.read_all_output(); + assert!(output.contains("Hello World")); + }); +} + #[test] fn pty_ignore_symbols() { util::with_pty(&["repl"], |mut console| { diff --git a/cli/tools/repl/channel.rs b/cli/tools/repl/channel.rs index 54ec6869d4..a02f885231 100644 --- a/cli/tools/repl/channel.rs +++ b/cli/tools/repl/channel.rs @@ -11,6 +11,8 @@ use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedSender; +use crate::lsp::ReplCompletionItem; + /// Rustyline uses synchronous methods in its interfaces, but we need to call /// async methods. To get around this, we communicate with async code by using /// a channel and blocking on the result. @@ -31,8 +33,21 @@ pub fn rustyline_channel( ) } -pub type RustylineSyncMessage = (String, Option); -pub type RustylineSyncResponse = Result; +pub enum RustylineSyncMessage { + PostMessage { + method: String, + params: Option, + }, + LspCompletions { + line_text: String, + position: usize, + }, +} + +pub enum RustylineSyncResponse { + PostMessage(Result), + LspCompletions(Vec), +} pub struct RustylineSyncMessageSender { message_tx: Sender, @@ -46,11 +61,41 @@ impl RustylineSyncMessageSender { params: Option, ) -> Result { if let Err(err) = - self.message_tx.blocking_send((method.to_string(), params)) + self + .message_tx + .blocking_send(RustylineSyncMessage::PostMessage { + method: method.to_string(), + params, + }) { Err(anyhow!("{}", err)) } else { - self.response_rx.borrow_mut().blocking_recv().unwrap() + match self.response_rx.borrow_mut().blocking_recv().unwrap() { + RustylineSyncResponse::PostMessage(result) => result, + RustylineSyncResponse::LspCompletions(_) => unreachable!(), + } + } + } + + pub fn lsp_completions( + &self, + line_text: &str, + position: usize, + ) -> Vec { + if self + .message_tx + .blocking_send(RustylineSyncMessage::LspCompletions { + line_text: line_text.to_string(), + position, + }) + .is_err() + { + Vec::new() + } else { + match self.response_rx.borrow_mut().blocking_recv().unwrap() { + RustylineSyncResponse::LspCompletions(result) => result, + RustylineSyncResponse::PostMessage(_) => unreachable!(), + } } } } diff --git a/cli/tools/repl/mod.rs b/cli/tools/repl/mod.rs index 687e8a300b..e971d0a7a5 100644 --- a/cli/tools/repl/mod.rs +++ b/cli/tools/repl/mod.rs @@ -4,7 +4,10 @@ use crate::ast::transpile; use crate::ast::Diagnostics; use crate::ast::ImportsNotUsedAsValues; use crate::colors; +use crate::lsp::ReplLanguageServer; use crate::proc_state::ProcState; +use crate::tools::repl::channel::RustylineSyncMessage; +use crate::tools::repl::channel::RustylineSyncResponse; use deno_ast::swc::parser::error::SyntaxError; use deno_ast::swc::parser::token::Token; use deno_ast::swc::parser::token::Word; @@ -181,6 +184,15 @@ impl Completer for EditorHelper { pos: usize, _ctx: &Context<'_>, ) -> Result<(usize, Vec), ReadlineError> { + let lsp_completions = self.sync_sender.lsp_completions(line, pos); + if !lsp_completions.is_empty() { + // assumes all lsp completions have the same start position + return Ok(( + lsp_completions[0].span.lo.0 as usize, + lsp_completions.into_iter().map(|c| c.new_text).collect(), + )); + } + let expr = get_expr_from_line_at_pos(line, pos); // check if the expression is in the form `obj.prop` @@ -428,14 +440,21 @@ impl std::fmt::Display for EvaluationOutput { } } +struct TsEvaluateResponse { + ts_code: String, + value: Value, +} + struct ReplSession { worker: MainWorker, session: LocalInspectorSession, pub context_id: u64, + language_server: ReplLanguageServer, } impl ReplSession { pub async fn initialize(mut worker: MainWorker) -> Result { + let language_server = ReplLanguageServer::new_initialized().await?; let mut session = worker.create_inspector_session().await; worker @@ -467,6 +486,7 @@ impl ReplSession { worker, session, context_id, + language_server, }; // inject prelude @@ -520,13 +540,18 @@ impl ReplSession { match self.evaluate_line_with_object_wrapping(line).await { Ok(evaluate_response) => { - let evaluate_result = evaluate_response.get("result").unwrap(); + let evaluate_result = evaluate_response.value.get("result").unwrap(); let evaluate_exception_details = - evaluate_response.get("exceptionDetails"); + evaluate_response.value.get("exceptionDetails"); if evaluate_exception_details.is_some() { self.set_last_thrown_error(evaluate_result).await?; } else { + self + .language_server + .commit_text(&evaluate_response.ts_code) + .await; + self.set_last_eval_result(evaluate_result).await?; } @@ -561,7 +586,7 @@ impl ReplSession { async fn evaluate_line_with_object_wrapping( &mut self, line: &str, - ) -> Result { + ) -> Result { // Expressions like { "foo": "bar" } are interpreted as block expressions at the // statement level rather than an object literal so we interpret it as an expression statement // to match the behavior found in a typical prompt including browser developer tools. @@ -582,6 +607,7 @@ impl ReplSession { || evaluate_response .as_ref() .unwrap() + .value .get("exceptionDetails") .is_some()) { @@ -658,7 +684,7 @@ impl ReplSession { async fn evaluate_ts_expression( &mut self, expression: &str, - ) -> Result { + ) -> Result { let parsed_module = deno_ast::parse_module(deno_ast::ParseParams { specifier: "repl.ts".to_string(), source: deno_ast::SourceTextInfo::from_string(expression.to_string()), @@ -688,12 +714,17 @@ impl ReplSession { )? .0; - self + let value = self .evaluate_expression(&format!( "'use strict'; void 0;\n{}", transpiled_src )) - .await + .await?; + + Ok(TsEvaluateResponse { + ts_code: expression.to_string(), + value, + }) } async fn evaluate_expression( @@ -727,11 +758,24 @@ async fn read_line_and_poll( return result.unwrap(); } result = message_handler.recv() => { - if let Some((method, params)) = result { - let result = repl_session - .post_message_with_event_loop(&method, params) - .await; - message_handler.send(result).unwrap(); + match result { + Some(RustylineSyncMessage::PostMessage { + method, + params + }) => { + let result = repl_session + .post_message_with_event_loop(&method, params) + .await; + message_handler.send(RustylineSyncResponse::PostMessage(result)).unwrap(); + }, + Some(RustylineSyncMessage::LspCompletions { + line_text, + position, + }) => { + let result = repl_session.language_server.completions(&line_text, position).await; + message_handler.send(RustylineSyncResponse::LspCompletions(result)).unwrap(); + } + None => {}, // channel closed } poll_worker = true;