diff --git a/cli/lsp/client.rs b/cli/lsp/client.rs index b5cdf8eb9fd4d3..210aa1da326ebb 100644 --- a/cli/lsp/client.rs +++ b/cli/lsp/client.rs @@ -84,6 +84,19 @@ impl Client { }); } + pub fn send_did_change_deno_configuration_notification( + &self, + params: lsp_custom::DidChangeDenoConfigurationNotificationParams, + ) { + // do on a task in case the caller currently is in the lsp lock + let client = self.0.clone(); + spawn(async move { + client + .send_did_change_deno_configuration_notification(params) + .await; + }); + } + pub fn show_message( &self, message_type: lsp::MessageType, @@ -184,6 +197,10 @@ trait ClientTrait: Send + Sync { params: lsp_custom::DiagnosticBatchNotificationParams, ); async fn send_test_notification(&self, params: TestingNotification); + async fn send_did_change_deno_configuration_notification( + &self, + params: lsp_custom::DidChangeDenoConfigurationNotificationParams, + ); async fn specifier_configurations( &self, uris: Vec, @@ -259,6 +276,18 @@ impl ClientTrait for TowerClient { } } + async fn send_did_change_deno_configuration_notification( + &self, + params: lsp_custom::DidChangeDenoConfigurationNotificationParams, + ) { + self + .0 + .send_notification::( + params, + ) + .await + } + async fn specifier_configurations( &self, uris: Vec, @@ -371,6 +400,12 @@ impl ClientTrait for ReplClient { async fn send_test_notification(&self, _params: TestingNotification) {} + async fn send_did_change_deno_configuration_notification( + &self, + _params: lsp_custom::DidChangeDenoConfigurationNotificationParams, + ) { + } + async fn specifier_configurations( &self, uris: Vec, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index b19b00b4aa0103..f6edc10f282002 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -20,6 +20,7 @@ use deno_runtime::deno_node::PackageJson; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::RootCertStoreProvider; use import_map::ImportMap; +use indexmap::IndexSet; use log::error; use serde_json::from_value; use std::collections::HashMap; @@ -64,6 +65,7 @@ use super::documents::UpdateDocumentConfigOptions; use super::logging::lsp_log; use super::logging::lsp_warn; use super::lsp_custom; +use super::lsp_custom::TaskDefinition; use super::npm::CliNpmSearchApi; use super::parent_process_checker; use super::performance::Performance; @@ -343,8 +345,8 @@ impl LanguageServer { self.0.write().await.reload_import_registries().await } - pub async fn task_request(&self) -> LspResult> { - self.0.read().await.get_tasks() + pub async fn task_definitions(&self) -> LspResult> { + self.0.read().await.task_definitions() } pub async fn test_run_request( @@ -1458,7 +1460,7 @@ impl Inner { } } - fn has_config_changed(config: &Config, changes: &HashSet) -> bool { + fn has_config_changed(config: &Config, changes: &IndexSet) -> bool { // Check the canonicalized specifier here because file watcher // changes will be for the canonicalized path in vscode, but also check the // non-canonicalized specifier in order to please the tests and handle @@ -1509,31 +1511,91 @@ impl Inner { .performance .mark("did_change_watched_files", Some(¶ms)); let mut touched = false; - let changes: HashSet = params + let changes: IndexSet = params .changes .iter() .map(|f| self.url_map.normalize_url(&f.uri, LspUrlKind::File)) .collect(); + let mut config_changes = IndexSet::with_capacity(changes.len()); + // if the current deno.json has changed, we need to reload it if has_config_changed(&self.config, &changes) { + // Check the 'current' config specifier from both before and after it's + // updated. Check canonicalized and uncanonicalized variants for each. + // If any are included in `changes`, send our custom notification for + // `deno.json` changes: `deno/didChangeDenoConfigurationNotification`. + let mut files_to_check = IndexSet::with_capacity(4); + // Collect previous config specifiers. + if let Some(url) = self.config.maybe_config_file().map(|c| &c.specifier) { + files_to_check.insert(url.clone()); + } + if let Some(url) = self.config.maybe_config_file_canonicalized_specifier() + { + files_to_check.insert(url.clone()); + } + // Update config. if let Err(err) = self.update_config_file().await { self.client.show_message(MessageType::WARNING, err); } + // Collect new config specifiers. + if let Some(url) = self.config.maybe_config_file().map(|c| &c.specifier) { + files_to_check.insert(url.clone()); + } + if let Some(url) = self.config.maybe_config_file_canonicalized_specifier() + { + files_to_check.insert(url.clone()); + } + config_changes.extend( + params + .changes + .iter() + .filter(|e| files_to_check.contains(&e.uri)) + .map(|e| lsp_custom::DenoConfigurationChangeEvent { + file_event: e.clone(), + configuration_type: lsp_custom::DenoConfigurationType::DenoJson, + }), + ); if let Err(err) = self.update_tsconfig().await { self.client.show_message(MessageType::WARNING, err); } touched = true; } - if let Some(package_json) = &self.maybe_package_json { - // always update the package json if the deno config changes - if touched || changes.contains(&package_json.specifier()) { - if let Err(err) = self.update_package_json() { - self.client.show_message(MessageType::WARNING, err); - } - touched = true; + let has_package_json_changed = changes + .iter() + .any(|e| e.as_str().ends_with("/package.json")); + + if has_package_json_changed { + let mut files_to_check = IndexSet::with_capacity(2); + if let Some(package_json) = &self.maybe_package_json { + files_to_check.insert(package_json.specifier()); + } + if let Err(err) = self.update_package_json() { + self.client.show_message(MessageType::WARNING, err); } + if let Some(package_json) = &self.maybe_package_json { + files_to_check.insert(package_json.specifier()); + } + config_changes.extend( + params + .changes + .iter() + .filter(|e| files_to_check.contains(&e.uri)) + .map(|e| lsp_custom::DenoConfigurationChangeEvent { + file_event: e.clone(), + configuration_type: lsp_custom::DenoConfigurationType::PackageJson, + }), + ); + touched = true; + } + + if !config_changes.is_empty() { + self.client.send_did_change_deno_configuration_notification( + lsp_custom::DidChangeDenoConfigurationNotificationParams { + changes: config_changes.into_iter().collect(), + }, + ); } // if the current import map, or config file has changed, we need to @@ -3580,13 +3642,35 @@ impl Inner { json!({ "averages": averages }) } - fn get_tasks(&self) -> LspResult> { - Ok( - self - .config - .maybe_config_file() - .and_then(|cf| cf.to_lsp_tasks()), - ) + fn task_definitions(&self) -> LspResult> { + let mut result = vec![]; + if let Some(config_file) = self.config.maybe_config_file() { + if let Some(tasks) = json!(&config_file.json.tasks).as_object() { + for (name, value) in tasks { + let Some(command) = value.as_str() else { + continue; + }; + result.push(TaskDefinition { + name: name.clone(), + command: command.to_string(), + source_uri: config_file.specifier.clone(), + }); + } + }; + } + if let Some(package_json) = &self.maybe_package_json { + if let Some(scripts) = &package_json.scripts { + for (name, command) in scripts { + result.push(TaskDefinition { + name: name.clone(), + command: command.clone(), + source_uri: package_json.specifier(), + }); + } + } + } + result.sort_by_key(|d| d.name.clone()); + Ok(result) } async fn inlay_hint( diff --git a/cli/lsp/lsp_custom.rs b/cli/lsp/lsp_custom.rs index d9dbae8a45f4b1..685da6b14ae957 100644 --- a/cli/lsp/lsp_custom.rs +++ b/cli/lsp/lsp_custom.rs @@ -6,7 +6,7 @@ use tower_lsp::lsp_types as lsp; pub const CACHE_REQUEST: &str = "deno/cache"; pub const PERFORMANCE_REQUEST: &str = "deno/performance"; -pub const TASK_REQUEST: &str = "deno/task"; +pub const TASK_REQUEST: &str = "deno/taskDefinitions"; pub const RELOAD_IMPORT_REGISTRIES_REQUEST: &str = "deno/reloadImportRegistries"; pub const VIRTUAL_TEXT_DOCUMENT: &str = "deno/virtualTextDocument"; @@ -24,6 +24,16 @@ pub struct CacheParams { pub uris: Vec, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskDefinition { + pub name: String, + // TODO(nayeemrmn): Rename this to `command` in vscode_deno. + #[serde(rename = "detail")] + pub command: String, + pub source_uri: lsp::Url, +} + #[derive(Debug, Deserialize, Serialize)] pub struct RegistryStateNotificationParams { pub origin: String, @@ -50,6 +60,37 @@ pub struct DiagnosticBatchNotificationParams { pub messages_len: usize, } +#[derive(Debug, Eq, Hash, PartialEq, Copy, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DenoConfigurationType { + DenoJson, + PackageJson, +} + +#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DenoConfigurationChangeEvent { + #[serde(flatten)] + pub file_event: lsp::FileEvent, + pub configuration_type: DenoConfigurationType, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DidChangeDenoConfigurationNotificationParams { + pub changes: Vec, +} + +pub enum DidChangeDenoConfigurationNotification {} + +impl lsp::notification::Notification + for DidChangeDenoConfigurationNotification +{ + type Params = DidChangeDenoConfigurationNotificationParams; + + const METHOD: &'static str = "deno/didChangeDenoConfiguration"; +} + /// This notification is only sent for testing purposes /// in order to know what the latest diagnostics are. pub enum DiagnosticBatchNotification {} diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index c2f3eda717f6d8..af6ef88c91b55a 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -56,7 +56,10 @@ pub async fn start() -> Result<(), AnyError> { lsp_custom::RELOAD_IMPORT_REGISTRIES_REQUEST, LanguageServer::reload_import_registries_request, ) - .custom_method(lsp_custom::TASK_REQUEST, LanguageServer::task_request) + .custom_method(lsp_custom::TASK_REQUEST, LanguageServer::task_definitions) + // TODO(nayeemrmn): Rename this to `deno/taskDefinitions` in vscode_deno and + // remove this alias. + .custom_method("deno/task", LanguageServer::task_definitions) .custom_method(testing::TEST_RUN_REQUEST, LanguageServer::test_run_request) .custom_method( testing::TEST_RUN_CANCEL_REQUEST, diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index c13053db8935f2..b5af78b1149bca 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -837,6 +837,137 @@ fn lsp_workspace_enable_paths_no_workspace_configuration() { client.shutdown(); } +#[test] +fn lsp_did_change_deno_configuration_notification() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + + temp_dir.write("deno.json", json!({}).to_string()); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 1, + }], + })); + let res = client + .read_notification_with_method::("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 1, + "configurationType": "denoJson" + }], + })) + ); + + temp_dir.write( + "deno.json", + json!({ "fmt": { "semiColons": false } }).to_string(), + ); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 2, + }], + })); + let res = client + .read_notification_with_method::("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 2, + "configurationType": "denoJson" + }], + })) + ); + + temp_dir.remove_file("deno.json"); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 3, + }], + })); + let res = client + .read_notification_with_method::("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 3, + "configurationType": "denoJson" + }], + })) + ); + + temp_dir.write("package.json", json!({}).to_string()); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 1, + }], + })); + let res = client + .read_notification_with_method::("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 1, + "configurationType": "packageJson" + }], + })) + ); + + temp_dir.write("package.json", json!({ "type": "module" }).to_string()); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 2, + }], + })); + let res = client + .read_notification_with_method::("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 2, + "configurationType": "packageJson" + }], + })) + ); + + temp_dir.remove_file("package.json"); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 3, + }], + })); + let res = client + .read_notification_with_method::("deno/didChangeDenoConfiguration"); + assert_eq!( + res, + Some(json!({ + "changes": [{ + "uri": temp_dir.uri().join("package.json").unwrap(), + "type": 3, + "configurationType": "packageJson" + }], + })) + ); +} + #[test] fn lsp_deno_task() { let context = TestContextBuilder::new().use_temp_cwd().build(); @@ -863,10 +994,12 @@ fn lsp_deno_task() { json!([ { "name": "build", - "detail": "deno test" + "detail": "deno test", + "sourceUri": temp_dir.uri().join("deno.jsonc").unwrap(), }, { "name": "some:test", - "detail": "deno bundle mod.ts" + "detail": "deno bundle mod.ts", + "sourceUri": temp_dir.uri().join("deno.jsonc").unwrap(), } ]) );