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>
This commit is contained in:
Bartek Iwańczuk 2023-12-22 02:04:02 +01:00 committed by GitHub
parent f86456fc26
commit cdbf902499
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 158 additions and 33 deletions

View file

@ -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,

View file

@ -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
{

View file

@ -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 {

View file

@ -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

View file

@ -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,)