summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2023-12-22 02:04:02 +0100
committerGitHub <noreply@github.com>2023-12-22 02:04:02 +0100
commitcdbf902499dc62a4fb9f8cbf2a47a7b446623929 (patch)
treeacded191f368894e02c024f39946b1e7cb0972a4
parentf86456fc26d1c02f6c511125037efed576f87458 (diff)
feat(lsp): allow to connect V8 inspector (#21482)
This commit adds a way to connect to the TS compiler host that is run as part of the "deno lsp" subcommand. This can be done by specifying "DENO_LSP_INSPECTOR" variable. --------- Co-authored-by: Nayeem Rahman <nayeemrmn99@gmail.com>
-rw-r--r--cli/lsp/config.rs33
-rw-r--r--cli/lsp/diagnostics.rs1
-rw-r--r--cli/lsp/language_server.rs4
-rw-r--r--cli/lsp/repl.rs1
-rw-r--r--cli/lsp/tsc.rs152
5 files changed, 158 insertions, 33 deletions
diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs
index 42478b593..717508752 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<String> {
+ 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 8034127e9..4bec3083d 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 2d3864e0a..e730e145f 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -1226,6 +1226,10 @@ impl Inner {
self.config.update_capabilities(&params.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 04905e3bd..297764fcf 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 2e1189e75..c3d16f038 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<Performance>,
+ cache: Arc<dyn HttpCache>,
sender: mpsc::UnboundedSender<Request>,
+ receiver: Mutex<Option<mpsc::UnboundedReceiver<Request>>>,
specifier_map: Arc<TscSpecifierMap>,
+ inspector_server: Mutex<Option<Arc<InspectorServer>>>,
+}
+
+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<Performance>, cache: Arc<dyn HttpCache>) -> Self {
- let specifier_map = Arc::new(TscSpecifierMap::new());
- let specifier_map_ = specifier_map.clone();
- let (tx, mut rx) = mpsc::unbounded_channel::<Request>();
- 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::<Request>();
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<String>) {
+ 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<StateSnapshot>,
@@ -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<Request>,
performance: Arc<Performance>,
cache: Arc<dyn HttpCache>,
specifier_map: Arc<TscSpecifierMap>,
-) -> JsRuntime {
- JsRuntime::new(RuntimeOptions {
+ maybe_inspector_server: Option<Arc<InspectorServer>>,
+) {
+ 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,)