diff --git a/Cargo.lock b/Cargo.lock index 2be38f3583..e38a58c9eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3984,6 +3984,7 @@ dependencies = [ "crossbeam", "ctrlc", "filetime", + "indicatif", "insta", "insta-cmd", "jiff", diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index fe282249a5..c67a457d64 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -16,7 +16,7 @@ use ruff_python_ast::PythonVersion; use ty_project::metadata::options::{EnvironmentOptions, Options}; use ty_project::metadata::value::RangedValue; use ty_project::watch::{ChangeEvent, ChangedKind}; -use ty_project::{Db, ProjectDatabase, ProjectMetadata}; +use ty_project::{Db, DummyReporter, ProjectDatabase, ProjectMetadata}; struct Case { db: ProjectDatabase, @@ -153,7 +153,7 @@ fn benchmark_incremental(criterion: &mut Criterion) { fn setup() -> Case { let case = setup_tomllib_case(); - let result: Vec<_> = case.db.check().unwrap(); + let result: Vec<_> = case.db.check(&DummyReporter).unwrap(); assert_diagnostics(&case.db, &result, EXPECTED_TOMLLIB_DIAGNOSTICS); @@ -181,7 +181,7 @@ fn benchmark_incremental(criterion: &mut Criterion) { None, ); - let result = db.check().unwrap(); + let result = db.check(&DummyReporter).unwrap(); assert_eq!(result.len(), EXPECTED_TOMLLIB_DIAGNOSTICS.len()); } @@ -201,7 +201,7 @@ fn benchmark_cold(criterion: &mut Criterion) { setup_tomllib_case, |case| { let Case { db, .. } = case; - let result: Vec<_> = db.check().unwrap(); + let result: Vec<_> = db.check(&DummyReporter).unwrap(); assert_diagnostics(db, &result, EXPECTED_TOMLLIB_DIAGNOSTICS); }, @@ -315,7 +315,7 @@ fn benchmark_many_string_assignments(criterion: &mut Criterion) { }, |case| { let Case { db, .. } = case; - let result = db.check().unwrap(); + let result = db.check(&DummyReporter).unwrap(); assert_eq!(result.len(), 0); }, BatchSize::SmallInput, diff --git a/crates/ty/Cargo.toml b/crates/ty/Cargo.toml index 70f698a95e..1a1222121d 100644 --- a/crates/ty/Cargo.toml +++ b/crates/ty/Cargo.toml @@ -28,6 +28,7 @@ colored = { workspace = true } countme = { workspace = true, features = ["enable"] } crossbeam = { workspace = true } ctrlc = { version = "3.4.4" } +indicatif = { workspace = true } jiff = { workspace = true } rayon = { workspace = true } salsa = { workspace = true } diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index 4d85d4594e..87fd617862 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -25,7 +25,7 @@ use ruff_db::Upcast; use salsa::plumbing::ZalsaDatabase; use ty_project::metadata::options::Options; use ty_project::watch::ProjectWatcher; -use ty_project::{watch, Db}; +use ty_project::{watch, Db, DummyReporter, Reporter}; use ty_project::{ProjectDatabase, ProjectMetadata}; use ty_server::run_server; @@ -200,22 +200,28 @@ impl MainLoop { self.watcher = Some(ProjectWatcher::new(watcher, db)); - self.run(db)?; + // Do not show progress bars with `--watch`, indicatif does not seem to + // handle cancelling independent progress bars very well. + self.run_with_progress::(db)?; Ok(ExitStatus::Success) } - fn run(mut self, db: &mut ProjectDatabase) -> Result { + fn run(self, db: &mut ProjectDatabase) -> Result { + self.run_with_progress::(db) + } + + fn run_with_progress(mut self, db: &mut ProjectDatabase) -> Result { self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); - let result = self.main_loop(db); + let result = self.main_loop::(db); tracing::debug!("Exiting main loop"); result } - fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result { + fn main_loop(&mut self, db: &mut ProjectDatabase) -> Result { // Schedule the first check. tracing::debug!("Starting main loop"); @@ -226,11 +232,12 @@ impl MainLoop { MainLoopMessage::CheckWorkspace => { let db = db.clone(); let sender = self.sender.clone(); + let reporter = R::default(); // Spawn a new task that checks the project. This needs to be done in a separate thread // to prevent blocking the main loop here. rayon::spawn(move || { - match db.check() { + match db.check(&reporter) { Ok(result) => { // Send the result back to the main loop for printing. sender @@ -340,6 +347,34 @@ impl MainLoop { } } +/// A progress reporter for `ty check`. +struct IndicatifReporter(indicatif::ProgressBar); + +impl Default for IndicatifReporter { + fn default() -> IndicatifReporter { + let progress = indicatif::ProgressBar::new(0); + progress.set_style( + indicatif::ProgressStyle::with_template( + "{msg:8.dim} {bar:60.green/dim} {pos}/{len} files", + ) + .unwrap() + .progress_chars("--"), + ); + progress.set_message("Checking"); + IndicatifReporter(progress) + } +} + +impl ty_project::Reporter for IndicatifReporter { + fn set_files(&self, files: usize) { + self.0.set_length(files as u64); + } + + fn report_file(&self, _file: &ruff_db::files::File) { + self.0.inc(1); + } +} + #[derive(Debug)] struct MainLoopCancellationToken { sender: crossbeam_channel::Sender, diff --git a/crates/ty/tests/file_watching.rs b/crates/ty/tests/file_watching.rs index c8205c9251..56ff5e2bfc 100644 --- a/crates/ty/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -14,7 +14,7 @@ use ty_project::metadata::options::{EnvironmentOptions, Options}; use ty_project::metadata::pyproject::{PyProject, Tool}; use ty_project::metadata::value::{RangedValue, RelativePathBuf}; use ty_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher}; -use ty_project::{Db, ProjectDatabase, ProjectMetadata}; +use ty_project::{Db, DummyReporter, ProjectDatabase, ProjectMetadata}; use ty_python_semantic::{resolve_module, ModuleName, PythonPlatform}; struct TestCase { @@ -1117,7 +1117,10 @@ print(sys.last_exc, os.getegid()) Ok(()) })?; - let diagnostics = case.db.check().context("Failed to check project.")?; + let diagnostics = case + .db + .check(&DummyReporter) + .context("Failed to check project.")?; assert_eq!(diagnostics.len(), 2); assert_eq!( @@ -1142,7 +1145,10 @@ print(sys.last_exc, os.getegid()) }) .expect("Search path settings to be valid"); - let diagnostics = case.db.check().context("Failed to check project.")?; + let diagnostics = case + .db + .check(&DummyReporter) + .context("Failed to check project.")?; assert!(diagnostics.is_empty()); Ok(()) diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index 1da5f38ed1..c15f0c99c4 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -2,7 +2,7 @@ use std::panic::RefUnwindSafe; use std::sync::Arc; use crate::DEFAULT_LINT_REGISTRY; -use crate::{Project, ProjectMetadata}; +use crate::{Project, ProjectMetadata, Reporter}; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, Files}; use ruff_db::system::System; @@ -68,8 +68,8 @@ impl ProjectDatabase { } /// Checks all open files in the project and its dependencies. - pub fn check(&self) -> Result, Cancelled> { - self.with_db(|db| db.project().check(db)) + pub fn check(&self, reporter: &impl Reporter) -> Result, Cancelled> { + self.with_db(|db| db.project().check(db, reporter)) } #[tracing::instrument(level = "debug", skip(self))] diff --git a/crates/ty_project/src/files.rs b/crates/ty_project/src/files.rs index 182e20c03a..db8cc169f6 100644 --- a/crates/ty_project/src/files.rs +++ b/crates/ty_project/src/files.rs @@ -161,6 +161,10 @@ impl Indexed<'_> { pub(super) fn diagnostics(&self) -> &[IOErrorDiagnostic] { &self.inner.diagnostics } + + pub(super) fn len(&self) -> usize { + self.inner.files.len() + } } impl Deref for Indexed<'_> { diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index cac5000db1..547f80bf35 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -18,7 +18,7 @@ use rustc_hash::FxHashSet; use salsa::Durability; use salsa::Setter; use std::backtrace::BacktraceStatus; -use std::panic::{AssertUnwindSafe, UnwindSafe}; +use std::panic::{AssertUnwindSafe, RefUnwindSafe, UnwindSafe}; use std::sync::Arc; use thiserror::Error; use tracing::error; @@ -106,6 +106,24 @@ pub struct Project { settings_diagnostics: Vec, } +/// A progress reporter. +pub trait Reporter: Default + Send + Sync + RefUnwindSafe + 'static { + /// Initialize the reporter with the number of files. + fn set_files(&self, files: usize); + + /// Report the completion of a given file. + fn report_file(&self, file: &File); +} + +/// A no-op implementation of [`Reporter`]. +#[derive(Default)] +pub struct DummyReporter; + +impl Reporter for DummyReporter { + fn set_files(&self, _files: usize) {} + fn report_file(&self, _file: &File) {} +} + #[salsa::tracked] impl Project { pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self { @@ -168,7 +186,7 @@ impl Project { } /// Checks all open files in the project and its dependencies. - pub(crate) fn check(self, db: &ProjectDatabase) -> Vec { + pub(crate) fn check(self, db: &ProjectDatabase, reporter: &impl Reporter) -> Vec { let project_span = tracing::debug_span!("Project::check"); let _span = project_span.enter(); @@ -182,6 +200,7 @@ impl Project { ); let files = ProjectFiles::new(db, self); + reporter.set_files(files.len()); diagnostics.extend( files @@ -190,36 +209,31 @@ impl Project { .map(IOErrorDiagnostic::to_diagnostic), ); - let file_diagnostics = Arc::new(std::sync::Mutex::new(vec![])); + let file_diagnostics = std::sync::Mutex::new(vec![]); { - let file_diagnostics = Arc::clone(&file_diagnostics); let db = db.clone(); - let project_span = project_span.clone(); + let file_diagnostics = &file_diagnostics; + let project_span = &project_span; rayon::scope(move |scope| { for file in &files { - let result = Arc::clone(&file_diagnostics); let db = db.clone(); - let project_span = project_span.clone(); - scope.spawn(move |_| { let check_file_span = - tracing::debug_span!(parent: &project_span, "check_file", ?file); + tracing::debug_span!(parent: project_span, "check_file", ?file); let _entered = check_file_span.entered(); - let file_diagnostics = check_file_impl(&db, file); - result.lock().unwrap().extend(file_diagnostics); + let result = check_file_impl(&db, file); + file_diagnostics.lock().unwrap().extend(result); + + reporter.report_file(&file); }); } }); } - let mut file_diagnostics = Arc::into_inner(file_diagnostics) - .unwrap() - .into_inner() - .unwrap(); - + let mut file_diagnostics = file_diagnostics.into_inner().unwrap(); file_diagnostics.sort_by(|left, right| { left.rendering_sort_key(db) .cmp(&right.rendering_sort_key(db)) @@ -493,6 +507,13 @@ impl<'a> ProjectFiles<'a> { ProjectFiles::Indexed(indexed) => indexed.diagnostics(), } } + + fn len(&self) -> usize { + match self { + ProjectFiles::OpenFiles(open_files) => open_files.len(), + ProjectFiles::Indexed(indexed) => indexed.len(), + } + } } impl<'a> IntoIterator for &'a ProjectFiles<'a> { diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 9ed86299e8..2a7cad6f8a 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -18,8 +18,8 @@ use ty_ide::{goto_type_definition, hover, inlay_hints, MarkupKind}; use ty_project::metadata::options::Options; use ty_project::metadata::value::ValueSource; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; -use ty_project::ProjectMetadata; use ty_project::{Db, ProjectDatabase}; +use ty_project::{DummyReporter, ProjectMetadata}; use ty_python_semantic::Program; use wasm_bindgen::prelude::*; @@ -186,7 +186,7 @@ impl Workspace { /// Checks all open files pub fn check(&self) -> Result, Error> { - let result = self.db.check().map_err(into_error)?; + let result = self.db.check(&DummyReporter).map_err(into_error)?; Ok(result.into_iter().map(Diagnostic::wrap).collect()) }