mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-11 04:46:38 +00:00
Refactor project state management to use Salsa (#216)
Some checks are pending
lint / cargo-check (push) Waiting to run
lint / pre-commit (push) Waiting to run
lint / rustfmt (push) Waiting to run
lint / clippy (push) Waiting to run
release / build (push) Waiting to run
release / test (push) Waiting to run
release / release (push) Blocked by required conditions
test / generate-matrix (push) Waiting to run
test / Python , Django () (push) Blocked by required conditions
test / tests (push) Blocked by required conditions
zizmor 🌈 / zizmor latest via PyPI (push) Waiting to run
Some checks are pending
lint / cargo-check (push) Waiting to run
lint / pre-commit (push) Waiting to run
lint / rustfmt (push) Waiting to run
lint / clippy (push) Waiting to run
release / build (push) Waiting to run
release / test (push) Waiting to run
release / release (push) Blocked by required conditions
test / generate-matrix (push) Waiting to run
test / Python , Django () (push) Blocked by required conditions
test / tests (push) Blocked by required conditions
zizmor 🌈 / zizmor latest via PyPI (push) Waiting to run
This commit is contained in:
parent
007a009d33
commit
d60b597478
15 changed files with 516 additions and 343 deletions
|
@ -21,7 +21,7 @@ pub enum ConfigError {
|
|||
PyprojectSerialize(#[from] toml::ser::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, PartialEq)]
|
||||
#[derive(Debug, Deserialize, Default, PartialEq, Clone)]
|
||||
pub struct Settings {
|
||||
#[serde(default)]
|
||||
debug: bool,
|
||||
|
|
|
@ -2,30 +2,33 @@
|
|||
//!
|
||||
//! This module extends the workspace database trait with project-specific
|
||||
//! functionality including metadata access and Python environment discovery.
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! Following the Salsa pattern established in workspace and templates crates:
|
||||
//! - `DjangoProject` is a Salsa input representing external project state
|
||||
//! - Tracked functions compute derived values (Python env, Django config)
|
||||
//! - Database trait provides stable configuration (metadata, template tags)
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use djls_workspace::Db as WorkspaceDb;
|
||||
|
||||
use crate::meta::ProjectMetadata;
|
||||
use crate::python::PythonEnvironment;
|
||||
use crate::inspector::pool::InspectorPool;
|
||||
use crate::project::Project;
|
||||
|
||||
/// Project-specific database trait extending the workspace database
|
||||
#[salsa::db]
|
||||
pub trait Db: WorkspaceDb {
|
||||
/// Get the project metadata containing root path and venv configuration
|
||||
fn metadata(&self) -> &ProjectMetadata;
|
||||
}
|
||||
/// Get the current project (if set)
|
||||
fn project(&self) -> Option<Project>;
|
||||
|
||||
/// Find the Python environment for the project.
|
||||
///
|
||||
/// This Salsa tracked function discovers the Python environment based on:
|
||||
/// 1. Explicit venv path from metadata
|
||||
/// 2. VIRTUAL_ENV environment variable
|
||||
/// 3. Common venv directories in project root (.venv, venv, env, .env)
|
||||
/// 4. System Python as fallback
|
||||
#[salsa::tracked]
|
||||
pub fn find_python_environment(db: &dyn Db) -> Option<PythonEnvironment> {
|
||||
let project_path = db.metadata().root().as_path();
|
||||
let venv_path = db.metadata().venv().and_then(|p| p.to_str());
|
||||
/// Get the shared inspector pool for executing Python queries
|
||||
fn inspector_pool(&self) -> Arc<InspectorPool>;
|
||||
|
||||
PythonEnvironment::new(project_path, venv_path)
|
||||
/// Get the project root path if a project is set
|
||||
fn project_path(&self) -> Option<&Path> {
|
||||
self.project().map(|p| p.root(self).as_path())
|
||||
}
|
||||
}
|
||||
|
|
71
crates/djls-project/src/django.rs
Normal file
71
crates/djls-project/src/django.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
mod templatetags;
|
||||
|
||||
pub use templatetags::get_templatetags;
|
||||
pub use templatetags::TemplateTags;
|
||||
|
||||
use crate::db::Db as ProjectDb;
|
||||
use crate::inspector::inspector_run;
|
||||
use crate::inspector::queries::Query;
|
||||
use crate::python::python_environment;
|
||||
use crate::Project;
|
||||
|
||||
/// Check if Django is available for the current project.
|
||||
///
|
||||
/// This determines if Django is installed and configured in the Python environment.
|
||||
/// First consults the inspector, then falls back to environment detection.
|
||||
#[salsa::tracked]
|
||||
pub fn django_available(db: &dyn ProjectDb, project: Project) -> bool {
|
||||
// First try to get Django availability from inspector
|
||||
if let Some(json_data) = inspector_run(db, Query::DjangoInit) {
|
||||
// Parse the JSON response - expect a boolean
|
||||
if let Ok(available) = serde_json::from_str::<bool>(&json_data) {
|
||||
return available;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to environment detection
|
||||
python_environment(db, project).is_some()
|
||||
}
|
||||
|
||||
/// Get the Django settings module name for the current project.
|
||||
///
|
||||
/// Returns the settings_module_override from project, or inspector result,
|
||||
/// or DJANGO_SETTINGS_MODULE env var, or attempts to detect it.
|
||||
#[salsa::tracked]
|
||||
pub fn django_settings_module(db: &dyn ProjectDb, project: Project) -> Option<String> {
|
||||
// Check project override first
|
||||
if let Some(settings) = project.settings_module(db) {
|
||||
return Some(settings.clone());
|
||||
}
|
||||
|
||||
// Try to get settings module from inspector
|
||||
if let Some(json_data) = inspector_run(db, Query::DjangoInit) {
|
||||
// Parse the JSON response - expect a string
|
||||
if let Ok(settings) = serde_json::from_str::<String>(&json_data) {
|
||||
return Some(settings);
|
||||
}
|
||||
}
|
||||
|
||||
let project_path = project.root(db);
|
||||
|
||||
// Try to detect settings module
|
||||
if project_path.join("manage.py").exists() {
|
||||
// Look for common settings modules
|
||||
for candidate in &["settings", "config.settings", "project.settings"] {
|
||||
let parts: Vec<&str> = candidate.split('.').collect();
|
||||
let mut path = project_path.clone();
|
||||
for part in &parts[..parts.len() - 1] {
|
||||
path = path.join(part);
|
||||
}
|
||||
if let Some(last) = parts.last() {
|
||||
path = path.join(format!("{last}.py"));
|
||||
}
|
||||
|
||||
if path.exists() {
|
||||
return Some((*candidate).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
|
@ -4,7 +4,30 @@ use anyhow::Context;
|
|||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
use crate::db::Db as ProjectDb;
|
||||
use crate::inspector::inspector_run;
|
||||
use crate::inspector::queries::Query;
|
||||
use crate::Project;
|
||||
|
||||
/// Get template tags for the current project by querying the inspector.
|
||||
///
|
||||
/// This tracked function calls the inspector to retrieve Django template tags
|
||||
/// and parses the JSON response into a TemplateTags struct.
|
||||
#[salsa::tracked]
|
||||
pub fn get_templatetags(db: &dyn ProjectDb, _project: Project) -> Option<TemplateTags> {
|
||||
let json_str = inspector_run(db, Query::Templatetags)?;
|
||||
|
||||
// Parse the JSON string into a Value first
|
||||
let json_value: serde_json::Value = match serde_json::from_str(&json_str) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
// Parse the JSON data into TemplateTags
|
||||
TemplateTags::from_json(&json_value).ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct TemplateTags(Vec<TemplateTag>);
|
||||
|
||||
impl Deref for TemplateTags {
|
||||
|
@ -83,3 +106,29 @@ impl TemplateTag {
|
|||
self.doc.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_template_tags_parsing() {
|
||||
// Test that TemplateTags can parse valid JSON
|
||||
let json_data = r#"{
|
||||
"templatetags": [
|
||||
{
|
||||
"name": "test_tag",
|
||||
"module": "test_module",
|
||||
"doc": "Test documentation"
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let value: serde_json::Value = serde_json::from_str(json_data).unwrap();
|
||||
let tags = TemplateTags::from_json(&value).unwrap();
|
||||
assert_eq!(tags.len(), 1);
|
||||
assert_eq!(tags[0].name(), "test_tag");
|
||||
assert_eq!(tags[0].library(), "test_module");
|
||||
assert_eq!(tags[0].doc(), Some(&"Test documentation".to_string()));
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@ pub use queries::Query;
|
|||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::db::Db as ProjectDb;
|
||||
use crate::python::python_environment;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DjlsRequest {
|
||||
#[serde(flatten)]
|
||||
|
@ -19,3 +22,32 @@ pub struct DjlsResponse {
|
|||
pub data: Option<serde_json::Value>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Run an inspector query and return the JSON result as a string.
|
||||
///
|
||||
/// This tracked function executes inspector queries through the shared pool
|
||||
/// and caches the results based on project state and query kind.
|
||||
pub fn inspector_run(db: &dyn ProjectDb, query: Query) -> Option<String> {
|
||||
let project = db.project()?;
|
||||
let python_env = python_environment(db, project)?;
|
||||
let project_path = project.root(db);
|
||||
|
||||
match db
|
||||
.inspector_pool()
|
||||
.query(&python_env, project_path, &DjlsRequest { query })
|
||||
{
|
||||
Ok(response) => {
|
||||
if response.ok {
|
||||
if let Some(data) = response.data {
|
||||
// Convert to JSON string
|
||||
serde_json::to_string(&data).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,12 +11,6 @@ use super::DjlsRequest;
|
|||
use super::DjlsResponse;
|
||||
use crate::python::PythonEnvironment;
|
||||
|
||||
/// Global singleton pool for convenience
|
||||
static GLOBAL_POOL: std::sync::OnceLock<InspectorPool> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn global_pool() -> &'static InspectorPool {
|
||||
GLOBAL_POOL.get_or_init(InspectorPool::new)
|
||||
}
|
||||
const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Manages a pool of inspector processes with automatic cleanup
|
||||
|
|
|
@ -3,16 +3,17 @@ use std::path::PathBuf;
|
|||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, Copy)]
|
||||
#[serde(tag = "query", content = "args")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Query {
|
||||
DjangoInit,
|
||||
PythonEnv,
|
||||
Templatetags,
|
||||
DjangoInit,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct PythonEnvironmentQueryData {
|
||||
pub sys_base_prefix: PathBuf,
|
||||
pub sys_executable: PathBuf,
|
||||
|
@ -42,3 +43,21 @@ pub struct TemplateTag {
|
|||
pub module: String,
|
||||
pub doc: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_query_enum() {
|
||||
// Test that Query variants exist and are copyable
|
||||
let python_env = Query::PythonEnv;
|
||||
let templatetags = Query::Templatetags;
|
||||
let django_init = Query::DjangoInit;
|
||||
|
||||
// Test that they can be copied
|
||||
assert_eq!(python_env, Query::PythonEnv);
|
||||
assert_eq!(templatetags, Query::Templatetags);
|
||||
assert_eq!(django_init, Query::DjangoInit);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,149 +1,15 @@
|
|||
mod db;
|
||||
pub mod inspector;
|
||||
mod meta;
|
||||
pub mod python;
|
||||
mod django;
|
||||
mod inspector;
|
||||
mod project;
|
||||
mod python;
|
||||
mod system;
|
||||
mod templatetags;
|
||||
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
pub use db::find_python_environment;
|
||||
pub use db::Db;
|
||||
use inspector::pool::InspectorPool;
|
||||
use inspector::DjlsRequest;
|
||||
use inspector::Query;
|
||||
pub use meta::ProjectMetadata;
|
||||
pub use python::PythonEnvironment;
|
||||
pub use templatetags::TemplateTags;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DjangoProject {
|
||||
path: PathBuf,
|
||||
env: Option<PythonEnvironment>,
|
||||
template_tags: Option<TemplateTags>,
|
||||
inspector: Arc<InspectorPool>,
|
||||
}
|
||||
|
||||
impl DjangoProject {
|
||||
#[must_use]
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
path,
|
||||
env: None,
|
||||
template_tags: None,
|
||||
inspector: Arc::new(InspectorPool::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self, db: &dyn Db) -> Result<()> {
|
||||
// Use the database to find the Python environment
|
||||
self.env = find_python_environment(db);
|
||||
let env = self
|
||||
.env
|
||||
.as_ref()
|
||||
.context("Could not find Python environment")?;
|
||||
|
||||
// Initialize Django
|
||||
let request = DjlsRequest {
|
||||
query: Query::DjangoInit,
|
||||
};
|
||||
let response = self.inspector.query(env, &self.path, &request)?;
|
||||
|
||||
if !response.ok {
|
||||
anyhow::bail!("Failed to initialize Django: {:?}", response.error);
|
||||
}
|
||||
|
||||
// Get template tags
|
||||
let request = DjlsRequest {
|
||||
query: Query::Templatetags,
|
||||
};
|
||||
let response = self.inspector.query(env, &self.path, &request)?;
|
||||
|
||||
if let Some(data) = response.data {
|
||||
self.template_tags = Some(TemplateTags::from_json(&data)?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn template_tags(&self) -> Option<&TemplateTags> {
|
||||
self.template_tags.as_ref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DjangoProject {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "Project path: {}", self.path.display())?;
|
||||
if let Some(py_env) = &self.env {
|
||||
write!(f, "{py_env}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn create_mock_django_project(dir: &Path) -> PathBuf {
|
||||
let project_path = dir.to_path_buf();
|
||||
fs::create_dir_all(&project_path).unwrap();
|
||||
|
||||
// Create a mock Django project structure
|
||||
fs::create_dir_all(project_path.join("myapp")).unwrap();
|
||||
fs::create_dir_all(project_path.join("myapp/templates")).unwrap();
|
||||
fs::write(project_path.join("manage.py"), "#!/usr/bin/env python").unwrap();
|
||||
|
||||
project_path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_django_project_initialization() {
|
||||
// This test needs to be run in an environment with Django installed
|
||||
// For this test to pass, you would need a real Python environment with Django
|
||||
// Here we're just testing the creation of the DjangoProject object
|
||||
let project_dir = tempdir().unwrap();
|
||||
let project_path = create_mock_django_project(project_dir.path());
|
||||
|
||||
let project = DjangoProject::new(project_path);
|
||||
|
||||
assert!(project.env.is_none()); // Environment not initialized yet
|
||||
assert!(project.template_tags.is_none()); // Template tags not loaded yet
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_django_project_path() {
|
||||
let project_dir = tempdir().unwrap();
|
||||
let project_path = create_mock_django_project(project_dir.path());
|
||||
|
||||
let project = DjangoProject::new(project_path.clone());
|
||||
|
||||
assert_eq!(project.path(), project_path.as_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_django_project_display() {
|
||||
let project_dir = tempdir().unwrap();
|
||||
let project_path = create_mock_django_project(project_dir.path());
|
||||
|
||||
let project = DjangoProject::new(project_path.clone());
|
||||
|
||||
let display_str = format!("{project}");
|
||||
assert!(display_str.contains(&format!("Project path: {}", project_path.display())));
|
||||
}
|
||||
}
|
||||
pub use django::django_available;
|
||||
pub use django::django_settings_module;
|
||||
pub use django::get_templatetags;
|
||||
pub use django::TemplateTags;
|
||||
pub use inspector::pool::InspectorPool;
|
||||
pub use project::Project;
|
||||
pub use python::Interpreter;
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ProjectMetadata {
|
||||
root: PathBuf,
|
||||
venv: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ProjectMetadata {
|
||||
#[must_use]
|
||||
pub fn new(root: PathBuf, venv: Option<PathBuf>) -> Self {
|
||||
ProjectMetadata { root, venv }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn root(&self) -> &PathBuf {
|
||||
&self.root
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn venv(&self) -> Option<&PathBuf> {
|
||||
self.venv.as_ref()
|
||||
}
|
||||
}
|
34
crates/djls-project/src/project.rs
Normal file
34
crates/djls-project/src/project.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::db::Db as ProjectDb;
|
||||
use crate::django_available;
|
||||
use crate::django_settings_module;
|
||||
use crate::get_templatetags;
|
||||
use crate::python::Interpreter;
|
||||
|
||||
/// Complete project configuration as a Salsa input.
|
||||
///
|
||||
/// Following Ruff's pattern, this contains all external project configuration
|
||||
/// rather than minimal keys that everything derives from. This replaces both
|
||||
/// Project input and ProjectMetadata.
|
||||
// TODO: Add templatetags as a field on this input
|
||||
#[salsa::input]
|
||||
#[derive(Debug)]
|
||||
pub struct Project {
|
||||
/// The project root path
|
||||
#[returns(ref)]
|
||||
pub root: PathBuf,
|
||||
/// Interpreter specification for Python environment discovery
|
||||
pub interpreter: Interpreter,
|
||||
/// Optional Django settings module override from configuration
|
||||
#[returns(ref)]
|
||||
pub settings_module: Option<String>,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn initialize(self, db: &dyn ProjectDb) {
|
||||
let _ = django_available(db, self);
|
||||
let _ = django_settings_module(db, self);
|
||||
let _ = get_templatetags(db, self);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,77 @@
|
|||
use std::fmt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::Db as ProjectDb;
|
||||
use crate::system;
|
||||
use crate::Project;
|
||||
|
||||
/// Interpreter specification for Python environment discovery.
|
||||
///
|
||||
/// This enum represents the different ways to specify which Python interpreter
|
||||
/// to use for a project.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Interpreter {
|
||||
/// Automatically discover interpreter (`VIRTUAL_ENV`, project venv dirs, system)
|
||||
Auto,
|
||||
/// Use specific virtual environment path
|
||||
VenvPath(String),
|
||||
/// Use specific interpreter executable path
|
||||
InterpreterPath(String),
|
||||
}
|
||||
|
||||
/// Resolve the Python interpreter path for the current project.
|
||||
///
|
||||
/// This tracked function determines the interpreter path based on the project's
|
||||
/// interpreter specification.
|
||||
#[salsa::tracked]
|
||||
pub fn resolve_interpreter(db: &dyn ProjectDb, project: Project) -> Option<PathBuf> {
|
||||
match &project.interpreter(db) {
|
||||
Interpreter::InterpreterPath(path) => {
|
||||
let path_buf = PathBuf::from(path.as_str());
|
||||
if path_buf.exists() {
|
||||
Some(path_buf)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Interpreter::VenvPath(venv_path) => {
|
||||
// Derive interpreter path from venv
|
||||
#[cfg(unix)]
|
||||
let interpreter_path = PathBuf::from(venv_path.as_str()).join("bin").join("python");
|
||||
#[cfg(windows)]
|
||||
let interpreter_path = PathBuf::from(venv_path.as_str())
|
||||
.join("Scripts")
|
||||
.join("python.exe");
|
||||
|
||||
if interpreter_path.exists() {
|
||||
Some(interpreter_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Interpreter::Auto => {
|
||||
// Try common venv directories
|
||||
for venv_dir in &[".venv", "venv", "env", ".env"] {
|
||||
let potential_venv = project.root(db).join(venv_dir);
|
||||
if potential_venv.is_dir() {
|
||||
#[cfg(unix)]
|
||||
let interpreter_path = potential_venv.join("bin").join("python");
|
||||
#[cfg(windows)]
|
||||
let interpreter_path = potential_venv.join("Scripts").join("python.exe");
|
||||
|
||||
if interpreter_path.exists() {
|
||||
return Some(interpreter_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to system python
|
||||
system::find_executable("python").ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PythonEnvironment {
|
||||
|
@ -128,6 +197,38 @@ impl fmt::Display for PythonEnvironment {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
///
|
||||
/// Find the Python environment for the current Django project.
|
||||
///
|
||||
/// This Salsa tracked function discovers the Python environment based on:
|
||||
/// 1. Explicit venv path from project config
|
||||
/// 2. VIRTUAL_ENV environment variable
|
||||
/// 3. Common venv directories in project root (.venv, venv, env, .env)
|
||||
/// 4. System Python as fallback
|
||||
#[salsa::tracked]
|
||||
pub fn python_environment(db: &dyn ProjectDb, project: Project) -> Option<Arc<PythonEnvironment>> {
|
||||
let interpreter_path = resolve_interpreter(db, project)?;
|
||||
let project_path = project.root(db);
|
||||
|
||||
// For venv paths, we need to determine the venv root
|
||||
let interpreter_spec = project.interpreter(db);
|
||||
let venv_path = match &interpreter_spec {
|
||||
Interpreter::InterpreterPath(_) => {
|
||||
// Try to determine venv from interpreter path
|
||||
interpreter_path
|
||||
.parent()
|
||||
.and_then(|bin_dir| bin_dir.parent())
|
||||
.and_then(|venv_root| venv_root.to_str())
|
||||
}
|
||||
Interpreter::VenvPath(path) => Some(path.as_str()),
|
||||
Interpreter::Auto => {
|
||||
// For auto-discovery, let PythonEnvironment::new handle it
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
PythonEnvironment::new(project_path, venv_path).map(Arc::new)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
@ -167,13 +268,13 @@ mod tests {
|
|||
}
|
||||
|
||||
mod env_discovery {
|
||||
use system::mock::MockGuard;
|
||||
use system::mock::{
|
||||
self as sys_mock,
|
||||
};
|
||||
use which::Error as WhichError;
|
||||
|
||||
use super::*;
|
||||
use crate::system::mock::MockGuard;
|
||||
use crate::system::mock::{
|
||||
self as sys_mock,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_explicit_venv_path_found() {
|
||||
|
@ -466,32 +567,37 @@ mod tests {
|
|||
|
||||
mod salsa_integration {
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use djls_workspace::FileSystem;
|
||||
use djls_workspace::InMemoryFileSystem;
|
||||
|
||||
use super::*;
|
||||
use crate::db::find_python_environment;
|
||||
use crate::db::Db as ProjectDb;
|
||||
use crate::meta::ProjectMetadata;
|
||||
use crate::inspector::pool::InspectorPool;
|
||||
|
||||
/// Test implementation of ProjectDb for unit tests
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
struct TestDatabase {
|
||||
storage: salsa::Storage<TestDatabase>,
|
||||
metadata: ProjectMetadata,
|
||||
project_root: PathBuf,
|
||||
project: Arc<Mutex<Option<Project>>>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
}
|
||||
|
||||
impl TestDatabase {
|
||||
fn new(metadata: ProjectMetadata) -> Self {
|
||||
fn new(project_root: PathBuf) -> Self {
|
||||
Self {
|
||||
storage: salsa::Storage::new(None),
|
||||
metadata,
|
||||
project_root,
|
||||
project: Arc::new(Mutex::new(None)),
|
||||
fs: Arc::new(InMemoryFileSystem::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_project(&self, project: Project) {
|
||||
*self.project.lock().unwrap() = Some(project);
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
@ -510,28 +616,51 @@ mod tests {
|
|||
|
||||
#[salsa::db]
|
||||
impl ProjectDb for TestDatabase {
|
||||
fn metadata(&self) -> &ProjectMetadata {
|
||||
&self.metadata
|
||||
fn project(&self) -> Option<Project> {
|
||||
// Return existing project or create a new one
|
||||
let mut project_lock = self.project.lock().unwrap();
|
||||
if project_lock.is_none() {
|
||||
let root = &self.project_root;
|
||||
let interpreter_spec = Interpreter::Auto;
|
||||
let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok();
|
||||
|
||||
*project_lock = Some(Project::new(
|
||||
self,
|
||||
root.clone(),
|
||||
interpreter_spec,
|
||||
django_settings,
|
||||
));
|
||||
}
|
||||
*project_lock
|
||||
}
|
||||
|
||||
fn inspector_pool(&self) -> Arc<InspectorPool> {
|
||||
Arc::new(InspectorPool::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_python_environment_with_salsa_db() {
|
||||
fn test_python_environment_with_salsa_db() {
|
||||
let project_dir = tempdir().unwrap();
|
||||
let venv_dir = tempdir().unwrap();
|
||||
|
||||
// Create a mock venv
|
||||
let venv_prefix = create_mock_venv(venv_dir.path(), None);
|
||||
|
||||
// Create a metadata instance with project path and explicit venv path
|
||||
let metadata =
|
||||
ProjectMetadata::new(project_dir.path().to_path_buf(), Some(venv_prefix.clone()));
|
||||
// Create a TestDatabase with the project root
|
||||
let db = TestDatabase::new(project_dir.path().to_path_buf());
|
||||
|
||||
// Create a TestDatabase with the metadata
|
||||
let db = TestDatabase::new(metadata);
|
||||
// Create and configure the project with the venv path
|
||||
let project = Project::new(
|
||||
&db,
|
||||
project_dir.path().to_path_buf(),
|
||||
Interpreter::VenvPath(venv_prefix.to_string_lossy().to_string()),
|
||||
None,
|
||||
);
|
||||
db.set_project(project);
|
||||
|
||||
// Call the tracked function
|
||||
let env = find_python_environment(&db);
|
||||
let env = python_environment(&db, project);
|
||||
|
||||
// Verify we found the environment
|
||||
assert!(env.is_some(), "Should find environment via salsa db");
|
||||
|
@ -553,24 +682,22 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_python_environment_with_project_venv() {
|
||||
fn test_python_environment_with_project_venv() {
|
||||
let project_dir = tempdir().unwrap();
|
||||
|
||||
// Create a .venv in the project directory
|
||||
let venv_prefix = create_mock_venv(&project_dir.path().join(".venv"), None);
|
||||
|
||||
// Create a metadata instance with project path but no explicit venv path
|
||||
let metadata = ProjectMetadata::new(project_dir.path().to_path_buf(), None);
|
||||
|
||||
// Create a TestDatabase with the metadata
|
||||
let db = TestDatabase::new(metadata);
|
||||
// Create a TestDatabase with the project root
|
||||
let db = TestDatabase::new(project_dir.path().to_path_buf());
|
||||
|
||||
// Mock to ensure VIRTUAL_ENV is not set
|
||||
let _guard = system::mock::MockGuard;
|
||||
system::mock::remove_env_var("VIRTUAL_ENV");
|
||||
|
||||
// Call the tracked function
|
||||
let env = find_python_environment(&db);
|
||||
// Call the tracked function (should find .venv)
|
||||
let project = db.project().unwrap();
|
||||
let env = python_environment(&db, project);
|
||||
|
||||
// Verify we found the environment
|
||||
assert!(
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
#[cfg(test)]
|
||||
use std::sync::Mutex;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use djls_project::Db as ProjectDb;
|
||||
use djls_project::ProjectMetadata;
|
||||
use djls_project::InspectorPool;
|
||||
use djls_project::Interpreter;
|
||||
use djls_project::Project;
|
||||
use djls_templates::db::Db as TemplateDb;
|
||||
use djls_templates::templatetags::TagSpecs;
|
||||
use djls_workspace::db::Db as WorkspaceDb;
|
||||
|
@ -36,8 +37,11 @@ pub struct DjangoDatabase {
|
|||
/// Maps paths to [`SourceFile`] entities for O(1) lookup.
|
||||
files: Arc<DashMap<PathBuf, SourceFile>>,
|
||||
|
||||
/// Project metadata containing root path and venv configuration.
|
||||
metadata: ProjectMetadata,
|
||||
/// The single project for this database instance
|
||||
project: Arc<Mutex<Option<Project>>>,
|
||||
|
||||
/// Shared inspector pool for executing Python queries
|
||||
inspector_pool: Arc<InspectorPool>,
|
||||
|
||||
storage: salsa::Storage<Self>,
|
||||
|
||||
|
@ -56,7 +60,8 @@ impl Default for DjangoDatabase {
|
|||
Self {
|
||||
fs: Arc::new(InMemoryFileSystem::new()),
|
||||
files: Arc::new(DashMap::new()),
|
||||
metadata: ProjectMetadata::new(PathBuf::from("/test"), None),
|
||||
project: Arc::new(Mutex::new(None)),
|
||||
inspector_pool: Arc::new(InspectorPool::new()),
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let logs = logs.clone();
|
||||
move |event| {
|
||||
|
@ -76,16 +81,26 @@ impl Default for DjangoDatabase {
|
|||
}
|
||||
|
||||
impl DjangoDatabase {
|
||||
/// Create a new [`DjangoDatabase`] with the given file system, file map, and project metadata.
|
||||
pub fn new(
|
||||
file_system: Arc<dyn FileSystem>,
|
||||
files: Arc<DashMap<PathBuf, SourceFile>>,
|
||||
metadata: ProjectMetadata,
|
||||
) -> Self {
|
||||
/// Set the project for this database instance
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the project mutex is poisoned.
|
||||
pub fn set_project(&self, root: &Path) {
|
||||
let interpreter = Interpreter::Auto;
|
||||
let django_settings = std::env::var("DJANGO_SETTINGS_MODULE").ok();
|
||||
|
||||
let project = Project::new(self, root.to_path_buf(), interpreter, django_settings);
|
||||
|
||||
*self.project.lock().unwrap() = Some(project);
|
||||
}
|
||||
/// Create a new [`DjangoDatabase`] with the given file system and file map.
|
||||
pub fn new(file_system: Arc<dyn FileSystem>, files: Arc<DashMap<PathBuf, SourceFile>>) -> Self {
|
||||
Self {
|
||||
fs: file_system,
|
||||
files,
|
||||
metadata,
|
||||
project: Arc::new(Mutex::new(None)),
|
||||
inspector_pool: Arc::new(InspectorPool::new()),
|
||||
storage: salsa::Storage::new(None),
|
||||
#[cfg(test)]
|
||||
logs: Arc::new(Mutex::new(None)),
|
||||
|
@ -163,7 +178,9 @@ impl WorkspaceDb for DjangoDatabase {
|
|||
#[salsa::db]
|
||||
impl TemplateDb for DjangoDatabase {
|
||||
fn tag_specs(&self) -> Arc<TagSpecs> {
|
||||
let project_root = self.metadata.root();
|
||||
let project_root_buf =
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
let project_root = project_root_buf.as_path();
|
||||
|
||||
if let Ok(user_specs) = TagSpecs::load_user_specs(project_root) {
|
||||
// If user specs exist and aren't empty, merge with built-in specs
|
||||
|
@ -182,7 +199,11 @@ impl TemplateDb for DjangoDatabase {
|
|||
|
||||
#[salsa::db]
|
||||
impl ProjectDb for DjangoDatabase {
|
||||
fn metadata(&self) -> &ProjectMetadata {
|
||||
&self.metadata
|
||||
fn project(&self) -> Option<Project> {
|
||||
*self.project.lock().unwrap()
|
||||
}
|
||||
|
||||
fn inspector_pool(&self) -> Arc<InspectorPool> {
|
||||
self.inspector_pool.clone()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use djls_project::Db as ProjectDb;
|
||||
use djls_templates::analyze_template;
|
||||
use djls_templates::TemplateDiagnostic;
|
||||
use djls_workspace::paths;
|
||||
|
@ -181,58 +182,31 @@ impl LanguageServer for DjangoLanguageServer {
|
|||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn initialized(&self, _params: lsp_types::InitializedParams) {
|
||||
tracing::info!("Server received initialized notification.");
|
||||
|
||||
self.with_session_task(move |session_arc| async move {
|
||||
let project_path_and_venv = {
|
||||
let session_lock = session_arc.lock().await;
|
||||
session_lock.project().map(|p| {
|
||||
(
|
||||
p.path().display().to_string(),
|
||||
session_lock
|
||||
.settings()
|
||||
.venv_path()
|
||||
.map(std::string::ToString::to_string),
|
||||
)
|
||||
})
|
||||
};
|
||||
let session_lock = session_arc.lock().await;
|
||||
|
||||
if let Some((path_display, venv_path)) = project_path_and_venv {
|
||||
let project_path = session_lock
|
||||
.project()
|
||||
.map(|p| p.root(session_lock.database()).clone());
|
||||
|
||||
if let Some(path) = project_path {
|
||||
tracing::info!(
|
||||
"Task: Starting initialization for project at: {}",
|
||||
path_display
|
||||
path.display()
|
||||
);
|
||||
|
||||
if let Some(ref path) = venv_path {
|
||||
tracing::info!("Using virtual environment from config: {}", path);
|
||||
if let Some(project) = session_lock.project() {
|
||||
project.initialize(session_lock.database());
|
||||
}
|
||||
|
||||
let init_result = {
|
||||
let mut session_lock = session_arc.lock().await;
|
||||
session_lock.initialize_project()
|
||||
};
|
||||
|
||||
match init_result {
|
||||
Ok(()) => {
|
||||
tracing::info!("Task: Successfully initialized project: {}", path_display);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Task: Failed to initialize Django project at {}: {}",
|
||||
path_display,
|
||||
e
|
||||
);
|
||||
|
||||
// Clear project on error
|
||||
let mut session_lock = session_arc.lock().await;
|
||||
*session_lock.project_mut() = None;
|
||||
}
|
||||
}
|
||||
tracing::info!("Task: Successfully initialized project: {}", path.display());
|
||||
} else {
|
||||
tracing::info!("Task: No project instance found to initialize.");
|
||||
tracing::info!("Task: No project configured, skipping initialization.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
@ -369,7 +343,13 @@ impl LanguageServer for DjangoLanguageServer {
|
|||
let position = params.text_document_position.position;
|
||||
let encoding = session.position_encoding();
|
||||
let file_kind = FileKind::from_path(&path);
|
||||
let template_tags = session.project().and_then(|p| p.template_tags());
|
||||
let template_tags = session.with_db(|db| {
|
||||
if let Some(project) = db.project() {
|
||||
djls_project::get_templatetags(db, project)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let tag_specs = session.with_db(djls_templates::Db::tag_specs);
|
||||
let supports_snippets = session.supports_snippets();
|
||||
|
||||
|
@ -378,7 +358,7 @@ impl LanguageServer for DjangoLanguageServer {
|
|||
position,
|
||||
encoding,
|
||||
file_kind,
|
||||
template_tags,
|
||||
template_tags.as_ref(),
|
||||
Some(&tag_specs),
|
||||
supports_snippets,
|
||||
);
|
||||
|
@ -474,20 +454,20 @@ impl LanguageServer for DjangoLanguageServer {
|
|||
async fn did_change_configuration(&self, _params: lsp_types::DidChangeConfigurationParams) {
|
||||
tracing::info!("Configuration change detected. Reloading settings...");
|
||||
|
||||
let project_path = self
|
||||
.with_session(|session| session.project().map(|p| p.path().to_path_buf()))
|
||||
.await;
|
||||
self.with_session_mut(|session| {
|
||||
if let Some(project) = session.project() {
|
||||
let project_root = project.root(session.database());
|
||||
|
||||
if let Some(path) = project_path {
|
||||
self.with_session_mut(|session| match djls_conf::Settings::new(path.as_path()) {
|
||||
Ok(new_settings) => {
|
||||
session.set_settings(new_settings);
|
||||
match djls_conf::Settings::new(project_root.as_path()) {
|
||||
Ok(new_settings) => {
|
||||
session.set_settings(new_settings);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error loading settings: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error loading settings: {}", e);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,16 +6,16 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use djls_conf::Settings;
|
||||
use djls_project::DjangoProject;
|
||||
use djls_project::ProjectMetadata;
|
||||
use djls_project::Db as ProjectDb;
|
||||
use djls_project::Interpreter;
|
||||
use djls_workspace::db::SourceFile;
|
||||
use djls_workspace::paths;
|
||||
use djls_workspace::PositionEncoding;
|
||||
use djls_workspace::TextDocument;
|
||||
use djls_workspace::Workspace;
|
||||
use salsa::Setter;
|
||||
use tower_lsp_server::lsp_types;
|
||||
use url::Url;
|
||||
|
||||
|
@ -28,14 +28,13 @@ use crate::db::DjangoDatabase;
|
|||
/// - Project configuration and settings
|
||||
/// - Client capabilities and position encoding
|
||||
/// - Workspace operations (buffers and file system)
|
||||
/// - All Salsa inputs (`SessionState`, Project)
|
||||
///
|
||||
/// Following Ruff's architecture, the concrete database lives at this level
|
||||
/// and is passed down to operations that need it.
|
||||
pub struct Session {
|
||||
/// The Django project configuration
|
||||
project: Option<DjangoProject>,
|
||||
|
||||
/// LSP server settings
|
||||
// TODO: this should really be in the database
|
||||
settings: Settings,
|
||||
|
||||
/// Workspace for buffer and file system management
|
||||
|
@ -44,12 +43,12 @@ pub struct Session {
|
|||
/// but not the database (which is owned directly by Session).
|
||||
workspace: Workspace,
|
||||
|
||||
#[allow(dead_code)]
|
||||
client_capabilities: lsp_types::ClientCapabilities,
|
||||
|
||||
/// Position encoding negotiated with client
|
||||
position_encoding: PositionEncoding,
|
||||
|
||||
/// The Salsa database for incremental computation
|
||||
db: DjangoDatabase,
|
||||
}
|
||||
|
||||
|
@ -65,47 +64,51 @@ impl Session {
|
|||
std::env::current_dir().ok()
|
||||
});
|
||||
|
||||
let (project, settings, metadata) = if let Some(path) = &project_path {
|
||||
let settings =
|
||||
djls_conf::Settings::new(path).unwrap_or_else(|_| djls_conf::Settings::default());
|
||||
|
||||
let project = Some(djls_project::DjangoProject::new(path.clone()));
|
||||
|
||||
// Create metadata for the project with venv path from settings
|
||||
let venv_path = settings.venv_path().map(PathBuf::from);
|
||||
let metadata = ProjectMetadata::new(path.clone(), venv_path);
|
||||
|
||||
(project, settings, metadata)
|
||||
let settings = if let Some(path) = &project_path {
|
||||
djls_conf::Settings::new(path).unwrap_or_else(|_| djls_conf::Settings::default())
|
||||
} else {
|
||||
// Default metadata for when there's no project path
|
||||
let metadata = ProjectMetadata::new(PathBuf::from("."), None);
|
||||
(None, Settings::default(), metadata)
|
||||
Settings::default()
|
||||
};
|
||||
|
||||
// Create workspace for buffer management
|
||||
let workspace = Workspace::new();
|
||||
|
||||
// Create the concrete database with the workspace's file system and metadata
|
||||
let files = Arc::new(DashMap::new());
|
||||
let db = DjangoDatabase::new(workspace.file_system(), files, metadata);
|
||||
let mut db = DjangoDatabase::new(workspace.file_system(), files);
|
||||
|
||||
if let Some(root_path) = &project_path {
|
||||
db.set_project(root_path);
|
||||
|
||||
if let Some(project) = db.project() {
|
||||
// TODO: should this logic live in the project?
|
||||
if let Some(venv_path) = settings.venv_path() {
|
||||
let interpreter = Interpreter::VenvPath(venv_path.to_string());
|
||||
project.set_interpreter(&mut db).to(interpreter);
|
||||
} else if let Ok(virtual_env) = std::env::var("VIRTUAL_ENV") {
|
||||
let interpreter = Interpreter::VenvPath(virtual_env);
|
||||
project.set_interpreter(&mut db).to(interpreter);
|
||||
}
|
||||
|
||||
// TODO: allow for configuring via settings
|
||||
if let Ok(settings_module) = std::env::var("DJANGO_SETTINGS_MODULE") {
|
||||
project
|
||||
.set_settings_module(&mut db)
|
||||
.to(Some(settings_module));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
db,
|
||||
project,
|
||||
settings,
|
||||
workspace,
|
||||
client_capabilities: params.capabilities.clone(),
|
||||
position_encoding: PositionEncoding::negotiate(params),
|
||||
db,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn project(&self) -> Option<&DjangoProject> {
|
||||
self.project.as_ref()
|
||||
}
|
||||
|
||||
pub fn project_mut(&mut self) -> &mut Option<DjangoProject> {
|
||||
&mut self.project
|
||||
pub fn db(&self) -> &DjangoDatabase {
|
||||
&self.db
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
@ -122,18 +125,6 @@ impl Session {
|
|||
self.position_encoding
|
||||
}
|
||||
|
||||
/// Check if the client supports snippet completions
|
||||
#[must_use]
|
||||
pub fn supports_snippets(&self) -> bool {
|
||||
self.client_capabilities
|
||||
.text_document
|
||||
.as_ref()
|
||||
.and_then(|td| td.completion.as_ref())
|
||||
.and_then(|c| c.completion_item.as_ref())
|
||||
.and_then(|ci| ci.snippet_support)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Execute a read-only operation with access to the database.
|
||||
pub fn with_db<F, R>(&self, f: F) -> R
|
||||
where
|
||||
|
@ -155,13 +146,9 @@ impl Session {
|
|||
&self.db
|
||||
}
|
||||
|
||||
/// Initialize the project with the database.
|
||||
pub fn initialize_project(&mut self) -> Result<()> {
|
||||
if let Some(project) = self.project.as_mut() {
|
||||
project.initialize(&self.db)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
/// Get the current project for this session
|
||||
pub fn project(&self) -> Option<djls_project::Project> {
|
||||
self.db.project()
|
||||
}
|
||||
|
||||
/// Open a document in the session.
|
||||
|
@ -256,6 +243,18 @@ impl Session {
|
|||
.and_then(|td| td.diagnostic.as_ref())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// Check if the client supports snippet completions
|
||||
#[must_use]
|
||||
pub fn supports_snippets(&self) -> bool {
|
||||
self.client_capabilities
|
||||
.text_document
|
||||
.as_ref()
|
||||
.and_then(|td| td.completion.as_ref())
|
||||
.and_then(|c| c.completion_item.as_ref())
|
||||
.and_then(|ci| ci.snippet_support)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
|
|
|
@ -48,6 +48,7 @@ pub trait Db: salsa::Database {
|
|||
pub struct SourceFile {
|
||||
/// The file's classification for analysis routing
|
||||
pub kind: FileKind,
|
||||
// TODO: Change from Arc<str> to PathBuf for consistency with Project.root
|
||||
/// The file path
|
||||
#[returns(ref)]
|
||||
pub path: Arc<str>,
|
||||
|
@ -78,6 +79,7 @@ pub fn source_text(db: &dyn Db, file: SourceFile) -> Arc<str> {
|
|||
/// on files identified by path rather than by SourceFile input.
|
||||
#[salsa::input]
|
||||
pub struct FilePath {
|
||||
// TODO: Change from Arc<str> to PathBuf for consistency with Project.root
|
||||
/// The file path as a string
|
||||
#[returns(ref)]
|
||||
pub path: Arc<str>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue