Skip to content

Commit 0d912c4

Browse files
refactor: apply project refresh briefly and warm queries off-lock
1 parent a077703 commit 0d912c4

7 files changed

Lines changed: 436 additions & 92 deletions

File tree

crates/djls-db/src/db.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,40 @@ mod invalidation_tests {
570570
);
571571
}
572572

573+
#[test]
574+
fn tagspecs_settings_change_reports_semantic_change() {
575+
let tempdir = tempdir().unwrap();
576+
let root = Utf8PathBuf::from_path_buf(tempdir.path().to_path_buf()).unwrap();
577+
std::fs::write(
578+
root.join("djls.toml").as_std_path(),
579+
r#"
580+
[tagspecs]
581+
version = "0.6.0"
582+
583+
[[tagspecs.libraries]]
584+
module = "myapp.templatetags.custom"
585+
586+
[[tagspecs.libraries.tags]]
587+
name = "switch"
588+
type = "block"
589+
"#,
590+
)
591+
.unwrap();
592+
593+
let mut db = DjangoDatabase::new(
594+
Arc::new(InMemoryFileSystem::new()),
595+
&Settings::default(),
596+
Some(root.as_path()),
597+
);
598+
let settings = Settings::new(root.as_path(), None).unwrap();
599+
600+
let update = db.set_settings(settings);
601+
602+
assert!(!update.env_changed);
603+
assert!(!update.diagnostics_changed);
604+
assert!(update.semantic_changed);
605+
}
606+
573607
#[test]
574608
fn tagspecs_change_invalidates_compute_tag_specs() {
575609
let (mut db, event_log) = test_db_with_project();

crates/djls-db/src/settings.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::db::DjangoDatabase;
1111
pub struct SettingsUpdate {
1212
pub env_changed: bool,
1313
pub diagnostics_changed: bool,
14+
pub semantic_changed: bool,
1415
}
1516

1617
impl DjangoDatabase {
@@ -37,29 +38,32 @@ impl DjangoDatabase {
3738
let previous = self.settings();
3839
*self.settings.lock().unwrap() = settings;
3940

40-
let diagnostics_changed = previous.diagnostics() != self.settings().diagnostics();
41+
let current = self.settings();
42+
let diagnostics_changed = previous.diagnostics() != current.diagnostics();
43+
let semantic_changed = previous.tagspecs() != current.tagspecs();
4144

4245
if self.project().is_some() {
43-
let settings = self.settings();
44-
let env_changed = self.update_project_from_settings(&settings);
46+
let env_changed = self.update_project_from_settings(&current);
4547
return SettingsUpdate {
4648
env_changed,
4749
diagnostics_changed,
50+
semantic_changed,
4851
};
4952
}
5053

5154
SettingsUpdate {
5255
env_changed: false,
5356
diagnostics_changed,
57+
semantic_changed,
5458
}
5559
}
5660

5761
/// Update an existing project's fields from new settings, only calling
5862
/// Salsa setters when values actually change (Ruff/RA pattern).
5963
///
6064
/// Returns `true` if environment-related fields changed (`interpreter`,
61-
/// `django_settings_module`, `pythonpath`), indicating project data should
62-
/// be refreshed.
65+
/// `django_settings_module`, `pythonpath`, `env_vars`), indicating project
66+
/// data should be refreshed by the caller.
6367
pub(crate) fn update_project_from_settings(&mut self, settings: &Settings) -> bool {
6468
let Some(project) = self.project() else {
6569
return false;
@@ -106,10 +110,6 @@ impl DjangoDatabase {
106110
project.set_tagspecs(self).to(new_tagspecs);
107111
}
108112

109-
if env_changed {
110-
project.refresh_source_roots(self);
111-
}
112-
113113
env_changed
114114
}
115115
}

crates/djls-project/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ pub use symbols::TemplateLibraries;
8484
pub use symbols::TemplateLibrary;
8585
pub use symbols::TemplateSymbol;
8686
pub use symbols::TemplateSymbolKind;
87+
pub use sync::apply_refresh;
88+
pub use sync::compute_refresh;
8789
pub use sync::refresh_external_data;
8890
pub use templates::FindTemplateResult;
8991
pub use templates::ProjectTemplateFile;

crates/djls-project/src/project.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use djls_conf::TagSpecDef;
77
use djls_source::File;
88
use djls_source::FileSystem;
99
use salsa::Durability;
10-
use salsa::Setter;
1110

1211
use crate::db::Db as ProjectDb;
1312
use crate::names::ModulePath;
@@ -88,19 +87,6 @@ pub struct Project {
8887
}
8988

9089
impl Project {
91-
pub fn refresh_source_roots(self, db: &mut dyn ProjectDb) {
92-
let search_paths = SearchPaths::from_project_settings(
93-
db.file_system(),
94-
self.root(db),
95-
self.interpreter(db),
96-
self.pythonpath(db),
97-
);
98-
search_paths.register_roots(db);
99-
if self.search_paths(db) != &search_paths {
100-
self.set_search_paths(db).to(search_paths);
101-
}
102-
}
103-
10490
pub(crate) fn touch_search_path_roots(self, db: &dyn ProjectDb) {
10591
for search_path in self.search_paths(db).iter() {
10692
if let Some(root) = db.files().root(db, search_path.path()) {

crates/djls-project/src/sync.rs

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
//! Django, Python, and the filesystem for facts, then writes changed facts to
55
//! the `Project` input. Pure semantic derivation stays in tracked queries.
66
7+
use camino::Utf8PathBuf;
8+
use salsa::Setter;
9+
710
use crate::db::Db as ProjectDb;
811
use crate::environment::templatetag_candidate_paths;
9-
use crate::project::Project;
12+
use crate::resolve::SearchPaths;
1013
use crate::resolve::model_modules;
1114
use crate::resolve::templatetag_modules;
1215
use crate::settings::settings_source_files;
@@ -18,33 +21,32 @@ use crate::settings::settings_source_files;
1821
/// into the `Project` input, then lets tracked semantic queries handle editor
1922
/// file contents and downstream derivations.
2023
pub fn refresh_external_data(db: &mut dyn ProjectDb) {
21-
let Some(project) = db.project() else {
24+
let Some(refresh) = compute_refresh(db) else {
2225
return;
2326
};
24-
25-
project.refresh_source_roots(db);
26-
refresh_python_modules(db, project);
27+
apply_refresh(db, refresh);
2728
}
2829

29-
fn refresh_python_modules(db: &mut dyn ProjectDb, project: Project) {
30-
// The LSP currently has no watched-file stream for dependency roots. Treat
31-
// an explicit refresh as the freshness boundary for module discovery and
32-
// currently discovered Python files.
33-
let roots: Vec<_> = project
34-
.search_paths(db)
35-
.iter()
36-
.filter_map(|search_path| db.files().root(db, search_path.path()))
37-
.collect();
30+
#[derive(Clone, Debug, PartialEq, Eq)]
31+
pub struct RefreshData {
32+
search_paths: SearchPaths,
33+
file_paths: Vec<Utf8PathBuf>,
34+
}
3835

39-
for root in roots {
40-
db.bump_file_root_revision(root);
41-
}
36+
pub fn compute_refresh(db: &dyn ProjectDb) -> Option<RefreshData> {
37+
let project = db.project()?;
38+
let search_paths = SearchPaths::from_project_settings(
39+
db.file_system(),
40+
project.root(db),
41+
project.interpreter(db),
42+
project.pythonpath(db),
43+
);
4244

43-
for file in settings_source_files(db, project) {
44-
db.bump_file_revision(file);
45-
}
45+
let mut file_paths: Vec<_> = settings_source_files(db, project)
46+
.into_iter()
47+
.map(|file| file.path(db).to_path_buf())
48+
.collect();
4649

47-
let mut file_paths = Vec::new();
4850
file_paths.extend(
4951
model_modules(db, project)
5052
.iter()
@@ -61,6 +63,39 @@ fn refresh_python_modules(db: &mut dyn ProjectDb, project: Project) {
6163
file_paths.sort();
6264
file_paths.dedup();
6365

66+
Some(RefreshData {
67+
search_paths,
68+
file_paths,
69+
})
70+
}
71+
72+
pub fn apply_refresh(db: &mut dyn ProjectDb, refresh: RefreshData) {
73+
let Some(project) = db.project() else {
74+
return;
75+
};
76+
let RefreshData {
77+
search_paths,
78+
file_paths,
79+
} = refresh;
80+
81+
search_paths.register_roots(db);
82+
if project.search_paths(db) != &search_paths {
83+
project.set_search_paths(db).to(search_paths);
84+
}
85+
86+
// The LSP currently has no watched-file stream for dependency roots. Treat
87+
// an explicit refresh as the freshness boundary for module discovery and
88+
// currently discovered Python files.
89+
let roots: Vec<_> = project
90+
.search_paths(db)
91+
.iter()
92+
.filter_map(|search_path| db.files().root(db, search_path.path()))
93+
.collect();
94+
95+
for root in roots {
96+
db.bump_file_root_revision(root);
97+
}
98+
6499
for path in file_paths {
65100
let file = db.get_or_create_file(&path);
66101
db.bump_file_revision(file);

crates/djls-project/tests/resolve.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,37 @@ fn refresh_external_data_discovers_site_packages_created_after_bootstrap() {
693693
assert!(graph.get("VenvArticle").is_some());
694694
}
695695

696+
#[test]
697+
fn compute_and_apply_refresh_discovers_site_packages_created_after_bootstrap() {
698+
let mut db = TestDatabase::new();
699+
let search_paths = SearchPaths::from_project_settings(
700+
db.file_system(),
701+
Utf8Path::new("/project"),
702+
&Interpreter::Auto,
703+
&[],
704+
);
705+
search_paths.register_roots(&db);
706+
let project = ProjectFixture::new("/project")
707+
.search_paths(search_paths)
708+
.interpreter(Interpreter::Auto)
709+
.register_roots(false)
710+
.install(&mut db);
711+
712+
db.add_file(
713+
"/project/.venv/lib/python3.12/site-packages/blog/models.py",
714+
"from django.db import models\nclass VenvArticle(models.Model):\n pass\n",
715+
);
716+
717+
let refresh = compute_refresh(&db).expect("project should be configured");
718+
apply_refresh(&mut db, refresh);
719+
720+
assert!(project.search_paths(&db).iter().any(|search_path| {
721+
search_path.path() == Utf8Path::new("/project/.venv/lib/python3.12/site-packages")
722+
}));
723+
let graph = compute_model_graph(&db, project);
724+
assert!(graph.get("VenvArticle").is_some());
725+
}
726+
696727
#[test]
697728
fn discover_external_model_files_finds_models() {
698729
let tmp = tempfile::TempDir::new().unwrap();

0 commit comments

Comments
 (0)