diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 42478b5932..717508752f 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -407,6 +407,30 @@ pub struct LanguageWorkspaceSettings { pub update_imports_on_file_move: UpdateImportsOnFileMoveOptions, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum InspectSetting { + Bool(bool), + String(String), +} + +impl Default for InspectSetting { + fn default() -> Self { + InspectSetting::Bool(false) + } +} + +impl InspectSetting { + pub fn to_address(&self) -> Option { + match self { + InspectSetting::Bool(false) => None, + InspectSetting::Bool(true) => Some("127.0.0.1:9222".to_string()), + InspectSetting::String(s) => Some(s.clone()), + } + } +} + /// Deno language server specific settings that are applied to a workspace. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -454,6 +478,9 @@ pub struct WorkspaceSettings { #[serde(default)] pub internal_debug: bool, + #[serde(default)] + pub internal_inspect: InspectSetting, + /// Write logs to a file in a project-local directory. #[serde(default)] pub log_file: bool, @@ -506,6 +533,7 @@ impl Default for WorkspaceSettings { import_map: None, code_lens: Default::default(), internal_debug: false, + internal_inspect: Default::default(), log_file: false, lint: true, document_preload_limit: default_document_preload_limit(), @@ -1080,6 +1108,10 @@ impl Config { self.settings.unscoped.log_file } + pub fn internal_inspect(&self) -> &InspectSetting { + &self.settings.unscoped.internal_inspect + } + pub fn update_capabilities( &mut self, capabilities: &lsp::ClientCapabilities, @@ -1330,6 +1362,7 @@ mod tests { test: true, }, internal_debug: false, + internal_inspect: InspectSetting::Bool(false), log_file: false, lint: true, document_preload_limit: 1_000, diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 8034127e90..4bec3083d9 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -1639,6 +1639,7 @@ let c: number = "a"; let cache = Arc::new(GlobalHttpCache::new(cache_location, RealDenoCacheEnv)); let ts_server = TsServer::new(Default::default(), cache); + ts_server.start(None); // test enabled { diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 2d3864e0ab..e730e145fe 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1226,6 +1226,10 @@ impl Inner { self.config.update_capabilities(¶ms.capabilities); } + self + .ts_server + .start(self.config.internal_inspect().to_address()); + self.update_debug_flag(); // Check to see if we need to change the cache path if let Err(err) = self.update_cache().await { diff --git a/cli/lsp/repl.rs b/cli/lsp/repl.rs index 04905e3bd6..297764fcff 100644 --- a/cli/lsp/repl.rs +++ b/cli/lsp/repl.rs @@ -300,6 +300,7 @@ pub fn get_repl_workspace_settings() -> WorkspaceSettings { import_map: None, code_lens: Default::default(), internal_debug: false, + internal_inspect: Default::default(), log_file: false, lint: false, document_preload_limit: 0, // don't pre-load any modules as it's expensive and not useful for the repl diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 2e1189e753..c3d16f038a 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -36,6 +36,7 @@ use deno_ast::MediaType; use deno_core::anyhow::anyhow; use deno_core::error::custom_error; use deno_core::error::AnyError; +use deno_core::futures::FutureExt; use deno_core::located_script_name; use deno_core::op2; use deno_core::parking_lot::Mutex; @@ -51,7 +52,9 @@ use deno_core::v8; use deno_core::JsRuntime; use deno_core::ModuleSpecifier; use deno_core::OpState; +use deno_core::PollEventLoopOptions; use deno_core::RuntimeOptions; +use deno_runtime::inspector_server::InspectorServer; use deno_runtime::tokio_util::create_basic_runtime; use lazy_regex::lazy_regex; use log::error; @@ -63,13 +66,16 @@ use serde_repr::Serialize_repr; use std::cmp; use std::collections::HashMap; use std::collections::HashSet; +use std::net::SocketAddr; use std::ops::Range; use std::path::Path; +use std::rc::Rc; use std::sync::Arc; use std::thread; use text_size::TextRange; use text_size::TextSize; use tokio::sync::mpsc; +use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::oneshot; use tokio_util::sync::CancellationToken; use tower_lsp::jsonrpc::Error as LspError; @@ -208,46 +214,70 @@ fn normalize_diagnostic( Ok(()) } -#[derive(Clone, Debug)] pub struct TsServer { performance: Arc, + cache: Arc, sender: mpsc::UnboundedSender, + receiver: Mutex>>, specifier_map: Arc, + inspector_server: Mutex>>, +} + +impl std::fmt::Debug for TsServer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TsServer") + .field("performance", &self.performance) + .field("cache", &self.cache) + .field("sender", &self.sender) + .field("receiver", &self.receiver) + .field("specifier_map", &self.specifier_map) + .field("inspector_server", &self.inspector_server.lock().is_some()) + .finish() + } } impl TsServer { pub fn new(performance: Arc, cache: Arc) -> Self { - let specifier_map = Arc::new(TscSpecifierMap::new()); - let specifier_map_ = specifier_map.clone(); - let (tx, mut rx) = mpsc::unbounded_channel::(); - let perf = performance.clone(); - let _join_handle = thread::spawn(move || { - let mut ts_runtime = js_runtime(perf, cache, specifier_map_); - - let runtime = create_basic_runtime(); - runtime.block_on(async { - start_tsc(&mut ts_runtime, false).unwrap(); - - while let Some((req, state_snapshot, tx, token)) = rx.recv().await { - let value = - request(&mut ts_runtime, state_snapshot, req, token.clone()); - let was_sent = tx.send(value).is_ok(); - // Don't print the send error if the token is cancelled, it's expected - // to fail in that case and this commonly occurs. - if !was_sent && !token.is_cancelled() { - lsp_warn!("Unable to send result to client."); - } - } - }) - }); - + let (tx, request_rx) = mpsc::unbounded_channel::(); Self { performance, + cache, sender: tx, - specifier_map, + receiver: Mutex::new(Some(request_rx)), + specifier_map: Arc::new(TscSpecifierMap::new()), + inspector_server: Mutex::new(None), } } + pub fn start(&self, inspector_server_addr: Option) { + let maybe_inspector_server = inspector_server_addr.and_then(|addr| { + let addr: SocketAddr = match addr.parse() { + Ok(addr) => addr, + Err(err) => { + lsp_warn!("Invalid inspector server address \"{}\": {}", &addr, err); + return None; + } + }; + Some(Arc::new(InspectorServer::new(addr, "deno-lsp-tsc"))) + }); + *self.inspector_server.lock() = maybe_inspector_server.clone(); + // TODO(bartlomieju): why is the join_handle ignored here? Should we store it + // on the `TsServer` struct. + let receiver = self.receiver.lock().take().unwrap(); + let performance = self.performance.clone(); + let cache = self.cache.clone(); + let specifier_map = self.specifier_map.clone(); + let _join_handle = thread::spawn(move || { + run_tsc_thread( + receiver, + performance.clone(), + cache.clone(), + specifier_map.clone(), + maybe_inspector_server, + ) + }); + } + pub async fn get_diagnostics( &self, snapshot: Arc, @@ -4028,19 +4058,74 @@ fn op_script_version( Ok(r) } -/// Create and setup a JsRuntime based on a snapshot. It is expected that the -/// supplied snapshot is an isolate that contains the TypeScript language -/// server. -fn js_runtime( +fn run_tsc_thread( + mut request_rx: UnboundedReceiver, performance: Arc, cache: Arc, specifier_map: Arc, -) -> JsRuntime { - JsRuntime::new(RuntimeOptions { + maybe_inspector_server: Option>, +) { + let has_inspector_server = maybe_inspector_server.is_some(); + // Create and setup a JsRuntime based on a snapshot. It is expected that the + // supplied snapshot is an isolate that contains the TypeScript language + // server. + let mut tsc_runtime = JsRuntime::new(RuntimeOptions { extensions: vec![deno_tsc::init_ops(performance, cache, specifier_map)], startup_snapshot: Some(tsc::compiler_snapshot()), + inspector: maybe_inspector_server.is_some(), ..Default::default() - }) + }); + + if let Some(server) = maybe_inspector_server { + server.register_inspector( + "ext:deno_tsc/99_main_compiler.js".to_string(), + &mut tsc_runtime, + false, + ); + } + + let tsc_future = async { + start_tsc(&mut tsc_runtime, false).unwrap(); + let (request_signal_tx, mut request_signal_rx) = mpsc::unbounded_channel::<()>(); + let tsc_runtime = Rc::new(tokio::sync::Mutex::new(tsc_runtime)); + let tsc_runtime_ = tsc_runtime.clone(); + let event_loop_fut = async { + loop { + if has_inspector_server { + tsc_runtime_.lock().await.run_event_loop(PollEventLoopOptions { + wait_for_inspector: false, + pump_v8_message_loop: true, + }).await.ok(); + } + request_signal_rx.recv_many(&mut vec![], 1000).await; + } + }; + tokio::pin!(event_loop_fut); + loop { + tokio::select! { + biased; + (maybe_request, mut tsc_runtime) = async { (request_rx.recv().await, tsc_runtime.lock().await) } => { + if let Some((req, state_snapshot, tx, token)) = maybe_request { + let value = request(&mut tsc_runtime, state_snapshot, req, token.clone()); + request_signal_tx.send(()).unwrap(); + let was_sent = tx.send(value).is_ok(); + // Don't print the send error if the token is cancelled, it's expected + // to fail in that case and this commonly occurs. + if !was_sent && !token.is_cancelled() { + lsp_warn!("Unable to send result to client."); + } + } else { + break; + } + }, + _ = &mut event_loop_fut => {} + } + } + } + .boxed_local(); + + let runtime = create_basic_runtime(); + runtime.block_on(tsc_future) } deno_core::extension!(deno_tsc, @@ -4531,6 +4616,7 @@ mod tests { let snapshot = Arc::new(mock_state_snapshot(sources, &location)); let performance = Arc::new(Performance::default()); let ts_server = TsServer::new(performance, cache.clone()); + ts_server.start(None); let ts_config = TsConfig::new(config); assert!(ts_server .configure(snapshot.clone(), ts_config,)