Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/ty_project/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ pub trait Db: SemanticDb {
#[salsa::db]
#[derive(Clone)]
pub struct ProjectDatabase {
// This handle must remain stable for the lifetime of the database.
//
// Many tracked queries branch on the untracked `db.project()` read before
// consulting tracked `Project` fields. Replacing the handle during reload
// therefore changes query behavior outside salsa's dependency graph and can
// trigger stale results.
//
// Structural reloads must update the existing `Project` in place via salsa
// setters instead of swapping in a freshly constructed handle.
project: Option<Project>,
files: Files,

Expand Down
32 changes: 7 additions & 25 deletions crates/ty_project/src/db/changes.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::ProjectMetadata;
use crate::db::{Db, ProjectDatabase};
use crate::metadata::options::ProjectOptionsOverrides;
use crate::watch::{ChangeEvent, CreatedKind, DeletedKind};
use crate::{Project, ProjectMetadata};
use std::collections::BTreeSet;

use crate::walk::ProjectFilesWalker;
Expand Down Expand Up @@ -38,7 +38,7 @@ impl ProjectDatabase {
changes: Vec<ChangeEvent>,
project_options_overrides: Option<&ProjectOptionsOverrides>,
) -> ChangeResult {
let mut project = self.project();
let project = self.project();
let project_root = project.root(self).to_path_buf();
let config_file_override =
project_options_overrides.and_then(|options| options.config_file_override.clone());
Expand All @@ -59,11 +59,12 @@ impl ProjectDatabase {
let mut sync_recursively = BTreeSet::default();

for change in changes {
tracing::trace!("Handle change: {:?}", change);
tracing::debug!("Handling file watcher change event: {:?}", change);
Copy link
Copy Markdown
Member Author

@MichaReiser MichaReiser Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was torn on whether promoting this to debug is a good idea, but debugging production file watcher issues is sort of impossible without knowing what "happened". But it has the downside of being somewhat verbose.


if let Some(path) = change.system_path() {
if let Some(config_file) = &config_file_override {
if config_file.as_path() == path {
File::sync_path(self, path);
result.project_changed = true;

continue;
Expand All @@ -74,6 +75,7 @@ impl ProjectDatabase {
path.file_name(),
Some(".gitignore" | ".ignore" | "ty.toml" | "pyproject.toml")
) {
File::sync_path(self, path);
// Changes to ignore files or settings can change the project structure or add/remove files.
result.project_changed = true;

Expand Down Expand Up @@ -279,28 +281,8 @@ impl ProjectDatabase {
}
}

if metadata.root() == project.root(self) {
tracing::debug!("Reloading project after structural change");
project.reload(self, metadata);
} else {
match Project::from_metadata(self, metadata, &FallibleStrategy) {
Ok(new_project) => {
tracing::debug!("Replace project after structural change");
project = new_project;
}
Err(error) => {
tracing::error!(
"Keeping old project configuration because loading the new settings failed with: {error}"
);

project
.set_settings_diagnostics(self)
.to(vec![error.into_diagnostic()]);
}
}

self.project = Some(project);
}
tracing::debug!("Reloading project after structural change");
project.reload(self, metadata);
}
Err(error) => {
tracing::error!(
Expand Down
61 changes: 35 additions & 26 deletions crates/ty_project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,22 +184,26 @@ impl Project {
.options()
.to_settings(db, metadata.root(), strategy)?;

// This adds a file root for the project itself. This enables
// tracking of when changes are made to the files in a project
// at the directory level. At time of writing (2025-07-17),
// this is used for caching completions for submodules.
db.files()
.try_add_root(db, metadata.root(), FileRootKind::Project);

let project = Project::builder(Box::new(metadata), Box::new(settings), diagnostics)
.durability(Durability::MEDIUM)
.open_fileset_durability(Durability::LOW)
.file_set_durability(Durability::LOW)
.new(db);

project.try_add_file_root(db);

Ok(project)
}

fn try_add_file_root(self, db: &dyn Db) {
// This adds a file root for the project itself. This enables
// tracking of when changes are made to the files in a project
// at the directory level. At time of writing (2025-07-17),
// this is used for caching completions for submodules.
db.files()
.try_add_root(db, self.root(db), FileRootKind::Project);
}

pub fn root(self, db: &dyn Db) -> &SystemPath {
self.metadata(db).root()
}
Expand Down Expand Up @@ -242,32 +246,37 @@ impl Project {

pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {
tracing::debug!("Reloading project");
assert_eq!(self.root(db), metadata.root());

if &metadata != self.metadata(db) {
match metadata
.options()
.to_settings(db, metadata.root(), &FallibleStrategy)
{
Ok((settings, settings_diagnostics)) => {
if self.settings(db) != &settings {
self.set_settings(db).to(Box::new(settings));
}
self.reload_files(db);

if self.settings_diagnostics(db) != settings_diagnostics {
self.set_settings_diagnostics(db).to(settings_diagnostics);
}
if &metadata == self.metadata(db) {
return;
}

match metadata
.options()
.to_settings(db, metadata.root(), &FallibleStrategy)
{
Ok((settings, settings_diagnostics)) => {
if self.settings(db) != &settings {
self.set_settings(db).to(Box::new(settings));
}
Err(error) => {
self.set_settings_diagnostics(db)
.to(vec![error.into_diagnostic()]);

if self.settings_diagnostics(db) != settings_diagnostics {
self.set_settings_diagnostics(db).to(settings_diagnostics);
}
}

self.set_metadata(db).to(Box::new(metadata));
Err(error) => {
tracing::warn!(
"Keeping old project configuration because loading the new settings failed with: {error}"
);
self.set_settings_diagnostics(db)
.to(vec![error.into_diagnostic()]);
}
}

self.reload_files(db);
self.set_metadata(db).to(Box::new(metadata));
self.try_add_file_root(db);
}

/// Checks the project and its dependencies according to the project's check mode.
Expand Down
Loading