diff options
Diffstat (limited to 'cli/lsp/diagnostics.rs')
-rw-r--r-- | cli/lsp/diagnostics.rs | 599 |
1 files changed, 246 insertions, 353 deletions
diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 853a8da73..d1eef1d9f 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -32,73 +32,19 @@ use tokio::sync::Mutex; use tokio::time::sleep; use tokio::time::Duration; use tokio::time::Instant; +use tokio_util::sync::CancellationToken; pub type DiagnosticRecord = (ModuleSpecifier, Option<i32>, Vec<lsp::Diagnostic>); pub type DiagnosticVec = Vec<DiagnosticRecord>; +type DiagnosticMap = + HashMap<ModuleSpecifier, (Option<i32>, Vec<lsp::Diagnostic>)>; type TsDiagnosticsMap = HashMap<String, Vec<diagnostics::Diagnostic>>; -#[derive(Debug, Hash, Clone, PartialEq, Eq)] -pub(crate) enum DiagnosticSource { - Deno, - DenoLint, - TypeScript, -} - -#[derive(Debug, Default)] -struct DiagnosticCollection { - map: HashMap<(ModuleSpecifier, DiagnosticSource), Vec<lsp::Diagnostic>>, - versions: HashMap<ModuleSpecifier, HashMap<DiagnosticSource, i32>>, - changes: HashSet<ModuleSpecifier>, -} - -impl DiagnosticCollection { - pub fn get( - &self, - specifier: &ModuleSpecifier, - source: DiagnosticSource, - ) -> impl Iterator<Item = &lsp::Diagnostic> { - self - .map - .get(&(specifier.clone(), source)) - .into_iter() - .flatten() - } - - pub fn get_version( - &self, - specifier: &ModuleSpecifier, - source: &DiagnosticSource, - ) -> Option<i32> { - let source_version = self.versions.get(specifier)?; - source_version.get(source).cloned() - } - - pub fn set(&mut self, source: DiagnosticSource, record: DiagnosticRecord) { - let (specifier, maybe_version, diagnostics) = record; - self - .map - .insert((specifier.clone(), source.clone()), diagnostics); - if let Some(version) = maybe_version { - let source_version = self.versions.entry(specifier.clone()).or_default(); - source_version.insert(source, version); - } - self.changes.insert(specifier); - } - - pub fn take_changes(&mut self) -> Option<HashSet<ModuleSpecifier>> { - if self.changes.is_empty() { - None - } else { - Some(mem::take(&mut self.changes)) - } - } -} - #[derive(Debug)] pub(crate) struct DiagnosticsServer { channel: Option<mpsc::UnboundedSender<()>>, - collection: Arc<Mutex<DiagnosticCollection>>, + ts_diagnostics: Arc<Mutex<DiagnosticMap>>, client: Client, performance: Arc<Performance>, ts_server: Arc<TsServer>, @@ -112,37 +58,40 @@ impl DiagnosticsServer { ) -> Self { DiagnosticsServer { channel: Default::default(), - collection: Default::default(), + ts_diagnostics: Default::default(), client, performance, ts_server, } } - pub(crate) async fn get( + pub(crate) async fn get_ts_diagnostics( &self, specifier: &ModuleSpecifier, - source: DiagnosticSource, + document_version: Option<i32>, ) -> Vec<lsp::Diagnostic> { - self - .collection - .lock() - .await - .get(specifier, source) - .cloned() - .collect() + let ts_diagnostics = self.ts_diagnostics.lock().await; + if let Some((diagnostics_doc_version, diagnostics)) = + ts_diagnostics.get(specifier) + { + // only get the diagnostics if they're up to date + if document_version == *diagnostics_doc_version { + return diagnostics.clone(); + } + } + Vec::new() } pub(crate) async fn invalidate(&self, specifiers: Vec<ModuleSpecifier>) { - let mut collection = self.collection.lock().await; + let mut ts_diagnostics = self.ts_diagnostics.lock().await; for specifier in &specifiers { - collection.versions.remove(specifier); + ts_diagnostics.remove(specifier); } } pub(crate) async fn invalidate_all(&self) { - let mut collection = self.collection.lock().await; - collection.versions.clear(); + let mut ts_diagnostics = self.ts_diagnostics.lock().await; + ts_diagnostics.clear(); } pub(crate) fn start( @@ -151,49 +100,28 @@ impl DiagnosticsServer { ) { let (tx, mut rx) = mpsc::unbounded_channel::<()>(); self.channel = Some(tx); - let collection = self.collection.clone(); let client = self.client.clone(); let performance = self.performance.clone(); + let stored_ts_diagnostics = self.ts_diagnostics.clone(); let ts_server = self.ts_server.clone(); let _join_handle = thread::spawn(move || { let runtime = create_basic_runtime(); runtime.block_on(async { - // Debounce timer delay. 150ms between keystrokes is about 45 WPM, so we - // want something that is longer than that, but not too long to - // introduce detectable UI delay; 200ms is a decent compromise. - const DELAY: Duration = Duration::from_millis(200); - // If the debounce timer isn't active, it will be set to expire "never", - // which is actually just 1 year in the future. - const NEVER: Duration = Duration::from_secs(365 * 24 * 60 * 60); - - // A flag that is set whenever something has changed that requires the - // diagnostics collection to be updated. - let mut dirty = false; - - let debounce_timer = sleep(NEVER); - tokio::pin!(debounce_timer); + let mut token = CancellationToken::new(); + let mut ts_handle: Option<tokio::task::JoinHandle<()>> = None; + let mut lint_handle: Option<tokio::task::JoinHandle<()>> = None; + let mut deps_handle: Option<tokio::task::JoinHandle<()>> = None; loop { - // "race" the next message off the rx queue or the debounce timer. - // The debounce timer gets reset every time a message comes off the - // queue. When the debounce timer expires, a snapshot of the most - // up-to-date state is used to produce diagnostics. - tokio::select! { - maybe_request = rx.recv() => { - match maybe_request { - // channel has closed - None => break, - Some(_) => { - dirty = true; - debounce_timer.as_mut().reset(Instant::now() + DELAY); - } - } - } - _ = debounce_timer.as_mut(), if dirty => { - dirty = false; - debounce_timer.as_mut().reset(Instant::now() + NEVER); + match rx.recv().await { + // channel has closed + None => break, + Some(()) => { + // cancel the previous run + token.cancel(); + token = CancellationToken::new(); let (snapshot, config, maybe_lint_config) = { let language_server = language_server.lock().await; @@ -203,15 +131,131 @@ impl DiagnosticsServer { language_server.maybe_lint_config.clone(), ) }; - update_diagnostics( - &client, - collection.clone(), - snapshot, - config, - maybe_lint_config, - &ts_server, - performance.clone(), - ).await; + + let previous_ts_handle = ts_handle.take(); + ts_handle = Some(tokio::spawn({ + let performance = performance.clone(); + let ts_server = ts_server.clone(); + let client = client.clone(); + let token = token.clone(); + let stored_ts_diagnostics = stored_ts_diagnostics.clone(); + let snapshot = snapshot.clone(); + let config = config.clone(); + async move { + if let Some(previous_handle) = previous_ts_handle { + // Wait on the previous run to complete in order to prevent + // multiple threads queueing up a lot of tsc requests. + // Do not race this with cancellation because we want a + // chain of events to wait for all the previous diagnostics to complete + previous_handle.await; + } + + // Debounce timer delay. 150ms between keystrokes is about 45 WPM, so we + // want something that is longer than that, but not too long to + // introduce detectable UI delay; 200ms is a decent compromise. + const DELAY: Duration = Duration::from_millis(200); + tokio::select! { + _ = token.cancelled() => { return; } + _ = tokio::time::sleep(DELAY) => {} + }; + + let mark = + performance.mark("update_diagnostics_ts", None::<()>); + let diagnostics = + generate_ts_diagnostics(snapshot.clone(), &ts_server) + .await + .map_err(|err| { + error!( + "Error generating TypeScript diagnostics: {}", + err + ); + }) + .unwrap_or_default(); + + if !token.is_cancelled() { + { + let mut stored_ts_diagnostics = + stored_ts_diagnostics.lock().await; + *stored_ts_diagnostics = diagnostics + .iter() + .map(|(specifier, version, diagnostics)| { + (specifier.clone(), (*version, diagnostics.clone())) + }) + .collect(); + } + + for (specifier, version, diagnostics) in diagnostics { + client + .publish_diagnostics(specifier, diagnostics, version) + .await; + } + performance.measure(mark); + } + } + })); + + let previous_deps_handle = deps_handle.take(); + deps_handle = Some(tokio::spawn({ + let performance = performance.clone(); + let client = client.clone(); + let token = token.clone(); + let snapshot = snapshot.clone(); + let config = config.clone(); + async move { + if let Some(previous_handle) = previous_deps_handle { + previous_handle.await; + } + let mark = + performance.mark("update_diagnostics_deps", None::<()>); + let diagnostics = generate_deps_diagnostics( + snapshot.clone(), + config.clone(), + token.clone(), + ) + .await; + + if !token.is_cancelled() { + for (specifier, version, diagnostics) in diagnostics { + client + .publish_diagnostics(specifier, diagnostics, version) + .await; + } + performance.measure(mark); + } + } + })); + + let previous_lint_handle = lint_handle.take(); + lint_handle = Some(tokio::spawn({ + let performance = performance.clone(); + let client = client.clone(); + let token = token.clone(); + let snapshot = snapshot.clone(); + let config = config.clone(); + async move { + if let Some(previous_handle) = previous_lint_handle { + previous_handle.await; + } + let mark = + performance.mark("update_diagnostics_lint", None::<()>); + let diagnostics = generate_lint_diagnostics( + &snapshot, + &config, + maybe_lint_config, + token.clone(), + ) + .await; + + if !token.is_cancelled() { + for (specifier, version, diagnostics) in diagnostics { + client + .publish_diagnostics(specifier, diagnostics, version) + .await; + } + performance.measure(mark); + } + } + })); } } } @@ -336,91 +380,73 @@ fn ts_json_to_diagnostics( async fn generate_lint_diagnostics( snapshot: &language_server::StateSnapshot, - collection: Arc<Mutex<DiagnosticCollection>>, config: &ConfigSnapshot, maybe_lint_config: Option<LintConfig>, -) -> Result<DiagnosticVec, AnyError> { + token: CancellationToken, +) -> DiagnosticVec { let documents = snapshot.documents.documents(true, true); let workspace_settings = config.settings.workspace.clone(); - tokio::task::spawn(async move { - let mut diagnostics_vec = Vec::new(); - if workspace_settings.lint { - for document in documents { - let version = document.maybe_lsp_version(); - let current_version = collection - .lock() - .await - .get_version(document.specifier(), &DiagnosticSource::DenoLint); - if version != current_version { - let is_allowed = match &maybe_lint_config { - Some(lint_config) => { - lint_config.files.matches_specifier(document.specifier()) - } - None => true, - }; - let diagnostics = if is_allowed { - match document.maybe_parsed_source() { - Some(Ok(parsed_source)) => { - if let Ok(references) = analysis::get_lint_references( - &parsed_source, - maybe_lint_config.as_ref(), - ) { - references - .into_iter() - .map(|r| r.to_diagnostic()) - .collect::<Vec<_>>() - } else { - Vec::new() - } - } - Some(Err(_)) => Vec::new(), - None => { - error!("Missing file contents for: {}", document.specifier()); - Vec::new() - } + let mut diagnostics_vec = Vec::new(); + if workspace_settings.lint { + for document in documents { + // exit early if cancelled + if token.is_cancelled() { + break; + } + + let version = document.maybe_lsp_version(); + let is_allowed = match &maybe_lint_config { + Some(lint_config) => { + lint_config.files.matches_specifier(document.specifier()) + } + None => true, + }; + let diagnostics = if is_allowed { + match document.maybe_parsed_source() { + Some(Ok(parsed_source)) => { + if let Ok(references) = analysis::get_lint_references( + &parsed_source, + maybe_lint_config.as_ref(), + ) { + references + .into_iter() + .map(|r| r.to_diagnostic()) + .collect::<Vec<_>>() + } else { + Vec::new() } - } else { + } + Some(Err(_)) => Vec::new(), + None => { + error!("Missing file contents for: {}", document.specifier()); Vec::new() - }; - diagnostics_vec.push(( - document.specifier().clone(), - version, - diagnostics, - )); + } } - } + } else { + Vec::new() + }; + diagnostics_vec.push(( + document.specifier().clone(), + version, + diagnostics, + )); } - Ok(diagnostics_vec) - }) - .await - .unwrap() + } + diagnostics_vec } async fn generate_ts_diagnostics( snapshot: Arc<language_server::StateSnapshot>, - collection: Arc<Mutex<DiagnosticCollection>>, ts_server: &tsc::TsServer, ) -> Result<DiagnosticVec, AnyError> { let mut diagnostics_vec = Vec::new(); - let specifiers: Vec<ModuleSpecifier> = { - let collection = collection.lock().await; - snapshot - .documents - .documents(true, true) - .iter() - .filter_map(|d| { - let version = d.maybe_lsp_version(); - let current_version = - collection.get_version(d.specifier(), &DiagnosticSource::TypeScript); - if version != current_version { - Some(d.specifier().clone()) - } else { - None - } - }) - .collect() - }; + let specifiers = snapshot + .documents + .documents(true, true) + .iter() + .map(|d| d.specifier().clone()) + .collect::<Vec<_>>(); if !specifiers.is_empty() { let req = tsc::RequestMethod::GetDiagnostics(specifiers); let ts_diagnostics_map: TsDiagnosticsMap = @@ -552,168 +578,42 @@ fn diagnose_dependency( async fn generate_deps_diagnostics( snapshot: Arc<language_server::StateSnapshot>, config: Arc<ConfigSnapshot>, - collection: Arc<Mutex<DiagnosticCollection>>, -) -> Result<DiagnosticVec, AnyError> { - tokio::task::spawn(async move { - let mut diagnostics_vec = Vec::new(); - - for document in snapshot.documents.documents(true, true) { - if !config.specifier_enabled(document.specifier()) { - continue; - } - let version = document.maybe_lsp_version(); - let current_version = collection - .lock() - .await - .get_version(document.specifier(), &DiagnosticSource::Deno); - if version != current_version { - let mut diagnostics = Vec::new(); - for (_, dependency) in document.dependencies() { - diagnose_dependency( - &mut diagnostics, - &snapshot.documents, - &dependency.maybe_code, - dependency.is_dynamic, - dependency.maybe_assert_type.as_deref(), - ); - diagnose_dependency( - &mut diagnostics, - &snapshot.documents, - &dependency.maybe_type, - dependency.is_dynamic, - dependency.maybe_assert_type.as_deref(), - ); - } - diagnostics_vec.push(( - document.specifier().clone(), - version, - diagnostics, - )); - } - } - - Ok(diagnostics_vec) - }) - .await - .unwrap() -} - -/// Publishes diagnostics to the client. -async fn publish_diagnostics( - client: &Client, - collection: &mut DiagnosticCollection, - snapshot: &language_server::StateSnapshot, - config: &ConfigSnapshot, -) { - if let Some(changes) = collection.take_changes() { - for specifier in changes { - let mut diagnostics: Vec<lsp::Diagnostic> = - if config.settings.workspace.lint { - collection - .get(&specifier, DiagnosticSource::DenoLint) - .cloned() - .collect() - } else { - Vec::new() - }; - if config.specifier_enabled(&specifier) { - diagnostics.extend( - collection - .get(&specifier, DiagnosticSource::TypeScript) - .cloned(), - ); - diagnostics - .extend(collection.get(&specifier, DiagnosticSource::Deno).cloned()); - } - let version = snapshot - .documents - .get(&specifier) - .map(|d| d.maybe_lsp_version()) - .flatten(); - client - .publish_diagnostics(specifier.clone(), diagnostics, version) - .await; - } - } -} - -/// Updates diagnostics for any specifiers that don't have the correct version -/// generated and publishes the diagnostics to the client. -async fn update_diagnostics( - client: &Client, - collection: Arc<Mutex<DiagnosticCollection>>, - snapshot: Arc<language_server::StateSnapshot>, - config: Arc<ConfigSnapshot>, - maybe_lint_config: Option<LintConfig>, - ts_server: &tsc::TsServer, - performance: Arc<Performance>, -) { - let mark = performance.mark("update_diagnostics", None::<()>); - - let lint = async { - let mark = performance.mark("update_diagnostics_lint", None::<()>); - let collection = collection.clone(); - let diagnostics = generate_lint_diagnostics( - &snapshot, - collection.clone(), - &config, - maybe_lint_config, - ) - .await - .map_err(|err| { - error!("Error generating lint diagnostics: {}", err); - }) - .unwrap_or_default(); + token: CancellationToken, +) -> DiagnosticVec { + let mut diagnostics_vec = Vec::new(); - let mut collection = collection.lock().await; - for diagnostic_record in diagnostics { - collection.set(DiagnosticSource::DenoLint, diagnostic_record); + for document in snapshot.documents.documents(true, true) { + if token.is_cancelled() { + break; } - publish_diagnostics(client, &mut collection, &snapshot, &config).await; - performance.measure(mark); - }; - - let ts = async { - let mark = performance.mark("update_diagnostics_ts", None::<()>); - let collection = collection.clone(); - let diagnostics = - generate_ts_diagnostics(snapshot.clone(), collection.clone(), ts_server) - .await - .map_err(|err| { - error!("Error generating TypeScript diagnostics: {}", err); - }) - .unwrap_or_default(); - let mut collection = collection.lock().await; - for diagnostic_record in diagnostics { - collection.set(DiagnosticSource::TypeScript, diagnostic_record); + if !config.specifier_enabled(document.specifier()) { + continue; } - publish_diagnostics(client, &mut collection, &snapshot, &config).await; - performance.measure(mark); - }; - - let deps = async { - let mark = performance.mark("update_diagnostics_deps", None::<()>); - let collection = collection.clone(); - let diagnostics = generate_deps_diagnostics( - snapshot.clone(), - config.clone(), - collection.clone(), - ) - .await - .map_err(|err| { - error!("Error generating Deno diagnostics: {}", err); - }) - .unwrap_or_default(); - let mut collection = collection.lock().await; - for diagnostic_record in diagnostics { - collection.set(DiagnosticSource::Deno, diagnostic_record); + let mut diagnostics = Vec::new(); + for (_, dependency) in document.dependencies() { + diagnose_dependency( + &mut diagnostics, + &snapshot.documents, + &dependency.maybe_code, + dependency.is_dynamic, + dependency.maybe_assert_type.as_deref(), + ); + diagnose_dependency( + &mut diagnostics, + &snapshot.documents, + &dependency.maybe_type, + dependency.is_dynamic, + dependency.maybe_assert_type.as_deref(), + ); } - publish_diagnostics(client, &mut collection, &snapshot, &config).await; - performance.measure(mark); - }; + diagnostics_vec.push(( + document.specifier().clone(), + document.maybe_lsp_version(), + diagnostics, + )); + } - tokio::join!(lint, ts, deps); - performance.measure(mark); + diagnostics_vec } #[cfg(test)] @@ -765,23 +665,17 @@ mod tests { fn setup( sources: &[(&str, &str, i32, LanguageId)], - ) -> ( - StateSnapshot, - Arc<Mutex<DiagnosticCollection>>, - PathBuf, - ConfigSnapshot, - ) { + ) -> (StateSnapshot, PathBuf, ConfigSnapshot) { let temp_dir = TempDir::new().expect("could not create temp dir"); let location = temp_dir.path().join("deps"); let state_snapshot = mock_state_snapshot(sources, &location); - let collection = Arc::new(Mutex::new(DiagnosticCollection::default())); let config = mock_config(); - (state_snapshot, collection, location, config) + (state_snapshot, location, config) } #[tokio::test] async fn test_generate_lint_diagnostics() { - let (snapshot, collection, _, config) = setup(&[( + let (snapshot, _, config) = setup(&[( "file:///a.ts", r#"import * as b from "./b.ts"; @@ -791,10 +685,9 @@ console.log(a); 1, LanguageId::TypeScript, )]); - let result = - generate_lint_diagnostics(&snapshot, collection, &config, None).await; - assert!(result.is_ok()); - let diagnostics = result.unwrap(); + let diagnostics = + generate_lint_diagnostics(&snapshot, &config, None, Default::default()) + .await; assert_eq!(diagnostics.len(), 1); let (_, _, diagnostics) = &diagnostics[0]; assert_eq!(diagnostics.len(), 2); |