Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): cache jsxImportSource automatically #21687

Merged
merged 7 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 83 additions & 7 deletions cli/lsp/language_server.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use base64::Engine;
use deno_ast::MediaType;
use deno_core::anyhow::anyhow;
use deno_core::anyhow::Context;
Expand Down Expand Up @@ -30,6 +31,9 @@ use std::env;
use std::fmt::Write as _;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::mpsc::unbounded_channel;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::mpsc::UnboundedSender;
use tokio_util::sync::CancellationToken;
use tower_lsp::jsonrpc::Error as LspError;
use tower_lsp::jsonrpc::Result as LspResult;
Expand Down Expand Up @@ -177,6 +181,44 @@ pub struct StateSnapshot {
pub npm: Option<StateNpmSnapshot>,
}

type LanguageServerTaskFn = Box<dyn FnOnce(LanguageServer) + Send + Sync>;

/// Used to queue tasks from inside of the language server lock that must be
/// commenced from outside of it. For example, queue a request to cache a module
/// after having loaded a config file which references it.
#[derive(Debug)]
struct LanguageServerTaskQueue {
nayeemrmn marked this conversation as resolved.
Show resolved Hide resolved
task_tx: UnboundedSender<LanguageServerTaskFn>,
/// This is moved out to its own task after initializing.
task_rx: Option<UnboundedReceiver<LanguageServerTaskFn>>,
}

impl Default for LanguageServerTaskQueue {
fn default() -> Self {
let (task_tx, task_rx) = unbounded_channel();
Self {
task_tx,
task_rx: Some(task_rx),
}
}
}

impl LanguageServerTaskQueue {
fn queue_task(&self, task_fn: LanguageServerTaskFn) -> bool {
self.task_tx.send(task_fn).is_ok()
}

/// Panics if called more than once.
fn start(&mut self, ls: LanguageServer) {
let mut task_rx = self.task_rx.take().unwrap();
spawn(async move {
while let Some(task_fn) = task_rx.recv().await {
task_fn(ls.clone());
}
});
}
}

#[derive(Debug)]
pub struct Inner {
/// Cached versions of "fixed" assets that can either be inlined in Rust or
Expand All @@ -196,6 +238,7 @@ pub struct Inner {
/// on disk or "open" within the client.
pub documents: Documents,
http_client: Arc<HttpClient>,
task_queue: LanguageServerTaskQueue,
/// Handles module registries, which allow discovery of modules
module_registries: ModuleRegistry,
/// The path to the module registries cache
Expand Down Expand Up @@ -500,6 +543,7 @@ impl Inner {
maybe_import_map_uri: None,
maybe_package_json: None,
fmt_options: Default::default(),
task_queue: Default::default(),
lint_options: Default::default(),
maybe_testing_server: None,
module_registries,
Expand Down Expand Up @@ -1023,6 +1067,41 @@ impl Inner {
self.lint_options = lint_options;
self.fmt_options = fmt_options;
self.recreate_http_client_and_dependents().await?;
if let Some(config_file) = self.config.maybe_config_file() {
if let Ok((compiler_options, _)) = config_file.to_compiler_options() {
if let Some(compiler_options_obj) = compiler_options.as_object() {
if let Some(jsx_import_source) =
Comment on lines +1070 to +1073
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, but all of this should be simplified using let else

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably it could just be extracted out to a separate method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case IMO it's more readable like this, I don't want to add more methods for this because it's going to be moved for #21029.

compiler_options_obj.get("jsxImportSource")
{
if let Some(jsx_import_source) = jsx_import_source.as_str() {
let cache_params = lsp_custom::CacheParams {
referrer: TextDocumentIdentifier {
uri: config_file.specifier.clone(),
},
uris: vec![TextDocumentIdentifier {
uri: Url::parse(&format!(
"data:application/typescript;base64,{}",
base64::engine::general_purpose::STANDARD.encode(
format!("import '{jsx_import_source}/jsx-runtime';")
)
))
.unwrap(),
}],
};
self.task_queue.queue_task(Box::new(|ls: LanguageServer| {
spawn(async move {
if let Err(err) =
ls.cache_request(Some(json!(cache_params))).await
{
lsp_warn!("{}", err);
}
});
nayeemrmn marked this conversation as resolved.
Show resolved Hide resolved
}));
}
}
}
}
}
}

Ok(())
Expand Down Expand Up @@ -3257,9 +3336,8 @@ impl tower_lsp::LanguageServer for LanguageServer {
ls.refresh_documents_config().await;
ls.diagnostics_server.invalidate_all();
ls.send_diagnostics_update();
}

lsp_log!("Server ready.");
ls.task_queue.start(self.clone());
};

if upgrade_check_enabled() {
// spawn to avoid lsp send/sync requirement, but also just
Expand All @@ -3282,6 +3360,8 @@ impl tower_lsp::LanguageServer for LanguageServer {
}
});
}

lsp_log!("Server ready.");
}

async fn shutdown(&self) -> LspResult<()> {
Expand Down Expand Up @@ -3596,10 +3676,6 @@ impl Inner {
let referrer = self
.url_map
.normalize_url(&params.referrer.uri, LspUrlKind::File);
if !self.is_diagnosable(&referrer) {
return Ok(None);
}
nayeemrmn marked this conversation as resolved.
Show resolved Hide resolved

let mark = self.performance.mark_with_args("lsp.cache", &params);
let roots = if !params.uris.is_empty() {
params
Expand Down
55 changes: 55 additions & 0 deletions cli/tests/integration/lsp_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9469,6 +9469,61 @@ export function B() {
client.shutdown();
}

#[test]
fn lsp_jsx_import_source_config_file_automatic_cache() {
let context = TestContextBuilder::new()
.use_http_server()
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
temp_dir.write(
"deno.json",
json!({
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "http://localhost:4545/jsx",
},
})
.to_string(),
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
let mut diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.uri().join("file.tsx").unwrap(),
"languageId": "typescriptreact",
"version": 1,
"text": "
export function Foo() {
return <div></div>;
}
",
},
}));
// The caching is done on an asynchronous task spawned after init, so there's
// a chance it wasn't done in time and we need to wait for another batch of
// diagnostics.
while !diagnostics.all().is_empty() {
std::thread::sleep(std::time::Duration::from_millis(50));
// The post-cache diagnostics update triggers inconsistently on CI for some
// reason. Force it with this notification.
diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.uri().join("file.tsx").unwrap(),
"languageId": "typescriptreact",
"version": 1,
"text": "
export function Foo() {
return <div></div>;
}
",
},
}));
}
assert_eq!(diagnostics.all(), vec![]);
client.shutdown();
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct TestData {
Expand Down