diff options
author | Nayeem Rahman <nayeemrmn99@gmail.com> | 2023-09-24 23:33:52 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-24 23:33:52 +0100 |
commit | bb4b00df929a84d0302659700b97160f0fdbab78 (patch) | |
tree | b389f460b21830da2837af059f218e00c3c7b138 /cli | |
parent | b2abae477115dc6ca97a767c6800c7c3f1aa0ebb (diff) |
feat(lsp): cache all dependencies quick fix (#20665)
Diffstat (limited to 'cli')
-rw-r--r-- | cli/lsp/analysis.rs | 18 | ||||
-rw-r--r-- | cli/lsp/diagnostics.rs | 112 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 60 | ||||
-rw-r--r-- | cli/tests/integration/lsp_tests.rs | 145 |
4 files changed, 267 insertions, 68 deletions
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index c1def6012..0d987163b 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -917,6 +917,24 @@ impl CodeActionCollection { } } } + + pub fn add_cache_all_action( + &mut self, + specifier: &ModuleSpecifier, + diagnostics: Vec<lsp::Diagnostic>, + ) { + self.actions.push(CodeActionKind::Deno(lsp::CodeAction { + title: "Cache all dependencies of this module.".to_string(), + kind: Some(lsp::CodeActionKind::QUICKFIX), + diagnostics: Some(diagnostics), + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![json!([]), json!(&specifier)]), + }), + ..Default::default() + })); + } } /// Prepend the whitespace characters found at the start of line_content to content. diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index f5a0af455..2ab9656a4 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -24,6 +24,7 @@ use crate::tools::lint::get_configured_rules; use deno_ast::MediaType; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; +use deno_core::parking_lot::RwLock; use deno_core::resolve_url; use deno_core::serde::Deserialize; use deno_core::serde_json; @@ -48,7 +49,6 @@ use std::sync::Arc; use std::thread; use tokio::sync::mpsc; use tokio::sync::Mutex; -use tokio::sync::RwLock; use tokio::time::Duration; use tokio_util::sync::CancellationToken; use tower_lsp::lsp_types as lsp; @@ -96,13 +96,13 @@ type DiagnosticsBySource = HashMap<DiagnosticSource, VersionedDiagnostics>; #[derive(Debug)] struct DiagnosticsPublisher { client: Client, - state: Arc<RwLock<DiagnosticsState>>, + state: Arc<DiagnosticsState>, diagnostics_by_specifier: Mutex<HashMap<ModuleSpecifier, DiagnosticsBySource>>, } impl DiagnosticsPublisher { - pub fn new(client: Client, state: Arc<RwLock<DiagnosticsState>>) -> Self { + pub fn new(client: Client, state: Arc<DiagnosticsState>) -> Self { Self { client, state, @@ -119,7 +119,6 @@ impl DiagnosticsPublisher { ) -> usize { let mut diagnostics_by_specifier = self.diagnostics_by_specifier.lock().await; - let mut state = self.state.write().await; let mut seen_specifiers = HashSet::with_capacity(diagnostics.len()); let mut messages_sent = 0; @@ -146,7 +145,9 @@ impl DiagnosticsPublisher { .cloned() .collect::<Vec<_>>(); - state.update(&record.specifier, version, &all_specifier_diagnostics); + self + .state + .update(&record.specifier, version, &all_specifier_diagnostics); self .client .when_outside_lsp_lock() @@ -177,7 +178,7 @@ impl DiagnosticsPublisher { specifiers_to_remove.push(specifier.clone()); if let Some(removed_value) = maybe_removed_value { // clear out any diagnostics for this specifier - state.update(specifier, removed_value.version, &[]); + self.state.update(specifier, removed_value.version, &[]); self .client .when_outside_lsp_lock() @@ -296,57 +297,72 @@ struct ChannelUpdateMessage { batch_index: Option<usize>, } -#[derive(Clone, Debug)] +#[derive(Debug)] struct SpecifierState { - has_no_cache_diagnostic: bool, + version: Option<i32>, + no_cache_diagnostics: Vec<lsp::Diagnostic>, } -#[derive(Clone, Debug, Default)] +#[derive(Debug, Default)] pub struct DiagnosticsState { - specifiers: HashMap<ModuleSpecifier, (Option<i32>, SpecifierState)>, + specifiers: RwLock<HashMap<ModuleSpecifier, SpecifierState>>, } impl DiagnosticsState { fn update( - &mut self, + &self, specifier: &ModuleSpecifier, version: Option<i32>, diagnostics: &[lsp::Diagnostic], ) { - let current_version = self.specifiers.get(specifier).and_then(|s| s.0); - let is_new = match (version, current_version) { - (Some(arg), Some(existing)) => arg >= existing, - _ => true, - }; - if is_new { - self.specifiers.insert( - specifier.clone(), - ( - version, - SpecifierState { - has_no_cache_diagnostic: diagnostics.iter().any(|d| { - d.code - == Some(lsp::NumberOrString::String("no-cache".to_string())) - || d.code - == Some(lsp::NumberOrString::String( - "no-cache-npm".to_string(), - )) - }), - }, - ), - ); + let mut specifiers = self.specifiers.write(); + let current_version = specifiers.get(specifier).and_then(|s| s.version); + match (version, current_version) { + (Some(arg), Some(existing)) if arg < existing => return, + _ => {} } + let mut no_cache_diagnostics = vec![]; + for diagnostic in diagnostics { + if diagnostic.code + == Some(lsp::NumberOrString::String("no-cache".to_string())) + || diagnostic.code + == Some(lsp::NumberOrString::String("no-cache-npm".to_string())) + { + no_cache_diagnostics.push(diagnostic.clone()); + } + } + specifiers.insert( + specifier.clone(), + SpecifierState { + version, + no_cache_diagnostics, + }, + ); } - pub fn clear(&mut self, specifier: &ModuleSpecifier) { - self.specifiers.remove(specifier); + pub fn clear(&self, specifier: &ModuleSpecifier) { + self.specifiers.write().remove(specifier); } - pub fn has_no_cache_diagnostic(&self, specifier: &ModuleSpecifier) -> bool { + pub fn has_no_cache_diagnostics(&self, specifier: &ModuleSpecifier) -> bool { self .specifiers + .read() .get(specifier) - .map_or(false, |s| s.1.has_no_cache_diagnostic) + .map(|s| !s.no_cache_diagnostics.is_empty()) + .unwrap_or(false) + } + + pub fn no_cache_diagnostics( + &self, + specifier: &ModuleSpecifier, + ) -> Vec<lsp::Diagnostic> { + self + .specifiers + .read() + .get(specifier) + .map(|s| s.no_cache_diagnostics.clone()) + .unwrap_or_default() } } @@ -358,7 +374,7 @@ pub struct DiagnosticsServer { performance: Arc<Performance>, ts_server: Arc<TsServer>, batch_counter: DiagnosticBatchCounter, - state: Arc<RwLock<DiagnosticsState>>, + state: Arc<DiagnosticsState>, } impl DiagnosticsServer { @@ -366,6 +382,7 @@ impl DiagnosticsServer { client: Client, performance: Arc<Performance>, ts_server: Arc<TsServer>, + state: Arc<DiagnosticsState>, ) -> Self { DiagnosticsServer { channel: Default::default(), @@ -374,14 +391,10 @@ impl DiagnosticsServer { performance, ts_server, batch_counter: Default::default(), - state: Default::default(), + state, } } - pub fn state(&self) -> Arc<RwLock<DiagnosticsState>> { - self.state.clone() - } - pub fn get_ts_diagnostics( &self, specifier: &ModuleSpecifier, @@ -906,7 +919,7 @@ async fn generate_ts_diagnostics( #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] -struct DiagnosticDataSpecifier { +pub struct DiagnosticDataSpecifier { pub specifier: ModuleSpecifier, } @@ -1050,14 +1063,11 @@ impl DenoDiagnostic { .clone() .ok_or_else(|| anyhow!("Diagnostic is missing data"))?; let data: DiagnosticDataSpecifier = serde_json::from_value(data)?; - let title = match code.as_str() { - "no-cache" | "no-cache-npm" => { - format!("Cache \"{}\" and its dependencies.", data.specifier) - } - _ => "Cache the data URL and its dependencies.".to_string(), - }; lsp::CodeAction { - title, + title: format!( + "Cache \"{}\" and its dependencies.", + data.specifier + ), kind: Some(lsp::CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), command: Some(lsp::Command { diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 2a17b2e59..c415cf45b 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -48,8 +48,10 @@ use super::config::ConfigSnapshot; use super::config::WorkspaceSettings; use super::config::SETTINGS_SECTION; use super::diagnostics; +use super::diagnostics::DiagnosticDataSpecifier; use super::diagnostics::DiagnosticServerUpdateMessage; use super::diagnostics::DiagnosticsServer; +use super::diagnostics::DiagnosticsState; use super::documents::to_hover_text; use super::documents::to_lsp_range; use super::documents::AssetOrDocument; @@ -180,6 +182,7 @@ pub struct Inner { /// Configuration information. pub config: Config, deps_http_cache: Arc<dyn HttpCache>, + diagnostics_state: Arc<diagnostics::DiagnosticsState>, diagnostics_server: diagnostics::DiagnosticsServer, /// The collection of documents that the server is currently handling, either /// on disk or "open" within the client. @@ -557,10 +560,12 @@ impl Inner { let ts_server = Arc::new(TsServer::new(performance.clone(), deps_http_cache.clone())); let config = Config::new(); + let diagnostics_state = Arc::new(DiagnosticsState::default()); let diagnostics_server = DiagnosticsServer::new( client.clone(), performance.clone(), ts_server.clone(), + diagnostics_state.clone(), ); let assets = Assets::new(ts_server.clone()); let registry_url = CliNpmRegistryApi::default_url(); @@ -587,6 +592,7 @@ impl Inner { client, config, deps_http_cache, + diagnostics_state, diagnostics_server, documents, http_client, @@ -1442,12 +1448,7 @@ impl Inner { async fn did_close(&mut self, params: DidCloseTextDocumentParams) { let mark = self.performance.mark("did_close", Some(¶ms)); - self - .diagnostics_server - .state() - .write() - .await - .clear(¶ms.text_document.uri); + self.diagnostics_state.clear(¶ms.text_document.uri); if params.text_document.uri.scheme() == "deno" { // we can ignore virtual text documents closing, as they don't need to // be tracked in memory, as they are static assets that won't change @@ -1914,6 +1915,7 @@ impl Inner { let file_diagnostics = self .diagnostics_server .get_ts_diagnostics(&specifier, asset_or_doc.document_lsp_version()); + let mut includes_no_cache = false; for diagnostic in &fixable_diagnostics { match diagnostic.source.as_deref() { Some("deno-ts") => { @@ -1957,12 +1959,21 @@ impl Inner { } } } - Some("deno") => code_actions - .add_deno_fix_action(&specifier, diagnostic) - .map_err(|err| { - error!("{}", err); - LspError::internal_error() - })?, + Some("deno") => { + if diagnostic.code + == Some(NumberOrString::String("no-cache".to_string())) + || diagnostic.code + == Some(NumberOrString::String("no-cache-npm".to_string())) + { + includes_no_cache = true; + } + code_actions + .add_deno_fix_action(&specifier, diagnostic) + .map_err(|err| { + error!("{}", err); + LspError::internal_error() + })? + } Some("deno-lint") => code_actions .add_deno_lint_ignore_action( &specifier, @@ -1977,6 +1988,24 @@ impl Inner { _ => (), } } + if includes_no_cache { + let no_cache_diagnostics = + self.diagnostics_state.no_cache_diagnostics(&specifier); + let uncached_deps = no_cache_diagnostics + .iter() + .filter_map(|d| { + let data = serde_json::from_value::<DiagnosticDataSpecifier>( + d.data.clone().into(), + ) + .ok()?; + Some(data.specifier) + }) + .collect::<HashSet<_>>(); + if uncached_deps.len() > 1 { + code_actions + .add_cache_all_action(&specifier, no_cache_diagnostics.to_owned()); + } + } code_actions.set_preferred_fixes(); all_actions.extend(code_actions.get_response()); } @@ -3277,17 +3306,14 @@ impl tower_lsp::LanguageServer for LanguageServer { if !is_importable_ext(&path) { return; } - let diagnostics_state = { + { let inner = self.0.read().await; if !inner.config.workspace_settings().cache_on_save || !inner.config.specifier_enabled(uri) + || !inner.diagnostics_state.has_no_cache_diagnostics(uri) { return; } - inner.diagnostics_server.state() - }; - if !diagnostics_state.read().await.has_no_cache_diagnostic(uri) { - return; } if let Err(err) = self .cache_request(Some( diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index 9e9b3f8cd..336304db7 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -4504,6 +4504,151 @@ fn lsp_code_actions_deno_cache_npm() { } #[test] +fn lsp_code_actions_deno_cache_all() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": r#" + import * as a from "https://deno.land/x/a/mod.ts"; + import chalk from "npm:chalk"; + console.log(a); + console.log(chalk); + "#, + } + })); + assert_eq!( + diagnostics.messages_with_source("deno"), + serde_json::from_value(json!({ + "uri": "file:///a/file.ts", + "diagnostics": [ + { + "range": { + "start": { "line": 1, "character": 27 }, + "end": { "line": 1, "character": 57 }, + }, + "severity": 1, + "code": "no-cache", + "source": "deno", + "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", + "data": { "specifier": "https://deno.land/x/a/mod.ts" }, + }, + { + "range": { + "start": { "line": 2, "character": 26 }, + "end": { "line": 2, "character": 37 }, + }, + "severity": 1, + "code": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: chalk", + "data": { "specifier": "npm:chalk" }, + }, + ], + "version": 1, + })).unwrap() + ); + + let res = + client + .write_request( "textDocument/codeAction", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "range": { + "start": { "line": 1, "character": 27 }, + "end": { "line": 1, "character": 57 }, + }, + "context": { + "diagnostics": [{ + "range": { + "start": { "line": 1, "character": 27 }, + "end": { "line": 1, "character": 57 }, + }, + "severity": 1, + "code": "no-cache", + "source": "deno", + "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", + "data": { + "specifier": "https://deno.land/x/a/mod.ts", + }, + }], + "only": ["quickfix"], + } + }), + ) + ; + assert_eq!( + res, + json!([ + { + "title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.", + "kind": "quickfix", + "diagnostics": [{ + "range": { + "start": { "line": 1, "character": 27 }, + "end": { "line": 1, "character": 57 }, + }, + "severity": 1, + "code": "no-cache", + "source": "deno", + "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", + "data": { + "specifier": "https://deno.land/x/a/mod.ts", + }, + }], + "command": { + "title": "", + "command": "deno.cache", + "arguments": [["https://deno.land/x/a/mod.ts"], "file:///a/file.ts"], + } + }, + { + "title": "Cache all dependencies of this module.", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { "line": 1, "character": 27 }, + "end": { "line": 1, "character": 57 }, + }, + "severity": 1, + "code": "no-cache", + "source": "deno", + "message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts", + "data": { + "specifier": "https://deno.land/x/a/mod.ts", + }, + }, + { + "range": { + "start": { "line": 2, "character": 26 }, + "end": { "line": 2, "character": 37 }, + }, + "severity": 1, + "code": "no-cache-npm", + "source": "deno", + "message": "Uncached or missing npm package: chalk", + "data": { "specifier": "npm:chalk" }, + }, + ], + "command": { + "title": "", + "command": "deno.cache", + "arguments": [[], "file:///a/file.ts"], + } + }, + ]) + ); + client.shutdown(); +} + +#[test] fn lsp_cache_on_save() { let context = TestContextBuilder::new() .use_http_server() |