mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-07 10:50:40 +00:00
updates
This commit is contained in:
parent
b04a589ae1
commit
bf39fd633a
3 changed files with 203 additions and 93 deletions
|
@ -12,3 +12,4 @@ toml = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = { version = "1.41", features = ["yaml"] }
|
insta = { version = "1.41", features = ["yaml"] }
|
||||||
|
tempfile = "3.8"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::ast::{Ast, AstError, Block, DjangoFilter, LineOffsets, Node, Span, Tag};
|
use crate::ast::{Ast, AstError, Block, DjangoFilter, LineOffsets, Node, Span, Tag};
|
||||||
use crate::tagspecs::{TagSpec, TagType};
|
use crate::tagspecs::{TagType, TagSpecs};
|
||||||
use crate::tokens::{Token, TokenStream, TokenType};
|
use crate::tokens::{Token, TokenStream, TokenType};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ impl Parser {
|
||||||
assignment: None,
|
assignment: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let specs = TagSpec::load_builtin_specs()?;
|
let specs = TagSpecs::load_builtin_specs()?;
|
||||||
let spec = match specs.get(&tag_name) {
|
let spec = match specs.get(&tag_name) {
|
||||||
Some(spec) => spec,
|
Some(spec) => spec,
|
||||||
None => return Ok(Node::Block(Block::Tag { tag })),
|
None => return Ok(Node::Block(Block::Tag { tag })),
|
||||||
|
|
|
@ -1,80 +1,112 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::ops::{Deref, Index};
|
use std::ops::{Deref, Index};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use thiserror::Error;
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Error)]
|
||||||
|
pub enum TagSpecError {
|
||||||
|
#[error("Failed to read file: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("Failed to parse TOML: {0}")]
|
||||||
|
Toml(#[from] toml::de::Error),
|
||||||
|
#[error("Failed to extract specs: {0}")]
|
||||||
|
Extract(String),
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct TagSpecs(HashMap<String, TagSpec>);
|
pub struct TagSpecs(HashMap<String, TagSpec>);
|
||||||
|
|
||||||
impl TagSpecs {
|
impl TagSpecs {
|
||||||
pub fn get(&self, key: &str) -> Option<&TagSpec> {
|
pub fn get(&self, key: &str) -> Option<&TagSpec> {
|
||||||
self.0.get(key)
|
self.0.get(key)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Path> for TagSpecs {
|
/// Load specs from a TOML file, looking under the specified table path
|
||||||
fn from(specs_dir: &Path) -> Self {
|
fn load_from_toml(path: &Path, table_path: &[&str]) -> Result<Self, anyhow::Error> {
|
||||||
|
let content = fs::read_to_string(path)?;
|
||||||
|
let value: Value = toml::from_str(&content)?;
|
||||||
|
|
||||||
|
// Navigate to the specified table
|
||||||
|
let table = table_path
|
||||||
|
.iter()
|
||||||
|
.try_fold(&value, |current, &key| {
|
||||||
|
current
|
||||||
|
.get(key)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing table: {}", key))
|
||||||
|
})
|
||||||
|
.unwrap_or(&value);
|
||||||
|
|
||||||
|
let mut specs = HashMap::new();
|
||||||
|
TagSpec::extract_specs(table, None, &mut specs)
|
||||||
|
.map_err(|e| TagSpecError::Extract(e.to_string()))?;
|
||||||
|
Ok(TagSpecs(specs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load specs from a user's project directory
|
||||||
|
pub fn load_user_specs(project_root: &Path) -> Result<Self, anyhow::Error> {
|
||||||
|
// List of config files to try, in priority order
|
||||||
|
let config_files = ["djls.toml", ".djls.toml", "pyproject.toml"];
|
||||||
|
|
||||||
|
for &file in &config_files {
|
||||||
|
let path = project_root.join(file);
|
||||||
|
if path.exists() {
|
||||||
|
return match file {
|
||||||
|
"pyproject.toml" => {
|
||||||
|
Self::load_from_toml(&path, &["tool", "djls", "template", "tags"])
|
||||||
|
}
|
||||||
|
_ => Self::load_from_toml(&path, &[]), // Root level for other files
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load builtin specs from the crate's tagspecs directory
|
||||||
|
pub fn load_builtin_specs() -> Result<Self, anyhow::Error> {
|
||||||
|
let specs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tagspecs");
|
||||||
let mut specs = HashMap::new();
|
let mut specs = HashMap::new();
|
||||||
|
|
||||||
for entry in fs::read_dir(specs_dir).expect("Failed to read specs directory") {
|
for entry in fs::read_dir(&specs_dir)? {
|
||||||
let entry = entry.expect("Failed to read directory entry");
|
let entry = entry?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
|
if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
|
||||||
let content = fs::read_to_string(&path).expect("Failed to read spec file");
|
let file_specs = Self::load_from_toml(&path, &[])?;
|
||||||
|
specs.extend(file_specs.0);
|
||||||
let value: Value = toml::from_str(&content).expect("Failed to parse TOML");
|
|
||||||
|
|
||||||
TagSpec::extract_specs(&value, None, &mut specs).expect("Failed to extract specs");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TagSpecs(specs)
|
Ok(TagSpecs(specs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge another TagSpecs into this one, with the other taking precedence
|
||||||
|
pub fn merge(&mut self, other: TagSpecs) -> &mut Self {
|
||||||
|
self.0.extend(other.0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load both builtin and user specs, with user specs taking precedence
|
||||||
|
pub fn load_all(project_root: &Path) -> Result<Self, anyhow::Error> {
|
||||||
|
let mut specs = Self::load_builtin_specs()?;
|
||||||
|
let user_specs = Self::load_user_specs(project_root)?;
|
||||||
|
Ok(specs.merge(user_specs).clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for TagSpecs {
|
impl TryFrom<&Path> for TagSpecs {
|
||||||
type Target = HashMap<String, TagSpec>;
|
type Error = TagSpecError;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn try_from(path: &Path) -> Result<Self, Self::Error> {
|
||||||
&self.0
|
Self::load_from_toml(path, &[]).map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoIterator for TagSpecs {
|
|
||||||
type Item = (String, TagSpec);
|
|
||||||
type IntoIter = std::collections::hash_map::IntoIter<String, TagSpec>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.0.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoIterator for &'a TagSpecs {
|
|
||||||
type Item = (&'a String, &'a TagSpec);
|
|
||||||
type IntoIter = std::collections::hash_map::Iter<'a, String, TagSpec>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.0.iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Index<&str> for TagSpecs {
|
|
||||||
type Output = TagSpec;
|
|
||||||
|
|
||||||
fn index(&self, index: &str) -> &Self::Output {
|
|
||||||
&self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<HashMap<String, TagSpec>> for TagSpecs {
|
|
||||||
fn as_ref(&self) -> &HashMap<String, TagSpec> {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct TagSpec {
|
pub struct TagSpec {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
@ -86,16 +118,11 @@ pub struct TagSpec {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TagSpec {
|
impl TagSpec {
|
||||||
pub fn load_builtin_specs() -> Result<TagSpecs> {
|
|
||||||
let specs_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tagspecs");
|
|
||||||
Ok(TagSpecs::from(specs_dir.as_path()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_specs(
|
fn extract_specs(
|
||||||
value: &Value,
|
value: &Value,
|
||||||
prefix: Option<&str>,
|
prefix: Option<&str>,
|
||||||
specs: &mut HashMap<String, TagSpec>,
|
specs: &mut HashMap<String, TagSpec>,
|
||||||
) -> Result<()> {
|
) -> Result<(), String> {
|
||||||
// Try to deserialize as a tag spec first
|
// Try to deserialize as a tag spec first
|
||||||
match TagSpec::deserialize(value.clone()) {
|
match TagSpec::deserialize(value.clone()) {
|
||||||
Ok(tag_spec) => {
|
Ok(tag_spec) => {
|
||||||
|
@ -159,63 +186,145 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specs_are_valid() -> Result<()> {
|
fn test_specs_are_valid() -> Result<(), anyhow::Error> {
|
||||||
let specs = TagSpec::load_builtin_specs()?;
|
let specs = TagSpecs::load_builtin_specs()?;
|
||||||
|
|
||||||
assert!(!specs.0.is_empty(), "Should have loaded at least one spec");
|
assert!(!specs.0.is_empty(), "Should have loaded at least one spec");
|
||||||
|
|
||||||
println!("Loaded {} tag specs:", specs.0.len());
|
|
||||||
for (name, spec) in &specs.0 {
|
for (name, spec) in &specs.0 {
|
||||||
println!(" {} ({:?})", name, spec.tag_type);
|
assert!(!name.is_empty(), "Tag name should not be empty");
|
||||||
|
assert!(
|
||||||
|
spec.tag_type == TagType::Block || spec.tag_type == TagType::Variable,
|
||||||
|
"Tag type should be block or variable"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_builtin_django_tags() -> Result<(), anyhow::Error> {
|
||||||
|
let specs = TagSpecs::load_builtin_specs()?;
|
||||||
|
|
||||||
|
// Test using get method
|
||||||
|
let if_tag = specs.get("if").expect("if tag should be present");
|
||||||
|
assert_eq!(if_tag.tag_type, TagType::Block);
|
||||||
|
assert_eq!(if_tag.closing.as_deref(), Some("endif"));
|
||||||
|
assert_eq!(if_tag.branches.as_ref().map(|b| b.len()), Some(2));
|
||||||
|
assert!(if_tag
|
||||||
|
.branches
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.contains(&"elif".to_string()));
|
||||||
|
assert!(if_tag
|
||||||
|
.branches
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.contains(&"else".to_string()));
|
||||||
|
|
||||||
|
let for_tag = specs.get("for").expect("for tag should be present");
|
||||||
|
assert_eq!(for_tag.tag_type, TagType::Block);
|
||||||
|
assert_eq!(for_tag.closing.as_deref(), Some("endfor"));
|
||||||
|
assert_eq!(for_tag.branches.as_ref().map(|b| b.len()), Some(1));
|
||||||
|
assert!(for_tag
|
||||||
|
.branches
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.contains(&"empty".to_string()));
|
||||||
|
|
||||||
|
let block_tag = specs.get("block").expect("block tag should be present");
|
||||||
|
assert_eq!(block_tag.tag_type, TagType::Block);
|
||||||
|
assert_eq!(block_tag.closing.as_deref(), Some("endblock"));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_builtin_django_tags() -> Result<()> {
|
fn test_user_defined_tags() -> Result<(), anyhow::Error> {
|
||||||
let specs = TagSpec::load_builtin_specs()?;
|
// Create a temporary directory for our test project
|
||||||
|
let dir = tempfile::tempdir()?;
|
||||||
|
let root = dir.path();
|
||||||
|
|
||||||
// Test using Index trait
|
// Create a pyproject.toml with custom tags
|
||||||
let if_tag = &specs["if"];
|
let pyproject_content = r#"
|
||||||
|
[tool.djls.template.tags.mytag]
|
||||||
|
type = "block"
|
||||||
|
closing = "endmytag"
|
||||||
|
branches = ["mybranch"]
|
||||||
|
args = [{ name = "myarg", required = true }]
|
||||||
|
"#;
|
||||||
|
fs::write(root.join("pyproject.toml"), pyproject_content)?;
|
||||||
|
|
||||||
|
// Load both builtin and user specs
|
||||||
|
let specs = TagSpecs::load_all(root)?;
|
||||||
|
|
||||||
|
// Check that builtin tags are still there
|
||||||
|
let if_tag = specs.get("if").expect("if tag should be present");
|
||||||
assert_eq!(if_tag.tag_type, TagType::Block);
|
assert_eq!(if_tag.tag_type, TagType::Block);
|
||||||
assert_eq!(if_tag.closing, Some("endif".to_string()));
|
|
||||||
|
|
||||||
let if_branches = if_tag
|
// Check our custom tag
|
||||||
|
let my_tag = specs.get("mytag").expect("mytag should be present");
|
||||||
|
assert_eq!(my_tag.tag_type, TagType::Block);
|
||||||
|
assert_eq!(my_tag.closing, Some("endmytag".to_string()));
|
||||||
|
|
||||||
|
let branches = my_tag
|
||||||
.branches
|
.branches
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("if tag should have branches");
|
.expect("mytag should have branches");
|
||||||
assert!(if_branches.iter().any(|b| b == "elif"));
|
assert!(branches.iter().any(|b| b == "mybranch"));
|
||||||
assert!(if_branches.iter().any(|b| b == "else"));
|
|
||||||
|
|
||||||
// Test using get method
|
let args = my_tag.args.as_ref().expect("mytag should have args");
|
||||||
let for_tag = specs.get("for").expect("for tag should be present");
|
let arg = &args[0];
|
||||||
assert_eq!(for_tag.tag_type, TagType::Block);
|
assert_eq!(arg.name, "myarg");
|
||||||
assert_eq!(for_tag.closing, Some("endfor".to_string()));
|
assert!(arg.required);
|
||||||
|
|
||||||
let for_branches = for_tag
|
// Clean up temp dir
|
||||||
.branches
|
dir.close()?;
|
||||||
.as_ref()
|
Ok(())
|
||||||
.expect("for tag should have branches");
|
|
||||||
assert!(for_branches.iter().any(|b| b == "empty"));
|
|
||||||
|
|
||||||
// Test using HashMap method directly via Deref
|
|
||||||
let block_tag = specs.get("block").expect("block tag should be present");
|
|
||||||
assert_eq!(block_tag.tag_type, TagType::Block);
|
|
||||||
assert_eq!(block_tag.closing, Some("endblock".to_string()));
|
|
||||||
|
|
||||||
// Test iteration
|
|
||||||
let mut count = 0;
|
|
||||||
for (name, spec) in &specs {
|
|
||||||
println!("Found tag: {} ({:?})", name, spec.tag_type);
|
|
||||||
count += 1;
|
|
||||||
}
|
}
|
||||||
assert!(count > 0, "Should have found some tags");
|
|
||||||
|
|
||||||
// Test as_ref
|
#[test]
|
||||||
let map_ref: &HashMap<_, _> = specs.as_ref();
|
fn test_config_file_priority() -> Result<(), anyhow::Error> {
|
||||||
assert_eq!(map_ref.len(), count);
|
// Create a temporary directory for our test project
|
||||||
|
let dir = tempfile::tempdir()?;
|
||||||
|
let root = dir.path();
|
||||||
|
|
||||||
|
// Create all config files with different tags
|
||||||
|
let djls_content = r#"
|
||||||
|
[mytag1]
|
||||||
|
type = "block"
|
||||||
|
closing = "endmytag1"
|
||||||
|
"#;
|
||||||
|
fs::write(root.join("djls.toml"), djls_content)?;
|
||||||
|
|
||||||
|
let pyproject_content = r#"
|
||||||
|
[tool.djls.template.tags]
|
||||||
|
mytag2.type = "block"
|
||||||
|
mytag2.closing = "endmytag2"
|
||||||
|
"#;
|
||||||
|
fs::write(root.join("pyproject.toml"), pyproject_content)?;
|
||||||
|
|
||||||
|
// Load user specs
|
||||||
|
let specs = TagSpecs::load_user_specs(root)?;
|
||||||
|
|
||||||
|
// Should only have mytag1 since djls.toml has highest priority
|
||||||
|
assert!(specs.get("mytag1").is_some(), "mytag1 should be present");
|
||||||
|
assert!(
|
||||||
|
specs.get("mytag2").is_none(),
|
||||||
|
"mytag2 should not be present"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove djls.toml and try again
|
||||||
|
fs::remove_file(root.join("djls.toml"))?;
|
||||||
|
let specs = TagSpecs::load_user_specs(root)?;
|
||||||
|
|
||||||
|
// Should now have mytag2 since pyproject.toml has second priority
|
||||||
|
assert!(
|
||||||
|
specs.get("mytag1").is_none(),
|
||||||
|
"mytag1 should not be present"
|
||||||
|
);
|
||||||
|
assert!(specs.get("mytag2").is_some(), "mytag2 should be present");
|
||||||
|
|
||||||
|
dir.close()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue