diff --git a/Cargo.lock b/Cargo.lock index 1572c3f..0ab5697 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,15 +93,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitmaps" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" -dependencies = [ - "typenum", -] - [[package]] name = "bytes" version = "1.5.0" @@ -291,20 +282,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "im-rc" -version = "15.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" -dependencies = [ - "bitmaps", - "rand_core", - "rand_xoshiro", - "sized-chunks", - "typenum", - "version_check", -] - [[package]] name = "itoa" version = "1.0.10" @@ -313,19 +290,16 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jinja-lsp" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", - "dashmap", "env_logger", - "im-rc", "jinja-lsp-queries", - "log", "ropey", "serde", "serde_json", "tokio", - "tower-lsp 0.19.0", + "tower-lsp", "tree-sitter", "tree-sitter-jinja2", "tree-sitter-rust", @@ -334,11 +308,10 @@ dependencies = [ [[package]] name = "jinja-lsp-queries" -version = "0.1.3" +version = "0.1.6" dependencies = [ - "dashmap", "ropey", - "tower-lsp 0.20.0", + "tower-lsp", "tree-sitter", "tree-sitter-jinja2", "tree-sitter-rust", @@ -542,21 +515,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -683,16 +641,6 @@ dependencies = [ "libc", ] -[[package]] -name = "sized-chunks" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps", - "typenum", -] - [[package]] name = "slab" version = "0.4.9" @@ -834,29 +782,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" -[[package]] -name = "tower-lsp" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b38fb0e6ce037835174256518aace3ca621c4f96383c56bb846cfc11b341910" -dependencies = [ - "async-trait", - "auto_impl", - "bytes", - "dashmap", - "futures", - "httparse", - "lsp-types", - "memchr", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tower", - "tower-lsp-macros 0.8.0", - "tracing", -] - [[package]] name = "tower-lsp" version = "0.20.0" @@ -876,21 +801,10 @@ dependencies = [ "tokio", "tokio-util", "tower", - "tower-lsp-macros 0.9.0", + "tower-lsp-macros", "tracing", ] -[[package]] -name = "tower-lsp-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34723c06344244474fdde365b76aebef8050bf6be61a935b91ee9ff7c4e91157" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "tower-lsp-macros" version = "0.9.0" @@ -969,12 +883,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "unicode-bidi" version = "0.3.14" diff --git a/jinja-lsp-queries/Cargo.toml b/jinja-lsp-queries/Cargo.toml index a8b778a..d740238 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.5" +version = "0.1.6" edition = "2021" description = "TreeSitter queries for jinja-lsp" license = "MIT" @@ -9,7 +9,6 @@ license = "MIT" tree-sitter = "0.20.10" tree-sitter-jinja2 = "0.0.5" tree-sitter-rust = "0.20.4" -dashmap = "5.1.0" tower-lsp = { version = "0.20.0", features = ["proposed"] } ropey = "1.5.0" diff --git a/jinja-lsp-queries/src/capturer/included.rs b/jinja-lsp-queries/src/capturer/included.rs index ec24ecb..28537e2 100644 --- a/jinja-lsp-queries/src/capturer/included.rs +++ b/jinja-lsp-queries/src/capturer/included.rs @@ -1,16 +1,29 @@ +use tower_lsp::lsp_types::Url; use tree_sitter::Point; use super::Capturer; #[derive(Default, Debug)] pub struct IncludeCapturer { - pub template: String, - point: (Point, Point), + pub included: Vec, } impl IncludeCapturer { - pub fn in_template(&self, trigger_point: Point) -> bool { - trigger_point >= self.point.0 && trigger_point <= self.point.1 + pub fn in_template(&self, trigger_point: Point) -> Option<&IncludedTemplate> { + if let Some(last) = self.included.last() { + if trigger_point >= last.range.0 && trigger_point <= last.range.1 { + return Some(last); + } + } + None + } + + pub fn add_template(&mut self, name: String, range: (Point, Point)) { + self.included.push(IncludedTemplate { name, range }); + } + + pub fn last(&self) -> Option<&String> { + Some(&self.included.last()?.name) } } @@ -26,10 +39,32 @@ impl Capturer for IncludeCapturer { if let Ok(value) = capture.node.utf8_text(source.as_bytes()) { let start = capture.node.start_position(); let end = capture.node.end_position(); - self.template = value.replace(['\'', '\"'], ""); - self.point.0 = start; - self.point.1 = end; + let name = value.replace(['\'', '\"'], ""); + self.add_template(name, (start, end)); } } } } + +#[derive(Default, Debug)] +pub struct IncludedTemplate { + pub name: String, + pub range: (Point, Point), +} + +impl IncludedTemplate { + pub fn new(name: &str) -> Self { + Self { + name: name.to_owned(), + range: (Point::default(), Point::default()), + } + } + + pub fn is_template(&self, root: &str) -> Option { + let template = format!("{}/{}", root, &self.name); + let template = std::fs::canonicalize(template).ok()?; + let url = format!("file://{}", template.to_str()?); + let uri = Url::parse(&url).ok()?; + Some(uri) + } +} diff --git a/jinja-lsp-queries/src/capturer/object.rs b/jinja-lsp-queries/src/capturer/object.rs index c3c1574..ffa0d41 100644 --- a/jinja-lsp-queries/src/capturer/object.rs +++ b/jinja-lsp-queries/src/capturer/object.rs @@ -65,28 +65,24 @@ impl JinjaObjectCapturer { let end = capture.node.end_position(); if let Ok(value) = value { if start.row == self.dot.1.row && start.column == self.dot.1.column { - match self - .objects - .last_mut() - .map(|last| { - last.fields.push((String::from(value), (start, end))); - self.ident = (start, end); - }) - .is_none() - { - true => { - self.ident = (start, end); - let is_filter = self.is_hover(start); - self.objects.push(JinjaObject::new( - String::from(value), - start, - end, - is_filter, - )); + if let Some(last) = self.objects.last_mut() { + last.fields.push((String::from(value), (start, end))); + self.ident = (start, end); + } else { + // TODO: in future add those to main library + if VALID_IDENTIFIERS.contains(&value) { + return; } - false => (), + self.ident = (start, end); + let is_filter = self.is_hover(start); + self.objects + .push(JinjaObject::new(String::from(value), start, end, is_filter)); } } else { + // TODO: in future add those to main library + if VALID_IDENTIFIERS.contains(&value) { + return; + } self.ident = (start, end); let is_filter = self.is_hover(start); self.objects @@ -143,3 +139,5 @@ pub enum CompletionType { Filter, Identifier, } + +static VALID_IDENTIFIERS: [&str; 4] = ["loop", "true", "false", "not"]; diff --git a/jinja-lsp-queries/src/lsp_helper.rs b/jinja-lsp-queries/src/lsp_helper.rs index 2b761fe..1269e3f 100644 --- a/jinja-lsp-queries/src/lsp_helper.rs +++ b/jinja-lsp-queries/src/lsp_helper.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; -use dashmap::DashMap; use tree_sitter::{Node, Point}; use crate::{ @@ -13,42 +12,38 @@ pub fn search_errors( root: Node<'_>, source: &str, query: &Queries, - variables: &DashMap>, + variables: &HashMap>, file_name: &String, - diags: &mut HashMap>, -) -> Option<()> { +) -> Option> { let trigger_point = Point::new(0, 0); let query = &query.jinja_idents; let capturer = JinjaObjectCapturer::default(); let props = query_props(root, source, trigger_point, query, true, capturer); let props = props.show(); + let mut diags = vec![]; for object in props { if object.is_filter { continue; } - let file = variables.get(file_name)?; + let jinja_variables = variables.get(file_name)?; let mut exist = false; let mut err_type = JinjaDiagnostic::Undefined; let mut to_warn = false; - let temp = file - .value() + // variable definition is in this file + let located = jinja_variables .iter() .filter(|variable| variable.name == object.name) .filter(|variable| { exist = true; object.location.0 >= variable.location.0 }); - let empty = temp.count() == 0; + let empty = located.count() == 0; if empty && exist { to_warn = true; } else if empty { to_warn = true; - drop(file); for i in variables { - let temp = i - .value() - .iter() - .filter(|variable| variable.name == object.name); + let temp = i.1.iter().filter(|variable| variable.name == object.name); if temp.count() != 0 { err_type = JinjaDiagnostic::DefinedSomewhere; @@ -59,12 +54,12 @@ pub fn search_errors( } if to_warn { let variable = JinjaVariable::new(&object.name, object.location, DataType::Variable); - if diags.get(file_name).is_none() { - diags.insert(file_name.to_string(), vec![(variable, err_type)]); - } else { - diags.get_mut(file_name).unwrap().push((variable, err_type)); - } + diags.push((variable, err_type)); } } - None + if diags.is_empty() { + None + } else { + Some(diags) + } } diff --git a/jinja-lsp-queries/src/test_queries.rs b/jinja-lsp-queries/src/test_queries.rs index 4b89bc2..ce10c0b 100644 --- a/jinja-lsp-queries/src/test_queries.rs +++ b/jinja-lsp-queries/src/test_queries.rs @@ -1,11 +1,11 @@ #[cfg(test)] mod query_tests { + use crate::capturer::included::IncludeCapturer; use tree_sitter::{Parser, Point}; use crate::{ capturer::{ - included::IncludeCapturer, init::JinjaInitCapturer, object::{CompletionType, JinjaObjectCapturer}, rust::RustCapturer, @@ -222,8 +222,9 @@ mod query_tests { false, capturer, ); - assert!(props.in_template(case.0)); - assert_eq!(&props.template, case.1); + let template = props.in_template(case.0); + assert!(template.is_some()); + assert_eq!(&template.unwrap().name, &case.1); } } } diff --git a/jinja-lsp/Cargo.toml b/jinja-lsp/Cargo.toml index 80071f0..66572b8 100644 --- a/jinja-lsp/Cargo.toml +++ b/jinja-lsp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jinja-lsp" -version = "0.1.5" +version = "0.1.6" edition = "2021" license = "MIT" authors = ["uros-5"] @@ -23,14 +23,11 @@ env_logger = "0.9.0" ropey = "1.5.0" serde_json = "1.0.78" tokio = { version = "1.17.0", features = ["full"] } -tower-lsp = { version = "0.19.0", features = ["proposed"]} +tower-lsp = { version = "0.20.0", features = ["proposed"]} serde = { version = "1.0", features = ["derive"] } -dashmap = "5.1.0" -log = "0.4.14" -im-rc = "15.0.0" tree-sitter = "0.20.10" walkdir = "2.4.0" anyhow = "1.0.75" tree-sitter-jinja2 = "0.0.5" tree-sitter-rust = "0.20.4" -jinja-lsp-queries = { path = "../jinja-lsp-queries", version = "0.1.3"} +jinja-lsp-queries = { path = "../jinja-lsp-queries", version = "0.1.6"} diff --git a/jinja-lsp/src/backend.rs b/jinja-lsp/src/backend.rs index 9305b2a..7212732 100644 --- a/jinja-lsp/src/backend.rs +++ b/jinja-lsp/src/backend.rs @@ -1,308 +1,134 @@ -use std::{ - collections::HashMap, - path::Path, - sync::{Arc, Mutex, RwLock}, -}; - -use dashmap::{mapref::one::RefMut, DashMap}; -use ropey::Rope; use serde_json::Value; +use tokio::sync::{ + mpsc::{self, Sender}, + oneshot, +}; use tower_lsp::{ jsonrpc::Result, - lsp_types::{InitializeParams, InitializeResult, MessageType}, + lsp_types::{ + CompletionParams, CompletionResponse, DidCloseTextDocumentParams, + DidOpenTextDocumentParams, InitializeParams, InitializeResult, + }, Client, LanguageServer, }; use tower_lsp::lsp_types::{ - CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, - CodeActionProviderCapability, CodeActionResponse, Command, CompletionContext, CompletionItem, - CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, - CompletionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, - DidSaveTextDocumentParams, Documentation, ExecuteCommandOptions, ExecuteCommandParams, - GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, - HoverProviderCapability, InitializedParams, MarkupContent, MarkupKind, OneOf, Position, Range, - ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, - TextDocumentSyncOptions, TextDocumentSyncSaveOptions, Url, -}; - -use jinja_lsp_queries::{ - capturer::object::CompletionType, - to_input_edit::ToInputEdit, - tree_builder::{JinjaDiagnostic, JinjaVariable, LangType}, + CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, Command, + DidChangeTextDocumentParams, DidSaveTextDocumentParams, ExecuteCommandParams, + GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, InitializedParams, }; -use crate::{ - config::{config_exist, read_config, walkdir, JinjaConfig}, - filter::{init_filter_completions, FilterCompletion}, - lsp_files::{FileWriter, LspFiles}, +use crate::channels::{ + diagnostics::diagnostics_task, + lsp::{lsp_task, LspMessage}, }; pub struct Backend { - client: Client, - document_map: DashMap, - can_complete: RwLock, - config: RwLock, - filter_values: Vec, - pub lsp_files: Arc>, + main_channel: Sender, } #[tower_lsp::async_trait] impl LanguageServer for Backend { async fn initialize(&self, params: InitializeParams) -> Result { - let mut root = Url::parse("file://").unwrap(); - if let Some(folders) = params.workspace_folders { - if let Some(dirs) = folders.first() { - root = dirs.uri.to_owned(); - } - } else if let Some(dir) = params.root_uri { - root = dir.to_owned(); - } - if let Ok(mut lsp_files) = self.lsp_files.lock() { - lsp_files.root_path = root; - } - let mut definition_provider = None; - let mut references_provider = None; - let mut code_action_provider = None; - let mut hover_provider = None; - let mut execute_command_provider = None; - - if let Some(client_info) = params.client_info { - if client_info.name == "helix" { - if let Ok(mut can_complete) = self.can_complete.write() { - *can_complete = true; - } - } - } - - match config_exist(params.initialization_options) { - Some(config) => { - self.config - .try_write() - .ok() - .and_then(|mut jinja_config| -> Option<()> { - definition_provider = Some(OneOf::Left(true)); - references_provider = Some(OneOf::Left(true)); - code_action_provider = Some(CodeActionProviderCapability::Simple(true)); - hover_provider = Some(HoverProviderCapability::Simple(true)); - execute_command_provider = Some(ExecuteCommandOptions { - commands: vec!["reset_variables".to_string(), "warn".to_string()], - ..Default::default() - }); - *jinja_config = config; - None - }); - } - None => { - self.client - .log_message(MessageType::INFO, "Config not found") - .await; - } + let (sender, rx) = oneshot::channel(); + let _ = self + .main_channel + .send(LspMessage::Initialize(Box::new(params), sender)) + .await; + if let Ok(msg) = rx.await { + Ok(msg) + } else { + Ok(InitializeResult::default()) } - - Ok(InitializeResult { - capabilities: ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Options( - TextDocumentSyncOptions { - change: Some(TextDocumentSyncKind::INCREMENTAL), - will_save: Some(true), - save: Some(TextDocumentSyncSaveOptions::Supported(true)), - ..Default::default() - }, - )), - completion_provider: Some(CompletionOptions { - resolve_provider: Some(false), - trigger_characters: Some(vec![ - "-".to_string(), - "\"".to_string(), - " ".to_string(), - ]), - all_commit_characters: None, - work_done_progress_options: Default::default(), - completion_item: None, - }), - definition_provider, - references_provider, - code_action_provider, - execute_command_provider, - hover_provider, - ..ServerCapabilities::default() - }, - server_info: Some(ServerInfo { - name: String::from("jinja-lsp"), - version: Some(String::from("0.1.3")), - }), - offset_encoding: None, - }) } async fn initialized(&self, _params: InitializedParams) { - self.client - .log_message(MessageType::INFO, "Initialized") + let (sender, rx) = oneshot::channel(); + let _ = self + .main_channel + .send(LspMessage::Initialized(sender)) .await; - - match read_config(&self.config, &self.lsp_files, &self.document_map) { - Ok(d) => { - self.publish_tag_diagnostics(d, None).await; - } - Err(err) => { - self.config - .write() - .ok() - .and_then(|mut config| config.user_defined(false)); - let msg = err.to_string(); - self.client.log_message(MessageType::INFO, msg).await; + if let Ok(msg) = rx.await { + if !msg { + let _ = self.shutdown().await; } + } else { + let _ = self.shutdown().await; } } + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let _ = self.main_channel.send(LspMessage::DidOpen(params)).await; + } + + async fn did_close(&self, _params: DidCloseTextDocumentParams) {} + async fn did_change(&self, params: DidChangeTextDocumentParams) { - let uri = params.text_document.uri.to_string(); - let rope = self.document_map.get_mut(&uri); - rope.and_then(|mut rope| self.on_change(&mut rope, params, uri)); + let _ = self.main_channel.send(LspMessage::DidChange(params)).await; } async fn did_save(&self, params: DidSaveTextDocumentParams) { - let uri = params.text_document.uri.to_string(); - let mut diags = HashMap::new(); - if let Ok(lsp_files) = self.lsp_files.lock() { - lsp_files.saved(&uri, &self.config, &self.document_map, &mut diags); - } - self.publish_tag_diagnostics(diags, Some(uri)).await; + let _ = self.main_channel.send(LspMessage::DidSave(params)).await; } async fn completion(&self, params: CompletionParams) -> Result> { - let can_complete = { - matches!( - params.context, - Some(CompletionContext { - trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, - .. - }) | Some(CompletionContext { - trigger_kind: CompletionTriggerKind::INVOKED, - .. - }) - ) - }; - if !can_complete { - let can_complete = self.can_complete.read().is_ok_and(|d| *d); - if !can_complete { - return Ok(None); - } - } - let uri = ¶ms.text_document_position.text_document.uri.clone(); - let position = params.text_document_position.position; - let lang_type = self.get_lang_type(uri.as_str()); - - let mut completion = None; - let mut items = None; - if let Some(lang_type) = lang_type { - if lang_type == LangType::Template { - completion = self.start_completion(params); - } - } - if let Some(completion) = completion { - match completion { - CompletionType::Filter => { - let completions = self.filter_values.clone(); - let mut ret = Vec::with_capacity(completions.len()); - for item in completions.into_iter() { - ret.push(CompletionItem { - label: item.name, - kind: Some(CompletionItemKind::TEXT), - documentation: Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: item.desc.to_string(), - })), - ..Default::default() - }); - } - items = Some(CompletionResponse::Array(ret)); - } - CompletionType::Identifier => { - if let Some(variables) = self.lsp_files.lock().ok().and_then(|lsp_files| { - lsp_files.get_variables( - uri, - &self.document_map, - LangType::Template, - position, - ) - }) { - items = Some(CompletionResponse::Array(variables)); - } - } - } + let (sender, tx) = oneshot::channel(); + let _ = self + .main_channel + .send(LspMessage::Completion(params, sender)) + .await; + if let Ok(completion) = tx.await { + return Ok(completion); } - Ok(items) + Ok(None) } async fn hover(&self, params: HoverParams) -> Result> { - let uri = ¶ms - .text_document_position_params - .text_document - .uri - .clone(); - let lang_type = self.get_lang_type(uri.as_str()); - let can_hover = lang_type.map_or(false, |lang_type| lang_type == LangType::Template); - if !can_hover { - return Ok(None); - } - let mut res = None; - - if let Some(filter) = self - .start_hover(params) - .and_then(|hover| self.filter_values.iter().find(|name| name.name == hover)) - { - let markup_content = MarkupContent { - kind: MarkupKind::Markdown, - value: filter.desc.to_string(), - }; - let hover_contents = HoverContents::Markup(markup_content); - let hover = Hover { - contents: hover_contents, - range: None, - }; - res = Some(hover); + let (sender, tx) = oneshot::channel(); + let _ = self + .main_channel + .send(LspMessage::Hover(params, sender)) + .await; + if let Ok(hover) = tx.await { + return Ok(hover); } - Ok(res) + Ok(None) } async fn goto_definition( &self, params: GotoDefinitionParams, ) -> Result> { - let uri = ¶ms - .text_document_position_params - .text_document - .uri - .clone(); - let lang_type = self.get_lang_type(uri.as_str()); - let can_def = lang_type.map_or(false, |lang_type| lang_type == LangType::Template); - if !can_def { - return Ok(None); + let (sender, tx) = oneshot::channel(); + let _ = self + .main_channel + .send(LspMessage::GoToDefinition(params, sender)) + .await; + if let Ok(definition) = tx.await { + return Ok(definition); } - let res = self.start_goto_definition(params); - Ok(res) + Ok(None) } async fn code_action(&self, params: CodeActionParams) -> Result> { - let uri = ¶ms.text_document.uri.clone(); - let lang_type = self.get_lang_type(uri.as_str()); - let can_def = lang_type.map_or(false, |lang_type| lang_type == LangType::Template); - if !can_def { - return Ok(None); - } - let mut res = None; - let code_action = self.start_code_action(params); - if let Some(code_action) = code_action { - if code_action { - res = Some(code_actions()); - } + let (sender, tx) = oneshot::channel(); + let _ = self + .main_channel + .send(LspMessage::CodeAction(params, sender)) + .await; + if let Ok(code_action) = tx.await { + return Ok(code_action); } - Ok(res) + Ok(None) } async fn execute_command(&self, params: ExecuteCommandParams) -> Result> { - Ok(self.start_command(params).await) + let (sender, _) = oneshot::channel(); + let _ = self + .main_channel + .send(LspMessage::ExecuteCommand(params, sender)) + .await; + Ok(None) } async fn shutdown(&self) -> Result<()> { @@ -331,178 +157,17 @@ pub fn code_actions() -> Vec { } impl Backend { pub fn new(client: Client) -> Self { - let document_map = DashMap::new(); - let can_complete = RwLock::new(false); - let config = RwLock::new(JinjaConfig::default()); - let filter_values = init_filter_completions(); - let lsp_files = Arc::new(Mutex::new(LspFiles::default())); + let (lsp_sender, lsp_recv) = mpsc::channel(20); + let (diagnostic_sender, diagnostic_recv) = mpsc::channel(20); + lsp_task( + client.clone(), + diagnostic_sender, + lsp_sender.clone(), + lsp_recv, + ); + diagnostics_task(client.clone(), diagnostic_recv); Self { - client, - document_map, - can_complete, - config, - filter_values, - lsp_files, - } - } - - fn on_change( - &self, - rope: &mut RefMut<'_, String, Rope>, - params: DidChangeTextDocumentParams, - uri: String, - ) -> Option<()> { - for change in params.content_changes { - let range = &change.range?; - let input_edit = range.to_input_edit(rope); - if change.text.is_empty() { - self.on_remove(range, rope); - } else { - self.on_insert(range, &change.text, rope); - } - let mut w = FileWriter::default(); - let _ = rope.write_to(&mut w); - self.lsp_files.lock().ok().and_then(|lsp_files| { - let lang_type = self.get_lang_type(&uri); - lsp_files.input_edit(&uri, w.content, input_edit, lang_type) - }); - } - - None - } - - fn on_remove(&self, range: &Range, rope: &mut RefMut<'_, String, Rope>) -> Option<()> { - let (start, end) = range.to_byte(rope); - rope.remove(start..end); - None - } - - fn on_insert( - &self, - range: &Range, - text: &str, - rope: &mut RefMut<'_, String, Rope>, - ) -> Option<()> { - let (start, _) = range.to_byte(rope); - rope.insert(start, text); - None - } - - fn get_lang_type(&self, path: &str) -> Option { - let path = Path::new(path); - let config = self.config.read(); - config.ok().and_then(|config| config.file_ext(&path)) - } - - pub async fn publish_tag_diagnostics( - &self, - diagnostics: HashMap>, - current_file: Option, - ) { - let mut hm: HashMap> = HashMap::new(); - let mut added = false; - - for (file, diags) in diagnostics { - for (variable, diag2) in diags { - let severity = { - match diag2 { - JinjaDiagnostic::DefinedSomewhere => DiagnosticSeverity::INFORMATION, - JinjaDiagnostic::Undefined => DiagnosticSeverity::WARNING, - } - }; - added = true; - - let diagnostic = Diagnostic { - range: Range::new( - Position::new( - variable.location.0.row as u32, - variable.location.0.column as u32, - ), - Position::new( - variable.location.1.row as u32, - variable.location.1.column as u32, - ), - ), - severity: Some(severity), - message: diag2.to_string(), - source: Some(String::from("jinja-lsp")), - ..Default::default() - }; - - if hm.contains_key(&file) { - let _ = hm.get_mut(&file).is_some_and(|d| { - d.push(diagnostic); - false - }); - } else { - hm.insert(String::from(&file), vec![diagnostic]); - } - } - } - - for (url, diagnostics) in hm { - if let Ok(uri) = Url::parse(&url) { - self.client - .publish_diagnostics(uri, diagnostics, None) - .await; - } - } - if let Some(uri) = current_file { - if !added { - let uri = Url::parse(&uri).unwrap(); - self.client.publish_diagnostics(uri, vec![], None).await; - } - } - } - - pub fn start_completion(&self, params: CompletionParams) -> Option { - self.lsp_files - .lock() - .ok() - .and_then(|lsp_files| lsp_files.completion(params, &self.config, &self.document_map)) - } - - pub fn start_hover(&self, params: HoverParams) -> Option { - self.lsp_files - .lock() - .ok() - .and_then(|lsp_files| lsp_files.hover(params, &self.document_map)) - } - - pub fn start_goto_definition( - &self, - params: GotoDefinitionParams, - ) -> Option { - self.lsp_files.lock().ok().and_then(|lsp_files| { - lsp_files.goto_definition(params, &self.document_map, &self.config) - }) - } - - pub fn start_code_action(&self, params: CodeActionParams) -> Option { - self.lsp_files - .lock() - .ok() - .and_then(|lsp_files| lsp_files.code_action(params, &self.document_map)) - } - - pub async fn start_command(&self, params: ExecuteCommandParams) -> Option { - let mut diagnostics = HashMap::new(); - let command = params.command; - if command == "reset_variables" { - self.config.read().ok().and_then(|config| -> Option<()> { - let diagnostics2 = walkdir(&config, &self.lsp_files, &self.document_map); - if let Ok(all) = diagnostics2 { - diagnostics = all; - } - None - }); - if diagnostics.is_empty() { - return None; - } - self.publish_tag_diagnostics(diagnostics, None).await; - None - } else { - None + main_channel: lsp_sender, } } } diff --git a/jinja-lsp/src/channels/diagnostics.rs b/jinja-lsp/src/channels/diagnostics.rs new file mode 100644 index 0000000..a0a950f --- /dev/null +++ b/jinja-lsp/src/channels/diagnostics.rs @@ -0,0 +1,82 @@ +use std::collections::HashMap; + +use jinja_lsp_queries::tree_builder::{JinjaDiagnostic, JinjaVariable}; +use tokio::sync::mpsc::Receiver; +use tower_lsp::{ + lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range, Url}, + Client, +}; + +pub fn diagnostics_task(client: Client, mut receiver: Receiver) { + tokio::spawn(async move { + while let Some(msg) = receiver.recv().await { + match msg { + DiagnosticMessage::Errors { + diagnostics, + current_file, + } => { + let mut hm: HashMap> = HashMap::new(); + let mut added = false; + for (file, diags) in diagnostics { + for (variable, diag2) in diags { + let severity = { + match diag2 { + JinjaDiagnostic::DefinedSomewhere => { + DiagnosticSeverity::INFORMATION + } + JinjaDiagnostic::Undefined => DiagnosticSeverity::WARNING, + } + }; + added = true; + + let diagnostic = Diagnostic { + range: Range::new( + Position::new( + variable.location.0.row as u32, + variable.location.0.column as u32, + ), + Position::new( + variable.location.1.row as u32, + variable.location.1.column as u32, + ), + ), + severity: Some(severity), + message: diag2.to_string(), + source: Some(String::from("jinja-lsp")), + ..Default::default() + }; + + if hm.contains_key(&file) { + let _ = hm.get_mut(&file).is_some_and(|d| { + d.push(diagnostic); + false + }); + } else { + hm.insert(String::from(&file), vec![diagnostic]); + } + } + } + + for (url, diagnostics) in hm { + if let Ok(uri) = Url::parse(&url) { + client.publish_diagnostics(uri, diagnostics, None).await; + } + } + if let Some(uri) = current_file { + if !added { + let uri = Url::parse(&uri).unwrap(); + client.publish_diagnostics(uri, vec![], None).await; + } + } + } + } + } + }); +} + +pub enum DiagnosticMessage { + Errors { + diagnostics: HashMap>, + current_file: Option, + }, +} diff --git a/jinja-lsp/src/channels/lsp.rs b/jinja-lsp/src/channels/lsp.rs new file mode 100644 index 0000000..3a39e68 --- /dev/null +++ b/jinja-lsp/src/channels/lsp.rs @@ -0,0 +1,259 @@ +use jinja_lsp_queries::capturer::object::CompletionType; +use serde_json::Value; +use tokio::sync::{mpsc, oneshot}; +use tower_lsp::{ + lsp_types::{ + CodeActionParams, CodeActionProviderCapability, CodeActionResponse, CompletionItem, + CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, + DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, + Documentation, ExecuteCommandOptions, ExecuteCommandParams, GotoDefinitionParams, + GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, + InitializeParams, InitializeResult, MarkupContent, MarkupKind, MessageType, OneOf, + ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, + TextDocumentSyncOptions, TextDocumentSyncSaveOptions, + }, + Client, +}; + +use crate::{ + backend::code_actions, + config::{walkdir, JinjaConfig}, + filter::init_filter_completions, + lsp_files::LspFiles, +}; + +use super::diagnostics::DiagnosticMessage; + +pub fn lsp_task( + client: Client, + diagnostics_channel: mpsc::Sender, + lsp_channel: mpsc::Sender, + mut lsp_recv: mpsc::Receiver, +) { + // let mut documents = HashMap::new(); + let mut can_complete = false; + let mut config = JinjaConfig::default(); + let mut lsp_data = LspFiles::default(); + let filters = init_filter_completions(); + tokio::spawn(async move { + while let Some(msg) = lsp_recv.recv().await { + match msg { + LspMessage::Initialize(params, sender) => { + if let Some(client_info) = params.client_info { + if client_info.name == "helix" { + can_complete = true; + } + } + params + .initialization_options + .map(serde_json::from_value) + .map(|res| res.ok()) + .and_then(|c| -> Option<()> { + config = c?; + config.user_defined = true; + None + }); + + if !config.user_defined { + drop(sender); + continue; + } + + let definition_provider = Some(OneOf::Left(true)); + let references_provider = None; + let code_action_provider = Some(CodeActionProviderCapability::Simple(true)); + let hover_provider = Some(HoverProviderCapability::Simple(true)); + let execute_command_provider = Some(ExecuteCommandOptions { + commands: vec!["reset_variables".to_string(), "warn".to_string()], + ..Default::default() + }); + + let msg = InitializeResult { + capabilities: ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + change: Some(TextDocumentSyncKind::INCREMENTAL), + will_save: Some(true), + save: Some(TextDocumentSyncSaveOptions::Supported(true)), + ..Default::default() + }, + )), + completion_provider: Some(CompletionOptions { + resolve_provider: Some(false), + trigger_characters: Some(vec![ + "-".to_string(), + "\"".to_string(), + " ".to_string(), + ]), + all_commit_characters: None, + work_done_progress_options: Default::default(), + completion_item: None, + }), + definition_provider, + references_provider, + code_action_provider, + execute_command_provider, + hover_provider, + ..ServerCapabilities::default() + }, + server_info: Some(ServerInfo { + name: String::from("jinja-lsp"), + version: Some(String::from("0.1.6")), + }), + offset_encoding: None, + }; + let _ = sender.send(msg); + } + LspMessage::Initialized(sender) => { + client.log_message(MessageType::INFO, "Initialized").await; + if !config.user_defined { + client + .log_message(MessageType::INFO, "Config doesn't exist.") + .await; + break; + } + if config.templates.is_empty() { + client + .log_message(MessageType::INFO, "Template directory not found") + .await; + break; + } + if config.lang != "rust" { + client + .log_message(MessageType::INFO, "Backend language not supported") + .await; + break; + } else { + match walkdir(&config) { + Ok(errors) => { + let _ = diagnostics_channel + .send(DiagnosticMessage::Errors { + diagnostics: errors.0, + current_file: None, + }) + .await; + lsp_data = errors.1; + let _ = sender.send(true); + } + Err(err) => { + let msg = err.to_string(); + client.log_message(MessageType::INFO, msg).await; + let _ = sender.send(false); + break; + } + } + } + } + LspMessage::DidChange(params) => { + lsp_data.did_change(params, &config); + } + LspMessage::DidSave(params) => { + if let Some(errors) = lsp_data.did_save(params, &config) { + let _ = diagnostics_channel.send(errors).await; + } + } + LspMessage::Completion(params, sender) => { + let position = params.text_document_position.position; + let uri = params.text_document_position.text_document.uri.clone(); + let completion = lsp_data.completion(params, &config, can_complete); + let mut items = None; + + if let Some(completion) = completion { + match completion { + CompletionType::Filter => { + let completions = filters.clone(); + let mut ret = Vec::with_capacity(completions.len()); + for item in completions.into_iter() { + ret.push(CompletionItem { + label: item.name, + kind: Some(CompletionItemKind::TEXT), + documentation: Some(Documentation::MarkupContent( + MarkupContent { + kind: MarkupKind::Markdown, + value: item.desc.to_string(), + }, + )), + ..Default::default() + }); + } + items = Some(CompletionResponse::Array(ret)); + } + CompletionType::Identifier => { + if let Some(variables) = lsp_data.read_variables(&uri, position) { + items = Some(CompletionResponse::Array(variables)); + } + } + }; + } + let _ = sender.send(items); + } + LspMessage::Hover(params, sender) => { + let mut res = None; + if let Some(hover) = lsp_data.hover(params, &config) { + let filter = filters.iter().find(|name| name.name == hover); + if let Some(filter) = filter { + let markup_content = MarkupContent { + kind: MarkupKind::Markdown, + value: filter.desc.to_string(), + }; + let hover_contents = HoverContents::Markup(markup_content); + let hover = Hover { + contents: hover_contents, + range: None, + }; + res = Some(hover); + } + } + let _ = sender.send(res); + } + LspMessage::GoToDefinition(params, sender) => { + if let Some(definition) = lsp_data.goto_definition(params, &config) { + let _ = sender.send(Some(definition)); + } + } + LspMessage::CodeAction(params, sender) => { + if let Some(is_code_action) = lsp_data.code_action(params, &config) { + if is_code_action { + let _ = sender.send(Some(code_actions())); + } + } + } + LspMessage::ExecuteCommand(params, sender) => { + let command = params.command; + if command == "reset_variables" { + let (sender2, _) = oneshot::channel(); + let _ = lsp_channel.send(LspMessage::Initialized(sender2)).await; + let _ = sender.send(None); + } + } + LspMessage::DidOpen(params) => { + if let Some(errors) = lsp_data.did_open(params, &config) { + let _ = diagnostics_channel.send(errors).await; + } + } + } + } + }); +} + +pub enum LspMessage { + Initialize(Box, oneshot::Sender), + Initialized(oneshot::Sender), + DidOpen(DidOpenTextDocumentParams), + DidChange(DidChangeTextDocumentParams), + DidSave(DidSaveTextDocumentParams), + Completion( + CompletionParams, + oneshot::Sender>, + ), + Hover(HoverParams, oneshot::Sender>), + GoToDefinition( + GotoDefinitionParams, + oneshot::Sender>, + ), + CodeAction( + CodeActionParams, + oneshot::Sender>, + ), + ExecuteCommand(ExecuteCommandParams, oneshot::Sender>), +} diff --git a/jinja-lsp/src/channels/mod.rs b/jinja-lsp/src/channels/mod.rs new file mode 100644 index 0000000..ce8d135 --- /dev/null +++ b/jinja-lsp/src/channels/mod.rs @@ -0,0 +1,2 @@ +pub mod diagnostics; +pub mod lsp; diff --git a/jinja-lsp/src/config.rs b/jinja-lsp/src/config.rs index 4928030..6124986 100644 --- a/jinja-lsp/src/config.rs +++ b/jinja-lsp/src/config.rs @@ -1,15 +1,7 @@ -use std::{ - collections::HashMap, - path::Path, - sync::{Arc, Mutex, RwLock}, -}; +use std::{collections::HashMap, path::Path}; -use anyhow::Error; -use dashmap::DashMap; use jinja_lsp_queries::tree_builder::{JinjaDiagnostic, JinjaVariable, LangType}; -use ropey::Rope; use serde::{Deserialize, Serialize}; -use serde_json::Value; use walkdir::WalkDir; use crate::lsp_files::LspFiles; @@ -19,8 +11,8 @@ use crate::lsp_files::LspFiles; #[derive(Serialize, Deserialize, Debug, Default)] pub struct JinjaConfig { pub templates: String, - backend: Vec, - lang: String, + pub backend: Vec, + pub lang: String, #[serde(skip)] pub user_defined: bool, } @@ -43,45 +35,17 @@ impl JinjaConfig { } } -pub fn config_exist(config: Option) -> Option { - let config = config?; - if let Ok(mut config) = serde_json::from_value::(config) { - config.user_defined = true; - return Some(config); - } - None -} +pub type InitLsp = ( + HashMap>, + LspFiles, +); -pub fn read_config( - config: &RwLock, - lsp_files: &Arc>, - document_map: &DashMap, -) -> anyhow::Result>> { - if let Ok(config) = config.read() { - if !config.user_defined { - return Err(Error::msg("Config doesn't exist")); - } - if config.templates.is_empty() { - return Err(Error::msg("Template directory not found")); - } - if !is_backend(&config.lang) { - Err(Error::msg("Backend language not supported")) - } else { - walkdir(&config, lsp_files, document_map) - } - } else { - Err(Error::msg("Config doesn't exist")) - } -} - -pub fn walkdir( - config: &JinjaConfig, - lsp_files: &Arc>, - document_map: &DashMap, -) -> anyhow::Result>> { +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(); let mut diags = HashMap::new(); for dir in all { let walk = WalkDir::new(dir); @@ -92,24 +56,12 @@ pub fn walkdir( let path = &entry.path(); let ext = config.file_ext(path); if let Some(ext) = ext { - let _ = lsp_files.lock().is_ok_and(|lsp_files| { - lsp_files.read_file(path, ext, document_map, &mut diags); - true - }); + lsp_files.read_file(path, ext); } } } } - let _ = lsp_files.lock().ok().and_then(|lsp_files| -> Option<()> { - let trees = lsp_files.get_trees_vec(LangType::Template); - for tree in trees { - lsp_files.read_tree(document_map, &mut diags, &tree); - } - None - }); - Ok(diags) -} -fn is_backend(lang: &str) -> bool { - lang == "rust" + lsp_files.read_trees(&mut diags); + Ok((diags, lsp_files)) } diff --git a/jinja-lsp/src/lsp_files.rs b/jinja-lsp/src/lsp_files.rs index 5781982..efc23e1 100644 --- a/jinja-lsp/src/lsp_files.rs +++ b/jinja-lsp/src/lsp_files.rs @@ -1,11 +1,7 @@ -use std::{ - collections::HashMap, - fs::read_to_string, - path::Path, - sync::{Arc, Mutex, RwLock}, -}; +use jinja_lsp_queries::tree_builder::{DataType, JinjaDiagnostic, JinjaVariable, LangType}; +use std::{collections::HashMap, fs::read_to_string, path::Path}; +use tower_lsp::lsp_types::{CompletionItemKind, DidOpenTextDocumentParams}; -use dashmap::DashMap; use jinja_lsp_queries::{ capturer::{ included::IncludeCapturer, @@ -16,150 +12,80 @@ use jinja_lsp_queries::{ lsp_helper::search_errors, parsers::Parsers, queries::{query_props, Queries}, - to_input_edit::{to_position, to_position2}, - tree_builder::{DataType, JinjaDiagnostic, JinjaVariable, LangType}, + to_input_edit::{to_position, to_position2, ToInputEdit}, }; use ropey::Rope; + use tower_lsp::lsp_types::{ - CodeActionParams, CompletionItem, CompletionItemKind, CompletionParams, GotoDefinitionParams, + CodeActionParams, CompletionContext, CompletionItem, CompletionParams, CompletionTriggerKind, + DidChangeTextDocumentParams, DidSaveTextDocumentParams, GotoDefinitionParams, GotoDefinitionResponse, HoverParams, Location, Position, Range, Url, }; use tree_sitter::{InputEdit, Point, Tree}; -use crate::config::JinjaConfig; +use crate::{channels::diagnostics::DiagnosticMessage, config::JinjaConfig}; -#[derive(Clone)] pub struct LspFiles { - trees: DashMap>, - pub parsers: Arc>, - pub variables: DashMap>, - pub queries: Arc>, - pub root_path: Url, -} - -impl Default for LspFiles { - fn default() -> Self { - let trees = DashMap::new(); - trees.insert(LangType::Template, DashMap::new()); - trees.insert(LangType::Backend, DashMap::new()); - Self { - trees, - parsers: Arc::new(Mutex::new(Parsers::default())), - variables: DashMap::new(), - queries: Arc::new(Mutex::new(Queries::default())), - root_path: Url::parse("file://").unwrap(), - } - } + trees: HashMap>, + documents: HashMap, + pub parsers: Parsers, + pub variables: HashMap>, + pub queries: Queries, } impl LspFiles { - pub fn read_file( - &self, - path: &&Path, - lang_type: LangType, - document_map: &DashMap, - _diags: &mut HashMap>, - ) -> Option<()> { - let res = None; - let mut errors = None; + pub fn read_file(&mut self, path: &&Path, lang_type: LangType) -> Option<()> { if let Ok(name) = std::fs::canonicalize(path) { let name = name.to_str()?; - let mut file_content = String::new(); - read_to_string(name).ok().and_then(|content| -> Option<()> { - file_content = content; - let rope = ropey::Rope::from_str(&file_content); - let name = format!("file://{}", name); - document_map.insert(name.to_string(), rope); - self.add_tree(&name, lang_type, &file_content); - None - }); - - let _ = self.queries.lock().ok().and_then(|query| -> Option<()> { - let name = format!("file://{}", name); - self.delete_variables(&name); - errors = self.add_variables(&name, lang_type, &file_content, &query); - None - }); + let file_content = read_to_string(name).ok()?; + let rope = Rope::from_str(&file_content); + let name = format!("file://{}", name); + let adding = name.clone(); + self.delete_variables(&name); + self.documents.insert(name.to_string(), rope); + self.add_tree(&name, lang_type, &file_content); + self.add_variables(&adding, lang_type, &file_content); } - res + None } - pub fn read_tree( - &self, - document_map: &DashMap, - diags: &mut HashMap>, - name: &str, + pub fn add_tree( + &mut self, + file_name: &str, + lang_type: LangType, + file_content: &str, ) -> Option<()> { - let rope = document_map.get(name)?; - let content = rope.value(); - let mut writter = FileWriter::default(); - let _ = content.write_to(&mut writter); - let content = writter.content; - self.queries.lock().ok().and_then(|query| -> Option<()> { - let trees = self.trees.get(&LangType::Template)?; - let tree = trees.get(name)?; - let closest_node = tree.root_node(); - search_errors( - closest_node, - &content, - &query, - &self.variables, - &name.to_string(), - diags, - ) - }); + let trees = self.trees.get_mut(&lang_type)?; + let old_tree = trees.get_mut(&file_name.to_string()); + match old_tree { + Some(old_tree) => { + let new_tree = self + .parsers + .parse(lang_type, file_content, Some(old_tree))?; + trees.insert(file_name.to_string(), new_tree); + } + None => { + // tree doesn't exist, first insertion + let new_tree = self.parsers.parse(lang_type, file_content, None)?; + trees.insert(file_name.to_string(), new_tree); + } + }; None } - pub fn get_trees_vec(&self, lang_type: LangType) -> Vec { - let trees = self.trees.get(&lang_type).unwrap(); - let mut all = vec![]; - for tree in trees.iter() { - all.push(tree.key().to_string()); - } - all - } - - fn add_tree(&self, file_name: &str, lang_type: LangType, file_content: &str) { - self.parsers - .lock() - .ok() - .and_then(|mut parsers| -> Option<()> { - let trees = self.trees.get_mut(&lang_type).unwrap(); - if let Some(old_tree) = trees.get_mut(&file_name.to_string()) { - if let Some(tree) = parsers.parse(lang_type, file_content, Some(&old_tree)) { - drop(old_tree); - trees.insert(file_name.to_string(), tree); - } - } else { - // tree doesn't exist, first insertion - if let Some(tree) = parsers.parse(lang_type, file_content, None) { - trees.insert(file_name.to_string(), tree); - } - } - None - }); - } - - fn delete_variables(&self, name: &str) { - self.variables.remove(name); + fn delete_variables(&mut self, name: &str) -> Option<()> { + self.variables.get_mut(name)?.clear(); + Some(()) } - fn add_variables( - &self, - name: &str, - lang_type: LangType, - file_content: &str, - query: &std::sync::MutexGuard<'_, Queries>, - ) -> Option> { + fn add_variables(&mut self, name: &str, lang_type: LangType, file_content: &str) -> Option<()> { let trees = self.trees.get(&lang_type).unwrap(); let tree = trees.get(name)?; let trigger_point = Point::new(0, 0); let closest_node = tree.root_node(); - let diags = vec![]; match lang_type { LangType::Backend => { - let query = &query.rust_idents; + let query = &self.queries.rust_idents; let capturer = RustCapturer::default(); let mut variables = vec![]; let capturer = query_props( @@ -170,11 +96,27 @@ impl LspFiles { true, capturer, ); - add_variable_from_rust(capturer, &mut variables); + + for variable in capturer.variables() { + variables.push(JinjaVariable::new( + &variable.0, + variable.1, + DataType::BackendVariable, + )); + } + for macros in capturer.macros() { + for variable in macros.1.variables() { + variables.push(JinjaVariable::new( + variable.0, + *variable.1, + DataType::BackendVariable, + )); + } + } self.variables.insert(name.to_string(), variables); } LangType::Template => { - let query = &query.jinja_init; + let query = &self.queries.jinja_init; let capturer = JinjaInitCapturer::default(); let capturer = query_props( closest_node, @@ -187,106 +129,166 @@ impl LspFiles { self.variables.insert(name.to_string(), capturer.to_vec()); } } - Some(diags) + Some(()) } pub fn input_edit( - &self, + &mut self, file: &String, code: String, input_edit: InputEdit, lang_type: Option, ) -> Option<()> { let lang_type = lang_type?; - self.parsers.lock().ok().and_then(|mut parsers| { - let trees = self.trees.get_mut(&lang_type)?; - let mut old_tree = trees.get_mut(file)?; - old_tree.edit(&input_edit); - let new_tree = parsers.parse(lang_type, &code, Some(&old_tree))?; - drop(old_tree); - drop(trees); - let trees = self.trees.get_mut(&lang_type)?; - trees.insert(file.to_string(), new_tree); - None - }) + let trees = self.trees.get_mut(&lang_type)?; + let old_tree = trees.get_mut(file)?; + old_tree.edit(&input_edit); + let new_tree = self.parsers.parse(lang_type, &code, Some(old_tree))?; + let trees = self.trees.get_mut(&lang_type)?; + trees.insert(file.to_string(), new_tree); + None } - pub fn saved( - &self, - uri: &String, - config: &RwLock, - document_map: &DashMap, - diagnostics: &mut HashMap>, + pub fn read_tree(&self, name: &str) -> Option> { + let rope = self.documents.get(name)?; + let mut writter = FileWriter::default(); + let _ = rope.write_to(&mut writter); + let content = writter.content; + let trees = self.trees.get(&LangType::Template)?; + let tree = trees.get(name)?; + let closest_node = tree.root_node(); + search_errors( + closest_node, + &content, + &self.queries, + &self.variables, + &name.to_string(), + ) + } + + pub fn did_change( + &mut self, + params: DidChangeTextDocumentParams, + config: &JinjaConfig, ) -> Option<()> { - let path = Path::new(&uri); - let res = None; - if let Ok(config) = config.read() { - let lang_type = config.file_ext(&path)?; - let doc = document_map.get(uri)?; - let content = doc.value(); - let mut contents = FileWriter::default(); - let _ = content.write_to(&mut contents); - let content = contents.content; - let _ = self.queries.lock().is_ok_and(|queries| { - self.delete_variables(uri); - let errors = self.add_variables(uri, lang_type, &content, &queries); - if let Some(errors) = errors { - diagnostics.insert(uri.to_string(), errors); - } - true - }); - if lang_type == LangType::Template { - self.read_tree(document_map, diagnostics, uri); + let uri = params.text_document.uri.to_string(); + let rope = self.documents.get_mut(&uri)?; + let lang_type = config.file_ext(&Path::new(&uri)); + let mut changes = vec![]; + for change in params.content_changes { + let range = &change.range?; + let input_edit = range.to_input_edit(rope); + if change.text.is_empty() { + let (start, end) = range.to_byte(rope); + rope.remove(start..end); + } else { + let (start, _) = range.to_byte(rope); + rope.insert(start, &change.text); } + let mut w = FileWriter::default(); + let _ = rope.write_to(&mut w); + changes.push((w.content, input_edit)); } - res + for change in changes { + self.input_edit(&uri, change.0, change.1, lang_type); + } + None + } + + pub fn did_save( + &mut self, + params: DidSaveTextDocumentParams, + config: &JinjaConfig, + ) -> Option { + let uri = params.text_document.uri.as_str(); + let path = Path::new(&uri); + let lang_type = config.file_ext(&path)?; + let doc = self.documents.get(uri)?; + let mut contents = FileWriter::default(); + let _ = doc.write_to(&mut contents); + let content = contents.content; + self.delete_variables(uri); + self.add_variables(uri, lang_type, &content); + let mut hm = HashMap::new(); + let v = self.read_tree(uri); + if let Some(v) = v { + hm.insert(uri.to_owned(), v); + } else { + hm.insert(uri.to_owned(), vec![]); + } + let message = DiagnosticMessage::Errors { + diagnostics: hm, + current_file: Some(uri.to_owned()), + }; + Some(message) } pub fn completion( &self, params: CompletionParams, - config: &RwLock, - document_map: &DashMap, + config: &JinjaConfig, + can_complete2: bool, ) -> Option { + let can_complete = { + matches!( + params.context, + Some(CompletionContext { + trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, + .. + }) | Some(CompletionContext { + trigger_kind: CompletionTriggerKind::INVOKED, + .. + }) + ) + }; + + if !can_complete { + let can_complete = can_complete2; + if !can_complete { + return None; + } + } + let uri = params.text_document_position.text_document.uri.to_string(); let row = params.text_document_position.position.line; let column = params.text_document_position.position.character; let point = Point::new(row as usize, column as usize); - let can_complete = config - .read() - .ok() - .and_then(|config| config.file_ext(&Path::new(&uri))) - .map_or(false, |lang_type| lang_type == LangType::Template); - if !can_complete { - None - } else { - let trees = self.trees.get(&LangType::Template)?; - let tree = trees.get(&uri)?; - let closest_node = tree.root_node(); - self.queries.lock().ok().and_then(|queries| { - let query = &queries.jinja_idents; - let capturer = JinjaObjectCapturer::default(); - let doc = document_map.get(&uri)?; - let mut writter = FileWriter::default(); - let _ = doc.write_to(&mut writter); - let props = query_props( - closest_node, - &writter.content, - point, - query, - false, - capturer, - ); - props.completion(point) - }) + let ext = config.file_ext(&Path::new(&uri))?; + if ext != LangType::Template { + return None; } + let trees = self.trees.get(&LangType::Template)?; + let tree = trees.get(&uri)?; + let closest_node = tree.root_node(); + let query = &self.queries.jinja_idents; + let capturer = JinjaObjectCapturer::default(); + let doc = self.documents.get(&uri)?; + let mut writter = FileWriter::default(); + let _ = doc.write_to(&mut writter); + let props = query_props( + closest_node, + &writter.content, + point, + query, + false, + capturer, + ); + // TODO check for include capturer + props.completion(point) } - pub fn hover( - &self, - params: HoverParams, - document_map: &DashMap, - ) -> Option { + pub fn hover(&self, params: HoverParams, config: &JinjaConfig) -> Option { + let uri = ¶ms + .text_document_position_params + .text_document + .uri + .clone(); + let lang_type = config.file_ext(&Path::new(uri.as_str())); + let can_hover = lang_type.map_or(false, |lang_type| lang_type == LangType::Template); + if !can_hover { + return None; + } + let uri = params .text_document_position_params .text_document @@ -298,36 +300,30 @@ impl LspFiles { let trees = self.trees.get(&LangType::Template)?; let tree = trees.get(&uri)?; let closest_node = tree.root_node(); - - let hover = self.queries.lock().ok().and_then(|queries| { - let query = &queries.jinja_idents; - let capturer = JinjaObjectCapturer::default(); - let doc = document_map.get(&uri)?; - let mut writter = FileWriter::default(); - let _ = doc.write_to(&mut writter); - let props = query_props( - closest_node, - &writter.content, - point, - query, - false, - capturer, - ); - if props.is_hover(point) { - let id = props.get_last_id()?; - return Some(id); - } - None - }); - - hover + let query = &self.queries.jinja_idents; + let capturer = JinjaObjectCapturer::default(); + let doc = self.documents.get(&uri)?; + let mut writter = FileWriter::default(); + let _ = doc.write_to(&mut writter); + let props = query_props( + closest_node, + &writter.content, + point, + query, + false, + capturer, + ); + if props.is_hover(point) { + let id = props.get_last_id()?; + return Some(id); + } + None } pub fn goto_definition( &self, params: GotoDefinitionParams, - document_map: &DashMap, - config: &RwLock, + config: &JinjaConfig, ) -> Option { let uri = params .text_document_position_params @@ -347,81 +343,61 @@ impl LspFiles { let closest_node = tree.root_node(); let mut current_ident = String::new(); - let mut res = self - .queries - .lock() - .ok() - .and_then(|queries| { - let query = &queries.jinja_idents; - let capturer = JinjaObjectCapturer::default(); - let doc = document_map.get(&uri)?; - let mut writter = FileWriter::default(); - let _ = doc.write_to(&mut writter); - let props = query_props( - closest_node, - &writter.content, - point, - query, - false, - capturer, - ); - props.is_ident(point) - }) - .and_then(|ident| { - current_ident = ident.to_string(); - let variables = self.variables.get(&uri)?; - let max = variables - .value() - .iter() - .filter(|item| item.name == ident && item.location.0 <= point) - .max()?; - let (start, end) = to_position(max); - let range = Range::new(start, end); - Some(GotoDefinitionResponse::Scalar(Location { - uri: uri2.clone(), - range, - })) - }); - + let query = &self.queries.jinja_idents; + let capturer = JinjaObjectCapturer::default(); + let doc = self.documents.get(&uri)?; + let mut writter = FileWriter::default(); + let _ = doc.write_to(&mut writter); + let props = query_props( + closest_node, + &writter.content, + point, + query, + false, + capturer, + ); + let mut res = props.is_ident(point).and_then(|ident| { + current_ident = ident.to_string(); + let variables = self.variables.get(&uri)?; + let max = variables + .iter() + .filter(|item| item.name == ident && item.location.0 <= point) + .max()?; + let (start, end) = to_position(max); + let range = Range::new(start, end); + Some(GotoDefinitionResponse::Scalar(Location { + uri: uri2.clone(), + range, + })) + }); res.is_none().then(|| -> Option<()> { - if current_ident.is_empty() { - self.queries.lock().ok().and_then(|queries| { - let query = &queries.jinja_imports; - let capturer = IncludeCapturer::default(); - let doc = document_map.get(&uri)?; - let mut writter = FileWriter::default(); - let _ = doc.write_to(&mut writter); - let props = query_props( - closest_node, - &writter.content, - point, - query, - false, - capturer, - ); - if props.in_template(point) { - if let Ok(config) = config.read() { - let template = format!("{}/{}", config.templates, &props.template); - let template = std::fs::canonicalize(template).ok()?; - let url = format!("file://{}", template.to_str()?); - let uri = Url::parse(&url).ok()?; - let start = to_position2(Point::new(0, 0)); - let end = to_position2(Point::new(0, 0)); - let range = Range::new(start, end); - let location = Location { uri, range }; - res = Some(GotoDefinitionResponse::Scalar(location)); - return Some(template); - } - } - None - }); + let query = &self.queries.jinja_imports; + let capturer = IncludeCapturer::default(); + let doc = self.documents.get(&uri)?; + let mut writter = FileWriter::default(); + let _ = doc.write_to(&mut writter); + let props = query_props( + closest_node, + &writter.content, + point, + query, + false, + capturer, + ); + if let Some(last) = props.in_template(point) { + let uri = last.is_template(&config.templates)?; + let start = to_position2(Point::new(0, 0)); + let end = to_position2(Point::new(0, 0)); + let range = Range::new(start, end); + let location = Location { uri, range }; + res = Some(GotoDefinitionResponse::Scalar(location)); None } else { let mut all: Vec = vec![]; for i in &self.variables { - let idents = i.value().iter().filter(|item| item.name == current_ident); + let idents = i.1.iter().filter(|item| item.name == current_ident); for id in idents { - let uri = Url::parse(i.key()).unwrap(); + let uri = Url::parse(i.0).unwrap(); let (start, end) = to_position(id); let range = Range::new(start, end); let location = Location { uri, range }; @@ -432,16 +408,20 @@ impl LspFiles { None } }); - res } pub fn code_action( &self, action_params: CodeActionParams, - document_map: &DashMap, + config: &JinjaConfig, ) -> Option { let uri = action_params.text_document.uri.to_string(); + let lang_type = config.file_ext(&Path::new(&uri)); + let can_def = lang_type.map_or(false, |lang_type| lang_type == LangType::Template); + if !can_def { + return None; + } let row = action_params.range.start.line; let column = action_params.range.start.character; let point = Point::new(row as usize, column as usize); @@ -449,31 +429,32 @@ impl LspFiles { let tree = trees.get(&uri)?; let closest_node = tree.root_node(); let _current_ident = String::new(); - self.queries.lock().ok().and_then(|queries| { - let query = &queries.jinja_idents; - let capturer = JinjaObjectCapturer::default(); - let doc = document_map.get(&uri)?; - let mut writter = FileWriter::default(); - let _ = doc.write_to(&mut writter); - let props = query_props( - closest_node, - &writter.content, - point, - query, - false, - capturer, - ); - Some(props.in_expr(point)) - }) + let query = &self.queries.jinja_idents; + let capturer = JinjaObjectCapturer::default(); + let doc = self.documents.get(&uri)?; + let mut writter = FileWriter::default(); + let _ = doc.write_to(&mut writter); + let props = query_props( + closest_node, + &writter.content, + point, + query, + false, + capturer, + ); + Some(props.in_expr(point)) } - pub fn get_variables( - &self, - uri: &Url, - _document_map: &DashMap, - _lang_type: LangType, - position: Position, - ) -> Option> { + 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 { + diags.insert(String::from(tree.0), errors); + } + } + } + + pub fn read_variables(&self, uri: &Url, position: Position) -> Option> { let start = position.line as usize; let end = position.character as usize; let position = Point::new(start, end); @@ -491,9 +472,8 @@ impl LspFiles { ..Default::default() }); } - drop(variables); for file in self.variables.iter() { - for variable in file.value() { + for variable in file.1 { if variable.data_type == DataType::BackendVariable { items.push(CompletionItem { label: variable.name.to_string(), @@ -509,23 +489,42 @@ impl LspFiles { } None } -} -pub fn add_variable_from_rust(rust: RustCapturer, variables: &mut Vec) { - for variable in rust.variables() { - variables.push(JinjaVariable::new( - &variable.0, - variable.1, - DataType::BackendVariable, - )); + pub fn did_open( + &mut self, + params: DidOpenTextDocumentParams, + config: &JinjaConfig, + ) -> Option { + let name = params.text_document.uri.as_str(); + let lang_type = config.file_ext(&Path::new(name))?; + let file_content = params.text_document.text; + let rope = Rope::from_str(&file_content); + self.delete_variables(name); + self.documents.insert(name.to_string(), rope); + self.add_tree(name, lang_type, &file_content); + self.add_variables(name, lang_type, &file_content); + let diagnostics = self.read_tree(name)?; + let mut hm = HashMap::new(); + hm.insert(name.to_owned(), diagnostics); + let msg = DiagnosticMessage::Errors { + diagnostics: hm, + current_file: Some(name.to_owned()), + }; + Some(msg) } - for macros in rust.macros() { - for variable in macros.1.variables() { - variables.push(JinjaVariable::new( - variable.0, - *variable.1, - DataType::BackendVariable, - )); +} + +impl Default for LspFiles { + fn default() -> Self { + let mut trees = HashMap::new(); + trees.insert(LangType::Template, HashMap::new()); + trees.insert(LangType::Backend, HashMap::new()); + Self { + trees, + parsers: Parsers::default(), + variables: HashMap::new(), + queries: Queries::default(), + documents: HashMap::new(), } } } diff --git a/jinja-lsp/src/main.rs b/jinja-lsp/src/main.rs index 107364a..1f6209f 100644 --- a/jinja-lsp/src/main.rs +++ b/jinja-lsp/src/main.rs @@ -1,7 +1,8 @@ mod backend; +pub mod channels; mod config; mod filter; -mod lsp_files; +pub mod lsp_files; use backend::Backend; use tower_lsp::LspService;