1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

refactor(lsp): Have JS drive TSC event loop in LSP (#23565)

This commit is contained in:
Nathan Whitaker 2024-05-09 13:49:10 -07:00 committed by GitHub
parent 439b3b8db9
commit 19c0633a94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 333 additions and 180 deletions

View file

@ -8,6 +8,7 @@ use super::documents::DocumentsFilter;
use super::language_server;
use super::language_server::StateSnapshot;
use super::performance::Performance;
use super::performance::PerformanceMark;
use super::refactor::RefactorCodeActionData;
use super::refactor::ALL_KNOWN_REFACTOR_ACTION_KINDS;
use super::refactor::EXTRACT_CONSTANT;
@ -28,16 +29,19 @@ use crate::tsc::ResolveArgs;
use crate::tsc::MISSING_DEPENDENCY_SPECIFIER;
use crate::util::path::relative_specifier;
use crate::util::path::to_percent_decoded_str;
use crate::util::result::InfallibleResultExt;
use crate::util::v8::convert;
use deno_core::convert::Smi;
use deno_core::convert::ToV8;
use deno_core::error::StdAnyError;
use deno_runtime::fs_util::specifier_to_file_path;
use dashmap::DashMap;
use deno_ast::MediaType;
use deno_core::anyhow::anyhow;
use deno_core::anyhow::Context as _;
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;
use deno_core::resolve_url;
@ -63,9 +67,11 @@ use regex::Captures;
use regex::Regex;
use serde_repr::Deserialize_repr;
use serde_repr::Serialize_repr;
use std::cell::RefCell;
use std::cmp;
use std::collections::HashMap;
use std::collections::HashSet;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::ops::Range;
use std::path::Path;
@ -244,6 +250,16 @@ pub enum ChangeKind {
Closed = 2,
}
impl<'a> ToV8<'a> for ChangeKind {
type Error = Infallible;
fn to_v8(
self,
scope: &mut v8::HandleScope<'a>,
) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
Smi(self as u8).to_v8(scope)
}
}
impl Serialize for ChangeKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@ -260,15 +276,28 @@ pub struct PendingChange {
pub config_changed: bool,
}
impl PendingChange {
fn to_v8<'s>(
&self,
scope: &mut v8::HandleScope<'s>,
) -> Result<v8::Local<'s, v8::Value>, AnyError> {
let modified_scripts = serde_v8::to_v8(scope, &self.modified_scripts)?;
impl<'a> ToV8<'a> for PendingChange {
type Error = Infallible;
fn to_v8(
self,
scope: &mut v8::HandleScope<'a>,
) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
let modified_scripts = {
let mut modified_scripts_v8 =
Vec::with_capacity(self.modified_scripts.len());
for (specifier, kind) in &self.modified_scripts {
let specifier = v8::String::new(scope, specifier).unwrap().into();
let kind = kind.to_v8(scope).unwrap_infallible();
let pair =
v8::Array::new_with_elements(scope, &[specifier, kind]).into();
modified_scripts_v8.push(pair);
}
v8::Array::new_with_elements(scope, &modified_scripts_v8).into()
};
let project_version =
v8::Integer::new_from_unsigned(scope, self.project_version as u32).into();
let config_changed = v8::Boolean::new(scope, self.config_changed).into();
Ok(
v8::Array::new_with_elements(
scope,
@ -277,7 +306,9 @@ impl PendingChange {
.into(),
)
}
}
impl PendingChange {
fn coalesce(
&mut self,
new_version: usize,
@ -1068,6 +1099,7 @@ impl TsServer {
let droppable_token = DroppableToken(token.clone());
let (tx, mut rx) = oneshot::channel::<Result<String, AnyError>>();
let change = self.pending_change.lock().take();
if self
.sender
.send((req, snapshot, tx, token.clone(), change))
@ -3951,10 +3983,12 @@ struct State {
last_id: usize,
performance: Arc<Performance>,
// the response from JS, as a JSON string
response: Option<String>,
response_tx: Option<oneshot::Sender<Result<String, AnyError>>>,
state_snapshot: Arc<StateSnapshot>,
specifier_map: Arc<TscSpecifierMap>,
token: CancellationToken,
pending_requests: Option<UnboundedReceiver<Request>>,
mark: Option<PerformanceMark>,
}
impl State {
@ -3962,14 +3996,17 @@ impl State {
state_snapshot: Arc<StateSnapshot>,
specifier_map: Arc<TscSpecifierMap>,
performance: Arc<Performance>,
pending_requests: UnboundedReceiver<Request>,
) -> Self {
Self {
last_id: 1,
performance,
response: None,
response_tx: None,
state_snapshot,
specifier_map,
token: Default::default(),
mark: None,
pending_requests: Some(pending_requests),
}
}
@ -4090,6 +4127,75 @@ fn op_resolve(
op_resolve_inner(state, ResolveArgs { base, specifiers })
}
struct TscRequestArray {
request: TscRequest,
id: Smi<usize>,
change: convert::OptionNull<PendingChange>,
}
impl<'a> ToV8<'a> for TscRequestArray {
type Error = StdAnyError;
fn to_v8(
self,
scope: &mut v8::HandleScope<'a>,
) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
let id = self.id.to_v8(scope).unwrap_infallible();
let (method_name, args) = self.request.to_server_request(scope)?;
let method_name = deno_core::FastString::from_static(method_name)
.v8_string(scope)
.into();
let args = args.unwrap_or_else(|| v8::Array::new(scope, 0).into());
let change = self.change.to_v8(scope).unwrap_infallible();
Ok(
v8::Array::new_with_elements(scope, &[id, method_name, args, change])
.into(),
)
}
}
#[op2(async)]
#[to_v8]
async fn op_poll_requests(
state: Rc<RefCell<OpState>>,
) -> convert::OptionNull<TscRequestArray> {
let mut pending_requests = {
let mut state = state.borrow_mut();
let state = state.try_borrow_mut::<State>().unwrap();
state.pending_requests.take().unwrap()
};
let Some((request, snapshot, response_tx, token, change)) =
pending_requests.recv().await
else {
return None.into();
};
let mut state = state.borrow_mut();
let state = state.try_borrow_mut::<State>().unwrap();
state.pending_requests = Some(pending_requests);
state.state_snapshot = snapshot;
state.token = token;
state.response_tx = Some(response_tx);
let id = state.last_id;
state.last_id += 1;
let mark = state
.performance
.mark_with_args(format!("tsc.host.{}", request.method()), &request);
state.mark = Some(mark);
Some(TscRequestArray {
request,
id: Smi(id),
change: change.into(),
})
.into()
}
#[inline]
fn op_resolve_inner(
state: &mut OpState,
@ -4118,9 +4224,25 @@ fn op_resolve_inner(
}
#[op2(fast)]
fn op_respond(state: &mut OpState, #[string] response: String) {
fn op_respond(
state: &mut OpState,
#[string] response: String,
#[string] error: String,
) {
let state = state.borrow_mut::<State>();
state.response = Some(response);
state.performance.measure(state.mark.take().unwrap());
let response = if !error.is_empty() {
Err(anyhow!("tsc error: {error}"))
} else {
Ok(response)
};
let was_sent = state.response_tx.take().unwrap().send(response).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 && !state.token.is_cancelled() {
lsp_warn!("Unable to send result to client.");
}
}
#[op2]
@ -4216,121 +4338,36 @@ fn op_project_version(state: &mut OpState) -> usize {
struct TscRuntime {
js_runtime: JsRuntime,
server_request_fn_global: v8::Global<v8::Function>,
server_main_loop_fn_global: v8::Global<v8::Function>,
}
impl TscRuntime {
fn new(mut js_runtime: JsRuntime) -> Self {
let server_request_fn_global = {
let server_main_loop_fn_global = {
let context = js_runtime.main_context();
let scope = &mut js_runtime.handle_scope();
let context_local = v8::Local::new(scope, context);
let global_obj = context_local.global(scope);
let server_request_fn_str =
v8::String::new_external_onebyte_static(scope, b"serverRequest")
let server_main_loop_fn_str =
v8::String::new_external_onebyte_static(scope, b"serverMainLoop")
.unwrap();
let server_request_fn = v8::Local::try_from(
global_obj.get(scope, server_request_fn_str.into()).unwrap(),
let server_main_loop_fn = v8::Local::try_from(
global_obj
.get(scope, server_main_loop_fn_str.into())
.unwrap(),
)
.unwrap();
v8::Global::new(scope, server_request_fn)
v8::Global::new(scope, server_main_loop_fn)
};
Self {
server_request_fn_global,
server_main_loop_fn_global,
js_runtime,
}
}
/// Send a request into the runtime and return the JSON string containing the response.
fn request(
&mut self,
state_snapshot: Arc<StateSnapshot>,
request: TscRequest,
change: Option<PendingChange>,
token: CancellationToken,
) -> Result<String, AnyError> {
if token.is_cancelled() {
return Err(anyhow!("Operation was cancelled."));
}
let (performance, id) = {
let op_state = self.js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
let state = op_state.borrow_mut::<State>();
state.state_snapshot = state_snapshot;
state.token = token;
state.last_id += 1;
let id = state.last_id;
(state.performance.clone(), id)
};
let mark = performance
.mark_with_args(format!("tsc.host.{}", request.method()), &request);
{
let scope = &mut self.js_runtime.handle_scope();
let tc_scope = &mut v8::TryCatch::new(scope);
let server_request_fn =
v8::Local::new(tc_scope, &self.server_request_fn_global);
let undefined = v8::undefined(tc_scope).into();
let change = if let Some(change) = change {
change.to_v8(tc_scope)?
} else {
v8::null(tc_scope).into()
};
let (method, req_args) = request.to_server_request(tc_scope)?;
let args = vec![
v8::Integer::new(tc_scope, id as i32).into(),
v8::String::new(tc_scope, method).unwrap().into(),
req_args.unwrap_or_else(|| v8::Array::new(tc_scope, 0).into()),
change,
];
server_request_fn.call(tc_scope, undefined, &args);
if tc_scope.has_caught() && !tc_scope.has_terminated() {
if let Some(stack_trace) = tc_scope.stack_trace() {
lsp_warn!(
"Error during TS request \"{method}\":\n {}",
stack_trace.to_rust_string_lossy(tc_scope),
);
} else if let Some(message) = tc_scope.message() {
lsp_warn!(
"Error during TS request \"{method}\":\n {}\n {}",
message.get(tc_scope).to_rust_string_lossy(tc_scope),
tc_scope
.exception()
.map(|exc| exc.to_rust_string_lossy(tc_scope))
.unwrap_or_default()
);
} else {
lsp_warn!(
"Error during TS request \"{method}\":\n {}",
tc_scope
.exception()
.map(|exc| exc.to_rust_string_lossy(tc_scope))
.unwrap_or_default(),
);
}
tc_scope.rethrow();
}
}
let op_state = self.js_runtime.op_state();
let mut op_state = op_state.borrow_mut();
let state = op_state.borrow_mut::<State>();
performance.measure(mark);
state.response.take().ok_or_else(|| {
custom_error(
"RequestError",
"The response was not received for the request.",
)
})
}
}
fn run_tsc_thread(
mut request_rx: UnboundedReceiver<Request>,
request_rx: UnboundedReceiver<Request>,
performance: Arc<Performance>,
specifier_map: Arc<TscSpecifierMap>,
maybe_inspector_server: Option<Arc<InspectorServer>>,
@ -4340,9 +4377,13 @@ fn run_tsc_thread(
// 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, specifier_map)],
extensions: vec![deno_tsc::init_ops(
performance,
specifier_map,
request_rx,
)],
startup_snapshot: Some(tsc::compiler_snapshot()),
inspector: maybe_inspector_server.is_some(),
inspector: has_inspector_server,
..Default::default()
});
@ -4355,40 +4396,53 @@ fn run_tsc_thread(
}
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(TscRuntime::new(tsc_runtime)));
// start_tsc(&mut tsc_runtime, false).unwrap();
let tsc_runtime =
Rc::new(tokio::sync::Mutex::new(TscRuntime::new(tsc_runtime)));
let tsc_runtime_ = tsc_runtime.clone();
let event_loop_fut = async {
loop {
if has_inspector_server {
tsc_runtime_.lock().await.js_runtime.run_event_loop(PollEventLoopOptions {
if let Err(e) = tsc_runtime_
.lock()
.await
.js_runtime
.run_event_loop(PollEventLoopOptions {
wait_for_inspector: false,
pump_v8_message_loop: true,
}).await.ok();
})
.await
{
log::error!("Error in TSC event loop: {e}");
}
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, pending_change)) = maybe_request {
let value = tsc_runtime.request(state_snapshot, req, pending_change, 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 => {}
let main_loop_fut = {
let enable_debug = std::env::var("DENO_TSC_DEBUG")
.map(|s| {
let s = s.trim();
s == "1" || s.eq_ignore_ascii_case("true")
})
.unwrap_or(false);
let mut runtime = tsc_runtime.lock().await;
let main_loop = runtime.server_main_loop_fn_global.clone();
let args = {
let scope = &mut runtime.js_runtime.handle_scope();
let enable_debug_local =
v8::Local::<v8::Value>::from(v8::Boolean::new(scope, enable_debug));
[v8::Global::new(scope, enable_debug_local)]
};
runtime.js_runtime.call_with_args(&main_loop, &args)
};
tokio::select! {
biased;
_ = event_loop_fut => {},
res = main_loop_fut => {
if let Err(err) = res {
log::error!("Error in TSC main loop: {err}");
}
}
}
}
@ -4410,30 +4464,23 @@ deno_core::extension!(deno_tsc,
op_script_version,
op_ts_config,
op_project_version,
op_poll_requests,
],
options = {
performance: Arc<Performance>,
specifier_map: Arc<TscSpecifierMap>,
request_rx: UnboundedReceiver<Request>,
},
state = |state, options| {
state.put(State::new(
Default::default(),
options.specifier_map,
options.performance,
options.request_rx,
));
},
);
/// Instruct a language server runtime to start the language server and provide
/// it with a minimal bootstrap configuration.
fn start_tsc(runtime: &mut JsRuntime, debug: bool) -> Result<(), AnyError> {
let init_config = json!({ "debug": debug });
let init_src = format!("globalThis.serverInit({init_config});");
runtime.execute_script(located_script_name!(), init_src)?;
Ok(())
}
#[derive(Debug, Deserialize_repr, Serialize_repr)]
#[repr(u32)]
pub enum CompletionTriggerKind {
@ -5123,8 +5170,9 @@ mod tests {
}
fn setup_op_state(state_snapshot: Arc<StateSnapshot>) -> OpState {
let (_tx, rx) = mpsc::unbounded_channel();
let state =
State::new(state_snapshot, Default::default(), Default::default());
State::new(state_snapshot, Default::default(), Default::default(), rx);
let mut op_state = OpState::new(None);
op_state.put(state);
op_state

View file

@ -1079,19 +1079,69 @@ delete Object.prototype.__proto__;
/**
* @param {number} _id
* @param {any} data
* @param {any | null} error
*/
// TODO(bartlomieju): this feels needlessly generic, both type chcking
// and language server use it with inefficient serialization. Id is not used
// anyway...
function respond(_id, data = null) {
ops.op_respond(JSON.stringify(data));
function respond(_id, data = null, error = null) {
if (error) {
ops.op_respond(
"error",
"stack" in error ? error.stack.toString() : error.toString(),
);
} else {
ops.op_respond(JSON.stringify(data), "");
}
}
/** @typedef {[[string, number][], number, boolean] } PendingChange */
/**
* @template T
* @typedef {T | null} Option<T> */
/** @returns {Promise<[number, string, any[], Option<PendingChange>] | null>} */
async function pollRequests() {
return await ops.op_poll_requests();
}
let hasStarted = false;
/** @param {boolean} enableDebugLogging */
async function serverMainLoop(enableDebugLogging) {
if (hasStarted) {
throw new Error("The language server has already been initialized.");
}
hasStarted = true;
languageService = ts.createLanguageService(host, documentRegistry);
setLogDebug(enableDebugLogging, "TSLS");
debug("serverInit()");
while (true) {
const request = await pollRequests();
if (request === null) {
break;
}
try {
serverRequest(request[0], request[1], request[2], request[3]);
} catch (err) {
const reqString = "[" + request.map((v) =>
JSON.stringify(v)
).join(", ") + "]";
error(
`Error occurred processing request ${reqString} : ${
"stack" in err ? err.stack : err
}`,
);
}
}
}
/**
* @param {number} id
* @param {string} method
* @param {any[]} args
* @param {[[string, number][], number, boolean] | null} maybeChange
* @param {PendingChange | null} maybeChange
*/
function serverRequest(id, method, args, maybeChange) {
if (logDebug) {
@ -1160,11 +1210,7 @@ delete Object.prototype.__proto__;
if (
!isCancellationError(e)
) {
if ("stack" in e) {
error(e.stack);
} else {
error(e);
}
respond(id, {}, e);
throw e;
}
return respond(id, {});
@ -1181,11 +1227,7 @@ delete Object.prototype.__proto__;
return respond(id, languageService[method](...args));
} catch (e) {
if (!isCancellationError(e)) {
if ("stack" in e) {
error(e.stack);
} else {
error(e);
}
respond(id, null, e);
throw e;
}
return respond(id);
@ -1198,18 +1240,6 @@ delete Object.prototype.__proto__;
}
}
let hasStarted = false;
/** @param {{ debug: boolean; }} init */
function serverInit({ debug: debugFlag }) {
if (hasStarted) {
throw new Error("The language server has already been initialized.");
}
hasStarted = true;
languageService = ts.createLanguageService(host, documentRegistry);
setLogDebug(debugFlag, "TSLS");
debug("serverInit()");
}
// A build time only op that provides some setup information that is used to
// ensure the snapshot is setup properly.
/** @type {{ buildSpecifier: string; libs: string[]; nodeBuiltInModuleNames: string[] }} */
@ -1300,6 +1330,5 @@ delete Object.prototype.__proto__;
// exposes the functions that are called when the compiler is used as a
// language service.
global.serverInit = serverInit;
global.serverRequest = serverRequest;
global.serverMainLoop = serverMainLoop;
})(this);

View file

@ -12,6 +12,7 @@ pub mod gitignore;
pub mod logger;
pub mod path;
pub mod progress_bar;
pub mod result;
pub mod sync;
pub mod text_encoding;
pub mod time;

16
cli/util/result.rs Normal file
View file

@ -0,0 +1,16 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use std::convert::Infallible;
pub trait InfallibleResultExt<T> {
fn unwrap_infallible(self) -> T;
}
impl<T> InfallibleResultExt<T> for Result<T, Infallible> {
fn unwrap_infallible(self) -> T {
match self {
Ok(value) => value,
Err(never) => match never {},
}
}
}

View file

@ -1,5 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
pub mod convert;
#[inline(always)]
pub fn get_v8_flags_from_env() -> Vec<String> {
std::env::var("DENO_V8_FLAGS")

57
cli/util/v8/convert.rs Normal file
View file

@ -0,0 +1,57 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use deno_core::v8;
use deno_core::FromV8;
use deno_core::ToV8;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// A wrapper type for `Option<T>` that (de)serializes `None` as `null`
#[repr(transparent)]
pub struct OptionNull<T>(pub Option<T>);
impl<T> From<Option<T>> for OptionNull<T> {
fn from(option: Option<T>) -> Self {
Self(option)
}
}
impl<T> From<OptionNull<T>> for Option<T> {
fn from(value: OptionNull<T>) -> Self {
value.0
}
}
impl<'a, T> ToV8<'a> for OptionNull<T>
where
T: ToV8<'a>,
{
type Error = T::Error;
fn to_v8(
self,
scope: &mut v8::HandleScope<'a>,
) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
match self.0 {
Some(value) => value.to_v8(scope),
None => Ok(v8::null(scope).into()),
}
}
}
impl<'a, T> FromV8<'a> for OptionNull<T>
where
T: FromV8<'a>,
{
type Error = T::Error;
fn from_v8(
scope: &mut v8::HandleScope<'a>,
value: v8::Local<'a, v8::Value>,
) -> Result<Self, Self::Error> {
if value.is_null() {
Ok(OptionNull(None))
} else {
T::from_v8(scope, value).map(|v| OptionNull(Some(v)))
}
}
}

View file

@ -895,7 +895,7 @@ fn repl_with_quiet_flag() {
assert!(!out.contains("Deno"));
assert!(!out.contains("exit using ctrl+d, ctrl+c, or close()"));
assert_ends_with!(out, "\"done\"\n");
assert!(err.is_empty());
assert!(err.is_empty(), "Error: {}", err);
}
#[test]
@ -959,7 +959,7 @@ fn npm_packages() {
);
assert_contains!(out, "hello");
assert!(err.is_empty());
assert!(err.is_empty(), "Error: {}", err);
}
{
@ -975,7 +975,7 @@ fn npm_packages() {
);
assert_contains!(out, "hello");
assert!(err.is_empty());
assert!(err.is_empty(), "Error: {}", err);
}
{
@ -989,7 +989,7 @@ fn npm_packages() {
assert_contains!(out, "[Module: null prototype] {");
assert_contains!(out, "Chalk: [class Chalk],");
assert!(err.is_empty());
assert!(err.is_empty(), "Error: {}", err);
}
{
@ -1005,7 +1005,7 @@ fn npm_packages() {
out,
"error: npm package 'asdfawe52345asdf' does not exist"
);
assert!(err.is_empty());
assert!(err.is_empty(), "Error: {}", err);
}
{
@ -1021,7 +1021,7 @@ fn npm_packages() {
);
assert_contains!(out, "no");
assert!(err.is_empty());
assert!(err.is_empty(), "Error: {}", err);
}
}