diff --git a/Cargo.lock b/Cargo.lock index f50bd9b..088d3e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,13 +136,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "example" -version = "0.1.0" -dependencies = [ - "minijinja", -] - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -286,7 +279,7 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jinja-lsp" -version = "0.1.71" +version = "0.1.80" dependencies = [ "anyhow", "env_logger", @@ -304,12 +297,13 @@ dependencies = [ [[package]] name = "jinja-lsp-queries" -version = "0.1.71" +version = "0.1.80" dependencies = [ "ropey", "tower-lsp", "tree-sitter", "tree-sitter-jinja2", + "tree-sitter-python", "tree-sitter-rust", ] @@ -354,15 +348,6 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" -[[package]] -name = "minijinja" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb3bf58a1ec4f3f228bec851a2066c7717ad308817cd8a08f67c10660c6ff7b" -dependencies = [ - "serde", -] - [[package]] name = "miniz_oxide" version = "0.7.2" @@ -834,6 +819,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-python" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c93b1b1fbd0d399db3445f51fd3058e43d0b4dcff62ddbdb46e66550978aa5" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-rust" version = "0.20.4" diff --git a/Cargo.toml b/Cargo.toml index 0bf687a..642ba5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ resolver = "2" members = [ "jinja-lsp-queries", "jinja-lsp", - "example" ] [profile.dev] diff --git a/README.md b/README.md index 0cc8d3d..11b0489 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ - +banner jinja-lsp enhances minijinja development experience by providing Helix/Nvim users with advanced features such as autocomplete, syntax highlighting, hover, goto definition, code actions and linting. +
+ crates.io + visualstudio.com +
+ ## Installation ```sh @@ -80,4 +85,4 @@ name = "jinja" language-servers = ["jinja-lsp"] ``` -[VSCode support](https://marketplace.visualstudio.com/search?term=jinja-lsp&target=VSCode&category=Other&sortBy=Relevance) +Supported languages: Python, Rust diff --git a/editors/code/client/package.json b/editors/code/client/package.json index 3640e0a..58411d6 100644 --- a/editors/code/client/package.json +++ b/editors/code/client/package.json @@ -3,7 +3,7 @@ "description": "VSCode part of a language server", "author": "uros-5", "license": "MIT", - "version": "0.1.73", + "version": "0.1.80", "publisher": "uros", "repository": { "type": "git", diff --git a/editors/code/client/src/extension.ts b/editors/code/client/src/extension.ts index c4ed615..1b2b6b6 100644 --- a/editors/code/client/src/extension.ts +++ b/editors/code/client/src/extension.ts @@ -20,7 +20,7 @@ export function activate(context: ExtensionContext) { // The server is implemented in node const serverModule = getServer(); if (!serverModule.valid) { - throw new Error(serverModule.name); + throw new Error(serverModule.name); } let config: Record = JSON.parse( @@ -30,7 +30,7 @@ export function activate(context: ExtensionContext) { // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used const serverOptions: ServerOptions = { - run: { command: serverModule.name}, + run: { command: serverModule.name }, debug: { command: serverModule.name, args: [], @@ -40,11 +40,11 @@ export function activate(context: ExtensionContext) { // Options to control the language client const clientOptions: LanguageClientOptions = { // Register the server for plain text documents - documentSelector: [{ scheme: 'file', language: 'jinja-html' }, { scheme: 'file', language: 'rust' }], + documentSelector: [{ scheme: 'file', language: 'jinja-html' }, { scheme: 'file', language: 'rust' }, { scheme: 'file', language: 'python' }], initializationOptions: config, synchronize: { // Notify the server about file changes to '.clientrc files contained in the workspace - fileEvents: workspace.createFileSystemWatcher('**/.{jinja, rs}') + fileEvents: workspace.createFileSystemWatcher('**/.{jinja, rs, python}') } }; diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 02ee973..769d299 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -1,12 +1,12 @@ { "name": "jinja-lsp", - "version": "0.1.72", + "version": "0.1.80", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jinja-lsp", - "version": "0.1.72", + "version": "0.1.8", "license": "MIT", "devDependencies": { "@types/mocha": "^10.0.6", diff --git a/editors/code/package.json b/editors/code/package.json index 9a63a12..da25a4a 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -2,7 +2,7 @@ "name": "jinja-lsp", "description": "jinja-lsp", "license": "MIT", - "version": "0.1.73", + "version": "0.1.80", "repository": { "url": "https://github.com/uros-5/jinja-lsp" }, @@ -17,14 +17,17 @@ "multi-root ready", "jinja", "minijinja", - "rust" + "rust", + "jinja-lsp", + "python" ], "engines": { "vscode": "^1.75.0" }, "activationEvents": [ "onLanguage:jinja-html", - "onLanguage:rust" + "onLanguage:rust", + "onLanguage:python" ], "main": "./client/out/extension", "contributes": { @@ -47,7 +50,8 @@ "jinja-lsp.lang": { "type": "string", "enum": [ - "rust" + "rust", + "python" ], "default": "rust", "description": "Language that is used on backend" diff --git a/jinja-lsp-queries/Cargo.toml b/jinja-lsp-queries/Cargo.toml index 4b66a1d..88022c0 100644 --- a/jinja-lsp-queries/Cargo.toml +++ b/jinja-lsp-queries/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jinja-lsp-queries" -version = "0.1.71" +version = "0.1.80" edition = "2021" description = "TreeSitter queries for jinja-lsp" license = "MIT" @@ -11,4 +11,5 @@ tree-sitter-jinja2 = "0.0.6" tree-sitter-rust = "0.20.4" tower-lsp = { version = "0.20.0", features = ["proposed"] } ropey = "1.5.0" +tree-sitter-python = "=0.20.4" diff --git a/jinja-lsp-queries/src/lsp_helper.rs b/jinja-lsp-queries/src/lsp_helper.rs index 0d54f7a..75a1887 100644 --- a/jinja-lsp-queries/src/lsp_helper.rs +++ b/jinja-lsp-queries/src/lsp_helper.rs @@ -19,7 +19,7 @@ pub fn search_errors( file_name: &String, templates: &String, lang_type: LangType, -) -> Option> { +) -> Option> { let mut diagnostics = vec![]; match lang_type { LangType::Template => { @@ -70,11 +70,7 @@ pub fn search_errors( } } if to_warn { - let diagnostic = create_diagnostic( - &Identifier::from(&object), - err_type.severity(), - err_type.to_string(), - ); + let diagnostic = (err_type, Identifier::from(&object)); diagnostics.push(diagnostic); } } @@ -90,15 +86,13 @@ pub fn search_errors( for i in id_templates { let err_type = JinjaDiagnostic::TemplateNotFound; if i.name.is_empty() { - let diagnostic = - create_diagnostic(i, err_type.severity(), err_type.to_string()); + let diagnostic = (err_type, i.to_owned()); diagnostics.push(diagnostic); } else { let path = format!("{templates}/{}", i.name); if let Err(err) = std::fs::canonicalize(path) { if err.kind() == ErrorKind::NotFound { - let diagnostic = - create_diagnostic(i, err_type.severity(), err_type.to_string()); + let diagnostic = (err_type, i.to_owned()); diagnostics.push(diagnostic); } } @@ -115,11 +109,7 @@ pub fn search_errors( let path = format!("{templates}/{}", template.name); if let Err(err) = std::fs::canonicalize(path) { if err.kind() == ErrorKind::NotFound { - let diagnostic = create_diagnostic( - template, - DiagnosticSeverity::WARNING, - "Template not found".to_string(), - ); + let diagnostic = (JinjaDiagnostic::TemplateNotFound, template.to_owned()); diagnostics.push(diagnostic); } } diff --git a/jinja-lsp-queries/src/parsers.rs b/jinja-lsp-queries/src/parsers.rs index 9748a4c..82c12c5 100644 --- a/jinja-lsp-queries/src/parsers.rs +++ b/jinja-lsp-queries/src/parsers.rs @@ -19,6 +19,13 @@ impl Parsers { LangType::Backend => self.backend.parse(text, old_tree), } } + + pub fn update_backend(&mut self, lang: &str) { + if lang == "python" { + self.backend = Parser::new(); + let _ = self.backend.set_language(tree_sitter_python::language()); + } + } } impl Default for Parsers { diff --git a/jinja-lsp-queries/src/search/definition2.rs b/jinja-lsp-queries/src/search/definition2.rs deleted file mode 100644 index 8b13789..0000000 --- a/jinja-lsp-queries/src/search/definition2.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/jinja-lsp-queries/src/search/jinja_state.rs b/jinja-lsp-queries/src/search/jinja_state.rs deleted file mode 100644 index 6f98966..0000000 --- a/jinja-lsp-queries/src/search/jinja_state.rs +++ /dev/null @@ -1,39 +0,0 @@ -use tree_sitter::{Point, Query, Tree}; - -use super::{ - definition::{definition_query, JinjaDefinitions}, - objects::{objects_query, JinjaObjects}, - templates::{templates_query, JinjaImports}, -}; - -#[derive(Default)] -pub struct JinjaState { - jinja_definitions: JinjaDefinitions, - jinja_objects: JinjaObjects, - jinja_imports: JinjaImports, -} - -impl JinjaState { - pub fn init( - trigger_point: Point, - query: (&Query, &Query, &Query), - tree: &Tree, - source: &str, - all: bool, - ) -> Self { - let definitions = definition_query(query.0, tree, trigger_point, source, all); - let objects = objects_query(query.1, tree, trigger_point, source, all); - let imports = templates_query(query.2, tree, trigger_point, source, all); - Self { - jinja_definitions: definitions, - jinja_objects: objects, - jinja_imports: imports, - } - } - - pub fn reset(&mut self) { - self.jinja_definitions = Default::default(); - self.jinja_objects = Default::default(); - self.jinja_imports = Default::default(); - } -} diff --git a/jinja-lsp-queries/src/search/mod.rs b/jinja-lsp-queries/src/search/mod.rs index 5c05a27..47c2893 100644 --- a/jinja-lsp-queries/src/search/mod.rs +++ b/jinja-lsp-queries/src/search/mod.rs @@ -4,11 +4,9 @@ use tree_sitter::Point; use self::objects::JinjaObject; pub mod definition; -pub mod jinja_state; pub mod objects; pub mod queries; pub mod rust_identifiers; -pub mod rust_state; pub mod rust_template_completion; pub mod snippets_completion; pub mod templates; diff --git a/jinja-lsp-queries/src/search/objects.rs b/jinja-lsp-queries/src/search/objects.rs index bd479d2..be59311 100644 --- a/jinja-lsp-queries/src/search/objects.rs +++ b/jinja-lsp-queries/src/search/objects.rs @@ -178,4 +178,6 @@ pub enum CompletionType { Snippets { range: Range }, } -static VALID_IDENTIFIERS: [&str; 6] = ["loop", "true", "false", "not", "as", "module"]; +static VALID_IDENTIFIERS: [&str; 8] = [ + "loop", "true", "false", "not", "as", "module", "super", "url_for", +]; diff --git a/jinja-lsp-queries/src/search/queries.rs b/jinja-lsp-queries/src/search/queries.rs index 509700d..4127389 100644 --- a/jinja-lsp-queries/src/search/queries.rs +++ b/jinja-lsp-queries/src/search/queries.rs @@ -5,8 +5,8 @@ pub struct Queries { pub jinja_definitions: Query, pub jinja_objects: Query, pub jinja_imports: Query, - pub rust_definitions: Query, - pub rust_templates: Query, + pub backend_definitions: Query, + pub backend_templates: Query, pub jinja_snippets: Query, } @@ -21,14 +21,26 @@ impl Default for Queries { Self { jinja_definitions: Query::new(tree_sitter_jinja2::language(), DEFINITIONS).unwrap(), jinja_objects: Query::new(tree_sitter_jinja2::language(), OBJECTS).unwrap(), - rust_definitions: Query::new(tree_sitter_rust::language(), RUST_DEFINITIONS).unwrap(), + backend_definitions: Query::new(tree_sitter_rust::language(), RUST_DEFINITIONS) + .unwrap(), jinja_imports: Query::new(tree_sitter_jinja2::language(), JINJA_IMPORTS).unwrap(), - rust_templates: Query::new(tree_sitter_rust::language(), RUST_TEMPLATES).unwrap(), + backend_templates: Query::new(tree_sitter_rust::language(), RUST_TEMPLATES).unwrap(), jinja_snippets: Query::new(tree_sitter_jinja2::language(), JINJA_SNIPPETS).unwrap(), } } } +impl Queries { + pub fn update_backend(&mut self, lang: &str) { + if lang == "python" { + self.backend_templates = + Query::new(tree_sitter_python::language(), PYTHON_TEMPLATES).unwrap(); + self.backend_definitions = + Query::new(tree_sitter_python::language(), PYTHON_DEFINITIONS).unwrap(); + } + } +} + const OBJECTS: &str = r#" ( [ @@ -79,6 +91,8 @@ pub static RUST_DEFINITIONS: &str = r#" (#match? @method "(add_global|add_filter|add_function)") ) @function + + (ERROR) @error ]) "#; @@ -197,3 +211,64 @@ const DEFINITIONS: &str = r#" (ERROR) @error "#; + +const PYTHON_TEMPLATES: &str = r#" +(call + [ + (attribute + (identifier) @method_name + ) + (identifier) @method_name + (#any-of? @method_name "render_jinja" "get_template") + ] + (argument_list + (string)+ @template_name + ) +) +"#; + +pub static PYTHON_DEFINITIONS: &str = r#" + +( + (subscript + (attribute + object: (identifier)* @object + attribute: (identifier) @field + (#match? @field "^(globals|filters)$") + (#eq? @object "jinja_env") + ) + (string + (string_content) @key_id + ) + ) +) + +( + [ + (call + function: (identifier) @method + arguments: (argument_list + (keyword_argument + name: (identifier) @key_id + ) + ) + ) + + (call + function: (attribute + object: (identifier) + attribute: (identifier) @method + ) + arguments: (argument_list + (keyword_argument + name: (identifier) @key_id + ) + ) + ) + ] + (#match? @method "^(render_template|render)$") + ) + + (ERROR) @error + +"#; diff --git a/jinja-lsp-queries/src/search/rust_identifiers.rs b/jinja-lsp-queries/src/search/rust_identifiers.rs index a68d825..10454b6 100644 --- a/jinja-lsp-queries/src/search/rust_identifiers.rs +++ b/jinja-lsp-queries/src/search/rust_identifiers.rs @@ -10,35 +10,24 @@ pub enum Current { } #[derive(Default, Debug, Clone)] -pub struct RustIdentifiers { +pub struct BackendIdentifiers { variables: Vec, - current: Current, } -impl RustIdentifiers { +impl BackendIdentifiers { pub fn show(self) -> Vec { self.variables } pub fn check(&mut self, name: &str, capture: &QueryCapture<'_>, text: &str) -> Option<()> { match name { - "macro" => { - let end = capture.node.end_position(); - self.current = Current::InMacro(end); - } "key_id" => { - if let Current::InMacro(end_macro) = self.current { - let start = capture.node.start_position(); - let end = capture.node.end_position(); - if start > end_macro { - self.current = Current::Free; - return None; - } - let name = capture.node.utf8_text(text.as_bytes()).ok()?; - let mut identifier = Identifier::new(name, start, end); - identifier.identifier_type = IdentifierType::BackendVariable; - self.variables.push(identifier); - } + let start = capture.node.start_position(); + let end = capture.node.end_position(); + let name = capture.node.utf8_text(text.as_bytes()).ok()?; + let mut identifier = Identifier::new(name, start, end); + identifier.identifier_type = IdentifierType::BackendVariable; + self.variables.push(identifier); } "name" => { let start = capture.node.start_position(); @@ -49,22 +38,25 @@ impl RustIdentifiers { identifier.identifier_type = IdentifierType::BackendVariable; self.variables.push(identifier); } + "error" => { + return None; + } _ => (), } - None + Some(()) } } -pub fn rust_definition_query( +pub fn backend_definition_query( query: &Query, tree: &Tree, trigger_point: Point, text: &str, all: bool, -) -> RustIdentifiers { +) -> BackendIdentifiers { let closest_node = tree.root_node(); let mut cursor_qry = QueryCursor::new(); - let mut rust = RustIdentifiers::default(); + let mut rust = BackendIdentifiers::default(); let capture_names = query.capture_names(); let matches = cursor_qry.matches(query, closest_node, text.as_bytes()); let captures = matches.into_iter().flat_map(|m| { @@ -74,7 +66,9 @@ pub fn rust_definition_query( }); for capture in captures { let name = &capture_names[capture.index as usize]; - rust.check(name, capture, text); + if rust.check(name, capture, text).is_none() { + break; + } } rust } diff --git a/jinja-lsp-queries/src/search/rust_state.rs b/jinja-lsp-queries/src/search/rust_state.rs deleted file mode 100644 index 7232dc0..0000000 --- a/jinja-lsp-queries/src/search/rust_state.rs +++ /dev/null @@ -1,70 +0,0 @@ -use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range, Url}; -use tree_sitter::{Point, Query, Tree}; - -use crate::search::{ - rust_identifiers::rust_definition_query, rust_template_completion::rust_templates_query, -}; - -use super::{rust_identifiers::RustIdentifiers, rust_template_completion::RustTemplates}; - -#[derive(Default)] -pub struct RustState { - rust_identifiers: RustIdentifiers, - rust_templates: RustTemplates, -} - -impl RustState { - pub fn init( - trigger_point: Point, - query: (&Query, &Query), - tree: &Tree, - source: &str, - all: bool, - ) -> Self { - let ids = rust_definition_query(query.0, tree, trigger_point, source, all); - let templates = rust_templates_query(query.1, tree, trigger_point, source, all); - RustState { - rust_identifiers: ids, - rust_templates: templates, - } - } - - pub fn reset(&mut self) { - self.rust_identifiers = RustIdentifiers::default(); - self.rust_templates = RustTemplates::default(); - } - - pub fn template_errors(&self, root: &str) -> Option> { - let mut diagnostics = vec![]; - for id in &self.rust_templates.templates { - let name = &id.name; - let template = format!("{}/{}", root, name); - let template = std::fs::canonicalize(template); - let mut is_error = false; - if template.is_err() { - is_error = true; - } else { - let buffer = template.ok()?; - let url = format!("file://{}", buffer.to_str()?); - let url = Url::parse(&url).ok(); - if url.is_none() { - is_error = true; - } - } - if is_error { - let diagnostic = Diagnostic { - range: Range::new( - Position::new(id.start.row as u32, id.start.column as u32), - Position::new(id.end.row as u32, id.end.column as u32), - ), - severity: Some(DiagnosticSeverity::WARNING), - message: "Template not found".to_owned(), - source: Some(String::from("jinja-lsp")), - ..Default::default() - }; - diagnostics.push(diagnostic); - } - } - Some(diagnostics) - } -} diff --git a/jinja-lsp-queries/src/search/rust_template_completion.rs b/jinja-lsp-queries/src/search/rust_template_completion.rs index 41e4d78..ca42323 100644 --- a/jinja-lsp-queries/src/search/rust_template_completion.rs +++ b/jinja-lsp-queries/src/search/rust_template_completion.rs @@ -3,17 +3,12 @@ use tree_sitter::{Point, Query, QueryCapture, QueryCursor, Tree}; use super::{Identifier, IdentifierType}; #[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RustTemplateCompletion { - pub template_name: Identifier, -} - -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct RustTemplates { +pub struct BackendTemplates { pub templates: Vec, in_method: bool, } -impl RustTemplates { +impl BackendTemplates { pub fn in_template(&self, trigger_point: Point) -> Option<&Identifier> { let last = self.templates.last()?; if trigger_point >= last.start && trigger_point <= last.end { @@ -48,14 +43,14 @@ impl RustTemplates { } } -pub fn rust_templates_query( +pub fn backend_templates_query( query: &Query, tree: &Tree, trigger_point: Point, text: &str, all: bool, -) -> RustTemplates { - let mut templates = RustTemplates::default(); +) -> BackendTemplates { + let mut templates = BackendTemplates::default(); let closest_node = tree.root_node(); let mut cursor_qry = QueryCursor::new(); let capture_names = query.capture_names(); diff --git a/jinja-lsp-queries/src/search/snippets_completion.rs b/jinja-lsp-queries/src/search/snippets_completion.rs index 0824e6e..207f682 100644 --- a/jinja-lsp-queries/src/search/snippets_completion.rs +++ b/jinja-lsp-queries/src/search/snippets_completion.rs @@ -25,29 +25,8 @@ impl Snippets { } "error_block" => { self.is_error = true; - let count = capture.node.child_count(); - if self.start == Point::default() { - self.start = capture.node.start_position(); - } - if count == 0 { - self.start = capture.node.start_position(); - self.end = capture.node.end_position(); - self.end.column += 1; - return None; - } else { - let end = capture.node.start_position(); - let first = capture.node.child(0).unwrap(); - if self.start.row != end.row { - self.start = capture.node.start_position(); - self.end = first.end_position(); - self.end.column += 1; - } else { - let first = capture.node.child(0).unwrap(); - self.end = first.start_position(); - self.end.column += 1; - } - return None; - } + self.end = capture.node.end_position(); + return None; } "keyword" => { self.keyword = capture.node.start_position(); @@ -58,6 +37,9 @@ impl Snippets { } pub fn to_complete(&self, trigger_point: Point) -> Option { + if self.is_error && trigger_point <= self.end { + return Some(Range::default()); + } if self.is_error && trigger_point >= self.start && trigger_point <= self.end { if self.keyword >= self.start && self.keyword <= self.end { return None; diff --git a/jinja-lsp-queries/src/search/templates.rs b/jinja-lsp-queries/src/search/templates.rs index 73b6084..b66fe43 100644 --- a/jinja-lsp-queries/src/search/templates.rs +++ b/jinja-lsp-queries/src/search/templates.rs @@ -67,7 +67,7 @@ impl Import { } } -#[derive(Default)] +#[derive(Default, Debug)] pub enum Current { Id(usize), #[default] diff --git a/jinja-lsp-queries/src/search/test_queries.rs b/jinja-lsp-queries/src/search/test_queries.rs index 41cd8b5..e0d8178 100644 --- a/jinja-lsp-queries/src/search/test_queries.rs +++ b/jinja-lsp-queries/src/search/test_queries.rs @@ -7,8 +7,8 @@ mod query_tests { search::objects::CompletionType, search::{ completion_start, definition::definition_query, queries::Queries, - rust_identifiers::rust_definition_query, - rust_template_completion::rust_templates_query, templates::templates_query, + rust_identifiers::backend_definition_query, + rust_template_completion::backend_templates_query, templates::templates_query, }, }; @@ -34,6 +34,17 @@ mod query_tests { parser.parse(text, None).expect("not to fail") } + fn prepare_python_tree(text: &str) -> tree_sitter::Tree { + let language = tree_sitter_python::language(); + let mut parser = Parser::new(); + + parser + .set_language(language) + .expect("could not load rust grammar"); + + parser.parse(text, None).expect("not to fail") + } + #[test] fn jinja_definitions() { let cases = [ @@ -136,7 +147,7 @@ mod query_tests { } #[test] - fn rust_macros() { + fn rust_definition() { let case = r#" let a = context!(name => 11 + abc, abc => "username"); let b = context!{name, username => "username" } @@ -149,11 +160,29 @@ mod query_tests { let tree = prepare_rust_tree(case); let trigger_point = Point::new(0, 0); let query = Queries::default(); - let query = &query.rust_definitions; - let rust = rust_definition_query(query, &tree, trigger_point, case, true); + let query = &query.backend_definitions; + let rust = backend_definition_query(query, &tree, trigger_point, case, true); assert_eq!(rust.show().len(), 8); } + #[test] + fn python_definition() { + let case = r#" + jinja_env.globals['a'] = 1 + render_template(data=123) + some_obj.render_template(first_name = "John", last_name = "Doe") + render(a=11) + "#; + + let tree = prepare_python_tree(case); + let trigger_point = Point::new(0, 0); + let mut query = Queries::default(); + query.update_backend("python"); + let query = &query.backend_definitions; + let rust = backend_definition_query(query, &tree, trigger_point, case, true); + assert_eq!(rust.show().len(), 5); + } + #[test] fn find_jinja_completion() { let source = r#" @@ -240,8 +269,8 @@ mod query_tests { let tree = prepare_rust_tree(source); let trigger_point = Point::default(); let query = Queries::default(); - let query = &query.rust_templates; - let templates = rust_templates_query(query, &tree, trigger_point, source, true); + let query = &query.backend_templates; + let templates = backend_templates_query(query, &tree, trigger_point, source, true); assert_eq!(templates.templates.len(), 3); } @@ -256,8 +285,29 @@ mod query_tests { let tree = prepare_rust_tree(source); let trigger_point = Point::new(3, 47); let query = Queries::default(); - let query = &query.rust_templates; - let templates = rust_templates_query(query, &tree, trigger_point, source, false); + let query = &query.backend_templates; + let templates = backend_templates_query(query, &tree, trigger_point, source, false); + if let Some(template) = templates.in_template(trigger_point) { + if let Some(completion) = completion_start(trigger_point, template) { + assert_eq!(completion, "accou"); + } + } + } + + #[test] + fn template_completion_in_python() { + let source = r#" + tmp2 = jinja.get_template("account3"); + tmp2 = jinja.get_template("account2"); + tmp = jinja.get_template("account"); + tmp = jinja.anything("account"); + "#; + let tree = prepare_python_tree(source); + let trigger_point = Point::new(3, 47); + let mut query = Queries::default(); + query.update_backend("python"); + let query = &query.backend_templates; + let templates = backend_templates_query(query, &tree, trigger_point, source, false); if let Some(template) = templates.in_template(trigger_point) { if let Some(completion) = completion_start(trigger_point, template) { assert_eq!(completion, "accou"); diff --git a/jinja-lsp-queries/src/tree_builder.rs b/jinja-lsp-queries/src/tree_builder.rs index 896c830..572128e 100644 --- a/jinja-lsp-queries/src/tree_builder.rs +++ b/jinja-lsp-queries/src/tree_builder.rs @@ -6,6 +6,7 @@ pub enum LangType { Backend, } +#[derive(PartialEq, Eq, Debug)] pub enum JinjaDiagnostic { DefinedSomewhere, Undefined, diff --git a/jinja-lsp/Cargo.toml b/jinja-lsp/Cargo.toml index a175ab9..f92ae91 100644 --- a/jinja-lsp/Cargo.toml +++ b/jinja-lsp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jinja-lsp" -version = "0.1.71" +version = "0.1.80" edition = "2021" license = "MIT" authors = ["uros-5"] @@ -30,4 +30,4 @@ walkdir = "2.4.0" anyhow = "1.0.75" tree-sitter-jinja2 = "0.0.6" tree-sitter-rust = "^0.20.4" -jinja-lsp-queries = { path = "../jinja-lsp-queries", version = "0.1.70"} +jinja-lsp-queries = { path = "../jinja-lsp-queries", version = "0.1.80"} diff --git a/jinja-lsp/src/backend.rs b/jinja-lsp/src/backend.rs index d95e1f4..f8fedf1 100644 --- a/jinja-lsp/src/backend.rs +++ b/jinja-lsp/src/backend.rs @@ -6,9 +6,10 @@ use tokio::sync::{ use tower_lsp::{ jsonrpc::Result, lsp_types::{ - CompletionParams, CompletionResponse, DidChangeConfigurationParams, - DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentSymbolParams, - DocumentSymbolResponse, InitializeParams, InitializeResult, + CompletionParams, CompletionResponse, CreateFile, CreateFileOptions, + DidChangeConfigurationParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, + DocumentChangeOperation, DocumentChanges, DocumentSymbolParams, DocumentSymbolResponse, + InitializeParams, InitializeResult, ResourceOp, Url, WorkspaceEdit, }, Client, LanguageServer, }; @@ -153,22 +154,49 @@ impl LanguageServer for Backend { } } -pub fn code_actions() -> Vec { +pub fn code_actions(template: Option<(String, String)>) -> Vec { let mut commands = vec![]; - for command in [ - ("Reset variables", "reset_variables"), - ("Warn about unused", "warn"), - ] { - commands.push(CodeActionOrCommand::CodeAction(CodeAction { - title: command.0.to_string(), - kind: Some(CodeActionKind::EMPTY), - command: Some(Command::new( - command.1.to_string(), - command.1.to_string(), - None, - )), - ..Default::default() - })); + if let Some((templates, template)) = template { + if let Ok(path) = std::fs::canonicalize(templates) { + let name = format!("file://{}/{template}", path.to_str().unwrap()); + let cf = CreateFile { + uri: Url::parse(&name).unwrap(), + options: Some(CreateFileOptions { + overwrite: Some(false), + ignore_if_exists: Some(true), + }), + annotation_id: None, + }; + + commands.push(CodeActionOrCommand::CodeAction(CodeAction { + title: "Generate new template".to_string(), + kind: Some(CodeActionKind::QUICKFIX), + edit: Some(WorkspaceEdit { + changes: None, + document_changes: Some(DocumentChanges::Operations(vec![ + DocumentChangeOperation::Op(ResourceOp::Create(cf)), + ])), + change_annotations: None, + }), + ..Default::default() + })); + } + } else { + for command in [ + ("Reset variables", "reset_variables"), + ("Warn about unused", "warn"), + ] { + commands.push(CodeActionOrCommand::CodeAction(CodeAction { + title: command.0.to_string(), + kind: Some(CodeActionKind::EMPTY), + command: Some(Command::new( + command.1.to_string(), + command.1.to_string(), + None, + )), + ..Default::default() + })); + } } commands } @@ -182,7 +210,7 @@ impl Backend { lsp_sender.clone(), lsp_recv, ); - diagnostics_task(client.clone(), diagnostic_recv); + diagnostics_task(client.clone(), diagnostic_recv, lsp_sender.clone()); Self { main_channel: lsp_sender, } diff --git a/jinja-lsp/src/channels/diagnostics.rs b/jinja-lsp/src/channels/diagnostics.rs index a9558d8..9b5dc06 100644 --- a/jinja-lsp/src/channels/diagnostics.rs +++ b/jinja-lsp/src/channels/diagnostics.rs @@ -1,28 +1,59 @@ use std::collections::HashMap; -use tokio::sync::mpsc::Receiver; +use jinja_lsp_queries::{ + lsp_helper::create_diagnostic, search::Identifier, tree_builder::JinjaDiagnostic, +}; +use tokio::sync::mpsc::{Receiver, Sender}; use tower_lsp::{ - lsp_types::{Diagnostic, MessageType, Url}, + lsp_types::{MessageType, Url}, Client, }; -pub fn diagnostics_task(client: Client, mut receiver: Receiver) { +use super::lsp::LspMessage; + +pub fn diagnostics_task( + client: Client, + mut receiver: Receiver, + lsp_channel: Sender, +) { tokio::spawn(async move { while let Some(msg) = receiver.recv().await { match msg { DiagnosticMessage::Str(msg) => client.log_message(MessageType::INFO, msg).await, DiagnosticMessage::Errors(all_errors) => { + let mut code_actions = HashMap::new(); for (uri, errors) in all_errors.into_iter() { + let template_errors = errors + .iter() + .filter(|err| err.0 == JinjaDiagnostic::TemplateNotFound); + let mut v = vec![]; + for err in template_errors { + v.push(err.1.to_owned()); + } + code_actions.insert(uri.to_owned(), v); + let mut v = vec![]; + for error in errors { + let diagnostic = create_diagnostic( + &error.1, + error.0.severity(), + error.0.to_string(), + ); + v.push(diagnostic); + } let uri = Url::parse(&uri).unwrap(); - client.publish_diagnostics(uri, errors, None).await; + client.publish_diagnostics(uri, v, None).await; } + let _ = lsp_channel + .send(LspMessage::CodeActions(code_actions)) + .await; } } } }); } +#[derive(Debug)] pub enum DiagnosticMessage { - Errors(HashMap>), + Errors(HashMap>), Str(String), } diff --git a/jinja-lsp/src/channels/lsp.rs b/jinja-lsp/src/channels/lsp.rs index 4d06e7f..443e42c 100644 --- a/jinja-lsp/src/channels/lsp.rs +++ b/jinja-lsp/src/channels/lsp.rs @@ -1,18 +1,24 @@ -use jinja_lsp_queries::search::{objects::CompletionType, snippets_completion::snippets}; +use jinja_lsp_queries::search::{ + objects::CompletionType, snippets_completion::snippets, Identifier, +}; use serde_json::Value; -use tokio::sync::{mpsc, oneshot}; +use std::{collections::HashMap, time::Duration}; +use tokio::{ + sync::{mpsc, oneshot}, + time::sleep, +}; use tower_lsp::{ lsp_types::{ - CodeActionParams, CodeActionProviderCapability, CodeActionResponse, CompletionItem, - CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, - CompletionTextEdit, DidChangeConfigurationParams, DidChangeTextDocumentParams, - DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbolParams, - DocumentSymbolResponse, Documentation, ExecuteCommandOptions, ExecuteCommandParams, - GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, - HoverProviderCapability, InitializeParams, InitializeResult, InsertReplaceEdit, - MarkupContent, MarkupKind, MessageType, OneOf, ServerCapabilities, ServerInfo, - TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, - TextDocumentSyncSaveOptions, TextEdit, + CodeActionKind, CodeActionOptions, CodeActionParams, CodeActionProviderCapability, + CodeActionResponse, CompletionItem, CompletionItemKind, CompletionOptions, + CompletionParams, CompletionResponse, CompletionTextEdit, DidChangeConfigurationParams, + DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, + DocumentSymbolParams, DocumentSymbolResponse, Documentation, ExecuteCommandOptions, + ExecuteCommandParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, + HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, + InsertReplaceEdit, MarkupContent, MarkupKind, MessageType, OneOf, ServerCapabilities, + ServerInfo, TextDocumentIdentifier, TextDocumentSyncCapability, TextDocumentSyncKind, + TextDocumentSyncOptions, TextDocumentSyncSaveOptions, TextEdit, WorkDoneProgressOptions, }, Client, }; @@ -21,7 +27,7 @@ use crate::{ backend::code_actions, config::{walkdir, JinjaConfig}, filter::init_filter_completions, - lsp_files2::LspFiles, + lsp_files2::{JinjaCodeAction, LspFiles}, }; use super::diagnostics::DiagnosticMessage; @@ -97,7 +103,7 @@ pub fn lsp_task( }, server_info: Some(ServerInfo { name: String::from("jinja-lsp"), - version: Some(String::from("0.1.62")), + version: Some(String::from("0.1.80")), }), offset_encoding: None, }; @@ -115,7 +121,7 @@ pub fn lsp_task( .log_message(MessageType::WARNING, "Template directory not found") .await; } - if config.lang != "rust" { + if !["rust", "python"].contains(&config.lang.as_str()) { client .log_message(MessageType::WARNING, "Backend language not supported") .await; @@ -262,9 +268,27 @@ pub fn lsp_task( } } LspMessage::CodeAction(params, sender) => { - if let Some(is_code_action) = lsp_data.code_action(params) { - if is_code_action { - let _ = sender.send(Some(code_actions())); + let param = DidSaveTextDocumentParams { + text_document: TextDocumentIdentifier::new( + params.text_document.uri.to_owned(), + ), + text: None, + }; + if let Some(code_action) = lsp_data.code_action2(params) { + match code_action { + JinjaCodeAction::Reset => { + let _ = sender.send(Some(code_actions(None))); + } + JinjaCodeAction::CreateTemplate(template) => { + let templates = lsp_data.config.templates.to_owned(); + let _ = + sender.send(Some(code_actions(Some((templates, template))))); + let lsp_channel = lsp_channel.clone(); + tokio::spawn(async move { + sleep(Duration::from_millis(1400)).await; + let _ = lsp_channel.send(LspMessage::DidSave(param)).await; + }); + } } } } @@ -298,6 +322,9 @@ pub fn lsp_task( } let _ = lsp_channel.send(LspMessage::Initialized(sender)).await; } + LspMessage::CodeActions(code_actions) => { + lsp_data.add_code_actions(code_actions); + } } } }); @@ -328,4 +355,5 @@ pub enum LspMessage { oneshot::Sender>, ), DidChangeConfiguration(DidChangeConfigurationParams), + CodeActions(HashMap>), } diff --git a/jinja-lsp/src/config.rs b/jinja-lsp/src/config.rs index 6939250..53977c7 100644 --- a/jinja-lsp/src/config.rs +++ b/jinja-lsp/src/config.rs @@ -1,8 +1,10 @@ use std::{collections::HashMap, path::Path}; -use jinja_lsp_queries::tree_builder::LangType; +use jinja_lsp_queries::{ + search::Identifier, + tree_builder::{JinjaDiagnostic, LangType}, +}; use serde::{Deserialize, Serialize}; -use tower_lsp::lsp_types::Diagnostic; use walkdir::WalkDir; use crate::lsp_files2::LspFiles; @@ -23,7 +25,7 @@ impl JinjaConfig { match path.extension()?.to_str() { Some(e) => match e { "html" | "jinja" | "j2" => Some(LangType::Template), - "rs" => Some(LangType::Backend), + "rs" | "py" => Some(LangType::Backend), _ => None, }, None => None, @@ -36,13 +38,21 @@ impl JinjaConfig { } } -pub type InitLsp = (HashMap>, LspFiles); +pub type InitLsp = ( + HashMap>, + LspFiles, +); pub fn walkdir(config: &JinjaConfig) -> anyhow::Result { let mut all = vec![config.templates.clone()]; let mut backend = config.backend.clone(); all.append(&mut backend); let mut lsp_files = LspFiles::default(); + lsp_files.config = config.clone(); + if config.lang == "python" { + lsp_files.queries.update_backend(&config.lang); + lsp_files.parsers.update_backend(&config.lang); + } let mut diags = HashMap::new(); for dir in all { let walk = WalkDir::new(dir); @@ -59,7 +69,6 @@ pub fn walkdir(config: &JinjaConfig) -> anyhow::Result { } } - lsp_files.config = config.clone(); lsp_files.read_trees(&mut diags); Ok((diags, lsp_files)) } diff --git a/jinja-lsp/src/lsp_files2.rs b/jinja-lsp/src/lsp_files2.rs index 094c0cf..2290ee3 100644 --- a/jinja-lsp/src/lsp_files2.rs +++ b/jinja-lsp/src/lsp_files2.rs @@ -2,11 +2,11 @@ use jinja_lsp_queries::{ lsp_helper::search_errors, search::{ completion_start, definition::definition_query, objects::objects_query, queries::Queries, - rust_identifiers::rust_definition_query, rust_template_completion::rust_templates_query, - snippets_completion::snippets_query, templates::templates_query, to_range, Identifier, - IdentifierType, + rust_identifiers::backend_definition_query, + rust_template_completion::backend_templates_query, snippets_completion::snippets_query, + templates::templates_query, to_range, Identifier, IdentifierType, }, - tree_builder::LangType, + tree_builder::{JinjaDiagnostic, LangType}, }; use std::{ collections::{HashMap, HashSet}, @@ -16,7 +16,7 @@ use std::{ }; use tokio::{sync::mpsc, task::JoinHandle, time::sleep}; use tower_lsp::lsp_types::{ - CompletionItemKind, CompletionTextEdit, Diagnostic, DidOpenTextDocumentParams, DocumentSymbol, + CompletionItemKind, CompletionTextEdit, DidOpenTextDocumentParams, DocumentSymbol, DocumentSymbolResponse, TextDocumentIdentifier, TextEdit, }; @@ -48,6 +48,7 @@ pub struct LspFiles { pub diagnostics_task: JoinHandle<()>, pub main_channel: Option>, pub variables: HashMap>, + pub code_actions: HashMap>, pub is_vscode: bool, } @@ -73,17 +74,23 @@ impl LspFiles { match lang_type { LangType::Backend => { let mut variables = vec![]; - let query_defs = &self.queries.rust_definitions; - let query_templates = &self.queries.rust_templates; + let query_defs = &self.queries.backend_definitions; + let query_templates = &self.queries.backend_templates; let mut ids = - rust_definition_query(query_defs, tree, trigger_point, file_content, true) + backend_definition_query(query_defs, tree, trigger_point, file_content, true) .show(); - let mut templates = - rust_templates_query(query_templates, tree, trigger_point, file_content, true) - .collect(); + let mut templates = backend_templates_query( + query_templates, + tree, + trigger_point, + file_content, + true, + ) + .collect(); variables.append(&mut ids); variables.append(&mut templates); self.variables.insert(String::from(name), variables); + self.code_actions.insert(String::from(name), vec![]); } LangType::Template => { let mut variables = vec![]; @@ -99,6 +106,7 @@ impl LspFiles { .identifiers(); variables.append(&mut definitions); self.variables.insert(String::from(name), variables); + self.code_actions.insert(String::from(name), vec![]); } } Some(()) @@ -185,7 +193,7 @@ impl LspFiles { None } - pub fn read_tree(&self, name: &str) -> Option> { + pub fn read_tree(&self, name: &str) -> Option> { let rope = self.documents.get(name)?; let mut writter = FileContent::default(); let _ = rope.write_to(&mut writter); @@ -280,8 +288,8 @@ impl LspFiles { }) } LangType::Backend => { - let rust_templates = rust_templates_query( - &self.queries.rust_templates, + let rust_templates = backend_templates_query( + &self.queries.backend_templates, tree, point, &writter.content, @@ -299,6 +307,7 @@ impl LspFiles { } fn delete_variables(&mut self, uri: &str) -> Option<()> { + self.variables.get_mut(uri)?.clear(); self.variables.get_mut(uri)?.clear(); Some(()) } @@ -396,9 +405,7 @@ impl LspFiles { let buffer = std::fs::canonicalize(path).ok()?; let url = format!("file://{}", buffer.to_str()?); let url = Url::parse(&url).ok()?; - let start = to_position2(identifier.start); - let end = to_position2(identifier.end); - let range = Range::new(start, end); + let range = Range::new(Position::default(), Position::default()); let location = Location::new(url, range); res = Some(GotoDefinitionResponse::Scalar(location)); None @@ -426,17 +433,16 @@ impl LspFiles { } LangType::Backend => { - let query = &self.queries.rust_templates; - let templates = rust_templates_query(query, tree, point, &writter.content, false); + let query = &self.queries.backend_templates; + let templates = + backend_templates_query(query, tree, point, &writter.content, false); let template = templates.in_template(point)?; let dir = &self.config.templates; let path = format!("{dir}/{}", template.name); let buffer = std::fs::canonicalize(path).ok()?; let url = format!("file://{}", buffer.to_str()?); let url = Url::parse(&url).ok()?; - let start = to_position2(template.start); - let end = to_position2(template.end); - let range = Range::new(start, end); + let range = Range::new(Position::default(), Position::default()); let location = Location::new(url, range); Some(GotoDefinitionResponse::Scalar(location)) } @@ -463,7 +469,56 @@ impl LspFiles { Some(objects.in_expr(point)) } - pub fn read_trees(&self, diags: &mut HashMap>) { + pub fn code_action2(&self, action_params: CodeActionParams) -> Option { + let uri = action_params.text_document.uri.to_string(); + let lang_type = self.config.file_ext(&Path::new(&uri))?; + let row = action_params.range.start.line; + let column = action_params.range.start.character; + let point = Point::new(row as usize, column as usize); + let trees = self.trees.get(&lang_type)?; + let tree = trees.get(&uri)?; + let code_actions = self.code_actions.get(&uri)?; + match lang_type { + LangType::Template => { + let query = &self.queries.jinja_objects; + let doc = self.documents.get(&uri)?; + let mut writter = FileWriter::default(); + let _ = doc.write_to(&mut writter); + let code_action = code_actions + .iter() + .find(|item| point >= item.start && point <= item.end); + if let Some(code_action) = code_action { + return Some(JinjaCodeAction::CreateTemplate(code_action.name.to_owned())); + } + let objects = objects_query(query, tree, point, &writter.content, false); + if objects.in_expr(point) { + return Some(JinjaCodeAction::Reset); + } + None + } + LangType::Backend => { + let code_action = code_actions + .iter() + .find(|item| point >= item.start && point <= item.end); + if let Some(code_action) = code_action { + return Some(JinjaCodeAction::CreateTemplate(code_action.name.to_owned())); + } + None + } + } + } + + pub fn add_code_actions(&mut self, code_actions: HashMap>) { + for code_action in code_actions { + if let Some(item) = self.code_actions.get_mut(&code_action.0) { + *item = code_action.1; + } else { + self.code_actions.insert(code_action.0, code_action.1); + } + } + } + + pub fn read_trees(&self, diags: &mut HashMap>) { for tree in self.trees.get(&LangType::Template).unwrap() { let errors = self.read_tree(tree.0); if let Some(errors) = errors { @@ -624,6 +679,7 @@ impl Default for LspFiles { main_channel, variables: HashMap::default(), is_vscode: false, + code_actions: HashMap::default(), } } } @@ -663,3 +719,8 @@ impl std::io::Write for FileWriter { Ok(()) } } + +pub enum JinjaCodeAction { + Reset, + CreateTemplate(String), +}