mirror of
https://github.com/denoland/deno.git
synced 2024-12-23 15:49:44 -05:00
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:
parent
f86456fc26
commit
cdbf902499
5 changed files with 158 additions and 33 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
152
cli/lsp/tsc.rs
152
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,)
|
||||
|
|
Loading…
Reference in a new issue