[ty] Track open files in the server (#19264)

## Summary

This PR updates the server to keep track of open files both system and
virtual files.

This is done by updating the project by adding the file in the open file
set in `didOpen` notification and removing it in `didClose`
notification.

This does mean that for workspace diagnostics, ty will only check open
files because the behavior of different diagnostic builder is to first
check `is_file_open` and only add diagnostics for open files. So, this
required updating the `is_file_open` model to be `should_check_file`
model which validates whether the file needs to be checked based on the
`CheckMode`. If the check mode is open files only then it will check
whether the file is open. If it's all files then it'll return `true` by
default.

Closes: astral-sh/ty#619

## Test Plan

### Before

There are two files in the project: `__init__.py` and `diagnostics.py`.

In the video, I'm demonstrating the old behavior where making changes to
the (open) `diagnostics.py` file results in re-parsing the file:


https://github.com/user-attachments/assets/c2ac0ecd-9c77-42af-a924-c3744b146045

### After

Same setup as above.

In the video, I'm demonstrating the new behavior where making changes to
the (open) `diagnostics.py` file doesn't result in re-parting the file:


https://github.com/user-attachments/assets/7b82fe92-f330-44c7-b527-c841c4545f8f
This commit is contained in:
Dhruv Manilawala 2025-07-18 19:33:35 +05:30 committed by GitHub
parent ba7ed3a6f9
commit 99d0ac60b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 220 additions and 140 deletions

View file

@ -18,7 +18,7 @@ use ruff_python_ast::PythonVersion;
use ty_project::metadata::options::{EnvironmentOptions, Options}; use ty_project::metadata::options::{EnvironmentOptions, Options};
use ty_project::metadata::value::{RangedValue, RelativePathBuf}; use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::watch::{ChangeEvent, ChangedKind}; use ty_project::watch::{ChangeEvent, ChangedKind};
use ty_project::{Db, ProjectDatabase, ProjectMetadata}; use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata};
struct Case { struct Case {
db: ProjectDatabase, db: ProjectDatabase,
@ -102,6 +102,7 @@ fn setup_tomllib_case() -> Case {
let re = re.unwrap(); let re = re.unwrap();
db.set_check_mode(CheckMode::OpenFiles);
db.project().set_open_files(&mut db, tomllib_files); db.project().set_open_files(&mut db, tomllib_files);
let re_path = re.path(&db).as_system_path().unwrap().to_owned(); let re_path = re.path(&db).as_system_path().unwrap().to_owned();
@ -237,6 +238,7 @@ fn setup_micro_case(code: &str) -> Case {
let mut db = ProjectDatabase::new(metadata, system).unwrap(); let mut db = ProjectDatabase::new(metadata, system).unwrap();
let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap(); let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap();
db.set_check_mode(CheckMode::OpenFiles);
db.project() db.project()
.set_open_files(&mut db, FxHashSet::from_iter([file])); .set_open_files(&mut db, FxHashSet::from_iter([file]));

View file

@ -486,7 +486,7 @@ impl fmt::Debug for File {
/// ///
/// This is a wrapper around a [`File`] that provides additional methods to interact with a virtual /// This is a wrapper around a [`File`] that provides additional methods to interact with a virtual
/// file. /// file.
#[derive(Copy, Clone)] #[derive(Copy, Clone, Debug)]
pub struct VirtualFile(File); pub struct VirtualFile(File);
impl VirtualFile { impl VirtualFile {

View file

@ -87,7 +87,7 @@ impl SourceDb for ModuleDb {
#[salsa::db] #[salsa::db]
impl Db for ModuleDb { impl Db for ModuleDb {
fn is_file_open(&self, file: File) -> bool { fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }

View file

@ -96,7 +96,7 @@ pub(crate) mod tests {
#[salsa::db] #[salsa::db]
impl SemanticDb for TestDb { impl SemanticDb for TestDb {
fn is_file_open(&self, file: File) -> bool { fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }

View file

@ -12,8 +12,8 @@ use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::System; use ruff_db::system::System;
use ruff_db::vendored::VendoredFileSystem; use ruff_db::vendored::VendoredFileSystem;
use salsa::Event;
use salsa::plumbing::ZalsaDatabase; use salsa::plumbing::ZalsaDatabase;
use salsa::{Event, Setter};
use ty_ide::Db as IdeDb; use ty_ide::Db as IdeDb;
use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{Db as SemanticDb, Program}; use ty_python_semantic::{Db as SemanticDb, Program};
@ -82,22 +82,25 @@ impl ProjectDatabase {
Ok(db) Ok(db)
} }
/// Checks all open files in the project and its dependencies. /// Checks the files in the project and its dependencies as per the project's check mode.
///
/// Use [`set_check_mode`] to update the check mode.
///
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
pub fn check(&self) -> Vec<Diagnostic> { pub fn check(&self) -> Vec<Diagnostic> {
self.check_with_mode(CheckMode::OpenFiles)
}
/// Checks all open files in the project and its dependencies, using the given reporter.
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, CheckMode::OpenFiles, reporter)
}
/// Check the project with the given mode.
pub fn check_with_mode(&self, mode: CheckMode) -> Vec<Diagnostic> {
let mut reporter = DummyReporter; let mut reporter = DummyReporter;
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter); let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter);
self.project().check(self, mode, reporter) self.project().check(self, reporter)
}
/// Checks the files in the project and its dependencies, using the given reporter.
///
/// Use [`set_check_mode`] to update the check mode.
///
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, reporter)
} }
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
@ -105,6 +108,12 @@ impl ProjectDatabase {
self.project().check_file(self, file) self.project().check_file(self, file)
} }
/// Set the check mode for the project.
pub fn set_check_mode(&mut self, mode: CheckMode) {
tracing::debug!("Updating project to check {mode}");
self.project().set_check_mode(self).to(mode);
}
/// Returns a mutable reference to the system. /// Returns a mutable reference to the system.
/// ///
/// WARNING: Triggers a new revision, canceling other database handles. This can lead to deadlock. /// WARNING: Triggers a new revision, canceling other database handles. This can lead to deadlock.
@ -163,17 +172,28 @@ impl std::fmt::Debug for ProjectDatabase {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize))]
pub enum CheckMode { pub enum CheckMode {
/// Checks only the open files in the project. /// Checks the open files in the project.
OpenFiles, OpenFiles,
/// Checks all files in the project, ignoring the open file set. /// Checks all files in the project, ignoring the open file set.
/// ///
/// This includes virtual files, such as those created by the language server. /// This includes virtual files, such as those opened in an editor.
#[default]
AllFiles, AllFiles,
} }
impl fmt::Display for CheckMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CheckMode::OpenFiles => write!(f, "open files"),
CheckMode::AllFiles => write!(f, "all files"),
}
}
}
/// Stores memory usage information. /// Stores memory usage information.
pub struct SalsaMemoryDump { pub struct SalsaMemoryDump {
total_fields: usize, total_fields: usize,
@ -389,12 +409,9 @@ impl IdeDb for ProjectDatabase {}
#[salsa::db] #[salsa::db]
impl SemanticDb for ProjectDatabase { impl SemanticDb for ProjectDatabase {
fn is_file_open(&self, file: File) -> bool { fn should_check_file(&self, file: File) -> bool {
let Some(project) = &self.project else { self.project
return false; .is_some_and(|project| project.should_check_file(self, file))
};
project.is_file_open(self, file)
} }
fn rule_selection(&self, file: File) -> &RuleSelection { fn rule_selection(&self, file: File) -> &RuleSelection {
@ -543,7 +560,7 @@ pub(crate) mod tests {
#[salsa::db] #[salsa::db]
impl ty_python_semantic::Db for TestDb { impl ty_python_semantic::Db for TestDb {
fn is_file_open(&self, file: ruff_db::files::File) -> bool { fn should_check_file(&self, file: ruff_db::files::File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }

View file

@ -18,7 +18,7 @@ use crate::{IOErrorDiagnostic, Project};
/// The implementation uses internal mutability to transition between the lazy and indexed state /// The implementation uses internal mutability to transition between the lazy and indexed state
/// without triggering a new salsa revision. This is safe because the initial indexing happens on first access, /// without triggering a new salsa revision. This is safe because the initial indexing happens on first access,
/// so no query can be depending on the contents of the indexed files before that. All subsequent mutations to /// so no query can be depending on the contents of the indexed files before that. All subsequent mutations to
/// the indexed files must go through `IndexedMut`, which uses the Salsa setter `package.set_file_set` to /// the indexed files must go through `IndexedMut`, which uses the Salsa setter `project.set_file_set` to
/// ensure that Salsa always knows when the set of indexed files have changed. /// ensure that Salsa always knows when the set of indexed files have changed.
#[derive(Debug)] #[derive(Debug)]
pub struct IndexedFiles { pub struct IndexedFiles {
@ -280,7 +280,7 @@ mod tests {
// Calling files a second time should not dead-lock. // Calling files a second time should not dead-lock.
// This can e.g. happen when `check_file` iterates over all files and // This can e.g. happen when `check_file` iterates over all files and
// `is_file_open` queries the open files. // `should_check_file` queries the open files.
let files_2 = project.file_set(&db).get(); let files_2 = project.file_set(&db).get();
match files_2 { match files_2 {

View file

@ -14,6 +14,8 @@ use rustc_hash::FxHashSet;
use salsa::Durability; use salsa::Durability;
use salsa::Setter; use salsa::Setter;
use std::backtrace::BacktraceStatus; use std::backtrace::BacktraceStatus;
use std::collections::hash_set;
use std::iter::FusedIterator;
use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::panic::{AssertUnwindSafe, UnwindSafe};
use std::sync::Arc; use std::sync::Arc;
use thiserror::Error; use thiserror::Error;
@ -54,13 +56,10 @@ pub fn default_lints_registry() -> LintRegistry {
#[salsa::input] #[salsa::input]
#[derive(Debug)] #[derive(Debug)]
pub struct Project { pub struct Project {
/// The files that are open in the project. /// The files that are open in the project, [`None`] if there are no open files.
/// #[returns(ref)]
/// Setting the open files to a non-`None` value changes `check` to only check the
/// open files rather than all files in the project.
#[returns(as_deref)]
#[default] #[default]
open_fileset: Option<Arc<FxHashSet<File>>>, open_fileset: FxHashSet<File>,
/// The first-party files of this project. /// The first-party files of this project.
#[default] #[default]
@ -110,6 +109,13 @@ pub struct Project {
/// Diagnostics that were generated when resolving the project settings. /// Diagnostics that were generated when resolving the project settings.
#[returns(deref)] #[returns(deref)]
settings_diagnostics: Vec<OptionDiagnostic>, settings_diagnostics: Vec<OptionDiagnostic>,
/// The mode in which the project should be checked.
///
/// This changes the behavior of `check` to either check only the open files or all files in
/// the project including the virtual files that might exists in the editor.
#[default]
check_mode: CheckMode,
} }
/// A progress reporter. /// A progress reporter.
@ -207,17 +213,20 @@ impl Project {
self.reload_files(db); self.reload_files(db);
} }
/// Checks all open files in the project and its dependencies. /// Checks the project and its dependencies according to the project's check mode.
pub(crate) fn check( pub(crate) fn check(
self, self,
db: &ProjectDatabase, db: &ProjectDatabase,
mode: CheckMode,
mut reporter: AssertUnwindSafe<&mut dyn ProgressReporter>, mut reporter: AssertUnwindSafe<&mut dyn ProgressReporter>,
) -> Vec<Diagnostic> { ) -> Vec<Diagnostic> {
let project_span = tracing::debug_span!("Project::check"); let project_span = tracing::debug_span!("Project::check");
let _span = project_span.enter(); let _span = project_span.enter();
tracing::debug!("Checking project '{name}'", name = self.name(db)); tracing::debug!(
"Checking {} in project '{name}'",
self.check_mode(db),
name = self.name(db)
);
let mut diagnostics: Vec<Diagnostic> = Vec::new(); let mut diagnostics: Vec<Diagnostic> = Vec::new();
diagnostics.extend( diagnostics.extend(
@ -226,11 +235,7 @@ impl Project {
.map(OptionDiagnostic::to_diagnostic), .map(OptionDiagnostic::to_diagnostic),
); );
let files = match mode { let files = ProjectFiles::new(db, self);
CheckMode::OpenFiles => ProjectFiles::new(db, self),
// TODO: Consider open virtual files as well
CheckMode::AllFiles => ProjectFiles::Indexed(self.files(db)),
};
reporter.set_files(files.len()); reporter.set_files(files.len());
diagnostics.extend( diagnostics.extend(
@ -284,7 +289,7 @@ impl Project {
} }
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> { pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
if !self.is_file_open(db, file) { if !self.should_check_file(db, file) {
return Vec::new(); return Vec::new();
} }
@ -292,8 +297,6 @@ impl Project {
} }
/// Opens a file in the project. /// Opens a file in the project.
///
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
pub fn open_file(self, db: &mut dyn Db, file: File) { pub fn open_file(self, db: &mut dyn Db, file: File) {
tracing::debug!("Opening file `{}`", file.path(db)); tracing::debug!("Opening file `{}`", file.path(db));
@ -340,45 +343,40 @@ impl Project {
} }
} }
/// Returns the open files in the project or `None` if the entire project should be checked. /// Returns the open files in the project or `None` if there are no open files.
pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet<File>> { pub fn open_files(self, db: &dyn Db) -> &FxHashSet<File> {
self.open_fileset(db) self.open_fileset(db)
} }
/// Sets the open files in the project. /// Sets the open files in the project.
///
/// This changes the behavior of `check` to only check the open files rather than all files in the project.
#[tracing::instrument(level = "debug", skip(self, db))] #[tracing::instrument(level = "debug", skip(self, db))]
pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet<File>) { pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet<File>) {
tracing::debug!("Set open project files (count: {})", open_files.len()); tracing::debug!("Set open project files (count: {})", open_files.len());
self.set_open_fileset(db).to(Some(Arc::new(open_files))); self.set_open_fileset(db).to(open_files);
} }
/// This takes the open files from the project and returns them. /// This takes the open files from the project and returns them.
///
/// This changes the behavior of `check` to check all files in the project instead of just the open files.
fn take_open_files(self, db: &mut dyn Db) -> FxHashSet<File> { fn take_open_files(self, db: &mut dyn Db) -> FxHashSet<File> {
tracing::debug!("Take open project files"); tracing::debug!("Take open project files");
// Salsa will cancel any pending queries and remove its own reference to `open_files` // Salsa will cancel any pending queries and remove its own reference to `open_files`
// so that the reference counter to `open_files` now drops to 1. // so that the reference counter to `open_files` now drops to 1.
let open_files = self.set_open_fileset(db).to(None); self.set_open_fileset(db).to(FxHashSet::default())
if let Some(open_files) = open_files {
Arc::try_unwrap(open_files).unwrap()
} else {
FxHashSet::default()
}
} }
/// Returns `true` if the file is open in the project. /// Returns `true` if the file should be checked.
/// ///
/// A file is considered open when: /// This depends on the project's check mode:
/// * explicitly set as an open file using [`open_file`](Self::open_file) /// * For [`OpenFiles`], it checks if the file is either explicitly set as an open file using
/// * It has a [`SystemPath`] and belongs to a package's `src` files /// [`open_file`] or a system virtual path
/// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath) /// * For [`AllFiles`], it checks if the file is either a system virtual path or a part of the
pub fn is_file_open(self, db: &dyn Db, file: File) -> bool { /// indexed files in the project
///
/// [`open_file`]: Self::open_file
/// [`OpenFiles`]: CheckMode::OpenFiles
/// [`AllFiles`]: CheckMode::AllFiles
pub fn should_check_file(self, db: &dyn Db, file: File) -> bool {
let path = file.path(db); let path = file.path(db);
// Try to return early to avoid adding a dependency on `open_files` or `file_set` which // Try to return early to avoid adding a dependency on `open_files` or `file_set` which
@ -387,12 +385,12 @@ impl Project {
return false; return false;
} }
if let Some(open_files) = self.open_files(db) { match self.check_mode(db) {
open_files.contains(&file) CheckMode::OpenFiles => self.open_files(db).contains(&file),
} else if file.path(db).is_system_path() { CheckMode::AllFiles => {
self.files(db).contains(&file) // Virtual files are always checked.
} else { path.is_system_virtual_path() || self.files(db).contains(&file)
file.path(db).is_system_virtual_path() }
} }
} }
@ -524,11 +522,7 @@ pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Box<[Diagnostic]> {
} }
} }
if db if !db.project().open_fileset(db).contains(&file) {
.project()
.open_fileset(db)
.is_none_or(|files| !files.contains(&file))
{
// Drop the AST now that we are done checking this file. It is not currently open, // Drop the AST now that we are done checking this file. It is not currently open,
// so it is unlikely to be accessed again soon. If any queries need to access the AST // so it is unlikely to be accessed again soon. If any queries need to access the AST
// from across files, it will be re-parsed. // from across files, it will be re-parsed.
@ -554,24 +548,23 @@ enum ProjectFiles<'a> {
impl<'a> ProjectFiles<'a> { impl<'a> ProjectFiles<'a> {
fn new(db: &'a dyn Db, project: Project) -> Self { fn new(db: &'a dyn Db, project: Project) -> Self {
if let Some(open_files) = project.open_files(db) { match project.check_mode(db) {
ProjectFiles::OpenFiles(open_files) CheckMode::OpenFiles => ProjectFiles::OpenFiles(project.open_files(db)),
} else { CheckMode::AllFiles => ProjectFiles::Indexed(project.files(db)),
ProjectFiles::Indexed(project.files(db))
} }
} }
fn diagnostics(&self) -> &[IOErrorDiagnostic] { fn diagnostics(&self) -> &[IOErrorDiagnostic] {
match self { match self {
ProjectFiles::OpenFiles(_) => &[], ProjectFiles::OpenFiles(_) => &[],
ProjectFiles::Indexed(indexed) => indexed.diagnostics(), ProjectFiles::Indexed(files) => files.diagnostics(),
} }
} }
fn len(&self) -> usize { fn len(&self) -> usize {
match self { match self {
ProjectFiles::OpenFiles(open_files) => open_files.len(), ProjectFiles::OpenFiles(open_files) => open_files.len(),
ProjectFiles::Indexed(indexed) => indexed.len(), ProjectFiles::Indexed(files) => files.len(),
} }
} }
} }
@ -583,16 +576,14 @@ impl<'a> IntoIterator for &'a ProjectFiles<'a> {
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
match self { match self {
ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()), ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()),
ProjectFiles::Indexed(indexed) => ProjectFilesIter::Indexed { ProjectFiles::Indexed(files) => ProjectFilesIter::Indexed(files.into_iter()),
files: indexed.into_iter(),
},
} }
} }
} }
enum ProjectFilesIter<'db> { enum ProjectFilesIter<'db> {
OpenFiles(std::collections::hash_set::Iter<'db, File>), OpenFiles(hash_set::Iter<'db, File>),
Indexed { files: files::IndexedIter<'db> }, Indexed(files::IndexedIter<'db>),
} }
impl Iterator for ProjectFilesIter<'_> { impl Iterator for ProjectFilesIter<'_> {
@ -601,11 +592,13 @@ impl Iterator for ProjectFilesIter<'_> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { match self {
ProjectFilesIter::OpenFiles(files) => files.next().copied(), ProjectFilesIter::OpenFiles(files) => files.next().copied(),
ProjectFilesIter::Indexed { files } => files.next(), ProjectFilesIter::Indexed(files) => files.next(),
} }
} }
} }
impl FusedIterator for ProjectFilesIter<'_> {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct IOErrorDiagnostic { pub struct IOErrorDiagnostic {
file: Option<File>, file: Option<File>,

View file

@ -5,7 +5,8 @@ use ruff_db::files::File;
/// Database giving access to semantic information about a Python program. /// Database giving access to semantic information about a Python program.
#[salsa::db] #[salsa::db]
pub trait Db: SourceDb { pub trait Db: SourceDb {
fn is_file_open(&self, file: File) -> bool; /// Returns `true` if the file should be checked.
fn should_check_file(&self, file: File) -> bool;
/// Resolves the rule selection for a given file. /// Resolves the rule selection for a given file.
fn rule_selection(&self, file: File) -> &RuleSelection; fn rule_selection(&self, file: File) -> &RuleSelection;
@ -114,7 +115,7 @@ pub(crate) mod tests {
#[salsa::db] #[salsa::db]
impl Db for TestDb { impl Db for TestDb {
fn is_file_open(&self, file: File) -> bool { fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }

View file

@ -2522,7 +2522,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> {
} }
fn report_semantic_error(&self, error: SemanticSyntaxError) { fn report_semantic_error(&self, error: SemanticSyntaxError) {
if self.db.is_file_open(self.file) { if self.db.should_check_file(self.file) {
self.semantic_syntax_errors.borrow_mut().push(error); self.semantic_syntax_errors.borrow_mut().push(error);
} }
} }

View file

@ -399,7 +399,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
// returns a rule selector for a given file that respects the package's settings, // returns a rule selector for a given file that respects the package's settings,
// any global pragma comments in the file, and any per-file-ignores. // any global pragma comments in the file, and any per-file-ignores.
if !ctx.db.is_file_open(ctx.file) { if !ctx.db.should_check_file(ctx.file) {
return None; return None;
} }
let lint_id = LintId::of(lint); let lint_id = LintId::of(lint);
@ -573,7 +573,7 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> {
id: DiagnosticId, id: DiagnosticId,
severity: Severity, severity: Severity,
) -> Option<DiagnosticGuardBuilder<'db, 'ctx>> { ) -> Option<DiagnosticGuardBuilder<'db, 'ctx>> {
if !ctx.db.is_file_open(ctx.file) { if !ctx.db.should_check_file(ctx.file) {
return None; return None;
} }
Some(DiagnosticGuardBuilder { ctx, id, severity }) Some(DiagnosticGuardBuilder { ctx, id, severity })

View file

@ -244,7 +244,7 @@ impl ruff_db::Db for CorpusDb {
#[salsa::db] #[salsa::db]
impl ty_python_semantic::Db for CorpusDb { impl ty_python_semantic::Db for CorpusDb {
fn is_file_open(&self, file: File) -> bool { fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }

View file

@ -98,7 +98,7 @@ impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
meta: &tracing::Metadata<'_>, meta: &tracing::Metadata<'_>,
_: &tracing_subscriber::layer::Context<'_, S>, _: &tracing_subscriber::layer::Context<'_, S>,
) -> bool { ) -> bool {
let filter = if meta.target().starts_with("ty") { let filter = if meta.target().starts_with("ty") || meta.target().starts_with("ruff") {
self.filter.trace_level() self.filter.trace_level()
} else { } else {
tracing::Level::WARN tracing::Level::WARN

View file

@ -8,7 +8,8 @@ use crate::system::AnySystemPath;
use lsp_server::ErrorCode; use lsp_server::ErrorCode;
use lsp_types::notification::DidCloseTextDocument; use lsp_types::notification::DidCloseTextDocument;
use lsp_types::{DidCloseTextDocumentParams, TextDocumentIdentifier}; use lsp_types::{DidCloseTextDocumentParams, TextDocumentIdentifier};
use ty_project::watch::ChangeEvent; use ruff_db::Db as _;
use ty_project::Db as _;
pub(crate) struct DidCloseTextDocumentHandler; pub(crate) struct DidCloseTextDocumentHandler;
@ -38,11 +39,29 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
.close_document(&key) .close_document(&key)
.with_failure_code(ErrorCode::InternalError)?; .with_failure_code(ErrorCode::InternalError)?;
if let AnySystemPath::SystemVirtual(virtual_path) = key.path() { let path = key.path();
session.apply_changes( let db = session.project_db_mut(path);
key.path(),
vec![ChangeEvent::DeletedVirtual(virtual_path.clone())], match path {
); AnySystemPath::System(system_path) => {
if let Some(file) = db.files().try_system(db, system_path) {
db.project().close_file(db, file);
} else {
// This can only fail when the path is a directory or it doesn't exists but the
// file should exists for this handler in this branch. This is because every
// close call is preceded by an open call, which ensures that the file is
// interned in the lookup table (`Files`).
tracing::warn!("Salsa file does not exists for {}", system_path);
}
}
AnySystemPath::SystemVirtual(virtual_path) => {
if let Some(virtual_file) = db.files().try_virtual_file(virtual_path) {
db.project().close_file(db, virtual_file.file());
virtual_file.close(db);
} else {
tracing::warn!("Salsa virtual file does not exists for {}", virtual_path);
}
}
} }
if !session.global_settings().diagnostic_mode().is_workspace() { if !session.global_settings().diagnostic_mode().is_workspace() {

View file

@ -1,5 +1,9 @@
use lsp_types::notification::DidOpenTextDocument; use lsp_types::notification::DidOpenTextDocument;
use lsp_types::{DidOpenTextDocumentParams, TextDocumentItem}; use lsp_types::{DidOpenTextDocumentParams, TextDocumentItem};
use ruff_db::Db as _;
use ruff_db::files::system_path_to_file;
use ty_project::Db as _;
use ty_project::watch::{ChangeEvent, CreatedKind};
use crate::TextDocument; use crate::TextDocument;
use crate::server::Result; use crate::server::Result;
@ -8,8 +12,6 @@ use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::session::Session; use crate::session::Session;
use crate::session::client::Client; use crate::session::client::Client;
use crate::system::AnySystemPath; use crate::system::AnySystemPath;
use ruff_db::Db;
use ty_project::watch::ChangeEvent;
pub(crate) struct DidOpenTextDocumentHandler; pub(crate) struct DidOpenTextDocumentHandler;
@ -46,13 +48,38 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
let path = key.path(); let path = key.path();
// This is a "maybe" because the `File` might've not been interned yet i.e., the
// `try_system` call will return `None` which doesn't mean that the file is new, it's just
// that the server didn't need the file yet.
let is_maybe_new_system_file = path.as_system().is_some_and(|system_path| {
let db = session.project_db(path);
db.files()
.try_system(db, system_path)
.is_none_or(|file| !file.exists(db))
});
match path { match path {
AnySystemPath::System(system_path) => { AnySystemPath::System(system_path) => {
session.apply_changes(path, vec![ChangeEvent::Opened(system_path.clone())]); let event = if is_maybe_new_system_file {
ChangeEvent::Created {
path: system_path.clone(),
kind: CreatedKind::File,
}
} else {
ChangeEvent::Opened(system_path.clone())
};
session.apply_changes(path, vec![event]);
let db = session.project_db_mut(path);
match system_path_to_file(db, system_path) {
Ok(file) => db.project().open_file(db, file),
Err(err) => tracing::warn!("Failed to open file {system_path}: {err}"),
}
} }
AnySystemPath::SystemVirtual(virtual_path) => { AnySystemPath::SystemVirtual(virtual_path) => {
let db = session.project_db_mut(path); let db = session.project_db_mut(path);
db.files().virtual_file(db, virtual_path); let virtual_file = db.files().virtual_file(db, virtual_path);
db.project().open_file(db, virtual_file.file());
} }
} }

View file

@ -7,7 +7,6 @@ use lsp_types::{
WorkspaceFullDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport,
}; };
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use ty_project::CheckMode;
use crate::server::Result; use crate::server::Result;
use crate::server::api::diagnostics::to_lsp_diagnostic; use crate::server::api::diagnostics::to_lsp_diagnostic;
@ -33,6 +32,8 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
let index = snapshot.index(); let index = snapshot.index();
if !index.global_settings().diagnostic_mode().is_workspace() { if !index.global_settings().diagnostic_mode().is_workspace() {
// VS Code sends us the workspace diagnostic request every 2 seconds, so these logs can
// be quite verbose.
tracing::trace!("Workspace diagnostics is disabled; returning empty report"); tracing::trace!("Workspace diagnostics is disabled; returning empty report");
return Ok(WorkspaceDiagnosticReportResult::Report( return Ok(WorkspaceDiagnosticReportResult::Report(
WorkspaceDiagnosticReport { items: vec![] }, WorkspaceDiagnosticReport { items: vec![] },
@ -42,7 +43,7 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
let mut items = Vec::new(); let mut items = Vec::new();
for db in snapshot.projects() { for db in snapshot.projects() {
let diagnostics = db.check_with_mode(CheckMode::AllFiles); let diagnostics = db.check();
// Group diagnostics by URL // Group diagnostics by URL
let mut diagnostics_by_url: FxHashMap<Url, Vec<_>> = FxHashMap::default(); let mut diagnostics_by_url: FxHashMap<Url, Vec<_>> = FxHashMap::default();

View file

@ -103,8 +103,8 @@ impl Session {
let index = Arc::new(Index::new(global_options.into_settings())); let index = Arc::new(Index::new(global_options.into_settings()));
let mut workspaces = Workspaces::default(); let mut workspaces = Workspaces::default();
for (url, options) in workspace_folders { for (url, workspace_options) in workspace_folders {
workspaces.register(url, options.into_settings())?; workspaces.register(url, workspace_options.into_settings())?;
} }
Ok(Self { Ok(Self {
@ -347,7 +347,10 @@ impl Session {
}); });
let (root, db) = match project { let (root, db) = match project {
Ok(db) => (root, db), Ok(mut db) => {
db.set_check_mode(workspace.settings.diagnostic_mode().into_check_mode());
(root, db)
}
Err(err) => { Err(err) => {
tracing::error!( tracing::error!(
"Failed to create project for `{root}`: {err:#}. Falling back to default settings" "Failed to create project for `{root}`: {err:#}. Falling back to default settings"
@ -747,17 +750,22 @@ impl DefaultProject {
pub(crate) fn get(&self, index: Option<&Arc<Index>>) -> &ProjectState { pub(crate) fn get(&self, index: Option<&Arc<Index>>) -> &ProjectState {
self.0.get_or_init(|| { self.0.get_or_init(|| {
tracing::info!("Initialize default project"); tracing::info!("Initializing the default project");
let system = LSPSystem::new(index.unwrap().clone()); let index = index.unwrap();
let system = LSPSystem::new(index.clone());
let metadata = ProjectMetadata::from_options( let metadata = ProjectMetadata::from_options(
Options::default(), Options::default(),
system.current_directory().to_path_buf(), system.current_directory().to_path_buf(),
None, None,
) )
.unwrap(); .unwrap();
let mut db = ProjectDatabase::new(metadata, system).unwrap();
db.set_check_mode(index.global_settings().diagnostic_mode().into_check_mode());
ProjectState { ProjectState {
db: ProjectDatabase::new(metadata, system).unwrap(), db,
untracked_files_with_pushed_diagnostics: Vec::new(), untracked_files_with_pushed_diagnostics: Vec::new(),
} }
}) })

View file

@ -176,7 +176,7 @@ impl Index {
// may need revisiting in the future as we support more editors with notebook support. // may need revisiting in the future as we support more editors with notebook support.
if let DocumentKey::NotebookCell { cell_url, .. } = key { if let DocumentKey::NotebookCell { cell_url, .. } = key {
if self.notebook_cells.remove(cell_url).is_none() { if self.notebook_cells.remove(cell_url).is_none() {
tracing::warn!("Tried to remove a notebook cell that does not exist: {cell_url}",); tracing::warn!("Tried to remove a notebook cell that does not exist: {cell_url}");
} }
return Ok(()); return Ok(());
} }

View file

@ -3,6 +3,7 @@ use ruff_db::system::SystemPathBuf;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::Deserialize; use serde::Deserialize;
use ty_project::CheckMode;
use ty_project::metadata::Options; use ty_project::metadata::Options;
use ty_project::metadata::options::ProjectOptionsOverrides; use ty_project::metadata::options::ProjectOptionsOverrides;
use ty_project::metadata::value::{RangedValue, RelativePathBuf}; use ty_project::metadata::value::{RangedValue, RelativePathBuf};
@ -76,6 +77,13 @@ impl DiagnosticMode {
pub(crate) fn is_workspace(self) -> bool { pub(crate) fn is_workspace(self) -> bool {
matches!(self, DiagnosticMode::Workspace) matches!(self, DiagnosticMode::Workspace)
} }
pub(crate) fn into_check_mode(self) -> CheckMode {
match self {
DiagnosticMode::OpenFilesOnly => CheckMode::OpenFiles,
DiagnosticMode::Workspace => CheckMode::AllFiles,
}
}
} }
impl ClientOptions { impl ClientOptions {

View file

@ -77,7 +77,7 @@ impl SourceDb for Db {
#[salsa::db] #[salsa::db]
impl SemanticDb for Db { impl SemanticDb for Db {
fn is_file_open(&self, file: File) -> bool { fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }

View file

@ -16,10 +16,10 @@ use ruff_source_file::{LineIndex, OneIndexed, SourceLocation};
use ruff_text_size::{Ranged, TextSize}; use ruff_text_size::{Ranged, TextSize};
use ty_ide::signature_help; use ty_ide::signature_help;
use ty_ide::{MarkupKind, goto_type_definition, hover, inlay_hints}; use ty_ide::{MarkupKind, goto_type_definition, hover, inlay_hints};
use ty_project::ProjectMetadata;
use ty_project::metadata::options::Options; use ty_project::metadata::options::Options;
use ty_project::metadata::value::ValueSource; use ty_project::metadata::value::ValueSource;
use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
use ty_project::{CheckMode, ProjectMetadata};
use ty_project::{Db, ProjectDatabase}; use ty_project::{Db, ProjectDatabase};
use ty_python_semantic::Program; use ty_python_semantic::Program;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -76,7 +76,11 @@ impl Workspace {
let project = ProjectMetadata::from_options(options, SystemPathBuf::from(root), None) let project = ProjectMetadata::from_options(options, SystemPathBuf::from(root), None)
.map_err(into_error)?; .map_err(into_error)?;
let db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?; let mut db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?;
// By default, it will check all files in the project but we only want to check the open
// files in the playground.
db.set_check_mode(CheckMode::OpenFiles);
Ok(Self { Ok(Self {
db, db,

View file

@ -82,7 +82,7 @@ impl DbWithTestSystem for TestDb {
#[salsa::db] #[salsa::db]
impl SemanticDb for TestDb { impl SemanticDb for TestDb {
fn is_file_open(&self, file: File) -> bool { fn should_check_file(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }