diff options
author | Norbert Preining <norbert@preining.info> | 2021-05-23 03:00:39 +0000 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2021-05-23 03:00:39 +0000 |
commit | f1261b349e875b842745b63258c3e338cb1fe3bf (patch) | |
tree | b5d402b3e80818cde2c079a42249f3dcb9732247 /support/texlab/src | |
parent | 58aa1ac09b1d9e4769d0a0661cf12e2b2db41b14 (diff) |
CTAN sync 202105230300
Diffstat (limited to 'support/texlab/src')
348 files changed, 19694 insertions, 19326 deletions
diff --git a/support/texlab/src/build.rs b/support/texlab/src/build.rs deleted file mode 100644 index fe4597c0d7..0000000000 --- a/support/texlab/src/build.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - BuildParams, BuildResult, BuildStatus, ClientCapabilitiesExt, LatexOptions, - LogMessageParams, LspClient, MessageType, ProgressParams, ProgressParamsValue, - ProgressToken, Uri, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressCreateParams, - WorkDoneProgressEnd, - }, -}; -use async_trait::async_trait; -use chashmap::CHashMap; -use futures::{ - future::{AbortHandle, Abortable, Aborted}, - lock::Mutex, - prelude::*, - stream, -}; -use log::error; -use std::{collections::HashMap, io, path::Path, process::Stdio, sync::Arc}; -use tokio::{ - io::{AsyncBufReadExt, BufReader}, - process::Command, -}; -use uuid::Uuid; - -pub struct BuildProvider<C> { - client: Arc<C>, - handles_by_token: Mutex<HashMap<ProgressToken, AbortHandle>>, - current_docs: CHashMap<Uri, ()>, -} - -impl<C> BuildProvider<C> { - pub fn new(client: Arc<C>) -> Self { - Self { - client, - handles_by_token: Mutex::new(HashMap::new()), - current_docs: CHashMap::new(), - } - } - - pub fn is_building(&self) -> bool { - self.current_docs.len() > 0 - } - - pub async fn cancel(&self, token: ProgressToken) { - let handles_by_token = self.handles_by_token.lock().await; - if let Some(handle) = handles_by_token.get(&token) { - handle.abort(); - } else if let ProgressToken::String(id) = token { - if id == "texlab-build-*" { - handles_by_token.values().for_each(|handle| handle.abort()); - } - } - } -} - -#[async_trait] -impl<C> FeatureProvider for BuildProvider<C> -where - C: LspClient + Send + Sync + 'static, -{ - type Params = BuildParams; - type Output = BuildResult; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<BuildParams>) -> BuildResult { - let token = ProgressToken::String(format!("texlab-build-{}", Uuid::new_v4())); - let (handle, reg) = AbortHandle::new_pair(); - { - let mut handles_by_token = self.handles_by_token.lock().await; - handles_by_token.insert(token.clone(), handle); - } - - let doc = req - .snapshot() - .parent(&req.current().uri, &req.options, &req.current_dir) - .unwrap_or_else(|| Arc::clone(&req.view.current)); - - if !doc.is_file() { - error!("Unable to build the document {}: wrong URI scheme", doc.uri); - return BuildResult { - status: BuildStatus::Failure, - }; - } - - if self.current_docs.get(&doc.uri).is_some() { - return BuildResult { - status: BuildStatus::Success, - }; - } - self.current_docs.insert(doc.uri.clone(), ()); - - let status = match doc.uri.to_file_path() { - Ok(path) => { - if req.client_capabilities.has_work_done_progress_support() { - let params = WorkDoneProgressCreateParams { - token: token.clone(), - }; - self.client.work_done_progress_create(params).await.unwrap(); - - let title = path.file_name().unwrap().to_string_lossy().into_owned(); - let params = ProgressParams { - token: token.clone(), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin( - WorkDoneProgressBegin { - title, - cancellable: Some(true), - message: Some("Building".into()), - percentage: None, - }, - )), - }; - self.client.progress(params).await; - } - - let latex_options = req.options.latex.clone().unwrap_or_default(); - let client = Arc::clone(&self.client); - match Abortable::new(build(&path, &latex_options, client), reg).await { - Ok(Ok(true)) => BuildStatus::Success, - Ok(Ok(false)) => BuildStatus::Error, - Ok(Err(why)) => { - error!("Unable to build the document {}: {}", doc.uri, why); - BuildStatus::Failure - } - Err(Aborted) => BuildStatus::Cancelled, - } - } - Err(()) => { - error!("Unable to build the document {}: invalid URI", doc.uri); - BuildStatus::Failure - } - }; - - if req.client_capabilities.has_work_done_progress_support() { - let params = ProgressParams { - token: token.clone(), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd { - message: None, - })), - }; - self.client.progress(params).await; - } - { - let mut handles_by_token = self.handles_by_token.lock().await; - handles_by_token.remove(&token); - } - - self.current_docs.remove(&doc.uri); - BuildResult { status } - } -} - -async fn build<C>(path: &Path, options: &LatexOptions, client: Arc<C>) -> io::Result<bool> -where - C: LspClient + Send + Sync + 'static, -{ - let build_options = options.build.as_ref().cloned().unwrap_or_default(); - let build_dir = options - .root_directory - .as_ref() - .map(AsRef::as_ref) - .or_else(|| path.parent()) - .unwrap(); - - let args: Vec<_> = build_options - .args() - .into_iter() - .map(|arg| replace_placeholder(arg, path)) - .collect(); - - let mut process = Command::new(build_options.executable()) - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .current_dir(build_dir) - .kill_on_drop(true) - .spawn()?; - - let stdout = BufReader::new(process.stdout.take().unwrap()).lines(); - let stderr = BufReader::new(process.stderr.take().unwrap()).lines(); - let mut output = stream::select(stdout, stderr); - - tokio::spawn(async move { - while let Some(Ok(line)) = output.next().await { - let params = LogMessageParams { - typ: MessageType::Log, - message: line, - }; - - client.log_message(params).await; - } - }); - - Ok(process.await?.success()) -} - -fn replace_placeholder(arg: String, file: &Path) -> String { - if arg.starts_with('"') || arg.ends_with('"') { - arg - } else { - arg.replace("%f", &file.to_string_lossy()) - } -} diff --git a/support/texlab/src/protocol/capabilities.rs b/support/texlab/src/capabilities.rs index c83b205aa8..e20a66b1ba 100644 --- a/support/texlab/src/protocol/capabilities.rs +++ b/support/texlab/src/capabilities.rs @@ -60,10 +60,13 @@ impl ClientCapabilitiesExt for ClientCapabilities { #[cfg(test)] mod tests { use super::*; - use lsp_types::*; + use lsp_types::{ + DocumentSymbolClientCapabilities, GotoCapability, HoverClientCapabilities, + TextDocumentClientCapabilities, WindowClientCapabilities, + }; #[test] - fn has_definition_link_support_true() { + fn test_has_definition_link_support_true() { let capabilities = ClientCapabilities { text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { @@ -78,18 +81,18 @@ mod tests { } #[test] - fn has_definition_link_support_false() { + fn test_has_definition_link_support_false() { let capabilities = ClientCapabilities::default(); assert!(!capabilities.has_definition_link_support()); } #[test] - fn has_hierarchical_document_symbol_support_true() { + fn test_has_hierarchical_document_symbol_support_true() { let capabilities = ClientCapabilities { text_document: Some(TextDocumentClientCapabilities { - document_symbol: Some(DocumentSymbolCapability { + document_symbol: Some(DocumentSymbolClientCapabilities { hierarchical_document_symbol_support: Some(true), - ..DocumentSymbolCapability::default() + ..DocumentSymbolClientCapabilities::default() }), ..TextDocumentClientCapabilities::default() }), @@ -99,13 +102,13 @@ mod tests { } #[test] - fn has_hierarchical_document_symbol_support_false() { + fn test_has_hierarchical_document_symbol_support_false() { let capabilities = ClientCapabilities::default(); assert!(!capabilities.has_hierarchical_document_symbol_support()); } #[test] - fn has_work_done_progress_support_true() { + fn test_has_work_done_progress_support_true() { let capabilities = ClientCapabilities { window: Some(WindowClientCapabilities { work_done_progress: Some(true), @@ -117,18 +120,18 @@ mod tests { } #[test] - fn has_work_done_progress_support_false() { + fn test_has_work_done_progress_support_false() { let capabilities = ClientCapabilities::default(); assert!(!capabilities.has_work_done_progress_support()); } #[test] - fn has_hover_markdown_support_true() { + fn test_has_hover_markdown_support_true() { let capabilities = ClientCapabilities { text_document: Some(TextDocumentClientCapabilities { - hover: Some(HoverCapability { + hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::PlainText, MarkupKind::Markdown]), - ..HoverCapability::default() + ..HoverClientCapabilities::default() }), ..TextDocumentClientCapabilities::default() }), @@ -138,7 +141,7 @@ mod tests { } #[test] - fn has_hover_markdown_support_false() { + fn test_has_hover_markdown_support_false() { let capabilities = ClientCapabilities::default(); assert!(!capabilities.has_hover_markdown_support()); } diff --git a/support/texlab/src/citeproc/mod.rs b/support/texlab/src/citation.rs index 6def2ed8a4..e185c14b55 100644 --- a/support/texlab/src/citeproc/mod.rs +++ b/support/texlab/src/citation.rs @@ -2,28 +2,30 @@ mod bibutils; mod name; mod ris; -use self::{ - bibutils::{InputFormat, OutputFormat}, - ris::{RisLibrary, RisReference}, -}; -use crate::{ - protocol::{BibtexFormattingOptions, MarkupContent, MarkupKind}, - syntax::bibtex, -}; -use citeproc::prelude::*; +use std::sync::Arc; + +use citeproc::{prelude::SupportedFormat, ClusterPosition, Processor}; use citeproc_db::PredefinedLocales; +use citeproc_io::{Cite, Cluster, Reference}; +use lsp_types::{MarkupContent, MarkupKind}; use once_cell::sync::Lazy; use regex::Regex; -use std::sync::Arc; -static APA_STYLE: &str = include_str!("apa.csl"); +use crate::syntax::{bibtex, CstNode}; + +use self::{ + bibutils::*, + ris::{RisLibrary, RisReference}, +}; + +static APA_STYLE: &str = include_str!("citation/apa.csl"); static DOI_URL_PATTERN: &str = r#"https://doi.org/\[.*\]\(.*\)"#; static DOI_URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(DOI_URL_PATTERN).unwrap()); -pub fn render_citation(tree: &bibtex::Tree, key: &str) -> Option<MarkupContent> { - let ris_reference = convert_to_ris(tree, key)?; +pub fn render_citation(root: &bibtex::SyntaxNode, key: &str) -> Option<MarkupContent> { + let ris_reference = convert_to_ris(root, key)?; let doi_url = get_doi_url_markdown(&ris_reference); let csl_reference: Reference = ris_reference.into(); let html = generate_bibliography(csl_reference)?; @@ -52,34 +54,22 @@ pub fn render_citation(tree: &bibtex::Tree, key: &str) -> Option<MarkupContent> Some(content) } -fn convert_to_ris(tree: &bibtex::Tree, key: &str) -> Option<RisReference> { - let options = BibtexFormattingOptions { - line_length: None, - formatter: None, - }; - let params = bibtex::FormattingParams { - insert_spaces: true, - tab_size: 4, - options: &options, - }; - +fn convert_to_ris(root: &bibtex::SyntaxNode, key: &str) -> Option<RisReference> { let mut bib_code = String::new(); - tree.children(tree.root) - .filter(|node| tree.as_string(*node).is_some()) - .map(|node| bibtex::format(tree, node, params)) - .for_each(|string| { - bib_code.push_str(&string); - bib_code.push('\n'); - }); - - let entry = tree.entry_by_key(key)?; - if let Some(crossref) = tree.crossref(entry) { - bib_code.push_str(&bibtex::format(tree, crossref, params)); - bib_code.push('\n'); + for string in root + .children() + .filter_map(bibtex::String::cast) + .filter(|string| string.name().is_some()) + { + bib_code.push_str(&string.syntax().to_string()); } - bib_code.push_str(&bibtex::format(tree, entry, params)); - bib_code.push('\n'); + let entry = root + .children() + .filter_map(bibtex::Entry::cast) + .find(|entry| entry.key().map(|key| key.text()) == Some(key))?; + bib_code.push_str(&entry.syntax().to_string()); + bib_code = bib_code.replace("\\hypen", "-"); let ris_code = bibutils::convert(&bib_code, InputFormat::Biblatex, OutputFormat::Ris)?; @@ -119,21 +109,20 @@ fn generate_bibliography(reference: Reference) -> Option<String> { #[cfg(test)] mod tests { use super::*; - use indoc::indoc; #[test] - fn simple() { - let tree = bibtex::open(indoc!( + fn test_simple() { + let document = bibtex::parse( r#" - @article{foo, - author = {Foo Bar}, + @article{foo, + author = {Foo Bar}, title = {Baz Qux}, year = {2020} } - "# - )); + "#, + ); - let actual_md = render_citation(&tree, "foo").unwrap(); + let actual_md = render_citation(&document.root, "foo").unwrap(); let expected_md = MarkupContent { kind: MarkupKind::Markdown, @@ -144,55 +133,18 @@ mod tests { } #[test] - fn crossref() { - let tree = bibtex::open(indoc!( - r#" - https://tex.stackexchange.com/questions/401138/what-is-the-bibtex-crossref-field-used-for - - @inproceedings{duck2015, - author = {Duck, D.}, - title = {Duck tales}, - crossref = {ICRC2015}, - } - - @inproceedings{mouse2015, - author = {Mouse, M.}, - title = {Mouse stories}, - crossref = {ICRC2015}, - } - - @proceedings{ICRC2015, - title = "{Proceedings of the 34\textsuperscript{th} International Cosmic Ray Conference}", - year = "2015", - month = aug, - } - "# - )); - - let actual_md = render_citation(&tree, "mouse2015").unwrap(); - - let expected_md = MarkupContent { - kind: MarkupKind::Markdown, - value: "Mouse, M. (2015). Mouse stories. In *Proceedings of the 34th International Cosmic Ray Conference*.".into(), - }; - - assert_eq!(actual_md, expected_md); - } - - #[test] - fn string() { - let tree = bibtex::open(indoc!( + fn test_string() { + let document = bibtex::parse( r#" @string{author = "Foo Bar"} - @article{foo, - author = author, + @article{foo, + author = author, title = {Baz Qux}, year = {2020} } - "# - )); - - let actual_md = render_citation(&tree, "foo").unwrap(); + "#, + ); + let actual_md = render_citation(&document.root, "foo").unwrap(); let expected_md = MarkupContent { kind: MarkupKind::Markdown, @@ -203,10 +155,10 @@ mod tests { } #[test] - fn unknown_key() { - let tree = bibtex::open(""); + fn test_unknown_key() { + let document = bibtex::parse(""); - let actual_md = render_citation(&tree, "foo"); + let actual_md = render_citation(&document.root, "foo"); assert_eq!(actual_md, None); } diff --git a/support/texlab/src/citeproc/apa.csl b/support/texlab/src/citation/apa.csl index 8454246a98..8454246a98 100644 --- a/support/texlab/src/citeproc/apa.csl +++ b/support/texlab/src/citation/apa.csl diff --git a/support/texlab/src/citeproc/bibutils.rs b/support/texlab/src/citation/bibutils.rs index b69ed66ed1..9931ba3f9f 100644 --- a/support/texlab/src/citeproc/bibutils.rs +++ b/support/texlab/src/citation/bibutils.rs @@ -1,3 +1,5 @@ +use std::{ffi::CString, fs, mem::MaybeUninit, path::Path}; + use bibutils_sys::{ bibl, bibl_free, bibl_freeparams, bibl_init, bibl_initparams, bibl_read, bibl_write, fclose, fopen, param, BIBL_ADSABSOUT, BIBL_BIBLATEXIN, BIBL_BIBTEXIN, BIBL_BIBTEXOUT, BIBL_COPACIN, @@ -5,10 +7,10 @@ use bibutils_sys::{ BIBL_MODSIN, BIBL_MODSOUT, BIBL_NBIBIN, BIBL_NBIBOUT, BIBL_OK, BIBL_RISIN, BIBL_RISOUT, BIBL_WORD2007OUT, BIBL_WORDIN, FILE, }; -use std::{ffi::CString, fs, mem::MaybeUninit, path::Path}; use tempfile::tempdir; #[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[allow(unused)] pub enum InputFormat { Bibtex, Biblatex, @@ -42,6 +44,7 @@ impl InputFormat { } #[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[allow(unused)] pub enum OutputFormat { Adsabs, Bibtex, @@ -183,6 +186,10 @@ pub fn convert(input: &str, from: InputFormat, to: OutputFormat) -> Option<Strin // Remove BOM let data = fs::read(&output_path).ok()?; + if data.is_empty() { + return None; + } + let text = String::from_utf8_lossy(&data[3..]).into_owned(); Some(text) } diff --git a/support/texlab/src/citeproc/name/mod.rs b/support/texlab/src/citation/name.rs index ef97d30130..9e22383a7d 100644 --- a/support/texlab/src/citeproc/name/mod.rs +++ b/support/texlab/src/citation/name.rs @@ -1,14 +1,15 @@ // Ported from: https://github.com/michel-kraemer/citeproc-java/blob/master/citeproc-java/grammars/InternalName.g4 // Michel Kraemer // Apache License 2.0 +use citeproc_io::Name; + +use parser::NamesParser; + mod parser { #![allow(warnings)] - include!(concat!(env!("OUT_DIR"), "/citeproc/name/parser.rs")); + include!(concat!(env!("OUT_DIR"), "/citation/name/parser.rs")); } -use self::parser::NamesParser; -use citeproc_io::Name; - pub fn parse(input: &str) -> Vec<Name> { let parser = NamesParser::new(); parser.parse(input).unwrap_or_else(|_| { @@ -24,7 +25,7 @@ mod tests { use citeproc_io::PersonName; #[test] - fn family_only() { + fn test_family_only() { let name = Name::Person(PersonName { family: Some("Thompson".into()), given: None, @@ -36,7 +37,7 @@ mod tests { } #[test] - fn simple() { + fn test_simple() { let name = Name::Person(PersonName { family: Some("Thompson".into()), given: Some("Ken".into()), @@ -48,7 +49,7 @@ mod tests { } #[test] - fn middle_name() { + fn test_middle_name() { let name = Name::Person(PersonName { family: Some("Ritchie".into()), given: Some("Dennis M.".into()), @@ -60,7 +61,7 @@ mod tests { } #[test] - fn initials() { + fn test_initials() { let name = Name::Person(PersonName { family: Some("Johnson".into()), given: Some("S. C.".into()), @@ -72,7 +73,7 @@ mod tests { } #[test] - fn non_dropping_particle() { + fn test_non_dropping_particle() { let name = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -84,7 +85,7 @@ mod tests { } #[test] - fn non_dropping_particle_family_only() { + fn test_non_dropping_particle_family_only() { let name = Name::Person(PersonName { family: Some("Gerwen".into()), given: None, @@ -96,7 +97,7 @@ mod tests { } #[test] - fn comma() { + fn test_comma() { let name = Name::Person(PersonName { family: Some("Thompson".into()), given: Some("Ken".into()), @@ -108,7 +109,7 @@ mod tests { } #[test] - fn comma_junior() { + fn test_comma_junior() { let name = Name::Person(PersonName { family: Some("Friedman".into()), given: Some("George".into()), @@ -120,7 +121,7 @@ mod tests { } #[test] - fn comma_no_junior() { + fn test_comma_no_junior() { let name = Name::Person(PersonName { family: Some("Familya Familyb".into()), given: Some("Given".into()), @@ -132,7 +133,7 @@ mod tests { } #[test] - fn comma_initials() { + fn test_comma_initials() { let name = Name::Person(PersonName { family: Some("Ritchie".into()), given: Some("Dennis M.".into()), @@ -144,7 +145,7 @@ mod tests { } #[test] - fn comma_non_dropping_particle() { + fn test_comma_non_dropping_particle() { let name = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -156,7 +157,7 @@ mod tests { } #[test] - fn comma_non_dropping_particles() { + fn test_comma_non_dropping_particles() { let name = Name::Person(PersonName { family: Some("Voort".into()), given: Some("Vincent".into()), @@ -168,7 +169,7 @@ mod tests { } #[test] - fn and() { + fn test_and() { let name1 = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -190,7 +191,7 @@ mod tests { } #[test] - fn and_comma1() { + fn test_and_comma1() { let name1 = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -212,7 +213,7 @@ mod tests { } #[test] - fn and_comma2() { + fn test_and_comma2() { let name1 = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -234,7 +235,7 @@ mod tests { } #[test] - fn and_comma_mix() { + fn test_and_comma_mix() { let name1 = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -256,7 +257,7 @@ mod tests { } #[test] - fn junior() { + fn test_junior() { let name = Name::Person(PersonName { family: Some("Friedman".into()), given: Some("George".into()), @@ -268,7 +269,7 @@ mod tests { } #[test] - fn non_parseable() { + fn test_non_parseable() { let literal = "Jerry Peek and Tim O'Reilly and Mike Loukides and other authors of the Nutshell handbooks"; let name = Name::Literal { literal: literal.into(), diff --git a/support/texlab/src/citeproc/name/parser.lalrpop b/support/texlab/src/citation/name/parser.lalrpop index e7bce8a302..e7bce8a302 100644 --- a/support/texlab/src/citeproc/name/parser.lalrpop +++ b/support/texlab/src/citation/name/parser.lalrpop diff --git a/support/texlab/src/citeproc/ris.rs b/support/texlab/src/citation/ris.rs index 1cb5754aec..5b666160a3 100644 --- a/support/texlab/src/citeproc/ris.rs +++ b/support/texlab/src/citation/ris.rs @@ -1,12 +1,13 @@ // Ported from: https://github.com/michel-kraemer/citeproc-java/tree/master/citeproc-java/templates // Michel Kraemer // Apache License 2.0 -use super::name; use citeproc_io::{Date, DateOrRange, Name, NumericValue, Reference}; use csl::*; use fnv::FnvHashMap; use serde::{Deserialize, Serialize}; +use super::name; + #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum RisType { diff --git a/support/texlab/src/client.rs b/support/texlab/src/client.rs new file mode 100644 index 0000000000..72f2ac9eba --- /dev/null +++ b/support/texlab/src/client.rs @@ -0,0 +1,53 @@ +use std::sync::Mutex; + +use anyhow::{anyhow, Result}; +use crossbeam_channel::{Receiver, Sender}; +use lsp_server::{Message, ResponseError}; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::req_queue::{OutgoingData, ReqQueue}; + +pub fn send_notification<N>(lsp_sender: &Sender<Message>, params: N::Params) -> Result<()> +where + N: lsp_types::notification::Notification, + N::Params: Serialize, +{ + lsp_sender.send(lsp_server::Notification::new(N::METHOD.to_string(), params).into())?; + Ok(()) +} + +pub fn send_request<R>( + req_queue: &Mutex<ReqQueue>, + lsp_sender: &Sender<Message>, + params: R::Params, +) -> Result<R::Result> +where + R: lsp_types::request::Request, + R::Params: Serialize, + R::Result: DeserializeOwned, +{ + let receiver = register_outgoing_request::<R>(req_queue, lsp_sender, params)?; + let params = receiver.recv()?.map_err(|err| anyhow!(err.message))?; + let result = serde_json::from_value(params)?; + Ok(result) +} + +fn register_outgoing_request<R>( + req_queue: &Mutex<ReqQueue>, + lsp_sender: &Sender<Message>, + params: R::Params, +) -> Result<Receiver<Result<serde_json::Value, ResponseError>>> +where + R: lsp_types::request::Request, + R::Params: Serialize, + R::Result: DeserializeOwned, +{ + let mut req_queue = req_queue.lock().unwrap(); + let (sender, receiver) = crossbeam_channel::bounded(1); + let method = R::METHOD.to_string(); + let data = OutgoingData { sender }; + let req = req_queue.outgoing.register(method, params, data); + drop(req_queue); + lsp_sender.send(req.into())?; + Ok(receiver) +} diff --git a/support/texlab/src/completion/bibtex/cmd.rs b/support/texlab/src/completion/bibtex/cmd.rs deleted file mode 100644 index eab905386a..0000000000 --- a/support/texlab/src/completion/bibtex/cmd.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::{ - completion::types::{Item, ItemData}, - components::COMPONENT_DATABASE, - feature::FeatureRequest, - protocol::{CompletionParams, RangeExt}, - syntax::SyntaxNode, - workspace::DocumentContent, -}; - -pub async fn complete_bibtex_commands<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - if let DocumentContent::Bibtex(tree) = &req.current().content { - let pos = req.params.text_document_position.position; - if let Some(cmd) = tree - .find(pos) - .into_iter() - .last() - .and_then(|node| tree.as_command(node)) - { - if cmd.token.range().contains(pos) && cmd.token.start().character != pos.character { - let mut range = cmd.range(); - range.start.character += 1; - for cmd in &COMPONENT_DATABASE.kernel().commands { - let item = Item::new( - range, - ItemData::ComponentCommand { - name: &cmd.name, - image: cmd.image.as_deref(), - glyph: cmd.glyph.as_deref(), - file_names: &[], - }, - ); - items.push(item); - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_comment() { - let req = FeatureTester::new() - .file("main.bib", r#"\"#) - .main("main.bib") - .position(0, 1) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_command() { - let req = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @article{foo, bar= - \} - "# - ), - ) - .main("main.bib") - .position(1, 1) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_commands(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(1, 1, 1, 2)); - } - - #[tokio::test] - async fn start_of_command() { - let req = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @article{foo, bar= - \} - "# - ), - ) - .main("main.bib") - .position(1, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_text() { - let req = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @article{foo, bar= - } - "# - ), - ) - .main("main.bib") - .position(1, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_latex_command() { - let req = FeatureTester::new() - .file("main.tex", r#"\"#) - .main("main.tex") - .position(0, 1) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } -} diff --git a/support/texlab/src/completion/bibtex/entry_type.rs b/support/texlab/src/completion/bibtex/entry_type.rs deleted file mode 100644 index e3b12ef00d..0000000000 --- a/support/texlab/src/completion/bibtex/entry_type.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::{CompletionParams, Position, Range, RangeExt}, - syntax::{bibtex, SyntaxNode, LANGUAGE_DATA}, - workspace::DocumentContent, -}; - -pub async fn complete_bibtex_entry_types<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - if let DocumentContent::Bibtex(tree) = &req.current().content { - let pos = req.params.text_document_position.position; - for decl in tree.children(tree.root) { - match &tree.graph[decl] { - bibtex::Node::Preamble(preamble) => { - if contains(&preamble.ty, pos) { - make_items(items, preamble.ty.range()); - return; - } - } - bibtex::Node::String(string) => { - if contains(&string.ty, pos) { - make_items(items, string.ty.range()); - return; - } - } - bibtex::Node::Entry(entry) => { - if contains(&entry.ty, pos) { - make_items(items, entry.ty.range()); - return; - } - } - _ => {} - } - } - } -} - -fn contains(ty: &bibtex::Token, pos: Position) -> bool { - ty.range().contains(pos) && ty.start().character != pos.character -} - -fn make_items(items: &mut Vec<Item>, mut range: Range) { - range.start.character += 1; - for ty in &LANGUAGE_DATA.entry_types { - let item = Item::new(range, ItemData::EntryType { ty }); - items.push(item); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_entry_types(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_entry_types(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn before_at_sign() { - let req = FeatureTester::new() - .file("main.bib", "@") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_entry_types(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn after_at_sign() { - let req = FeatureTester::new() - .file("main.bib", "@") - .main("main.bib") - .position(0, 1) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_entry_types(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 1, 0, 1)); - } - - #[tokio::test] - async fn inside_entry_type() { - let req = FeatureTester::new() - .file("main.bib", "@foo") - .main("main.bib") - .position(0, 2) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_entry_types(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 1, 0, 4)); - } - - #[tokio::test] - async fn inside_entry_key() { - let req = FeatureTester::new() - .file("main.bib", "@article{foo,}") - .main("main.bib") - .position(0, 11) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_entry_types(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_comments() { - let req = FeatureTester::new() - .file("main.bib", "foo") - .main("main.bib") - .position(0, 2) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_entry_types(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } -} diff --git a/support/texlab/src/completion/bibtex/field_name.rs b/support/texlab/src/completion/bibtex/field_name.rs deleted file mode 100644 index 039cc5d6b3..0000000000 --- a/support/texlab/src/completion/bibtex/field_name.rs +++ /dev/null @@ -1,190 +0,0 @@ -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::{CompletionParams, Range, RangeExt}, - syntax::{bibtex, SyntaxNode, LANGUAGE_DATA}, - workspace::DocumentContent, -}; - -pub async fn complete_bibtex_fields<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - if let DocumentContent::Bibtex(tree) = &req.current().content { - let pos = req.params.text_document_position.position; - match tree - .find(pos) - .into_iter() - .last() - .map(|node| &tree.graph[node]) - { - Some(bibtex::Node::Field(field)) => { - if field.name.range().contains(pos) { - make_items(items, field.name.range()); - return; - } - } - Some(bibtex::Node::Entry(entry)) => { - if !entry.is_comment() && !entry.ty.range().contains(pos) { - let range = Range::new(pos, pos); - if let Some(key) = &entry.key { - if !key.range().contains(pos) { - make_items(items, range); - return; - } - } else { - make_items(items, range); - return; - } - } - } - _ => (), - } - } -} - -fn make_items(items: &mut Vec<Item>, range: Range) { - for field in &LANGUAGE_DATA.fields { - let item = Item::new(range, ItemData::Field { field }); - items.push(item); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_fields(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_fields(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_first_field() { - let req = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @article{foo, - bar} - "# - ), - ) - .main("main.bib") - .position(1, 1) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_fields(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(1, 0, 1, 3)); - } - - #[tokio::test] - async fn inside_second_field() { - let req = FeatureTester::new() - .file("main.bib", "@article{foo, bar = {baz}, qux}") - .main("main.bib") - .position(0, 27) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_fields(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 27, 0, 30)); - } - - #[tokio::test] - async fn inside_entry() { - let req = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @article{foo, - } - "# - ), - ) - .main("main.bib") - .position(1, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_fields(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(1, 0, 1, 0)); - } - - #[tokio::test] - async fn inside_content() { - let req = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @article{foo, - bar = {baz}} - "# - ), - ) - .main("main.bib") - .position(1, 7) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_fields(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_entry_type() { - let req = FeatureTester::new() - .file("main.bib", "@article{foo,}") - .main("main.bib") - .position(0, 3) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_bibtex_fields(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } -} diff --git a/support/texlab/src/completion/bibtex/mod.rs b/support/texlab/src/completion/bibtex/mod.rs deleted file mode 100644 index 96f3a94932..0000000000 --- a/support/texlab/src/completion/bibtex/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod cmd; -pub mod entry_type; -pub mod field_name; diff --git a/support/texlab/src/completion/latex/argument.rs b/support/texlab/src/completion/latex/argument.rs deleted file mode 100644 index 5736099ca6..0000000000 --- a/support/texlab/src/completion/latex/argument.rs +++ /dev/null @@ -1,215 +0,0 @@ -use super::combinators::{self, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::CompletionParams, -}; -use std::iter; - -pub async fn complete_latex_arguments<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - for comp in req.view.components() { - for cmd in &comp.commands { - for (i, param) in cmd.parameters.iter().enumerate() { - complete_internal(req, items, &cmd.name, i, param).await; - } - } - } -} - -async fn complete_internal<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, - name: &'a str, - index: usize, - param: &'a crate::components::Parameter, -) { - combinators::argument( - req, - iter::once(Parameter { name, index }), - |ctx| async move { - for arg in ¶m.0 { - let item = Item::new( - ctx.range, - ItemData::Argument { - name: &arg.name, - image: arg.image.as_deref(), - }, - ); - items.push(item); - } - }, - ) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_arguments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_arguments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_mathbb_empty() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \usepackage{amsfonts} - \mathbb{} - "# - ), - ) - .main("main.tex") - .position(1, 8) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_arguments(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(1, 8, 1, 8)); - } - - #[tokio::test] - async fn inside_mathbb_non_empty() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \usepackage{amsfonts} - \mathbb{foo} - "# - ), - ) - .main("main.tex") - .position(1, 8) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_arguments(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(1, 8, 1, 11)); - } - - #[tokio::test] - async fn outside_mathbb_empty() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \usepackage{amsfonts} - \mathbb{} - "# - ), - ) - .main("main.tex") - .position(1, 9) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_arguments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } -} - -/* -use super::combinators::{self, Parameter}; -use crate::{ - completion::factory, - feature::{FeatureProvider, FeatureRequest}, - protocol::{CompletionItem, CompletionParams, TextEdit}, -}; -use async_trait::async_trait; -use std::iter; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexArgumentCompletionProvider; - -#[async_trait] -impl FeatureProvider for LatexArgumentCompletionProvider { - type Params = CompletionParams; - type Output = Vec<CompletionItem>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut all_items = Vec::new(); - for comp in req.view.components() { - for cmd in &comp.commands { - let name = format!("\\{}", cmd.name); - for (i, param) in cmd.parameters.iter().enumerate() { - let mut items = combinators::argument( - req, - iter::once(Parameter { - name: &name, - index: i, - }), - |ctx| async move { - let mut items = Vec::new(); - for arg in ¶m.0 { - let text_edit = TextEdit::new(ctx.range, (&arg.name).into()); - let item = factory::argument( - req, - &arg.name, - text_edit, - arg.image.as_ref().map(AsRef::as_ref), - ); - items.push(item); - } - items - }, - ) - .await; - all_items.append(&mut items); - } - } - } - all_items - } -} - - - -*/ diff --git a/support/texlab/src/completion/latex/begin_cmd.rs b/support/texlab/src/completion/latex/begin_cmd.rs deleted file mode 100644 index 44e7a4f60d..0000000000 --- a/support/texlab/src/completion/latex/begin_cmd.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::combinators; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::CompletionParams, -}; - -pub async fn complete_latex_begin_command<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - combinators::command(req, |cmd_node| async move { - let table = req.current().content.as_latex().unwrap(); - let cmd = table.as_command(cmd_node).unwrap(); - let range = cmd.short_name_range(); - items.push(Item::new(range, ItemData::BeginCommand)); - }) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_begin_command(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_begin_command(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn after_backslash() { - let req = FeatureTester::new() - .file("main.tex", r#"\"#) - .main("main.tex") - .position(0, 1) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_begin_command(&req, &mut actual_items).await; - - assert_eq!(actual_items.len(), 1); - } -} diff --git a/support/texlab/src/completion/latex/citation.rs b/support/texlab/src/completion/latex/citation.rs deleted file mode 100644 index 3e7b1851d1..0000000000 --- a/support/texlab/src/completion/latex/citation.rs +++ /dev/null @@ -1,261 +0,0 @@ -use super::combinators::{self, ArgumentContext, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::{BibtexFormattingOptions, CompletionParams}, - syntax::{bibtex, BibtexEntryTypeCategory, Structure, LANGUAGE_DATA}, - workspace::{Document, DocumentContent}, -}; -use once_cell::sync::Lazy; -use petgraph::graph::NodeIndex; -use regex::Regex; - -pub async fn complete_latex_citations<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - let parameters = LANGUAGE_DATA.citation_commands.iter().map(|cmd| Parameter { - name: &cmd.name[1..], - index: cmd.index, - }); - - combinators::argument(req, parameters, |ctx| async move { - for doc in req.related() { - if let DocumentContent::Bibtex(tree) = &doc.content { - for entry_node in tree.children(tree.root) { - if let Some(item) = make_item(ctx, doc, tree, entry_node) { - items.push(item); - } - } - } - } - }) - .await; -} - -fn make_item<'a>( - ctx: ArgumentContext, - doc: &'a Document, - tree: &'a bibtex::Tree, - entry_node: NodeIndex, -) -> Option<Item<'a>> { - let entry = tree.as_entry(entry_node)?; - if entry.is_comment() { - return None; - } - - let key = entry.key.as_ref()?.text(); - let options = BibtexFormattingOptions::default(); - let params = bibtex::FormattingParams { - insert_spaces: true, - tab_size: 4, - options: &options, - }; - let entry_code = bibtex::format(tree, entry_node, params); - let text = format!( - "{} {}", - &key, - WHITESPACE_REGEX - .replace_all( - &entry_code - .replace('{', "") - .replace('}', "") - .replace(',', " ") - .replace('=', " "), - " ", - ) - .trim() - ); - - let ty = LANGUAGE_DATA - .find_entry_type(&entry.ty.text()[1..]) - .map(|ty| Structure::Entry(ty.category)) - .unwrap_or_else(|| Structure::Entry(BibtexEntryTypeCategory::Misc)); - - let item = Item::new( - ctx.range, - ItemData::Citation { - uri: &doc.uri, - key, - text, - ty, - }, - ); - Some(item) -} - -static WHITESPACE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\s+").unwrap()); - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_citations(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_citations(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn incomplete() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \addbibresource{main.bib} - \cite{ - \begin{foo} - \end{bar} - "# - ), - ) - .file("main.bib", "@article{foo,}") - .main("main.tex") - .position(1, 6) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_citations(&req, &mut actual_items).await; - - assert_eq!(actual_items.len(), 1); - assert_eq!(actual_items[0].data.label(), "foo"); - assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 6)); - } - - #[tokio::test] - async fn empty_key() { - let req = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \addbibresource{bar.bib} - \cite{} - "# - ), - ) - .file("bar.bib", "@article{foo,}") - .file("baz.bib", "@article{bar,}") - .main("foo.tex") - .position(1, 6) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_citations(&req, &mut actual_items).await; - - assert_eq!(actual_items.len(), 1); - assert_eq!(actual_items[0].data.label(), "foo"); - assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 6)); - } - - #[tokio::test] - async fn single_key() { - let req = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \addbibresource{bar.bib} - \cite{foo} - "# - ), - ) - .file("bar.bib", "@article{foo,}") - .file("baz.bib", "@article{bar,}") - .main("foo.tex") - .position(1, 6) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_citations(&req, &mut actual_items).await; - - assert_eq!(actual_items.len(), 1); - assert_eq!(actual_items[0].data.label(), "foo"); - assert_eq!(actual_items[0].range, Range::new_simple(1, 6, 1, 9)); - } - - #[tokio::test] - async fn second_key() { - let req = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \addbibresource{bar.bib} - \cite{foo,} - "# - ), - ) - .file("bar.bib", "@article{foo,}") - .file("baz.bib", "@article{bar,}") - .main("foo.tex") - .position(1, 10) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_citations(&req, &mut actual_items).await; - - assert_eq!(actual_items.len(), 1); - assert_eq!(actual_items[0].data.label(), "foo"); - assert_eq!(actual_items[0].range, Range::new_simple(1, 10, 1, 10)); - } - - #[tokio::test] - async fn outside_cite() { - let req = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \addbibresource{bar.bib} - \cite{} - "# - ), - ) - .file("bar.bib", "@article{foo,}") - .file("baz.bib", "@article{bar,}") - .main("foo.tex") - .position(1, 7) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_citations(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } -} diff --git a/support/texlab/src/completion/latex/color.rs b/support/texlab/src/completion/latex/color.rs deleted file mode 100644 index 93b06b178e..0000000000 --- a/support/texlab/src/completion/latex/color.rs +++ /dev/null @@ -1,91 +0,0 @@ -use super::combinators::{self, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::CompletionParams, - syntax::LANGUAGE_DATA, -}; - -pub async fn complete_latex_colors<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - let parameters = LANGUAGE_DATA.color_commands.iter().map(|cmd| Parameter { - name: &cmd.name[1..], - index: cmd.index, - }); - - combinators::argument(req, parameters, |ctx| async move { - for name in &LANGUAGE_DATA.colors { - let item = Item::new(ctx.range, ItemData::Color { name }); - items.push(item); - } - }) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - complete_latex_colors(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - complete_latex_colors(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_color() { - let req = FeatureTester::new() - .file("main.tex", r#"\color{}"#) - .main("main.tex") - .position(0, 7) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - complete_latex_colors(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 7)); - } - - #[tokio::test] - async fn inside_define_color_set() { - let req = FeatureTester::new() - .file("main.tex", r#"\color{}"#) - .main("main.tex") - .position(0, 8) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - complete_latex_colors(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } -} diff --git a/support/texlab/src/completion/latex/color_model.rs b/support/texlab/src/completion/latex/color_model.rs deleted file mode 100644 index e0151da2f0..0000000000 --- a/support/texlab/src/completion/latex/color_model.rs +++ /dev/null @@ -1,101 +0,0 @@ -use super::combinators::{self, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::CompletionParams, - syntax::LANGUAGE_DATA, -}; - -const MODEL_NAMES: &[&str] = &["gray", "rgb", "RGB", "HTML", "cmyk"]; - -pub async fn complete_latex_color_models<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - let parameters = LANGUAGE_DATA - .color_model_commands - .iter() - .map(|cmd| Parameter { - name: &cmd.name[1..], - index: cmd.index, - }); - - combinators::argument(req, parameters, |ctx| async move { - for name in MODEL_NAMES { - let item = Item::new(ctx.range, ItemData::ColorModel { name }); - items.push(item); - } - }) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_color_models(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_color_models(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_define_color() { - let req = FeatureTester::new() - .file("main.tex", r#"\definecolor{name}{}"#) - .main("main.tex") - .position(0, 19) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_color_models(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 19, 0, 19)); - } - - #[tokio::test] - async fn inside_define_color_set() { - let req = FeatureTester::new() - .file("main.tex", r#"\definecolorset{}"#) - .main("main.tex") - .position(0, 16) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_color_models(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 16, 0, 16)); - } -} diff --git a/support/texlab/src/completion/latex/combinators.rs b/support/texlab/src/completion/latex/combinators.rs deleted file mode 100644 index d8ef336421..0000000000 --- a/support/texlab/src/completion/latex/combinators.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::{ - feature::FeatureRequest, - protocol::{CompletionParams, Position, Range, RangeExt}, - syntax::{latex, AstNodeIndex, SyntaxNode, LANGUAGE_DATA}, - workspace::DocumentContent, -}; -use std::future::Future; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct Parameter<'a> { - pub name: &'a str, - pub index: usize, -} - -pub async fn command<E, F>(req: &FeatureRequest<CompletionParams>, execute: E) -where - E: FnOnce(AstNodeIndex) -> F, - F: Future<Output = ()>, -{ - if let DocumentContent::Latex(table) = &req.current().content { - let pos = req.params.text_document_position.position; - if let Some(cmd) = table.find_command_by_short_name_range(pos) { - execute(cmd).await; - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct ArgumentContext<'a> { - pub parameter: Parameter<'a>, - pub node: AstNodeIndex, - pub range: Range, -} - -pub async fn argument<'a, I, E, F>( - req: &'a FeatureRequest<CompletionParams>, - mut parameters: I, - execute: E, -) where - I: Iterator<Item = Parameter<'a>>, - E: FnOnce(ArgumentContext<'a>) -> F, - F: Future<Output = ()>, -{ - if let DocumentContent::Latex(table) = &req.current().content { - let pos = req.params.text_document_position.position; - if let Some(node) = find_command(&table, pos) { - let cmd = table.as_command(node).unwrap(); - for parameter in parameters - .by_ref() - .filter(|param| param.name == &cmd.name.text()[1..]) - { - if let Some(args_node) = - table.extract_group(node, latex::GroupKind::Group, parameter.index) - { - let args = table.as_group(args_node).unwrap(); - if args.right.is_some() && !args.range().contains_exclusive(pos) { - continue; - } - - let range = table - .children(args_node) - .filter_map(|child| table.as_text(child)) - .flat_map(|text| text.words.iter()) - .map(|word| word.range()) - .find(|range| range.contains(pos)) - .unwrap_or_else(|| Range::new(pos, pos)); - - let context = ArgumentContext { - parameter, - node, - range, - }; - execute(context).await; - return; - } - } - } - } -} - -pub async fn argument_word<'a, I, E, F>( - req: &'a FeatureRequest<CompletionParams>, - mut parameters: I, - execute: E, -) where - I: Iterator<Item = Parameter<'a>>, - E: FnOnce(AstNodeIndex, usize) -> F, - F: Future<Output = ()>, -{ - if let DocumentContent::Latex(table) = &req.current().content { - let pos = req.params.text_document_position.position; - if let Some(node) = find_command(&table, pos) { - let cmd = table.as_command(node).unwrap(); - for parameter in parameters - .by_ref() - .filter(|param| param.name == &cmd.name.text()[1..]) - { - if let Some(args_node) = - table.extract_group(node, latex::GroupKind::Group, parameter.index) - { - let args = table.as_group(args_node).unwrap(); - if args.right.is_some() && !args.range().contains_exclusive(pos) { - continue; - } - - if table.children(args_node).next().is_some() - && table - .extract_word(node, latex::GroupKind::Group, parameter.index) - .is_none() - { - continue; - } - - execute(node, parameter.index).await; - return; - } - } - } - } -} - -pub async fn environment<'a, E, F>(req: &'a FeatureRequest<CompletionParams>, execute: E) -where - E: FnOnce(ArgumentContext<'a>) -> F, - F: Future<Output = ()>, -{ - let parameters = LANGUAGE_DATA - .environment_commands - .iter() - .map(|cmd| Parameter { - name: &cmd.name[1..], - index: cmd.index, - }); - argument(req, parameters, execute).await; -} - -fn find_command(table: &latex::SymbolTable, pos: Position) -> Option<AstNodeIndex> { - table - .find(pos) - .into_iter() - .rev() - .find(|node| table.as_command(*node).is_some()) -} diff --git a/support/texlab/src/completion/latex/component.rs b/support/texlab/src/completion/latex/component.rs deleted file mode 100644 index 8cf9f494a4..0000000000 --- a/support/texlab/src/completion/latex/component.rs +++ /dev/null @@ -1,351 +0,0 @@ -use super::combinators; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::CompletionParams, -}; - -pub async fn complete_latex_component_commands<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - combinators::command(req, |cmd_node| async move { - let table = req.current().content.as_latex().unwrap(); - let cmd = table.as_command(cmd_node).unwrap(); - let range = cmd.short_name_range(); - - for comp in req.view.components() { - for cmd in &comp.commands { - items.push(Item::new( - range, - ItemData::ComponentCommand { - name: &cmd.name, - image: cmd.image.as_deref(), - glyph: cmd.glyph.as_deref(), - file_names: &comp.file_names, - }, - )); - } - } - }) - .await; -} - -pub async fn complete_latex_component_environments<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - combinators::environment(req, |ctx| async move { - for comp in req.view.components() { - for env in &comp.environments { - items.push(Item::new( - ctx.range, - ItemData::ComponentEnvironment { - name: env, - file_names: &comp.file_names, - }, - )); - } - } - }) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document_command() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_command() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_latex_document_environment() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_environment() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn command_start() { - let req = FeatureTester::new() - .file("main.tex", r#"\use"#) - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn command_end() { - let req = FeatureTester::new() - .file("main.tex", r#"\use"#) - .main("main.tex") - .position(0, 4) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_commands(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 1, 0, 4)); - } - - #[tokio::test] - async fn command_word() { - let req = FeatureTester::new() - .file("main.tex", r#"use"#) - .main("main.tex") - .position(0, 2) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn command_package() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \usepackage{lipsum} - \lips - "# - ), - ) - .main("main.tex") - .position(1, 2) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_commands(&req, &mut actual_items).await; - - assert!(actual_items - .iter() - .any(|item| item.data.label() == "lipsum")); - } - - #[tokio::test] - async fn command_package_comma_separated() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \usepackage{geometry, lipsum} - \lips - "# - ), - ) - .main("main.tex") - .position(1, 2) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_commands(&req, &mut actual_items).await; - - assert!(actual_items - .iter() - .any(|item| item.data.label() == "lipsum")); - } - - #[tokio::test] - async fn command_class() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \documentclass{book} - \chap - "# - ), - ) - .main("main.tex") - .position(1, 2) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_commands(&req, &mut actual_items).await; - - assert!(actual_items - .iter() - .any(|item| item.data.label() == "chapter")); - } - - #[tokio::test] - async fn environment_inside_of_empty_begin() { - let req = FeatureTester::new() - .file("main.tex", r#"\begin{}"#) - .main("main.tex") - .position(0, 7) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 7)); - } - - #[tokio::test] - async fn environment_inside_of_non_empty_end() { - let req = FeatureTester::new() - .file("main.tex", r#"\end{foo}"#) - .main("main.tex") - .position(0, 6) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 5, 0, 8)); - } - - #[tokio::test] - async fn environment_outside_of_empty_begin() { - let req = FeatureTester::new() - .file("main.tex", r#"\begin{}"#) - .main("main.tex") - .position(0, 6) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn environment_outside_of_empty_end() { - let req = FeatureTester::new() - .file("main.tex", r#"\end{}"#) - .main("main.tex") - .position(0, 6) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn environment_inside_of_other_command() { - let req = FeatureTester::new() - .file("main.tex", r#"\foo{bar}"#) - .main("main.tex") - .position(0, 6) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn environment_inside_second_argument() { - let req = FeatureTester::new() - .file("main.tex", r#"\begin{foo}{bar}"#) - .main("main.tex") - .position(0, 14) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn environment_unterminated() { - let req = FeatureTester::new() - .file("main.tex", r#"\begin{foo"#) - .main("main.tex") - .position(0, 7) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_component_environments(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - assert_eq!(actual_items[0].range, Range::new_simple(0, 7, 0, 10)); - } -} diff --git a/support/texlab/src/completion/latex/glossary.rs b/support/texlab/src/completion/latex/glossary.rs deleted file mode 100644 index 417152dd21..0000000000 --- a/support/texlab/src/completion/latex/glossary.rs +++ /dev/null @@ -1,114 +0,0 @@ -use super::combinators::{self, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::CompletionParams, - syntax::{ - LatexGlossaryEntryKind::{Acronym, General}, - LANGUAGE_DATA, - }, - workspace::DocumentContent, -}; - -pub async fn complete_latex_glossary_entries<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - let parameters = LANGUAGE_DATA - .glossary_entry_reference_commands - .iter() - .map(|cmd| Parameter { - name: &cmd.name[1..], - index: cmd.index, - }); - - combinators::argument(req, parameters, |ctx| async move { - let cmd_kind = LANGUAGE_DATA - .glossary_entry_reference_commands - .iter() - .find(|cmd| &cmd.name[1..] == ctx.parameter.name) - .unwrap() - .kind; - - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - for entry in &table.glossary_entries { - match (cmd_kind, entry.kind) { - (Acronym, Acronym) | (General, General) | (General, Acronym) => { - let name = entry.label(&table).text(); - let item = Item::new(ctx.range, ItemData::GlossaryEntry { name }); - items.push(item); - } - (Acronym, General) => {} - } - } - } - } - }) - .await -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_glossary_entries(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_glossary_entries(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn acronym() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \newacronym{lvm}{LVM}{Logical Volume Manager} - \acrfull{foo} - "# - ), - ) - .main("main.tex") - .position(1, 9) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_glossary_entries(&req, &mut actual_items).await; - - assert_eq!(actual_items.len(), 1); - assert_eq!(actual_items[0].data.label(), "lvm"); - assert_eq!(actual_items[0].range, Range::new_simple(1, 9, 1, 12)); - } -} diff --git a/support/texlab/src/completion/latex/import.rs b/support/texlab/src/completion/latex/import.rs deleted file mode 100644 index 06c28e189b..0000000000 --- a/support/texlab/src/completion/latex/import.rs +++ /dev/null @@ -1,188 +0,0 @@ -use super::combinators::{self, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - components::COMPONENT_DATABASE, - feature::FeatureRequest, - protocol::CompletionParams, - syntax::{LatexIncludeKind, LANGUAGE_DATA}, -}; -use std::{borrow::Cow, collections::HashSet}; - -pub async fn complete_latex_classes<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - complete_latex_imports(req, items, LatexIncludeKind::Class, |name| { - ItemData::Class { name } - }) - .await; -} - -pub async fn complete_latex_packages<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - complete_latex_imports(req, items, LatexIncludeKind::Package, |name| { - ItemData::Package { name } - }) - .await; -} - -async fn complete_latex_imports<'a, F>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, - kind: LatexIncludeKind, - mut factory: F, -) where - F: FnMut(Cow<'a, str>) -> ItemData<'a>, -{ - let extension = if kind == LatexIncludeKind::Package { - "sty" - } else { - "cls" - }; - - let parameters = LANGUAGE_DATA - .include_commands - .iter() - .filter(|cmd| cmd.kind == kind) - .map(|cmd| Parameter { - name: &cmd.name[1..], - index: cmd.index, - }); - - combinators::argument(req, parameters, |ctx| async move { - let resolver = req.distro.resolver().await; - let mut file_names = HashSet::new(); - COMPONENT_DATABASE - .components - .iter() - .flat_map(|comp| comp.file_names.iter()) - .filter(|file_name| file_name.ends_with(extension)) - .for_each(|file_name| { - file_names.insert(file_name); - let stem = &file_name[0..file_name.len() - 4]; - let data = factory(stem.into()); - let item = Item::new(ctx.range, data); - items.push(item); - }); - - resolver - .files_by_name - .keys() - .filter(|file_name| file_name.ends_with(extension) && !file_names.contains(file_name)) - .for_each(|file_name| { - let stem = &file_name[0..file_name.len() - 4]; - let data = factory(stem.to_owned().into()); - let item = Item::new(ctx.range, data); - items.push(item); - }); - }) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - - #[tokio::test] - async fn empty_latex_document_class() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_classes(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_class() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_classes(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_latex_document_package() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_packages(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_package() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_packages(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn class() { - let req = FeatureTester::new() - .file("main.tex", r#"\documentclass{}"#) - .main("main.tex") - .position(0, 15) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_classes(&req, &mut actual_items).await; - - assert!(actual_items - .iter() - .any(|item| item.data.label() == "beamer")); - assert!(actual_items - .iter() - .all(|item| item.data.label() != "amsmath")); - } - - #[tokio::test] - async fn package() { - let req = FeatureTester::new() - .file("main.tex", r#"\usepackage{}"#) - .main("main.tex") - .position(0, 12) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_packages(&req, &mut actual_items).await; - - assert!(actual_items - .iter() - .all(|item| item.data.label() != "beamer")); - assert!(actual_items - .iter() - .any(|item| item.data.label() == "amsmath")); - } -} diff --git a/support/texlab/src/completion/latex/include.rs b/support/texlab/src/completion/latex/include.rs deleted file mode 100644 index 1a8555d07c..0000000000 --- a/support/texlab/src/completion/latex/include.rs +++ /dev/null @@ -1,133 +0,0 @@ -use super::combinators::{self, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::{CompletionParams, Range, RangeExt}, - syntax::{latex, AstNodeIndex, SyntaxNode, LANGUAGE_DATA}, -}; -use std::path::{Path, PathBuf}; -use tokio::fs; - -pub async fn complete_latex_includes<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - let parameters = LANGUAGE_DATA.include_commands.iter().map(|cmd| Parameter { - name: &cmd.name[1..], - index: cmd.index, - }); - - combinators::argument_word(req, parameters, |cmd_node, index| async move { - if !req.current().is_file() { - return; - } - - make_items(req, items, cmd_node, index).await; - }) - .await; -} - -async fn make_items<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, - cmd_node: AstNodeIndex, - index: usize, -) -> Option<()> { - let table = req.current().content.as_latex()?; - let pos = req.params.text_document_position.position; - let path_word = table.extract_word(cmd_node, latex::GroupKind::Group, index); - let name_range = match path_word { - Some(path_word) => Range::new_simple( - path_word.start().line, - path_word.end().character - path_word.text().split('/').last()?.chars().count() as u64, - path_word.end().line, - path_word.end().character, - ), - None => Range::new(pos, pos), - }; - - let cmd = table.as_command(cmd_node)?; - let current_dir = current_dir(req, table, cmd_node)?; - let mut entries = fs::read_dir(current_dir).await.ok()?; - while let Some(entry) = entries.next_entry().await.ok()? { - let mut path = entry.path(); - - let file_type = entry.file_type().await.ok()?; - if file_type.is_file() && is_included(&cmd, &path) { - let include_extension = LANGUAGE_DATA - .include_commands - .iter() - .find(|c| cmd.name.text() == c.name)? - .include_extension; - - if !include_extension { - remove_extension(&mut path); - } - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - let item = Item::new(name_range, ItemData::File { name }); - items.push(item); - } else if file_type.is_dir() { - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - let item = Item::new(name_range, ItemData::Directory { name }); - items.push(item); - } - } - Some(()) -} - -fn current_dir( - req: &FeatureRequest<CompletionParams>, - table: &latex::SymbolTable, - cmd_node: AstNodeIndex, -) -> Option<PathBuf> { - let mut path = req - .options - .latex - .as_ref() - .and_then(|latex| latex.root_directory.as_ref()) - .map_or_else( - || { - let mut path = req.current().uri.to_file_path().unwrap(); - path.pop(); - path - }, - Clone::clone, - ); - - path = PathBuf::from(path.to_str()?.replace('\\', "/")); - if let Some(include) = table.extract_word(cmd_node, latex::GroupKind::Group, 0) { - path.push(include.text()); - if !include.text().ends_with('/') { - path.pop(); - } - } - Some(path) -} - -fn is_included(cmd: &latex::Command, file: &Path) -> bool { - if let Some(allowed_extensions) = LANGUAGE_DATA - .include_commands - .iter() - .find(|c| c.name == cmd.name.text()) - .and_then(|c| c.kind.extensions()) - { - file.extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.to_lowercase()) - .map(|ext| allowed_extensions.contains(&ext.as_str())) - .unwrap_or_default() - } else { - true - } -} - -fn remove_extension(path: &mut PathBuf) { - if let Some(stem) = path - .file_stem() - .and_then(|stem| stem.to_str()) - .map(ToOwned::to_owned) - { - path.pop(); - path.push(stem); - } -} diff --git a/support/texlab/src/completion/latex/label.rs b/support/texlab/src/completion/latex/label.rs deleted file mode 100644 index 91aa80e532..0000000000 --- a/support/texlab/src/completion/latex/label.rs +++ /dev/null @@ -1,230 +0,0 @@ -use super::combinators::{self, ArgumentContext, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - feature::{DocumentView, FeatureRequest}, - outline::{Outline, OutlineContext, OutlineContextItem}, - protocol::{CompletionParams, RangeExt}, - syntax::{ - latex, LatexLabelKind, LatexLabelReferenceSource, Structure, SyntaxNode, LANGUAGE_DATA, - }, - workspace::DocumentContent, -}; -use std::sync::Arc; - -pub async fn complete_latex_labels<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - let parameters = LANGUAGE_DATA - .label_commands - .iter() - .filter(|cmd| cmd.kind.is_reference()) - .map(|cmd| Parameter { - name: &cmd.name[1..], - index: cmd.index, - }); - - combinators::argument(req, parameters, |ctx| async move { - let source = find_source(ctx); - for doc in req.related() { - let snapshot = Arc::clone(&req.view.snapshot); - let view = - DocumentView::analyze(snapshot, Arc::clone(&doc), &req.options, &req.current_dir); - let outline = Outline::analyze(&view, &req.options, &req.current_dir); - - if let DocumentContent::Latex(table) = &doc.content { - for label in table - .labels - .iter() - .filter(|label| label.kind == LatexLabelKind::Definition) - .filter(|label| is_included(&table, label, source)) - { - let outline_ctx = OutlineContext::parse(&view, &outline, *label); - - let kind = match outline_ctx.as_ref().map(|ctx| &ctx.item) { - Some(OutlineContextItem::Section { .. }) => Structure::Section, - Some(OutlineContextItem::Caption { .. }) => Structure::Float, - Some(OutlineContextItem::Theorem { .. }) => Structure::Theorem, - Some(OutlineContextItem::Equation) => Structure::Equation, - Some(OutlineContextItem::Item) => Structure::Item, - None => Structure::Label, - }; - - for name in label.names(&table) { - let header = outline_ctx.as_ref().and_then(|ctx| ctx.detail()); - let footer = outline_ctx.as_ref().and_then(|ctx| match &ctx.item { - OutlineContextItem::Caption { text, .. } => Some(text.clone()), - _ => None, - }); - - let text = outline_ctx - .as_ref() - .map(|ctx| format!("{} {}", name.text(), ctx.reference())) - .unwrap_or_else(|| name.text().into()); - - let item = Item::new( - ctx.range, - ItemData::Label { - name: name.text(), - kind, - header, - footer, - text, - }, - ); - items.push(item); - } - } - } - } - }) - .await; -} - -fn find_source(ctx: ArgumentContext) -> LatexLabelReferenceSource { - match LANGUAGE_DATA - .label_commands - .iter() - .find(|cmd| &cmd.name[1..] == ctx.parameter.name && cmd.index == ctx.parameter.index) - .map(|cmd| cmd.kind) - .unwrap() - { - LatexLabelKind::Definition => unreachable!(), - LatexLabelKind::Reference(source) => source, - } -} - -fn is_included( - table: &latex::SymbolTable, - label: &latex::Label, - source: LatexLabelReferenceSource, -) -> bool { - let label_range = table[label.parent].range(); - match source { - LatexLabelReferenceSource::Everything => true, - LatexLabelReferenceSource::Math => table - .environments - .iter() - .filter(|env| env.left.is_math(&table)) - .any(|env| env.range(&table).contains_exclusive(label_range.start)), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_labels(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_labels(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_of_ref() { - let req = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \addbibresource{bar.bib} - \include{baz} - \ref{} - "# - ), - ) - .file("bar.bib", "") - .file("baz.tex", r#"\label{foo}\label{bar}\ref{baz}"#) - .main("foo.tex") - .position(2, 5) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_labels(&req, &mut actual_items).await; - - let actual_labels: Vec<_> = actual_items - .into_iter() - .map(|item| item.data.label().to_owned()) - .collect(); - assert_eq!(actual_labels, vec!["foo", "bar"]); - } - - #[tokio::test] - async fn outside_of_ref() { - let req = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \include{bar} - \ref{} - "# - ), - ) - .file("bar.tex", r#"\label{foo}\label{bar}"#) - .main("foo.tex") - .position(1, 6) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_labels(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn eqref() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \begin{align}\label{foo}\end{align}\label{bar} - \eqref{} - "# - ), - ) - .main("main.tex") - .position(1, 7) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_labels(&req, &mut actual_items).await; - - let actual_labels: Vec<_> = actual_items - .into_iter() - .map(|item| item.data.label().to_owned()) - .collect(); - - assert_eq!(actual_labels, vec!["foo"]); - } -} diff --git a/support/texlab/src/completion/latex/mod.rs b/support/texlab/src/completion/latex/mod.rs deleted file mode 100644 index 519d00e5f7..0000000000 --- a/support/texlab/src/completion/latex/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod argument; -pub mod begin_cmd; -pub mod citation; -pub mod color; -pub mod color_model; -mod combinators; -pub mod component; -pub mod glossary; -pub mod import; -pub mod include; -pub mod label; -pub mod theorem; -pub mod tikz_lib; -pub mod user; diff --git a/support/texlab/src/completion/latex/theorem.rs b/support/texlab/src/completion/latex/theorem.rs deleted file mode 100644 index a8706bed0b..0000000000 --- a/support/texlab/src/completion/latex/theorem.rs +++ /dev/null @@ -1,115 +0,0 @@ -use super::combinators; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::CompletionParams, -}; - -pub async fn complete_latex_theorem_environments<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - combinators::environment(req, |ctx| async move { - for table in req - .related() - .into_iter() - .filter_map(|doc| doc.content.as_latex()) - { - for theorem in &table.theorem_definitions { - let name = theorem.name(&table).text(); - let data = ItemData::UserEnvironment { name }; - let item = Item::new(ctx.range, data); - items.push(item); - } - } - }) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_theorem_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_theorem_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn inside_begin() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \newtheorem{theorem}{Theorem} - \begin{th} - "# - ), - ) - .main("main.tex") - .position(1, 8) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_theorem_environments(&req, &mut actual_items).await; - - assert_eq!(actual_items.len(), 1); - assert_eq!(actual_items[0].data.label(), "theorem"); - assert_eq!(actual_items[0].range, Range::new_simple(1, 7, 1, 9)); - } - - #[tokio::test] - async fn outside_begin() { - let req = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \newtheorem{theorem}{Theorem} - \begin{th} - "# - ), - ) - .main("main.tex") - .position(1, 10) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_theorem_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } -} diff --git a/support/texlab/src/completion/latex/tikz_lib.rs b/support/texlab/src/completion/latex/tikz_lib.rs deleted file mode 100644 index fb51fd7229..0000000000 --- a/support/texlab/src/completion/latex/tikz_lib.rs +++ /dev/null @@ -1,138 +0,0 @@ -use super::combinators::{self, Parameter}; -use crate::{ - completion::types::{Item, ItemData}, - feature::FeatureRequest, - protocol::CompletionParams, - syntax::LANGUAGE_DATA, -}; -use std::iter; - -pub async fn complete_latex_pgf_libraries<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - let param = Parameter { - name: "usepgflibrary", - index: 0, - }; - combinators::argument(req, iter::once(param), |ctx| async move { - for name in &LANGUAGE_DATA.pgf_libraries { - let item = Item::new(ctx.range, ItemData::PgfLibrary { name }); - items.push(item); - } - }) - .await; -} - -pub async fn complete_latex_tikz_libraries<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - let param = Parameter { - name: "usetikzlibrary", - index: 0, - }; - combinators::argument(req, iter::once(param), |ctx| async move { - for name in &LANGUAGE_DATA.tikz_libraries { - let item = Item::new(ctx.range, ItemData::TikzLibrary { name }); - items.push(item); - } - }) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - - #[tokio::test] - async fn empty_latex_document_pgf() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_pgf_libraries(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_pgf() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_pgf_libraries(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_latex_document_tikz() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_tikz_libraries(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_tikz() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_tikz_libraries(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn pgf_library() { - let req = FeatureTester::new() - .file("main.tex", r#"\usepgflibrary{}"#) - .main("main.tex") - .position(0, 15) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_pgf_libraries(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - } - - #[tokio::test] - async fn tikz_library() { - let req = FeatureTester::new() - .file("main.tex", r#"\usetikzlibrary{}"#) - .main("main.tex") - .position(0, 16) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_tikz_libraries(&req, &mut actual_items).await; - - assert!(!actual_items.is_empty()); - } -} diff --git a/support/texlab/src/completion/latex/user.rs b/support/texlab/src/completion/latex/user.rs deleted file mode 100644 index 0a5706958a..0000000000 --- a/support/texlab/src/completion/latex/user.rs +++ /dev/null @@ -1,204 +0,0 @@ -use super::combinators; -use crate::{ - completion::{Item, ItemData}, - feature::FeatureRequest, - protocol::{CompletionParams, Range}, - syntax::latex, - workspace::DocumentContent, -}; - -pub async fn complete_latex_user_commands<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - combinators::command(req, |current_cmd_node| async move { - let current_cmd = req - .current() - .content - .as_latex() - .unwrap() - .as_command(current_cmd_node) - .unwrap(); - - for table in req - .related() - .into_iter() - .flat_map(|doc| doc.content.as_latex()) - { - table - .commands - .iter() - .filter(|cmd_node| **cmd_node != current_cmd_node) - .map(|cmd_node| { - let name = &table.as_command(*cmd_node).unwrap().name.text()[1..]; - Item::new( - current_cmd.short_name_range(), - ItemData::UserCommand { name }, - ) - }) - .for_each(|item| items.push(item)); - } - }) - .await; -} - -pub async fn complete_latex_user_environments<'a>( - req: &'a FeatureRequest<CompletionParams>, - items: &mut Vec<Item<'a>>, -) { - fn make_item( - table: &latex::SymbolTable, - delim: latex::EnvironmentDelimiter, - name_range: Range, - ) -> Option<Item> { - delim - .name(&table) - .map(|name| Item::new(name_range, ItemData::UserEnvironment { name: &name.text() })) - } - - combinators::environment(req, |ctx| async move { - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - for env in &table.environments { - if (env.left.parent == ctx.node || env.right.parent == ctx.node) - && doc.uri == req.current().uri - { - continue; - } - - if let Some(item) = make_item(&table, env.left, ctx.range) { - items.push(item); - } - - if let Some(item) = make_item(&table, env.right, ctx.range) { - items.push(item); - } - } - } - } - }) - .await; -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - use indoc::indoc; - use itertools::Itertools; - - #[tokio::test] - async fn empty_latex_document_command() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - complete_latex_user_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_command() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - complete_latex_user_commands(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_latex_document_environment() { - let req = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - complete_latex_user_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_environment() { - let req = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - complete_latex_user_environments(&req, &mut actual_items).await; - - assert!(actual_items.is_empty()); - } - - #[tokio::test] - async fn command() { - let req = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \include{bar} - \foo - "# - ), - ) - .file("bar.tex", r#"\bar"#) - .file("baz.tex", r#"\baz"#) - .main("foo.tex") - .position(1, 2) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_user_commands(&req, &mut actual_items).await; - - let actual_labels: Vec<_> = actual_items - .into_iter() - .map(|item| item.data.label().to_owned()) - .collect(); - assert_eq!(actual_labels, vec!["include", "bar"]); - } - - #[tokio::test] - async fn environment() { - let req = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \include{bar} - \begin{foo} - "# - ), - ) - .file("bar.tex", r#"\begin{bar}\end{bar}"#) - .file("baz.tex", r#"\begin{baz}\end{baz}"#) - .main("foo.tex") - .position(1, 9) - .test_completion_request() - .await; - let mut actual_items = Vec::new(); - - complete_latex_user_environments(&req, &mut actual_items).await; - - let actual_labels: Vec<_> = actual_items - .into_iter() - .map(|item| item.data.label().to_owned()) - .unique() - .collect(); - assert_eq!(actual_labels, vec!["bar"]); - } -} diff --git a/support/texlab/src/completion/mod.rs b/support/texlab/src/completion/mod.rs deleted file mode 100644 index d354ed1d98..0000000000 --- a/support/texlab/src/completion/mod.rs +++ /dev/null @@ -1,419 +0,0 @@ -mod bibtex; -mod latex; -mod types; -mod util; - -pub use self::types::{CompletionItemData, Item, ItemData}; - -use self::{ - bibtex::{ - cmd::complete_bibtex_commands, entry_type::complete_bibtex_entry_types, - field_name::complete_bibtex_fields, - }, - latex::{ - argument::complete_latex_arguments, - begin_cmd::complete_latex_begin_command, - citation::complete_latex_citations, - color::complete_latex_colors, - color_model::complete_latex_color_models, - component::{complete_latex_component_commands, complete_latex_component_environments}, - glossary::complete_latex_glossary_entries, - import::{complete_latex_classes, complete_latex_packages}, - include::complete_latex_includes, - label::complete_latex_labels, - theorem::complete_latex_theorem_environments, - tikz_lib::{complete_latex_pgf_libraries, complete_latex_tikz_libraries}, - user::{complete_latex_user_commands, complete_latex_user_environments}, - }, - util::{adjust_kind, component_detail, current_word, image_documentation}, -}; -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - CompletionItem, CompletionParams, CompletionTextEdit, Documentation, InsertTextFormat, - MarkupContent, MarkupKind, RangeExt, TextEdit, - }, - syntax::{self, Structure, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use fuzzy_matcher::FuzzyMatcher; -use fuzzy_matcher::skim::SkimMatcherV2; -use std::collections::HashSet; - -pub const COMPLETION_LIMIT: usize = 50; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct CompletionProvider; - -impl CompletionProvider { - pub fn new() -> Self { - Self - } -} - -#[async_trait] -impl FeatureProvider for CompletionProvider { - type Params = CompletionParams; - type Output = Vec<CompletionItem>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let items = complete_all(req).await; - let mut items = dedup(items); - preselect(req, &mut items); - score(req, &mut items); - - items.sort_by_key(|item| (!item.preselect, -item.score.unwrap_or(std::i64::MIN + 1))); - items - .into_iter() - .take(COMPLETION_LIMIT) - .filter(|item| item.score.is_some()) - .map(|item| convert(req, item)) - .enumerate() - .map(|(i, item)| append_sort_text(item, i)) - .collect() - } -} - -async fn complete_all<'a>(req: &'a FeatureRequest<CompletionParams>) -> Vec<Item<'a>> { - let mut items = Vec::new(); - complete_bibtex_commands(req, &mut items).await; - complete_bibtex_entry_types(req, &mut items).await; - complete_bibtex_fields(req, &mut items).await; - complete_latex_arguments(req, &mut items).await; - complete_latex_begin_command(req, &mut items).await; - complete_latex_colors(req, &mut items).await; - complete_latex_color_models(req, &mut items).await; - complete_latex_glossary_entries(req, &mut items).await; - complete_latex_citations(req, &mut items).await; - complete_latex_classes(req, &mut items).await; - complete_latex_packages(req, &mut items).await; - complete_latex_includes(req, &mut items).await; - complete_latex_labels(req, &mut items).await; - complete_latex_pgf_libraries(req, &mut items).await; - complete_latex_tikz_libraries(req, &mut items).await; - complete_latex_component_environments(req, &mut items).await; - complete_latex_theorem_environments(req, &mut items).await; - complete_latex_user_environments(req, &mut items).await; - complete_latex_component_commands(req, &mut items).await; - complete_latex_user_commands(req, &mut items).await; - items -} - -fn dedup<'a>(items: Vec<Item<'a>>) -> Vec<Item<'a>> { - let mut labels = HashSet::new(); - let mut insert = vec![false; items.len()]; - for (i, item) in items.iter().enumerate() { - insert[i] = labels.insert(item.data.label()); - } - items - .into_iter() - .enumerate() - .filter(|(i, _)| insert[*i]) - .map(|(_, item)| item) - .collect() -} - -fn preselect(req: &FeatureRequest<CompletionParams>, items: &mut [Item]) { - let pos = req.params.text_document_position.position; - if let DocumentContent::Latex(table) = &req.current().content { - for env in &table.environments { - if let Some(name) = env.left.name(&table) { - let right_args = table - .extract_group(env.right.parent, syntax::latex::GroupKind::Group, 0) - .unwrap(); - let right_args_range = table[right_args].range(); - let cond1 = right_args_range.contains_exclusive(pos); - let cond2 = table - .as_group(right_args) - .and_then(|group| group.right.as_ref()) - .is_none() - && right_args_range.contains(pos); - - if cond1 || cond2 { - for symbol in items.iter_mut() { - if symbol.data.label() == name.text() { - symbol.preselect = true; - break; - } - } - } - } - } - } -} - -fn score(req: &FeatureRequest<CompletionParams>, items: &mut Vec<Item>) { - let current_word = current_word(req); - let pattern = current_word.as_deref().unwrap_or_default(); - let matcher = SkimMatcherV2::default(); - for item in items { - item.score = match &item.data { - ItemData::ComponentCommand { name, .. } => matcher.fuzzy_match(name, pattern), - ItemData::ComponentEnvironment { name, .. } => matcher.fuzzy_match(name, pattern), - ItemData::UserCommand { name } => matcher.fuzzy_match(name, pattern), - ItemData::UserEnvironment { name } => matcher.fuzzy_match(name, pattern), - ItemData::Label { text, .. } => matcher.fuzzy_match(&text, pattern), - ItemData::Class { name } => matcher.fuzzy_match(&name, pattern), - ItemData::Package { name } => matcher.fuzzy_match(&name, pattern), - ItemData::PgfLibrary { name } => matcher.fuzzy_match(name, pattern), - ItemData::TikzLibrary { name } => matcher.fuzzy_match(name, pattern), - ItemData::File { name } => matcher.fuzzy_match(name, pattern), - ItemData::Directory { name } => matcher.fuzzy_match(name, pattern), - ItemData::Citation { text, .. } => matcher.fuzzy_match(&text, pattern), - ItemData::Argument { name, .. } => matcher.fuzzy_match(&name, pattern), - ItemData::BeginCommand => matcher.fuzzy_match("begin", pattern), - ItemData::Color { name } => matcher.fuzzy_match(name, pattern), - ItemData::ColorModel { name } => matcher.fuzzy_match(name, pattern), - ItemData::GlossaryEntry { name } => matcher.fuzzy_match(name, pattern), - ItemData::EntryType { ty } => matcher.fuzzy_match(&ty.name, pattern), - ItemData::Field { field } => matcher.fuzzy_match(&field.name, pattern), - }; - } -} - -fn convert(req: &FeatureRequest<CompletionParams>, item: Item) -> CompletionItem { - let mut new_item = match item.data { - ItemData::ComponentCommand { - name, - image, - glyph, - file_names, - } => { - let detail = glyph.map_or_else( - || component_detail(file_names), - |glyph| format!("{}, {}", glyph, component_detail(file_names)), - ); - let documentation = image.and_then(|img| image_documentation(&req, &name, img)); - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - kind: Some(adjust_kind(req, Structure::Command.completion_kind())), - data: Some(CompletionItemData::Command.into()), - documentation, - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::new_simple(name.into(), detail) - } - } - ItemData::ComponentEnvironment { name, file_names } => { - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - kind: Some(adjust_kind(req, Structure::Environment.completion_kind())), - data: Some(CompletionItemData::Environment.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::new_simple(name.into(), component_detail(file_names)) - } - } - ItemData::UserCommand { name } => { - let detail = "user-defined".into(); - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - kind: Some(adjust_kind(req, Structure::Command.completion_kind())), - data: Some(CompletionItemData::Command.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::new_simple(name.into(), detail) - } - } - ItemData::UserEnvironment { name } => { - let detail = "user-defined".into(); - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - kind: Some(adjust_kind(req, Structure::Environment.completion_kind())), - data: Some(CompletionItemData::Environment.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::new_simple(name.into(), detail) - } - } - ItemData::Label { - name, - kind, - header, - footer, - text, - } => { - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - label: name.into(), - kind: Some(adjust_kind(req, kind.completion_kind())), - data: Some(CompletionItemData::Label.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - filter_text: Some(text.clone()), - sort_text: Some(text), - detail: header, - documentation: footer.map(Documentation::String), - ..CompletionItem::default() - } - } - ItemData::Class { name } => { - let text_edit = TextEdit::new(item.range, name.as_ref().into()); - CompletionItem { - label: name.into_owned(), - kind: Some(adjust_kind(req, Structure::Class.completion_kind())), - data: Some(CompletionItemData::Class.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::Package { name } => { - let text_edit = TextEdit::new(item.range, name.as_ref().into()); - CompletionItem { - label: name.into_owned(), - kind: Some(adjust_kind(req, Structure::Package.completion_kind())), - data: Some(CompletionItemData::Package.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::PgfLibrary { name } => { - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - label: name.into(), - kind: Some(adjust_kind(req, Structure::PgfLibrary.completion_kind())), - data: Some(CompletionItemData::PgfLibrary.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::TikzLibrary { name } => { - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - label: name.into(), - kind: Some(adjust_kind(req, Structure::TikzLibrary.completion_kind())), - data: Some(CompletionItemData::TikzLibrary.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::File { name } => { - let text_edit = TextEdit::new(item.range, name.clone()); - CompletionItem { - label: name, - kind: Some(adjust_kind(req, Structure::File.completion_kind())), - data: Some(CompletionItemData::File.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::Directory { name } => { - let text_edit = TextEdit::new(item.range, name.clone()); - CompletionItem { - label: name, - kind: Some(adjust_kind(req, Structure::Folder.completion_kind())), - data: Some(CompletionItemData::Folder.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::Citation { uri, key, text, ty } => { - let text_edit = TextEdit::new(item.range, key.into()); - CompletionItem { - label: key.into(), - kind: Some(adjust_kind(req, ty.completion_kind())), - filter_text: Some(text.clone()), - sort_text: Some(text), - data: Some( - CompletionItemData::Citation { - uri: uri.clone(), - key: key.into(), - } - .into(), - ), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::Argument { name, image } => { - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - label: name.into(), - kind: Some(adjust_kind(req, Structure::Argument.completion_kind())), - data: Some(CompletionItemData::Argument.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - documentation: image.and_then(|image| image_documentation(&req, &name, image)), - ..CompletionItem::default() - } - } - ItemData::BeginCommand => CompletionItem { - kind: Some(adjust_kind(req, Structure::Snippet.completion_kind())), - data: Some(CompletionItemData::CommandSnippet.into()), - insert_text: Some("begin{$1}\n\t$0\n\\end{$1}".into()), - insert_text_format: Some(InsertTextFormat::Snippet), - ..CompletionItem::new_simple("begin".into(), component_detail(&[])) - }, - ItemData::Color { name } => { - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - label: name.into(), - kind: Some(adjust_kind(req, Structure::Color.completion_kind())), - data: Some(CompletionItemData::Color.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::ColorModel { name } => { - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - label: name.into(), - kind: Some(adjust_kind(req, Structure::ColorModel.completion_kind())), - data: Some(CompletionItemData::ColorModel.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::GlossaryEntry { name } => { - let text_edit = TextEdit::new(item.range, name.into()); - CompletionItem { - label: name.into(), - kind: Some(adjust_kind(req, Structure::GlossaryEntry.completion_kind())), - data: Some(CompletionItemData::GlossaryEntry.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - ..CompletionItem::default() - } - } - ItemData::EntryType { ty } => { - let text_edit = TextEdit::new(item.range, (&ty.name).into()); - let kind = Structure::Entry(ty.category).completion_kind(); - CompletionItem { - label: (&ty.name).into(), - kind: Some(adjust_kind(req, kind)), - data: Some(CompletionItemData::EntryType.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - documentation: ty.documentation.as_ref().map(|doc| { - Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: doc.into(), - }) - }), - ..CompletionItem::default() - } - } - ItemData::Field { field } => { - let text_edit = TextEdit::new(item.range, (&field.name).into()); - CompletionItem { - label: (&field.name).into(), - kind: Some(adjust_kind(req, Structure::Field.completion_kind())), - data: Some(CompletionItemData::FieldName.into()), - text_edit: Some(CompletionTextEdit::Edit(text_edit)), - documentation: Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: (&field.documentation).into(), - })), - ..CompletionItem::default() - } - } - }; - new_item.preselect = Some(item.preselect); - new_item -} - -fn append_sort_text(mut item: CompletionItem, index: usize) -> CompletionItem { - let sort_prefix = format!("{:0>2}", index); - match &item.sort_text { - Some(sort_text) => { - item.sort_text = Some(format!("{} {}", sort_prefix, sort_text)); - } - None => { - item.sort_text = Some(sort_prefix); - } - }; - item -} diff --git a/support/texlab/src/completion/util.rs b/support/texlab/src/completion/util.rs deleted file mode 100644 index ec3c8eb406..0000000000 --- a/support/texlab/src/completion/util.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::{ - feature::FeatureRequest, - protocol::{ - CompletionItemKind, CompletionParams, Documentation, MarkupContent, MarkupKind, Position, - RangeExt, - }, - syntax::{bibtex, latex, SyntaxNode}, - workspace::DocumentContent, -}; -use std::borrow::Cow; - -pub fn current_word(req: &FeatureRequest<CompletionParams>) -> Option<Cow<str>> { - let pos = req.params.text_document_position.position; - match &req.current().content { - DocumentContent::Latex(table) => { - if let Some(node) = table.find_command_by_short_name_range(pos) { - return Some(command_word(table.as_command(node).unwrap())); - } - - match &table[table.find(pos).into_iter().last()?] { - latex::Node::Root(_) | latex::Node::Group(_) => Some("".into()), - latex::Node::Command(cmd) => Some(command_word(cmd)), - latex::Node::Text(text) => text - .words - .iter() - .find(|word| word.range().contains(pos)) - .map(|word| word.text().split('/').last().unwrap().to_owned().into()), - latex::Node::Comma(_) => Some(",".into()), - latex::Node::Math(math) => Some(math.token.text().to_owned().into()), - } - } - DocumentContent::Bibtex(tree) => { - fn type_query(ty: &bibtex::Token, pos: Position) -> Option<Cow<str>> { - if ty.range().contains(pos) { - Some((&ty.text()[1..]).into()) - } else { - Some("".into()) - } - } - - match &tree.graph[tree.find(pos).pop()?] { - bibtex::Node::Root(_) => Some("".into()), - bibtex::Node::Preamble(preamble) => type_query(&preamble.ty, pos), - bibtex::Node::String(string) => type_query(&string.ty, pos), - bibtex::Node::Entry(entry) => type_query(&entry.ty, pos), - bibtex::Node::Comment(comment) => Some(comment.token.text().into()), - bibtex::Node::Field(field) => { - if field.name.range().contains(pos) { - Some(field.name.text().into()) - } else { - Some("".into()) - } - } - bibtex::Node::Word(word) => Some(word.token.text().into()), - bibtex::Node::Command(cmd) => Some((&cmd.token.text()[1..]).into()), - bibtex::Node::QuotedContent(_) - | bibtex::Node::BracedContent(_) - | bibtex::Node::Concat(_) => Some("".into()), - } - } - } -} - -fn command_word(cmd: &latex::Command) -> Cow<str> { - cmd.name.text()[1..].into() -} - -pub fn component_detail(file_names: &[String]) -> String { - if file_names.is_empty() { - "built-in".to_owned() - } else { - file_names.join(", ") - } -} - -pub fn image_documentation( - req: &FeatureRequest<CompletionParams>, - name: &str, - image: &str, -) -> Option<Documentation> { - if supports_images(req) { - Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: format!( - "![{}](data:image/png;base64,{}|width=48,height=48)", - name, image - ), - })) - } else { - None - } -} - -fn supports_images(req: &FeatureRequest<CompletionParams>) -> bool { - req.client_capabilities - .text_document - .as_ref() - .and_then(|cap| cap.completion.as_ref()) - .and_then(|cap| cap.completion_item.as_ref()) - .and_then(|cap| cap.documentation_format.as_ref()) - .map_or(true, |formats| formats.contains(&MarkupKind::Markdown)) -} - -pub fn adjust_kind( - req: &FeatureRequest<CompletionParams>, - kind: CompletionItemKind, -) -> CompletionItemKind { - if let Some(value_set) = req - .client_capabilities - .text_document - .as_ref() - .and_then(|cap| cap.completion.as_ref()) - .and_then(|cap| cap.completion_item_kind.as_ref()) - .and_then(|cap| cap.value_set.as_ref()) - { - if value_set.contains(&kind) { - return kind; - } - } - CompletionItemKind::Text -} diff --git a/support/texlab/src/components.rs b/support/texlab/src/component_db.rs index 76890c8b15..a0c728a7ae 100644 --- a/support/texlab/src/components.rs +++ b/support/texlab/src/component_db.rs @@ -1,15 +1,19 @@ -use crate::protocol::{MarkupContent, MarkupKind}; +use itertools::Itertools; +use lsp_types::{MarkupContent, MarkupKind}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; + +use crate::WorkspaceSubset; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Database { +pub struct ComponentDatabase { pub components: Vec<Component>, - pub metadata: Vec<Metadata>, + pub metadata: Vec<ComponentMetadata>, } -impl Database { +impl ComponentDatabase { pub fn find(&self, name: &str) -> Option<&Component> { self.components.iter().find(|component| { component @@ -19,6 +23,44 @@ impl Database { }) } + pub fn find_no_ext(&self, name: &str) -> Option<&Component> { + self.components.iter().find(|component| { + component + .file_names + .iter() + .any(|file_name| &file_name[0..file_name.len() - 4] == name) + }) + } + + pub fn linked_components<'a>(&self, subset: &WorkspaceSubset) -> Vec<&Component> { + let mut start_components = vec![self.kernel()]; + for document in &subset.documents { + if let Some(data) = document.data.as_latex() { + data.extras + .explicit_links + .iter() + .filter_map(|link| link.as_component_name()) + .filter_map(|name| self.find(&name)) + .for_each(|component| start_components.push(component)); + } + } + + let mut all_components = Vec::new(); + for component in start_components { + all_components.push(component); + component + .references + .iter() + .flat_map(|file| self.find(&file)) + .for_each(|component| all_components.push(component)) + } + + all_components + .into_iter() + .unique_by(|component| &component.file_names) + .collect() + } + pub fn contains(&self, short_name: &str) -> bool { let sty = format!("{}.sty", short_name); let cls = format!("{}.cls", short_name); @@ -55,35 +97,35 @@ impl Database { #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Component { - pub file_names: Vec<String>, - pub references: Vec<String>, - pub commands: Vec<Command>, - pub environments: Vec<String>, + pub file_names: Vec<SmolStr>, + pub references: Vec<SmolStr>, + pub commands: Vec<ComponentCommand>, + pub environments: Vec<SmolStr>, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Command { - pub name: String, +pub struct ComponentCommand { + pub name: SmolStr, pub image: Option<String>, - pub glyph: Option<String>, - pub parameters: Vec<Parameter>, + pub glyph: Option<SmolStr>, + pub parameters: Vec<ComponentParameter>, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Parameter(pub Vec<Argument>); +pub struct ComponentParameter(pub Vec<ComponentArgument>); #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Argument { - pub name: String, +pub struct ComponentArgument { + pub name: SmolStr, pub image: Option<String>, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Metadata { +pub struct ComponentMetadata { pub name: String, pub caption: Option<String>, pub description: Option<String>, @@ -91,4 +133,5 @@ pub struct Metadata { const JSON: &str = include_str!("../data/components.json"); -pub static COMPONENT_DATABASE: Lazy<Database> = Lazy::new(|| serde_json::from_str(JSON).unwrap()); +pub static COMPONENT_DATABASE: Lazy<ComponentDatabase> = + Lazy::new(|| serde_json::from_str(JSON).unwrap()); diff --git a/support/texlab/src/config.rs b/support/texlab/src/config.rs index 84de2268fa..5690bee091 100644 --- a/support/texlab/src/config.rs +++ b/support/texlab/src/config.rs @@ -1,103 +1,90 @@ -use crate::protocol::*; -use futures::lock::Mutex; -use log::{error, warn}; -use serde::de::DeserializeOwned; -use std::sync::Arc; +use std::sync::{Mutex, RwLock}; -#[derive(Debug)] -pub struct ConfigManager<C> { - client: Arc<C>, - client_capabilities: Arc<ClientCapabilities>, - options: Mutex<Options>, -} +use crossbeam_channel::Sender; +use log::{error, warn}; +use lsp_server::Message; +use lsp_types::{ + notification::{DidChangeConfiguration, Notification}, + request::{RegisterCapability, WorkspaceConfiguration}, + ClientCapabilities, ConfigurationItem, ConfigurationParams, Registration, RegistrationParams, +}; -impl<C: LspClient + Send + Sync + 'static> ConfigManager<C> { - pub fn new(client: Arc<C>, client_capabilities: Arc<ClientCapabilities>) -> Self { - Self { - client, - client_capabilities, - options: Mutex::default(), - } - } +use crate::{client::send_request, req_queue::ReqQueue, ClientCapabilitiesExt, Options}; - pub async fn get(&self) -> Options { - self.options.lock().await.clone() - } +pub fn register_config_capability( + req_queue: &Mutex<ReqQueue>, + sender: &Sender<Message>, + client_capabilities: &Mutex<ClientCapabilities>, +) { + let client_capabilities = client_capabilities.lock().unwrap(); + if !client_capabilities.has_pull_configuration_support() + && client_capabilities.has_push_configuration_support() + { + drop(client_capabilities); + let reg = Registration { + id: "pull-config".to_string(), + method: DidChangeConfiguration::METHOD.to_string(), + register_options: None, + }; - pub async fn register(&self) { - if !self.client_capabilities.has_pull_configuration_support() - && self.client_capabilities.has_push_configuration_support() - { - let registration = Registration { - id: "pull-config".into(), - method: "workspace/didChangeConfiguration".into(), - register_options: None, - }; - let params = RegistrationParams { - registrations: vec![registration], - }; + let params = RegistrationParams { + registrations: vec![reg], + }; - if let Err(why) = self.client.register_capability(params).await { - error!( - "Failed to register \"workspace/didChangeConfiguration\": {}", - why.message - ); - } + if let Err(why) = send_request::<RegisterCapability>(&req_queue, &sender, params) { + error!( + "Failed to register \"{}\" notification: {}", + DidChangeConfiguration::METHOD, + why + ); } } +} - pub async fn push(&self, options: serde_json::Value) { - match serde_json::from_value(options) { - Ok(options) => { - *self.options.lock().await = options; - } - Err(why) => { - error!("Invalid configuration: {}", why); - } - } +pub fn pull_config( + req_queue: &Mutex<ReqQueue>, + sender: &Sender<Message>, + options: &RwLock<Options>, + client_capabilities: &ClientCapabilities, +) { + if !client_capabilities.has_pull_configuration_support() { + return; } - pub async fn pull(&self) -> bool { - if self.client_capabilities.has_pull_configuration_support() { - let latex = self.pull_section("latex").await; - let bibtex = self.pull_section("bibtex").await; + let params = ConfigurationParams { + items: vec![ConfigurationItem { + section: Some("texlab".to_string()), + scope_uri: None, + }], + }; - let new_options = Options { - latex: Some(latex), - bibtex: Some(bibtex), + match send_request::<WorkspaceConfiguration>(req_queue, sender, params) { + Ok(mut json) => { + let value = json.pop().expect("invalid configuration request"); + let new_options = match serde_json::from_value(value) { + Ok(new_options) => new_options, + Err(why) => { + warn!("Invalid configuration section \"texlab\": {}", why); + Options::default() + } }; - let mut old_options = self.options.lock().await; - let has_changed = *old_options != new_options; - *old_options = new_options; - has_changed - } else { - false - } - } - async fn pull_section<T: DeserializeOwned + Default>(&self, section: &str) -> T { - let params = ConfigurationParams { - items: vec![ConfigurationItem { - section: Some(section.into()), - scope_uri: None, - }], - }; + let mut options = options.write().unwrap(); + *options = new_options; + } + Err(why) => { + error!("Retrieving configuration failed: {}", why); + } + }; +} - match self.client.configuration(params).await { - Ok(json) => match serde_json::from_value::<Vec<T>>(json) { - Ok(config) => config.into_iter().next().unwrap(), - Err(_) => { - warn!("Invalid configuration: {}", section); - T::default() - } - }, - Err(why) => { - error!( - "Retrieving configuration for {} failed: {}", - section, why.message - ); - T::default() - } +pub fn push_config(options: &RwLock<Options>, config: serde_json::Value) { + match serde_json::from_value(config) { + Ok(new_options) => { + *options.write().unwrap() = new_options; } - } + Err(why) => { + error!("Invalid configuration: {}", why); + } + }; } diff --git a/support/texlab/src/context.rs b/support/texlab/src/context.rs new file mode 100644 index 0000000000..a9d6f530a6 --- /dev/null +++ b/support/texlab/src/context.rs @@ -0,0 +1,34 @@ +use std::{ + path::PathBuf, + sync::{Mutex, RwLock}, +}; + +use lsp_types::{ClientCapabilities, ClientInfo}; + +use crate::{ + distro::{DistributionKind, Resolver}, + Options, +}; + +#[derive(Debug)] +pub struct ServerContext { + pub current_directory: PathBuf, + pub distro_kind: Mutex<DistributionKind>, + pub resolver: Mutex<Resolver>, + pub client_capabilities: Mutex<ClientCapabilities>, + pub client_info: Mutex<Option<ClientInfo>>, + pub options: RwLock<Options>, +} + +impl ServerContext { + pub fn new(current_dir: PathBuf) -> Self { + Self { + current_directory: current_dir, + distro_kind: Mutex::new(DistributionKind::Unknown), + resolver: Mutex::new(Resolver::default()), + client_capabilities: Mutex::default(), + client_info: Mutex::default(), + options: RwLock::default(), + } + } +} diff --git a/support/texlab/src/definition/bibtex_string.rs b/support/texlab/src/definition/bibtex_string.rs deleted file mode 100644 index a1fe5132c0..0000000000 --- a/support/texlab/src/definition/bibtex_string.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{LocationLink, Position, TextDocumentPositionParams, Uri}, - syntax::{bibtex, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexStringDefinitionProvider; - -#[async_trait] -impl FeatureProvider for BibtexStringDefinitionProvider { - type Params = TextDocumentPositionParams; - type Output = Vec<LocationLink>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - if let DocumentContent::Bibtex(tree) = &req.current().content { - if let Some(reference) = Self::find_reference(tree, req.params.position) { - return Self::find_definitions(&req.current().uri, tree, reference); - } - } - Vec::new() - } -} - -impl BibtexStringDefinitionProvider { - fn find_reference(tree: &bibtex::Tree, pos: Position) -> Option<&bibtex::Token> { - let mut nodes = tree.find(pos); - nodes.reverse(); - match ( - &tree.graph[nodes[0]], - nodes.get(1).map(|node| &tree.graph[*node]), - ) { - (bibtex::Node::Word(word), Some(bibtex::Node::Field(_))) - | (bibtex::Node::Word(word), Some(bibtex::Node::Concat(_))) => Some(&word.token), - _ => None, - } - } - - fn find_definitions( - uri: &Uri, - tree: &bibtex::Tree, - reference: &bibtex::Token, - ) -> Vec<LocationLink> { - let mut links = Vec::new(); - for node in tree.children(tree.root) { - if let Some(string) = tree.as_string(node) { - if let Some(name) = &string.name { - if name.text() == reference.text() { - links.push(LocationLink { - origin_selection_range: Some(reference.range()), - target_uri: uri.clone().into(), - target_range: string.range(), - target_selection_range: name.range(), - }); - } - } - } - } - links - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let actual_links = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(BibtexStringDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_links = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(BibtexStringDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn simple() { - let actual_links = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = {bar}} - @article{bar, author = foo} - "# - ), - ) - .main("main.bib") - .position(1, 24) - .test_position(BibtexStringDefinitionProvider) - .await; - - let expected_links = vec![LocationLink { - origin_selection_range: Some(Range::new_simple(1, 23, 1, 26)), - target_uri: FeatureTester::uri("main.bib").into(), - target_range: Range::new_simple(0, 0, 0, 20), - target_selection_range: Range::new_simple(0, 8, 0, 11), - }]; - - assert_eq!(actual_links, expected_links); - } - - #[tokio::test] - async fn concat() { - let actual_links = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = {bar}} - @article{bar, author = foo # "bar"} - "# - ), - ) - .main("main.bib") - .position(1, 24) - .test_position(BibtexStringDefinitionProvider) - .await; - - let expected_links = vec![LocationLink { - origin_selection_range: Some(Range::new_simple(1, 23, 1, 26)), - target_uri: FeatureTester::uri("main.bib").into(), - target_range: Range::new_simple(0, 0, 0, 20), - target_selection_range: Range::new_simple(0, 8, 0, 11), - }]; - - assert_eq!(actual_links, expected_links); - } - - #[tokio::test] - async fn field() { - let actual_links = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = {bar}} - @article{bar, author = foo} - "# - ), - ) - .main("main.bib") - .position(1, 18) - .test_position(BibtexStringDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } -} diff --git a/support/texlab/src/definition/latex_citation.rs b/support/texlab/src/definition/latex_citation.rs deleted file mode 100644 index aa93e25fc0..0000000000 --- a/support/texlab/src/definition/latex_citation.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{LocationLink, RangeExt, TextDocumentPositionParams}, - syntax::{latex, SyntaxNode}, - workspace::{Document, DocumentContent}, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct LatexCitationDefinitionProvider; - -#[async_trait] -impl FeatureProvider for LatexCitationDefinitionProvider { - type Params = TextDocumentPositionParams; - type Output = Vec<LocationLink>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut links = Vec::new(); - if let Some(reference) = Self::find_reference(req) { - for doc in req.related() { - Self::find_definitions(&doc, &reference, &mut links); - } - } - links - } -} - -impl LatexCitationDefinitionProvider { - fn find_reference(req: &FeatureRequest<TextDocumentPositionParams>) -> Option<&latex::Token> { - req.current().content.as_latex().and_then(|table| { - table - .citations - .iter() - .flat_map(|citation| citation.keys(&table)) - .find(|key| key.range().contains(req.params.position)) - }) - } - - fn find_definitions(doc: &Document, reference: &latex::Token, links: &mut Vec<LocationLink>) { - if let DocumentContent::Bibtex(tree) = &doc.content { - for entry in tree - .children(tree.root) - .filter_map(|node| tree.as_entry(node)) - { - if let Some(key) = &entry.key { - if key.text() == reference.text() { - links.push(LocationLink { - origin_selection_range: Some(reference.range()), - target_uri: doc.uri.clone().into(), - target_range: entry.range(), - target_selection_range: key.range(), - }); - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let actual_links = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(LatexCitationDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_links = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(LatexCitationDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn has_definition() { - let actual_links = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \addbibresource{baz.bib} - \cite{foo} - "# - ), - ) - .file("bar.bib", r#"@article{foo, bar = {baz}}"#) - .file("baz.bib", r#"@article{foo, bar = {baz}}"#) - .main("foo.tex") - .position(1, 6) - .test_position(LatexCitationDefinitionProvider) - .await; - - let exepcted_links = vec![LocationLink { - origin_selection_range: Some(Range::new_simple(1, 6, 1, 9)), - target_uri: FeatureTester::uri("baz.bib").into(), - target_range: Range::new_simple(0, 0, 0, 26), - target_selection_range: Range::new_simple(0, 9, 0, 12), - }]; - - assert_eq!(actual_links, exepcted_links); - } -} diff --git a/support/texlab/src/definition/latex_cmd.rs b/support/texlab/src/definition/latex_cmd.rs deleted file mode 100644 index 35fc42be4f..0000000000 --- a/support/texlab/src/definition/latex_cmd.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{LocationLink, TextDocumentPositionParams}, - syntax::SyntaxNode, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexCommandDefinitionProvider; - -#[async_trait] -impl FeatureProvider for LatexCommandDefinitionProvider { - type Params = TextDocumentPositionParams; - type Output = Vec<LocationLink>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut links = Vec::new(); - if let DocumentContent::Latex(table) = &req.current().content { - if let Some(cmd) = table - .find(req.params.position) - .last() - .and_then(|node| table.as_command(*node)) - { - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - table - .command_definitions - .iter() - .filter(|def| def.definition_name(&table) == cmd.name.text()) - .map(|def| { - let def_range = table[def.parent].range(); - LocationLink { - origin_selection_range: Some(cmd.range()), - target_uri: doc.uri.clone().into(), - target_range: def_range, - target_selection_range: def_range, - } - }) - .for_each(|link| links.push(link)); - - table - .math_operators - .iter() - .filter(|op| op.definition_name(&table) == cmd.name.text()) - .map(|op| { - let def_range = table[op.parent].range(); - LocationLink { - origin_selection_range: Some(cmd.range()), - target_uri: doc.uri.clone().into(), - target_range: def_range, - target_selection_range: def_range, - } - }) - .for_each(|link| links.push(link)); - } - } - } - } - links - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let actual_links = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(LatexCommandDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_links = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(LatexCommandDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn command_definition() { - let actual_links = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \include{bar} - \foo - "# - ), - ) - .file("bar.tex", r#"\newcommand{\foo}{bar}"#) - .file("baz.tex", r#"\newcommand{\foo}{baz}"#) - .main("foo.tex") - .position(1, 3) - .test_position(LatexCommandDefinitionProvider) - .await; - - let expected_links = vec![LocationLink { - origin_selection_range: Some(Range::new_simple(1, 0, 1, 4)), - target_uri: FeatureTester::uri("bar.tex").into(), - target_range: Range::new_simple(0, 0, 0, 22), - target_selection_range: Range::new_simple(0, 0, 0, 22), - }]; - - assert_eq!(actual_links, expected_links); - } - - #[tokio::test] - async fn math_operator() { - let actual_links = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \include{bar} - \foo - "# - ), - ) - .file("bar.tex", r#"\DeclareMathOperator{\foo}{bar}"#) - .file("baz.tex", r#"\DeclareMathOperator{\foo}{baz}"#) - .main("foo.tex") - .position(1, 3) - .test_position(LatexCommandDefinitionProvider) - .await; - - let expected_links = vec![LocationLink { - origin_selection_range: Some(Range::new_simple(1, 0, 1, 4)), - target_uri: FeatureTester::uri("bar.tex").into(), - target_range: Range::new_simple(0, 0, 0, 31), - target_selection_range: Range::new_simple(0, 0, 0, 31), - }]; - - assert_eq!(actual_links, expected_links); - } -} diff --git a/support/texlab/src/definition/latex_label.rs b/support/texlab/src/definition/latex_label.rs deleted file mode 100644 index 5318982ccd..0000000000 --- a/support/texlab/src/definition/latex_label.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::{ - feature::{DocumentView, FeatureProvider, FeatureRequest}, - outline::{Outline, OutlineContext, OutlineContextItem}, - protocol::{LocationLink, Options, RangeExt, TextDocumentPositionParams}, - symbol::build_section_tree, - syntax::{latex, LatexLabelKind, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use std::{path::Path, sync::Arc}; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexLabelDefinitionProvider; - -#[async_trait] -impl FeatureProvider for LatexLabelDefinitionProvider { - type Params = TextDocumentPositionParams; - type Output = Vec<LocationLink>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut links = Vec::new(); - if let Some(reference) = Self::find_reference(req) { - for doc in req.related() { - let snapshot = Arc::clone(&req.view.snapshot); - let view = DocumentView::analyze( - snapshot, - Arc::clone(&doc), - &req.options, - &req.current_dir, - ); - Self::find_definitions( - &view, - &req.options, - &req.current_dir, - &reference, - &mut links, - ); - } - } - links - } -} - -impl LatexLabelDefinitionProvider { - fn find_reference(req: &FeatureRequest<TextDocumentPositionParams>) -> Option<&latex::Token> { - if let DocumentContent::Latex(table) = &req.current().content { - table - .labels - .iter() - .flat_map(|label| label.names(&table)) - .find(|label| label.range().contains(req.params.position)) - } else { - None - } - } - - fn find_definitions( - view: &DocumentView, - options: &Options, - current_dir: &Path, - reference: &latex::Token, - links: &mut Vec<LocationLink>, - ) { - if let DocumentContent::Latex(table) = &view.current.content { - let outline = Outline::analyze(view, options, current_dir); - let section_tree = build_section_tree(view, table, options, current_dir); - for label in &table.labels { - if label.kind == LatexLabelKind::Definition { - let context = OutlineContext::parse(view, &outline, *label); - for name in label.names(&table) { - if name.text() == reference.text() { - let target_range = if let Some(OutlineContextItem::Section { .. }) = - context.as_ref().map(|ctx| &ctx.item) - { - section_tree - .find(reference.text()) - .map(|sec| sec.full_range) - } else { - context.as_ref().map(|ctx| ctx.range) - }; - - links.push(LocationLink { - origin_selection_range: Some(reference.range()), - target_uri: view.current.uri.clone().into(), - target_range: target_range - .unwrap_or_else(|| table[label.parent].range()), - target_selection_range: table[label.parent].range(), - }); - } - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let actual_links = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(LatexLabelDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_links = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(LatexLabelDefinitionProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn unknown_context() { - let actual_links = FeatureTester::new() - .file("foo.tex", r#"\label{foo}"#) - .file( - "bar.tex", - indoc!( - r#" - \begin{a}\begin{b}\label{foo}\end{b}\end{a} - \input{baz.tex} - "# - ), - ) - .file("baz.tex", r#"\ref{foo}"#) - .main("baz.tex") - .position(0, 5) - .test_position(LatexLabelDefinitionProvider) - .await; - - let expected_links = vec![LocationLink { - origin_selection_range: Some(Range::new_simple(0, 5, 0, 8)), - target_uri: FeatureTester::uri("bar.tex").into(), - target_range: Range::new_simple(0, 18, 0, 29), - target_selection_range: Range::new_simple(0, 18, 0, 29), - }]; - - assert_eq!(actual_links, expected_links); - } -} diff --git a/support/texlab/src/definition/mod.rs b/support/texlab/src/definition/mod.rs deleted file mode 100644 index 08901454fe..0000000000 --- a/support/texlab/src/definition/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -mod bibtex_string; -mod latex_citation; -mod latex_cmd; -mod latex_label; - -use self::{ - bibtex_string::BibtexStringDefinitionProvider, latex_citation::LatexCitationDefinitionProvider, - latex_cmd::LatexCommandDefinitionProvider, latex_label::LatexLabelDefinitionProvider, -}; -use crate::{ - feature::{ConcatProvider, FeatureProvider, FeatureRequest}, - protocol::{LocationLink, TextDocumentPositionParams}, -}; -use async_trait::async_trait; - -pub struct DefinitionProvider { - provider: ConcatProvider<TextDocumentPositionParams, LocationLink>, -} - -impl DefinitionProvider { - pub fn new() -> Self { - Self { - provider: ConcatProvider::new(vec![ - Box::new(BibtexStringDefinitionProvider), - Box::new(LatexCitationDefinitionProvider), - Box::new(LatexCommandDefinitionProvider), - Box::new(LatexLabelDefinitionProvider), - ]), - } - } -} - -impl Default for DefinitionProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for DefinitionProvider { - type Params = TextDocumentPositionParams; - type Output = Vec<LocationLink>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - self.provider.execute(req).await - } -} diff --git a/support/texlab/src/diagnostics.rs b/support/texlab/src/diagnostics.rs new file mode 100644 index 0000000000..a4adbafd7d --- /dev/null +++ b/support/texlab/src/diagnostics.rs @@ -0,0 +1,55 @@ +mod bibtex; +mod build_log; +mod chktex; +mod debouncer; +mod latex; + +use std::sync::Arc; + +use lsp_types::Diagnostic; +use multimap::MultiMap; +use rustc_hash::FxHashMap; + +use crate::{Options, Uri, Workspace}; + +pub use self::debouncer::{DiagnosticsDebouncer, DiagnosticsMessage}; + +use self::{ + bibtex::analyze_bibtex_static, build_log::analyze_build_log_static, + chktex::analyze_latex_chktex, latex::analyze_latex_static, +}; + +#[derive(Default)] +pub struct DiagnosticsManager { + static_diagnostics: FxHashMap<Arc<Uri>, MultiMap<Arc<Uri>, Diagnostic>>, + chktex_diagnostics: MultiMap<Arc<Uri>, Diagnostic>, +} + +impl DiagnosticsManager { + pub fn update_static(&mut self, workspace: &dyn Workspace, uri: Arc<Uri>) { + let mut diagnostics_by_uri = MultiMap::new(); + analyze_build_log_static(workspace, &mut diagnostics_by_uri, &uri); + analyze_bibtex_static(workspace, &mut diagnostics_by_uri, &uri); + analyze_latex_static(workspace, &mut diagnostics_by_uri, &uri); + self.static_diagnostics.insert(uri, diagnostics_by_uri); + } + + pub fn update_chktex(&mut self, workspace: &dyn Workspace, uri: Arc<Uri>, options: &Options) { + analyze_latex_chktex(workspace, &mut self.chktex_diagnostics, &uri, options); + } + + pub fn publish(&self, uri: Arc<Uri>) -> Vec<Diagnostic> { + let mut all_diagnostics = Vec::new(); + for diagnostics_by_uri in self.static_diagnostics.values() { + if let Some(diagnostics) = diagnostics_by_uri.get_vec(&uri) { + all_diagnostics.append(&mut diagnostics.clone()); + } + } + + if let Some(diagnostics) = self.chktex_diagnostics.get_vec(&uri) { + all_diagnostics.append(&mut diagnostics.clone()); + } + + all_diagnostics + } +} diff --git a/support/texlab/src/diagnostics/bibtex.rs b/support/texlab/src/diagnostics/bibtex.rs index 80cca80319..efea141db0 100644 --- a/support/texlab/src/diagnostics/bibtex.rs +++ b/support/texlab/src/diagnostics/bibtex.rs @@ -1,341 +1,147 @@ -use crate::{ - protocol::{Diagnostic, DiagnosticSeverity, Position, Range}, - syntax::{bibtex, SyntaxNode}, - workspace::{Document, DocumentContent}, -}; -use petgraph::graph::NodeIndex; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum BibtexErrorCode { - MissingBeginBrace, - MissingEntryKey, - MissingComma, - MissingEndBrace, - MissingAssign, - MissingContent, - MissingQuote, -} - -impl BibtexErrorCode { - pub fn message(self) -> &'static str { - match self { - BibtexErrorCode::MissingBeginBrace => "Expecting a curly bracket: \"{\"", - BibtexErrorCode::MissingEntryKey => "Expecting an entry key", - BibtexErrorCode::MissingComma => "Expecting a comma: \",\"", - BibtexErrorCode::MissingEndBrace => "Expecting a curly bracket: \"}\"", - BibtexErrorCode::MissingAssign => "Expecting an equals sign: \"=\"", - BibtexErrorCode::MissingContent => "Expecting content", - BibtexErrorCode::MissingQuote => "Expecting a quote: '\"'", - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct BibtexError { - code: BibtexErrorCode, - position: Position, -} - -impl BibtexError { - pub fn new(code: BibtexErrorCode, position: Position) -> Self { - Self { code, position } - } - - pub fn analyze(tree: &bibtex::Tree) -> Vec<Self> { - let mut errors = Vec::new(); - for entry_node in tree.children(tree.root) { - if let Some(entry) = tree.as_entry(entry_node) { - if entry.is_comment() { - continue; - } - - if entry.left.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingBeginBrace, - entry.ty.end(), - )); - continue; - } +use std::sync::Arc; - if entry.key.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingEntryKey, - entry.left.as_ref().unwrap().end(), - )); - continue; - } +use cstree::TextRange; +use lsp_types::{Diagnostic, DiagnosticSeverity}; +use multimap::MultiMap; - if entry.comma.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingComma, - entry.key.as_ref().unwrap().end(), - )); - continue; - } - - let field_count = tree.children(entry_node).count(); - for (i, field_node) in tree.children(entry_node).enumerate() { - let field = tree.as_field(field_node).unwrap(); - if field.assign.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingAssign, - field.name.end(), - )); - continue; - } - - let content = tree.children(field_node).next(); - - if content.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingContent, - field.assign.as_ref().unwrap().end(), - )); - continue; - } - - Self::analyze_content(&mut errors, tree, content.unwrap()); - - if i != field_count - 1 && field.comma.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingComma, - tree.graph[content.unwrap()].end(), - )); - continue; - } - } - - if entry.right.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingEndBrace, - entry.end(), - )); - continue; - } - } - } - errors - } - - fn analyze_content( - mut errors: &mut Vec<BibtexError>, - tree: &bibtex::Tree, - content_node: NodeIndex, - ) { - match &tree.graph[content_node] { - bibtex::Node::QuotedContent(content) => { - for child in tree.children(content_node) { - Self::analyze_content(&mut errors, tree, child); - } - - if content.right.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingQuote, - content.end(), - )); - } - } - bibtex::Node::BracedContent(content) => { - for child in tree.children(content_node) { - Self::analyze_content(&mut errors, tree, child); - } +use crate::{ + syntax::{ + bibtex::{self, HasDelimiters, HasType}, + CstNode, + }, + Document, LineIndexExt, Uri, Workspace, +}; - if content.right.is_none() { - errors.push(BibtexError::new( - BibtexErrorCode::MissingEndBrace, - content.end(), - )); - } - } - bibtex::Node::Concat(concat) => { - let mut children = tree.children(content_node); - Self::analyze_content(&mut errors, tree, children.next().unwrap()); - match children.next() { - Some(right) => { - Self::analyze_content(&mut errors, tree, right); - } - None => { - errors.push(BibtexError::new( - BibtexErrorCode::MissingContent, - concat.end(), - )); - } - } - } - bibtex::Node::Root(_) - | bibtex::Node::Comment(_) - | bibtex::Node::Preamble(_) - | bibtex::Node::String(_) - | bibtex::Node::Entry(_) - | bibtex::Node::Field(_) - | bibtex::Node::Word(_) - | bibtex::Node::Command(_) => {} - } - } -} +pub fn analyze_bibtex_static( + workspace: &dyn Workspace, + diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>, + uri: &Uri, +) -> Option<()> { + let document = workspace.get(uri)?; + let data = document.data.as_bibtex()?; -impl Into<Diagnostic> for BibtexError { - fn into(self) -> Diagnostic { - Diagnostic { - source: Some("bibtex".into()), - range: Range::new(self.position, self.position), - message: self.code.message().into(), - severity: Some(DiagnosticSeverity::Error), - code: None, - related_information: None, - tags: None, - } + for node in data.root.descendants() { + analyze_entry(&document, diagnostics_by_uri, node) + .or_else(|| analyze_field(&document, diagnostics_by_uri, node)); } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexDiagnosticsProvider; -impl BibtexDiagnosticsProvider { - pub fn get(self, doc: &Document) -> Vec<Diagnostic> { - if let DocumentContent::Bibtex(tree) = &doc.content { - BibtexError::analyze(&tree) - .into_iter() - .map(Into::into) - .collect() - } else { - Vec::new() - } - } + Some(()) } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn begin_brace() { - let errors = BibtexError::analyze(&bibtex::open("@article")); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingBeginBrace, - Position::new(0, 8), - )] - ); - } - - #[test] - fn entry_key() { - let errors = BibtexError::analyze(&bibtex::open("@article{")); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingEntryKey, - Position::new(0, 9), - )] - ); - } - - #[test] - fn entry_comma() { - let errors = BibtexError::analyze(&bibtex::open("@article{foo")); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingComma, - Position::new(0, 12), - )] - ); - } - - #[test] - fn entry_end_brace() { - let errors = BibtexError::analyze(&bibtex::open("@article{foo,")); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingEndBrace, - Position::new(0, 13), - )] +fn analyze_entry( + document: &Document, + diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>, + node: &bibtex::SyntaxNode, +) -> Option<()> { + let entry = bibtex::Entry::cast(node)?; + if entry.left_delimiter().is_none() { + diagnostics_by_uri.insert( + Arc::clone(&document.uri), + Diagnostic { + range: document + .line_index + .line_col_lsp_range(entry.ty()?.text_range()), + severity: Some(DiagnosticSeverity::Error), + code: None, + code_description: None, + source: Some("texlab".to_string()), + message: "Expecting a curly bracket: \"{\"".to_string(), + related_information: None, + tags: None, + data: None, + }, ); - } - - #[test] - fn field_equals() { - let errors = BibtexError::analyze(&bibtex::open("@article{foo, bar}")); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingAssign, - Position::new(0, 17), - )] + return Some(()); + } + + if entry.key().is_none() { + diagnostics_by_uri.insert( + Arc::clone(&document.uri), + Diagnostic { + range: document + .line_index + .line_col_lsp_range(entry.left_delimiter()?.text_range()), + severity: Some(DiagnosticSeverity::Error), + code: None, + code_description: None, + source: Some("texlab".to_string()), + message: "Expecting a key".to_string(), + related_information: None, + tags: None, + data: None, + }, ); - } - - #[test] - fn field_content() { - let errors = BibtexError::analyze(&bibtex::open("@article{foo,\nbar = }")); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingContent, - Position::new(1, 5), - )] - ); - } - - #[test] - fn field_comma() { - let text = "@article{foo,\nfoo = bar\nbaz = qux}"; - let errors = BibtexError::analyze(&bibtex::open(text)); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingComma, - Position::new(1, 9), - )] + return Some(()); + } + + if entry.key().is_none() { + diagnostics_by_uri.insert( + Arc::clone(&document.uri), + Diagnostic { + range: document + .line_index + .line_col_lsp_range(entry.right_delimiter()?.text_range()), + severity: Some(DiagnosticSeverity::Error), + code: None, + code_description: None, + source: Some("texlab".to_string()), + message: "Expecting a curly bracket: \"}\"".to_string(), + related_information: None, + tags: None, + data: None, + }, ); + return Some(()); } - #[test] - fn content_quote() { - let text = "@article{foo, bar =\n\"}"; - let errors = BibtexError::analyze(&bibtex::open(text)); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingQuote, - Position::new(1, 1), - )] - ); - } + Some(()) +} - #[test] - fn content_brace() { - let text = "@article{foo, bar =\n{"; - let errors = BibtexError::analyze(&bibtex::open(text)); - assert_eq!( - errors, - vec![ - BibtexError::new(BibtexErrorCode::MissingEndBrace, Position::new(1, 1)), - BibtexError::new(BibtexErrorCode::MissingEndBrace, Position::new(1, 1)), - ] +fn analyze_field( + document: &Document, + diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>, + node: &bibtex::SyntaxNode, +) -> Option<()> { + let field = bibtex::Field::cast(node)?; + if field.equality_sign().is_none() { + diagnostics_by_uri.insert( + Arc::clone(&document.uri), + Diagnostic { + range: document + .line_index + .line_col_lsp_range(TextRange::empty(field.name()?.text_range().end())), + severity: Some(DiagnosticSeverity::Error), + code: None, + code_description: None, + source: Some("texlab".to_string()), + message: "Expecting an equality sign: \"=\"".to_string(), + related_information: None, + tags: None, + data: None, + }, ); - } - - #[test] - fn content_concat() { - let text = "@article{foo, bar = baz \n# }"; - let errors = BibtexError::analyze(&bibtex::open(text)); - assert_eq!( - errors, - vec![BibtexError::new( - BibtexErrorCode::MissingContent, - Position::new(1, 1) - )] + return Some(()); + } + + if field.value().is_none() { + diagnostics_by_uri.insert( + Arc::clone(&document.uri), + Diagnostic { + range: document.line_index.line_col_lsp_range(TextRange::empty( + field.equality_sign()?.text_range().end(), + )), + severity: Some(DiagnosticSeverity::Error), + code: None, + code_description: None, + source: Some("texlab".to_string()), + message: "Expecting a field value".to_string(), + related_information: None, + tags: None, + data: None, + }, ); + return Some(()); } - #[test] - fn entry_valid() { - let text = "@article{foo, bar = \"baz {qux}\" # quux}"; - let errors = BibtexError::analyze(&bibtex::open(text)); - assert_eq!(errors, Vec::new()); - } + Some(()) } diff --git a/support/texlab/src/diagnostics/build_log.rs b/support/texlab/src/diagnostics/build_log.rs new file mode 100644 index 0000000000..a2069ff762 --- /dev/null +++ b/support/texlab/src/diagnostics/build_log.rs @@ -0,0 +1,65 @@ +use std::{path::PathBuf, sync::Arc}; + +use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range}; +use multimap::MultiMap; + +use crate::{syntax::build_log::BuildErrorLevel, Uri, Workspace}; + +pub fn analyze_build_log_static( + workspace: &dyn Workspace, + diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>, + build_log_uri: &Uri, +) -> Option<()> { + let build_log_document = workspace.get(build_log_uri)?; + let parse = build_log_document.data.as_build_log()?; + + let root_document = workspace.documents().into_iter().find(|document| { + if let Some(data) = document.data.as_latex() { + data.extras + .implicit_links + .log + .iter() + .any(|u| u.as_ref() == build_log_uri) + } else { + false + } + })?; + + let base_path = PathBuf::from(root_document.uri.path()); + + for error in &parse.errors { + let pos = Position::new(error.line.unwrap_or(0), 0); + let severity = match error.level { + BuildErrorLevel::Error => DiagnosticSeverity::Error, + BuildErrorLevel::Warning => DiagnosticSeverity::Warning, + }; + let range = Range::new(pos, pos); + let diagnostic = Diagnostic { + range, + severity: Some(severity), + code: None, + code_description: None, + source: Some("latex".into()), + message: error.message.clone(), + related_information: None, + tags: None, + data: None, + }; + + let full_path = base_path.join(&error.relative_path); + + let uri = if full_path.starts_with(&base_path) { + error + .relative_path + .to_str() + .and_then(|path| root_document.uri.join(path).map(Into::into).ok()) + .map(Arc::new) + .unwrap_or_else(|| Arc::clone(&root_document.uri)) + } else { + Arc::clone(&root_document.uri) + }; + + diagnostics_by_uri.insert(uri, diagnostic); + } + Some(()) +} diff --git a/support/texlab/src/diagnostics/chktex.rs b/support/texlab/src/diagnostics/chktex.rs new file mode 100644 index 0000000000..897887c473 --- /dev/null +++ b/support/texlab/src/diagnostics/chktex.rs @@ -0,0 +1,105 @@ +use std::{ + fs, io, + path::Path, + process::{Command, Stdio}, + sync::Arc, +}; + +use lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Range}; +use multimap::MultiMap; +use once_cell::sync::Lazy; +use regex::Regex; +use tempfile::tempdir; + +use crate::{Options, RangeExt, Uri, Workspace}; + +pub fn analyze_latex_chktex( + workspace: &dyn Workspace, + diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>, + uri: &Uri, + options: &Options, +) -> Option<()> { + let document = workspace.get(uri)?; + document.data.as_latex()?; + + let current_dir = options + .root_directory + .as_ref() + .cloned() + .or_else(|| { + if document.uri.scheme() == "file" { + document + .uri + .to_file_path() + .unwrap() + .parent() + .map(ToOwned::to_owned) + } else { + None + } + }) + .unwrap_or_else(|| ".".into()); + + diagnostics_by_uri.remove(uri); + diagnostics_by_uri.insert_many( + Arc::clone(&document.uri), + lint(&document.text, ¤t_dir).unwrap_or_default(), + ); + Some(()) +} + +pub static LINE_REGEX: Lazy<Regex> = + Lazy::new(|| Regex::new("(\\d+):(\\d+):(\\d+):(\\w+):(\\w+):(.*)").unwrap()); + +fn lint(text: &str, current_dir: &Path) -> io::Result<Vec<Diagnostic>> { + let directory = tempdir()?; + fs::write(directory.path().join("file.tex"), text)?; + let _ = fs::copy( + current_dir.join("chktexrc"), + directory.path().join("chktexrc"), + ); + + let output = Command::new("chktex") + .args(&["-I0", "-f%l:%c:%d:%k:%n:%m\n", "file.tex"]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(directory.path()) + .output()?; + + // let mut writer = BufWriter::new(process.stdin.take().unwrap()); + // writer.write_all(text.as_bytes())?; + // // writer.flush()?; + // let output = process.wait_with_output()?; + + let mut diagnostics = Vec::new(); + for line in String::from_utf8_lossy(&output.stdout).lines() { + let captures = LINE_REGEX.captures(&line).unwrap(); + let line = captures[1].parse::<u32>().unwrap() - 1; + let character = captures[2].parse::<u32>().unwrap() - 1; + let digit = captures[3].parse::<u32>().unwrap(); + let kind = &captures[4]; + let code = &captures[5]; + let message = captures[6].into(); + let range = Range::new_simple(line, character, line, character + digit); + let severity = match kind { + "Message" => DiagnosticSeverity::Information, + "Warning" => DiagnosticSeverity::Warning, + _ => DiagnosticSeverity::Error, + }; + + diagnostics.push(Diagnostic { + range, + severity: Some(severity), + code: Some(NumberOrString::String(code.into())), + code_description: None, + source: Some("chktex".into()), + message, + related_information: None, + tags: None, + data: None, + }); + } + + Ok(diagnostics) +} diff --git a/support/texlab/src/diagnostics/debouncer.rs b/support/texlab/src/diagnostics/debouncer.rs new file mode 100644 index 0000000000..46bea80176 --- /dev/null +++ b/support/texlab/src/diagnostics/debouncer.rs @@ -0,0 +1,77 @@ +use std::{ + sync::Arc, + thread::{self, JoinHandle}, + time::{Duration, Instant}, +}; + +use chashmap::CHashMap; +use crossbeam_channel::Sender; + +use crate::{Document, ServerContext, Uri, Workspace}; + +pub enum DiagnosticsMessage { + Analyze { + workspace: Arc<dyn Workspace>, + document: Arc<Document>, + }, + Shutdown, +} + +pub struct DiagnosticsDebouncer { + pub sender: Sender<DiagnosticsMessage>, + handle: Option<JoinHandle<()>>, +} + +impl DiagnosticsDebouncer { + pub fn launch<A>(context: Arc<ServerContext>, action: A) -> Self + where + A: Fn(Arc<dyn Workspace>, Arc<Document>) + Send + Clone + 'static, + { + let (sender, receiver) = crossbeam_channel::unbounded(); + + let handle = thread::spawn(move || { + let pool = threadpool::Builder::new().build(); + let last_task_time_by_uri: Arc<CHashMap<Arc<Uri>, Instant>> = Arc::default(); + while let Ok(DiagnosticsMessage::Analyze { + workspace, + document, + }) = receiver.recv() + { + let delay = { + context + .options + .read() + .unwrap() + .diagnostics_delay + .unwrap_or(300) + }; + + if let Some(time) = last_task_time_by_uri.get(&document.uri) { + if time.elapsed().as_millis() < delay as u128 { + continue; + } + } + + let last_task_time_by_uri = Arc::clone(&last_task_time_by_uri); + let action = action.clone(); + pool.execute(move || { + thread::sleep(Duration::from_millis(delay)); + last_task_time_by_uri.insert(Arc::clone(&document.uri), Instant::now()); + action(workspace, document); + }); + } + }); + + Self { + sender, + handle: Some(handle), + } + } +} + +impl Drop for DiagnosticsDebouncer { + fn drop(&mut self) { + self.sender.send(DiagnosticsMessage::Shutdown).unwrap(); + self.handle.take().unwrap().join().unwrap(); + } +} diff --git a/support/texlab/src/diagnostics/latex.rs b/support/texlab/src/diagnostics/latex.rs index 95fcaeeacf..b835f88398 100644 --- a/support/texlab/src/diagnostics/latex.rs +++ b/support/texlab/src/diagnostics/latex.rs @@ -1,113 +1,129 @@ +use std::sync::Arc; + +use cstree::TextRange; +use lsp_types::{Diagnostic, DiagnosticSeverity}; +use multimap::MultiMap; + use crate::{ - protocol::{Diagnostic, DiagnosticSeverity, NumberOrString, Range, RangeExt, Uri}, - workspace::Document, -}; -use chashmap::CHashMap; -use futures::{ - future::{AbortHandle, Abortable, Aborted}, - lock::Mutex, + syntax::{latex, CstNode}, + Document, LineIndexExt, Uri, Workspace, }; -use log::trace; -use once_cell::sync::Lazy; -use regex::Regex; -use std::process::Stdio; -use tokio::{prelude::*, process::Command}; - -#[derive(Debug, Default)] -pub struct LatexDiagnosticsProvider { - diagnostics_by_uri: CHashMap<Uri, Vec<Diagnostic>>, - handle: Mutex<Option<AbortHandle>>, -} -impl LatexDiagnosticsProvider { - pub fn get(&self, document: &Document) -> Vec<Diagnostic> { - match self.diagnostics_by_uri.get(&document.uri) { - Some(diagnostics) => diagnostics.to_owned(), - None => Vec::new(), - } +pub fn analyze_latex_static( + workspace: &dyn Workspace, + diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>, + uri: &Uri, +) -> Option<()> { + let document = workspace.get(uri)?; + if !document.uri.as_str().ends_with(".tex") { + return None; } - pub async fn update(&self, uri: &Uri, text: &str) { - if uri.scheme() != "file" { - return; - } + let data = document.data.as_latex()?; - let mut handle_guard = self.handle.lock().await; - if let Some(handle) = &*handle_guard { - handle.abort(); - } + for node in data.root.descendants() { + analyze_environment(&document, diagnostics_by_uri, node) + .or_else(|| analyze_curly_group(&document, diagnostics_by_uri, node)) + .or_else(|| { + if node.kind() == latex::ERROR && node.first_token()?.text() == "}" { + diagnostics_by_uri.insert( + Arc::clone(&document.uri), + Diagnostic { + range: document.line_index.line_col_lsp_range(node.text_range()), + severity: Some(DiagnosticSeverity::Error), + code: None, + code_description: None, + source: Some("texlab".to_string()), + message: "Unexpected \"}\"".to_string(), + related_information: None, + tags: None, + data: None, + }, + ); + Some(()) + } else { + None + } + }); + } - let (handle, registration) = AbortHandle::new_pair(); - *handle_guard = Some(handle); - drop(handle_guard); + Some(()) +} - let future = Abortable::new( - async move { - self.diagnostics_by_uri - .insert(uri.clone(), lint(text.into()).await.unwrap_or_default()); +fn analyze_environment( + document: &Document, + diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>, + node: &latex::SyntaxNode, +) -> Option<()> { + let environment = latex::Environment::cast(node)?; + let name1 = environment.begin()?.name()?.word()?; + let name2 = environment.end()?.name()?.word()?; + if name1.text() != name2.text() { + diagnostics_by_uri.insert( + Arc::clone(&document.uri), + Diagnostic { + range: document.line_index.line_col_lsp_range(name1.text_range()), + severity: Some(DiagnosticSeverity::Error), + code: None, + code_description: None, + source: Some("texlab".to_string()), + message: "Mismatched environment".to_string(), + related_information: None, + tags: None, + data: None, }, - registration, ); - - if let Err(Aborted) = future.await { - trace!("Killed ChkTeX because it took too long to execute") - } } + Some(()) } -pub static LINE_REGEX: Lazy<Regex> = - Lazy::new(|| Regex::new("(\\d+):(\\d+):(\\d+):(\\w+):(\\w+):(.*)").unwrap()); - -async fn lint(text: String) -> io::Result<Vec<Diagnostic>> { - let mut process: tokio::process::Child = Command::new("chktex") - .args(&["-I0", "-f%l:%c:%d:%k:%n:%m\n"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .kill_on_drop(true) - .spawn()?; - - process - .stdin - .take() - .unwrap() - .write_all(text.as_bytes()) - .await?; - - let mut stdout = String::new(); - process - .stdout - .take() - .unwrap() - .read_to_string(&mut stdout) - .await?; +fn analyze_curly_group( + document: &Document, + diagnostics_by_uri: &mut MultiMap<Arc<Uri>, Diagnostic>, + node: &latex::SyntaxNode, +) -> Option<()> { + if !matches!( + node.kind(), + latex::CURLY_GROUP + | latex::CURLY_GROUP_COMMAND + | latex::CURLY_GROUP_KEY_VALUE + | latex::CURLY_GROUP_WORD + | latex::CURLY_GROUP_WORD_LIST + ) { + return None; + } - let mut diagnostics = Vec::new(); - for line in stdout.lines() { - if let Some(captures) = LINE_REGEX.captures(line) { - let line = captures[1].parse::<u64>().unwrap() - 1; - let character = captures[2].parse::<u64>().unwrap() - 1; - let digit = captures[3].parse::<u64>().unwrap(); - let kind = &captures[4]; - let code = &captures[5]; - let message = captures[6].into(); - let range = Range::new_simple(line, character, line, character + digit); - let severity = match kind { - "Message" => DiagnosticSeverity::Information, - "Warning" => DiagnosticSeverity::Warning, - _ => DiagnosticSeverity::Error, - }; + let is_inside_verbatim_environment = node + .ancestors() + .filter_map(latex::Environment::cast) + .filter_map(|env| env.begin()) + .filter_map(|begin| begin.name()) + .filter_map(|name| name.word()) + .any(|name| ["asy", "lstlisting", "minted", "verbatim"].contains(&name.text())); - diagnostics.push(Diagnostic { - source: Some("chktex".into()), - code: Some(NumberOrString::String(code.into())), - message, - severity: Some(severity), - range, + if !is_inside_verbatim_environment + && !node + .children_with_tokens() + .filter_map(|element| element.into_token()) + .any(|token| token.kind() == latex::R_CURLY) + { + diagnostics_by_uri.insert( + Arc::clone(&document.uri), + Diagnostic { + range: document + .line_index + .line_col_lsp_range(TextRange::empty(node.text_range().end())), + severity: Some(DiagnosticSeverity::Error), + code: None, + code_description: None, + source: Some("texlab".to_string()), + message: "Missing \"}\" inserted".to_string(), related_information: None, tags: None, - }) - } + data: None, + }, + ); } - Ok(diagnostics) + + Some(()) } diff --git a/support/texlab/src/diagnostics/mod.rs b/support/texlab/src/diagnostics/mod.rs deleted file mode 100644 index 906da9858f..0000000000 --- a/support/texlab/src/diagnostics/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod bibtex; -mod build; -mod latex; - -pub use self::{ - bibtex::{BibtexDiagnosticsProvider, BibtexError, BibtexErrorCode}, - build::BuildDiagnosticsProvider, - latex::LatexDiagnosticsProvider, -}; - -use crate::{protocol::Diagnostic, workspace::Document}; - -#[derive(Debug, Default)] -pub struct DiagnosticsManager { - pub bibtex: BibtexDiagnosticsProvider, - pub latex: LatexDiagnosticsProvider, - pub build: BuildDiagnosticsProvider, -} - -impl DiagnosticsManager { - pub async fn get(&self, doc: &Document) -> Vec<Diagnostic> { - let mut diagnostics = Vec::new(); - diagnostics.append(&mut self.bibtex.get(doc)); - diagnostics.append(&mut self.latex.get(doc)); - diagnostics.append(&mut self.build.get(doc).await); - diagnostics - } -} diff --git a/support/texlab/src/dispatch.rs b/support/texlab/src/dispatch.rs new file mode 100644 index 0000000000..a080016480 --- /dev/null +++ b/support/texlab/src/dispatch.rs @@ -0,0 +1,81 @@ +use anyhow::Result; +use log::warn; +use lsp_server::{ErrorCode, Notification, Request, RequestId, Response}; +use serde::de::DeserializeOwned; + +pub struct NotificationDispatcher { + not: Option<Notification>, +} + +impl NotificationDispatcher { + pub fn new(not: Notification) -> Self { + Self { not: Some(not) } + } + + pub fn on<N, F>(mut self, handler: F) -> Result<Self> + where + N: lsp_types::notification::Notification, + N::Params: DeserializeOwned, + F: FnOnce(N::Params) -> Result<()>, + { + if let Some(not) = self.not { + match not.extract::<N::Params>(N::METHOD) { + Ok(params) => { + handler(params)?; + self.not = None; + } + Err(not) => { + self.not = Some(not); + } + }; + } + Ok(self) + } + + pub fn default(self) { + if let Some(not) = &self.not { + warn!("Unknown notification: {}", not.method); + } + } +} + +pub struct RequestDispatcher { + req: Option<Request>, +} + +impl RequestDispatcher { + pub fn new(req: Request) -> Self { + Self { req: Some(req) } + } + + pub fn on<R, F>(mut self, handler: F) -> Result<Self> + where + R: lsp_types::request::Request, + R::Params: DeserializeOwned, + F: FnOnce(RequestId, R::Params) -> Result<()>, + { + if let Some(req) = self.req { + match req.extract::<R::Params>(R::METHOD) { + Ok((id, params)) => { + handler(id, params)?; + self.req = None; + } + Err(req) => { + self.req = Some(req); + } + } + } + Ok(self) + } + + pub fn default(self) -> Option<Response> { + self.req.map(|req| { + warn!("Unknown request: {}", req.method); + Response::new_err( + req.id, + ErrorCode::MethodNotFound as i32, + "method not found".to_string(), + ) + }) + } +} diff --git a/support/texlab/src/distro.rs b/support/texlab/src/distro.rs new file mode 100644 index 0000000000..1e67a14285 --- /dev/null +++ b/support/texlab/src/distro.rs @@ -0,0 +1,74 @@ +mod kpsewhich; +mod miktex; +mod texlive; + +use std::process::{Command, Stdio}; + +use anyhow::Result; +use derive_more::Display; +use log::warn; + +pub use kpsewhich::Resolver; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Display)] +pub enum DistributionKind { + #[display(fmt = "TeXLive")] + Texlive, + #[display(fmt = "MikTeX")] + Miktex, + #[display(fmt = "Tectonic")] + Tectonic, + #[display(fmt = "Unknown")] + Unknown, +} + +#[derive(Debug, Clone)] +pub struct Distribution { + pub kind: DistributionKind, + pub resolver: Resolver, +} + +impl Distribution { + pub fn detect() -> Self { + let kind = match Command::new("latex").arg("--version").output() { + Ok(output) => { + let stdout = String::from_utf8_lossy(&output.stdout); + if stdout.contains("TeX Live") { + DistributionKind::Texlive + } else if stdout.contains("MiKTeX") { + DistributionKind::Miktex + } else { + DistributionKind::Unknown + } + } + Err(_) => { + if Command::new("tectonic") + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok() + { + DistributionKind::Tectonic + } else { + DistributionKind::Unknown + } + } + }; + + let resolver = match kind { + DistributionKind::Texlive => Self::load_resolver(texlive::load_resolver), + DistributionKind::Miktex => Self::load_resolver(miktex::load_resolver), + DistributionKind::Tectonic | DistributionKind::Unknown => Resolver::default(), + }; + Self { kind, resolver } + } + + fn load_resolver(loader: impl FnOnce() -> Result<Resolver>) -> Resolver { + match loader() { + Ok(resolver) => return resolver, + Err(why) => warn!("Failed to load resolver: {}", why), + }; + Resolver::default() + } +} diff --git a/support/texlab/src/distro/kpsewhich.rs b/support/texlab/src/distro/kpsewhich.rs new file mode 100644 index 0000000000..2dcc98118f --- /dev/null +++ b/support/texlab/src/distro/kpsewhich.rs @@ -0,0 +1,74 @@ +use std::{ + env, + ffi::OsStr, + fs, + path::{Path, PathBuf}, + process::Command, +}; + +use anyhow::Result; +use rustc_hash::FxHashMap; +use smol_str::SmolStr; + +use crate::DocumentLanguage; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Resolver { + pub files_by_name: FxHashMap<SmolStr, PathBuf>, +} + +impl Resolver { + pub fn new(files_by_name: FxHashMap<SmolStr, PathBuf>) -> Self { + Self { files_by_name } + } +} + +pub fn parse_database( + root_directories: &[PathBuf], + mut reader: impl FnMut(&Path) -> Result<Vec<PathBuf>>, +) -> Result<Resolver> { + let mut files_by_name = FxHashMap::default(); + for directory in root_directories { + for path in reader(directory)? { + if DocumentLanguage::by_path(&path).is_some() { + if let Some(path) = make_absolute(root_directories, &path) { + if let Some(name) = path.file_name().and_then(OsStr::to_str).map(Into::into) { + files_by_name.insert(name, path); + } + } + } + } + } + Ok(Resolver::new(files_by_name)) +} + +fn make_absolute(root_directories: &[PathBuf], relative_path: &Path) -> Option<PathBuf> { + for dir in root_directories.iter().rev() { + if let Ok(path) = fs::canonicalize(dir.join(&relative_path)) { + return Some(path); + } + } + None +} + +pub fn root_directories() -> Result<Vec<PathBuf>> { + let texmf = run(&["-var-value", "TEXMF"])?; + let expand_arg = format!("--expand-braces={}", texmf); + let expanded = run(&[&expand_arg])?; + let directories = env::split_paths(&expanded.replace("!", "")) + .filter(|path| path.exists()) + .collect(); + Ok(directories) +} + +fn run(args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> Result<String> { + let output = Command::new("kpsewhich").args(args).output()?; + + let result = String::from_utf8(output.stdout)? + .lines() + .next() + .unwrap() + .into(); + + Ok(result) +} diff --git a/support/texlab/src/tex/miktex.rs b/support/texlab/src/distro/miktex.rs index b543aa8a1b..563c8445e3 100644 --- a/support/texlab/src/tex/miktex.rs +++ b/support/texlab/src/distro/miktex.rs @@ -1,45 +1,19 @@ -use super::{ - compile, - kpsewhich::{self, KpsewhichError, Resolver}, - Artifacts, CompileError, CompileParams, Distribution, DistributionKind, -}; -use async_trait::async_trait; -use byteorder::{LittleEndian, ReadBytesExt}; -use futures::lock::Mutex; use std::{ ffi::OsStr, + fs, io::{self, Cursor}, path::{Path, PathBuf}, - sync::Arc, }; -use tokio::fs; - -#[derive(Debug, Default)] -pub struct Miktex { - resolver: Mutex<Arc<Resolver>>, -} - -#[async_trait] -impl Distribution for Miktex { - fn kind(&self) -> DistributionKind { - DistributionKind::Miktex - } - async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> { - compile(params).await - } +use anyhow::{Context, Result}; +use byteorder::{LittleEndian, ReadBytesExt}; - async fn load(&self) -> Result<(), KpsewhichError> { - let root_directories = kpsewhich::root_directories().await?; - let resolver = kpsewhich::parse_database(&root_directories, read_database).await?; - *self.resolver.lock().await = Arc::new(resolver); - Ok(()) - } +use super::kpsewhich::{self, Resolver}; - async fn resolver(&self) -> Arc<Resolver> { - let resolver = self.resolver.lock().await; - Arc::clone(&resolver) - } +pub fn load_resolver() -> Result<Resolver> { + let root_directories = kpsewhich::root_directories()?; + let resolver = kpsewhich::parse_database(&root_directories, read_database)?; + Ok(resolver) } const DATABASE_PATH: &str = "miktex/data/le"; @@ -49,18 +23,17 @@ const FNDB_TABLE_POINTER_OFFSET: usize = 4 * FNDB_WORD_SIZE; const FNDB_TABLE_SIZE_OFFSET: usize = 6 * FNDB_WORD_SIZE; const FNDB_ENTRY_SIZE: usize = 4 * FNDB_WORD_SIZE; -async fn read_database(directory: &Path) -> Result<Vec<PathBuf>, KpsewhichError> { +fn read_database(directory: &Path) -> Result<Vec<PathBuf>> { let database_directory = directory.join(DATABASE_PATH); if !database_directory.exists() { return Ok(Vec::new()); } let mut database = Vec::new(); - let mut files: tokio::fs::ReadDir = fs::read_dir(database_directory).await?; - while let Some(file) = files.next_entry().await? { + for file in fs::read_dir(database_directory)?.filter_map(Result::ok) { if file.path().extension().and_then(OsStr::to_str) == Some("fndb-5") { - let bytes = fs::read(file.path()).await?; - database.extend(parse_database(&bytes).map_err(|_| KpsewhichError::CorruptDatabase)?); + let bytes = fs::read(file.path())?; + database.extend(parse_database(&bytes).context("parsing kpsewhich database")?); } } Ok(database) diff --git a/support/texlab/src/distro/texlive.rs b/support/texlab/src/distro/texlive.rs new file mode 100644 index 0000000000..c9b4951e34 --- /dev/null +++ b/support/texlab/src/distro/texlive.rs @@ -0,0 +1,43 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, + str::Lines, +}; + +use anyhow::Result; + +use super::kpsewhich::{self, Resolver}; + +pub fn load_resolver() -> Result<Resolver> { + let root_directories = kpsewhich::root_directories()?; + let resolver = kpsewhich::parse_database(&root_directories, read_database)?; + Ok(resolver) +} + +const DATABASE_PATH: &str = "ls-R"; + +fn read_database(directory: &Path) -> Result<Vec<PathBuf>> { + let file = directory.join(DATABASE_PATH); + if !file.is_file() { + return Ok(Vec::new()); + } + + let text = fs::read_to_string(file)?; + let files = parse_database(text.lines())?; + Ok(files) +} + +fn parse_database(lines: Lines) -> io::Result<Vec<PathBuf>> { + let mut paths = Vec::new(); + let mut directory = ""; + + for line in lines.filter(|x| !x.trim().is_empty() && !x.starts_with('%')) { + if line.ends_with(':') { + directory = &line[..line.len() - 1]; + } else { + let path = PathBuf::from(directory).join(line); + paths.push(path); + } + } + Ok(paths) +} diff --git a/support/texlab/src/feature.rs b/support/texlab/src/feature.rs deleted file mode 100644 index 2b32b64188..0000000000 --- a/support/texlab/src/feature.rs +++ /dev/null @@ -1,405 +0,0 @@ -use crate::{ - components::{Component, COMPONENT_DATABASE}, - protocol::*, - tex::{Distribution, Language, UnknownDistribution}, - workspace::{Document, DocumentContent, DocumentParams, Snapshot}, -}; -use async_trait::async_trait; -use itertools::Itertools; -use std::{ - env, - path::{Path, PathBuf}, - sync::Arc, -}; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct DocumentView { - pub snapshot: Arc<Snapshot>, - pub current: Arc<Document>, - pub related: Vec<Arc<Document>>, -} - -impl DocumentView { - pub fn analyze( - snapshot: Arc<Snapshot>, - current: Arc<Document>, - options: &Options, - current_dir: &Path, - ) -> Self { - let related = snapshot.relations(¤t.uri, options, current_dir); - Self { - snapshot, - current, - related, - } - } - - pub fn components(&self) -> Vec<&'static Component> { - let mut start_components = vec![COMPONENT_DATABASE.kernel()]; - for doc in &self.related { - if let DocumentContent::Latex(table) = &doc.content { - table - .components - .iter() - .flat_map(|file| COMPONENT_DATABASE.find(file)) - .for_each(|component| start_components.push(component)) - } - } - - let mut all_components = Vec::new(); - for component in start_components { - all_components.push(component); - component - .references - .iter() - .flat_map(|file| COMPONENT_DATABASE.find(&file)) - .for_each(|component| all_components.push(component)) - } - - all_components - .into_iter() - .unique_by(|component| &component.file_names) - .collect() - } -} - -#[derive(Clone)] -pub struct FeatureRequest<P> { - pub params: P, - pub view: DocumentView, - pub distro: Arc<dyn Distribution>, - pub client_capabilities: Arc<ClientCapabilities>, - pub options: Options, - pub current_dir: Arc<PathBuf>, -} - -impl<P> FeatureRequest<P> { - pub fn snapshot(&self) -> &Snapshot { - &self.view.snapshot - } - - pub fn current(&self) -> &Document { - &self.view.current - } - - pub fn related(&self) -> &[Arc<Document>] { - &self.view.related - } -} - -#[async_trait] -pub trait FeatureProvider { - type Params; - type Output; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output; -} - -type ListProvider<P, O> = Box<dyn FeatureProvider<Params = P, Output = Vec<O>> + Send + Sync>; - -#[derive(Default)] -pub struct ConcatProvider<P, O> { - providers: Vec<ListProvider<P, O>>, -} - -impl<P, O> ConcatProvider<P, O> { - pub fn new(providers: Vec<ListProvider<P, O>>) -> Self { - Self { providers } - } -} - -#[async_trait] -impl<P, O> FeatureProvider for ConcatProvider<P, O> -where - P: Send + Sync, - O: Send + Sync, -{ - type Params = P; - type Output = Vec<O>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<P>) -> Vec<O> { - let mut items = Vec::new(); - for provider in &self.providers { - items.append(&mut provider.execute(req).await); - } - items - } -} - -type OptionProvider<P, O> = Box<dyn FeatureProvider<Params = P, Output = Option<O>> + Send + Sync>; - -#[derive(Default)] -pub struct ChoiceProvider<P, O> { - providers: Vec<OptionProvider<P, O>>, -} - -impl<P, O> ChoiceProvider<P, O> { - pub fn new(providers: Vec<OptionProvider<P, O>>) -> Self { - Self { providers } - } -} - -#[async_trait] -impl<P, O> FeatureProvider for ChoiceProvider<P, O> -where - P: Send + Sync, - O: Send + Sync, -{ - type Params = P; - type Output = Option<O>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<P>) -> Option<O> { - for provider in &self.providers { - let item = provider.execute(req).await; - if item.is_some() { - return item; - } - } - None - } -} - -#[derive(Debug)] -pub struct FeatureTester { - main: String, - files: Vec<(String, String)>, - distro: Arc<dyn Distribution>, - position: Position, - new_name: String, - include_declaration: bool, - client_capabilities: Arc<ClientCapabilities>, - current_dir: Arc<PathBuf>, - root_dir: Option<PathBuf>, - output_dir: Option<PathBuf>, -} - -impl Default for FeatureTester { - fn default() -> Self { - Self::new() - } -} - -impl FeatureTester { - pub fn new() -> Self { - Self { - main: String::new(), - files: Vec::new(), - distro: Arc::new(UnknownDistribution::default()), - position: Position::default(), - new_name: String::new(), - include_declaration: false, - client_capabilities: Arc::default(), - current_dir: Arc::new(env::temp_dir()), - root_dir: None, - output_dir: None, - } - } - - pub fn main<S: Into<String>>(&mut self, name: S) -> &mut Self { - self.main = name.into(); - self - } - - pub fn file<S, T>(&mut self, name: S, text: T) -> &mut Self - where - S: Into<String>, - T: Into<String>, - { - self.files.push((name.into(), text.into())); - self - } - - pub fn position(&mut self, line: u64, character: u64) -> &mut Self { - self.position = Position::new(line, character); - self - } - - pub fn new_name<S: Into<String>>(&mut self, value: S) -> &mut Self { - self.new_name = value.into(); - self - } - - pub fn include_declaration(&mut self) -> &mut Self { - self.include_declaration = true; - self - } - - pub fn root_directory<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self { - self.root_dir = Some(path.into()); - self - } - - pub fn output_directory<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self { - self.output_dir = Some(path.into()); - self - } - - pub fn uri(name: &str) -> Uri { - let path = env::temp_dir().join(name); - Uri::from_file_path(path).unwrap() - } - - fn identifier(&self) -> TextDocumentIdentifier { - let uri = Self::uri(&self.main); - TextDocumentIdentifier::new(uri.into()) - } - - fn options(&self) -> Options { - Options { - latex: Some(LatexOptions { - build: Some(LatexBuildOptions { - output_directory: self.output_dir.clone(), - ..LatexBuildOptions::default() - }), - root_directory: self.root_dir.clone(), - ..LatexOptions::default() - }), - ..Options::default() - } - } - - async fn view(&self) -> DocumentView { - let mut snapshot = Snapshot::new(); - let resolver = self.distro.resolver().await; - let options = self.options(); - for (name, text) in &self.files { - let uri = Self::uri(name); - let path = uri.to_file_path().unwrap(); - let language = path - .extension() - .and_then(|ext| ext.to_str()) - .and_then(Language::by_extension) - .unwrap(); - let doc = Document::open(DocumentParams { - uri, - text: text.trim().into(), - language, - resolver: &resolver, - options: &options, - current_dir: &self.current_dir, - }); - snapshot.push(doc); - } - let current = snapshot.find(&Self::uri(&self.main)).unwrap(); - DocumentView::analyze(Arc::new(snapshot), current, &options, &self.current_dir) - } - - async fn request<P>(&self, params: P) -> FeatureRequest<P> { - FeatureRequest { - params, - view: self.view().await, - client_capabilities: Arc::clone(&self.client_capabilities), - distro: self.distro.clone(), - options: self.options(), - current_dir: Arc::clone(&self.current_dir), - } - } - - pub async fn test_position<F, O>(&self, provider: F) -> O - where - F: FeatureProvider<Params = TextDocumentPositionParams, Output = O>, - { - let text_document = self.identifier(); - let params = TextDocumentPositionParams::new(text_document, self.position); - let req = self.request(params).await; - provider.execute(&req).await - } - - pub async fn test_completion<F, O>(&self, provider: F) -> O - where - F: FeatureProvider<Params = CompletionParams, Output = O>, - { - let req = self.test_completion_request().await; - provider.execute(&req).await - } - - pub async fn test_completion_request(&self) -> FeatureRequest<CompletionParams> { - let params = CompletionParams { - text_document_position: TextDocumentPositionParams::new( - self.identifier(), - self.position, - ), - context: None, - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - self.request(params).await - } - - pub async fn test_folding<F, O>(&self, provider: F) -> O - where - F: FeatureProvider<Params = FoldingRangeParams, Output = O>, - { - let text_document = self.identifier(); - let params = FoldingRangeParams { - text_document, - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - let req = self.request(params).await; - provider.execute(&req).await - } - - pub async fn test_link<F, O>(&self, provider: F) -> O - where - F: FeatureProvider<Params = DocumentLinkParams, Output = O>, - { - let text_document = self.identifier(); - let params = DocumentLinkParams { - text_document, - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - let req = self.request(params).await; - provider.execute(&req).await - } - - pub async fn test_reference<F, O>(&self, provider: F) -> O - where - F: FeatureProvider<Params = ReferenceParams, Output = O>, - { - let params = ReferenceParams { - text_document_position: TextDocumentPositionParams::new( - self.identifier(), - self.position, - ), - context: ReferenceContext { - include_declaration: self.include_declaration, - }, - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - let req = self.request(params).await; - provider.execute(&req).await - } - - pub async fn test_rename<F, O>(&self, provider: F) -> O - where - F: FeatureProvider<Params = RenameParams, Output = O>, - { - let params = RenameParams { - text_document_position: TextDocumentPositionParams::new( - self.identifier(), - self.position, - ), - new_name: self.new_name.clone(), - work_done_progress_params: WorkDoneProgressParams::default(), - }; - let req = self.request(params).await; - provider.execute(&req).await - } - - pub async fn test_symbol<F, O>(&self, provider: F) -> O - where - F: FeatureProvider<Params = DocumentSymbolParams, Output = O>, - { - let text_document = self.identifier(); - let params = DocumentSymbolParams { - text_document, - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - let req = self.request(params).await; - provider.execute(&req).await - } -} diff --git a/support/texlab/src/features.rs b/support/texlab/src/features.rs new file mode 100644 index 0000000000..09743c0035 --- /dev/null +++ b/support/texlab/src/features.rs @@ -0,0 +1,272 @@ +mod build; +#[cfg(feature = "completion")] +mod completion; +mod cursor; +mod definition; +mod folding; +mod formatting; +mod forward_search; +mod highlight; +mod hover; +mod link; +mod lsp_kinds; +mod reference; +mod rename; +#[cfg(feature = "semantic")] +mod semantic; +mod symbol; + +use std::sync::Arc; + +use crate::{Document, ServerContext, Workspace, WorkspaceSubset}; + +#[cfg(feature = "completion")] +pub use self::completion::{complete, CompletionItemData, COMPLETION_LIMIT}; +#[cfg(feature = "semantic")] +pub use self::semantic::{find_semantic_tokens_range, legend}; +pub use self::{ + build::{build_document, BuildParams, BuildResult, BuildStatus}, + definition::goto_definition, + folding::find_foldings, + formatting::format_source_code, + forward_search::{execute_forward_search, ForwardSearchResult, ForwardSearchStatus}, + highlight::find_document_highlights, + hover::find_hover, + link::find_document_links, + reference::find_all_references, + rename::{prepare_rename_all, rename_all}, + symbol::{find_document_symbols, find_workspace_symbols}, +}; + +#[derive(Clone)] +pub struct FeatureRequest<P> { + pub context: Arc<ServerContext>, + pub params: P, + pub workspace: Arc<dyn Workspace>, + pub subset: WorkspaceSubset, +} + +impl<P> FeatureRequest<P> { + pub fn main_document(&self) -> &Document { + &self.subset.documents[0] + } +} + +#[cfg(test)] +mod testing { + use std::{path::PathBuf, sync::Arc}; + + use lsp_types::{ + ClientCapabilities, ClientInfo, CompletionParams, DocumentFormattingParams, + DocumentHighlightParams, DocumentLinkParams, FoldingRangeParams, FormattingOptions, + GotoDefinitionParams, HoverParams, PartialResultParams, Position, ReferenceContext, + ReferenceParams, RenameParams, TextDocumentIdentifier, TextDocumentPositionParams, + WorkDoneProgressParams, + }; + use typed_builder::TypedBuilder; + + use crate::{ + create_workspace_fast, distro::Resolver, DocumentLanguage, Options, ServerContext, Uri, + Workspace, WorkspaceSource, + }; + + use super::*; + + #[derive(Debug, Clone, TypedBuilder)] + pub struct FeatureTester<'a> { + main: &'a str, + + files: Vec<(&'a str, &'a str)>, + + #[builder(default)] + line: u32, + + #[builder(default)] + character: u32, + + #[builder(default)] + new_name: &'a str, + + #[builder(default)] + include_declaration: bool, + + #[builder(default)] + client_capabilities: ClientCapabilities, + + #[builder(default)] + client_info: Option<ClientInfo>, + + #[builder(default)] + resolver: Resolver, + + #[builder(default=std::env::temp_dir())] + current_directory: PathBuf, + + #[builder(default, setter(strip_option))] + root_directory: Option<PathBuf>, + + #[builder(default, setter(strip_option))] + aux_directory: Option<PathBuf>, + } + + impl<'a> FeatureTester<'a> { + pub fn uri(&self, name: &str) -> Arc<Uri> { + let path = self.current_directory.join(name); + Arc::new(Uri::from_file_path(path).unwrap()) + } + + fn options(&self) -> Options { + let mut options = Options::default(); + options.aux_directory = self.aux_directory.clone(); + options.root_directory = self.root_directory.clone(); + options + } + + fn identifier(&self) -> TextDocumentIdentifier { + let uri = self.uri(&self.main); + TextDocumentIdentifier::new(uri.as_ref().clone().into()) + } + + fn context(&self) -> Arc<ServerContext> { + let cx = ServerContext::new(self.current_directory.clone()); + *cx.client_capabilities.lock().unwrap() = self.client_capabilities.clone(); + *cx.client_info.lock().unwrap() = self.client_info.clone(); + *cx.options.write().unwrap() = self.options(); + *cx.resolver.lock().unwrap() = self.resolver.clone(); + Arc::new(cx) + } + + fn workspace(&self, cx: Arc<ServerContext>) -> Arc<dyn Workspace> { + let workspace = create_workspace_fast(cx).unwrap(); + for (name, source_code) in &self.files { + let uri = self.uri(name); + let path = uri.to_file_path().unwrap(); + let language = DocumentLanguage::by_path(&path).expect("unknown document language"); + workspace.open( + uri, + source_code.trim().to_string(), + language, + WorkspaceSource::Client, + ); + } + Arc::new(workspace) + } + + fn request<P>(&self, params: P) -> FeatureRequest<P> { + let cx = self.context(); + let workspace = self.workspace(Arc::clone(&cx)); + let subset = workspace.subset(self.uri(self.main)).unwrap(); + FeatureRequest { + context: cx, + workspace, + subset, + params, + } + } + + pub fn link(self) -> FeatureRequest<DocumentLinkParams> { + let text_document = self.identifier(); + let params = DocumentLinkParams { + text_document, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }; + self.request(params) + } + + pub fn folding(self) -> FeatureRequest<FoldingRangeParams> { + let text_document = self.identifier(); + let params = FoldingRangeParams { + text_document, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }; + self.request(params) + } + + pub fn reference(self) -> FeatureRequest<ReferenceParams> { + let params = ReferenceParams { + text_document_position: TextDocumentPositionParams::new( + self.identifier(), + Position::new(self.line, self.character), + ), + context: ReferenceContext { + include_declaration: self.include_declaration, + }, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }; + self.request(params) + } + + pub fn hover(self) -> FeatureRequest<HoverParams> { + let params = HoverParams { + text_document_position_params: TextDocumentPositionParams::new( + self.identifier(), + Position::new(self.line, self.character), + ), + work_done_progress_params: WorkDoneProgressParams::default(), + }; + self.request(params) + } + + pub fn completion(self) -> FeatureRequest<CompletionParams> { + let params = CompletionParams { + text_document_position: TextDocumentPositionParams::new( + self.identifier(), + Position::new(self.line, self.character), + ), + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + context: None, + }; + + self.request(params) + } + + pub fn definition(self) -> FeatureRequest<GotoDefinitionParams> { + let params = GotoDefinitionParams { + text_document_position_params: TextDocumentPositionParams::new( + self.identifier(), + Position::new(self.line, self.character), + ), + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }; + self.request(params) + } + + pub fn rename(self) -> FeatureRequest<RenameParams> { + let params = RenameParams { + text_document_position: TextDocumentPositionParams::new( + self.identifier(), + Position::new(self.line, self.character), + ), + new_name: self.new_name.to_string(), + work_done_progress_params: WorkDoneProgressParams::default(), + }; + self.request(params) + } + + pub fn formatting(self) -> FeatureRequest<DocumentFormattingParams> { + let params = DocumentFormattingParams { + text_document: self.identifier(), + work_done_progress_params: WorkDoneProgressParams::default(), + options: FormattingOptions::default(), + }; + self.request(params) + } + + pub fn highlight(self) -> FeatureRequest<DocumentHighlightParams> { + let params = DocumentHighlightParams { + text_document_position_params: TextDocumentPositionParams::new( + self.identifier(), + Position::new(self.line, self.character), + ), + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }; + self.request(params) + } + } +} diff --git a/support/texlab/src/features/build.rs b/support/texlab/src/features/build.rs new file mode 100644 index 0000000000..c6a670528f --- /dev/null +++ b/support/texlab/src/features/build.rs @@ -0,0 +1,133 @@ +use std::{ + io::{self, BufRead, BufReader, Read}, + path::Path, + process::{Command, Stdio}, + thread, +}; + +use cancellation::CancellationToken; +use crossbeam_channel::Sender; +use encoding_rs_io::DecodeReaderBytesBuilder; +use lsp_types::TextDocumentIdentifier; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::Options; + +use super::FeatureRequest; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildParams { + pub text_document: TextDocumentIdentifier, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] +#[repr(i32)] +pub enum BuildStatus { + Success = 0, + Error = 1, + Failure = 2, + Cancelled = 3, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildResult { + pub status: BuildStatus, +} + +pub fn build_document( + request: FeatureRequest<BuildParams>, + _cancellation_token: &CancellationToken, + log_sender: Sender<String>, +) -> BuildResult { + let document = request + .subset + .documents + .iter() + .find(|document| { + if let Some(data) = document.data.as_latex() { + data.extras.has_document_environment + } else { + false + } + }) + .map(|document| document.as_ref()) + .unwrap_or_else(|| request.main_document()); + + if document.uri.scheme() != "file" { + return BuildResult { + status: BuildStatus::Failure, + }; + } + log::info!("Building document {}", document.uri.as_str()); + + let options = { request.context.options.read().unwrap().clone() }; + + let status = match build_internal(&document.uri.to_file_path().unwrap(), &options, log_sender) { + Ok(true) => BuildStatus::Success, + Ok(false) => BuildStatus::Error, + Err(why) => { + log::error!("Failed to execute textDocument/build: {}", why); + BuildStatus::Failure + } + }; + BuildResult { status } +} + +fn build_internal(path: &Path, options: &Options, log_sender: Sender<String>) -> io::Result<bool> { + let build_dir = options + .root_directory + .as_ref() + .map(AsRef::as_ref) + .or_else(|| path.parent()) + .unwrap(); + + let args: Vec<_> = options + .build + .args() + .into_iter() + .map(|arg| replace_placeholder(arg, path)) + .collect(); + + let mut process = Command::new(options.build.executable()) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(build_dir) + .spawn()?; + + track_output(process.stdout.take().unwrap(), log_sender.clone()); + track_output(process.stderr.take().unwrap(), log_sender.clone()); + + if !options.build.is_continuous { + process.wait().map(|status| status.success()) + } else { + Ok(true) + } +} + +fn replace_placeholder(arg: String, file: &Path) -> String { + if arg.starts_with('"') || arg.ends_with('"') { + arg + } else { + arg.replace("%f", &file.to_string_lossy()) + } +} + +fn track_output(output: impl Read + Send + 'static, sender: Sender<String>) { + let reader = BufReader::new( + DecodeReaderBytesBuilder::new() + .encoding(Some(encoding_rs::UTF_8)) + .utf8_passthru(true) + .strip_bom(true) + .build(output), + ); + thread::spawn(move || { + for line in reader.lines() { + sender.send(line.unwrap()).unwrap(); + } + }); +} diff --git a/support/texlab/src/features/completion.rs b/support/texlab/src/features/completion.rs new file mode 100644 index 0000000000..13c7aad22b --- /dev/null +++ b/support/texlab/src/features/completion.rs @@ -0,0 +1,567 @@ +mod acronym_ref; +mod argument; +mod begin_command; +mod citation; +mod color; +mod color_model; +mod component_command; +mod component_environment; +mod entry_type; +mod field; +mod glossary_ref; +mod import; +mod include; +mod label; +mod theorem; +mod tikz_library; +mod types; +mod user_command; +mod user_environment; +mod util; + +use cancellation::CancellationToken; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use lsp_types::{ + CompletionItem, CompletionList, CompletionParams, CompletionTextEdit, Documentation, + InsertTextFormat, MarkupContent, MarkupKind, TextEdit, +}; +use rustc_hash::FxHashSet; + +use crate::{ + syntax::{bibtex, latex, CstNode}, + LineIndexExt, +}; + +use self::{ + acronym_ref::complete_acronyms, + argument::complete_arguments, + begin_command::complete_begin_command, + citation::complete_citations, + color::complete_colors, + color_model::complete_color_models, + component_command::complete_component_commands, + component_environment::complete_component_environments, + entry_type::complete_entry_types, + field::complete_fields, + glossary_ref::complete_glossary_entries, + import::complete_imports, + include::complete_includes, + label::complete_labels, + theorem::complete_theorem_environments, + tikz_library::complete_tikz_libraries, + types::{InternalCompletionItem, InternalCompletionItemData}, + user_command::complete_user_commands, + user_environment::complete_user_environments, + util::{adjust_kind, component_detail, image_documentation}, +}; + +pub use self::types::CompletionItemData; + +use super::{ + cursor::{Cursor, CursorContext}, + lsp_kinds::Structure, + FeatureRequest, +}; + +pub const COMPLETION_LIMIT: usize = 50; + +pub fn complete( + request: FeatureRequest<CompletionParams>, + cancellation_token: &CancellationToken, +) -> Option<CompletionList> { + let mut items = Vec::new(); + let context = CursorContext::new(request); + log::debug!("[Completion] Cursor: {:?}", context.cursor); + complete_entry_types(&context, &mut items, cancellation_token); + complete_fields(&context, &mut items, cancellation_token); + complete_arguments(&context, &mut items, cancellation_token); + complete_citations(&context, &mut items, cancellation_token); + complete_imports(&context, &mut items, cancellation_token); + complete_colors(&context, &mut items, cancellation_token); + complete_color_models(&context, &mut items, cancellation_token); + complete_acronyms(&context, &mut items, cancellation_token); + complete_glossary_entries(&context, &mut items, cancellation_token); + complete_includes(&context, &mut items, cancellation_token); + complete_labels(&context, &mut items, cancellation_token); + complete_tikz_libraries(&context, &mut items, cancellation_token); + complete_component_environments(&context, &mut items, cancellation_token); + complete_theorem_environments(&context, &mut items, cancellation_token); + complete_user_environments(&context, &mut items, cancellation_token); + complete_begin_command(&context, &mut items, cancellation_token); + complete_component_commands(&context, &mut items, cancellation_token); + complete_user_commands(&context, &mut items, cancellation_token); + + cancellation_token.result().ok()?; + + let mut items = dedup(items); + preselect(&context, &mut items); + score(&context, &mut items); + + items.sort_by_key(|item| (!item.preselect, -item.score.unwrap_or(std::i64::MIN + 1))); + let items: Vec<_> = items + .into_iter() + .take(COMPLETION_LIMIT) + .filter(|item| item.score.is_some()) + .map(|item| convert_internal_items(&context, item)) + .enumerate() + .map(|(i, item)| append_sort_text(item, i)) + .collect(); + + let is_incomplete = if context + .request + .context + .client_info + .lock() + .unwrap() + .as_ref() + .map(|info| info.name.as_str()) + .unwrap_or_default() + == "Visual Studio Code" + { + true + } else { + items.len() >= COMPLETION_LIMIT + }; + + Some(CompletionList { + is_incomplete, + items, + }) +} + +fn dedup(items: Vec<InternalCompletionItem>) -> Vec<InternalCompletionItem> { + let mut labels = FxHashSet::default(); + let mut insert = vec![false; items.len()]; + for (i, item) in items.iter().enumerate() { + insert[i] = labels.insert(item.data.label()); + } + items + .into_iter() + .enumerate() + .filter(|(i, _)| insert[*i]) + .map(|(_, item)| item) + .collect() +} + +fn score(context: &CursorContext<CompletionParams>, items: &mut Vec<InternalCompletionItem>) { + let pattern = match &context.cursor { + Cursor::Latex(token) if token.kind().is_command_name() => token.text().trim_end(), + Cursor::Latex(token) if token.kind() == latex::WORD => token.text(), + Cursor::Latex(_) => "", + Cursor::Bibtex(token) if token.kind().is_type() => token.text(), + Cursor::Bibtex(token) if token.kind() == bibtex::WORD => token.text(), + Cursor::Bibtex(token) if token.kind() == bibtex::COMMAND_NAME => token.text().trim_end(), + Cursor::Bibtex(_) => "", + Cursor::Nothing => "", + }; + + let file_pattern = pattern.split('/').last().unwrap(); + let matcher = SkimMatcherV2::default().ignore_case(); + for item in items { + item.score = match &item.data { + InternalCompletionItemData::EntryType { ty } => { + matcher.fuzzy_match(&ty.name, &pattern[1..]) + } + InternalCompletionItemData::Field { field } => { + matcher.fuzzy_match(&field.name, &pattern) + } + InternalCompletionItemData::Argument { name, .. } => { + matcher.fuzzy_match(name, &pattern) + } + InternalCompletionItemData::BeginCommand => matcher.fuzzy_match("begin", &pattern[1..]), + InternalCompletionItemData::Citation { key, .. } => matcher.fuzzy_match(&key, &pattern), + InternalCompletionItemData::ComponentCommand { name, .. } => { + matcher.fuzzy_match(name, &pattern[1..]) + } + InternalCompletionItemData::ComponentEnvironment { name, .. } => { + matcher.fuzzy_match(name, &pattern) + } + InternalCompletionItemData::Class { name } => matcher.fuzzy_match(&name, &pattern), + InternalCompletionItemData::Package { name } => matcher.fuzzy_match(&name, &pattern), + InternalCompletionItemData::Color { name } => matcher.fuzzy_match(&name, &pattern), + InternalCompletionItemData::ColorModel { name } => matcher.fuzzy_match(&name, &pattern), + InternalCompletionItemData::Acronym { name } => matcher.fuzzy_match(&name, &pattern), + InternalCompletionItemData::GlossaryEntry { name } => { + matcher.fuzzy_match(&name, &pattern) + } + InternalCompletionItemData::File { name } => matcher.fuzzy_match(&name, file_pattern), + InternalCompletionItemData::Directory { name } => { + matcher.fuzzy_match(&name, file_pattern) + } + InternalCompletionItemData::Label { name, .. } => matcher.fuzzy_match(&name, &pattern), + InternalCompletionItemData::UserCommand { name } => { + matcher.fuzzy_match(&name, &pattern[1..]) + } + InternalCompletionItemData::UserEnvironment { name } => { + matcher.fuzzy_match(&name, &pattern) + } + InternalCompletionItemData::PgfLibrary { name } => matcher.fuzzy_match(&name, &pattern), + InternalCompletionItemData::TikzLibrary { name } => { + matcher.fuzzy_match(&name, &pattern) + } + }; + } +} + +fn preselect( + context: &CursorContext<CompletionParams>, + items: &mut [InternalCompletionItem], +) -> Option<()> { + let name = context.cursor.as_latex()?; + let group = latex::CurlyGroupWord::cast(name.parent())?; + let end = latex::End::cast(group.syntax().parent()?)?; + let environment = latex::Environment::cast(end.syntax().parent()?)?; + let name = environment.begin()?.name()?.word()?.text(); + + for item in items { + if item.data.label() == name { + item.preselect = true; + } + } + Some(()) +} + +fn convert_internal_items( + context: &CursorContext<CompletionParams>, + item: InternalCompletionItem, +) -> CompletionItem { + let range = context + .request + .main_document() + .line_index + .line_col_lsp_range(item.range); + + let mut new_item = match item.data { + InternalCompletionItemData::EntryType { ty } => { + let text_edit = TextEdit::new(range, (&ty.name).into()); + let kind = Structure::Entry(ty.category).completion_kind(); + CompletionItem { + label: (&ty.name).into(), + kind: Some(adjust_kind(&context.request, kind)), + documentation: ty.documentation.as_ref().map(|doc| { + Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: doc.into(), + }) + }), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + data: Some(serde_json::to_value(CompletionItemData::EntryType).unwrap()), + ..CompletionItem::default() + } + } + InternalCompletionItemData::Field { field } => { + let text_edit = TextEdit::new(range, (&field.name).into()); + CompletionItem { + label: (&field.name).into(), + kind: Some(adjust_kind( + &context.request, + Structure::Field.completion_kind(), + )), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: (&field.documentation).into(), + })), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + data: Some(serde_json::to_value(CompletionItemData::FieldName).unwrap()), + ..CompletionItem::default() + } + } + InternalCompletionItemData::Argument { name, image } => { + let text_edit = TextEdit::new(range, name.into()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::Argument.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Argument).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + documentation: image + .and_then(|image| image_documentation(&context.request, &name, image)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::BeginCommand => { + if context + .request + .context + .client_capabilities + .lock() + .unwrap() + .text_document + .as_ref() + .and_then(|cap| cap.completion.as_ref()) + .and_then(|cap| cap.completion_item.as_ref()) + .and_then(|cap| cap.snippet_support) + == Some(true) + { + CompletionItem { + kind: Some(adjust_kind( + &context.request, + Structure::Snippet.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::CommandSnippet).unwrap()), + insert_text: Some("begin{$1}\n\t$0\n\\end{$1}".into()), + insert_text_format: Some(InsertTextFormat::Snippet), + ..CompletionItem::new_simple("begin".into(), component_detail(&[])) + } + } else { + let text_edit = TextEdit::new(range, "begin".to_string()); + CompletionItem { + kind: Some(adjust_kind( + &context.request, + Structure::Command.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::new_simple("begin".to_string(), component_detail(&[])) + } + } + } + InternalCompletionItemData::Citation { uri, key, text, ty } => { + let text_edit = TextEdit::new(range, key.to_string()); + CompletionItem { + label: key.to_string(), + kind: Some(adjust_kind(&context.request, ty.completion_kind())), + filter_text: Some(text.clone()), + sort_text: Some(text), + data: Some( + serde_json::to_value(CompletionItemData::Citation { + uri: uri.as_ref().clone(), + key: key.into(), + }) + .unwrap(), + ), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::ComponentCommand { + name, + image, + glyph, + file_names, + } => { + let detail = glyph.map_or_else( + || component_detail(file_names), + |glyph| format!("{}, {}", glyph, component_detail(file_names)), + ); + let documentation = + image.and_then(|img| image_documentation(&context.request, &name, img)); + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + kind: Some(adjust_kind( + &context.request, + Structure::Command.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()), + documentation, + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::new_simple(name.to_string(), detail) + } + } + InternalCompletionItemData::ComponentEnvironment { name, file_names } => { + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + kind: Some(adjust_kind( + &context.request, + Structure::Environment.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Environment).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::new_simple(name.to_string(), component_detail(file_names)) + } + } + InternalCompletionItemData::Class { name } => { + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::Class.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Class).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::Package { name } => { + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::Package.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Package).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::Color { name } => { + let text_edit = TextEdit::new(range, name.into()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::Color.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Color).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::ColorModel { name } => { + let text_edit = TextEdit::new(range, name.into()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::ColorModel.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::ColorModel).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::Acronym { name } => { + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::GlossaryEntry.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Acronym).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::GlossaryEntry { name } => { + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::GlossaryEntry.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::GlossaryEntry).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::File { name } => { + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::File.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::File).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::Directory { name } => { + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::Folder.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Folder).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::Label { + name, + kind, + header, + footer, + text, + } => { + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind(&context.request, kind.completion_kind())), + detail: header, + documentation: footer.map(Documentation::String), + sort_text: Some(text.clone()), + filter_text: Some(text), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + data: Some(serde_json::to_value(CompletionItemData::Label).unwrap()), + ..CompletionItem::default() + } + } + InternalCompletionItemData::UserCommand { name } => { + let detail = "user-defined".into(); + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + kind: Some(adjust_kind( + &context.request, + Structure::Command.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Command).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::new_simple(name.into(), detail) + } + } + InternalCompletionItemData::UserEnvironment { name } => { + let detail = "user-defined".into(); + let text_edit = TextEdit::new(range, name.to_string()); + CompletionItem { + kind: Some(adjust_kind( + &context.request, + Structure::Environment.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::Environment).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::new_simple(name.into(), detail) + } + } + InternalCompletionItemData::PgfLibrary { name } => { + let text_edit = TextEdit::new(range, name.into()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::PgfLibrary.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::PgfLibrary).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + InternalCompletionItemData::TikzLibrary { name } => { + let text_edit = TextEdit::new(range, name.into()); + CompletionItem { + label: name.into(), + kind: Some(adjust_kind( + &context.request, + Structure::TikzLibrary.completion_kind(), + )), + data: Some(serde_json::to_value(CompletionItemData::TikzLibrary).unwrap()), + text_edit: Some(CompletionTextEdit::Edit(text_edit)), + ..CompletionItem::default() + } + } + }; + new_item.preselect = Some(item.preselect); + new_item +} + +fn append_sort_text(mut item: CompletionItem, index: usize) -> CompletionItem { + let sort_prefix = format!("{:0>2}", index); + match &item.sort_text { + Some(sort_text) => { + item.sort_text = Some(format!("{} {}", sort_prefix, sort_text)); + } + None => { + item.sort_text = Some(sort_prefix); + } + }; + item +} diff --git a/support/texlab/src/features/completion/acronym_ref.rs b/support/texlab/src/features/completion/acronym_ref.rs new file mode 100644 index 0000000000..9d8bcc6c5f --- /dev/null +++ b/support/texlab/src/features/completion/acronym_ref.rs @@ -0,0 +1,169 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_acronyms<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let group = latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + + latex::AcronymReference::cast(group.syntax().parent()?)?; + + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + for name in data + .root + .descendants() + .filter_map(latex::AcronymDefinition::cast) + .filter_map(|node| node.name()) + .filter_map(|name| name.word()) + .map(|name| name.text()) + { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::Acronym { name }, + )); + } + } + } + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_acronyms(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_acronyms(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\acrshort{f}")]) + .main("main.tex") + .line(1) + .character(11) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_acronyms(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(87.into(), 88.into())); + } + } + + #[test] + fn test_empty() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\acrshort{}")]) + .main("main.tex") + .line(1) + .character(10) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_acronyms(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(87.into(), 87.into())); + } + } + + #[test] + fn test_after_group() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\acrshort{}")]) + .main("main.tex") + .line(1) + .character(11) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_acronyms(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_open_brace() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\acrshort{f")]) + .main("main.tex") + .line(1) + .character(11) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_acronyms(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(87.into(), 88.into())); + } + } +} diff --git a/support/texlab/src/features/completion/argument.rs b/support/texlab/src/features/completion/argument.rs new file mode 100644 index 0000000000..e2637e026f --- /dev/null +++ b/support/texlab/src/features/completion/argument.rs @@ -0,0 +1,189 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + component_db::COMPONENT_DATABASE, + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_arguments<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + let group = latex::CurlyGroup::cast(token.parent()) + .or_else(|| token.parent().parent().and_then(latex::CurlyGroup::cast)) + .filter(|group| context.is_inside_latex_curly(group))?; + + let command = latex::GenericCommand::cast(group.syntax().parent()?)?; + + let index = command + .syntax() + .children() + .filter_map(latex::CurlyGroup::cast) + .position(|g| g.syntax().text_range() == group.syntax().text_range())?; + + let command_name = &command.name()?.text()[1..]; + + for component in COMPONENT_DATABASE.linked_components(&context.request.subset) { + for component_command in component + .commands + .iter() + .filter(|command| command.name == command_name) + { + for (_, param) in component_command + .parameters + .iter() + .enumerate() + .filter(|(i, _)| *i == index) + { + for arg in ¶m.0 { + let item = InternalCompletionItem::new( + range, + InternalCompletionItemData::Argument { + name: &arg.name, + image: arg.image.as_deref(), + }, + ); + items.push(item); + } + } + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_arguments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_arguments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", r#"\mathbb{}\usepackage{amsfonts}"#)]) + .main("main.tex") + .line(0) + .character(8) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_arguments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(8.into(), 8.into())); + } + } + + #[test] + fn test_latex_word() { + let request = FeatureTester::builder() + .files(vec![("main.tex", r#"\mathbb{foo}\usepackage{amsfonts}"#)]) + .main("main.tex") + .line(0) + .character(8) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_arguments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(8.into(), 11.into())); + } + } + + #[test] + fn test_latex_open_brace() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\mathbb{ \\usepackage{amsfonts}")]) + .main("main.tex") + .line(0) + .character(8) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_arguments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(8.into(), 8.into())); + } + } + + #[test] + fn test_latex_open_brace_second() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\mathbb{}{\\usepackage{amsfonts}")]) + .main("main.tex") + .line(0) + .character(10) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_arguments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } +} diff --git a/support/texlab/src/features/completion/begin_command.rs b/support/texlab/src/features/completion/begin_command.rs new file mode 100644 index 0000000000..ebd272711e --- /dev/null +++ b/support/texlab/src/features/completion/begin_command.rs @@ -0,0 +1,84 @@ +use cancellation::CancellationToken; +use lsp_types::CompletionParams; + +use crate::features::cursor::CursorContext; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_begin_command( + context: &CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + let range = context.cursor.command_range(context.offset)?; + + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::BeginCommand, + )); + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_begin_command(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_begin_command(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\")]) + .main("main.tex") + .line(0) + .character(1) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_begin_command(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 1.into())); + } + } +} diff --git a/support/texlab/src/features/completion/citation.rs b/support/texlab/src/features/completion/citation.rs new file mode 100644 index 0000000000..6df3364099 --- /dev/null +++ b/support/texlab/src/features/completion/citation.rs @@ -0,0 +1,244 @@ +use std::sync::Arc; + +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; +use once_cell::sync::Lazy; +use regex::Regex; + +use crate::{ + features::{cursor::CursorContext, lsp_kinds::Structure}, + syntax::{ + bibtex::{self, HasType}, + latex, CstNode, + }, + BibtexEntryTypeCategory, Document, LANGUAGE_DATA, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_citations<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + let token = context.cursor.as_latex()?; + + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + check_citation(context, token).or_else(|| check_acronym(token))?; + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_bibtex() { + for entry in data.root.children().filter_map(bibtex::Entry::cast) { + cancellation_token.result().ok()?; + if let Some(item) = make_item(document, entry, range) { + items.push(item); + } + } + } + } + + Some(()) +} + +fn check_citation( + context: &CursorContext<CompletionParams>, + token: &latex::SyntaxToken, +) -> Option<()> { + let group = latex::CurlyGroupWordList::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + latex::Citation::cast(group.syntax().parent()?)?; + Some(()) +} + +fn check_acronym(token: &latex::SyntaxToken) -> Option<()> { + let pair = token.ancestors().find_map(latex::KeyValuePair::cast)?; + let mut key_words = pair.key()?.words(); + if key_words.next()?.text() != "cite" || key_words.next().is_some() { + return None; + } + + latex::AcronymDeclaration::cast(pair.syntax().parent()?.parent()?.parent()?)?; + Some(()) +} + +fn make_item<'a>( + document: &'a Document, + entry: bibtex::Entry<'a>, + range: TextRange, +) -> Option<InternalCompletionItem<'a>> { + let key = entry.key()?.text(); + let ty = LANGUAGE_DATA + .find_entry_type(&entry.ty()?.text()[1..]) + .map(|ty| Structure::Entry(ty.category)) + .unwrap_or_else(|| Structure::Entry(BibtexEntryTypeCategory::Misc)); + + let entry_code = entry.syntax().text().to_string(); + let text = format!( + "{} {}", + key, + WHITESPACE_REGEX + .replace_all( + &entry_code + .replace('{', "") + .replace('}', "") + .replace(',', " ") + .replace("=", " "), + " " + ) + .trim(), + ); + + Some(InternalCompletionItem::new( + range, + InternalCompletionItemData::Citation { + uri: Arc::clone(&document.uri), + key: key.into(), + text, + ty, + }, + )) +} + +static WHITESPACE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\s+").unwrap()); + +#[cfg(test)] +mod tests { + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_citations(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_citations(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple() { + let request = FeatureTester::builder() + .files(vec![ + ("main.tex", "\\addbibresource{main.bib}\n\\cite{}"), + ("main.bib", "@article{foo,}"), + ]) + .main("main.tex") + .line(1) + .character(6) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_citations(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(32.into(), 32.into())); + } + } + + #[test] + fn test_latex_open_brace() { + let request = FeatureTester::builder() + .files(vec![ + ("main.tex", "\\addbibresource{main.bib}\n\\cite{"), + ("main.bib", "@article{foo,}"), + ]) + .main("main.tex") + .line(1) + .character(6) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_citations(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(32.into(), 32.into())); + } + } + + #[test] + fn test_latex_open_brace_second() { + let request = FeatureTester::builder() + .files(vec![ + ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo,a"), + ("main.bib", "@article{foo,}"), + ]) + .main("main.tex") + .line(1) + .character(10) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_citations(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(36.into(), 37.into())); + } + } + + #[test] + fn test_latex_acronym() { + let request = FeatureTester::builder() + .files(vec![ + ( + "main.tex", + "\\addbibresource{main.bib}\n\\DeclareAcronym{foo}{cite={\n}}", + ), + ("main.bib", "@article{foo,}"), + ]) + .main("main.tex") + .line(2) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_citations(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(54.into(), 54.into())); + } + } +} diff --git a/support/texlab/src/features/completion/color.rs b/support/texlab/src/features/completion/color.rs new file mode 100644 index 0000000000..0e94af15fe --- /dev/null +++ b/support/texlab/src/features/completion/color.rs @@ -0,0 +1,122 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, + LANGUAGE_DATA, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_colors<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + let group = latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + latex::ColorReference::cast(group.syntax().parent()?)?; + + for name in &LANGUAGE_DATA.colors { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::Color { name }, + )); + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_colors(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_colors(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\color{}")]) + .main("main.tex") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_colors(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(7.into(), 7.into())); + } + } + + #[test] + fn test_latex_open_brace() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\color{")]) + .main("main.tex") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_colors(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(7.into(), 7.into())); + } + } +} diff --git a/support/texlab/src/features/completion/color_model.rs b/support/texlab/src/features/completion/color_model.rs new file mode 100644 index 0000000000..f7f09b6367 --- /dev/null +++ b/support/texlab/src/features/completion/color_model.rs @@ -0,0 +1,188 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +const MODEL_NAMES: &[&str] = &["gray", "rgb", "RGB", "HTML", "cmyk"]; + +pub fn complete_color_models<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + check_color_definition(context, token) + .or_else(|| check_color_definition_set(context, token))?; + + for name in MODEL_NAMES { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::ColorModel { name }, + )); + } + + Some(()) +} + +fn check_color_definition( + context: &CursorContext<CompletionParams>, + token: &latex::SyntaxToken, +) -> Option<()> { + let group = latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + let definition = latex::ColorDefinition::cast(group.syntax().parent()?)?; + definition + .model() + .filter(|model| model.syntax().text_range() == group.syntax().text_range())?; + Some(()) +} + +fn check_color_definition_set( + context: &CursorContext<CompletionParams>, + token: &latex::SyntaxToken, +) -> Option<()> { + let group = latex::CurlyGroupWordList::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + let definition = latex::ColorSetDefinition::cast(group.syntax().parent()?)?; + definition + .model_list() + .filter(|model| model.syntax().text_range() == group.syntax().text_range())?; + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_color_models(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_color_models(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_definition_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\definecolor{foo}{}")]) + .main("main.tex") + .line(0) + .character(18) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_color_models(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(18.into(), 18.into())); + } + } + + #[test] + fn test_definition_open_brace() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\definecolor{foo}{")]) + .main("main.tex") + .line(0) + .character(18) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_color_models(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(18.into(), 18.into())); + } + } + + #[test] + fn test_definition_set_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\definecolorset{}")]) + .main("main.tex") + .line(0) + .character(16) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_color_models(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(16.into(), 16.into())); + } + } + + #[test] + fn test_definition_set_open_brace() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\definecolorset{")]) + .main("main.tex") + .line(0) + .character(16) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_color_models(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(16.into(), 16.into())); + } + } +} diff --git a/support/texlab/src/features/completion/component_command.rs b/support/texlab/src/features/completion/component_command.rs new file mode 100644 index 0000000000..aba5a78597 --- /dev/null +++ b/support/texlab/src/features/completion/component_command.rs @@ -0,0 +1,177 @@ +use cancellation::CancellationToken; +use lsp_types::CompletionParams; + +use crate::{component_db::COMPONENT_DATABASE, features::cursor::CursorContext}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_component_commands<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let range = context.cursor.command_range(context.offset)?; + + for component in COMPONENT_DATABASE.linked_components(&context.request.subset) { + cancellation_token.result().ok()?; + for command in &component.commands { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::ComponentCommand { + name: &command.name, + image: command.image.as_deref(), + glyph: command.glyph.as_deref(), + file_names: &component.file_names, + }, + )); + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\")]) + .main("main.tex") + .line(0) + .character(1) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 1.into())); + } + } + + #[test] + fn test_latex_simple_before() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple_package() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\\n\\usepackage{lipsum}")]) + .main("main.tex") + .line(0) + .character(1) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in &actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 1.into())); + } + + assert!(actual_items + .iter() + .any(|item| item.data.label() == "lipsum")); + } + + #[test] + fn test_latex_simple_existing() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\foo")]) + .main("main.tex") + .line(0) + .character(2) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 4.into())); + } + } + + #[test] + fn test_bibtex_simple() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@a{b,c={\\ }}")]) + .main("main.bib") + .line(0) + .character(9) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(9.into(), 10.into())); + } + } +} diff --git a/support/texlab/src/features/completion/component_environment.rs b/support/texlab/src/features/completion/component_environment.rs new file mode 100644 index 0000000000..fca9d03780 --- /dev/null +++ b/support/texlab/src/features/completion/component_environment.rs @@ -0,0 +1,174 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + component_db::COMPONENT_DATABASE, + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_component_environments<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let group = latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + if !matches!(group.syntax().parent()?.kind(), latex::BEGIN | latex::END) { + return None; + } + + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + for component in COMPONENT_DATABASE.linked_components(&context.request.subset) { + cancellation_token.result().ok()?; + for name in &component.environments { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::ComponentEnvironment { + name, + file_names: &component.file_names, + }, + )); + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\begin{")]) + .main("main.tex") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(7.into(), 7.into())); + } + } + + #[test] + fn test_simple_end() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\begin{a}\n\\end{")]) + .main("main.tex") + .line(1) + .character(5) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(15.into(), 15.into())); + } + } + + #[test] + fn test_simple_class() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\begin{}\n\\documentclass{article}")]) + .main("main.tex") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in &actual_items { + assert_eq!(item.range, TextRange::new(7.into(), 7.into())); + } + + assert!(actual_items + .iter() + .any(|item| item.data.label() == "theindex")); + } + + #[test] + fn test_simple_existing() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\begin{d}")]) + .main("main.tex") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_component_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(7.into(), 8.into())); + } + } +} diff --git a/support/texlab/src/features/completion/entry_type.rs b/support/texlab/src/features/completion/entry_type.rs new file mode 100644 index 0000000000..c8774ea3ff --- /dev/null +++ b/support/texlab/src/features/completion/entry_type.rs @@ -0,0 +1,231 @@ +use cancellation::CancellationToken; +use cstree::{TextRange, TextSize}; +use lsp_types::CompletionParams; + +use crate::{features::cursor::CursorContext, LANGUAGE_DATA}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_entry_types<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + if cancellation_token.is_canceled() { + return None; + } + + let range = context + .cursor + .as_bibtex() + .filter(|token| token.kind().is_type()) + .filter(|token| token.text_range().start() != context.offset) + .map(|token| token.text_range()) + .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end()))?; + + for ty in &LANGUAGE_DATA.entry_types { + let data = InternalCompletionItemData::EntryType { ty }; + let item = InternalCompletionItem::new(range, data); + items.push(item); + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_at_empty() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@")]) + .main("main.bib") + .line(0) + .character(1) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 1.into())); + } + } + + #[test] + fn test_before_preamble() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@preamble")]) + .main("main.bib") + .line(0) + .character(1) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 9.into())); + } + } + + #[test] + fn test_before_string() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@string")]) + .main("main.bib") + .line(0) + .character(1) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 7.into())); + } + } + + #[test] + fn test_before_article() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article")]) + .main("main.bib") + .line(0) + .character(1) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 8.into())); + } + } + + #[test] + fn test_after_preamble() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@preamble{")]) + .main("main.bib") + .line(0) + .character(9) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 9.into())); + } + } + + #[test] + fn test_after_string() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@string{")]) + .main("main.bib") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 7.into())); + } + } + + #[test] + fn test_complete_entry() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, author = {foo}}")]) + .main("main.bib") + .line(0) + .character(3) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 8.into())); + } + } + + #[test] + fn test_complete_entry_field() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, author = {foo}}")]) + .main("main.bib") + .line(0) + .character(17) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_entry_types(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } +} diff --git a/support/texlab/src/features/completion/field.rs b/support/texlab/src/features/completion/field.rs new file mode 100644 index 0000000000..d7297b481b --- /dev/null +++ b/support/texlab/src/features/completion/field.rs @@ -0,0 +1,200 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{bibtex, CstNode}, + LANGUAGE_DATA, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_fields<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_bibtex()?; + let range = if token.kind() == bibtex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + let parent = token.parent(); + if let Some(entry) = bibtex::Entry::cast(parent) { + if entry.key()?.text_range() == token.text_range() { + return None; + } + } else { + bibtex::Field::cast(parent)?; + } + + for field in &LANGUAGE_DATA.fields { + let data = InternalCompletionItemData::Field { field }; + let item = InternalCompletionItem::new(range, data); + items.push(item); + } + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_fields(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_fields(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_entry_open() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo,")]) + .main("main.bib") + .line(0) + .character(13) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_fields(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(13.into(), 13.into())); + } + } + + #[test] + fn test_empty_entry_closed() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo,}")]) + .main("main.bib") + .line(0) + .character(13) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_fields(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(13.into(), 13.into())); + } + } + + #[test] + fn test_entry_field_name() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, a")]) + .main("main.bib") + .line(0) + .character(15) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_fields(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(14.into(), 15.into())); + } + } + + #[test] + fn test_entry_field_value() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, author = bar}")]) + .main("main.bib") + .line(0) + .character(24) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_fields(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_entry_two_fields_name_closed() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, author = bar, baz}")]) + .main("main.bib") + .line(0) + .character(29) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_fields(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(28.into(), 31.into())); + } + } + + #[test] + fn test_entry_two_fields_name_open() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, author = bar, baz")]) + .main("main.bib") + .line(0) + .character(29) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_fields(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(28.into(), 31.into())); + } + } +} diff --git a/support/texlab/src/features/completion/glossary_ref.rs b/support/texlab/src/features/completion/glossary_ref.rs new file mode 100644 index 0000000000..4529129fd7 --- /dev/null +++ b/support/texlab/src/features/completion/glossary_ref.rs @@ -0,0 +1,141 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_glossary_entries<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let group = latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + latex::GlossaryEntryReference::cast(group.syntax().parent()?)?; + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + for node in data.root.descendants() { + cancellation_token.result().ok()?; + + if let Some(name) = latex::GlossaryEntryDefinition::cast(node) + .and_then(|entry| entry.name()) + .and_then(|name| name.word()) + .map(|name| name.text()) + { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::GlossaryEntry { name }, + )); + } else if let Some(name) = latex::AcronymDefinition::cast(node) + .and_then(|entry| entry.name()) + .and_then(|name| name.word()) + .map(|name| name.text()) + { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::Acronym { name }, + )); + } + } + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_glossary_entries(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_glossary_entries(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\gls{f}")]) + .main("main.tex") + .line(1) + .character(6) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_glossary_entries(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(82.into(), 83.into())); + } + } + + #[test] + fn test_open_brace() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\n\\gls{f")]) + .main("main.tex") + .line(1) + .character(6) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_glossary_entries(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(82.into(), 83.into())); + } + } +} diff --git a/support/texlab/src/features/completion/import.rs b/support/texlab/src/features/completion/import.rs new file mode 100644 index 0000000000..ac5f2e6e40 --- /dev/null +++ b/support/texlab/src/features/completion/import.rs @@ -0,0 +1,196 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; +use rustc_hash::FxHashSet; +use smol_str::SmolStr; + +use crate::{ + component_db::COMPONENT_DATABASE, + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_imports<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + let token = context.cursor.as_latex()?; + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + let group = latex::CurlyGroupWordList::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + + let (extension, mut factory): ( + &str, + Box<dyn FnMut(SmolStr) -> InternalCompletionItemData<'a>>, + ) = match group.syntax().parent()?.kind() { + latex::PACKAGE_INCLUDE => ( + "sty", + Box::new(|name| InternalCompletionItemData::Package { name }), + ), + latex::CLASS_INCLUDE => ( + "cls", + Box::new(|name| InternalCompletionItemData::Class { name }), + ), + _ => return None, + }; + + let mut file_names = FxHashSet::default(); + for file_name in COMPONENT_DATABASE + .components + .iter() + .flat_map(|comp| comp.file_names.iter()) + .filter(|file_name| file_name.ends_with(extension)) + { + cancellation_token.result().ok()?; + file_names.insert(file_name); + let stem = &file_name[0..file_name.len() - 4]; + let data = factory(stem.into()); + let item = InternalCompletionItem::new(range, data); + items.push(item); + } + + let resolver = context.request.context.resolver.lock().unwrap(); + for file_name in resolver + .files_by_name + .keys() + .filter(|file_name| file_name.ends_with(extension) && !file_names.contains(file_name)) + { + cancellation_token.result().ok()?; + + let stem = &file_name[0..file_name.len() - 4]; + let data = factory(stem.into()); + let item = InternalCompletionItem::new(range, data); + items.push(item); + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_imports(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_imports(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple_package() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\usepackage{}")]) + .main("main.tex") + .line(0) + .character(12) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_imports(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(12.into(), 12.into())); + } + } + + #[test] + fn test_latex_open_brace_package() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\usepackage{ \\foo")]) + .main("main.tex") + .line(0) + .character(12) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_imports(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(12.into(), 12.into())); + } + } + + #[test] + fn test_latex_simple_class() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\documentclass{}")]) + .main("main.tex") + .line(0) + .character(15) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_imports(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(15.into(), 15.into())); + } + } + + #[test] + fn test_latex_open_brace_class() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\documentclass{ \\foo")]) + .main("main.tex") + .line(0) + .character(15) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_imports(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(15.into(), 15.into())); + } + } +} diff --git a/support/texlab/src/features/completion/include.rs b/support/texlab/src/features/completion/include.rs new file mode 100644 index 0000000000..c711ea5ce7 --- /dev/null +++ b/support/texlab/src/features/completion/include.rs @@ -0,0 +1,181 @@ +use std::{ + convert::TryFrom, + fs, + path::{Path, PathBuf}, +}; + +use cancellation::CancellationToken; +use cstree::{TextRange, TextSize}; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_includes<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + if context.request.main_document().uri.scheme() != "file" { + return None; + } + + let token = context.cursor.as_latex()?; + let (path_text, path_range) = if token.kind() == latex::WORD { + (token.text(), token.text_range()) + } else { + ("", TextRange::empty(context.offset)) + }; + + let group = latex::CurlyGroupWordList::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + let include = group.syntax().parent()?; + let (include_extension, extensions): (bool, &[&str]) = match include.kind() { + latex::PACKAGE_INCLUDE => (false, &["sty"]), + latex::CLASS_INCLUDE => (false, &["cls"]), + latex::LATEX_INCLUDE => { + let include = latex::Include::cast(include)?; + ( + matches!(include.command()?.text(), "\\input" | "\\subfile"), + &["tex"], + ) + } + latex::BIBLATEX_INCLUDE => (true, &["bib"]), + latex::BIBTEX_INCLUDE => (false, &["bib"]), + latex::GRAPHICS_INCLUDE => (true, &["pdf", "png", "jpg", "jpeg", "bmp"]), + latex::SVG_INCLUDE => (true, &["svg"]), + latex::INKSCAPE_INCLUDE => (true, &["pdf", "eps", "ps", "png"]), + latex::VERBATIM_INCLUDE => (true, &[]), + _ => return None, + }; + + let segment_range = if path_text.is_empty() { + path_range + } else { + let start = + path_range.end() - TextSize::try_from(path_text.split('/').last()?.len()).ok()?; + TextRange::new(start, path_range.end()) + }; + + let current_dir = current_dir(context, path_text)?; + for entry in fs::read_dir(current_dir).ok()?.filter_map(Result::ok) { + let mut path = entry.path(); + + let file_type = entry.file_type().ok()?; + if file_type.is_file() && is_included(&path, extensions) { + if !include_extension { + remove_extension(&mut path); + } + let name = path.file_name()?.to_str()?.into(); + let data = InternalCompletionItemData::File { name }; + let item = InternalCompletionItem::new(segment_range, data); + items.push(item); + } else if file_type.is_dir() { + let name = path.file_name()?.to_str()?.into(); + let data = InternalCompletionItemData::Directory { name }; + let item = InternalCompletionItem::new(segment_range, data); + items.push(item); + } + } + + Some(()) +} + +fn current_dir(context: &CursorContext<CompletionParams>, path_text: &str) -> Option<PathBuf> { + let mut path = context + .request + .context + .options + .read() + .unwrap() + .root_directory + .as_ref() + .map(|root_directory| { + context + .request + .context + .current_directory + .join(root_directory) + }) + .unwrap_or_else(|| { + let mut path = context.request.main_document().uri.to_file_path().unwrap(); + path.pop(); + path + }); + + path = PathBuf::from(path.to_str()?.replace('\\', "/")); + if !path_text.is_empty() { + path.push(&path_text); + if !path_text.ends_with('/') { + path.pop(); + } + } + Some(path) +} + +fn is_included(file: &Path, allowed_extensions: &[&str]) -> bool { + allowed_extensions.is_empty() + || file + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_lowercase()) + .map(|ext| allowed_extensions.contains(&ext.as_str())) + .unwrap_or_default() +} + +fn remove_extension(path: &mut PathBuf) { + if let Some(stem) = path + .file_stem() + .and_then(|stem| stem.to_str()) + .map(ToOwned::to_owned) + { + path.pop(); + path.push(stem); + } +} + +#[cfg(test)] +mod tests { + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_includes(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_includes(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } +} diff --git a/support/texlab/src/features/completion/label.rs b/support/texlab/src/features/completion/label.rs new file mode 100644 index 0000000000..2f7e2d8f77 --- /dev/null +++ b/support/texlab/src/features/completion/label.rs @@ -0,0 +1,196 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::{cursor::CursorContext, lsp_kinds::Structure}, + render_label, + syntax::{latex, CstNode}, + LabelledObject, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_labels<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let is_math = latex::CurlyGroupWordList::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group)) + .and_then(|group| group.syntax().parent()) + .and_then(|reference| latex::LabelReference::cast(reference)) + .and_then(|refernce| refernce.command()) + .map(|reference| reference.text() == "\\eqref") + .or_else(|| { + latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group)) + .and_then(|group| group.syntax().parent()) + .and_then(|reference| latex::LabelReferenceRange::cast(reference)) + .map(|_| false) + })?; + + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + for label in data + .root + .descendants() + .filter(|_| !cancellation_token.is_canceled()) + .filter_map(latex::LabelDefinition::cast) + { + if let Some(name) = label + .name() + .and_then(|name| name.word()) + .map(|name| name.text()) + { + match render_label(&context.request.subset, name, Some(label)) { + Some(rendered_label) => { + let kind = match &rendered_label.object { + LabelledObject::Section { .. } => Structure::Section, + LabelledObject::Float { .. } => Structure::Float, + LabelledObject::Theorem { .. } => Structure::Theorem, + LabelledObject::Equation => Structure::Equation, + LabelledObject::EnumItem => Structure::Item, + }; + + if is_math && kind != Structure::Equation { + continue; + } + + let header = rendered_label.detail(); + let footer = match &rendered_label.object { + LabelledObject::Float { caption, .. } => Some(caption.clone()), + _ => None, + }; + + let text = format!("{} {}", name, rendered_label.reference()); + + let item = InternalCompletionItem::new( + range, + InternalCompletionItemData::Label { + name, + kind, + header, + footer, + text, + }, + ); + items.push(item); + } + None => { + let kind = Structure::Label; + let header = None; + let footer = None; + let text = name.to_string(); + let item = InternalCompletionItem::new( + range, + InternalCompletionItemData::Label { + name, + kind, + header, + footer, + text, + }, + ); + items.push(item); + } + } + } + } + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_labels(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_labels(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\ref{}\\label{foo}")]) + .main("main.tex") + .line(0) + .character(5) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_labels(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(5.into(), 5.into())); + } + } + + #[test] + fn test_simple_range() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\crefrange{\n\\label{foo}")]) + .main("main.tex") + .line(0) + .character(11) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_labels(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(11.into(), 11.into())); + } + } +} diff --git a/support/texlab/src/features/completion/theorem.rs b/support/texlab/src/features/completion/theorem.rs new file mode 100644 index 0000000000..54db031a4b --- /dev/null +++ b/support/texlab/src/features/completion/theorem.rs @@ -0,0 +1,152 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_theorem_environments<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + token: &CancellationToken, +) -> Option<()> { + if token.is_canceled() { + return None; + } + + let token = context.cursor.as_latex()?; + let group = latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + if !matches!(group.syntax().parent()?.kind(), latex::BEGIN | latex::END) { + return None; + } + + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + for environment in &data.extras.theorem_environments { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::UserEnvironment { + name: &environment.name, + }, + )); + } + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_theorem_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_theorem_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\begin{ \\newtheorem{lemma}{Lemma}")]) + .main("main.tex") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_theorem_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(7.into(), 7.into())); + } + } + + #[test] + fn test_simple_end() { + let request = FeatureTester::builder() + .files(vec![( + "main.tex", + "\\newtheorem{lemma}{Lemma}\\begin{a}\n\\end{", + )]) + .main("main.tex") + .line(1) + .character(5) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_theorem_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(40.into(), 40.into())); + } + } + + #[test] + fn test_simple_existing() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\begin{d}\\newtheorem{lemma}{Lemma}")]) + .main("main.tex") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_theorem_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(7.into(), 8.into())); + } + } +} diff --git a/support/texlab/src/features/completion/tikz_library.rs b/support/texlab/src/features/completion/tikz_library.rs new file mode 100644 index 0000000000..da2a26cc9b --- /dev/null +++ b/support/texlab/src/features/completion/tikz_library.rs @@ -0,0 +1,169 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, + LANGUAGE_DATA, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_tikz_libraries<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + let group = latex::CurlyGroupWordList::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + let import = latex::TikzLibraryImport::cast(group.syntax().parent()?)?; + + if import.command()?.text() == "\\usepgflibrary" { + for name in &LANGUAGE_DATA.pgf_libraries { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::PgfLibrary { name }, + )); + } + } else { + for name in &LANGUAGE_DATA.tikz_libraries { + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::TikzLibrary { name }, + )); + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple_pgf() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\usepgflibrary{}")]) + .main("main.tex") + .line(0) + .character(15) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(15.into(), 15.into())); + } + } + + #[test] + fn test_latex_open_brace_pgf() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\usepgflibrary{")]) + .main("main.tex") + .line(0) + .character(15) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(15.into(), 15.into())); + } + } + + #[test] + fn test_latex_simple_tikz() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\usetikzlibrary{}")]) + .main("main.tex") + .line(0) + .character(16) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(16.into(), 16.into())); + } + } + + #[test] + fn test_latex_open_brace_tikz() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\usetikzlibrary{")]) + .main("main.tex") + .line(0) + .character(16) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_tikz_libraries(&context, &mut actual_items, CancellationToken::none()); + + assert!(!actual_items.is_empty()); + for item in actual_items { + assert_eq!(item.range, TextRange::new(16.into(), 16.into())); + } + } +} diff --git a/support/texlab/src/completion/types.rs b/support/texlab/src/features/completion/types.rs index f1b272f21e..b58a155fc4 100644 --- a/support/texlab/src/completion/types.rs +++ b/support/texlab/src/features/completion/types.rs @@ -1,20 +1,21 @@ -use crate::{ - protocol::{Range, Uri}, - syntax::{BibtexEntryTypeDoc, BibtexFieldDoc, Structure}, -}; +use std::sync::Arc; + +use cstree::TextRange; use serde::{Deserialize, Serialize}; -use std::borrow::Cow; +use smol_str::SmolStr; + +use crate::{features::lsp_kinds::Structure, BibtexEntryTypeDoc, BibtexFieldDoc, Uri}; #[derive(Debug, Clone)] -pub struct Item<'a> { - pub range: Range, - pub data: ItemData<'a>, +pub struct InternalCompletionItem<'a> { + pub range: TextRange, + pub data: InternalCompletionItemData<'a>, pub preselect: bool, pub score: Option<i64>, } -impl<'a> Item<'a> { - pub fn new(range: Range, data: ItemData<'a>) -> Self { +impl<'a> InternalCompletionItem<'a> { + pub fn new(range: TextRange, data: InternalCompletionItemData<'a>) -> Self { Self { range, data, @@ -25,98 +26,102 @@ impl<'a> Item<'a> { } #[derive(Debug, Clone)] -pub enum ItemData<'a> { - ComponentCommand { +pub enum InternalCompletionItemData<'a> { + EntryType { + ty: &'a BibtexEntryTypeDoc, + }, + Field { + field: &'a BibtexFieldDoc, + }, + Argument { name: &'a str, image: Option<&'a str>, + }, + BeginCommand, + Citation { + uri: Arc<Uri>, + key: &'a str, + text: String, + ty: Structure, + }, + ComponentCommand { + name: &'a SmolStr, + image: Option<&'a str>, glyph: Option<&'a str>, - file_names: &'a [String], + file_names: &'a [SmolStr], }, ComponentEnvironment { - name: &'a str, - file_names: &'a [String], + name: &'a SmolStr, + file_names: &'a [SmolStr], }, - UserCommand { - name: &'a str, + Class { + name: SmolStr, }, - UserEnvironment { - name: &'a str, + Package { + name: SmolStr, }, - Label { + Color { name: &'a str, - kind: Structure, - header: Option<String>, - footer: Option<String>, - text: String, }, - PgfLibrary { + ColorModel { name: &'a str, }, - TikzLibrary { + Acronym { name: &'a str, }, - Package { - name: Cow<'a, str>, - }, - Class { - name: Cow<'a, str>, + GlossaryEntry { + name: &'a str, }, File { - name: String, + name: SmolStr, }, Directory { - name: String, + name: SmolStr, }, - Citation { - uri: &'a Uri, - key: &'a str, + Label { + name: &'a str, + kind: Structure, + header: Option<String>, + footer: Option<String>, text: String, - ty: Structure, }, - Argument { + UserCommand { name: &'a str, - image: Option<&'a str>, }, - BeginCommand, - Color { + UserEnvironment { name: &'a str, }, - ColorModel { + PgfLibrary { name: &'a str, }, - GlossaryEntry { + TikzLibrary { name: &'a str, }, - EntryType { - ty: &'a BibtexEntryTypeDoc, - }, - Field { - field: &'a BibtexFieldDoc, - }, } -impl<'a> ItemData<'a> { - pub fn label<'b: 'a>(&'b self) -> &'b str { +impl<'a> InternalCompletionItemData<'a> { + pub fn label<'b: 'a>(&'b self) -> &'a str { match self { + Self::EntryType { ty } => &ty.name, + Self::Field { field } => &field.name, + Self::Argument { name, .. } => name, + Self::BeginCommand => "begin", + Self::Citation { key, .. } => &key, Self::ComponentCommand { name, .. } => name, Self::ComponentEnvironment { name, .. } => name, + Self::Class { name } => name, + Self::Package { name } => name, + Self::Color { name } => name, + Self::ColorModel { name } => name, + Self::Acronym { name } => name, + Self::GlossaryEntry { name } => name, + Self::File { name } => name, + Self::Directory { name } => name, + Self::Label { name, .. } => name, Self::UserCommand { name } => name, Self::UserEnvironment { name } => name, - Self::Label { name, .. } => name, - Self::Class { name } => &name, - Self::Package { name } => &name, Self::PgfLibrary { name } => name, Self::TikzLibrary { name } => name, - Self::File { name } => &name, - Self::Directory { name } => &name, - Self::Citation { key, .. } => key, - Self::Argument { name, .. } => name, - Self::BeginCommand => "begin", - Self::Color { name } => name, - Self::ColorModel { name } => name, - Self::GlossaryEntry { name } => name, - Self::EntryType { ty } => &ty.name, - Self::Field { field } => &field.name, } } } @@ -138,13 +143,8 @@ pub enum CompletionItemData { Class, EntryType, FieldName, - Citation { uri: Uri, key: String }, + Citation { uri: Uri, key: SmolStr }, Argument, + Acronym, GlossaryEntry, } - -impl Into<serde_json::Value> for CompletionItemData { - fn into(self) -> serde_json::Value { - serde_json::to_value(self).unwrap() - } -} diff --git a/support/texlab/src/features/completion/user_command.rs b/support/texlab/src/features/completion/user_command.rs new file mode 100644 index 0000000000..8b4473f31c --- /dev/null +++ b/support/texlab/src/features/completion/user_command.rs @@ -0,0 +1,100 @@ +use cancellation::CancellationToken; +use lsp_types::CompletionParams; + +use crate::features::cursor::CursorContext; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_user_commands<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let range = context.cursor.command_range(context.offset)?; + let token = context.cursor.as_latex()?; + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + for name in data + .extras + .command_names + .iter() + .filter(|name| name.as_str() != token.text()) + .map(|name| &name[1..]) + { + cancellation_token.result().ok()?; + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::UserCommand { name }, + )); + } + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_user_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_user_commands(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "\\foo\\bar")]) + .main("main.tex") + .line(0) + .character(4) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_user_commands(&context, &mut actual_items, CancellationToken::none()); + + assert_eq!(actual_items.len(), 1); + for item in actual_items { + assert_eq!(item.range, TextRange::new(1.into(), 4.into())); + } + } +} diff --git a/support/texlab/src/features/completion/user_environment.rs b/support/texlab/src/features/completion/user_environment.rs new file mode 100644 index 0000000000..a1a5701fac --- /dev/null +++ b/support/texlab/src/features/completion/user_environment.rs @@ -0,0 +1,116 @@ +use cancellation::CancellationToken; +use cstree::TextRange; +use lsp_types::CompletionParams; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, +}; + +use super::types::{InternalCompletionItem, InternalCompletionItemData}; + +pub fn complete_user_environments<'a>( + context: &'a CursorContext<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + cancellation_token: &CancellationToken, +) -> Option<()> { + cancellation_token.result().ok()?; + + let token = context.cursor.as_latex()?; + let group = latex::CurlyGroupWord::cast(token.parent()) + .filter(|group| context.is_inside_latex_curly(group))?; + if !matches!(group.syntax().parent()?.kind(), latex::BEGIN | latex::END) { + return None; + } + + let range = if token.kind() == latex::WORD { + token.text_range() + } else { + TextRange::empty(context.offset) + }; + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + for name in data + .extras + .environment_names + .iter() + .filter(|name| name.as_str() != token.text()) + { + cancellation_token.result().ok()?; + items.push(InternalCompletionItem::new( + range, + InternalCompletionItemData::UserEnvironment { name }, + )); + } + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use cstree::TextRange; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_user_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_user_environments(&context, &mut actual_items, CancellationToken::none()); + + assert!(actual_items.is_empty()); + } + + #[test] + fn test_latex_simple() { + let request = FeatureTester::builder() + .files(vec![( + "main.tex", + "\\begin{foo}\\end{foo} \\begin{bar}\\end{bar}", + )]) + .main("main.tex") + .line(0) + .character(7) + .build() + .completion(); + + let context = CursorContext::new(request); + let mut actual_items = Vec::new(); + complete_user_environments(&context, &mut actual_items, CancellationToken::none()); + + assert_eq!(actual_items.len(), 1); + for item in actual_items { + assert_eq!(item.range, TextRange::new(7.into(), 10.into())); + } + } +} diff --git a/support/texlab/src/features/completion/util.rs b/support/texlab/src/features/completion/util.rs new file mode 100644 index 0000000000..296d6d25d9 --- /dev/null +++ b/support/texlab/src/features/completion/util.rs @@ -0,0 +1,66 @@ +use lsp_types::{CompletionItemKind, CompletionParams, Documentation, MarkupContent, MarkupKind}; +use smol_str::SmolStr; + +use crate::features::FeatureRequest; + +pub fn component_detail(file_names: &[SmolStr]) -> String { + if file_names.is_empty() { + "built-in".to_owned() + } else { + file_names.join(", ") + } +} + +pub fn image_documentation( + request: &FeatureRequest<CompletionParams>, + name: &str, + image: &str, +) -> Option<Documentation> { + if supports_images(request) { + Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format!( + "![{}](data:image/png;base64,{}|width=48,height=48)", + name, image + ), + })) + } else { + None + } +} + +fn supports_images(request: &FeatureRequest<CompletionParams>) -> bool { + request + .context + .client_capabilities + .lock() + .unwrap() + .text_document + .as_ref() + .and_then(|cap| cap.completion.as_ref()) + .and_then(|cap| cap.completion_item.as_ref()) + .and_then(|cap| cap.documentation_format.as_ref()) + .map_or(true, |formats| formats.contains(&MarkupKind::Markdown)) +} + +pub fn adjust_kind( + request: &FeatureRequest<CompletionParams>, + kind: CompletionItemKind, +) -> CompletionItemKind { + if let Some(value_set) = request + .context + .client_capabilities + .lock() + .unwrap() + .text_document + .as_ref() + .and_then(|cap| cap.completion.as_ref()) + .and_then(|cap| cap.completion_item_kind.as_ref()) + .and_then(|cap| cap.value_set.as_ref()) + { + if value_set.contains(&kind) { + return kind; + } + } + CompletionItemKind::Text +} diff --git a/support/texlab/src/features/cursor.rs b/support/texlab/src/features/cursor.rs new file mode 100644 index 0000000000..19079b841b --- /dev/null +++ b/support/texlab/src/features/cursor.rs @@ -0,0 +1,252 @@ +use cstree::{TextRange, TextSize}; +use lsp_types::{ + CompletionParams, GotoDefinitionParams, HoverParams, Position, ReferenceParams, RenameParams, + TextDocumentPositionParams, +}; + +use crate::{ + syntax::{bibtex, latex, CstNode}, + DocumentData, LineIndexExt, +}; + +use super::FeatureRequest; + +#[derive(Debug)] +pub enum Cursor { + Latex(latex::SyntaxToken), + Bibtex(bibtex::SyntaxToken), + Nothing, +} + +impl Cursor { + pub fn new_latex( + left: Option<latex::SyntaxToken>, + right: Option<latex::SyntaxToken>, + ) -> Option<Self> { + let left = left?; + let right = right?; + + if left.kind().is_command_name() { + return Some(Self::Latex(left)); + } + + if right.kind() == latex::WORD { + return Some(Self::Latex(right)); + } + + if left.kind() == latex::WORD { + return Some(Self::Latex(left)); + } + + if right.kind().is_command_name() { + return Some(Self::Latex(right)); + } + + Some(Self::Latex(right)) + } + + pub fn new_bibtex( + left: Option<bibtex::SyntaxToken>, + right: Option<bibtex::SyntaxToken>, + ) -> Option<Self> { + let left = left?; + let right = right?; + + if right.kind().is_type() { + return Some(Self::Bibtex(right)); + } + + if left.kind().is_type() { + return Some(Self::Bibtex(left)); + } + + if left.kind() == bibtex::COMMAND_NAME { + return Some(Self::Bibtex(left)); + } + + if right.kind() == bibtex::WORD { + return Some(Self::Bibtex(right)); + } + + if left.kind() == bibtex::WORD { + return Some(Self::Bibtex(left)); + } + + if right.kind() == bibtex::COMMAND_NAME { + return Some(Self::Bibtex(right)); + } + + Some(Self::Bibtex(right)) + } + + pub fn as_latex(&self) -> Option<&latex::SyntaxToken> { + if let Self::Latex(v) = self { + Some(v) + } else { + None + } + } + + pub fn as_bibtex(&self) -> Option<&bibtex::SyntaxToken> { + if let Self::Bibtex(v) = self { + Some(v) + } else { + None + } + } + + pub fn command_range(&self, offset: TextSize) -> Option<TextRange> { + self.as_latex() + .filter(|token| token.kind().is_command_name()) + .filter(|token| token.text_range().start() != offset) + .map(|token| token.text_range()) + .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end())) + .or_else(|| { + self.as_bibtex() + .filter(|token| token.kind() == bibtex::COMMAND_NAME) + .filter(|token| token.text_range().start() != offset) + .map(|token| token.text_range()) + .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end())) + }) + } +} + +pub struct CursorContext<P> { + pub request: FeatureRequest<P>, + pub cursor: Cursor, + pub offset: TextSize, +} + +impl<P: HasPosition> CursorContext<P> { + pub fn new(request: FeatureRequest<P>) -> Self { + let main_document = request.main_document(); + let offset = main_document + .line_index + .offset_lsp(request.params.position()); + + let cursor = match &main_document.data { + DocumentData::Latex(data) => { + let left = data.root.token_at_offset(offset).left_biased(); + let right = data.root.token_at_offset(offset).right_biased(); + Cursor::new_latex(left, right) + } + DocumentData::Bibtex(data) => { + let left = data.root.token_at_offset(offset).left_biased(); + let right = data.root.token_at_offset(offset).right_biased(); + Cursor::new_bibtex(left, right) + } + DocumentData::BuildLog(_) => None, + }; + + Self { + request, + cursor: cursor.unwrap_or(Cursor::Nothing), + offset, + } + } + + pub fn is_inside_latex_curly<'a>(&self, group: &impl latex::HasCurly<'a>) -> bool { + group.small_range().contains(self.offset) || group.right_curly().is_none() + } + + pub fn find_citation_key_word(&self) -> Option<(&str, TextRange)> { + let key = self + .cursor + .as_latex() + .filter(|token| token.kind() == latex::WORD)?; + + let group = latex::CurlyGroupWordList::cast(key.parent())?; + latex::Citation::cast(group.syntax().parent()?)?; + Some((key.text(), key.text_range())) + } + + pub fn find_citation_key_command(&self) -> Option<(&str, TextRange)> { + let command = self.cursor.as_latex()?; + + let citation = latex::Citation::cast(command.parent())?; + let key = citation.key_list()?.words().next()?; + Some((key.text(), key.text_range())) + } + + pub fn find_entry_key(&self) -> Option<(&str, TextRange)> { + let key = self + .cursor + .as_bibtex() + .filter(|token| token.kind() == bibtex::WORD)?; + + bibtex::Entry::cast(key.parent())?; + Some((key.text(), key.text_range())) + } + + pub fn find_label_name_word(&self) -> Option<(&str, TextRange)> { + let name = self + .cursor + .as_latex() + .filter(|token| token.kind() == latex::WORD)?; + + if matches!( + name.parent().parent()?.kind(), + latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE + ) { + Some((name.text(), name.text_range())) + } else { + None + } + } + + pub fn find_label_name_command(&self) -> Option<(&str, TextRange)> { + let node = self.cursor.as_latex()?.parent(); + if let Some(label) = latex::LabelDefinition::cast(node) { + let name = label.name()?.word()?; + Some((name.text(), name.text_range())) + } else if let Some(label) = latex::LabelReference::cast(node) { + let name = label.name_list()?.words().next()?; + Some((name.text(), name.text_range())) + } else if let Some(label) = latex::LabelReferenceRange::cast(node) { + let name = label.from()?.word()?; + Some((name.text(), name.text_range())) + } else { + None + } + } +} + +pub trait HasPosition { + fn position(&self) -> Position; +} + +impl HasPosition for CompletionParams { + fn position(&self) -> Position { + self.text_document_position.position + } +} + +impl HasPosition for TextDocumentPositionParams { + fn position(&self) -> Position { + self.position + } +} + +impl HasPosition for RenameParams { + fn position(&self) -> Position { + self.text_document_position.position + } +} + +impl HasPosition for ReferenceParams { + fn position(&self) -> Position { + self.text_document_position.position + } +} + +impl HasPosition for HoverParams { + fn position(&self) -> Position { + self.text_document_position_params.position + } +} + +impl HasPosition for GotoDefinitionParams { + fn position(&self) -> Position { + self.text_document_position_params.position + } +} diff --git a/support/texlab/src/features/definition.rs b/support/texlab/src/features/definition.rs new file mode 100644 index 0000000000..c49f172399 --- /dev/null +++ b/support/texlab/src/features/definition.rs @@ -0,0 +1,29 @@ +mod command; +mod document; +mod entry; +mod label; +mod string; + +use cancellation::CancellationToken; +use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse}; + +use self::{ + command::goto_command_definition, document::goto_document_definition, + entry::goto_entry_definition, label::goto_label_definition, string::goto_string_definition, +}; + +use super::{cursor::CursorContext, FeatureRequest}; + +pub fn goto_definition( + request: FeatureRequest<GotoDefinitionParams>, + cancellation_token: &CancellationToken, +) -> Option<GotoDefinitionResponse> { + let context = CursorContext::new(request); + log::debug!("[Definition] Cursor: {:?}", context.cursor); + let links = goto_command_definition(&context, cancellation_token) + .or_else(|| goto_document_definition(&context, cancellation_token)) + .or_else(|| goto_entry_definition(&context, cancellation_token)) + .or_else(|| goto_label_definition(&context, cancellation_token)) + .or_else(|| goto_string_definition(&context, cancellation_token))?; + Some(GotoDefinitionResponse::Link(links)) +} diff --git a/support/texlab/src/features/definition/command.rs b/support/texlab/src/features/definition/command.rs new file mode 100644 index 0000000000..a69796c33f --- /dev/null +++ b/support/texlab/src/features/definition/command.rs @@ -0,0 +1,131 @@ +use cancellation::CancellationToken; +use lsp_types::{GotoDefinitionParams, LocationLink}; + +use crate::{ + features::cursor::CursorContext, + syntax::{latex, CstNode}, + LineIndexExt, +}; + +pub fn goto_command_definition( + context: &CursorContext<GotoDefinitionParams>, + cancellation_token: &CancellationToken, +) -> Option<Vec<LocationLink>> { + let main_document = context.request.main_document(); + + let name = context + .cursor + .as_latex() + .filter(|token| token.kind().is_command_name())?; + + let origin_selection_range = main_document + .line_index + .line_col_lsp_range(name.text_range()); + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + for node in data.root.descendants() { + cancellation_token.result().ok()?; + + if let Some(defintion) = latex::CommandDefinition::cast(node).filter(|def| { + def.name() + .and_then(|name| name.command()) + .map(|name| name.text()) + == Some(name.text()) + }) { + let target_selection_range = document + .line_index + .line_col_lsp_range(defintion.name()?.command()?.text_range()); + + let target_range = document + .line_index + .line_col_lsp_range(defintion.small_range()); + + return Some(vec![LocationLink { + origin_selection_range: Some(origin_selection_range), + target_uri: document.uri.as_ref().clone().into(), + target_range, + target_selection_range, + }]); + } + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + + let actual_links = goto_command_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_command_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_command_definition() { + let tester = FeatureTester::builder() + .files(vec![( + "main.tex", + indoc! { + r#" + \DeclareMathOperator{\foo}{foo} + \foo + "# + }, + )]) + .main("main.tex") + .line(1) + .character(2) + .build(); + let target_uri = tester.uri("main.tex").as_ref().clone().into(); + + let request = tester.definition(); + let context = CursorContext::new(request); + let actual_links = goto_command_definition(&context, CancellationToken::none()).unwrap(); + + let expected_links = vec![LocationLink { + origin_selection_range: Some(Range::new_simple(1, 0, 1, 4)), + target_uri, + target_range: Range::new_simple(0, 0, 0, 31), + target_selection_range: Range::new_simple(0, 21, 0, 25), + }]; + + assert_eq!(actual_links, expected_links); + } +} diff --git a/support/texlab/src/features/definition/document.rs b/support/texlab/src/features/definition/document.rs new file mode 100644 index 0000000000..b2b713f260 --- /dev/null +++ b/support/texlab/src/features/definition/document.rs @@ -0,0 +1,112 @@ +use cancellation::CancellationToken; +use lsp_types::{GotoDefinitionParams, LocationLink, Range}; + +use crate::{features::cursor::CursorContext, LineIndexExt, RangeExt}; + +pub fn goto_document_definition( + context: &CursorContext<GotoDefinitionParams>, + cancellation_token: &CancellationToken, +) -> Option<Vec<LocationLink>> { + let main_document = context.request.main_document(); + if let Some(data) = main_document.data.as_latex() { + for include in data + .extras + .explicit_links + .iter() + .filter(|link| link.stem_range.contains_inclusive(context.offset)) + { + for target in &include.targets { + cancellation_token.result().ok()?; + + if context + .request + .subset + .documents + .iter() + .any(|document| document.uri.as_ref() == target.as_ref()) + { + return Some(vec![LocationLink { + origin_selection_range: Some( + main_document + .line_index + .line_col_lsp_range(include.stem_range), + ), + target_uri: target.as_ref().clone().into(), + target_range: Range::new_simple(0, 0, 0, 0), + target_selection_range: Range::new_simple(0, 0, 0, 0), + }]); + } + } + } + } + None +} + +#[cfg(test)] +mod tests { + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_document_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_document_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_simple() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\addbibresource{baz.bib}"#), + ("bar.bib", r#"@article{foo, bar = {baz}}"#), + ("baz.bib", r#"@article{foo, bar = {baz}}"#), + ]) + .main("foo.tex") + .line(0) + .character(18) + .build(); + let target_uri = tester.uri("baz.bib").as_ref().clone().into(); + + let request = tester.definition(); + let context = CursorContext::new(request); + let actual_links = goto_document_definition(&context, CancellationToken::none()).unwrap(); + + let expected_links = vec![LocationLink { + origin_selection_range: Some(Range::new_simple(0, 16, 0, 23)), + target_uri, + target_range: Range::new_simple(0, 0, 0, 0), + target_selection_range: Range::new_simple(0, 0, 0, 0), + }]; + + assert_eq!(actual_links, expected_links); + } +} diff --git a/support/texlab/src/features/definition/entry.rs b/support/texlab/src/features/definition/entry.rs new file mode 100644 index 0000000000..7f489ec848 --- /dev/null +++ b/support/texlab/src/features/definition/entry.rs @@ -0,0 +1,125 @@ +use cancellation::CancellationToken; +use lsp_types::{GotoDefinitionParams, LocationLink}; + +use crate::{ + features::cursor::CursorContext, + syntax::{bibtex, latex, CstNode}, + LineIndexExt, +}; + +pub fn goto_entry_definition( + context: &CursorContext<GotoDefinitionParams>, + cancellation_token: &CancellationToken, +) -> Option<Vec<LocationLink>> { + let main_document = context.request.main_document(); + + let key = context + .cursor + .as_latex() + .filter(|token| token.kind() == latex::WORD)?; + + latex::Citation::cast(key.parent().parent()?)?; + + let origin_selection_range = main_document + .line_index + .line_col_lsp_range(key.text_range()); + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_bibtex() { + for entry in data.root.children().filter_map(bibtex::Entry::cast) { + cancellation_token.result().ok()?; + + if let Some(key) = entry.key().filter(|k| k.text() == key.text()) { + return Some(vec![LocationLink { + origin_selection_range: Some(origin_selection_range), + target_uri: document.uri.as_ref().clone().into(), + target_selection_range: document + .line_index + .line_col_lsp_range(key.text_range()), + target_range: document.line_index.line_col_lsp_range(entry.small_range()), + }]); + } + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_entry_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_entry_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_simple() { + let tester = FeatureTester::builder() + .files(vec![ + ( + "foo.tex", + indoc!( + r#" + \addbibresource{baz.bib} + \cite{foo} + "# + ), + ), + ("bar.bib", r#"@article{foo, bar = {baz}}"#), + ("baz.bib", r#"@article{foo, bar = {baz}}"#), + ]) + .main("foo.tex") + .line(1) + .character(6) + .build(); + let target_uri = tester.uri("baz.bib").as_ref().clone().into(); + + let request = tester.definition(); + let context = CursorContext::new(request); + let actual_links = goto_entry_definition(&context, CancellationToken::none()).unwrap(); + + let expected_links = vec![LocationLink { + origin_selection_range: Some(Range::new_simple(1, 6, 1, 9)), + target_uri, + target_range: Range::new_simple(0, 0, 0, 26), + target_selection_range: Range::new_simple(0, 9, 0, 12), + }]; + + assert_eq!(actual_links, expected_links); + } +} diff --git a/support/texlab/src/features/definition/label.rs b/support/texlab/src/features/definition/label.rs new file mode 100644 index 0000000000..65f0981de0 --- /dev/null +++ b/support/texlab/src/features/definition/label.rs @@ -0,0 +1,80 @@ +use cancellation::CancellationToken; +use lsp_types::{GotoDefinitionParams, LocationLink}; + +use crate::{features::cursor::CursorContext, find_label_definition, render_label, LineIndexExt}; + +pub fn goto_label_definition( + context: &CursorContext<GotoDefinitionParams>, + cancellation_token: &CancellationToken, +) -> Option<Vec<LocationLink>> { + let main_document = context.request.main_document(); + + let (name_text, name_range) = context + .find_label_name_word() + .or_else(|| context.find_label_name_command())?; + + let origin_selection_range = main_document.line_index.line_col_lsp_range(name_range); + + for document in &context.request.subset.documents { + cancellation_token.result().ok()?; + if let Some(data) = document.data.as_latex() { + if let Some(definition) = find_label_definition(&data.root, name_text) { + let target_selection_range = definition.name()?.word()?.text_range(); + let target_range = + render_label(&context.request.subset, name_text, Some(definition)) + .map(|label| label.range) + .unwrap_or(target_selection_range); + + return Some(vec![LocationLink { + origin_selection_range: Some(origin_selection_range), + target_uri: document.uri.as_ref().clone().into(), + target_range: document.line_index.line_col_lsp_range(target_range), + target_selection_range: document + .line_index + .line_col_lsp_range(target_selection_range), + }]); + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_label_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_label_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } +} diff --git a/support/texlab/src/features/definition/string.rs b/support/texlab/src/features/definition/string.rs new file mode 100644 index 0000000000..7162c8b722 --- /dev/null +++ b/support/texlab/src/features/definition/string.rs @@ -0,0 +1,176 @@ +use cancellation::CancellationToken; +use lsp_types::{GotoDefinitionParams, LocationLink}; + +use crate::{ + features::cursor::CursorContext, + syntax::{bibtex, CstNode}, + LineIndexExt, +}; + +pub fn goto_string_definition( + context: &CursorContext<GotoDefinitionParams>, + cancellation_token: &CancellationToken, +) -> Option<Vec<LocationLink>> { + let main_document = context.request.main_document(); + + let data = main_document.data.as_bibtex()?; + let name = context + .cursor + .as_bibtex() + .filter(|token| token.kind() == bibtex::WORD)?; + + bibtex::Token::cast(name.parent())?; + + let origin_selection_range = main_document + .line_index + .line_col_lsp_range(name.text_range()); + + for string in data.root.children().filter_map(bibtex::String::cast) { + cancellation_token.result().ok()?; + + if let Some(string_name) = string.name().filter(|n| n.text() == name.text()) { + return Some(vec![LocationLink { + origin_selection_range: Some(origin_selection_range), + target_uri: main_document.uri.as_ref().clone().into(), + target_selection_range: main_document + .line_index + .line_col_lsp_range(string_name.text_range()), + target_range: main_document + .line_index + .line_col_lsp_range(string.small_range()), + }]); + } + } + + None +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_string_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .definition(); + + let context = CursorContext::new(request); + let actual_links = goto_string_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_simple() { + let tester = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { + r#" + @string{foo = {bar}} + @article{bar, author = foo} + "# + }, + )]) + .main("main.bib") + .line(1) + .character(24) + .build(); + let target_uri = tester.uri("main.bib").as_ref().clone().into(); + + let request = tester.definition(); + let context = CursorContext::new(request); + let actual_links = goto_string_definition(&context, CancellationToken::none()).unwrap(); + + let expected_links = vec![LocationLink { + origin_selection_range: Some(Range::new_simple(1, 23, 1, 26)), + target_uri, + target_range: Range::new_simple(0, 0, 0, 20), + target_selection_range: Range::new_simple(0, 8, 0, 11), + }]; + + assert_eq!(actual_links, expected_links); + } + + #[test] + fn concat() { + let tester = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { + r#" + @string{foo = {bar}} + @article{bar, author = foo # "bar"} + "# + }, + )]) + .main("main.bib") + .line(1) + .character(24) + .build(); + let target_uri = tester.uri("main.bib").as_ref().clone().into(); + + let request = tester.definition(); + let context = CursorContext::new(request); + let actual_links = goto_string_definition(&context, CancellationToken::none()).unwrap(); + + let expected_links = vec![LocationLink { + origin_selection_range: Some(Range::new_simple(1, 23, 1, 26)), + target_uri, + target_range: Range::new_simple(0, 0, 0, 20), + target_selection_range: Range::new_simple(0, 8, 0, 11), + }]; + + assert_eq!(actual_links, expected_links); + } + + #[test] + fn test_field() { + let tester = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { + r#" + @string{foo = {bar}} + @article{bar, author = foo} + "# + }, + )]) + .main("main.bib") + .line(1) + .character(18) + .build(); + + let request = tester.definition(); + let context = CursorContext::new(request); + let actual_links = goto_string_definition(&context, CancellationToken::none()); + + assert!(actual_links.is_none()); + } +} diff --git a/support/texlab/src/features/folding.rs b/support/texlab/src/features/folding.rs new file mode 100644 index 0000000000..0c44b6aa61 --- /dev/null +++ b/support/texlab/src/features/folding.rs @@ -0,0 +1,237 @@ +use cancellation::CancellationToken; +use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams, Range}; + +use crate::{ + syntax::{bibtex, latex, CstNode}, + DocumentData, LineIndexExt, +}; + +use super::FeatureRequest; + +pub fn find_foldings( + request: FeatureRequest<FoldingRangeParams>, + token: &CancellationToken, +) -> Vec<FoldingRange> { + let mut foldings = Vec::new(); + let main_document = request.main_document(); + match &main_document.data { + DocumentData::Latex(data) => { + for node in data.root.descendants() { + if token.is_canceled() { + break; + } + + if let Some(folding) = latex::Environment::cast(node) + .map(|node| node.small_range()) + .or_else(|| latex::Section::cast(node).map(|node| node.small_range())) + .or_else(|| latex::EnumItem::cast(node).map(|node| node.small_range())) + .map(|node| main_document.line_index.line_col_lsp_range(node)) + .map(create_range) + { + foldings.push(folding); + } + } + } + DocumentData::Bibtex(data) => { + for node in data.root.descendants() { + if token.is_canceled() { + break; + } + + if let Some(folding) = bibtex::Preamble::cast(node) + .map(|node| node.small_range()) + .or_else(|| bibtex::String::cast(node).map(|node| node.small_range())) + .or_else(|| bibtex::Entry::cast(node).map(|node| node.small_range())) + .map(|node| main_document.line_index.line_col_lsp_range(node)) + .map(create_range) + { + foldings.push(folding); + } + } + } + DocumentData::BuildLog(_) => {} + } + foldings +} + +fn create_range(range: Range) -> FoldingRange { + FoldingRange { + start_line: range.start.line, + start_character: Some(range.start.character), + end_line: range.end.line, + end_character: Some(range.end.character), + kind: Some(FoldingRangeKind::Region), + } +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .build() + .folding(); + + let actual_foldings = find_foldings(request, CancellationToken::none()); + assert!(actual_foldings.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .build() + .folding(); + + let actual_foldings = find_foldings(request, CancellationToken::none()); + assert!(actual_foldings.is_empty()); + } + + #[test] + fn test_latex() { + let req = FeatureTester::builder() + .files(vec![( + "main.tex", + indoc! {r#" + \begin{document} + \section{Foo} + foo + \subsection{Bar} + bar + \section{Baz} + baz + \section{Qux} + \end{document}"# }, + )]) + .main("main.tex") + .build() + .folding(); + + let mut actual_foldings = find_foldings(req, CancellationToken::none()); + actual_foldings.sort_by_key(|folding| (folding.start_line, folding.start_character)); + + assert_eq!(actual_foldings.len(), 5); + assert_eq!( + actual_foldings[0], + FoldingRange { + start_line: 0, + start_character: Some(0), + end_line: 8, + end_character: Some(14), + kind: Some(FoldingRangeKind::Region) + } + ); + assert_eq!( + actual_foldings[1], + FoldingRange { + start_line: 1, + start_character: Some(4), + end_line: 4, + end_character: Some(7), + kind: Some(FoldingRangeKind::Region) + } + ); + assert_eq!( + actual_foldings[2], + FoldingRange { + start_line: 3, + start_character: Some(4), + end_line: 4, + end_character: Some(7), + kind: Some(FoldingRangeKind::Region) + } + ); + assert_eq!( + actual_foldings[3], + FoldingRange { + start_line: 5, + start_character: Some(4), + end_line: 6, + end_character: Some(7), + kind: Some(FoldingRangeKind::Region) + } + ); + assert_eq!( + actual_foldings[4], + FoldingRange { + start_line: 7, + start_character: Some(4), + end_line: 7, + end_character: Some(17), + kind: Some(FoldingRangeKind::Region) + } + ); + } + + #[test] + fn test_bibtex() { + let request = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! {r#" + some junk + here + + @article{foo, + author = {bar}, + title = {baz} + } + + @string{foo = "bar"} + + @comment{foo, + author = {bar}, + title = {baz} + } + + @preamble{"foo"}"# }, + )]) + .main("main.bib") + .build() + .folding(); + + let mut actual_foldings = find_foldings(request, CancellationToken::none()); + actual_foldings.sort_by_key(|folding| (folding.start_line, folding.start_character)); + + assert_eq!(actual_foldings.len(), 3); + assert_eq!( + actual_foldings[0], + FoldingRange { + start_line: 3, + start_character: Some(0), + end_line: 6, + end_character: Some(1), + kind: Some(FoldingRangeKind::Region) + } + ); + assert_eq!( + actual_foldings[1], + FoldingRange { + start_line: 8, + start_character: Some(0), + end_line: 8, + end_character: Some(20), + kind: Some(FoldingRangeKind::Region) + } + ); + assert_eq!( + actual_foldings[2], + FoldingRange { + start_line: 15, + start_character: Some(0), + end_line: 15, + end_character: Some(16), + kind: Some(FoldingRangeKind::Region) + } + ); + } +} diff --git a/support/texlab/src/features/formatting.rs b/support/texlab/src/features/formatting.rs new file mode 100644 index 0000000000..4492a0532d --- /dev/null +++ b/support/texlab/src/features/formatting.rs @@ -0,0 +1,24 @@ +mod bibtex_internal; +mod latexindent; + +use cancellation::CancellationToken; +use lsp_types::{DocumentFormattingParams, TextEdit}; + +use crate::BibtexFormatter; + +use self::{bibtex_internal::format_bibtex_internal, latexindent::format_with_latexindent}; + +use super::FeatureRequest; + +pub fn format_source_code( + request: FeatureRequest<DocumentFormattingParams>, + cancellation_token: &CancellationToken, +) -> Option<Vec<TextEdit>> { + let mut edits = None; + if request.context.options.read().unwrap().bibtex_formatter == BibtexFormatter::Texlab { + edits = edits.or_else(|| format_bibtex_internal(&request, cancellation_token)); + } + + edits = edits.or_else(|| format_with_latexindent(&request, cancellation_token)); + edits +} diff --git a/support/texlab/src/features/formatting/bibtex_internal.rs b/support/texlab/src/features/formatting/bibtex_internal.rs new file mode 100644 index 0000000000..8481f561e7 --- /dev/null +++ b/support/texlab/src/features/formatting/bibtex_internal.rs @@ -0,0 +1,381 @@ +use cancellation::CancellationToken; +use cstree::NodeOrToken; +use lsp_types::{DocumentFormattingParams, TextEdit}; + +use crate::{ + features::FeatureRequest, + syntax::{ + bibtex::{self, HasType}, + CstNode, + }, + LineIndex, LineIndexExt, +}; + +pub fn format_bibtex_internal( + request: &FeatureRequest<DocumentFormattingParams>, + _cancellation_token: &CancellationToken, +) -> Option<Vec<TextEdit>> { + let mut indent = String::new(); + if request.params.options.insert_spaces { + for _ in 0..request.params.options.tab_size { + indent.push(' '); + } + } else { + indent.push('\t'); + } + + let line_length = { + request + .context + .options + .read() + .unwrap() + .formatter_line_length + .map(|value| { + if value <= 0 { + usize::MAX + } else { + value as usize + } + }) + .unwrap_or(80) + }; + + let document = request.main_document(); + let data = document.data.as_bibtex()?; + let mut edits = Vec::new(); + + for node in data.root.children() { + let range = if let Some(entry) = bibtex::Entry::cast(node) { + entry.small_range() + } else if let Some(string) = bibtex::String::cast(node) { + string.small_range() + } else if let Some(preamble) = bibtex::Preamble::cast(node) { + preamble.small_range() + } else { + continue; + }; + + let mut formatter = Formatter::new( + indent.clone(), + request.params.options.tab_size, + line_length, + &document.line_index, + ); + + formatter.visit_node(node); + edits.push(TextEdit { + range: document.line_index.line_col_lsp_range(range), + new_text: formatter.output, + }); + } + + Some(edits) +} + +struct Formatter<'a> { + indent: String, + tab_size: u32, + line_length: usize, + output: String, + align: Vec<usize>, + line_index: &'a LineIndex, +} + +impl<'a> Formatter<'a> { + fn new(indent: String, tab_size: u32, line_length: usize, line_index: &'a LineIndex) -> Self { + Self { + indent, + tab_size, + line_length, + output: String::new(), + align: Vec::new(), + line_index, + } + } + + fn visit_token_lowercase(&mut self, token: &bibtex::SyntaxToken) { + self.output.push_str(&token.text().to_lowercase()); + } + + fn should_insert_space( + &self, + previous: &bibtex::SyntaxToken, + current: &bibtex::SyntaxToken, + ) -> bool { + let previous_range = self.line_index.line_col_lsp_range(previous.text_range()); + let current_range = self.line_index.line_col_lsp_range(current.text_range()); + previous_range.start.line != current_range.start.line + || previous_range.end.character < current_range.start.character + } + + fn base_align(&self) -> usize { + self.output[self.output.rfind('\n').unwrap_or(0)..] + .chars() + .count() + } + + fn visit_node(&mut self, parent: &bibtex::SyntaxNode) { + match parent.kind() { + bibtex::PREAMBLE => { + let preamble = bibtex::Preamble::cast(parent).unwrap(); + self.visit_token_lowercase(preamble.ty().unwrap()); + self.output.push('{'); + if preamble.syntax().arity() > 0 { + self.align.push(self.base_align()); + for node in preamble.syntax().children() { + self.visit_node(node); + } + self.output.push('}'); + } + } + bibtex::STRING => { + let string = bibtex::String::cast(parent).unwrap(); + self.visit_token_lowercase(string.ty().unwrap()); + self.output.push('{'); + if let Some(name) = string.name() { + self.output.push_str(name.text()); + self.output.push_str(" = "); + if let Some(value) = string.value() { + self.align.push(self.base_align()); + self.visit_node(value.syntax()); + self.output.push('}'); + } + } + } + bibtex::ENTRY => { + let entry = bibtex::Entry::cast(parent).unwrap(); + self.visit_token_lowercase(entry.ty().unwrap()); + self.output.push('{'); + if let Some(key) = entry.key() { + self.output.push_str(key.text()); + self.output.push(','); + self.output.push('\n'); + for field in entry.fields() { + self.visit_node(field.syntax()); + } + self.output.push('}'); + } + } + bibtex::FIELD => { + let field = bibtex::Field::cast(parent).unwrap(); + self.output.push_str(&self.indent); + let name = field.name().unwrap(); + self.output.push_str(name.text()); + self.output.push_str(" = "); + if let Some(value) = field.value() { + let count = name.text().chars().count(); + self.align.push(self.tab_size as usize + count + 3); + self.visit_node(value.syntax()); + self.output.push(','); + self.output.push('\n'); + } + } + bibtex::VALUE => { + let tokens: Vec<_> = parent + .descendants_with_tokens() + .filter_map(|element| element.into_token()) + .filter(|token| token.kind() != bibtex::WHITESPACE) + .collect(); + + self.output.push_str(tokens[0].text()); + + let align = self.align.pop().unwrap_or_default(); + let mut length = align + tokens[0].text().chars().count(); + for i in 1..tokens.len() { + let previous = tokens[i - 1]; + let current = tokens[i]; + let current_length = current.text().chars().count(); + + let insert_space = self.should_insert_space(previous, current); + let space_length = if insert_space { 1 } else { 0 }; + + if length + current_length + space_length > self.line_length { + self.output.push('\n'); + self.output.push_str(self.indent.as_ref()); + for _ in 0..=align - self.tab_size as usize { + self.output.push(' '); + } + length = align; + } else if insert_space { + self.output.push(' '); + length += 1; + } + self.output.push_str(current.text()); + length += current_length; + } + } + bibtex::ROOT | bibtex::JUNK | bibtex::COMMENT => { + for element in parent.children_with_tokens() { + match element { + NodeOrToken::Token(token) => { + self.output.push_str(token.text()); + } + NodeOrToken::Node(node) => { + self.visit_node(node); + } + } + } + } + _ => unreachable!(), + } + } +} + +#[cfg(test)] +mod tests { + use insta::{assert_debug_snapshot, assert_display_snapshot}; + + use crate::features::testing::FeatureTester; + + use super::*; + + #[test] + fn test_wrap_long_lines() { + let request = FeatureTester::builder() + .files(vec![( + "main.bib", + "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}", + )]) + .main("main.bib") + .build() + .formatting(); + + let edit = format_bibtex_internal(&request, CancellationToken::none()) + .unwrap() + .pop() + .unwrap(); + + assert_display_snapshot!(edit.new_text); + } + + #[test] + fn test_multiple_entries() { + let request = FeatureTester::builder() + .files(vec![( + "main.bib", + "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}\n\n@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}", + )]) + .main("main.bib") + .build() + .formatting(); + + let mut edits = format_bibtex_internal(&request, CancellationToken::none()).unwrap(); + let edit2 = edits.pop().unwrap(); + let edit1 = edits.pop().unwrap(); + + assert_debug_snapshot!((edit1.new_text, edit2.new_text)); + } + + #[test] + fn test_trailing_comma() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, bar = baz}")]) + .main("main.bib") + .build() + .formatting(); + + let edit = format_bibtex_internal(&request, CancellationToken::none()) + .unwrap() + .pop() + .unwrap(); + + assert_display_snapshot!(edit.new_text); + } + + #[test] + fn test_insert_braces() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, bar = baz,")]) + .main("main.bib") + .build() + .formatting(); + + let edit = format_bibtex_internal(&request, CancellationToken::none()) + .unwrap() + .pop() + .unwrap(); + + assert_display_snapshot!(edit.new_text); + } + + #[test] + fn test_command() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, bar = \"\\baz\",}")]) + .main("main.bib") + .build() + .formatting(); + + let edit = format_bibtex_internal(&request, CancellationToken::none()) + .unwrap() + .pop() + .unwrap(); + + assert_display_snapshot!(edit.new_text); + } + + #[test] + fn test_concatenation() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article{foo, bar = \"baz\" # \"qux\"}")]) + .main("main.bib") + .build() + .formatting(); + + let edit = format_bibtex_internal(&request, CancellationToken::none()) + .unwrap() + .pop() + .unwrap(); + + assert_display_snapshot!(edit.new_text); + } + + #[test] + fn test_parens() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@article(foo,)")]) + .main("main.bib") + .build() + .formatting(); + + let edit = format_bibtex_internal(&request, CancellationToken::none()) + .unwrap() + .pop() + .unwrap(); + + assert_display_snapshot!(edit.new_text); + } + + #[test] + fn test_string() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@string{foo=\"bar\"}")]) + .main("main.bib") + .build() + .formatting(); + + let edit = format_bibtex_internal(&request, CancellationToken::none()) + .unwrap() + .pop() + .unwrap(); + + assert_display_snapshot!(edit.new_text); + } + + #[test] + fn test_preamble() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "@preamble{\n\"foo bar baz\"}")]) + .main("main.bib") + .build() + .formatting(); + + let edit = format_bibtex_internal(&request, CancellationToken::none()) + .unwrap() + .pop() + .unwrap(); + + assert_display_snapshot!(edit.new_text); + } +} diff --git a/support/texlab/src/features/formatting/latexindent.rs b/support/texlab/src/features/formatting/latexindent.rs new file mode 100644 index 0000000000..f603728a5d --- /dev/null +++ b/support/texlab/src/features/formatting/latexindent.rs @@ -0,0 +1,50 @@ +use std::{ + io::{BufWriter, Write}, + process::{Command, Stdio}, +}; + +use cancellation::CancellationToken; +use cstree::{TextLen, TextRange}; +use lsp_types::{DocumentFormattingParams, TextEdit}; + +use crate::{features::FeatureRequest, LineIndexExt}; + +pub fn format_with_latexindent( + request: &FeatureRequest<DocumentFormattingParams>, + _cancellation_token: &CancellationToken, +) -> Option<Vec<TextEdit>> { + let document = request.main_document(); + + let current_dir = &request.context.current_directory; + let options = request.context.options.read().unwrap(); + let current_dir = match &options.root_directory { + Some(root_directory) => current_dir.join(root_directory), + None => current_dir.clone(), + }; + drop(options); + + let mut process = Command::new("latexindent") + .arg("-l") + .current_dir(current_dir) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .ok()?; + + let stdin = process.stdin.take()?; + let mut stdin = BufWriter::new(stdin); + stdin.write_all(document.text.as_bytes()).ok()?; + drop(stdin); + + let output = process.wait_with_output().ok()?; + + let new_text = String::from_utf8_lossy(&output.stdout).into_owned(); + + Some(vec![TextEdit { + range: document + .line_index + .line_col_lsp_range(TextRange::new(0.into(), document.text.text_len())), + new_text, + }]) +} diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__command.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__command.snap new file mode 100644 index 0000000000..792e7be277 --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__command.snap @@ -0,0 +1,8 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: edit.new_text + +--- +@article{foo, + bar = "\baz", +} diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__concatenation.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__concatenation.snap new file mode 100644 index 0000000000..8cd51dfa92 --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__concatenation.snap @@ -0,0 +1,8 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: edit.new_text + +--- +@article{foo, + bar = "baz" # "qux", +} diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__insert_braces.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__insert_braces.snap new file mode 100644 index 0000000000..3be9b15e9d --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__insert_braces.snap @@ -0,0 +1,8 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: edit.new_text + +--- +@article{foo, + bar = baz, +} diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__multiple_entries.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__multiple_entries.snap new file mode 100644 index 0000000000..636b4b665c --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__multiple_entries.snap @@ -0,0 +1,9 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: "(edit1.new_text, edit2.new_text)" + +--- +( + "@article{foo,\n\tbar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum\n\t dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,\n\t consectetur adipiscing elit.},\n}", + "@article{foo,\n\tbar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum\n\t dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,\n\t consectetur adipiscing elit.},\n}", +) diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__parens.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__parens.snap new file mode 100644 index 0000000000..5aff28cb5f --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__parens.snap @@ -0,0 +1,7 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: edit.new_text + +--- +@article{foo, +} diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__preamble.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__preamble.snap new file mode 100644 index 0000000000..663569eb5b --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__preamble.snap @@ -0,0 +1,6 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: edit.new_text + +--- +@preamble{"foo bar baz"} diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__string.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__string.snap new file mode 100644 index 0000000000..6d37a69e8e --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__string.snap @@ -0,0 +1,6 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: edit.new_text + +--- +@string{foo = "bar"} diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__trailing_comma.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__trailing_comma.snap new file mode 100644 index 0000000000..3be9b15e9d --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__trailing_comma.snap @@ -0,0 +1,8 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: edit.new_text + +--- +@article{foo, + bar = baz, +} diff --git a/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__wrap_long_lines.snap b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__wrap_long_lines.snap new file mode 100644 index 0000000000..6c0ec724b5 --- /dev/null +++ b/support/texlab/src/features/formatting/snapshots/texlab__features__formatting__bibtex_internal__tests__wrap_long_lines.snap @@ -0,0 +1,10 @@ +--- +source: src/features/formatting/bibtex_internal.rs +expression: edit.new_text + +--- +@article{foo, + bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum + dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, + consectetur adipiscing elit.}, +} diff --git a/support/texlab/src/features/forward_search.rs b/support/texlab/src/features/forward_search.rs new file mode 100644 index 0000000000..39470a64ec --- /dev/null +++ b/support/texlab/src/features/forward_search.rs @@ -0,0 +1,124 @@ +use std::{ + io, + path::Path, + process::{Command, Stdio}, +}; + +use cancellation::CancellationToken; +use log::error; +use lsp_types::TextDocumentPositionParams; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use super::FeatureRequest; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] +#[repr(i32)] +pub enum ForwardSearchStatus { + Success = 0, + Error = 1, + Failure = 2, + Unconfigured = 3, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct ForwardSearchResult { + pub status: ForwardSearchStatus, +} + +pub fn execute_forward_search( + request: FeatureRequest<TextDocumentPositionParams>, + _cancellation_token: &CancellationToken, +) -> Option<ForwardSearchResult> { + let options = { + request + .context + .options + .read() + .unwrap() + .forward_search + .clone() + .unwrap_or_default() + }; + + if options.executable.is_none() || options.args.is_none() { + return Some(ForwardSearchResult { + status: ForwardSearchStatus::Unconfigured, + }); + } + + let root_document = request + .subset + .documents + .iter() + .find(|document| { + if let Some(data) = document.data.as_latex() { + data.extras.has_document_environment + && !data + .extras + .explicit_links + .iter() + .filter_map(|link| link.as_component_name()) + .any(|name| name == "subfiles.cls") + } else { + false + } + }) + .filter(|document| document.uri.scheme() == "file")?; + + let data = root_document.data.as_latex()?; + let pdf_path = data + .extras + .implicit_links + .pdf + .iter() + .filter_map(|uri| uri.to_file_path().ok()) + .find(|path| path.exists())?; + + let tex_path = request.main_document().uri.to_file_path().ok()?; + + let args: Vec<String> = options + .args + .unwrap() + .into_iter() + .flat_map(|arg| { + replace_placeholder(&tex_path, &pdf_path, request.params.position.line, arg) + }) + .collect(); + + let status = match run_process(options.executable.unwrap(), args) { + Ok(()) => ForwardSearchStatus::Success, + Err(why) => { + error!("Unable to execute forward search: {}", why); + ForwardSearchStatus::Failure + } + }; + Some(ForwardSearchResult { status }) +} + +fn replace_placeholder( + tex_file: &Path, + pdf_file: &Path, + line_number: u32, + argument: String, +) -> Option<String> { + let result = if argument.starts_with('"') || argument.ends_with('"') { + argument + } else { + argument + .replace("%f", tex_file.to_str()?) + .replace("%p", pdf_file.to_str()?) + .replace("%l", &(line_number + 1).to_string()) + }; + Some(result) +} + +fn run_process(executable: String, args: Vec<String>) -> io::Result<()> { + Command::new(executable) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + Ok(()) +} diff --git a/support/texlab/src/features/highlight.rs b/support/texlab/src/features/highlight.rs new file mode 100644 index 0000000000..e1aea6e2ad --- /dev/null +++ b/support/texlab/src/features/highlight.rs @@ -0,0 +1,15 @@ +mod label; + +use cancellation::CancellationToken; +use lsp_types::{DocumentHighlight, DocumentHighlightParams}; + +use self::label::find_label_highlights; + +use super::FeatureRequest; + +pub fn find_document_highlights( + request: FeatureRequest<DocumentHighlightParams>, + token: &CancellationToken, +) -> Option<Vec<DocumentHighlight>> { + find_label_highlights(&request, token) +} diff --git a/support/texlab/src/features/highlight/label.rs b/support/texlab/src/features/highlight/label.rs new file mode 100644 index 0000000000..4c94180f2a --- /dev/null +++ b/support/texlab/src/features/highlight/label.rs @@ -0,0 +1,167 @@ +use cancellation::CancellationToken; +use lsp_types::{DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams}; + +use crate::{ + features::FeatureRequest, + syntax::{latex, CstNode}, + LineIndexExt, +}; + +pub fn find_label_highlights( + request: &FeatureRequest<DocumentHighlightParams>, + token: &CancellationToken, +) -> Option<Vec<DocumentHighlight>> { + let main_document = request.main_document(); + + let offset = main_document + .line_index + .offset_lsp(request.params.text_document_position_params.position); + + let data = main_document.data.as_latex()?; + let name = data.root.token_at_offset(offset).right_biased()?; + if name.kind() != latex::WORD { + return None; + } + + if !matches!( + name.parent().parent()?.kind(), + latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE + ) { + return None; + } + + let mut highlights = Vec::new(); + for node in data.root.descendants() { + if token.is_canceled() { + return None; + } + + if let Some(label_name) = latex::LabelDefinition::cast(node) + .and_then(|label| label.name()) + .and_then(|label_name| label_name.word()) + .filter(|label_name| label_name.text() == name.text()) + { + let range = main_document + .line_index + .line_col_lsp_range(label_name.text_range()); + + highlights.push(DocumentHighlight { + range, + kind: Some(DocumentHighlightKind::Write), + }); + } else if let Some(label) = latex::LabelReference::cast(node) { + for label_name in label + .name_list() + .into_iter() + .flat_map(|name| name.words()) + .filter(|label_name| label_name.text() == name.text()) + { + let range = main_document + .line_index + .line_col_lsp_range(label_name.text_range()); + + highlights.push(DocumentHighlight { + range, + kind: Some(DocumentHighlightKind::Read), + }); + } + } else if let Some(label) = latex::LabelReferenceRange::cast(node) { + if let Some(label_name) = label + .from() + .and_then(|label_name| label_name.word()) + .filter(|label_name| label_name.text() == name.text()) + { + let range = main_document + .line_index + .line_col_lsp_range(label_name.text_range()); + + highlights.push(DocumentHighlight { + range, + kind: Some(DocumentHighlightKind::Read), + }); + } + + if let Some(label_name) = label + .to() + .and_then(|label_name| label_name.word()) + .filter(|label_name| label_name.text() == name.text()) + { + let range = main_document + .line_index + .line_col_lsp_range(label_name.text_range()); + + highlights.push(DocumentHighlight { + range, + kind: Some(DocumentHighlightKind::Read), + }); + } + } + } + + Some(highlights) +} + +#[cfg(test)] +mod tests { + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .highlight(); + + let actual_links = find_label_highlights(&request, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .highlight(); + + let actual_links = find_label_highlights(&request, CancellationToken::none()); + + assert!(actual_links.is_none()); + } + + #[test] + fn test_label() { + let tester = FeatureTester::builder() + .files(vec![("main.tex", "\\label{foo}\n\\ref{foo}\\label{bar}")]) + .main("main.tex") + .line(0) + .character(7) + .build(); + let request = tester.highlight(); + + let actual_highlights = find_label_highlights(&request, CancellationToken::none()).unwrap(); + + let expected_highlights = vec![ + DocumentHighlight { + range: Range::new_simple(0, 7, 0, 10), + kind: Some(DocumentHighlightKind::Write), + }, + DocumentHighlight { + range: Range::new_simple(1, 5, 1, 8), + kind: Some(DocumentHighlightKind::Read), + }, + ]; + + assert_eq!(actual_highlights, expected_highlights); + } +} diff --git a/support/texlab/src/features/hover.rs b/support/texlab/src/features/hover.rs new file mode 100644 index 0000000000..a78978bc33 --- /dev/null +++ b/support/texlab/src/features/hover.rs @@ -0,0 +1,43 @@ +#[cfg(feature = "citeproc")] +mod citation; +mod component; +mod entry_type; +mod field; +mod label; +mod string_ref; + +use cancellation::CancellationToken; +use cfg_if::cfg_if; +use lsp_types::{Hover, HoverParams}; + +use crate::features::cursor::CursorContext; + +use self::{ + component::find_component_hover, entry_type::find_entry_type_hover, field::find_field_hover, + label::find_label_hover, string_ref::find_string_reference_hover, +}; + +use super::FeatureRequest; + +pub fn find_hover( + request: FeatureRequest<HoverParams>, + cabcellation_token: &CancellationToken, +) -> Option<Hover> { + let context = CursorContext::new(request); + log::debug!("[Hover] Cursor: {:?}", context.cursor); + let mut hover = find_label_hover(&context, cabcellation_token); + + cfg_if! { + if #[cfg(feature = "citation")] { + hover = hover.or_else(|| self::citation::find_citation_hover(&context, cabcellation_token)); + } + } + + hover = hover + .or_else(|| find_component_hover(&context, cabcellation_token)) + .or_else(|| find_string_reference_hover(&context, cabcellation_token)) + .or_else(|| find_field_hover(&context, cabcellation_token)) + .or_else(|| find_entry_type_hover(&context, cabcellation_token)); + + hover +} diff --git a/support/texlab/src/features/hover/citation.rs b/support/texlab/src/features/hover/citation.rs new file mode 100644 index 0000000000..1fd6d6c06b --- /dev/null +++ b/support/texlab/src/features/hover/citation.rs @@ -0,0 +1,133 @@ +use cancellation::CancellationToken; +use lsp_types::{Hover, HoverContents, HoverParams}; + +use crate::{citation, features::cursor::CursorContext, LineIndexExt}; + +pub fn find_citation_hover( + context: &CursorContext<HoverParams>, + cancellation_token: &CancellationToken, +) -> Option<Hover> { + cancellation_token.result().ok()?; + let main_document = context.request.main_document(); + + let (key_text, key_range) = context + .find_citation_key_word() + .or_else(|| context.find_citation_key_command()) + .or_else(|| context.find_entry_key())?; + + let contents = context + .request + .subset + .documents + .iter() + .find_map(|document| { + document + .data + .as_bibtex() + .and_then(|data| citation::render_citation(&data.root, &key_text)) + })?; + + Some(Hover { + range: Some(main_document.line_index.line_col_lsp_range(key_range)), + contents: HoverContents::Markup(contents), + }) +} + +#[cfg(test)] +mod tests { + use lsp_types::{MarkupContent, MarkupKind, Range}; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_citation_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_citation_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_inside_cite() { + let request = FeatureTester::builder() + .files(vec![ + ( + "main.bib", + "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}", + ), + ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo}"), + ]) + .main("main.tex") + .line(1) + .character(7) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_citation_hover(&context, CancellationToken::none()).unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: "Bar, F. (1337). *Baz Qux*.".into(), + }), + range: Some(Range::new_simple(1, 6, 1, 9)), + }; + assert_eq!(actual_hover, expected_hover); + } + + #[test] + fn test_inside_entry() { + let request = FeatureTester::builder() + .files(vec![ + ( + "main.bib", + "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}", + ), + ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo}"), + ]) + .main("main.bib") + .line(0) + .character(11) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_citation_hover(&context, CancellationToken::none()).unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: "Bar, F. (1337). *Baz Qux*.".into(), + }), + range: Some(Range::new_simple(0, 9, 0, 12)), + }; + assert_eq!(actual_hover, expected_hover); + } +} diff --git a/support/texlab/src/features/hover/component.rs b/support/texlab/src/features/hover/component.rs new file mode 100644 index 0000000000..abfdb781f1 --- /dev/null +++ b/support/texlab/src/features/hover/component.rs @@ -0,0 +1,105 @@ +use cancellation::CancellationToken; +use lsp_types::{Hover, HoverContents, HoverParams}; + +use crate::{ + component_db::COMPONENT_DATABASE, features::cursor::CursorContext, syntax::latex, LineIndexExt, +}; + +pub fn find_component_hover( + context: &CursorContext<HoverParams>, + token: &CancellationToken, +) -> Option<Hover> { + let main_document = context.request.main_document(); + let data = main_document.data.as_latex()?; + for link in &data.extras.explicit_links { + if token.is_canceled() { + break; + } + + if matches!( + link.kind, + latex::ExplicitLinkKind::Package | latex::ExplicitLinkKind::Class + ) && link.stem_range.contains_inclusive(context.offset) + { + let docs = COMPONENT_DATABASE.documentation(&link.stem)?; + return Some(Hover { + contents: HoverContents::Markup(docs), + range: Some(main_document.line_index.line_col_lsp_range(link.stem_range)), + }); + } + } + None +} + +#[cfg(test)] +mod tests { + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_component_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_component_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_known_package() { + let request = FeatureTester::builder() + .files(vec![("main.tex", r#"\usepackage{amsmath}"#)]) + .main("main.tex") + .line(0) + .character(15) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_component_hover(&context, CancellationToken::none()).unwrap(); + + assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 12, 0, 19)); + } + + #[test] + fn test_unknown_class() { + let request = FeatureTester::builder() + .files(vec![("main.tex", r#"\documentclass{abcdefghijklmnop}"#)]) + .main("main.tex") + .line(0) + .character(20) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_component_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } +} diff --git a/support/texlab/src/features/hover/entry_type.rs b/support/texlab/src/features/hover/entry_type.rs new file mode 100644 index 0000000000..e480b2f835 --- /dev/null +++ b/support/texlab/src/features/hover/entry_type.rs @@ -0,0 +1,128 @@ +use cancellation::CancellationToken; +use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent}; + +use crate::{features::cursor::CursorContext, LineIndexExt, LANGUAGE_DATA}; + +pub fn find_entry_type_hover( + context: &CursorContext<HoverParams>, + _token: &CancellationToken, +) -> Option<Hover> { + let name = context + .cursor + .as_bibtex() + .filter(|token| token.kind().is_type())?; + + let docs = LANGUAGE_DATA.entry_type_documentation(&name.text()[1..])?; + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: lsp_types::MarkupKind::Markdown, + value: docs.to_string(), + }), + range: Some( + context + .request + .main_document() + .line_index + .line_col_lsp_range(name.text_range()), + ), + }) +} + +#[cfg(test)] +mod tests { + use lsp_types::{MarkupKind, Range}; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_entry_type_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_entry_type_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_known_entry_type() { + let request = FeatureTester::builder() + .files(vec![("main.bib", r#"@article{foo,}"#)]) + .main("main.bib") + .line(0) + .character(3) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_entry_type_hover(&context, CancellationToken::none()).unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: LANGUAGE_DATA + .entry_type_documentation("article") + .unwrap() + .into(), + }), + range: Some(Range::new_simple(0, 0, 0, 8)), + }; + assert_eq!(actual_hover, expected_hover); + } + + #[test] + fn test_unknown_field() { + let request = FeatureTester::builder() + .files(vec![("main.bib", r#"@foo{bar,}"#)]) + .main("main.bib") + .line(0) + .character(3) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_entry_type_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_entry_key() { + let request = FeatureTester::builder() + .files(vec![("main.bib", r#"@article{foo,}"#)]) + .main("main.bib") + .line(0) + .character(11) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_entry_type_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } +} diff --git a/support/texlab/src/features/hover/field.rs b/support/texlab/src/features/hover/field.rs new file mode 100644 index 0000000000..54a8718d69 --- /dev/null +++ b/support/texlab/src/features/hover/field.rs @@ -0,0 +1,128 @@ +use cancellation::CancellationToken; +use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent}; + +use crate::{ + features::cursor::CursorContext, + syntax::{bibtex, CstNode}, + LineIndexExt, LANGUAGE_DATA, +}; + +pub fn find_field_hover( + context: &CursorContext<HoverParams>, + _token: &CancellationToken, +) -> Option<Hover> { + let main_document = context.request.main_document(); + + let name = context + .cursor + .as_bibtex() + .filter(|token| token.kind() == bibtex::WORD)?; + + bibtex::Field::cast(name.parent())?; + + let docs = LANGUAGE_DATA.field_documentation(&name.text())?; + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: lsp_types::MarkupKind::Markdown, + value: docs.to_string(), + }), + range: Some( + main_document + .line_index + .line_col_lsp_range(name.text_range()), + ), + }) +} + +#[cfg(test)] +mod tests { + use lsp_types::{MarkupKind, Range}; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_field_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_field_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_known_field() { + let request = FeatureTester::builder() + .files(vec![("main.bib", r#"@article{foo, author = bar}"#)]) + .main("main.bib") + .line(0) + .character(15) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_field_hover(&context, CancellationToken::none()).unwrap(); + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: LANGUAGE_DATA.field_documentation("author").unwrap().into(), + }), + range: Some(Range::new_simple(0, 14, 0, 20)), + }; + assert_eq!(actual_hover, expected_hover); + } + + #[test] + fn test_unknown_field() { + let request = FeatureTester::builder() + .files(vec![("main.bib", r#"@article{foo, bar = baz}"#)]) + .main("main.bib") + .line(0) + .character(15) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_field_hover(&context, CancellationToken::none()); + assert_eq!(actual_hover, None); + } + + #[test] + fn test_entry_key() { + let request = FeatureTester::builder() + .files(vec![("main.bib", r#"@article{foo, author = bar}"#)]) + .main("main.bib") + .line(0) + .character(11) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_field_hover(&context, CancellationToken::none()); + assert_eq!(actual_hover, None); + } +} diff --git a/support/texlab/src/features/hover/label.rs b/support/texlab/src/features/hover/label.rs new file mode 100644 index 0000000000..79afe7d250 --- /dev/null +++ b/support/texlab/src/features/hover/label.rs @@ -0,0 +1,79 @@ +use cancellation::CancellationToken; +use lsp_types::{Hover, HoverContents, HoverParams}; + +use crate::{features::cursor::CursorContext, render_label, LineIndexExt}; + +pub fn find_label_hover( + context: &CursorContext<HoverParams>, + _token: &CancellationToken, +) -> Option<Hover> { + let main_document = context.request.main_document(); + + let (name_text, name_range) = context + .find_label_name_word() + .or_else(|| context.find_label_name_command())?; + + let label = render_label(&context.request.subset, name_text, None)?; + + Some(Hover { + range: Some(main_document.line_index.line_col_lsp_range(name_range)), + contents: HoverContents::Markup(label.documentation()), + }) +} + +#[cfg(test)] +mod tests { + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_label_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_label_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_section() { + let request = FeatureTester::builder() + .files(vec![("main.tex", r#"\section{Foo}\label{sec:foo}"#)]) + .main("main.tex") + .line(0) + .character(23) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_label_hover(&context, CancellationToken::none()).unwrap(); + + assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 20, 0, 27)); + } +} diff --git a/support/texlab/src/features/hover/string_ref.rs b/support/texlab/src/features/hover/string_ref.rs new file mode 100644 index 0000000000..f6e7f1c873 --- /dev/null +++ b/support/texlab/src/features/hover/string_ref.rs @@ -0,0 +1,141 @@ +use cancellation::CancellationToken; +use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; + +use crate::{ + features::cursor::CursorContext, + syntax::{bibtex, CstNode}, + LineIndexExt, +}; + +pub fn find_string_reference_hover( + context: &CursorContext<HoverParams>, + cancellation_token: &CancellationToken, +) -> Option<Hover> { + let main_document = context.request.main_document(); + let data = main_document.data.as_bibtex()?; + + let name = context + .cursor + .as_bibtex() + .filter(|token| token.kind() == bibtex::WORD) + .filter(|name| matches!(name.parent().kind(), bibtex::TOKEN | bibtex::STRING))?; + + for string in data.root.children().filter_map(bibtex::String::cast) { + if cancellation_token.is_canceled() { + return None; + } + + if string.name().filter(|n| n.text() == name.text()).is_some() { + let value = string.value()?.syntax().text().to_string(); + return Some(Hover { + range: Some( + main_document + .line_index + .line_col_lsp_range(name.text_range()), + ), + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::PlainText, + value, + }), + }); + } + } + + None +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_string_reference_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_string_reference_hover(&context, CancellationToken::none()); + + assert_eq!(actual_hover, None); + } + + #[test] + fn test_inside_reference() { + let request = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { r#" + @string{foo = "Foo"} + @string{bar = "Bar"} + @article{baz, author = bar} + "# }, + )]) + .main("main.bib") + .line(2) + .character(24) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = + find_string_reference_hover(&context, CancellationToken::none()).unwrap(); + + let expected_hover = Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::PlainText, + value: "\"Bar\"".into(), + }), + range: Some(Range::new_simple(2, 23, 2, 26)), + }; + + assert_eq!(actual_hover, expected_hover); + } + + #[test] + fn test_inside_field() { + let request = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { r#" + @string{foo = "Foo"} + @string{bar = "Bar"} + @article{baz, author = bar} + "# }, + )]) + .main("main.bib") + .line(2) + .character(20) + .build() + .hover(); + + let context = CursorContext::new(request); + let actual_hover = find_string_reference_hover(&context, CancellationToken::none()); + assert_eq!(actual_hover, None); + } +} diff --git a/support/texlab/src/features/link.rs b/support/texlab/src/features/link.rs new file mode 100644 index 0000000000..a6016c9f76 --- /dev/null +++ b/support/texlab/src/features/link.rs @@ -0,0 +1,115 @@ +use cancellation::CancellationToken; +use lsp_types::{DocumentLink, DocumentLinkParams}; + +use crate::LineIndexExt; + +use super::FeatureRequest; + +pub fn find_document_links( + request: FeatureRequest<DocumentLinkParams>, + cancellation_token: &CancellationToken, +) -> Vec<DocumentLink> { + let mut links = Vec::new(); + let main_document = request.main_document(); + if let Some(data) = main_document.data.as_latex() { + for include in &data.extras.explicit_links { + for target in &include.targets { + if cancellation_token.is_canceled() { + return links; + } + + if request + .subset + .documents + .iter() + .any(|document| document.uri.as_ref() == target.as_ref()) + { + links.push(DocumentLink { + range: main_document + .line_index + .line_col_lsp_range(include.stem_range), + target: Some(target.as_ref().clone().into()), + tooltip: None, + data: None, + }); + break; + } + } + } + } + links +} + +#[cfg(test)] +mod tests { + use super::*; + + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .build() + .link(); + + let items = find_document_links(request, CancellationToken::none()); + assert!(items.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .build() + .link(); + + let items = find_document_links(request, CancellationToken::none()); + assert!(items.is_empty()); + } + + #[test] + fn test_includes() { + let tester = FeatureTester::builder() + .files(vec![("foo.tex", r#"\input{bar.tex}"#), ("bar.tex", r#""#)]) + .main("foo.tex") + .build(); + let target = tester.uri("bar.tex"); + + let actual_items = find_document_links(tester.link(), CancellationToken::none()); + + let expected_items = vec![DocumentLink { + range: Range::new_simple(0, 7, 0, 14), + target: Some(target.as_ref().clone().into()), + tooltip: None, + data: None, + }]; + assert_eq!(actual_items, expected_items); + } + + #[test] + fn test_imports() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\import{bar/}{baz}"#), + ("bar/baz.tex", r#""#), + ]) + .main("foo.tex") + .build(); + let target = tester.uri("bar/baz.tex"); + + let actual_items = find_document_links(tester.link(), CancellationToken::none()); + + let expected_items = vec![DocumentLink { + range: Range::new_simple(0, 14, 0, 17), + target: Some(target.as_ref().clone().into()), + tooltip: None, + data: None, + }]; + assert_eq!(actual_items, expected_items); + } +} diff --git a/support/texlab/src/syntax/lsp_kind.rs b/support/texlab/src/features/lsp_kinds.rs index 7d92fd4ee4..fbfc50cace 100644 --- a/support/texlab/src/syntax/lsp_kind.rs +++ b/support/texlab/src/features/lsp_kinds.rs @@ -1,5 +1,6 @@ -use super::lang_data::BibtexEntryTypeCategory; -use crate::protocol::{CompletionItemKind, SymbolKind}; +use lsp_types::{CompletionItemKind, SymbolKind}; + +use crate::BibtexEntryTypeCategory; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Structure { diff --git a/support/texlab/src/features/reference.rs b/support/texlab/src/features/reference.rs new file mode 100644 index 0000000000..14cf3bc297 --- /dev/null +++ b/support/texlab/src/features/reference.rs @@ -0,0 +1,25 @@ +mod entry; +mod label; +mod string; + +use cancellation::CancellationToken; +use lsp_types::{Location, ReferenceParams}; + +use self::{ + entry::find_entry_references, label::find_label_references, string::find_string_references, +}; + +use super::{cursor::CursorContext, FeatureRequest}; + +pub fn find_all_references( + request: FeatureRequest<ReferenceParams>, + cancellation_token: &CancellationToken, +) -> Option<Vec<Location>> { + let mut references = Vec::new(); + let context = CursorContext::new(request); + log::debug!("[References] Cursor: {:?}", context.cursor); + find_label_references(&context, cancellation_token, &mut references); + find_entry_references(&context, cancellation_token, &mut references); + find_string_references(&context, cancellation_token, &mut references); + Some(references) +} diff --git a/support/texlab/src/features/reference/entry.rs b/support/texlab/src/features/reference/entry.rs new file mode 100644 index 0000000000..fbbcbdb4cc --- /dev/null +++ b/support/texlab/src/features/reference/entry.rs @@ -0,0 +1,198 @@ +use cancellation::CancellationToken; +use lsp_types::{Location, ReferenceParams}; + +use crate::{ + features::cursor::CursorContext, + syntax::{bibtex, latex, CstNode}, + DocumentData, LineIndexExt, +}; + +pub fn find_entry_references( + context: &CursorContext<ReferenceParams>, + cancellation_token: &CancellationToken, + references: &mut Vec<Location>, +) -> Option<()> { + let (key_text, _) = context + .find_citation_key_word() + .or_else(|| context.find_citation_key_command()) + .or_else(|| context.find_entry_key())?; + + for document in &context.request.subset.documents { + cancellation_token.result().ok()?; + + match &document.data { + DocumentData::Latex(data) => { + data.root + .descendants() + .filter_map(latex::Citation::cast) + .filter_map(|citation| citation.key_list()) + .flat_map(|keys| keys.words()) + .filter(|key| key.text() == key_text) + .map(|key| document.line_index.line_col_lsp_range(key.text_range())) + .for_each(|range| { + references.push(Location::new(document.uri.as_ref().clone().into(), range)); + }); + } + DocumentData::Bibtex(data) if context.request.params.context.include_declaration => { + data.root + .children() + .filter_map(bibtex::Entry::cast) + .filter_map(|entry| entry.key()) + .filter(|key| key.text() == key_text) + .map(|key| document.line_index.line_col_lsp_range(key.text_range())) + .for_each(|range| { + references.push(Location::new(document.uri.as_ref().clone().into(), range)); + }); + } + DocumentData::Bibtex(_) | DocumentData::BuildLog(_) => {} + } + } + Some(()) +} + +#[cfg(test)] +mod tests { + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .reference(); + + let mut actual_references = Vec::new(); + let context = CursorContext::new(request); + find_entry_references(&context, CancellationToken::none(), &mut actual_references); + + assert!(actual_references.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .reference(); + + let mut actual_references = Vec::new(); + let context = CursorContext::new(request); + find_entry_references(&context, CancellationToken::none(), &mut actual_references); + + assert!(actual_references.is_empty()); + } + + #[test] + fn test_definition() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.bib", r#"@article{foo,}"#), + ("bar.tex", r#"\cite{foo}\addbibresource{foo.bib}"#), + ]) + .main("foo.bib") + .line(0) + .character(11) + .build(); + let uri = tester.uri("bar.tex"); + let mut actual_references = Vec::new(); + + let request = tester.reference(); + let context = CursorContext::new(request); + find_entry_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![Location::new( + uri.as_ref().clone().into(), + Range::new_simple(0, 6, 0, 9), + )]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_definition_include_declaration() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.bib", r#"@article{foo,}"#), + ("bar.tex", r#"\cite{foo}\addbibresource{foo.bib}"#), + ]) + .main("foo.bib") + .line(0) + .character(11) + .include_declaration(true) + .build(); + let uri1 = tester.uri("foo.bib"); + let uri2 = tester.uri("bar.tex"); + let mut actual_references = Vec::new(); + + let request = tester.reference(); + let context = CursorContext::new(request); + find_entry_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![ + Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 9, 0, 12)), + Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 6, 0, 9)), + ]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_reference() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.bib", r#"@article{foo,}"#), + ("bar.tex", r#"\cite{foo}\addbibresource{foo.bib}"#), + ]) + .main("bar.tex") + .line(0) + .character(8) + .build(); + let uri = tester.uri("bar.tex"); + let mut actual_references = Vec::new(); + + let request = tester.reference(); + let context = CursorContext::new(request); + find_entry_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![Location::new( + uri.as_ref().clone().into(), + Range::new_simple(0, 6, 0, 9), + )]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_reference_include_declaration() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.bib", r#"@article{foo,}"#), + ("bar.tex", r#"\cite{foo}\addbibresource{foo.bib}"#), + ]) + .main("bar.tex") + .line(0) + .character(6) + .include_declaration(true) + .build(); + let uri1 = tester.uri("foo.bib"); + let uri2 = tester.uri("bar.tex"); + let mut actual_references = Vec::new(); + + let request = tester.reference(); + let context = CursorContext::new(request); + find_entry_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![ + Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 6, 0, 9)), + Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 9, 0, 12)), + ]; + assert_eq!(actual_references, expected_references); + } +} diff --git a/support/texlab/src/features/reference/label.rs b/support/texlab/src/features/reference/label.rs new file mode 100644 index 0000000000..e9aa3a8510 --- /dev/null +++ b/support/texlab/src/features/reference/label.rs @@ -0,0 +1,187 @@ +use cancellation::CancellationToken; +use lsp_types::{Location, ReferenceParams}; + +use crate::{features::cursor::CursorContext, LineIndexExt}; + +pub fn find_label_references( + context: &CursorContext<ReferenceParams>, + cancellation_token: &CancellationToken, + references: &mut Vec<Location>, +) -> Option<()> { + cancellation_token.result().ok()?; + + let (name_text, _) = context + .find_label_name_word() + .or_else(|| context.find_label_name_command())?; + + for document in &context.request.subset.documents { + if let Some(data) = document.data.as_latex() { + cancellation_token.result().ok()?; + for name in data + .extras + .label_names + .iter() + .filter(|name| name.text == name_text) + .filter(|name| { + !name.is_definition || context.request.params.context.include_declaration + }) + { + references.push(Location::new( + document.uri.as_ref().clone().into(), + document.line_index.line_col_lsp_range(name.range), + )); + } + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_definition() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\label{foo}"#), + ("bar.tex", r#"\ref{foo}\input{foo.tex}"#), + ]) + .main("foo.tex") + .line(0) + .character(8) + .build(); + let uri = tester.uri("bar.tex"); + let mut actual_references = Vec::new(); + + let request = tester.reference(); + let context = CursorContext::new(request); + find_label_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![Location::new( + uri.as_ref().clone().into(), + Range::new_simple(0, 5, 0, 8), + )]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_definition_include_declaration() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\label{foo}\input{bar.tex}"#), + ("bar.tex", r#"\ref{foo}"#), + ]) + .main("foo.tex") + .line(0) + .character(9) + .include_declaration(true) + .build(); + let uri1 = tester.uri("foo.tex"); + let uri2 = tester.uri("bar.tex"); + let mut actual_references = Vec::new(); + + let request = tester.reference(); + let context = CursorContext::new(request); + find_label_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![ + Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 7, 0, 10)), + Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 5, 0, 8)), + ]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_reference() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\label{foo}\input{bar.tex}"#), + ("bar.tex", r#"\ref{foo}"#), + ("baz.tex", r#"\ref{foo}\input{bar.tex}"#), + ]) + .main("bar.tex") + .line(0) + .character(7) + .build(); + let uri1 = tester.uri("bar.tex"); + let uri2 = tester.uri("baz.tex"); + let mut actual_references = Vec::new(); + + let request = tester.reference(); + let context = CursorContext::new(request); + find_label_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![ + Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 5, 0, 8)), + Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 5, 0, 8)), + ]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_reference_include_declaration() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\label{foo}"#), + ("bar.tex", r#"\ref{foo}\input{foo.tex}"#), + ]) + .main("bar.tex") + .line(0) + .character(7) + .include_declaration(true) + .build(); + let uri1 = tester.uri("foo.tex"); + let uri2 = tester.uri("bar.tex"); + let mut actual_references = Vec::new(); + + let request = tester.reference(); + let context = CursorContext::new(request); + find_label_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![ + Location::new(uri2.as_ref().clone().into(), Range::new_simple(0, 5, 0, 8)), + Location::new(uri1.as_ref().clone().into(), Range::new_simple(0, 7, 0, 10)), + ]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .reference(); + let mut actual_references = Vec::new(); + + let context = CursorContext::new(request); + find_label_references(&context, CancellationToken::none(), &mut actual_references); + + assert!(actual_references.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .reference(); + let mut actual_references = Vec::new(); + + let context = CursorContext::new(request); + find_label_references(&context, CancellationToken::none(), &mut actual_references); + + assert!(actual_references.is_empty()); + } +} diff --git a/support/texlab/src/features/reference/string.rs b/support/texlab/src/features/reference/string.rs new file mode 100644 index 0000000000..b7dd4e039a --- /dev/null +++ b/support/texlab/src/features/reference/string.rs @@ -0,0 +1,206 @@ +use cancellation::CancellationToken; +use lsp_types::{Location, ReferenceParams}; + +use crate::{ + features::cursor::CursorContext, + syntax::{bibtex, CstNode}, + LineIndexExt, +}; + +pub fn find_string_references( + context: &CursorContext<ReferenceParams>, + cancellation_token: &CancellationToken, + items: &mut Vec<Location>, +) -> Option<()> { + let name_text = context + .cursor + .as_bibtex() + .filter(|token| token.kind() == bibtex::WORD) + .filter(|token| matches!(token.parent().kind(), bibtex::TOKEN | bibtex::STRING))? + .text(); + + let document = context.request.main_document(); + let data = document.data.as_bibtex()?; + for node in data.root.descendants() { + cancellation_token.result().ok()?; + if let Some(name) = bibtex::String::cast(node) + .and_then(|string| string.name()) + .filter(|name| { + context.request.params.context.include_declaration && name.text() == name_text + }) + .or_else(|| { + bibtex::Token::cast(node) + .and_then(|token| token.syntax().first_token()) + .filter(|name| name.text() == name_text) + }) + { + items.push(Location::new( + document.uri.as_ref().clone().into(), + document.line_index.line_col_lsp_range(name.text_range()), + )); + } + } + Some(()) +} + +#[cfg(test)] +mod tests { + use indoc::indoc; + use lsp_types::Range; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_definition() { + let tester = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { r#" + @string{foo = {Foo}} + @string{bar = {Bar}} + @article{baz, author = foo} + "# }, + )]) + .main("main.bib") + .line(2) + .character(24) + .build(); + let uri = tester.uri("main.bib"); + + let mut actual_references = Vec::new(); + let request = tester.reference(); + let context = CursorContext::new(request); + find_string_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![Location::new( + uri.as_ref().clone().into(), + Range::new_simple(2, 23, 2, 26), + )]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_definition_include_declaration() { + let tester = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { r#" + @string{foo = {Foo}} + @string{bar = {Bar}} + @article{baz, author = foo} + "# }, + )]) + .main("main.bib") + .line(2) + .character(24) + .include_declaration(true) + .build(); + let uri = tester.uri("main.bib"); + + let mut actual_references = Vec::new(); + let request = tester.reference(); + let context = CursorContext::new(request); + find_string_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![ + Location::new(uri.as_ref().clone().into(), Range::new_simple(0, 8, 0, 11)), + Location::new(uri.as_ref().clone().into(), Range::new_simple(2, 23, 2, 26)), + ]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_reference() { + let tester = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { r#" + @string{foo = {Foo}} + @string{bar = {Bar}} + @article{baz, author = foo} + "# }, + )]) + .main("main.bib") + .line(0) + .character(10) + .build(); + let uri = tester.uri("main.bib"); + + let mut actual_references = Vec::new(); + let request = tester.reference(); + let context = CursorContext::new(request); + find_string_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![Location::new( + uri.as_ref().clone().into(), + Range::new_simple(2, 23, 2, 26), + )]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_reference_include_declaration() { + let tester = FeatureTester::builder() + .files(vec![( + "main.bib", + indoc! { r#" + @string{foo = {Foo}} + @string{bar = {Bar}} + @article{baz, author = foo} + "# }, + )]) + .main("main.bib") + .line(0) + .character(10) + .include_declaration(true) + .build(); + let uri = tester.uri("main.bib"); + + let mut actual_references = Vec::new(); + let request = tester.reference(); + let context = CursorContext::new(request); + find_string_references(&context, CancellationToken::none(), &mut actual_references); + + let expected_references = vec![ + Location::new(uri.as_ref().clone().into(), Range::new_simple(0, 8, 0, 11)), + Location::new(uri.as_ref().clone().into(), Range::new_simple(2, 23, 2, 26)), + ]; + assert_eq!(actual_references, expected_references); + } + + #[test] + fn test_empty_latex_document() { + let request = FeatureTester::builder() + .files(vec![("main.tex", "")]) + .main("main.tex") + .line(0) + .character(0) + .build() + .reference(); + + let mut actual_references = Vec::new(); + let context = CursorContext::new(request); + find_string_references(&context, CancellationToken::none(), &mut actual_references); + + assert!(actual_references.is_empty()); + } + + #[test] + fn test_empty_bibtex_document() { + let request = FeatureTester::builder() + .files(vec![("main.bib", "")]) + .main("main.bib") + .line(0) + .character(0) + .build() + .reference(); + + let mut actual_references = Vec::new(); + let context = CursorContext::new(request); + find_string_references(&context, CancellationToken::none(), &mut actual_references); + + assert!(actual_references.is_empty()); + } +} diff --git a/support/texlab/src/features/rename.rs b/support/texlab/src/features/rename.rs new file mode 100644 index 0000000000..ba09fa268c --- /dev/null +++ b/support/texlab/src/features/rename.rs @@ -0,0 +1,34 @@ +mod command; +mod entry; +mod label; + +use cancellation::CancellationToken; +use lsp_types::{Range, RenameParams, TextDocumentPositionParams, WorkspaceEdit}; + +use self::{ + command::{prepare_command_rename, rename_command}, + entry::{prepare_entry_rename, rename_entry}, + label::{prepare_label_rename, rename_label}, +}; + +use super::{cursor::CursorContext, FeatureRequest}; + +pub fn prepare_rename_all( + request: FeatureRequest<TextDocumentPositionParams>, + cancellation_token: &CancellationToken, +) -> Option<Range> { + let context = CursorContext::new(request); + prepare_entry_rename(&context, cancellation_token) + .or_else(|| prepare_label_rename(&context, cancellation_token)) + .or_else(|| prepare_command_rename(&context, cancellation_token)) +} + +pub fn rename_all( + request: FeatureRequest<RenameParams>, + cancellation_token: &CancellationToken, +) -> Option<WorkspaceEdit> { + let context = CursorContext::new(request); + rename_entry(&context, cancellation_token) + .or_else(|| rename_label(&context, cancellation_token)) + .or_else(|| rename_command(&context, cancellation_token)) +} diff --git a/support/texlab/src/features/rename/command.rs b/support/texlab/src/features/rename/command.rs new file mode 100644 index 0000000000..402783f3ae --- /dev/null +++ b/support/texlab/src/features/rename/command.rs @@ -0,0 +1,98 @@ +use std::collections::HashMap; + +use cancellation::CancellationToken; +use cstree::{TextRange, TextSize}; +use lsp_types::{Range, RenameParams, TextEdit, WorkspaceEdit}; + +use crate::{ + features::cursor::{CursorContext, HasPosition}, + LineIndexExt, +}; + +pub fn prepare_command_rename<P: HasPosition>( + context: &CursorContext<P>, + _cancellation_token: &CancellationToken, +) -> Option<Range> { + Some( + context + .request + .main_document() + .line_index + .line_col_lsp_range(context.cursor.command_range(context.offset)?), + ) +} + +pub fn rename_command( + context: &CursorContext<RenameParams>, + cancellation_token: &CancellationToken, +) -> Option<WorkspaceEdit> { + cancellation_token.result().ok()?; + prepare_command_rename(context, cancellation_token)?; + let name = context.cursor.as_latex()?.text(); + let mut changes = HashMap::new(); + for document in &context.request.subset.documents { + cancellation_token.result().ok()?; + + if let Some(data) = document.data.as_latex() { + let edits = data + .root + .descendants_with_tokens() + .filter_map(|element| element.into_token()) + .filter(|token| token.kind().is_command_name() && token.text() == name) + .map(|token| { + let range = token.text_range(); + let range = document.line_index.line_col_lsp_range(TextRange::new( + range.start() + TextSize::from(1), + range.end(), + )); + TextEdit::new(range, context.request.params.new_name.clone()) + }) + .collect(); + + changes.insert(document.uri.as_ref().clone().into(), edits); + } + } + + Some(WorkspaceEdit::new(changes)) +} + +#[cfg(test)] +mod tests { + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_command() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\baz\include{bar.tex}"#), + ("bar.tex", r#"\baz"#), + ]) + .main("foo.tex") + .line(0) + .character(2) + .new_name("qux") + .build(); + + let uri1 = tester.uri("foo.tex"); + let uri2 = tester.uri("bar.tex"); + let req = tester.rename(); + + let context = CursorContext::new(req); + let actual_edit = rename_command(&context, CancellationToken::none()).unwrap(); + + let mut expected_changes = HashMap::new(); + expected_changes.insert( + uri1.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(0, 1, 0, 4), "qux".into())], + ); + expected_changes.insert( + uri2.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(0, 1, 0, 4), "qux".into())], + ); + let expected_edit = WorkspaceEdit::new(expected_changes); + + assert_eq!(actual_edit, expected_edit); + } +} diff --git a/support/texlab/src/features/rename/entry.rs b/support/texlab/src/features/rename/entry.rs new file mode 100644 index 0000000000..14fbfd42d7 --- /dev/null +++ b/support/texlab/src/features/rename/entry.rs @@ -0,0 +1,161 @@ +use std::collections::HashMap; + +use cancellation::CancellationToken; +use lsp_types::{Range, RenameParams, TextEdit, WorkspaceEdit}; + +use crate::{ + features::cursor::{Cursor, CursorContext, HasPosition}, + syntax::{bibtex, latex, CstNode}, + DocumentData, LineIndexExt, +}; + +pub fn prepare_entry_rename<P: HasPosition>( + context: &CursorContext<P>, + _cancellation_token: &CancellationToken, +) -> Option<Range> { + let range = match &context.cursor { + Cursor::Latex(token) if token.kind() == latex::WORD => { + let group = latex::CurlyGroupWordList::cast(token.parent())?; + latex::Citation::cast(group.syntax().parent()?)?; + token.text_range() + } + Cursor::Bibtex(token) if token.kind() == bibtex::WORD => { + bibtex::Entry::cast(token.parent())?; + token.text_range() + } + _ => return None, + }; + + Some( + context + .request + .main_document() + .line_index + .line_col_lsp_range(range), + ) +} + +pub fn rename_entry( + context: &CursorContext<RenameParams>, + cancellation_token: &CancellationToken, +) -> Option<WorkspaceEdit> { + cancellation_token.result().ok()?; + prepare_entry_rename(context, cancellation_token)?; + let key_text = context + .cursor + .as_latex() + .map(|token| token.text()) + .or_else(|| context.cursor.as_bibtex().map(|token| token.text()))?; + + let mut changes = HashMap::new(); + for document in &context.request.subset.documents { + cancellation_token.result().ok()?; + match &document.data { + DocumentData::Latex(data) => { + let edits: Vec<_> = data + .root + .descendants() + .filter_map(latex::Citation::cast) + .filter_map(|citation| citation.key_list()) + .flat_map(|keys| keys.words()) + .filter(|key| key.text() == key_text) + .map(|key| document.line_index.line_col_lsp_range(key.text_range())) + .map(|range| TextEdit::new(range, context.request.params.new_name.clone())) + .collect(); + changes.insert(document.uri.as_ref().clone().into(), edits); + } + DocumentData::Bibtex(data) => { + let edits: Vec<_> = data + .root + .descendants() + .filter_map(bibtex::Entry::cast) + .filter_map(|entry| entry.key()) + .filter(|key| key.text() == key_text) + .map(|key| document.line_index.line_col_lsp_range(key.text_range())) + .map(|range| TextEdit::new(range, context.request.params.new_name.clone())) + .collect(); + changes.insert(document.uri.as_ref().clone().into(), edits); + } + DocumentData::BuildLog(_) => {} + } + } + + Some(WorkspaceEdit::new(changes)) +} + +#[cfg(test)] +mod tests { + use lsp_types::TextEdit; + + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_entry() { + let tester = FeatureTester::builder() + .files(vec![ + ("main.bib", r#"@article{foo, bar = baz}"#), + ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo}"), + ]) + .main("main.bib") + .line(0) + .character(9) + .new_name("qux") + .build(); + + let uri1 = tester.uri("main.bib"); + let uri2 = tester.uri("main.tex"); + let request = tester.rename(); + + let context = CursorContext::new(request); + let actual_edit = rename_entry(&context, CancellationToken::none()).unwrap(); + + let mut expected_changes = HashMap::new(); + expected_changes.insert( + uri1.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(0, 9, 0, 12), "qux".into())], + ); + expected_changes.insert( + uri2.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(1, 6, 1, 9), "qux".into())], + ); + let expected_edit = WorkspaceEdit::new(expected_changes); + + assert_eq!(actual_edit, expected_edit); + } + + #[test] + fn test_citation() { + let tester = FeatureTester::builder() + .files(vec![ + ("main.bib", r#"@article{foo, bar = baz}"#), + ("main.tex", "\\addbibresource{main.bib}\n\\cite{foo}"), + ]) + .main("main.tex") + .line(1) + .character(6) + .new_name("qux") + .build(); + + let uri1 = tester.uri("main.bib"); + let uri2 = tester.uri("main.tex"); + let request = tester.rename(); + + let context = CursorContext::new(request); + let actual_edit = rename_entry(&context, CancellationToken::none()).unwrap(); + + let mut expected_changes = HashMap::new(); + expected_changes.insert( + uri1.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(0, 9, 0, 12), "qux".into())], + ); + expected_changes.insert( + uri2.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(1, 6, 1, 9), "qux".into())], + ); + let expected_edit = WorkspaceEdit::new(expected_changes); + + assert_eq!(actual_edit, expected_edit); + } +} diff --git a/support/texlab/src/features/rename/label.rs b/support/texlab/src/features/rename/label.rs new file mode 100644 index 0000000000..11b414ccfa --- /dev/null +++ b/support/texlab/src/features/rename/label.rs @@ -0,0 +1,142 @@ +use std::collections::HashMap; + +use cancellation::CancellationToken; +use lsp_types::{Range, RenameParams, TextEdit, WorkspaceEdit}; + +use crate::{ + features::cursor::{CursorContext, HasPosition}, + syntax::{latex, CstNode}, + LineIndexExt, +}; + +pub fn prepare_label_rename<P: HasPosition>( + context: &CursorContext<P>, + _cancellation_token: &CancellationToken, +) -> Option<Range> { + let name = context.cursor.as_latex()?; + name.parent().parent().filter(|node| { + matches!( + node.kind(), + latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE + ) + })?; + + Some( + context + .request + .main_document() + .line_index + .line_col_lsp_range(name.text_range()), + ) +} + +pub fn rename_label( + context: &CursorContext<RenameParams>, + cancellation_token: &CancellationToken, +) -> Option<WorkspaceEdit> { + prepare_label_rename(context, cancellation_token)?; + let name_text = context.cursor.as_latex()?.text(); + let mut changes = HashMap::new(); + for document in &context.request.subset.documents { + cancellation_token.result().ok()?; + if let Some(data) = document.data.as_latex() { + let mut edits = Vec::new(); + for node in data.root.descendants() { + if let Some(range) = latex::LabelDefinition::cast(node) + .and_then(|label| label.name()) + .and_then(|name| name.word()) + .filter(|name| name.text() == name_text) + .map(|name| document.line_index.line_col_lsp_range(name.text_range())) + { + edits.push(TextEdit::new( + range, + context.request.params.new_name.clone(), + )); + } + + latex::LabelReference::cast(node) + .and_then(|label| label.name_list()) + .into_iter() + .flat_map(|label| label.words()) + .filter(|name| name.text() == name_text) + .map(|name| document.line_index.line_col_lsp_range(name.text_range())) + .for_each(|range| { + edits.push(TextEdit::new( + range, + context.request.params.new_name.clone(), + )); + }); + + if let Some(label) = latex::LabelReferenceRange::cast(node) { + if let Some(name1) = label + .from() + .and_then(|name| name.word()) + .filter(|name| name.text() == name_text) + { + edits.push(TextEdit::new( + document.line_index.line_col_lsp_range(name1.text_range()), + context.request.params.new_name.clone(), + )); + } + + if let Some(name2) = label + .from() + .and_then(|name| name.word()) + .filter(|name| name.text() == name_text) + { + edits.push(TextEdit::new( + document.line_index.line_col_lsp_range(name2.text_range()), + context.request.params.new_name.clone(), + )); + } + } + } + + changes.insert(document.uri.as_ref().clone().into(), edits); + } + } + + Some(WorkspaceEdit::new(changes)) +} + +#[cfg(test)] +mod tests { + use crate::{features::testing::FeatureTester, RangeExt}; + + use super::*; + + #[test] + fn test_label() { + let tester = FeatureTester::builder() + .files(vec![ + ("foo.tex", r#"\label{foo}\include{bar}"#), + ("bar.tex", r#"\ref{foo}"#), + ("baz.tex", r#"\ref{foo}"#), + ]) + .main("foo.tex") + .line(0) + .character(7) + .new_name("bar") + .build(); + + let uri1 = tester.uri("foo.tex"); + let uri2 = tester.uri("bar.tex"); + let request = tester.rename(); + + let context = CursorContext::new(request); + let actual_edit = rename_label(&context, CancellationToken::none()).unwrap(); + + let mut expected_changes = HashMap::new(); + expected_changes.insert( + uri1.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(0, 7, 0, 10), "bar".into())], + ); + expected_changes.insert( + uri2.as_ref().clone().into(), + vec![TextEdit::new(Range::new_simple(0, 5, 0, 8), "bar".into())], + ); + let expected_edit = WorkspaceEdit::new(expected_changes); + + assert_eq!(actual_edit, expected_edit); + } +} diff --git a/support/texlab/src/features/semantic.rs b/support/texlab/src/features/semantic.rs new file mode 100644 index 0000000000..8dd2674601 --- /dev/null +++ b/support/texlab/src/features/semantic.rs @@ -0,0 +1,21 @@ +mod bibtex; +pub mod legend; + +use cancellation::CancellationToken; +use lsp_types::{SemanticTokens, SemanticTokensRangeParams}; + +use self::bibtex::find_bibtex_semantic_tokens_range; + +use super::FeatureRequest; + +pub fn find_semantic_tokens_range( + request: FeatureRequest<SemanticTokensRangeParams>, + cancellation_token: &CancellationToken, +) -> Option<SemanticTokens> { + let mut data = Vec::new(); + find_bibtex_semantic_tokens_range(&request, &mut data, cancellation_token); + Some(SemanticTokens { + result_id: None, + data, + }) +} diff --git a/support/texlab/src/features/semantic/bibtex.rs b/support/texlab/src/features/semantic/bibtex.rs new file mode 100644 index 0000000000..6d6a46b6ce --- /dev/null +++ b/support/texlab/src/features/semantic/bibtex.rs @@ -0,0 +1,289 @@ +use cancellation::CancellationToken; +use cstree::{NodeOrToken, TextRange}; +use lsp_types::{ + SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokensRangeParams, +}; + +use crate::{ + features::FeatureRequest, + syntax::{bibtex, CstNode}, + BibtexEntryTypeCategory, LineIndex, LineIndexExt, LANGUAGE_DATA, +}; + +use super::legend::*; + +struct Context<'a> { + line_index: &'a LineIndex, + previous_line: u32, + previous_character: u32, + tokens: &'a mut Vec<SemanticToken>, + cancellation_token: &'a CancellationToken, +} + +impl<'a> Context<'a> { + pub fn insert( + &mut self, + range: TextRange, + token_type: SemanticTokenType, + token_modifiers_bitset: ModifierSet, + ) { + let range = self.line_index.line_col_lsp_range(range); + + let mut delta_line = range.start.line; + let mut delta_start = range.start.character; + if !self.tokens.is_empty() { + delta_line -= self.previous_line; + if delta_line == 0 { + delta_start -= self.previous_character; + } + } + self.tokens.push(SemanticToken { + delta_line, + delta_start, + length: range.end.character - range.start.character, + token_type: type_index(token_type), + token_modifiers_bitset: token_modifiers_bitset.0, + }); + + self.previous_line = range.start.line; + self.previous_character = range.start.character; + } +} + +pub fn find_bibtex_semantic_tokens_range( + request: &FeatureRequest<SemanticTokensRangeParams>, + tokens: &mut Vec<SemanticToken>, + cancellation_token: &CancellationToken, +) -> Option<()> { + let document = request.main_document(); + let data = document.data.as_bibtex()?; + + let mut context = Context { + line_index: &document.line_index, + previous_line: 0, + previous_character: 0, + tokens, + cancellation_token, + }; + + let range = document.line_index.offset_lsp_range(request.params.range); + + for node in data + .root + .children() + .filter(|node| node.text_range().intersect(range).is_some()) + { + cancellation_token.result().ok()?; + visit_junk(&mut context, node) + .or_else(|| visit_preamble(&mut context, node)) + .or_else(|| visit_string(&mut context, node)) + .or_else(|| visit_entry(&mut context, node)); + } + + Some(()) +} + +fn visit_junk(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> { + let junk = bibtex::Junk::cast(node)?; + for token in junk + .syntax() + .children_with_tokens() + .filter_map(|element| element.into_token()) + .filter(|token| token.kind() != bibtex::WHITESPACE) + { + context.insert(token.text_range(), JUNK, ModifierSet::default()); + } + Some(()) +} + +fn visit_preamble(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> { + let preamble = bibtex::Preamble::cast(node)?; + for element in preamble.syntax().children_with_tokens() { + match element { + NodeOrToken::Node(value) => { + visit_value(context, value); + } + NodeOrToken::Token(token) => match token.kind() { + bibtex::PREAMBLE_TYPE => { + context.insert(token.text_range(), PREAMBLE_TYPE, ModifierSet::default()); + } + bibtex::L_CURLY | bibtex::R_CURLY => { + context.insert(token.text_range(), CURLY, ModifierSet::default()); + } + bibtex::L_PAREN | bibtex::R_PAREN => { + context.insert(token.text_range(), PAREN, ModifierSet::default()); + } + _ => {} + }, + } + } + + Some(()) +} + +fn visit_string(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> { + let string = bibtex::String::cast(node)?; + for element in string.syntax().children_with_tokens() { + match element { + NodeOrToken::Node(value) => { + visit_value(context, value); + } + NodeOrToken::Token(token) => match token.kind() { + bibtex::STRING_TYPE => { + context.insert(token.text_range(), STRING_TYPE, ModifierSet::default()); + } + bibtex::L_CURLY | bibtex::R_CURLY => { + context.insert(token.text_range(), CURLY, ModifierSet::default()); + } + bibtex::L_PAREN | bibtex::R_PAREN => { + context.insert(token.text_range(), PAREN, ModifierSet::default()); + } + bibtex::WORD => { + context.insert( + token.text_range(), + STRING_NAME, + ModifierSet::default() | SemanticTokenModifier::DEFINITION, + ); + } + bibtex::EQUALITY_SIGN => { + context.insert(token.text_range(), EQUALITY_SIGN, ModifierSet::default()); + } + _ => {} + }, + } + } + + Some(()) +} + +fn visit_entry(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> { + let entry = bibtex::Entry::cast(node)?; + + for element in entry.syntax().children_with_tokens() { + match element { + NodeOrToken::Node(field) => { + visit_field(context, field); + } + NodeOrToken::Token(token) => match token.kind() { + bibtex::ENTRY_TYPE => { + context.insert( + token.text_range(), + match LANGUAGE_DATA.find_entry_type(&token.text()[1..]) { + Some(ty) => match ty.category { + BibtexEntryTypeCategory::Article => ARTICLE_TYPE, + BibtexEntryTypeCategory::Misc => MISC_TYPE, + BibtexEntryTypeCategory::String => STRING_TYPE, + BibtexEntryTypeCategory::Book => BOOK_TYPE, + BibtexEntryTypeCategory::Collection => COLLECTION_TYPE, + BibtexEntryTypeCategory::Part => PART_TYPE, + BibtexEntryTypeCategory::Thesis => THESIS_TYPE, + }, + None => UNKNOWN_TYPE, + }, + ModifierSet::default(), + ); + } + bibtex::L_CURLY | bibtex::R_CURLY => { + context.insert(token.text_range(), CURLY, ModifierSet::default()); + } + bibtex::L_PAREN | bibtex::R_PAREN => { + context.insert(token.text_range(), PAREN, ModifierSet::default()); + } + bibtex::WORD => { + context.insert( + token.text_range(), + ENTRY_KEY, + ModifierSet::default() + | SemanticTokenModifier::DEFINITION + | SemanticTokenModifier::READONLY, + ); + } + bibtex::COMMA => { + context.insert(token.text_range(), COMMA, ModifierSet::default()); + } + _ => {} + }, + } + } + + Some(()) +} + +fn visit_field(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> { + context.cancellation_token.result().ok()?; + let field = bibtex::Field::cast(node)?; + context.insert(field.name()?.text_range(), FIELD, ModifierSet::default()); + if let Some(token) = field.equality_sign() { + context.insert(token.text_range(), EQUALITY_SIGN, ModifierSet::default()); + } + + if let Some(value) = field.value() { + visit_value(context, value.syntax()); + } + Some(()) +} + +fn visit_value(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> { + context.cancellation_token.result().ok()?; + let value = bibtex::Value::cast(node)?; + for child in value.syntax().children_with_tokens() { + match child { + NodeOrToken::Token(token) if token.kind() == bibtex::HASH => { + context.insert(token.text_range(), HASH, ModifierSet::default()); + } + NodeOrToken::Token(_) => {} + NodeOrToken::Node(token) => { + if let Some(word) = token + .first_child_or_token() + .and_then(|child| child.into_token()) + { + if word.text().chars().all(|c| c.is_ascii_digit()) { + context.insert( + word.text_range(), + SemanticTokenType::NUMBER, + ModifierSet::default(), + ); + } else { + context.insert(word.text_range(), STRING_NAME, ModifierSet::default()); + } + } else { + visit_content(context, token); + } + } + } + } + Some(()) +} + +fn visit_content(context: &mut Context, node: &bibtex::SyntaxNode) -> Option<()> { + for token in node + .descendants_with_tokens() + .filter_map(|element| element.into_token()) + { + match token.kind() { + bibtex::WORD => { + context.insert(token.text_range(), TEXT, ModifierSet::default()); + } + bibtex::COMMA => { + context.insert(token.text_range(), COMMA, ModifierSet::default()); + } + bibtex::L_CURLY | bibtex::R_CURLY => { + context.insert(token.text_range(), CURLY, ModifierSet::default()); + } + bibtex::L_PAREN | bibtex::R_PAREN => { + context.insert(token.text_range(), PAREN, ModifierSet::default()); + } + bibtex::SyntaxKind::QUOTE => { + context.insert(token.text_range(), QUOTE, ModifierSet::default()); + } + bibtex::SyntaxKind::EQUALITY_SIGN => { + context.insert(token.text_range(), EQUALITY_SIGN, ModifierSet::default()); + } + bibtex::SyntaxKind::COMMAND_NAME => { + context.insert(token.text_range(), GENERIC_COMMAND, ModifierSet::default()); + } + _ => {} + } + } + Some(()) +} diff --git a/support/texlab/src/features/semantic/legend.rs b/support/texlab/src/features/semantic/legend.rs new file mode 100644 index 0000000000..0b2a53cd38 --- /dev/null +++ b/support/texlab/src/features/semantic/legend.rs @@ -0,0 +1,83 @@ +use std::ops; + +use lsp_types::{SemanticTokenModifier, SemanticTokenType}; + +macro_rules! define_semantic_token_types { + ($(($ident:ident, $string:literal)),*$(,)?) => { + $(pub const $ident: SemanticTokenType = SemanticTokenType::new($string);)* + + pub const SUPPORTED_TYPES: &[SemanticTokenType] = &[ + SemanticTokenType::COMMENT, + SemanticTokenType::NUMBER, + $($ident),* + ]; + }; +} + +define_semantic_token_types![ + (JUNK, "junk"), + (PREAMBLE_TYPE, "preambleType"), + (STRING_TYPE, "stringType"), + (COMMENT_TYPE, "commentType"), + (ARTICLE_TYPE, "articleType"), + (BOOK_TYPE, "bookType"), + (COLLECTION_TYPE, "collectionType"), + (PART_TYPE, "partType"), + (THESIS_TYPE, "thesisType"), + (MISC_TYPE, "miscType"), + (UNKNOWN_TYPE, "unknownType"), + (CURLY, "curly"), + (BRACK, "brack"), + (PAREN, "paren"), + (COMMA, "comma"), + (ENTRY_KEY, "entryKey"), + (FIELD, "field"), + (TEXT, "text"), + (STRING_NAME, "string"), + (EQUALITY_SIGN, "equalitySign"), + (HASH, "hash"), + (QUOTE, "quote"), + (GENERIC_COMMAND, "genericCommand"), +]; + +macro_rules! define_semantic_token_modifiers { + ($(($ident:ident, $string:literal)),*$(,)?) => { + $(pub const $ident: SemanticTokenModifier = SemanticTokenModifier::new($string);)* + + pub const SUPPORTED_MODIFIERS: &[SemanticTokenModifier] = &[ + SemanticTokenModifier::DEFAULT_LIBRARY, + SemanticTokenModifier::DECLARATION, + SemanticTokenModifier::DEFINITION, + SemanticTokenModifier::READONLY, + $($ident),* + ]; + }; +} + +define_semantic_token_modifiers![(ITALIC, "italic"),]; + +pub fn type_index(ty: SemanticTokenType) -> u32 { + SUPPORTED_TYPES.iter().position(|t| *t == ty).unwrap() as u32 +} + +#[derive(Default)] +pub struct ModifierSet(pub u32); + +impl ops::BitOrAssign<SemanticTokenModifier> for ModifierSet { + fn bitor_assign(&mut self, rhs: SemanticTokenModifier) { + let index = SUPPORTED_MODIFIERS + .iter() + .position(|modifier| modifier == &rhs) + .unwrap(); + self.0 |= 1 << index; + } +} + +impl ops::BitOr<SemanticTokenModifier> for ModifierSet { + type Output = ModifierSet; + + fn bitor(mut self, rhs: SemanticTokenModifier) -> Self::Output { + self |= rhs; + self + } +} diff --git a/support/texlab/src/features/symbol.rs b/support/texlab/src/features/symbol.rs new file mode 100644 index 0000000000..349b6d0a37 --- /dev/null +++ b/support/texlab/src/features/symbol.rs @@ -0,0 +1,125 @@ +mod bibtex; +mod latex; +mod project_order; +mod types; + +use std::{cmp::Reverse, sync::Arc}; + +use cancellation::CancellationToken; +use lsp_types::{ + DocumentSymbolParams, DocumentSymbolResponse, SymbolInformation, WorkspaceSymbolParams, +}; + +use crate::{ClientCapabilitiesExt, Uri, Workspace}; + +use self::{ + bibtex::find_bibtex_symbols, latex::find_latex_symbols, project_order::ProjectOrdering, +}; + +use super::FeatureRequest; + +pub fn find_document_symbols( + req: FeatureRequest<DocumentSymbolParams>, + token: &CancellationToken, +) -> DocumentSymbolResponse { + let mut buf = Vec::new(); + find_latex_symbols(&req.subset, &mut buf, token); + find_bibtex_symbols(&req.subset, &mut buf, token); + if req + .context + .client_capabilities + .lock() + .unwrap() + .has_hierarchical_document_symbol_support() + { + DocumentSymbolResponse::Nested( + buf.into_iter() + .map(|symbol| symbol.into_document_symbol()) + .collect(), + ) + } else { + let mut new_buf = Vec::new(); + for symbol in buf { + symbol.flatten(&mut new_buf); + } + let mut new_buf = new_buf + .into_iter() + .map(|symbol| symbol.into_symbol_info(req.main_document().uri.as_ref().clone())) + .collect(); + sort_symbols(req.workspace.as_ref(), &mut new_buf); + DocumentSymbolResponse::Flat(new_buf) + } +} + +#[derive(Debug, Clone)] +struct WorkspaceSymbol { + info: SymbolInformation, + search_text: String, +} + +pub fn find_workspace_symbols( + workspace: &dyn Workspace, + params: &WorkspaceSymbolParams, + token: &CancellationToken, +) -> Vec<SymbolInformation> { + let mut symbols = Vec::new(); + + for document in workspace.documents() { + if let Some(subset) = workspace.subset(Arc::clone(&document.uri)) { + let mut buf = Vec::new(); + find_latex_symbols(&subset, &mut buf, token); + find_bibtex_symbols(&subset, &mut buf, token); + let mut new_buf = Vec::new(); + + for symbol in buf { + symbol.flatten(&mut new_buf); + } + + for symbol in new_buf { + symbols.push(WorkspaceSymbol { + search_text: symbol.search_text(), + info: symbol.into_symbol_info(document.uri.as_ref().clone()), + }); + } + } + } + + let query_words: Vec<_> = params + .query + .split_whitespace() + .map(str::to_lowercase) + .collect(); + let mut filtered = Vec::new(); + for symbol in symbols { + let mut included = true; + for word in &query_words { + if !symbol.search_text.contains(word) { + included = false; + break; + } + } + + if included { + filtered.push(symbol.info); + } + } + sort_symbols(workspace, &mut filtered); + filtered +} + +fn sort_symbols(workspace: &dyn Workspace, symbols: &mut Vec<SymbolInformation>) { + let ordering = ProjectOrdering::from(workspace); + symbols.sort_by(|left, right| { + let left_key = ( + ordering.get(&Uri::from(left.location.uri.clone())), + left.location.range.start, + Reverse(left.location.range.end), + ); + let right_key = ( + ordering.get(&Uri::from(right.location.uri.clone())), + right.location.range.start, + Reverse(right.location.range.end), + ); + left_key.cmp(&right_key) + }); +} diff --git a/support/texlab/src/features/symbol/bibtex.rs b/support/texlab/src/features/symbol/bibtex.rs new file mode 100644 index 0000000000..e030195880 --- /dev/null +++ b/support/texlab/src/features/symbol/bibtex.rs @@ -0,0 +1,88 @@ +use cancellation::CancellationToken; + +use crate::{ + syntax::{ + bibtex::{self, HasType}, + CstNode, + }, + BibtexEntryTypeCategory, LineIndexExt, WorkspaceSubset, LANGUAGE_DATA, +}; + +use super::types::{InternalSymbol, InternalSymbolKind}; + +pub fn find_bibtex_symbols( + subset: &WorkspaceSubset, + buf: &mut Vec<InternalSymbol>, + token: &CancellationToken, +) -> Option<()> { + let main_document = subset.documents.first()?; + let data = main_document.data.as_bibtex()?; + + for node in data.root.children() { + if token.is_canceled() { + return None; + } + + if let Some(string) = bibtex::String::cast(node) { + if let Some(name) = string.name() { + buf.push(InternalSymbol { + name: name.text().into(), + label: None, + kind: InternalSymbolKind::String, + deprecated: false, + full_range: main_document + .line_index + .line_col_lsp_range(string.small_range()), + selection_range: main_document + .line_index + .line_col_lsp_range(name.text_range()), + children: Vec::new(), + }) + } + } else if let Some(entry) = bibtex::Entry::cast(node) { + if let Some(ty) = entry.ty() { + if let Some(key) = entry.key() { + let mut children = Vec::new(); + for field in entry.fields() { + if let Some(name) = field.name() { + let symbol = InternalSymbol { + name: name.text().to_string(), + label: None, + kind: InternalSymbolKind::Field, + deprecated: false, + full_range: main_document + .line_index + .line_col_lsp_range(field.small_range()), + selection_range: main_document + .line_index + .line_col_lsp_range(name.text_range()), + children: Vec::new(), + }; + children.push(symbol); + } + } + + let category = LANGUAGE_DATA + .find_entry_type(&ty.text()[1..]) + .map(|ty| ty.category) + .unwrap_or(BibtexEntryTypeCategory::Misc); + + buf.push(InternalSymbol { + name: key.text().to_string(), + label: None, + kind: InternalSymbolKind::Entry(category), + deprecated: false, + full_range: main_document + .line_index + .line_col_lsp_range(entry.small_range()), + selection_range: main_document + .line_index + .line_col_lsp_range(key.text_range()), + children, + }); + } + } + } + } + Some(()) +} diff --git a/support/texlab/src/features/symbol/latex.rs b/support/texlab/src/features/symbol/latex.rs new file mode 100644 index 0000000000..ce584c0c76 --- /dev/null +++ b/support/texlab/src/features/symbol/latex.rs @@ -0,0 +1,488 @@ +use std::str::FromStr; + +use cancellation::CancellationToken; +use lsp_types::Range; +use smol_str::SmolStr; +use titlecase::titlecase; + +use crate::{ + find_caption_by_parent, find_label_number, + syntax::{ + latex::{self, HasBrack, HasCurly}, + CstNode, + }, + LabelledFloatKind, LatexDocumentData, LineIndexExt, WorkspaceSubset, LANGUAGE_DATA, +}; + +use super::types::{InternalSymbol, InternalSymbolKind}; + +pub fn find_latex_symbols( + subset: &WorkspaceSubset, + buf: &mut Vec<InternalSymbol>, + token: &CancellationToken, +) -> Option<()> { + let main_document = subset + .documents + .first() + .filter(|document| document.uri.as_str().ends_with(".tex"))?; + + let data = main_document.data.as_latex()?; + let mut context = Context { + subset, + data, + token, + }; + + let root = &context.data.root; + let mut symbols = visit(&mut context, root); + buf.append(&mut symbols); + Some(()) +} + +struct Context<'a> { + subset: &'a WorkspaceSubset, + data: &'a LatexDocumentData, + token: &'a CancellationToken, +} + +fn visit(context: &mut Context, node: &latex::SyntaxNode) -> Vec<InternalSymbol> { + let symbol = match node.kind() { + latex::PART + | latex::CHAPTER + | latex::SECTION + | latex::SUBSECTION + | latex::SUBSUBSECTION + | latex::PARAGRAPH + | latex::SUBPARAGRAPH => visit_section(context, node), + latex::ENUM_ITEM => visit_enum_item(context, node), + latex::EQUATION => visit_equation(context, node), + latex::ENVIRONMENT => latex::Environment::cast(node) + .and_then(|env| env.begin()) + .and_then(|begin| begin.name()) + .and_then(|name| name.word()) + .map(|name| name.text()) + .and_then(|name| { + if LANGUAGE_DATA + .math_environments + .iter() + .any(|env| env == name) + { + visit_equation_environment(context, node) + } else if LANGUAGE_DATA + .enum_environments + .iter() + .any(|env| env == name) + { + visit_enumeration(context, node, name) + } else if let Ok(float_kind) = LabelledFloatKind::from_str(name) { + visit_float(context, node, float_kind) + } else { + visit_theorem(context, node, name) + } + }), + _ => None, + }; + + match symbol { + Some(mut parent) => { + for child in node.children() { + if context.token.is_canceled() { + break; + } + + parent.children.append(&mut visit(context, child)); + } + vec![parent] + } + None => { + let mut symbols = Vec::new(); + for child in node.children() { + if context.token.is_canceled() { + break; + } + symbols.append(&mut visit(context, child)); + } + symbols + } + } +} + +fn visit_section(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> { + let section = latex::Section::cast(node)?; + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(section.small_range()); + + let group = section.name()?; + let group_text = group.content_text()?; + + let symbol = match find_label_by_parent(context, section.syntax()) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match number { + Some(number) => format!("{} {}", number, group_text), + None => group_text.to_string(), + }; + + InternalSymbol { + name, + label: Some(label), + kind: InternalSymbolKind::Section, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => InternalSymbol { + name: group_text.to_string(), + label: None, + kind: InternalSymbolKind::Section, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + Some(symbol) +} + +fn visit_enum_item(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> { + let enum_item = latex::EnumItem::cast(node)?; + if !enum_item + .syntax() + .ancestors() + .filter_map(latex::Environment::cast) + .filter_map(|environment| environment.begin()) + .filter_map(|begin| begin.name()) + .filter_map(|name| name.word()) + .any(|name| { + LANGUAGE_DATA + .enum_environments + .iter() + .any(|e| e == name.text()) + }) + { + return None; + } + + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(enum_item.small_range()); + + let name = enum_item + .label() + .and_then(|label| label.content_text()) + .unwrap_or_else(|| "Item".to_string()); + + let symbol = match find_label_by_parent(context, node) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => InternalSymbol { + name: number.map(Into::into).unwrap_or_else(|| name.to_string()), + label: Some(label), + kind: InternalSymbolKind::EnumerationItem, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + }, + None => InternalSymbol { + name: name.to_string(), + label: None, + kind: InternalSymbolKind::EnumerationItem, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + Some(symbol) +} + +fn visit_equation(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> { + let equation = latex::Equation::cast(node)?; + + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(equation.small_range()); + + make_equation_symbol(context, node, full_range) +} + +fn visit_equation_environment( + context: &mut Context, + node: &latex::SyntaxNode, +) -> Option<InternalSymbol> { + let environment = latex::Environment::cast(node)?; + + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(environment.small_range()); + + make_equation_symbol(context, node, full_range) +} + +fn make_equation_symbol( + context: &mut Context, + node: &cstree::ResolvedNode<latex::Language>, + full_range: Range, +) -> Option<InternalSymbol> { + let symbol = match find_label_by_parent(context, node) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match number { + Some(number) => format!("Equation ({})", number), + None => "Equation".to_string(), + }; + + InternalSymbol { + name, + label: Some(label), + kind: InternalSymbolKind::Equation, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => InternalSymbol { + name: "Equation".to_string(), + label: None, + kind: InternalSymbolKind::Equation, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + Some(symbol) +} + +fn visit_enumeration( + context: &mut Context, + node: &latex::SyntaxNode, + env_name: &str, +) -> Option<InternalSymbol> { + let environment = latex::Environment::cast(node)?; + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(environment.small_range()); + + let name = titlecase(env_name); + let symbol = match find_label_by_parent(context, environment.syntax()) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match number { + Some(number) => format!("{} {}", name, number), + None => name, + }; + + InternalSymbol { + name, + label: Some(label), + kind: InternalSymbolKind::Enumeration, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => InternalSymbol { + name, + label: None, + kind: InternalSymbolKind::Enumeration, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + Some(symbol) +} + +fn visit_float( + context: &mut Context, + node: &latex::SyntaxNode, + float_kind: LabelledFloatKind, +) -> Option<InternalSymbol> { + let environment = latex::Environment::cast(node)?; + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(environment.small_range()); + + let (float_kind, symbol_kind) = match float_kind { + LabelledFloatKind::Algorithm => ("Algorithm", InternalSymbolKind::Algorithm), + LabelledFloatKind::Figure => ("Figure", InternalSymbolKind::Figure), + LabelledFloatKind::Listing => ("Listing", InternalSymbolKind::Listing), + LabelledFloatKind::Table => ("Table", InternalSymbolKind::Table), + }; + + let caption = find_caption_by_parent(environment.syntax())?; + let symbol = match find_label_by_parent(context, environment.syntax()) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match number { + Some(number) => format!("{} {}: {}", float_kind, number, caption), + None => format!("{}: {}", float_kind, caption), + }; + + InternalSymbol { + name, + label: Some(label), + kind: symbol_kind, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => InternalSymbol { + name: format!("{}: {}", float_kind, caption), + label: None, + kind: symbol_kind, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + + Some(symbol) +} + +fn visit_theorem( + context: &mut Context, + node: &latex::SyntaxNode, + environment_name: &str, +) -> Option<InternalSymbol> { + let definition = context + .subset + .documents + .iter() + .filter_map(|document| document.data.as_latex()) + .find_map(|data| { + data.extras + .theorem_environments + .iter() + .find(|environment| environment.name == environment_name) + .cloned() + })?; + + let node = latex::Environment::cast(node)?; + let theorem_description = node + .begin()? + .options() + .and_then(|option| option.content_text()); + + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(node.small_range()); + + let symbol = match find_label_by_parent(context, node.syntax()) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match (number, theorem_description) { + (Some(number), Some(desc)) => { + format!("{} {} ({})", definition.description, number, desc) + } + (Some(number), None) => format!("{} {}", definition.description, number), + (None, Some(desc)) => format!("{} ({})", definition.description, desc), + (None, None) => definition.description.clone(), + }; + + InternalSymbol { + name, + label: Some(label), + kind: InternalSymbolKind::Theorem, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => { + let name = match theorem_description { + Some(desc) => format!("{} ({})", definition.description, desc), + None => definition.description.to_string(), + }; + InternalSymbol { + name, + label: None, + kind: InternalSymbolKind::Theorem, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + } + } + }; + Some(symbol) +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct NumberedLabel { + name: String, + range: Range, + number: Option<SmolStr>, +} + +fn find_label_by_parent( + context: &mut Context, + parent: &latex::SyntaxNode, +) -> Option<NumberedLabel> { + let node = parent.children().find_map(latex::LabelDefinition::cast)?; + + let name = node.name()?.word()?.text(); + let range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(node.small_range()); + + let number = find_label_number(&context.subset, name); + Some(NumberedLabel { + name: name.to_string(), + range, + number: number.map(Into::into), + }) +} diff --git a/support/texlab/src/features/symbol/project_order.rs b/support/texlab/src/features/symbol/project_order.rs new file mode 100644 index 0000000000..08b43ad50c --- /dev/null +++ b/support/texlab/src/features/symbol/project_order.rs @@ -0,0 +1,223 @@ +use std::{sync::Arc, usize}; + +use petgraph::{algo::tarjan_scc, Directed, Graph}; +use rustc_hash::FxHashSet; + +use crate::{Uri, Workspace, WorkspaceSubset}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ProjectOrdering { + ordering: Vec<Arc<Uri>>, +} + +impl ProjectOrdering { + pub fn get(&self, uri: &Uri) -> usize { + self.ordering + .iter() + .position(|u| u.as_ref() == uri) + .unwrap_or(usize::MAX) + } +} + +impl From<&dyn Workspace> for ProjectOrdering { + fn from(workspace: &dyn Workspace) -> Self { + let mut ordering = Vec::new(); + let uris: FxHashSet<Arc<Uri>> = workspace + .documents() + .into_iter() + .map(|document| Arc::clone(&document.uri)) + .collect(); + + let comps = connected_components(workspace); + for comp in comps { + let graph = build_dependency_graph(&comp); + + let mut visited = FxHashSet::default(); + let root_index = *graph.node_weight(tarjan_scc(&graph)[0][0]).unwrap(); + let mut stack = vec![Arc::clone(&comp.documents[root_index].uri)]; + + while let Some(uri) = stack.pop() { + if !visited.insert(Arc::clone(&uri)) { + continue; + } + + ordering.push(Arc::clone(&uri)); + if let Some(document) = workspace.get(&uri) { + if let Some(data) = document.data.as_latex() { + for link in data.extras.explicit_links.iter().rev() { + for target in &link.targets { + if uris.contains(target.as_ref()) { + stack.push(Arc::clone(target)); + } + } + } + } + } + } + } + + Self { ordering } + } +} + +fn connected_components(workspace: &dyn Workspace) -> Vec<WorkspaceSubset> { + let mut components = Vec::new(); + let mut visited = FxHashSet::default(); + for root_document in workspace.documents() { + if !visited.insert(Arc::clone(&root_document.uri)) { + continue; + } + + let subset = workspace.subset(Arc::clone(&root_document.uri)).unwrap(); + for document in &subset.documents { + visited.insert(Arc::clone(&document.uri)); + } + components.push(subset); + } + components +} + +fn build_dependency_graph(subset: &WorkspaceSubset) -> Graph<usize, (), Directed> { + let mut graph = Graph::new(); + let nodes: Vec<_> = (0..subset.documents.len()) + .map(|i| graph.add_node(i)) + .collect(); + + for (i, document) in subset.documents.iter().enumerate() { + if let Some(data) = document.data.as_latex() { + for link in &data.extras.explicit_links { + for target in &link.targets { + if let Some(j) = subset + .documents + .iter() + .position(|document| document.uri.as_ref() == target.as_ref()) + { + graph.add_edge(nodes[j], nodes[i], ()); + break; + } + } + } + } + } + graph +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use crate::{create_workspace_fast, DocumentLanguage, ServerContext, WorkspaceSource}; + + use super::*; + + #[test] + fn test_no_cycles() -> Result<()> { + let workspace: Arc<dyn Workspace> = Arc::new(create_workspace_fast(Arc::new( + ServerContext::new(std::env::temp_dir()), + ))?); + + let a = workspace.open( + Arc::new(Uri::parse("http://example.com/a.tex")?), + "".into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let b = workspace.open( + Arc::new(Uri::parse("http://example.com/b.tex")?), + "".into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let c = workspace.open( + Arc::new(Uri::parse("http://example.com/c.tex")?), + r#"\include{b}\include{a}"#.into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let ordering = ProjectOrdering::from(workspace.as_ref()); + + assert_eq!(ordering.get(&a.uri), 2); + assert_eq!(ordering.get(&b.uri), 1); + assert_eq!(ordering.get(&c.uri), 0); + Ok(()) + } + + #[test] + fn test_cycles() -> Result<()> { + let workspace: Arc<dyn Workspace> = Arc::new(create_workspace_fast(Arc::new( + ServerContext::new(std::env::temp_dir()), + ))?); + + let a = workspace.open( + Arc::new(Uri::parse("http://example.com/a.tex")?), + r#"\include{b}"#.into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let b = workspace.open( + Arc::new(Uri::parse("http://example.com/b.tex")?), + r#"\include{a}"#.into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let c = workspace.open( + Arc::new(Uri::parse("http://example.com/c.tex")?), + r#"\include{a}"#.into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let ordering = ProjectOrdering::from(workspace.as_ref()); + + assert_eq!(ordering.get(&a.uri), 1); + assert_eq!(ordering.get(&b.uri), 2); + assert_eq!(ordering.get(&c.uri), 0); + Ok(()) + } + + #[test] + fn test_multiple_roots() -> Result<()> { + let workspace: Arc<dyn Workspace> = Arc::new(create_workspace_fast(Arc::new( + ServerContext::new(std::env::temp_dir()), + ))?); + + let a = workspace.open( + Arc::new(Uri::parse("http://example.com/a.tex")?), + r#"\include{b}"#.into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let b = workspace.open( + Arc::new(Uri::parse("http://example.com/b.tex")?), + r#""#.into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let c = workspace.open( + Arc::new(Uri::parse("http://example.com/c.tex")?), + r#""#.into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let d = workspace.open( + Arc::new(Uri::parse("http://example.com/d.tex")?), + r#"\include{c}"#.into(), + DocumentLanguage::Latex, + WorkspaceSource::Client, + ); + + let ordering = ProjectOrdering::from(workspace.as_ref()); + + assert!(ordering.get(&a.uri) < ordering.get(&b.uri)); + assert!(ordering.get(&d.uri) < ordering.get(&c.uri)); + Ok(()) + } +} diff --git a/support/texlab/src/symbol/types.rs b/support/texlab/src/features/symbol/types.rs index f10f43c637..bb00a932d8 100644 --- a/support/texlab/src/symbol/types.rs +++ b/support/texlab/src/features/symbol/types.rs @@ -1,10 +1,9 @@ -use crate::{ - protocol::{DocumentSymbol, Location, Range, SymbolInformation, SymbolKind, Uri}, - syntax::{BibtexEntryTypeCategory, Structure}, -}; +use lsp_types::{DocumentSymbol, Location, Range, SymbolInformation, SymbolKind}; + +use crate::{features::lsp_kinds::Structure, BibtexEntryTypeCategory, Uri}; #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum LatexSymbolKind { +pub enum InternalSymbolKind { Section, Figure, Algorithm, @@ -19,8 +18,8 @@ pub enum LatexSymbolKind { String, } -impl Into<SymbolKind> for LatexSymbolKind { - fn into(self) -> SymbolKind { +impl InternalSymbolKind { + pub fn into_symbol_kind(self) -> SymbolKind { match self { Self::Section => Structure::Section.symbol_kind(), Self::Figure | Self::Algorithm | Self::Table | Self::Listing => { @@ -38,37 +37,37 @@ impl Into<SymbolKind> for LatexSymbolKind { } #[derive(Debug, PartialEq, Eq, Clone)] -pub struct LatexSymbol { +pub struct InternalSymbol { pub name: String, pub label: Option<String>, - pub kind: LatexSymbolKind, + pub kind: InternalSymbolKind, pub deprecated: bool, pub full_range: Range, pub selection_range: Range, - pub children: Vec<LatexSymbol>, + pub children: Vec<InternalSymbol>, } -impl LatexSymbol { +impl InternalSymbol { pub fn search_text(&self) -> String { let kind = match self.kind { - LatexSymbolKind::Section => "latex section", - LatexSymbolKind::Figure => "latex float figure", - LatexSymbolKind::Algorithm => "latex float algorithm", - LatexSymbolKind::Table => "latex float table", - LatexSymbolKind::Listing => "latex float listing", - LatexSymbolKind::Enumeration => "latex enumeration", - LatexSymbolKind::EnumerationItem => "latex enumeration item", - LatexSymbolKind::Theorem => "latex math", - LatexSymbolKind::Equation => "latex math equation", - LatexSymbolKind::Entry(_) => "bibtex entry", - LatexSymbolKind::Field => "bibtex field", - LatexSymbolKind::String => "bibtex string", + InternalSymbolKind::Section => "latex section", + InternalSymbolKind::Figure => "latex float figure", + InternalSymbolKind::Algorithm => "latex float algorithm", + InternalSymbolKind::Table => "latex float table", + InternalSymbolKind::Listing => "latex float listing", + InternalSymbolKind::Enumeration => "latex enumeration", + InternalSymbolKind::EnumerationItem => "latex enumeration item", + InternalSymbolKind::Theorem => "latex math", + InternalSymbolKind::Equation => "latex math equation", + InternalSymbolKind::Entry(_) => "bibtex entry", + InternalSymbolKind::Field => "bibtex field", + InternalSymbolKind::String => "bibtex string", }; format!("{} {}", kind, self.name).to_lowercase() } pub fn flatten(mut self, buffer: &mut Vec<Self>) { - if self.kind == LatexSymbolKind::Field { + if self.kind == InternalSymbolKind::Field { return; } for symbol in self.children.drain(..) { @@ -77,28 +76,34 @@ impl LatexSymbol { buffer.push(self); } - pub fn into_symbol_info(self, uri: Uri) -> SymbolInformation { - SymbolInformation { + pub fn into_document_symbol(self) -> DocumentSymbol { + let children = self + .children + .into_iter() + .map(|child| child.into_document_symbol()) + .collect(); + #[allow(deprecated)] + DocumentSymbol { name: self.name, + detail: self.label, + kind: self.kind.into_symbol_kind(), deprecated: Some(self.deprecated), - kind: self.kind.into(), - container_name: None, - location: Location::new(uri.into(), self.full_range), + range: self.full_range, + selection_range: self.selection_range, + children: Some(children), + tags: None, } } -} -impl Into<DocumentSymbol> for LatexSymbol { - fn into(self) -> DocumentSymbol { - let children = self.children.into_iter().map(Into::into).collect(); - DocumentSymbol { + pub fn into_symbol_info(self, uri: Uri) -> SymbolInformation { + #[allow(deprecated)] + SymbolInformation { name: self.name, + kind: self.kind.into_symbol_kind(), deprecated: Some(self.deprecated), - detail: self.label, - kind: self.kind.into(), - selection_range: self.selection_range, - range: self.full_range, - children: Some(children), + location: Location::new(uri.into(), self.full_range), + container_name: None, + tags: None, } } } diff --git a/support/texlab/src/folding/bibtex_decl.rs b/support/texlab/src/folding/bibtex_decl.rs deleted file mode 100644 index f0ffa787e1..0000000000 --- a/support/texlab/src/folding/bibtex_decl.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{FoldingRange, FoldingRangeKind, FoldingRangeParams}, - syntax::{bibtex, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use petgraph::graph::NodeIndex; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexDeclarationFoldingProvider; - -#[async_trait] -impl FeatureProvider for BibtexDeclarationFoldingProvider { - type Params = FoldingRangeParams; - type Output = Vec<FoldingRange>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - if let DocumentContent::Bibtex(tree) = &req.current().content { - tree.children(tree.root) - .filter_map(|decl| Self::fold(tree, decl)) - .collect() - } else { - Vec::new() - } - } -} - -impl BibtexDeclarationFoldingProvider { - fn fold(tree: &bibtex::Tree, decl: NodeIndex) -> Option<FoldingRange> { - let (ty, right) = match &tree.graph[decl] { - bibtex::Node::Preamble(preamble) => (Some(&preamble.ty), preamble.right.as_ref()), - bibtex::Node::String(string) => (Some(&string.ty), string.right.as_ref()), - bibtex::Node::Entry(entry) => (Some(&entry.ty), entry.right.as_ref()), - bibtex::Node::Root(_) - | bibtex::Node::Comment(_) - | bibtex::Node::Field(_) - | bibtex::Node::Word(_) - | bibtex::Node::Command(_) - | bibtex::Node::QuotedContent(_) - | bibtex::Node::BracedContent(_) - | bibtex::Node::Concat(_) => (None, None), - }; - - Some(FoldingRange { - start_line: ty?.start().line, - start_character: Some(ty?.start().character), - end_line: right?.end().line, - end_character: Some(right?.end().character), - kind: Some(FoldingRangeKind::Region), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - use indoc::indoc; - - #[tokio::test] - async fn preamble() { - let actual_foldings = FeatureTester::new() - .file("main.bib", r#"@preamble{"foo"}"#) - .main("main.bib") - .test_folding(BibtexDeclarationFoldingProvider) - .await; - - let expected_foldings = vec![FoldingRange { - start_line: 0, - start_character: Some(0), - end_line: 0, - end_character: Some(16), - kind: Some(FoldingRangeKind::Region), - }]; - - assert_eq!(actual_foldings, expected_foldings); - } - - #[tokio::test] - async fn string() { - let actual_foldings = FeatureTester::new() - .file("main.bib", r#"@string{foo = "bar"}"#) - .main("main.bib") - .test_folding(BibtexDeclarationFoldingProvider) - .await; - - let expected_foldings = vec![FoldingRange { - start_line: 0, - start_character: Some(0), - end_line: 0, - end_character: Some(20), - kind: Some(FoldingRangeKind::Region), - }]; - - assert_eq!(actual_foldings, expected_foldings); - } - - #[tokio::test] - async fn entry() { - let actual_foldings = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @article{foo, - bar = baz - } - "# - ), - ) - .main("main.bib") - .test_folding(BibtexDeclarationFoldingProvider) - .await; - - let expected_foldings = vec![FoldingRange { - start_line: 0, - start_character: Some(0), - end_line: 2, - end_character: Some(1), - kind: Some(FoldingRangeKind::Region), - }]; - - assert_eq!(actual_foldings, expected_foldings); - } - - #[tokio::test] - async fn comment() { - let actual_foldings = FeatureTester::new() - .file("main.bib", "foo") - .main("main.bib") - .test_folding(BibtexDeclarationFoldingProvider) - .await; - - assert!(actual_foldings.is_empty()); - } - - #[tokio::test] - async fn entry_invalid() { - let actual_foldings = FeatureTester::new() - .file("main.bib", "@article{foo,") - .main("main.bib") - .test_folding(BibtexDeclarationFoldingProvider) - .await; - - assert!(actual_foldings.is_empty()); - } - - #[tokio::test] - async fn latex() { - let actual_foldings = FeatureTester::new() - .file("main.tex", "foo") - .main("main.tex") - .test_folding(BibtexDeclarationFoldingProvider) - .await; - - assert!(actual_foldings.is_empty()); - } -} diff --git a/support/texlab/src/folding/latex_env.rs b/support/texlab/src/folding/latex_env.rs deleted file mode 100644 index ced2567b41..0000000000 --- a/support/texlab/src/folding/latex_env.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{FoldingRange, FoldingRangeKind, FoldingRangeParams}, - syntax::SyntaxNode, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexEnvironmentFoldingProvider; - -#[async_trait] -impl FeatureProvider for LatexEnvironmentFoldingProvider { - type Params = FoldingRangeParams; - type Output = Vec<FoldingRange>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut foldings = Vec::new(); - if let DocumentContent::Latex(table) = &req.current().content { - for env in &table.environments { - let left_node = &table[env.left.parent]; - let right_node = &table[env.right.parent]; - let folding = FoldingRange { - start_line: left_node.end().line, - start_character: Some(left_node.end().character), - end_line: right_node.start().line, - end_character: Some(right_node.start().character), - kind: Some(FoldingRangeKind::Region), - }; - foldings.push(folding); - } - } - foldings - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - use indoc::indoc; - - #[tokio::test] - async fn multiline() { - let actual_foldings = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \begin{foo} - \end{foo} - "# - ), - ) - .main("main.tex") - .test_folding(LatexEnvironmentFoldingProvider) - .await; - - let expected_foldings = vec![FoldingRange { - start_line: 0, - start_character: Some(11), - end_line: 1, - end_character: Some(0), - kind: Some(FoldingRangeKind::Region), - }]; - - assert_eq!(actual_foldings, expected_foldings); - } - - #[tokio::test] - async fn bibtex() { - let actual_foldings = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .test_folding(LatexEnvironmentFoldingProvider) - .await; - - assert!(actual_foldings.is_empty()); - } -} diff --git a/support/texlab/src/folding/latex_section.rs b/support/texlab/src/folding/latex_section.rs deleted file mode 100644 index a37efe1a7a..0000000000 --- a/support/texlab/src/folding/latex_section.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{FoldingRange, FoldingRangeKind, FoldingRangeParams}, - syntax::SyntaxNode, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexSectionFoldingProvider; - -#[async_trait] -impl FeatureProvider for LatexSectionFoldingProvider { - type Params = FoldingRangeParams; - type Output = Vec<FoldingRange>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut foldings = Vec::new(); - if let DocumentContent::Latex(table) = &req.current().content { - let sections = &table.sections; - for i in 0..sections.len() { - let current = §ions[i]; - if let Some(next) = sections - .iter() - .skip(i + 1) - .find(|sec| current.level >= sec.level) - { - let next_node = &table[next.parent]; - if next_node.start().line > 0 { - let current_node = &table[current.parent]; - let folding = FoldingRange { - start_line: current_node.end().line, - start_character: Some(current_node.end().character), - end_line: next_node.start().line - 1, - end_character: Some(0), - kind: Some(FoldingRangeKind::Region), - }; - foldings.push(folding); - } - } - } - } - foldings - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - use indoc::indoc; - - #[tokio::test] - async fn nested() { - let actual_foldings = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \section{Foo} - foo - \subsection{Bar} - bar - \section{Baz} - baz - \section{Qux} - "# - ), - ) - .main("main.tex") - .test_folding(LatexSectionFoldingProvider) - .await; - - let expected_foldings = vec![ - FoldingRange { - start_line: 0, - start_character: Some(13), - end_line: 3, - end_character: Some(0), - kind: Some(FoldingRangeKind::Region), - }, - FoldingRange { - start_line: 2, - start_character: Some(16), - end_line: 3, - end_character: Some(0), - kind: Some(FoldingRangeKind::Region), - }, - FoldingRange { - start_line: 4, - start_character: Some(13), - end_line: 5, - end_character: Some(0), - kind: Some(FoldingRangeKind::Region), - }, - ]; - - assert_eq!(actual_foldings, expected_foldings); - } - - #[tokio::test] - async fn bibtex() { - let actual_foldings = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .test_folding(LatexSectionFoldingProvider) - .await; - - assert!(actual_foldings.is_empty()); - } -} diff --git a/support/texlab/src/folding/mod.rs b/support/texlab/src/folding/mod.rs deleted file mode 100644 index 448e3d26fc..0000000000 --- a/support/texlab/src/folding/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -mod bibtex_decl; -mod latex_env; -mod latex_section; - -use self::{ - bibtex_decl::BibtexDeclarationFoldingProvider, latex_env::LatexEnvironmentFoldingProvider, - latex_section::LatexSectionFoldingProvider, -}; -use crate::{ - feature::{ConcatProvider, FeatureProvider, FeatureRequest}, - protocol::{FoldingRange, FoldingRangeParams}, -}; -use async_trait::async_trait; - -pub struct FoldingProvider { - provider: ConcatProvider<FoldingRangeParams, FoldingRange>, -} - -impl FoldingProvider { - pub fn new() -> Self { - Self { - provider: ConcatProvider::new(vec![ - Box::new(BibtexDeclarationFoldingProvider), - Box::new(LatexEnvironmentFoldingProvider), - Box::new(LatexSectionFoldingProvider), - ]), - } - } -} - -impl Default for FoldingProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for FoldingProvider { - type Params = FoldingRangeParams; - type Output = Vec<FoldingRange>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - self.provider.execute(req).await - } -} diff --git a/support/texlab/src/forward_search.rs b/support/texlab/src/forward_search.rs deleted file mode 100644 index a3911ed9f3..0000000000 --- a/support/texlab/src/forward_search.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::{ - protocol::{ForwardSearchResult, ForwardSearchStatus, Options, Uri}, - workspace::Snapshot, -}; -use log::error; -use std::{io, path::Path, process::Stdio}; -use tokio::process::Command; - -pub async fn search<'a>( - snapshot: &'a Snapshot, - tex_uri: &'a Uri, - line_number: u64, - options: &Options, - current_dir: &'a Path, -) -> Option<ForwardSearchResult> { - let pdf_path = snapshot - .resolve_aux_targets( - &snapshot.parent_subfile(tex_uri, options, current_dir)?.uri, - options, - current_dir, - "pdf", - )? - .into_iter() - .filter(|uri| uri.scheme() == "file") - .filter_map(|uri| uri.to_file_path().ok()) - .find(|path| path.exists())?; - - if tex_uri.scheme() != "file" { - return Some(ForwardSearchResult { - status: ForwardSearchStatus::Failure, - }); - } - - let search_options = options - .latex - .as_ref() - .and_then(|opts| opts.forward_search.as_ref()) - .map(Clone::clone) - .unwrap_or_default(); - - if search_options.executable.is_none() || search_options.args.is_none() { - return Some(ForwardSearchResult { - status: ForwardSearchStatus::Unconfigured, - }); - } - - let tex_path = tex_uri.to_file_path().ok()?; - let args: Vec<String> = search_options - .args - .unwrap() - .into_iter() - .flat_map(|arg| replace_placeholder(&tex_path, &pdf_path, line_number, arg)) - .collect(); - - let status = match spawn_process(search_options.executable.unwrap(), args).await { - Ok(()) => ForwardSearchStatus::Success, - Err(why) => { - error!("Unable to execute forward search: {}", why); - ForwardSearchStatus::Failure - } - }; - Some(ForwardSearchResult { status }) -} - -fn replace_placeholder( - tex_file: &Path, - pdf_file: &Path, - line_number: u64, - argument: String, -) -> Option<String> { - let result = if argument.starts_with('"') || argument.ends_with('"') { - argument - } else { - argument - .replace("%f", tex_file.to_str()?) - .replace("%p", pdf_file.to_str()?) - .replace("%l", &(line_number + 1).to_string()) - }; - Some(result) -} - -async fn spawn_process(executable: String, args: Vec<String>) -> io::Result<()> { - Command::new(executable) - .args(args) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()? - .await?; - Ok(()) -} diff --git a/support/texlab/src/highlight/latex_label.rs b/support/texlab/src/highlight/latex_label.rs deleted file mode 100644 index c6d4aab6ee..0000000000 --- a/support/texlab/src/highlight/latex_label.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{DocumentHighlight, DocumentHighlightKind, RangeExt, TextDocumentPositionParams}, - syntax::{latex, LatexLabelKind, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexLabelHighlightProvider; - -#[async_trait] -impl FeatureProvider for LatexLabelHighlightProvider { - type Params = TextDocumentPositionParams; - type Output = Vec<DocumentHighlight>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut highlights = Vec::new(); - if let DocumentContent::Latex(table) = &req.current().content { - if let Some(name) = table - .labels - .iter() - .flat_map(|label| label.names(&table)) - .find(|label| label.range().contains(req.params.position)) - .map(latex::Token::text) - { - for label_group in &table.labels { - for label in label_group.names(&table) { - if label.text() == name { - let kind = match label_group.kind { - LatexLabelKind::Definition => DocumentHighlightKind::Write, - LatexLabelKind::Reference(_) => DocumentHighlightKind::Read, - }; - - let highlight = DocumentHighlight { - range: label.range(), - kind: Some(kind), - }; - highlights.push(highlight); - } - } - } - } - } - highlights - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - use indoc::indoc; - - #[tokio::test] - async fn has_label() { - let actual_highlights = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \label{foo} - \ref{foo} - "# - ), - ) - .main("foo.tex") - .position(0, 7) - .test_position(LatexLabelHighlightProvider) - .await; - - let expected_highlights = vec![ - DocumentHighlight { - range: Range::new_simple(0, 7, 0, 10), - kind: Some(DocumentHighlightKind::Write), - }, - DocumentHighlight { - range: Range::new_simple(1, 5, 1, 8), - kind: Some(DocumentHighlightKind::Read), - }, - ]; - - assert_eq!(actual_highlights, expected_highlights); - } - - #[tokio::test] - async fn no_label_latex() { - let actual_highlights = FeatureTester::new() - .file("foo.tex", "") - .main("foo.tex") - .position(0, 0) - .test_position(LatexLabelHighlightProvider) - .await; - - assert!(actual_highlights.is_empty()); - } - - #[tokio::test] - async fn no_label_bibtex() { - let actual_highlights = FeatureTester::new() - .file("foo.bib", "") - .main("foo.bib") - .position(0, 0) - .test_position(LatexLabelHighlightProvider) - .await; - - assert!(actual_highlights.is_empty()); - } -} diff --git a/support/texlab/src/highlight/mod.rs b/support/texlab/src/highlight/mod.rs deleted file mode 100644 index e47a7f99c2..0000000000 --- a/support/texlab/src/highlight/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -mod latex_label; - -use self::latex_label::LatexLabelHighlightProvider; -use crate::{ - feature::{ConcatProvider, FeatureProvider, FeatureRequest}, - protocol::{DocumentHighlight, TextDocumentPositionParams}, -}; -use async_trait::async_trait; - -pub struct HighlightProvider { - provider: ConcatProvider<TextDocumentPositionParams, DocumentHighlight>, -} - -impl HighlightProvider { - pub fn new() -> Self { - Self { - provider: ConcatProvider::new(vec![Box::new(LatexLabelHighlightProvider)]), - } - } -} - -impl Default for HighlightProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for HighlightProvider { - type Params = TextDocumentPositionParams; - type Output = Vec<DocumentHighlight>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - self.provider.execute(req).await - } -} diff --git a/support/texlab/src/hover/bibtex/entry_type.rs b/support/texlab/src/hover/bibtex/entry_type.rs deleted file mode 100644 index 1e009565d7..0000000000 --- a/support/texlab/src/hover/bibtex/entry_type.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - Hover, HoverContents, MarkupContent, MarkupKind, RangeExt, TextDocumentPositionParams, - }, - syntax::{SyntaxNode, LANGUAGE_DATA}, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexEntryTypeHoverProvider; - -#[async_trait] -impl FeatureProvider for BibtexEntryTypeHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let tree = req.current().content.as_bibtex()?; - for entry in tree - .children(tree.root) - .filter_map(|node| tree.as_entry(node)) - { - if entry.ty.range().contains(req.params.position) { - let ty = &entry.ty.text()[1..]; - let docs = LANGUAGE_DATA.entry_type_documentation(ty)?; - return Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: docs.into(), - }), - range: Some(entry.ty.range()), - }); - } - } - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - - #[tokio::test] - async fn empty_latex_document() { - let actual_hover = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(BibtexEntryTypeHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_hover = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(BibtexEntryTypeHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn known_entry_type() { - let actual_hover = FeatureTester::new() - .file("main.bib", "@article{foo,}") - .main("main.bib") - .position(0, 3) - .test_position(BibtexEntryTypeHoverProvider) - .await - .unwrap(); - - let expected_hover = Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: LANGUAGE_DATA - .entry_type_documentation("article") - .unwrap() - .into(), - }), - range: Some(Range::new_simple(0, 0, 0, 8)), - }; - - assert_eq!(actual_hover, expected_hover); - } - - #[tokio::test] - async fn unknown_entry_type() { - let actual_hover = FeatureTester::new() - .file("main.bib", "@foo{bar,}") - .main("main.bib") - .position(0, 3) - .test_position(BibtexEntryTypeHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn entry_key() { - let actual_hover = FeatureTester::new() - .file("main.bib", "@article{foo,}") - .main("main.bib") - .position(0, 11) - .test_position(BibtexEntryTypeHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } -} diff --git a/support/texlab/src/hover/bibtex/field.rs b/support/texlab/src/hover/bibtex/field.rs deleted file mode 100644 index 4b00f3364c..0000000000 --- a/support/texlab/src/hover/bibtex/field.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - Hover, HoverContents, MarkupContent, MarkupKind, RangeExt, TextDocumentPositionParams, - }, - syntax::{SyntaxNode, LANGUAGE_DATA}, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexFieldHoverProvider; - -#[async_trait] -impl FeatureProvider for BibtexFieldHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let tree = req.current().content.as_bibtex()?; - let name = tree - .find(req.params.position) - .into_iter() - .filter_map(|node| tree.as_field(node)) - .map(|field| &field.name) - .find(|name| name.range().contains(req.params.position))?; - - let docs = LANGUAGE_DATA.field_documentation(name.text())?; - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: docs.into(), - }), - range: Some(name.range()), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - - #[tokio::test] - async fn empty_latex_document() { - let actual_hover = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(BibtexFieldHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_hover = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(BibtexFieldHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn known_field() { - let actual_hover = FeatureTester::new() - .file("main.bib", "@article{foo, author = bar}") - .main("main.bib") - .position(0, 15) - .test_position(BibtexFieldHoverProvider) - .await - .unwrap(); - - let expected_hover = Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: LANGUAGE_DATA.field_documentation("author").unwrap().into(), - }), - range: Some(Range::new_simple(0, 14, 0, 20)), - }; - - assert_eq!(actual_hover, expected_hover); - } - - #[tokio::test] - async fn unknown_field() { - let actual_hover = FeatureTester::new() - .file("main.bib", "@article{foo, bar = baz}") - .main("main.bib") - .position(0, 15) - .test_position(BibtexFieldHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn entry_key() { - let actual_hover = FeatureTester::new() - .file("main.bib", "@article{foo, bar = baz}") - .main("main.bib") - .position(0, 11) - .test_position(BibtexFieldHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } -} diff --git a/support/texlab/src/hover/bibtex/mod.rs b/support/texlab/src/hover/bibtex/mod.rs deleted file mode 100644 index 59ff356fa0..0000000000 --- a/support/texlab/src/hover/bibtex/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod entry_type; -pub mod field; -pub mod string_reference; diff --git a/support/texlab/src/hover/bibtex/string_reference.rs b/support/texlab/src/hover/bibtex/string_reference.rs deleted file mode 100644 index 46724a4f60..0000000000 --- a/support/texlab/src/hover/bibtex/string_reference.rs +++ /dev/null @@ -1,165 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - BibtexFormattingOptions, Hover, HoverContents, MarkupContent, MarkupKind, Position, - TextDocumentPositionParams, - }, - syntax::{bibtex, SyntaxNode}, -}; -use async_trait::async_trait; -use petgraph::graph::NodeIndex; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexStringReferenceHoverProvider; - -#[async_trait] -impl FeatureProvider for BibtexStringReferenceHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let tree = req.current().content.as_bibtex()?; - let reference = Self::find_reference(tree, req.params.position)?; - for string_node in tree.children(tree.root) { - let hover = Self::find_definition(tree, string_node, reference); - if hover.is_some() { - return hover; - } - } - None - } -} - -impl BibtexStringReferenceHoverProvider { - fn find_reference(tree: &bibtex::Tree, pos: Position) -> Option<&bibtex::Token> { - let mut results = tree.find(pos); - results.reverse(); - match ( - &tree.graph[results[0]], - results.get(1).map(|node| &tree.graph[*node]), - ) { - (bibtex::Node::Word(reference), Some(bibtex::Node::Concat(_))) => { - Some(&reference.token) - } - (bibtex::Node::Word(reference), Some(bibtex::Node::Field(_))) => Some(&reference.token), - _ => None, - } - } - - fn find_definition( - tree: &bibtex::Tree, - string_node: NodeIndex, - reference: &bibtex::Token, - ) -> Option<Hover> { - let string = tree.as_string(string_node)?; - if string.name.as_ref()?.text() != reference.text() { - return None; - } - - let options = BibtexFormattingOptions { - line_length: None, - formatter: None, - }; - let text = bibtex::format( - tree, - tree.children(string_node).next()?, - bibtex::FormattingParams { - tab_size: 4, - insert_spaces: true, - options: &options, - }, - ); - Some(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::PlainText, - value: text, - }), - range: Some(reference.range()), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let actual_hover = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(BibtexStringReferenceHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_hover = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(BibtexStringReferenceHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn inside_reference() { - let actual_hover = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = "Foo"} - @string{bar = "Bar"} - @article{baz, author = bar} - "# - ), - ) - .main("main.bib") - .position(2, 24) - .test_position(BibtexStringReferenceHoverProvider) - .await - .unwrap(); - - let expected_hover = Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::PlainText, - value: "\"Bar\"".into(), - }), - range: Some(Range::new_simple(2, 23, 2, 26)), - }; - - assert_eq!(actual_hover, expected_hover); - } - - #[tokio::test] - async fn outside_reference() { - let actual_hover = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = "Foo"} - @string{bar = "Bar"} - @article{baz, author = bar} - "# - ), - ) - .main("main.bib") - .position(2, 20) - .test_position(BibtexStringReferenceHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } -} diff --git a/support/texlab/src/hover/latex/citation.rs b/support/texlab/src/hover/latex/citation.rs deleted file mode 100644 index 8b5468e52e..0000000000 --- a/support/texlab/src/hover/latex/citation.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::{ - citeproc::render_citation, - feature::{FeatureProvider, FeatureRequest}, - protocol::{Hover, HoverContents, RangeExt, TextDocumentPositionParams}, - syntax::{bibtex, Span, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use log::warn; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexCitationHoverProvider; - -#[async_trait] -impl FeatureProvider for LatexCitationHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let (tree, src_key, entry) = Self::get_entry(req)?; - if entry.is_comment() { - None - } else { - let key = entry.key.as_ref()?; - match render_citation(&tree, key.text()) { - Some(markdown) => Some(Hover { - contents: HoverContents::Markup(markdown), - range: Some(src_key.range()), - }), - None => { - warn!("Failed to render entry: {}", key.text()); - None - } - } - } - } -} - -impl LatexCitationHoverProvider { - fn get_entry( - req: &FeatureRequest<TextDocumentPositionParams>, - ) -> Option<(&bibtex::Tree, &Span, &bibtex::Entry)> { - let key = Self::get_key(req)?; - for tree in req - .related() - .iter() - .filter_map(|doc| doc.content.as_bibtex()) - { - for entry in tree - .children(tree.root) - .filter_map(|node| tree.as_entry(node)) - { - if let Some(current_key) = &entry.key { - if current_key.text() == key.text { - return Some((tree, key, entry)); - } - } - } - } - None - } - - fn get_key(req: &FeatureRequest<TextDocumentPositionParams>) -> Option<&Span> { - match &req.current().content { - DocumentContent::Latex(table) => table - .citations - .iter() - .flat_map(|citation| citation.keys(&table)) - .find(|key| key.range().contains(req.params.position)) - .map(|token| &token.span), - DocumentContent::Bibtex(tree) => tree - .children(tree.root) - .filter_map(|node| tree.as_entry(node)) - .filter_map(|entry| entry.key.as_ref()) - .find(|key| key.range().contains(req.params.position)) - .map(|token| &token.span), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{MarkupContent, MarkupKind, Range, RangeExt}, - }; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let actual_hover = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(LatexCitationHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_hover = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(LatexCitationHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn inside_label() { - let actual_hover = FeatureTester::new() - .file( - "main.bib", - "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}", - ) - .file( - "main.tex", - indoc!( - r#" - \addbibresource{main.bib} - \cite{foo} - "# - ), - ) - .main("main.tex") - .position(1, 7) - .test_position(LatexCitationHoverProvider) - .await - .unwrap(); - - let expected_hover = Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: "Bar, F. (1337). *Baz Qux*.".into(), - }), - range: Some(Range::new_simple(1, 6, 1, 9)), - }; - - assert_eq!(actual_hover, expected_hover); - } - - #[tokio::test] - async fn inside_entry() { - let actual_hover = FeatureTester::new() - .file( - "main.bib", - "@article{foo, author = {Foo Bar}, title = {Baz Qux}, year = 1337}", - ) - .file( - "main.tex", - indoc!( - r#" - \addbibresource{main.bib} - \cite{foo} - "# - ), - ) - .main("main.bib") - .position(0, 11) - .test_position(LatexCitationHoverProvider) - .await - .unwrap(); - - let expected_hover = Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: "Bar, F. (1337). *Baz Qux*.".into(), - }), - range: Some(Range::new_simple(0, 9, 0, 12)), - }; - - assert_eq!(actual_hover, expected_hover); - } -} diff --git a/support/texlab/src/hover/latex/component.rs b/support/texlab/src/hover/latex/component.rs deleted file mode 100644 index 5285ec795a..0000000000 --- a/support/texlab/src/hover/latex/component.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::{ - components::COMPONENT_DATABASE, - feature::{FeatureProvider, FeatureRequest}, - protocol::{Hover, HoverContents, RangeExt, TextDocumentPositionParams}, - syntax::{LatexIncludeKind, SyntaxNode}, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexComponentHoverProvider; - -#[async_trait] -impl FeatureProvider for LatexComponentHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let table = req.current().content.as_latex()?; - for include in &table.includes { - match include.kind { - LatexIncludeKind::Package | LatexIncludeKind::Class => { - for path in include.paths(&table) { - if path.range().contains(req.params.position) { - let docs = COMPONENT_DATABASE.documentation(path.text())?; - return Some(Hover { - contents: HoverContents::Markup(docs), - range: Some(path.range()), - }); - } - } - } - _ => {} - } - } - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - - #[tokio::test] - async fn empty_latex_document() { - let actual_hover = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(LatexComponentHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_hover = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(LatexComponentHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn known_package() { - let actual_hover = FeatureTester::new() - .file("main.tex", r#"\usepackage{amsmath}"#) - .main("main.tex") - .position(0, 15) - .test_position(LatexComponentHoverProvider) - .await - .unwrap(); - - assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 12, 0, 19)); - } - - #[tokio::test] - async fn unknown_class() { - let actual_hover = FeatureTester::new() - .file("main.tex", r#"\documentclass{abcdefghijklmnop}"#) - .main("main.tex") - .position(0, 20) - .test_position(LatexComponentHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } -} diff --git a/support/texlab/src/hover/latex/label.rs b/support/texlab/src/hover/latex/label.rs deleted file mode 100644 index 66724d442b..0000000000 --- a/support/texlab/src/hover/latex/label.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::{ - feature::{DocumentView, FeatureProvider, FeatureRequest}, - outline::{Outline, OutlineContext}, - protocol::{Hover, HoverContents, Position, RangeExt, TextDocumentPositionParams}, - syntax::{latex, LatexLabelKind, SyntaxNode}, - workspace::{Document, DocumentContent}, -}; -use async_trait::async_trait; -use std::sync::Arc; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexLabelHoverProvider; - -#[async_trait] -impl FeatureProvider for LatexLabelHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let table = req.current().content.as_latex()?; - let reference = Self::find_reference(table, req.params.position)?; - let (doc, def) = Self::find_definition(&req.view, reference)?; - - let snapshot = Arc::clone(&req.view.snapshot); - let view = DocumentView::analyze(snapshot, doc, &req.options, &req.current_dir); - let outline = Outline::analyze(&view, &req.options, &req.current_dir); - let outline_ctx = OutlineContext::parse(&view, &outline, def)?; - let markup = outline_ctx.documentation(); - Some(Hover { - contents: HoverContents::Markup(markup), - range: Some(reference.range()), - }) - } -} - -impl LatexLabelHoverProvider { - fn find_reference(table: &latex::SymbolTable, pos: Position) -> Option<&latex::Token> { - for label in &table.labels { - let names = label.names(&table); - if names.len() == 1 && table[label.parent].range().contains(pos) { - return Some(&label.names(&table)[0]); - } - - for name in &names { - if name.range().contains(pos) { - return Some(name); - } - } - } - None - } - - fn find_definition( - view: &DocumentView, - reference: &latex::Token, - ) -> Option<(Arc<Document>, latex::Label)> { - for doc in &view.related { - if let DocumentContent::Latex(table) = &doc.content { - for label in &table.labels { - if label.kind == LatexLabelKind::Definition { - for name in label.names(&table) { - if name.text() == reference.text() { - return Some((Arc::clone(&doc), *label)); - } - } - } - } - } - } - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - - #[tokio::test] - async fn empty_latex_document() { - let actual_hover = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_position(LatexLabelHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_hover = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_position(LatexLabelHoverProvider) - .await; - - assert_eq!(actual_hover, None); - } - - #[tokio::test] - async fn section() { - let actual_hover = FeatureTester::new() - .file("main.tex", r#"\section{Foo}\label{sec:foo}"#) - .main("main.tex") - .position(0, 23) - .test_position(LatexLabelHoverProvider) - .await - .unwrap(); - - assert_eq!(actual_hover.range.unwrap(), Range::new_simple(0, 20, 0, 27)); - } -} diff --git a/support/texlab/src/hover/latex/mod.rs b/support/texlab/src/hover/latex/mod.rs deleted file mode 100644 index c119bfc832..0000000000 --- a/support/texlab/src/hover/latex/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[cfg(feature = "citation")] -pub mod citation; - -pub mod component; -pub mod label; -pub mod preview; diff --git a/support/texlab/src/hover/latex/preview.rs b/support/texlab/src/hover/latex/preview.rs deleted file mode 100644 index 8d2ca00cb5..0000000000 --- a/support/texlab/src/hover/latex/preview.rs +++ /dev/null @@ -1,338 +0,0 @@ -use crate::{ - components::COMPONENT_DATABASE, - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - ClientCapabilitiesExt, Hover, HoverContents, MarkupContent, MarkupKind, Range, RangeExt, - TextDocumentPositionParams, - }, - syntax::{latex, CharStream, LatexIncludeKind, SyntaxNode}, - tex::{CompileError, CompileParams, DistributionKind, Format}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use image::{png::PNGEncoder, ColorType, DynamicImage, GenericImageView, ImageBuffer, RgbaImage}; -use io::Cursor; -use log::warn; -use std::{io, process::Stdio, time::Duration}; -use tempfile::TempDir; -use thiserror::Error; -use tokio::process::Command; - -const PREVIEW_ENVIRONMENTS: &[&str] = &[ - "align", - "alignat", - "aligned", - "alignedat", - "algorithmic", - "array", - "Bmatrix", - "bmatrix", - "cases", - "CD", - "eqnarray", - "equation", - "gather", - "gathered", - "matrix", - "multline", - "pmatrix", - "smallmatrix", - "split", - "subarray", - "Vmatrix", - "vmatrix", -]; - -const IGNORED_PACKAGES: &[&str] = &["biblatex", "pgf", "tikz"]; - -#[derive(Debug, Clone, Copy)] -enum MathElement { - Environment(latex::Environment), - Equation(latex::Equation), - Inline(latex::Inline), -} - -impl MathElement { - fn range(&self, tree: &latex::Tree) -> Range { - match self { - Self::Environment(env) => env.range(tree), - Self::Equation(eq) => eq.range(tree), - Self::Inline(inline) => inline.range(tree), - } - } -} - -#[derive(Debug, Error)] -enum RenderError { - #[error("an I/O error occurred: `{0}`")] - IO(#[from] io::Error), - #[error("a compile error occurred: `{0}`")] - Compile(#[from] CompileError), - #[error("compilation failed")] - DviNotFound, - #[error("dvipng is not installed")] - DviPngNotInstalled, - #[error("calling dvipng failed")] - DviPngFaulty, - #[error("failed to decode image")] - DecodeImage, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexPreviewHoverProvider; - -impl LatexPreviewHoverProvider { - fn is_preview_environment( - req: &FeatureRequest<TextDocumentPositionParams>, - table: &latex::SymbolTable, - environment: latex::Environment, - ) -> bool { - let canonical_name = environment - .left - .name(&table) - .map(latex::Token::text) - .unwrap_or_default() - .replace('*', ""); - - PREVIEW_ENVIRONMENTS.contains(&canonical_name.as_ref()) - || Self::theorem_environments(req).contains(&canonical_name.as_ref()) - } - - fn theorem_environments(req: &FeatureRequest<TextDocumentPositionParams>) -> Vec<&str> { - let mut names = Vec::new(); - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - table - .theorem_definitions - .iter() - .map(|thm| thm.name(&table).text()) - .for_each(|thm| names.push(thm)); - } - } - names - } - - async fn render( - req: &FeatureRequest<TextDocumentPositionParams>, - range: Range, - ) -> Result<Hover, RenderError> { - let code = Self::generate_code(req, range); - let params = CompileParams { - file_name: "preview.tex", - code: &code, - format: Format::Latex, - timeout: Duration::from_secs(10), - }; - let dir = req.distro.compile(params).await?.dir; - if !dir.path().join("preview.dvi").exists() { - return Err(RenderError::DviNotFound); - } - - let img = Self::add_margin(Self::dvipng(&dir).await?); - let base64 = Self::encode_image(img); - let markdown = format!("![preview](data:image/png;base64,{})", base64); - dir.close()?; - Ok(Hover { - contents: HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: markdown, - }), - range: Some(range), - }) - } - - fn generate_code(req: &FeatureRequest<TextDocumentPositionParams>, range: Range) -> String { - let mut code = String::new(); - code.push_str("\\documentclass{article}\n"); - code.push_str("\\thispagestyle{empty}\n"); - Self::generate_includes(req, &mut code); - Self::generate_command_definitions(req, &mut code); - Self::generate_math_operators(req, &mut code); - Self::generate_theorem_definitions(req, &mut code); - code.push_str("\\begin{document}\n"); - code.push_str(&CharStream::extract(&req.current().text, range)); - code.push('\n'); - code.push_str("\\end{document}\n"); - code - } - - fn generate_includes(req: &FeatureRequest<TextDocumentPositionParams>, code: &mut String) { - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - let text = &doc.text; - for include in &table.includes { - if include.kind == LatexIncludeKind::Package { - if include - .paths(&table) - .iter() - .all(|path| IGNORED_PACKAGES.contains(&path.text())) - { - continue; - } - - if include - .paths(&table) - .iter() - .map(|path| format!("{}.sty", path.text())) - .any(|name| !COMPONENT_DATABASE.exists(&name)) - { - continue; - } - - code.push_str(&CharStream::extract(&text, table[include.parent].range())); - code.push('\n'); - } - } - } - } - } - - fn generate_command_definitions( - req: &FeatureRequest<TextDocumentPositionParams>, - code: &mut String, - ) { - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - table - .command_definitions - .iter() - .filter(|def| !def.definition_name(&table).contains("@")) - .map(|def| CharStream::extract(&doc.text, table[def.parent].range())) - .for_each(|def| { - code.push_str(&def); - code.push('\n'); - }); - } - } - } - - fn generate_math_operators( - req: &FeatureRequest<TextDocumentPositionParams>, - code: &mut String, - ) { - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - table - .math_operators - .iter() - .filter(|op| !op.definition_name(&table).contains("@")) - .map(|op| CharStream::extract(&doc.text, table[op.parent].range())) - .for_each(|op| { - code.push_str(&op); - code.push('\n'); - }); - } - } - } - - fn generate_theorem_definitions( - req: &FeatureRequest<TextDocumentPositionParams>, - code: &mut String, - ) { - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - table - .theorem_definitions - .iter() - .map(|thm| CharStream::extract(&doc.text, table[thm.parent].range())) - .for_each(|thm| { - code.push_str(&thm); - code.push('\n'); - }) - } - } - } - - async fn dvipng(dir: &TempDir) -> Result<DynamicImage, RenderError> { - let process = Command::new("dvipng") - .args(&["-D", "175", "-T", "tight", "preview.dvi"]) - .current_dir(dir.path()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map_err(|_| RenderError::DviPngNotInstalled)?; - - process.await.map_err(|_| RenderError::DviPngFaulty)?; - - let png_file = dir.path().join("preview1.png"); - let png = image::open(png_file).map_err(|_| RenderError::DecodeImage)?; - Ok(png) - } - - fn add_margin(image: DynamicImage) -> RgbaImage { - let margin = 5; - let width = image.width() + 2 * margin; - let height = image.height() + 2 * margin; - let mut result = - ImageBuffer::from_pixel(width, height, image::Rgba([0xFF, 0xFF, 0xFF, 0xFF])); - - for x in 0..image.width() { - for y in 0..image.height() { - let pixel = image.get_pixel(x, y); - result.put_pixel(x + margin, y + margin, pixel); - } - } - result - } - - fn encode_image(image: RgbaImage) -> String { - let mut image_buf = Cursor::new(Vec::new()); - let png_encoder = PNGEncoder::new(&mut image_buf); - let width = image.width(); - let height = image.height(); - png_encoder - .encode(&image.into_raw(), width, height, ColorType::Rgba8) - .unwrap(); - base64::encode(&image_buf.into_inner()) - } -} - -#[async_trait] -impl FeatureProvider for LatexPreviewHoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - if !req.client_capabilities.has_hover_markdown_support() - || req.distro.kind() == DistributionKind::Tectonic - { - return None; - } - - if let DocumentContent::Latex(table) = &req.current().content { - let mut elements = Vec::new(); - table - .inlines - .iter() - .map(|inline| MathElement::Inline(*inline)) - .for_each(|inline| elements.push(inline)); - - table - .equations - .iter() - .map(|eq| MathElement::Equation(*eq)) - .for_each(|eq| elements.push(eq)); - - table - .environments - .iter() - .filter(|env| Self::is_preview_environment(req, table, **env)) - .map(|env| MathElement::Environment(*env)) - .for_each(|env| elements.push(env)); - - let range = elements - .iter() - .map(|elem| elem.range(&table)) - .find(|range| range.contains(req.params.position))?; - - return match Self::render(req, range).await { - Ok(hover) => Some(hover), - Err(why) => { - warn!("Preview failed: {}", why); - None - } - }; - } - None - } -} diff --git a/support/texlab/src/hover/mod.rs b/support/texlab/src/hover/mod.rs deleted file mode 100644 index e740657add..0000000000 --- a/support/texlab/src/hover/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -mod bibtex; -mod latex; - -#[cfg(feature = "citation")] -use self::latex::citation::LatexCitationHoverProvider; - -use self::{ - bibtex::{ - entry_type::BibtexEntryTypeHoverProvider, field::BibtexFieldHoverProvider, - string_reference::BibtexStringReferenceHoverProvider, - }, - latex::{ - component::LatexComponentHoverProvider, label::LatexLabelHoverProvider, - preview::LatexPreviewHoverProvider, - }, -}; -use crate::{ - feature::{ChoiceProvider, FeatureProvider, FeatureRequest}, - protocol::{Hover, TextDocumentPositionParams}, -}; -use async_trait::async_trait; - -pub struct HoverProvider { - provider: ChoiceProvider<TextDocumentPositionParams, Hover>, -} - -impl HoverProvider { - pub fn new() -> Self { - Self { - provider: ChoiceProvider::new(vec![ - Box::new(BibtexEntryTypeHoverProvider), - Box::new(BibtexStringReferenceHoverProvider), - Box::new(BibtexFieldHoverProvider), - #[cfg(feature = "citation")] - Box::new(LatexCitationHoverProvider), - Box::new(LatexComponentHoverProvider), - Box::new(LatexLabelHoverProvider), - Box::new(LatexPreviewHoverProvider), - ]), - } - } -} - -impl Default for HoverProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for HoverProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Hover>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - self.provider.execute(req).await - } -} diff --git a/support/texlab/src/label.rs b/support/texlab/src/label.rs new file mode 100644 index 0000000000..eead07814e --- /dev/null +++ b/support/texlab/src/label.rs @@ -0,0 +1,291 @@ +use std::str::FromStr; + +use cstree::TextRange; +use lsp_types::{MarkupContent, MarkupKind}; + +use crate::{ + syntax::{ + latex::{self, HasBrack, HasCurly}, + CstNode, + }, + WorkspaceSubset, LANGUAGE_DATA, +}; + +use self::LabelledObject::*; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum LabelledFloatKind { + Figure, + Table, + Listing, + Algorithm, +} + +impl LabelledFloatKind { + pub fn as_str(self) -> &'static str { + match self { + Self::Figure => "Figure", + Self::Table => "Table", + Self::Listing => "Listing", + Self::Algorithm => "Algorithm", + } + } +} + +impl FromStr for LabelledFloatKind { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "figure" | "subfigure" => Ok(Self::Figure), + "table" | "subtable" => Ok(Self::Table), + "listing" | "lstlisting" => Ok(Self::Listing), + "algorithm" => Ok(Self::Algorithm), + _ => Err(()), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum LabelledObject { + Section { + prefix: &'static str, + text: String, + }, + Float { + kind: LabelledFloatKind, + caption: String, + }, + Theorem { + kind: String, + description: Option<String>, + }, + Equation, + EnumItem, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RenderedLabel { + pub range: TextRange, + pub number: Option<String>, + pub object: LabelledObject, +} + +impl RenderedLabel { + pub fn reference(&self) -> String { + match &self.number { + Some(number) => match &self.object { + Section { prefix, text } => format!("{} {} ({})", prefix, number, text), + Float { kind, caption } => format!("{} {}: {}", kind.as_str(), number, caption), + Theorem { + kind, + description: None, + } => format!("{} {}", kind, number), + Theorem { + kind, + description: Some(description), + } => format!("{} {} ({})", kind, number, description), + Equation => format!("Equation ({})", number), + EnumItem => format!("Item {}", number), + }, + None => match &self.object { + Section { prefix, text } => format!("{} ({})", prefix, text), + Float { kind, caption } => format!("{}: {}", kind.as_str(), caption), + Theorem { + kind, + description: None, + } => kind.into(), + Theorem { + kind, + description: Some(description), + } => format!("{} ({})", kind, description), + Equation => "Equation".into(), + EnumItem => "Item".into(), + }, + } + } + + pub fn detail(&self) -> Option<String> { + match &self.object { + Section { .. } | Theorem { .. } | Equation | EnumItem => Some(self.reference()), + Float { kind, .. } => { + let result = match &self.number { + Some(number) => format!("{} {}", kind.as_str(), number), + None => kind.as_str().to_owned(), + }; + Some(result) + } + } + } + + pub fn documentation(&self) -> MarkupContent { + MarkupContent { + kind: MarkupKind::PlainText, + value: self.reference(), + } + } +} + +pub fn render_label<'a>( + subset: &'a WorkspaceSubset, + label_name: &str, + mut label: Option<latex::LabelDefinition<'a>>, +) -> Option<RenderedLabel> { + let mut number = find_label_number(subset, label_name).map(ToString::to_string); + + for document in &subset.documents { + if let Some(data) = document.data.as_latex() { + label = label.or_else(|| find_label_definition(&data.root, label_name)); + } + } + + label?.syntax().ancestors().find_map(|parent| { + render_label_float(parent, &mut number) + .or_else(|| render_label_section(parent, &mut number)) + .or_else(|| render_label_enum_item(parent, &mut number)) + .or_else(|| render_label_equation(parent, &mut number)) + .or_else(|| render_label_theorem(subset, parent, &mut number)) + }) +} + +pub fn find_label_definition<'a>( + root: &'a latex::SyntaxNode, + label_name: &str, +) -> Option<latex::LabelDefinition<'a>> { + root.descendants() + .filter_map(latex::LabelDefinition::cast) + .find(|label| { + label + .name() + .and_then(|name| name.word()) + .map(|name| name.text()) + == Some(label_name) + }) +} + +pub fn find_label_number<'a>(subset: &'a WorkspaceSubset, label_name: &str) -> Option<&'a str> { + subset.documents.iter().find_map(|document| { + document + .data + .as_latex() + .and_then(|data| data.extras.label_numbers_by_name.get(label_name)) + .map(|number| number.as_str()) + }) +} + +fn render_label_float( + parent: &latex::SyntaxNode, + number: &mut Option<String>, +) -> Option<RenderedLabel> { + let environment = latex::Environment::cast(parent)?; + let environment_name = environment.begin()?.name()?.word()?.text(); + let kind = LabelledFloatKind::from_str(&environment_name).ok()?; + let caption = find_caption_by_parent(&parent)?; + Some(RenderedLabel { + range: environment.small_range(), + number: number.take(), + object: LabelledObject::Float { caption, kind }, + }) +} + +fn render_label_section( + parent: &latex::SyntaxNode, + number: &mut Option<String>, +) -> Option<RenderedLabel> { + let section = latex::Section::cast(parent)?; + let text_group = section.name()?; + let text = text_group.content_text()?; + + Some(RenderedLabel { + range: section.small_range(), + number: number.take(), + object: LabelledObject::Section { + prefix: match section.syntax().kind() { + latex::PART => "Part", + latex::CHAPTER => "Chapter", + latex::SECTION => "Section", + latex::SUBSECTION => "Subsection", + latex::SUBSUBSECTION => "Subsubsection", + latex::PARAGRAPH => "Paragraph", + latex::SUBPARAGRAPH => "Subparagraph", + _ => unreachable!(), + }, + text, + }, + }) +} + +fn render_label_enum_item( + parent: &latex::SyntaxNode, + number: &mut Option<String>, +) -> Option<RenderedLabel> { + let enum_item = latex::EnumItem::cast(parent)?; + Some(RenderedLabel { + range: enum_item.small_range(), + number: enum_item + .label() + .and_then(|number| number.content_text()) + .or_else(|| number.take()), + object: LabelledObject::EnumItem, + }) +} + +fn render_label_equation( + parent: &latex::SyntaxNode, + number: &mut Option<String>, +) -> Option<RenderedLabel> { + let environment = latex::Environment::cast(parent)?; + let environment_name = environment.begin()?.name()?.word()?.text(); + + if !LANGUAGE_DATA + .math_environments + .iter() + .any(|name| name == environment_name) + { + return None; + } + + Some(RenderedLabel { + range: environment.small_range(), + number: number.take(), + object: LabelledObject::Equation, + }) +} + +fn render_label_theorem( + subset: &WorkspaceSubset, + parent: &latex::SyntaxNode, + number: &mut Option<String>, +) -> Option<RenderedLabel> { + let environment = latex::Environment::cast(parent)?; + let begin = environment.begin()?; + let description = begin.options().and_then(|options| options.content_text()); + + let environment_name = begin.name()?.word()?.text(); + + let theorem = subset.documents.iter().find_map(|document| { + document.data.as_latex().and_then(|data| { + data.extras + .theorem_environments + .iter() + .find(|theorem| theorem.name.as_str() == environment_name) + }) + })?; + + Some(RenderedLabel { + range: environment.small_range(), + number: number.take(), + object: LabelledObject::Theorem { + kind: theorem.description.clone(), + description, + }, + }) +} + +pub fn find_caption_by_parent(parent: &latex::SyntaxNode) -> Option<String> { + parent + .children() + .filter_map(latex::Caption::cast) + .find_map(|node| node.long()) + .and_then(|node| node.content_text()) +} diff --git a/support/texlab/src/lang_data.rs b/support/texlab/src/lang_data.rs new file mode 100644 index 0000000000..9aa939c483 --- /dev/null +++ b/support/texlab/src/lang_data.rs @@ -0,0 +1,67 @@ +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum BibtexEntryTypeCategory { + Misc, + String, + Article, + Book, + Collection, + Part, + Thesis, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BibtexEntryTypeDoc { + pub name: String, + pub category: BibtexEntryTypeCategory, + pub documentation: Option<String>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BibtexFieldDoc { + pub name: String, + pub documentation: String, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LanguageData { + pub colors: Vec<String>, + pub entry_types: Vec<BibtexEntryTypeDoc>, + pub fields: Vec<BibtexFieldDoc>, + pub pgf_libraries: Vec<String>, + pub tikz_libraries: Vec<String>, + pub math_environments: Vec<String>, + pub enum_environments: Vec<String>, +} + +impl LanguageData { + pub fn find_entry_type(&self, name: &str) -> Option<&BibtexEntryTypeDoc> { + let name = name.to_lowercase(); + self.entry_types + .iter() + .find(|ty| ty.name.to_lowercase() == name) + } + + pub fn entry_type_documentation(&self, name: &str) -> Option<&str> { + self.find_entry_type(name) + .and_then(|ty| ty.documentation.as_ref().map(AsRef::as_ref)) + } + + pub fn field_documentation(&self, name: &str) -> Option<&str> { + self.fields + .iter() + .find(|field| field.name.to_lowercase() == name.to_lowercase()) + .map(|field| field.documentation.as_ref()) + } +} + +pub static LANGUAGE_DATA: Lazy<LanguageData> = Lazy::new(|| { + const JSON: &str = include_str!("../data/lang_data.json"); + serde_json::from_str(JSON).expect("Failed to deserialize language.json") +}); diff --git a/support/texlab/src/language.rs b/support/texlab/src/language.rs new file mode 100644 index 0000000000..45ef22daba --- /dev/null +++ b/support/texlab/src/language.rs @@ -0,0 +1,33 @@ +use std::{ffi::OsStr, path::Path}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum DocumentLanguage { + Latex, + Bibtex, + BuildLog, +} + +impl DocumentLanguage { + pub fn by_path(path: &Path) -> Option<Self> { + path.extension() + .and_then(OsStr::to_str) + .and_then(Self::by_extension) + } + + pub fn by_extension(extension: &str) -> Option<Self> { + match extension.to_lowercase().as_str() { + "tex" | "sty" | "cls" | "def" | "lco" | "aux" | "rnw" => Some(Self::Latex), + "bib" | "bibtex" => Some(Self::Bibtex), + "log" => Some(Self::BuildLog), + _ => None, + } + } + + pub fn by_language_id(language_id: &str) -> Option<Self> { + match language_id { + "latex" | "tex" => Some(Self::Latex), + "bibtex" | "bib" => Some(Self::Bibtex), + _ => None, + } + } +} diff --git a/support/texlab/src/lib.rs b/support/texlab/src/lib.rs index 8644a40f10..0441c1a1d7 100644 --- a/support/texlab/src/lib.rs +++ b/support/texlab/src/lib.rs @@ -1,33 +1,38 @@ +mod capabilities; #[cfg(feature = "citation")] -pub mod citeproc; - -#[cfg(feature = "test")] -pub mod test; - -cfg_if::cfg_if! { - if #[cfg(feature = "server")] { - mod config; - mod build; - - pub mod server; - } -} - -pub mod completion; -pub mod components; -pub mod definition; +pub mod citation; +mod client; +pub mod component_db; +mod config; +mod context; pub mod diagnostics; -pub mod feature; -pub mod folding; -pub mod forward_search; -pub mod highlight; -pub mod hover; -pub mod link; -pub mod outline; -pub mod protocol; -pub mod reference; -pub mod rename; -pub mod symbol; +mod dispatch; +pub mod distro; +pub mod features; +mod label; +mod lang_data; +mod language; +mod line_index; +mod line_index_ext; +mod options; +mod range; +mod req_queue; +mod server; pub mod syntax; -pub mod tex; -pub mod workspace; +mod uri; +mod workspace; + +pub use self::{ + capabilities::ClientCapabilitiesExt, + context::ServerContext, + label::*, + lang_data::*, + language::DocumentLanguage, + line_index::{LineCol, LineColUtf16, LineIndex}, + line_index_ext::LineIndexExt, + options::*, + range::RangeExt, + server::Server, + uri::Uri, + workspace::*, +}; diff --git a/support/texlab/src/line_index.rs b/support/texlab/src/line_index.rs new file mode 100644 index 0000000000..868fe41232 --- /dev/null +++ b/support/texlab/src/line_index.rs @@ -0,0 +1,217 @@ +// The following code has been copied from rust-analyzer. + +//! `LineIndex` maps flat `TextSize` offsets into `(Line, Column)` +//! representation. +use std::iter; + +use cstree::{TextRange, TextSize}; +use rustc_hash::FxHashMap; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LineIndex { + /// Offset the the beginning of each line, zero-based + pub(crate) newlines: Vec<TextSize>, + /// List of non-ASCII characters on each line + pub(crate) utf16_lines: FxHashMap<u32, Vec<Utf16Char>>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct LineColUtf16 { + /// Zero-based + pub line: u32, + /// Zero-based + pub col: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct LineCol { + /// Zero-based + pub line: u32, + /// Zero-based utf8 offset + pub col: u32, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) struct Utf16Char { + /// Start offset of a character inside a line, zero-based + pub(crate) start: TextSize, + /// End offset of a character inside a line, zero-based + pub(crate) end: TextSize, +} + +impl Utf16Char { + /// Returns the length in 8-bit UTF-8 code units. + fn len(&self) -> TextSize { + self.end - self.start + } + + /// Returns the length in 16-bit UTF-16 code units. + fn len_utf16(&self) -> usize { + if self.len() == TextSize::from(4) { + 2 + } else { + 1 + } + } +} + +impl LineIndex { + pub fn new(text: &str) -> LineIndex { + let mut utf16_lines = FxHashMap::default(); + let mut utf16_chars = Vec::new(); + + let mut newlines = vec![0.into()]; + let mut curr_row = 0.into(); + let mut curr_col = 0.into(); + let mut line = 0; + for c in text.chars() { + let c_len = TextSize::of(c); + curr_row += c_len; + if c == '\n' { + newlines.push(curr_row); + + // Save any utf-16 characters seen in the previous line + if !utf16_chars.is_empty() { + utf16_lines.insert(line, utf16_chars); + utf16_chars = Vec::new(); + } + + // Prepare for processing the next line + curr_col = 0.into(); + line += 1; + continue; + } + + if !c.is_ascii() { + utf16_chars.push(Utf16Char { + start: curr_col, + end: curr_col + c_len, + }); + } + + curr_col += c_len; + } + + // Save any utf-16 characters seen in the last line + if !utf16_chars.is_empty() { + utf16_lines.insert(line, utf16_chars); + } + + LineIndex { + newlines, + utf16_lines, + } + } + + pub fn line_col(&self, offset: TextSize) -> LineCol { + let line = partition_point(&self.newlines, |&it| it <= offset) - 1; + let line_start_offset = self.newlines[line]; + let col = offset - line_start_offset; + LineCol { + line: line as u32, + col: col.into(), + } + } + + pub fn offset(&self, line_col: LineCol) -> TextSize { + self.newlines[line_col.line as usize] + TextSize::from(line_col.col) + } + + pub fn to_utf16(&self, line_col: LineCol) -> LineColUtf16 { + let col = self.utf8_to_utf16_col(line_col.line, line_col.col.into()); + LineColUtf16 { + line: line_col.line, + col: col as u32, + } + } + + pub fn to_utf8(&self, line_col: LineColUtf16) -> LineCol { + let col = self.utf16_to_utf8_col(line_col.line, line_col.col); + LineCol { + line: line_col.line, + col: col.into(), + } + } + + pub fn lines(&self, range: TextRange) -> impl Iterator<Item = TextRange> + '_ { + let lo = partition_point(&self.newlines, |&it| it < range.start()); + let hi = partition_point(&self.newlines, |&it| it <= range.end()); + let all = iter::once(range.start()) + .chain(self.newlines[lo..hi].iter().copied()) + .chain(iter::once(range.end())); + + all.clone() + .zip(all.skip(1)) + .map(|(lo, hi)| TextRange::new(lo, hi)) + .filter(|it| !it.is_empty()) + } + + fn utf8_to_utf16_col(&self, line: u32, col: TextSize) -> usize { + let mut res: usize = col.into(); + if let Some(utf16_chars) = self.utf16_lines.get(&line) { + for c in utf16_chars { + if c.end <= col { + res -= usize::from(c.len()) - c.len_utf16(); + } else { + // From here on, all utf16 characters come *after* the character we are mapping, + // so we don't need to take them into account + break; + } + } + } + res + } + + fn utf16_to_utf8_col(&self, line: u32, mut col: u32) -> TextSize { + if let Some(utf16_chars) = self.utf16_lines.get(&line) { + for c in utf16_chars { + if col > u32::from(c.start) { + col += u32::from(c.len()) - c.len_utf16() as u32; + } else { + // From here on, all utf16 characters come *after* the character we are mapping, + // so we don't need to take them into account + break; + } + } + } + + col.into() + } +} + +/// Returns `idx` such that: +/// +/// ```text +/// ∀ x in slice[..idx]: pred(x) +/// && ∀ x in slice[idx..]: !pred(x) +/// ``` +/// +/// https://github.com/rust-lang/rust/issues/73831 +fn partition_point<T, P>(slice: &[T], mut pred: P) -> usize +where + P: FnMut(&T) -> bool, +{ + let mut left = 0; + let mut right = slice.len(); + + while left != right { + let mid = left + (right - left) / 2; + // SAFETY: + // When left < right, left <= mid < right. + // Therefore left always increases and right always decreases, + // and either of them is selected. + // In both cases left <= right is satisfied. + // Therefore if left < right in a step, + // left <= right is satisfied in the next step. + // Therefore as long as left != right, 0 <= left < right <= len is satisfied + // and if this case 0 <= mid < len is satisfied too. + let value = unsafe { slice.get_unchecked(mid) }; + if pred(value) { + left = mid + 1; + } else { + right = mid; + } + } + + left +}
\ No newline at end of file diff --git a/support/texlab/src/line_index_ext.rs b/support/texlab/src/line_index_ext.rs new file mode 100644 index 0000000000..8e28207edd --- /dev/null +++ b/support/texlab/src/line_index_ext.rs @@ -0,0 +1,42 @@ +use cstree::{TextRange, TextSize}; +use lsp_types::{Position, Range}; + +use crate::{LineColUtf16, LineIndex}; + +pub trait LineIndexExt { + fn offset_lsp(&self, line_col: Position) -> TextSize; + + fn offset_lsp_range(&self, line_col: Range) -> TextRange; + + fn line_col_lsp(&self, offset: TextSize) -> Position; + + fn line_col_lsp_range(&self, offset: TextRange) -> Range; +} + +impl LineIndexExt for LineIndex { + fn offset_lsp(&self, line_col: Position) -> TextSize { + let line_col = LineColUtf16 { + line: line_col.line, + col: line_col.character, + }; + self.offset(self.to_utf8(line_col)) + } + + fn offset_lsp_range(&self, line_col: Range) -> TextRange { + let start = self.offset_lsp(line_col.start); + let end = self.offset_lsp(line_col.end); + TextRange::new(start, end) + } + + fn line_col_lsp(&self, offset: TextSize) -> Position { + let position = self.line_col(offset); + let LineColUtf16 { line , col } = self.to_utf16(position); + Position::new(line, col) + } + + fn line_col_lsp_range(&self, offset: TextRange) -> Range { + let start = self.line_col_lsp(offset.start()); + let end = self.line_col_lsp(offset.end()); + Range::new(start, end) + } +} diff --git a/support/texlab/src/link/latex_import.rs b/support/texlab/src/link/latex_import.rs deleted file mode 100644 index 494a72b481..0000000000 --- a/support/texlab/src/link/latex_import.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{DocumentLink, DocumentLinkParams}, - syntax::{latex, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexImportLinkProvider; - -#[async_trait] -impl FeatureProvider for LatexImportLinkProvider { - type Params = DocumentLinkParams; - type Output = Vec<DocumentLink>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - if let DocumentContent::Latex(table) = &req.current().content { - table - .imports - .iter() - .flat_map(|import| Self::resolve(req, table, import)) - .collect() - } else { - Vec::new() - } - } -} - -impl LatexImportLinkProvider { - fn resolve( - req: &FeatureRequest<DocumentLinkParams>, - table: &latex::SymbolTable, - import: &latex::Import, - ) -> Vec<DocumentLink> { - let mut links = Vec::new(); - let file = import.file(&table); - for target in &import.targets { - if let Some(link) = req.snapshot().find(target).map(|doc| DocumentLink { - range: file.range(), - target: doc.uri.clone().into(), - tooltip: None, - }) { - links.push(link); - break; - } - } - links - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - - #[tokio::test] - async fn empty_latex_document_command() { - let actual_links = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .test_link(LatexImportLinkProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_command() { - let actual_links = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .test_link(LatexImportLinkProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn has_links() { - let actual_links = FeatureTester::new() - .file("foo.tex", r#"\import{bar/}{baz}"#) - .file("bar/baz.tex", r#""#) - .main("foo.tex") - .test_link(LatexImportLinkProvider) - .await; - - let expected_links = vec![DocumentLink { - range: Range::new_simple(0, 14, 0, 17), - target: FeatureTester::uri("bar/baz.tex").into(), - tooltip: None, - }]; - - assert_eq!(actual_links, expected_links); - } -} diff --git a/support/texlab/src/link/latex_include.rs b/support/texlab/src/link/latex_include.rs deleted file mode 100644 index 5f4f592f14..0000000000 --- a/support/texlab/src/link/latex_include.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{DocumentLink, DocumentLinkParams}, - syntax::{latex, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexIncludeLinkProvider; - -#[async_trait] -impl FeatureProvider for LatexIncludeLinkProvider { - type Params = DocumentLinkParams; - type Output = Vec<DocumentLink>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - if let DocumentContent::Latex(table) = &req.current().content { - table - .includes - .iter() - .flat_map(|include| Self::resolve(req, table, include)) - .collect() - } else { - Vec::new() - } - } -} - -impl LatexIncludeLinkProvider { - fn resolve( - req: &FeatureRequest<DocumentLinkParams>, - table: &latex::SymbolTable, - include: &latex::Include, - ) -> Vec<DocumentLink> { - let mut links = Vec::new(); - let paths = include.paths(&table); - for (i, targets) in include.all_targets.iter().enumerate() { - for target in targets { - if let Some(link) = req.snapshot().find(target).map(|doc| DocumentLink { - range: paths[i].range(), - target: doc.uri.clone().into(), - tooltip: None, - }) { - links.push(link); - break; - } - } - } - links - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - - #[tokio::test] - async fn empty_latex_document_command() { - let actual_links = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .test_link(LatexIncludeLinkProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document_command() { - let actual_links = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .test_link(LatexIncludeLinkProvider) - .await; - - assert!(actual_links.is_empty()); - } - - #[tokio::test] - async fn has_links() { - let actual_links = FeatureTester::new() - .file("foo.tex", r#"\input{bar.tex}"#) - .file("bar.tex", r#""#) - .main("foo.tex") - .test_link(LatexIncludeLinkProvider) - .await; - - let expected_links = vec![DocumentLink { - range: Range::new_simple(0, 7, 0, 14), - target: FeatureTester::uri("bar.tex").into(), - tooltip: None, - }]; - - assert_eq!(actual_links, expected_links); - } -} diff --git a/support/texlab/src/link/mod.rs b/support/texlab/src/link/mod.rs deleted file mode 100644 index 934975f060..0000000000 --- a/support/texlab/src/link/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -mod latex_import; -mod latex_include; - -use self::{latex_import::LatexImportLinkProvider, latex_include::LatexIncludeLinkProvider}; -use crate::{ - feature::{ConcatProvider, FeatureProvider, FeatureRequest}, - protocol::{DocumentLink, DocumentLinkParams}, -}; -use async_trait::async_trait; - -pub struct LinkProvider { - provider: ConcatProvider<DocumentLinkParams, DocumentLink>, -} - -impl LinkProvider { - pub fn new() -> Self { - Self { - provider: ConcatProvider::new(vec![ - Box::new(LatexImportLinkProvider), - Box::new(LatexIncludeLinkProvider), - ]), - } - } -} - -impl Default for LinkProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for LinkProvider { - type Params = DocumentLinkParams; - type Output = Vec<DocumentLink>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - self.provider.execute(req).await - } -} diff --git a/support/texlab/src/main.rs b/support/texlab/src/main.rs index 16c5be8f26..415de3b804 100644 --- a/support/texlab/src/main.rs +++ b/support/texlab/src/main.rs @@ -1,15 +1,10 @@ -use futures::{channel::mpsc, prelude::*}; -use jsonrpc::MessageHandler; +use std::{env, fs::OpenOptions, io, path::PathBuf}; + +use anyhow::Result; use log::LevelFilter; -use std::path::PathBuf; -use std::{env, error, fs::OpenOptions, sync::Arc}; +use lsp_server::Connection; use structopt::StructOpt; -use texlab::{ - protocol::{LatexLspClient, LspCodec}, - server::LatexLspServer, - tex::Distribution, -}; -use tokio_util::codec::{FramedRead, FramedWrite}; +use texlab::Server; /// An implementation of the Language Server Protocol for LaTeX #[derive(Debug, StructOpt)] @@ -27,37 +22,13 @@ struct Opts { log_file: Option<PathBuf>, } -#[tokio::main] -async fn main() -> Result<(), Box<dyn error::Error>> { +fn main() -> Result<()> { let opts = Opts::from_args(); setup_logger(opts); - let mut stdin = FramedRead::new(tokio::io::stdin(), LspCodec); - let (stdout_tx, mut stdout_rx) = mpsc::channel(0); - - let client = Arc::new(LatexLspClient::new(stdout_tx.clone())); - let server = Arc::new(LatexLspServer::new( - Distribution::detect().await, - Arc::clone(&client), - Arc::new(env::current_dir().expect("failed to get working directory")), - )); - let mut handler = MessageHandler { - server, - client, - output: stdout_tx, - }; - - tokio::spawn(async move { - let mut stdout = FramedWrite::new(tokio::io::stdout(), LspCodec); - loop { - let message = stdout_rx.next().await.unwrap(); - stdout.send(message).await.unwrap(); - } - }); - - while let Some(json) = stdin.next().await { - handler.handle(&json.unwrap()).await; - } + let (connection, threads) = Connection::stdio(); + Server::with_connection(connection, env::current_dir()?, true)?.run()?; + threads.join()?; Ok(()) } @@ -78,8 +49,10 @@ fn setup_logger(opts: Opts) { let logger = fern::Dispatch::new() .format(|out, message, record| out.finish(format_args!("{} - {}", record.level(), message))) .level(verbosity_level) - .filter(|metadata| metadata.target() == "jsonrpc" || metadata.target().contains("texlab")) - .chain(std::io::stderr()); + .filter(|metadata| { + metadata.target().contains("texlab") || metadata.target().contains("lsp_server") + }) + .chain(io::stderr()); let logger = match opts.log_file { Some(log_file) => logger.chain( diff --git a/support/texlab/src/options.rs b/support/texlab/src/options.rs new file mode 100644 index 0000000000..09eea1110f --- /dev/null +++ b/support/texlab/src/options.rs @@ -0,0 +1,86 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Options { + pub root_directory: Option<PathBuf>, + + pub aux_directory: Option<PathBuf>, + + #[serde(default)] + pub bibtex_formatter: BibtexFormatter, + + pub formatter_line_length: Option<i32>, + + pub diagnostics_delay: Option<u64>, + + #[serde(default)] + pub build: BuildOptions, + + #[serde(default)] + pub chktex: ChktexOptions, + + pub forward_search: Option<ForwardSearchOptions>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum BibtexFormatter { + Texlab, + Latexindent, +} + +impl Default for BibtexFormatter { + fn default() -> Self { + Self::Texlab + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildOptions { + pub executable: Option<String>, + + pub args: Option<Vec<String>>, + + #[serde(default)] + pub is_continuous: bool, +} + +impl BuildOptions { + pub fn executable(&self) -> String { + self.executable + .as_ref() + .map(Clone::clone) + .unwrap_or_else(|| "latexmk".to_string()) + } + + pub fn args(&self) -> Vec<String> { + self.args.as_ref().map(Clone::clone).unwrap_or_else(|| { + vec![ + "-pdf".to_string(), + "-interaction=nonstopmode".to_string(), + "-synctex=1".to_string(), + "%f".to_string(), + ] + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChktexOptions { + #[serde(default)] + pub on_open_and_save: bool, + + #[serde(default)] + pub on_edit: bool, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +pub struct ForwardSearchOptions { + pub executable: Option<String>, + pub args: Option<Vec<String>>, +} diff --git a/support/texlab/src/outline.rs b/support/texlab/src/outline.rs deleted file mode 100644 index 07ffaee09f..0000000000 --- a/support/texlab/src/outline.rs +++ /dev/null @@ -1,437 +0,0 @@ -use crate::{ - feature::DocumentView, - protocol::{MarkupContent, MarkupKind, Options, Position, Range, RangeExt, Uri}, - syntax::{latex, SyntaxNode}, - workspace::{Document, DocumentContent}, -}; -use std::{borrow::Cow, collections::HashSet, path::Path}; -use titlecase::titlecase; -use OutlineContextItem::*; - -#[derive(Debug, Clone, Copy)] -pub struct OutlineSection<'a> { - pub document: &'a Document, - pub item: &'a latex::Section, -} - -pub struct Outline<'a> { - sections: Vec<OutlineSection<'a>>, -} - -impl<'a> Outline<'a> { - pub fn find(&'a self, uri: &Uri, pos: Position) -> Option<&'a latex::Section> { - self.sections - .iter() - .filter(|section| section.document.uri == *uri) - .rev() - .find(|section| { - let table = section.document.content.as_latex().unwrap(); - table[section.item.parent].end() <= pos - }) - .map(|sec| sec.item) - } - - pub fn analyze(view: &'a DocumentView, options: &Options, current_dir: &Path) -> Self { - let mut finder = OutlineSectionFinder::default(); - let doc = view - .snapshot - .parent(&view.current.uri, options, current_dir) - .map(|parent| { - view.related - .iter() - .find(|doc| doc.uri == parent.uri) - .unwrap() - }) - .unwrap_or(&view.current); - - finder.analyze(view, doc); - Self { - sections: finder.sections, - } - } -} - -#[derive(Debug, Clone, Copy)] -enum OutlineItem<'a> { - Section(&'a latex::Section), - Include(&'a latex::Include), -} - -#[derive(Debug, Default)] -struct OutlineSectionFinder<'a> { - visited: HashSet<&'a Uri>, - sections: Vec<OutlineSection<'a>>, -} - -impl<'a> OutlineSectionFinder<'a> { - fn analyze(&mut self, view: &'a DocumentView, doc: &'a Document) { - if !self.visited.insert(&doc.uri) { - return; - } - - if let DocumentContent::Latex(table) = &doc.content { - let mut items = Vec::new(); - for section in &table.sections { - items.push(OutlineItem::Section(section)); - } - for include in &table.includes { - items.push(OutlineItem::Include(include)); - } - items.sort_by_key(|item| match item { - OutlineItem::Include(include) => table[include.parent].start(), - OutlineItem::Section(section) => table[section.parent].start(), - }); - - for item in items { - match item { - OutlineItem::Section(item) => { - let section = OutlineSection { - document: doc, - item, - }; - self.sections.push(section); - } - OutlineItem::Include(item) => { - for doc in &view.related { - for targets in &item.all_targets { - if targets.contains(&doc.uri) { - self.analyze(view, doc); - break; - } - } - } - } - } - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum OutlineCaptionKind { - Figure, - Table, - Listing, - Algorithm, -} - -impl OutlineCaptionKind { - pub fn as_str(self) -> &'static str { - match self { - Self::Figure => "Figure", - Self::Table => "Table", - Self::Listing => "Listing", - Self::Algorithm => "Algorithm", - } - } - - pub fn parse(env_name: &str) -> Option<Self> { - match env_name { - "figure" | "subfigure" => Some(Self::Figure), - "table" | "subtable" => Some(Self::Table), - "listing" | "lstlisting" => Some(Self::Listing), - "algorithm" => Some(Self::Algorithm), - _ => None, - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum OutlineContextItem { - Section { - prefix: Cow<'static, str>, - text: String, - }, - Caption { - kind: Option<OutlineCaptionKind>, - text: String, - }, - Theorem { - kind: String, - description: Option<String>, - }, - Equation, - Item, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct OutlineContext { - pub range: Range, - pub number: Option<String>, - pub item: OutlineContextItem, -} - -impl OutlineContext { - pub fn reference(&self) -> String { - match &self.number { - Some(number) => match &self.item { - Section { prefix, text } => format!("{} {} ({})", prefix, number, text), - Caption { kind: None, text } => format!("{} {}", number, text), - Caption { - kind: Some(kind), - text, - } => format!("{} {}: {}", kind.as_str(), number, text), - Theorem { - kind, - description: None, - } => format!("{} {}", kind, number), - Theorem { - kind, - description: Some(description), - } => format!("{} {} ({})", kind, number, description), - Equation => format!("Equation ({})", number), - Item => format!("Item {}", number), - }, - None => match &self.item { - Section { prefix, text } => format!("{} ({})", prefix, text), - Caption { kind: None, text } => text.clone(), - Caption { - kind: Some(kind), - text, - } => format!("{}: {}", kind.as_str(), text), - Theorem { - kind, - description: None, - } => kind.into(), - Theorem { - kind, - description: Some(description), - } => format!("{} ({})", kind, description), - Equation => "Equation".into(), - Item => "Item".into(), - }, - } - } - - pub fn detail(&self) -> Option<String> { - match &self.item { - Section { .. } | Theorem { .. } | Equation | Item => Some(self.reference()), - Caption { - kind: Some(kind), .. - } => { - let result = match &self.number { - Some(number) => format!("{} {}", kind.as_str(), number), - None => kind.as_str().to_owned(), - }; - Some(result) - } - Caption { .. } => None, - } - } - - pub fn documentation(&self) -> MarkupContent { - MarkupContent { - kind: MarkupKind::PlainText, - value: self.reference(), - } - } - - pub fn parse(view: &DocumentView, outline: &Outline, label: latex::Label) -> Option<Self> { - if let DocumentContent::Latex(table) = &view.current.content { - Self::find_caption(view, table, label) - .or_else(|| Self::find_theorem(view, table, label)) - .or_else(|| Self::find_equation(view, table, label)) - .or_else(|| Self::find_item(view, table, label)) - .or_else(|| Self::find_section(view, outline, table, label)) - } else { - None - } - } - - fn find_caption( - view: &DocumentView, - table: &latex::SymbolTable, - label: latex::Label, - ) -> Option<Self> { - let label_range = table[label.parent].range(); - let caption_env = table - .environments - .iter() - .filter(|env| !env.is_root(&table)) - .find(|env| env.range(&table).contains(label_range.start))?; - - let caption = table - .captions - .iter() - .find(|cap| table.is_direct_child(*caption_env, table[cap.parent].start()))?; - - let caption_text = table.print_group_content( - caption.parent, - latex::GroupKind::Group, - caption.arg_index, - )?; - - let caption_kind = caption_env - .left - .name(&table) - .map(latex::Token::text) - .and_then(OutlineCaptionKind::parse); - - Some(Self { - range: caption_env.range(&table), - number: Self::find_number(view, table, label), - item: Caption { - kind: caption_kind, - text: caption_text, - }, - }) - } - - fn find_theorem( - view: &DocumentView, - main_table: &latex::SymbolTable, - label: latex::Label, - ) -> Option<Self> { - let label_range = main_table[label.parent].range(); - let env = main_table - .environments - .iter() - .find(|env| env.range(&main_table).contains(label_range.start))?; - - let env_name = env.left.name(&main_table).map(latex::Token::text)?; - - for doc in &view.related { - if let DocumentContent::Latex(table) = &doc.content { - for def in &table.theorem_definitions { - if env_name == def.name(&table).text() { - let kind = table - .print_group_content( - def.parent, - latex::GroupKind::Group, - def.arg_index + 1, - ) - .unwrap_or_else(|| titlecase(&env_name)); - - let description = main_table.print_group_content( - env.left.parent, - latex::GroupKind::Options, - 0, - ); - - return Some(Self { - range: env.range(&main_table), - number: Self::find_number(view, main_table, label), - item: Theorem { kind, description }, - }); - } - } - } - } - None - } - - fn find_equation( - view: &DocumentView, - table: &latex::SymbolTable, - label: latex::Label, - ) -> Option<Self> { - let label_range = table[label.parent].range(); - table - .environments - .iter() - .filter(|env| env.left.is_math(&table)) - .map(|env| env.range(&table)) - .find(|range| range.contains(label_range.start)) - .map(|range| Self { - range, - number: Self::find_number(view, table, label), - item: Equation, - }) - } - - fn find_item( - view: &DocumentView, - table: &latex::SymbolTable, - label: latex::Label, - ) -> Option<Self> { - let label_range = table[label.parent].range(); - struct LatexItemNode { - item: latex::Item, - range: Range, - } - - let enumeration = table - .environments - .iter() - .filter(|env| env.left.is_enum(&table)) - .find(|env| env.range(&table).contains(label_range.start))?; - - let mut item_nodes: Vec<_> = table - .items - .iter() - .filter(|item| table.is_enum_item(*enumeration, **item)) - .map(|item| LatexItemNode { - item: *item, - range: Range::default(), - }) - .collect(); - - for i in 0..item_nodes.len() { - let start = table[item_nodes[i].item.parent].start(); - let end = item_nodes - .get(i + 1) - .map(|node| table[node.item.parent].start()) - .unwrap_or_else(|| table[enumeration.right.parent].start()); - item_nodes[i].range = Range::new(start, end); - } - - let node = item_nodes - .iter() - .find(|node| node.range.contains(label_range.start))?; - - let number = node - .item - .name(&table) - .or_else(|| Self::find_number(view, table, label)); - - Some(Self { - range: enumeration.range(&table), - number, - item: Item, - }) - } - - fn find_section( - view: &DocumentView, - outline: &Outline, - table: &latex::SymbolTable, - label: latex::Label, - ) -> Option<Self> { - let label_range = table[label.parent].range(); - let section = outline.find(&view.current.uri, label_range.start)?; - Some(Self { - range: table[section.parent].range(), - number: Self::find_number(view, table, label), - item: Section { - prefix: section.prefix.clone(), - text: table.print_group_content( - section.parent, - latex::GroupKind::Group, - section.arg_index, - )?, - }, - }) - } - - pub fn find_number( - view: &DocumentView, - table: &latex::SymbolTable, - label: latex::Label, - ) -> Option<String> { - let label_names = label.names(&table); - if label_names.len() != 1 { - return None; - } - - for doc in &view.related { - if let DocumentContent::Latex(table) = &doc.content { - for numbering in &table.label_numberings { - if numbering.name(&table).text() == label_names[0].text() { - return Some(numbering.number.clone()); - } - } - } - } - None - } -} diff --git a/support/texlab/src/protocol/client.rs b/support/texlab/src/protocol/client.rs deleted file mode 100644 index 471f1fa41d..0000000000 --- a/support/texlab/src/protocol/client.rs +++ /dev/null @@ -1,27 +0,0 @@ -use jsonrpc::client::Result; -use jsonrpc_derive::{jsonrpc_client, jsonrpc_method}; -use lsp_types::*; - -#[jsonrpc_client(LatexLspClient)] -pub trait LspClient { - #[jsonrpc_method("workspace/configuration", kind = "request")] - async fn configuration(&self, params: ConfigurationParams) -> Result<serde_json::Value>; - - #[jsonrpc_method("window/showMessage", kind = "notification")] - async fn show_message(&self, params: ShowMessageParams); - - #[jsonrpc_method("client/registerCapability", kind = "request")] - async fn register_capability(&self, params: RegistrationParams) -> Result<()>; - - #[jsonrpc_method("textDocument/publishDiagnostics", kind = "notification")] - async fn publish_diagnostics(&self, params: PublishDiagnosticsParams); - - #[jsonrpc_method("$/progress", kind = "notification")] - async fn progress(&self, params: ProgressParams); - - #[jsonrpc_method("window/workDoneProgress/create", kind = "request")] - async fn work_done_progress_create(&self, params: WorkDoneProgressCreateParams) -> Result<()>; - - #[jsonrpc_method("window/logMessage", kind = "notification")] - async fn log_message(&self, params: LogMessageParams); -} diff --git a/support/texlab/src/protocol/codec.rs b/support/texlab/src/protocol/codec.rs deleted file mode 100644 index 980ae1307e..0000000000 --- a/support/texlab/src/protocol/codec.rs +++ /dev/null @@ -1,144 +0,0 @@ -use bytes::{BufMut, BytesMut}; -use log::trace; -use std::io::{Error, ErrorKind}; -use tokio_util::codec::{Decoder, Encoder}; - -pub struct LspCodec; - -impl Decoder for LspCodec { - type Item = String; - type Error = Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> { - match parser::parse(src) { - Ok((remaining, content)) => { - trace!("Received message:\n{}\n", content); - - let offset = src.len() - remaining.len(); - let _ = src.split_to(offset); - Ok(Some(content)) - } - Err(error) => { - if error.is_incomplete() { - Ok(None) - } else { - Err(ErrorKind::InvalidData.into()) - } - } - } - } -} - -impl Encoder<String> for LspCodec { - type Error = Error; - - fn encode(&mut self, item: String, dst: &mut BytesMut) -> Result<(), Self::Error> { - let message = format!("Content-Length: {}\r\n\r\n{}", item.len(), item); - trace!("Sent message:\n{}\n", message); - - dst.reserve(message.len()); - dst.put(message.as_bytes()); - Ok(()) - } -} - -mod parser { - use nom::{ - bytes::streaming::{tag, take, take_while}, - character::{is_digit, streaming::line_ending}, - combinator::{map_res, opt}, - IResult, - }; - use std::str; - - pub fn parse(input: &[u8]) -> IResult<&[u8], String> { - let (input, _) = opt(content_type)(input)?; - let (input, length) = content_length(input)?; - let (input, _) = opt(content_type)(input)?; - let (input, _) = line_ending(input)?; - let (input, content) = map_res(take(length), str::from_utf8)(input)?; - Ok((input, content.to_owned())) - } - - fn content_type(input: &[u8]) -> IResult<&[u8], &[u8]> { - let (input, _) = tag("Content-Type: application/vscode-jsonrpc;charset=utf")(input)?; - let (input, _) = opt(tag("-"))(input)?; - let (input, _) = tag("8")(input)?; - line_ending(input) - } - - fn content_length(input: &[u8]) -> IResult<&[u8], usize> { - let (input, _) = tag("Content-Length: ")(input)?; - let (input, length) = map_res(take_while(is_digit), from_bytes)(input)?; - let (input, _) = line_ending(input)?; - Ok((input, length)) - } - - fn from_bytes(input: &[u8]) -> Result<usize, std::num::ParseIntError> { - usize::from_str_radix(str::from_utf8(input).unwrap(), 10) - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn parse_content_type() { - let result = - content_type(b"Content-Type: application/vscode-jsonrpc;charset=utf-8\r\n"); - assert!(result.is_ok()); - } - - #[test] - fn parse_content_type_utf8() { - let result = content_type(b"Content-Type: application/vscode-jsonrpc;charset=utf8\r\n"); - assert!(result.is_ok()); - } - - #[test] - fn parse_content_length() { - let result = content_length(b"Content-Length: 42\r\n"); - assert_eq!(result.unwrap().1, 42usize); - } - - #[test] - fn parse_message_full() { - let result = parse( - b"Content-Length: 2\r\nContent-Type: application/vscode-jsonrpc;charset=utf8\r\n\r\n{}", - ); - assert_eq!(result.unwrap().1, "{}"); - } - - #[test] - fn parse_message_type_first() { - let result = parse( - b"Content-Type: application/vscode-jsonrpc;charset=utf8\r\nContent-Length: 2\r\n\r\n{}", - ); - assert_eq!(result.unwrap().1, "{}"); - } - - #[test] - fn parse_message_without_type() { - let result = parse(b"Content-Length: 2\r\n\r\n{}"); - assert_eq!(result.unwrap().1, "{}"); - } - - #[test] - fn parse_message_incomplete() { - let result = parse(b"Content-Length:"); - assert!(result.unwrap_err().is_incomplete()); - } - - #[test] - fn parse_message_invalid() { - let error = parse(b"foo").unwrap_err(); - assert!(!error.is_incomplete()); - } - - #[test] - fn parse_message_overflow() { - let result = parse(b"Content-Length: 4\r\n\r\n{}"); - assert!(result.unwrap_err().is_incomplete()); - } - } -} diff --git a/support/texlab/src/protocol/edit.rs b/support/texlab/src/protocol/edit.rs deleted file mode 100644 index 9091f2d393..0000000000 --- a/support/texlab/src/protocol/edit.rs +++ /dev/null @@ -1,12 +0,0 @@ -use lsp_types::{CompletionTextEdit, TextEdit}; - -pub trait CompletionTextEditExt { - fn text_edit(&self) -> Option<&TextEdit>; -} - -impl CompletionTextEditExt for CompletionTextEdit { - fn text_edit(&self) -> Option<&TextEdit> { - let CompletionTextEdit::Edit(edit) = self; - Some(edit) - } -} diff --git a/support/texlab/src/protocol/mod.rs b/support/texlab/src/protocol/mod.rs deleted file mode 100644 index c08a7d82f5..0000000000 --- a/support/texlab/src/protocol/mod.rs +++ /dev/null @@ -1,71 +0,0 @@ -cfg_if::cfg_if! { - if #[cfg(feature = "server")] { - mod client; - mod codec; - - pub use self::{ - client::{LatexLspClient, LspClient}, - codec::LspCodec, - }; - } -} - -mod capabilities; -mod edit; -mod options; -mod range; -mod uri; - -pub use self::{ - capabilities::ClientCapabilitiesExt, - edit::*, - options::*, - range::RangeExt, - uri::{AsUri, Uri}, -}; -pub use lsp_types::*; - -use serde::{Deserialize, Serialize}; -use serde_repr::*; - -#[serde(untagged)] -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub enum DefinitionResponse { - Locations(Vec<Location>), - LocationLinks(Vec<LocationLink>), -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] -#[repr(i32)] -pub enum ForwardSearchStatus { - Success = 0, - Error = 1, - Failure = 2, - Unconfigured = 3, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct ForwardSearchResult { - pub status: ForwardSearchStatus, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildParams { - pub text_document: TextDocumentIdentifier, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] -#[repr(i32)] -pub enum BuildStatus { - Success = 0, - Error = 1, - Failure = 2, - Cancelled = 3, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildResult { - pub status: BuildStatus, -} diff --git a/support/texlab/src/protocol/options.rs b/support/texlab/src/protocol/options.rs deleted file mode 100644 index 86718d1e72..0000000000 --- a/support/texlab/src/protocol/options.rs +++ /dev/null @@ -1,104 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum BibtexFormatter { - Texlab, - Latexindent, -} - -impl Default for BibtexFormatter { - fn default() -> Self { - Self::Texlab - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BibtexFormattingOptions { - pub line_length: Option<i32>, - pub formatter: Option<BibtexFormatter>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -pub struct LatexForwardSearchOptions { - pub executable: Option<String>, - pub args: Option<Vec<String>>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct LatexLintOptions { - pub on_change: Option<bool>, - pub on_save: Option<bool>, -} - -impl LatexLintOptions { - pub fn on_change(&self) -> bool { - self.on_change.unwrap_or(false) - } - - pub fn on_save(&self) -> bool { - self.on_save.unwrap_or(false) - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexBuildOptions { - pub executable: Option<String>, - pub args: Option<Vec<String>>, - pub on_save: Option<bool>, - pub output_directory: Option<PathBuf>, - pub forward_search_after: Option<bool>, -} - -impl LatexBuildOptions { - pub fn executable(&self) -> String { - self.executable - .as_ref() - .map(Clone::clone) - .unwrap_or_else(|| "latexmk".to_owned()) - } - - pub fn args(&self) -> Vec<String> { - self.args.as_ref().map(Clone::clone).unwrap_or_else(|| { - vec![ - "-pdf".to_owned(), - "-interaction=nonstopmode".to_owned(), - "-synctex=1".to_owned(), - ] - }) - } - - pub fn on_save(&self) -> bool { - self.on_save.unwrap_or(false) - } - - pub fn forward_search_after(&self) -> bool { - self.forward_search_after.unwrap_or(false) - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexOptions { - pub forward_search: Option<LatexForwardSearchOptions>, - pub lint: Option<LatexLintOptions>, - pub build: Option<LatexBuildOptions>, - pub root_directory: Option<PathBuf>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BibtexOptions { - pub formatting: Option<BibtexFormattingOptions>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Options { - pub latex: Option<LatexOptions>, - pub bibtex: Option<BibtexOptions>, -} diff --git a/support/texlab/src/protocol/range.rs b/support/texlab/src/protocol/range.rs deleted file mode 100644 index 774df1e699..0000000000 --- a/support/texlab/src/protocol/range.rs +++ /dev/null @@ -1,97 +0,0 @@ -use lsp_types::{Position, Range}; - -pub trait RangeExt { - fn new_simple(start_line: u64, start_character: u64, end_line: u64, end_character: u64) - -> Self; - - fn contains(&self, pos: Position) -> bool; - - fn contains_exclusive(&self, pos: Position) -> bool; -} - -impl RangeExt for Range { - fn new_simple( - start_line: u64, - start_character: u64, - end_line: u64, - end_character: u64, - ) -> Self { - Self { - start: Position::new(start_line, start_character), - end: Position::new(end_line, end_character), - } - } - - fn contains(&self, pos: Position) -> bool { - pos >= self.start && pos <= self.end - } - - fn contains_exclusive(&self, pos: Position) -> bool { - pos > self.start && pos < self.end - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn contains_inside() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(range.contains(Position::new(2, 5))); - } - - #[test] - fn contains_begin() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(range.contains(range.start)); - } - - #[test] - fn contains_end() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(range.contains(range.end)); - } - - #[test] - fn contains_outside_left() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(!range.contains(Position::new(0, 5))); - } - - #[test] - fn contains_outside_right() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(!range.contains(Position::new(5, 1))); - } - - #[test] - fn contains_exclusive_inside() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(range.contains_exclusive(Position::new(2, 5))); - } - - #[test] - fn contains_exclusive_begin() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(!range.contains_exclusive(range.start)); - } - - #[test] - fn contains_exclusive_end() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(!range.contains_exclusive(range.end)); - } - - #[test] - fn contains_exclusive_outside_left() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(!range.contains_exclusive(Position::new(0, 5))); - } - - #[test] - fn contains_exclusive_outside_right() { - let range = Range::new_simple(1, 2, 3, 4); - assert!(!range.contains_exclusive(Position::new(5, 1))); - } -} diff --git a/support/texlab/src/range.rs b/support/texlab/src/range.rs new file mode 100644 index 0000000000..c70a573f39 --- /dev/null +++ b/support/texlab/src/range.rs @@ -0,0 +1,20 @@ +use lsp_types::{Position, Range}; + +pub trait RangeExt { + fn new_simple(start_line: u32, start_character: u32, end_line: u32, end_character: u32) + -> Self; +} + +impl RangeExt for Range { + fn new_simple( + start_line: u32, + start_character: u32, + end_line: u32, + end_character: u32, + ) -> Self { + Self { + start: Position::new(start_line, start_character), + end: Position::new(end_line, end_character), + } + } +} diff --git a/support/texlab/src/reference/bibtex_entry.rs b/support/texlab/src/reference/bibtex_entry.rs deleted file mode 100644 index 2acf7bdfef..0000000000 --- a/support/texlab/src/reference/bibtex_entry.rs +++ /dev/null @@ -1,218 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{Location, RangeExt, ReferenceParams, Url}, - syntax::{bibtex, latex, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexEntryReferenceProvider; - -#[async_trait] -impl FeatureProvider for BibtexEntryReferenceProvider { - type Params = ReferenceParams; - type Output = Vec<Location>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut refs = Vec::new(); - if let Some(key) = Self::find_key(req) { - for doc in req.related() { - match &doc.content { - DocumentContent::Latex(table) => table - .citations - .iter() - .flat_map(|citation| citation.keys(&table)) - .filter(|citation| citation.text() == key) - .map(|citation| Location::new(doc.uri.clone().into(), citation.range())) - .for_each(|location| refs.push(location)), - DocumentContent::Bibtex(tree) => { - if req.params.context.include_declaration { - let uri: Url = doc.uri.clone().into(); - tree.children(tree.root) - .filter_map(|node| tree.as_entry(node)) - .filter_map(|entry| entry.key.as_ref()) - .filter(|key_tok| key_tok.text() == key) - .map(|key_tok| Location::new(uri.clone(), key_tok.range())) - .for_each(|location| refs.push(location)); - } - } - } - } - } - refs - } -} - -impl BibtexEntryReferenceProvider { - fn find_key(req: &FeatureRequest<ReferenceParams>) -> Option<&str> { - let pos = req.params.text_document_position.position; - match &req.current().content { - DocumentContent::Latex(table) => table - .citations - .iter() - .flat_map(|citation| citation.keys(&table)) - .find(|key| key.range().contains(pos)) - .map(latex::Token::text), - DocumentContent::Bibtex(tree) => tree - .children(tree.root) - .filter_map(|node| tree.as_entry(node)) - .filter_map(|entry| entry.key.as_ref()) - .find(|key| key.range().contains(pos)) - .map(bibtex::Token::text), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - use indoc::indoc; - - #[tokio::test] - async fn entry() { - let actual_refs = FeatureTester::new() - .file("foo.bib", r#"@article{foo, bar = {baz}}"#) - .file( - "bar.tex", - indoc!( - r#" - \addbibresource{foo.bib} - \cite{foo} - "# - ), - ) - .file("baz.tex", r#"\cite{foo}"#) - .main("foo.bib") - .position(0, 9) - .test_reference(BibtexEntryReferenceProvider) - .await; - - let expected_refs = vec![Location::new( - FeatureTester::uri("bar.tex").into(), - Range::new_simple(1, 6, 1, 9), - )]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn entry_include_declaration() { - let actual_refs = FeatureTester::new() - .file("foo.bib", r#"@article{foo, bar = {baz}}"#) - .file( - "bar.tex", - indoc!( - r#" - \addbibresource{foo.bib} - \cite{foo} - "# - ), - ) - .file("baz.tex", r#"\cite{foo}"#) - .main("foo.bib") - .position(0, 9) - .include_declaration() - .test_reference(BibtexEntryReferenceProvider) - .await; - - let expected_refs = vec![ - Location::new( - FeatureTester::uri("foo.bib").into(), - Range::new_simple(0, 9, 0, 12), - ), - Location::new( - FeatureTester::uri("bar.tex").into(), - Range::new_simple(1, 6, 1, 9), - ), - ]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn citation() { - let actual_refs = FeatureTester::new() - .file("foo.bib", r#"@article{foo, bar = {baz}}"#) - .file( - "bar.tex", - indoc!( - r#" - \addbibresource{foo.bib} - \cite{foo} - "# - ), - ) - .file("baz.tex", r#"\cite{foo}"#) - .main("bar.tex") - .position(1, 8) - .test_reference(BibtexEntryReferenceProvider) - .await; - - let expected_refs = vec![Location::new( - FeatureTester::uri("bar.tex").into(), - Range::new_simple(1, 6, 1, 9), - )]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn citation_include_declaration() { - let actual_refs = FeatureTester::new() - .file("foo.bib", r#"@article{foo, bar = {baz}}"#) - .file( - "bar.tex", - indoc!( - r#" - \addbibresource{foo.bib} - \cite{foo} - "# - ), - ) - .file("baz.tex", r#"\cite{foo}"#) - .main("bar.tex") - .position(1, 8) - .include_declaration() - .test_reference(BibtexEntryReferenceProvider) - .await; - - let expected_refs = vec![ - Location::new( - FeatureTester::uri("bar.tex").into(), - Range::new_simple(1, 6, 1, 9), - ), - Location::new( - FeatureTester::uri("foo.bib").into(), - Range::new_simple(0, 9, 0, 12), - ), - ]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn empty_latex_document() { - let actual_refs = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_reference(BibtexEntryReferenceProvider) - .await; - - assert!(actual_refs.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_refs = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_reference(BibtexEntryReferenceProvider) - .await; - - assert!(actual_refs.is_empty()); - } -} diff --git a/support/texlab/src/reference/bibtex_string.rs b/support/texlab/src/reference/bibtex_string.rs deleted file mode 100644 index 5f9804fbd6..0000000000 --- a/support/texlab/src/reference/bibtex_string.rs +++ /dev/null @@ -1,253 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{Location, Position, RangeExt, ReferenceParams, Url}, - syntax::{ - bibtex::{self, Visitor}, - SyntaxNode, - }, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use petgraph::graph::NodeIndex; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexStringReferenceProvider; - -#[async_trait] -impl FeatureProvider for BibtexStringReferenceProvider { - type Params = ReferenceParams; - type Output = Vec<Location>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut refs = Vec::new(); - if let DocumentContent::Bibtex(tree) = &req.current().content { - if let Some(name) = Self::find_name(tree, req.params.text_document_position.position) { - let uri: Url = req.current().uri.clone().into(); - if req.params.context.include_declaration { - tree.children(tree.root) - .filter_map(|node| tree.as_string(node)) - .filter_map(|string| string.name.as_ref()) - .filter(|string| string.text() == name.text()) - .for_each(|string| refs.push(Location::new(uri.clone(), string.range()))); - } - - let mut visitor = BibtexStringReferenceVisitor::default(); - visitor.visit(tree, tree.root); - visitor - .refs - .into_iter() - .filter(|reference| reference.text() == name.text()) - .for_each(|reference| refs.push(Location::new(uri.clone(), reference.range()))); - } - } - refs - } -} - -impl BibtexStringReferenceProvider { - fn find_name(tree: &bibtex::Tree, pos: Position) -> Option<&bibtex::Token> { - let mut nodes = tree.find(pos); - nodes.reverse(); - let node0 = &tree.graph[nodes[0]]; - let node1 = nodes.get(1).map(|node| &tree.graph[*node]); - match (node0, node1) { - (bibtex::Node::Word(word), Some(bibtex::Node::Field(_))) - | (bibtex::Node::Word(word), Some(bibtex::Node::Concat(_))) => Some(&word.token), - (bibtex::Node::String(string), _) => string - .name - .as_ref() - .filter(|name| name.range().contains(pos)), - _ => None, - } - } -} - -#[derive(Debug, Default)] -pub struct BibtexStringReferenceVisitor<'a> { - refs: Vec<&'a bibtex::Token>, -} - -impl<'a> bibtex::Visitor<'a> for BibtexStringReferenceVisitor<'a> { - fn visit(&mut self, tree: &'a bibtex::Tree, node: NodeIndex) { - match &tree.graph[node] { - bibtex::Node::Root(_) - | bibtex::Node::Comment(_) - | bibtex::Node::Preamble(_) - | bibtex::Node::String(_) - | bibtex::Node::Entry(_) - | bibtex::Node::Word(_) - | bibtex::Node::Command(_) - | bibtex::Node::QuotedContent(_) - | bibtex::Node::BracedContent(_) => (), - bibtex::Node::Field(_) => { - if let Some(word) = tree - .children(node) - .next() - .and_then(|content| tree.as_word(content)) - { - self.refs.push(&word.token); - } - } - bibtex::Node::Concat(_) => { - let mut children = tree.children(node); - if let Some(word) = children.next().and_then(|left| tree.as_word(left)) { - self.refs.push(&word.token); - } - - if let Some(word) = children.next().and_then(|right| tree.as_word(right)) { - self.refs.push(&word.token); - } - } - } - tree.walk(self, node); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - use indoc::indoc; - - #[tokio::test] - async fn definition() { - let actual_refs = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = {Foo}} - @string{bar = {Bar}} - @article{baz, author = foo} - "# - ), - ) - .main("main.bib") - .position(2, 24) - .test_reference(BibtexStringReferenceProvider) - .await; - - let expected_refs = vec![Location::new( - FeatureTester::uri("main.bib").into(), - Range::new_simple(2, 23, 2, 26), - )]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn definition_include_declaration() { - let actual_refs = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = {Foo}} - @string{bar = {Bar}} - @article{baz, author = foo} - "# - ), - ) - .main("main.bib") - .position(2, 24) - .include_declaration() - .test_reference(BibtexStringReferenceProvider) - .await; - - let expected_refs = vec![ - Location::new( - FeatureTester::uri("main.bib").into(), - Range::new_simple(0, 8, 0, 11), - ), - Location::new( - FeatureTester::uri("main.bib").into(), - Range::new_simple(2, 23, 2, 26), - ), - ]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn reference() { - let actual_refs = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = {Foo}} - @string{bar = {Bar}} - @article{baz, author = foo} - "# - ), - ) - .main("main.bib") - .position(0, 10) - .test_reference(BibtexStringReferenceProvider) - .await; - - let expected_refs = vec![Location::new( - FeatureTester::uri("main.bib").into(), - Range::new_simple(2, 23, 2, 26), - )]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn reference_include_declaration() { - let actual_refs = FeatureTester::new() - .file( - "main.bib", - indoc!( - r#" - @string{foo = {Foo}} - @string{bar = {Bar}} - @article{baz, author = foo} - "# - ), - ) - .main("main.bib") - .position(0, 10) - .include_declaration() - .test_reference(BibtexStringReferenceProvider) - .await; - - let expected_refs = vec![ - Location::new( - FeatureTester::uri("main.bib").into(), - Range::new_simple(0, 8, 0, 11), - ), - Location::new( - FeatureTester::uri("main.bib").into(), - Range::new_simple(2, 23, 2, 26), - ), - ]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn empty_latex_document() { - let actual_refs = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_reference(BibtexStringReferenceProvider) - .await; - - assert!(actual_refs.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_refs = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_reference(BibtexStringReferenceProvider) - .await; - - assert!(actual_refs.is_empty()); - } -} diff --git a/support/texlab/src/reference/latex_label.rs b/support/texlab/src/reference/latex_label.rs deleted file mode 100644 index 16686f6015..0000000000 --- a/support/texlab/src/reference/latex_label.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{Location, RangeExt, ReferenceParams}, - syntax::{latex, LatexLabelKind, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexLabelReferenceProvider; - -#[async_trait] -impl FeatureProvider for LatexLabelReferenceProvider { - type Params = ReferenceParams; - type Output = Vec<Location>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut refs = Vec::new(); - if let Some(def) = Self::find_name(req) { - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - table - .labels - .iter() - .filter(|label| Self::is_included(req, label)) - .flat_map(|label| label.names(&table)) - .filter(|label| label.text() == def) - .map(|label| Location::new(doc.uri.clone().into(), label.range())) - .for_each(|location| refs.push(location)); - } - } - } - refs - } -} - -impl LatexLabelReferenceProvider { - fn find_name(req: &FeatureRequest<ReferenceParams>) -> Option<&str> { - let pos = req.params.text_document_position.position; - if let DocumentContent::Latex(table) = &req.current().content { - table - .labels - .iter() - .flat_map(|label| label.names(&table)) - .find(|label| label.range().contains(pos)) - .map(latex::Token::text) - } else { - None - } - } - - fn is_included(req: &FeatureRequest<ReferenceParams>, label: &latex::Label) -> bool { - match label.kind { - LatexLabelKind::Reference(_) => true, - LatexLabelKind::Definition => req.params.context.include_declaration, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::Range}; - use indoc::indoc; - - #[tokio::test] - async fn definition() { - let actual_refs = FeatureTester::new() - .file("foo.tex", r#"\label{foo}"#) - .file( - "bar.tex", - indoc!( - r#" - \input{foo.tex} - \ref{foo} - "# - ), - ) - .file("baz.tex", r#"\ref{foo}"#) - .main("foo.tex") - .position(0, 8) - .test_reference(LatexLabelReferenceProvider) - .await; - - let expected_refs = vec![Location::new( - FeatureTester::uri("bar.tex").into(), - Range::new_simple(1, 5, 1, 8), - )]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn definition_include_declaration() { - let actual_refs = FeatureTester::new() - .file("foo.tex", r#"\label{foo}"#) - .file( - "bar.tex", - indoc!( - r#" - \input{foo.tex} - \ref{foo} - "# - ), - ) - .file("baz.tex", r#"\ref{foo}"#) - .main("foo.tex") - .position(0, 8) - .include_declaration() - .test_reference(LatexLabelReferenceProvider) - .await; - - let expected_refs = vec![ - Location::new( - FeatureTester::uri("foo.tex").into(), - Range::new_simple(0, 7, 0, 10), - ), - Location::new( - FeatureTester::uri("bar.tex").into(), - Range::new_simple(1, 5, 1, 8), - ), - ]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn reference() { - let actual_refs = FeatureTester::new() - .file("foo.tex", r#"\label{foo}"#) - .file( - "bar.tex", - indoc!( - r#" - \input{foo.tex} - \ref{foo} - "# - ), - ) - .file("baz.tex", r#"\ref{foo}"#) - .main("bar.tex") - .position(1, 7) - .test_reference(LatexLabelReferenceProvider) - .await; - - let expected_refs = vec![Location::new( - FeatureTester::uri("bar.tex").into(), - Range::new_simple(1, 5, 1, 8), - )]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn reference_include_declaration() { - let actual_refs = FeatureTester::new() - .file("foo.tex", r#"\label{foo}"#) - .file( - "bar.tex", - indoc!( - r#" - \input{foo.tex} - \ref{foo} - "# - ), - ) - .file("baz.tex", r#"\ref{foo}"#) - .main("bar.tex") - .position(1, 7) - .include_declaration() - .test_reference(LatexLabelReferenceProvider) - .await; - - let expected_refs = vec![ - Location::new( - FeatureTester::uri("bar.tex").into(), - Range::new_simple(1, 5, 1, 8), - ), - Location::new( - FeatureTester::uri("foo.tex").into(), - Range::new_simple(0, 7, 0, 10), - ), - ]; - - assert_eq!(actual_refs, expected_refs); - } - - #[tokio::test] - async fn empty_latex_document() { - let actual_refs = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .test_reference(LatexLabelReferenceProvider) - .await; - - assert!(actual_refs.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_refs = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .test_reference(LatexLabelReferenceProvider) - .await; - - assert!(actual_refs.is_empty()); - } -} diff --git a/support/texlab/src/reference/mod.rs b/support/texlab/src/reference/mod.rs deleted file mode 100644 index fc4379d394..0000000000 --- a/support/texlab/src/reference/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -mod bibtex_entry; -mod bibtex_string; -mod latex_label; - -use self::{ - bibtex_entry::BibtexEntryReferenceProvider, bibtex_string::BibtexStringReferenceProvider, - latex_label::LatexLabelReferenceProvider, -}; -use crate::{ - feature::{ConcatProvider, FeatureProvider, FeatureRequest}, - protocol::{Location, ReferenceParams}, -}; -use async_trait::async_trait; - -pub struct ReferenceProvider { - provider: ConcatProvider<ReferenceParams, Location>, -} - -impl ReferenceProvider { - pub fn new() -> Self { - Self { - provider: ConcatProvider::new(vec![ - Box::new(BibtexEntryReferenceProvider), - Box::new(BibtexStringReferenceProvider), - Box::new(LatexLabelReferenceProvider), - ]), - } - } -} - -impl Default for ReferenceProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for ReferenceProvider { - type Params = ReferenceParams; - type Output = Vec<Location>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - self.provider.execute(req).await - } -} diff --git a/support/texlab/src/rename/bibtex_entry.rs b/support/texlab/src/rename/bibtex_entry.rs deleted file mode 100644 index 97eaa8fd9d..0000000000 --- a/support/texlab/src/rename/bibtex_entry.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - Position, Range, RangeExt, RenameParams, TextDocumentPositionParams, TextEdit, - WorkspaceEdit, - }, - syntax::{Span, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use std::collections::HashMap; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexEntryPrepareRenameProvider; - -#[async_trait] -impl FeatureProvider for BibtexEntryPrepareRenameProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Range>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - find_key(&req.current().content, req.params.position).map(Span::range) - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexEntryRenameProvider; - -#[async_trait] -impl FeatureProvider for BibtexEntryRenameProvider { - type Params = RenameParams; - type Output = Option<WorkspaceEdit>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let key_name = find_key( - &req.current().content, - req.params.text_document_position.position, - )?; - let mut changes = HashMap::new(); - for doc in req.related() { - let edits = match &doc.content { - DocumentContent::Latex(table) => table - .citations - .iter() - .flat_map(|citation| citation.keys(&table)) - .filter(|citation| citation.text() == key_name.text) - .map(|citation| TextEdit::new(citation.range(), req.params.new_name.clone())) - .collect(), - DocumentContent::Bibtex(tree) => tree - .children(tree.root) - .filter_map(|node| tree.as_entry(node)) - .filter_map(|entry| entry.key.as_ref()) - .filter(|entry_key| entry_key.text() == key_name.text) - .map(|entry_key| TextEdit::new(entry_key.range(), req.params.new_name.clone())) - .collect(), - }; - changes.insert(doc.uri.clone().into(), edits); - } - Some(WorkspaceEdit::new(changes)) - } -} - -fn find_key(content: &DocumentContent, pos: Position) -> Option<&Span> { - match content { - DocumentContent::Latex(table) => table - .citations - .iter() - .flat_map(|citation| citation.keys(&table)) - .find(|key| key.range().contains(pos)) - .map(|key| &key.span), - DocumentContent::Bibtex(tree) => tree - .children(tree.root) - .filter_map(|node| tree.as_entry(node)) - .filter_map(|entry| entry.key.as_ref()) - .find(|key| key.range().contains(pos)) - .map(|key| &key.span), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::RangeExt}; - use indoc::indoc; - - #[tokio::test] - async fn entry() { - let actual_edit = FeatureTester::new() - .file("main.bib", r#"@article{foo, bar = baz}"#) - .file( - "main.tex", - indoc!( - r#" - \addbibresource{main.bib} - \cite{foo} - "# - ), - ) - .main("main.bib") - .position(0, 9) - .new_name("qux") - .test_rename(BibtexEntryRenameProvider) - .await - .unwrap(); - - let mut expected_changes = HashMap::new(); - expected_changes.insert( - FeatureTester::uri("main.bib").into(), - vec![TextEdit::new(Range::new_simple(0, 9, 0, 12), "qux".into())], - ); - expected_changes.insert( - FeatureTester::uri("main.tex").into(), - vec![TextEdit::new(Range::new_simple(1, 6, 1, 9), "qux".into())], - ); - let expected_edit = WorkspaceEdit::new(expected_changes); - - assert_eq!(actual_edit, expected_edit); - } - - #[tokio::test] - async fn citation() { - let actual_edit = FeatureTester::new() - .file("main.bib", r#"@article{foo, bar = baz}"#) - .file( - "main.tex", - indoc!( - r#" - \addbibresource{main.bib} - \cite{foo} - "# - ), - ) - .main("main.tex") - .position(1, 6) - .new_name("qux") - .test_rename(BibtexEntryRenameProvider) - .await - .unwrap(); - - let mut expected_changes = HashMap::new(); - expected_changes.insert( - FeatureTester::uri("main.bib").into(), - vec![TextEdit::new(Range::new_simple(0, 9, 0, 12), "qux".into())], - ); - expected_changes.insert( - FeatureTester::uri("main.tex").into(), - vec![TextEdit::new(Range::new_simple(1, 6, 1, 9), "qux".into())], - ); - let expected_edit = WorkspaceEdit::new(expected_changes); - - assert_eq!(actual_edit, expected_edit); - } - - #[tokio::test] - async fn field_name() { - let actual_edit = FeatureTester::new() - .file("main.bib", r#"@article{foo, bar = baz}"#) - .main("main.bib") - .position(0, 14) - .new_name("qux") - .test_rename(BibtexEntryRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } - - #[tokio::test] - async fn empty_latex_document() { - let actual_edit = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .new_name("") - .test_rename(BibtexEntryRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_edit = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .new_name("") - .test_rename(BibtexEntryRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } -} diff --git a/support/texlab/src/rename/latex_cmd.rs b/support/texlab/src/rename/latex_cmd.rs deleted file mode 100644 index 4770c4c485..0000000000 --- a/support/texlab/src/rename/latex_cmd.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - Position, Range, RenameParams, TextDocumentPositionParams, TextEdit, WorkspaceEdit, - }, - syntax::{latex, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use std::collections::HashMap; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexCommandPrepareRenameProvider; - -#[async_trait] -impl FeatureProvider for LatexCommandPrepareRenameProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Range>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let pos = req.params.position; - find_command(&req.current().content, pos).map(SyntaxNode::range) - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexCommandRenameProvider; - -#[async_trait] -impl FeatureProvider for LatexCommandRenameProvider { - type Params = RenameParams; - type Output = Option<WorkspaceEdit>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let pos = req.params.text_document_position.position; - let cmd_name = find_command(&req.current().content, pos)?.name.text(); - let mut changes = HashMap::new(); - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - let edits = table - .commands - .iter() - .filter_map(|node| table.as_command(*node)) - .filter(|cmd| cmd.name.text() == cmd_name) - .map(|cmd| { - TextEdit::new(cmd.name.range(), format!("\\{}", req.params.new_name)) - }) - .collect(); - changes.insert(doc.uri.clone().into(), edits); - } - } - Some(WorkspaceEdit::new(changes)) - } -} - -fn find_command(content: &DocumentContent, pos: Position) -> Option<&latex::Command> { - if let DocumentContent::Latex(table) = &content { - table.as_command(table.find_command_by_short_name_range(pos)?) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{feature::FeatureTester, protocol::RangeExt}; - use indoc::indoc; - - #[tokio::test] - async fn command() { - let actual_edit = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \include{bar.tex} - \baz - "# - ), - ) - .file("bar.tex", r#"\baz"#) - .main("foo.tex") - .position(1, 2) - .new_name("qux") - .test_rename(LatexCommandRenameProvider) - .await - .unwrap(); - - let mut expected_changes = HashMap::new(); - expected_changes.insert( - FeatureTester::uri("foo.tex").into(), - vec![TextEdit::new(Range::new_simple(1, 0, 1, 4), "\\qux".into())], - ); - expected_changes.insert( - FeatureTester::uri("bar.tex").into(), - vec![TextEdit::new(Range::new_simple(0, 0, 0, 4), "\\qux".into())], - ); - - assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes)); - } - - #[tokio::test] - async fn empty_latex_document() { - let actual_edit = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .new_name("") - .test_rename(LatexCommandRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_edit = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .new_name("") - .test_rename(LatexCommandRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } -} diff --git a/support/texlab/src/rename/latex_env.rs b/support/texlab/src/rename/latex_env.rs deleted file mode 100644 index 6e302ef8b6..0000000000 --- a/support/texlab/src/rename/latex_env.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - Position, Range, RangeExt, RenameParams, TextDocumentPositionParams, TextEdit, - WorkspaceEdit, - }, - syntax::{latex, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use std::collections::HashMap; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexEnvironmentPrepareRenameProvider; - -#[async_trait] -impl FeatureProvider for LatexEnvironmentPrepareRenameProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Range>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let pos = req.params.position; - let (left_name, right_name) = find_environment(&req.current().content, pos)?; - let range = if left_name.range().contains(pos) { - left_name.range() - } else { - right_name.range() - }; - Some(range) - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexEnvironmentRenameProvider; - -#[async_trait] -impl FeatureProvider for LatexEnvironmentRenameProvider { - type Params = RenameParams; - type Output = Option<WorkspaceEdit>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let (left_name, right_name) = find_environment( - &req.current().content, - req.params.text_document_position.position, - )?; - let edits = vec![ - TextEdit::new(left_name.range(), req.params.new_name.clone()), - TextEdit::new(right_name.range(), req.params.new_name.clone()), - ]; - let mut changes = HashMap::new(); - changes.insert(req.current().uri.clone().into(), edits); - Some(WorkspaceEdit::new(changes)) - } -} - -fn find_environment( - content: &DocumentContent, - pos: Position, -) -> Option<(&latex::Token, &latex::Token)> { - if let DocumentContent::Latex(table) = content { - for env in &table.environments { - if let Some(left_name) = env.left.name(&table) { - if let Some(right_name) = env.right.name(&table) { - if left_name.range().contains(pos) || right_name.range().contains(pos) { - return Some((left_name, right_name)); - } - } - } - } - } - None -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - use indoc::indoc; - - #[tokio::test] - async fn environment() { - let actual_edit = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \begin{foo} - \end{bar} - "# - ), - ) - .main("main.tex") - .position(0, 8) - .new_name("baz") - .test_rename(LatexEnvironmentRenameProvider) - .await - .unwrap(); - - let mut expected_changes = HashMap::new(); - expected_changes.insert( - FeatureTester::uri("main.tex").into(), - vec![ - TextEdit::new(Range::new_simple(0, 7, 0, 10), "baz".into()), - TextEdit::new(Range::new_simple(1, 5, 1, 8), "baz".into()), - ], - ); - - assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes)); - } - - #[tokio::test] - async fn command() { - let actual_edit = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \begin{foo} - \end{bar} - "# - ), - ) - .main("main.tex") - .position(0, 5) - .new_name("baz") - .test_rename(LatexEnvironmentRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } - - #[tokio::test] - async fn empty_latex_document() { - let actual_edit = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .new_name("") - .test_rename(LatexEnvironmentRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_edit = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .new_name("") - .test_rename(LatexEnvironmentRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } -} diff --git a/support/texlab/src/rename/latex_label.rs b/support/texlab/src/rename/latex_label.rs deleted file mode 100644 index 27e0f9b224..0000000000 --- a/support/texlab/src/rename/latex_label.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::{ - Position, Range, RangeExt, RenameParams, TextDocumentPositionParams, TextEdit, - WorkspaceEdit, - }, - syntax::{Span, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use std::collections::HashMap; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexLabelPrepareRenameProvider; - -#[async_trait] -impl FeatureProvider for LatexLabelPrepareRenameProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Range>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let pos = req.params.position; - find_label(&req.current().content, pos).map(Span::range) - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexLabelRenameProvider; - -#[async_trait] -impl FeatureProvider for LatexLabelRenameProvider { - type Params = RenameParams; - type Output = Option<WorkspaceEdit>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let pos = req.params.text_document_position.position; - let name = find_label(&req.current().content, pos)?; - let mut changes = HashMap::new(); - for doc in req.related() { - if let DocumentContent::Latex(table) = &doc.content { - let edits = table - .labels - .iter() - .flat_map(|label| label.names(&table)) - .filter(|label| label.text() == name.text) - .map(|label| TextEdit::new(label.range(), req.params.new_name.clone())) - .collect(); - changes.insert(doc.uri.clone().into(), edits); - } - } - Some(WorkspaceEdit::new(changes)) - } -} - -fn find_label(content: &DocumentContent, pos: Position) -> Option<&Span> { - if let DocumentContent::Latex(table) = content { - table - .labels - .iter() - .flat_map(|label| label.names(&table)) - .find(|label| label.range().contains(pos)) - .map(|label| &label.span) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - use indoc::indoc; - - #[tokio::test] - async fn label() { - let actual_edit = FeatureTester::new() - .file( - "foo.tex", - indoc!( - r#" - \label{foo} - \include{bar} - "# - ), - ) - .file("bar.tex", r#"\ref{foo}"#) - .file("baz.tex", r#"\ref{foo}"#) - .main("foo.tex") - .position(0, 7) - .new_name("bar") - .test_rename(LatexLabelRenameProvider) - .await - .unwrap(); - - let mut expected_changes = HashMap::new(); - expected_changes.insert( - FeatureTester::uri("foo.tex").into(), - vec![TextEdit::new(Range::new_simple(0, 7, 0, 10), "bar".into())], - ); - expected_changes.insert( - FeatureTester::uri("bar.tex").into(), - vec![TextEdit::new(Range::new_simple(0, 5, 0, 8), "bar".into())], - ); - - assert_eq!(actual_edit, WorkspaceEdit::new(expected_changes)); - } - - #[tokio::test] - async fn command_args() { - let actual_edit = FeatureTester::new() - .file("main.tex", r#"\foo{bar}"#) - .main("main.tex") - .position(0, 5) - .new_name("baz") - .test_rename(LatexLabelRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } - - #[tokio::test] - async fn empty_latex_document() { - let actual_edit = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .position(0, 0) - .new_name("") - .test_rename(LatexLabelRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_edit = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .position(0, 0) - .new_name("") - .test_rename(LatexLabelRenameProvider) - .await; - - assert_eq!(actual_edit, None); - } -} diff --git a/support/texlab/src/rename/mod.rs b/support/texlab/src/rename/mod.rs deleted file mode 100644 index 40bf8ce1e1..0000000000 --- a/support/texlab/src/rename/mod.rs +++ /dev/null @@ -1,88 +0,0 @@ -mod bibtex_entry; -mod latex_cmd; -mod latex_env; -mod latex_label; - -use self::{ - bibtex_entry::{BibtexEntryPrepareRenameProvider, BibtexEntryRenameProvider}, - latex_cmd::{LatexCommandPrepareRenameProvider, LatexCommandRenameProvider}, - latex_env::{LatexEnvironmentPrepareRenameProvider, LatexEnvironmentRenameProvider}, - latex_label::{LatexLabelPrepareRenameProvider, LatexLabelRenameProvider}, -}; -use crate::{ - feature::{ChoiceProvider, FeatureProvider, FeatureRequest}, - protocol::{Range, RenameParams, TextDocumentPositionParams, WorkspaceEdit}, -}; -use async_trait::async_trait; - -pub struct PrepareRenameProvider { - provider: ChoiceProvider<TextDocumentPositionParams, Range>, -} - -impl PrepareRenameProvider { - pub fn new() -> Self { - Self { - provider: ChoiceProvider::new(vec![ - Box::new(BibtexEntryPrepareRenameProvider), - Box::new(LatexCommandPrepareRenameProvider), - Box::new(LatexEnvironmentPrepareRenameProvider), - Box::new(LatexLabelPrepareRenameProvider), - ]), - } - } -} - -impl Default for PrepareRenameProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for PrepareRenameProvider { - type Params = TextDocumentPositionParams; - type Output = Option<Range>; - - async fn execute<'a>( - &'a self, - req: &'a FeatureRequest<TextDocumentPositionParams>, - ) -> Option<Range> { - self.provider.execute(req).await - } -} - -pub struct RenameProvider { - provider: ChoiceProvider<RenameParams, WorkspaceEdit>, -} - -impl RenameProvider { - pub fn new() -> Self { - Self { - provider: ChoiceProvider::new(vec![ - Box::new(BibtexEntryRenameProvider), - Box::new(LatexCommandRenameProvider), - Box::new(LatexEnvironmentRenameProvider), - Box::new(LatexLabelRenameProvider), - ]), - } - } -} - -impl Default for RenameProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for RenameProvider { - type Params = RenameParams; - type Output = Option<WorkspaceEdit>; - - async fn execute<'a>( - &'a self, - request: &'a FeatureRequest<RenameParams>, - ) -> Option<WorkspaceEdit> { - self.provider.execute(request).await - } -} diff --git a/support/texlab/src/req_queue.rs b/support/texlab/src/req_queue.rs new file mode 100644 index 0000000000..3fb875a8be --- /dev/null +++ b/support/texlab/src/req_queue.rs @@ -0,0 +1,13 @@ +use cancellation::CancellationTokenSource; +use crossbeam_channel::Sender; +use lsp_server::ResponseError; + +pub struct IncomingData { + pub(crate) token_source: CancellationTokenSource, +} + +pub struct OutgoingData { + pub(crate) sender: Sender<Result<serde_json::Value, ResponseError>>, +} + +pub type ReqQueue = lsp_server::ReqQueue<IncomingData, OutgoingData>; diff --git a/support/texlab/src/server.rs b/support/texlab/src/server.rs index 9c5e1b857b..5a78d5e2e2 100644 --- a/support/texlab/src/server.rs +++ b/support/texlab/src/server.rs @@ -1,131 +1,113 @@ -#[cfg(feature = "citation")] -use crate::citeproc::render_citation; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, + thread, +}; + +use anyhow::Result; +use cancellation::{CancellationToken, CancellationTokenSource}; +use crossbeam_channel::Sender; +use log::{info, warn}; +use lsp_server::{Connection, ErrorCode, Message, RequestId}; +use lsp_types::{ + notification::{ + Cancel, DidChangeConfiguration, DidChangeTextDocument, DidOpenTextDocument, + DidSaveTextDocument, LogMessage, PublishDiagnostics, + }, + request::{ + DocumentLinkRequest, FoldingRangeRequest, Formatting, GotoDefinition, PrepareRenameRequest, + References, Rename, SemanticTokensRangeRequest, + }, + *, +}; +use notification::DidCloseTextDocument; +use request::{ + Completion, DocumentHighlightRequest, DocumentSymbolRequest, HoverRequest, + ResolveCompletionItem, WorkspaceSymbol, +}; +use serde::Serialize; +use threadpool::ThreadPool; use crate::{ - build::BuildProvider, - completion::{CompletionItemData, CompletionProvider, COMPLETION_LIMIT}, - components::COMPONENT_DATABASE, - config::ConfigManager, - definition::DefinitionProvider, - diagnostics::DiagnosticsManager, - feature::{DocumentView, FeatureProvider, FeatureRequest}, - folding::FoldingProvider, - forward_search, - highlight::HighlightProvider, - hover::HoverProvider, - link::LinkProvider, - protocol::*, - reference::ReferenceProvider, - rename::{PrepareRenameProvider, RenameProvider}, - symbol::{document_symbols, workspace_symbols, SymbolProvider}, - syntax::{bibtex, latexindent, CharStream, SyntaxNode}, - tex::{Distribution, DistributionKind, KpsewhichError}, - workspace::{DocumentContent, Workspace}, + client::send_notification, + component_db::COMPONENT_DATABASE, + config::{pull_config, push_config, register_config_capability}, + create_workspace_full, + diagnostics::{DiagnosticsDebouncer, DiagnosticsManager, DiagnosticsMessage}, + dispatch::{NotificationDispatcher, RequestDispatcher}, + distro::Distribution, + features::{ + build_document, find_all_references, find_document_highlights, find_document_links, + find_document_symbols, find_foldings, find_hover, find_workspace_symbols, + format_source_code, goto_definition, prepare_rename_all, rename_all, BuildParams, + BuildResult, FeatureRequest, ForwardSearchResult, + }, + req_queue::{IncomingData, ReqQueue}, + DocumentLanguage, ServerContext, Uri, Workspace, WorkspaceSource, }; -use async_trait::async_trait; -use chashmap::CHashMap; -use futures::lock::Mutex; -use jsonrpc::{server::Result, Middleware}; -use jsonrpc_derive::{jsonrpc_method, jsonrpc_server}; -use log::{debug, error, info, warn}; -use once_cell::sync::{Lazy, OnceCell}; -use std::{mem, path::PathBuf, sync::Arc}; - -pub struct LatexLspServer<C> { - distro: Arc<dyn Distribution>, - client: Arc<C>, - client_capabilities: OnceCell<Arc<ClientCapabilities>>, - client_info: OnceCell<Option<ClientInfo>>, - current_dir: Arc<PathBuf>, - config_manager: OnceCell<ConfigManager<C>>, - action_manager: ActionManager, - workspace: Workspace, - build_provider: BuildProvider<C>, - completion_provider: CompletionProvider, - definition_provider: DefinitionProvider, - folding_provider: FoldingProvider, - highlight_provider: HighlightProvider, - link_provider: LinkProvider, - reference_provider: ReferenceProvider, - prepare_rename_provider: PrepareRenameProvider, - rename_provider: RenameProvider, - symbol_provider: SymbolProvider, - hover_provider: HoverProvider, - diagnostics_manager: DiagnosticsManager, - last_position_by_uri: CHashMap<Uri, Position>, + +pub struct Server { + connection: Connection, + context: Arc<ServerContext>, + req_queue: Arc<Mutex<ReqQueue>>, + workspace: Arc<dyn Workspace>, + static_debouncer: DiagnosticsDebouncer, + chktex_debouncer: DiagnosticsDebouncer, + pool: ThreadPool, + load_resolver: bool, } -#[jsonrpc_server] -impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> { - pub fn new(distro: Arc<dyn Distribution>, client: Arc<C>, current_dir: Arc<PathBuf>) -> Self { - let workspace = Workspace::new(distro.clone(), Arc::clone(¤t_dir)); - Self { - distro, - client: Arc::clone(&client), - client_capabilities: OnceCell::new(), - client_info: OnceCell::new(), - current_dir, - config_manager: OnceCell::new(), - action_manager: ActionManager::default(), +impl Server { + pub fn with_connection( + connection: Connection, + current_dir: PathBuf, + load_resolver: bool, + ) -> Result<Self> { + let context = Arc::new(ServerContext::new(current_dir)); + let req_queue = Arc::default(); + let workspace = Arc::new(create_workspace_full(Arc::clone(&context))?); + let diag_manager = Arc::new(Mutex::new(DiagnosticsManager::default())); + + let static_debouncer = + create_static_debouncer(Arc::clone(&diag_manager), &connection, Arc::clone(&context)); + + let chktex_debouncer = + create_chktex_debouncer(diag_manager, &connection, Arc::clone(&context)); + + Ok(Self { + connection, + context, + req_queue, workspace, - build_provider: BuildProvider::new(client), - completion_provider: CompletionProvider::new(), - definition_provider: DefinitionProvider::new(), - folding_provider: FoldingProvider::new(), - highlight_provider: HighlightProvider::new(), - link_provider: LinkProvider::new(), - reference_provider: ReferenceProvider::new(), - prepare_rename_provider: PrepareRenameProvider::new(), - rename_provider: RenameProvider::new(), - symbol_provider: SymbolProvider::new(), - hover_provider: HoverProvider::new(), - diagnostics_manager: DiagnosticsManager::default(), - last_position_by_uri: CHashMap::new(), - } - } - - fn client_capabilities(&self) -> Arc<ClientCapabilities> { - Arc::clone( - self.client_capabilities - .get() - .expect("initialize has not been called"), - ) - } - - fn config_manager(&self) -> &ConfigManager<C> { - self.config_manager - .get() - .expect("initialize has not been called") + static_debouncer, + chktex_debouncer, + pool: threadpool::Builder::new().build(), + load_resolver, + }) } - #[jsonrpc_method("initialize", kind = "request")] - pub async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> { - self.client_capabilities - .set(Arc::new(params.capabilities)) - .expect("initialize was called two times"); - - self.client_info - .set(params.client_info) - .expect("initialize was called two times"); - - let _ = self.config_manager.set(ConfigManager::new( - Arc::clone(&self.client), - self.client_capabilities(), - )); - - let capabilities = ServerCapabilities { + fn capabilities(&self) -> ServerCapabilities { + ServerCapabilities { text_document_sync: Some(TextDocumentSyncCapability::Options( TextDocumentSyncOptions { open_close: Some(true), change: Some(TextDocumentSyncKind::Full), will_save: None, will_save_wait_until: None, - save: Some(SaveOptions { + save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { include_text: Some(false), - }), + })), }, )), - hover_provider: Some(true), + document_link_provider: Some(DocumentLinkOptions { + resolve_provider: Some(false), + work_done_progress_options: WorkDoneProgressOptions::default(), + }), + folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), + definition_provider: Some(OneOf::Left(true)), + references_provider: Some(OneOf::Left(true)), + hover_provider: Some(HoverProviderCapability::Simple(true)), + #[cfg(feature = "completion")] completion_provider: Some(CompletionOptions { resolve_provider: Some(true), trigger_characters: Some(vec![ @@ -138,625 +120,728 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> { ]), ..CompletionOptions::default() }), - definition_provider: Some(true), - references_provider: Some(true), - document_highlight_provider: Some(true), - document_symbol_provider: Some(true), - workspace_symbol_provider: Some(true), - document_formatting_provider: Some(true), - rename_provider: Some(RenameProviderCapability::Options(RenameOptions { + document_symbol_provider: Some(OneOf::Left(true)), + workspace_symbol_provider: Some(OneOf::Left(true)), + rename_provider: Some(OneOf::Right(RenameOptions { prepare_provider: Some(true), work_done_progress_options: WorkDoneProgressOptions::default(), })), - document_link_provider: Some(DocumentLinkOptions { - resolve_provider: Some(false), - work_done_progress_options: WorkDoneProgressOptions::default(), - }), - folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), + document_highlight_provider: Some(OneOf::Left(true)), + document_formatting_provider: Some(OneOf::Left(true)), + #[cfg(feature = "semantic")] + semantic_tokens_provider: Some( + SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions { + full: None, + range: Some(true), + legend: SemanticTokensLegend { + token_types: crate::features::legend::SUPPORTED_TYPES.to_vec(), + token_modifiers: crate::features::legend::SUPPORTED_MODIFIERS.to_vec(), + }, + work_done_progress_options: WorkDoneProgressOptions::default(), + }), + ), ..ServerCapabilities::default() - }; + } + } + + fn initialize(&mut self) -> Result<()> { + let (id, params) = self.connection.initialize_start()?; + let params: InitializeParams = serde_json::from_value(params)?; - Lazy::force(&COMPONENT_DATABASE); - Ok(InitializeResult { - capabilities, + *self.context.client_capabilities.lock().unwrap() = params.capabilities; + *self.context.client_info.lock().unwrap() = params.client_info; + + let result = InitializeResult { + capabilities: self.capabilities(), server_info: Some(ServerInfo { name: "TexLab".to_owned(), version: Some(env!("CARGO_PKG_VERSION").to_owned()), }), - }) + }; + self.connection + .initialize_finish(id, serde_json::to_value(result)?)?; + + let cx = Arc::clone(&self.context); + if self.load_resolver { + self.pool.execute(move || { + let distro = Distribution::detect(); + info!("Detected distribution: {}", distro.kind); + *cx.resolver.lock().unwrap() = distro.resolver; + }); + } + + self.register_diagnostics_handler(); + + let req_queue = Arc::clone(&self.req_queue); + let sender = self.connection.sender.clone(); + let context = Arc::clone(&self.context); + self.pool.execute(move || { + register_config_capability(&req_queue, &sender, &context.client_capabilities); + pull_config( + &req_queue, + &sender, + &context.options, + &context.client_capabilities.lock().unwrap(), + ); + }); + Ok(()) } - #[jsonrpc_method("initialized", kind = "notification")] - pub async fn initialized(&self, _params: InitializedParams) { - self.action_manager.push(Action::PullConfiguration).await; - self.action_manager.push(Action::RegisterCapabilities).await; - self.action_manager.push(Action::LoadDistribution).await; - self.action_manager.push(Action::PublishDiagnostics).await; + fn register_diagnostics_handler(&mut self) { + let sender = self.static_debouncer.sender.clone(); + self.workspace + .register_open_handler(Arc::new(move |workspace, document| { + let message = DiagnosticsMessage::Analyze { + workspace, + document, + }; + sender.send(message).unwrap(); + })); + } + + fn register_incoming_request(&self, id: RequestId) -> Arc<CancellationToken> { + let token_source = CancellationTokenSource::new(); + let token = Arc::clone(token_source.token()); + let mut req_queue = self.req_queue.lock().unwrap(); + req_queue + .incoming + .register(id.clone(), IncomingData { token_source }); + token + } + + fn cancel(&self, params: CancelParams) -> Result<()> { + let id = match params.id { + NumberOrString::Number(id) => RequestId::from(id), + NumberOrString::String(id) => RequestId::from(id), + }; + + let mut req_queue = self.req_queue.lock().unwrap(); + if let Some(data) = req_queue.incoming.complete(id.clone()) { + data.token_source.cancel(); + } + + Ok(()) } - #[jsonrpc_method("shutdown", kind = "request")] - pub async fn shutdown(&self, _params: ()) -> Result<()> { + fn did_change_configuration(&self, params: DidChangeConfigurationParams) -> Result<()> { + push_config(&self.context.options, params.settings); Ok(()) } - #[jsonrpc_method("exit", kind = "notification")] - pub async fn exit(&self, _params: ()) {} - - #[jsonrpc_method("$/cancelRequest", kind = "notification")] - pub async fn cancel_request(&self, _params: CancelParams) {} - - #[jsonrpc_method("textDocument/didOpen", kind = "notification")] - pub async fn did_open(&self, params: DidOpenTextDocumentParams) { - let uri = params.text_document.uri.clone(); - let options = self.config_manager().get().await; - self.workspace.add(params.text_document, &options).await; - self.action_manager - .push(Action::DetectRoot(uri.clone().into())) - .await; - self.action_manager - .push(Action::RunLinter(uri.into(), LintReason::Save)) - .await; - self.action_manager.push(Action::PublishDiagnostics).await; - } - - #[jsonrpc_method("textDocument/didChange", kind = "notification")] - pub async fn did_change(&self, params: DidChangeTextDocumentParams) { - let options = self.config_manager().get().await; - for change in params.content_changes { - let uri = params.text_document.uri.clone(); - self.workspace - .update(uri.into(), change.text, &options) - .await; - } - self.action_manager - .push(Action::RunLinter( - params.text_document.uri.clone().into(), - LintReason::Change, - )) - .await; - self.action_manager.push(Action::PublishDiagnostics).await; - } - - #[jsonrpc_method("textDocument/didSave", kind = "notification")] - pub async fn did_save(&self, params: DidSaveTextDocumentParams) { - self.action_manager - .push(Action::Build(params.text_document.uri.clone().into())) - .await; - - self.action_manager - .push(Action::RunLinter( - params.text_document.uri.into(), - LintReason::Save, - )) - .await; - self.action_manager.push(Action::PublishDiagnostics).await; - } - - #[jsonrpc_method("textDocument/didClose", kind = "notification")] - pub async fn did_close(&self, _params: DidCloseTextDocumentParams) {} - - #[jsonrpc_method("workspace/didChangeConfiguration", kind = "notification")] - pub async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { - let config_manager = self.config_manager(); - config_manager.push(params.settings).await; - let options = config_manager.get().await; - self.workspace.reparse(&options).await; - } - - #[jsonrpc_method("window/workDoneProgress/cancel", kind = "notification")] - pub async fn work_done_progress_cancel(&self, params: WorkDoneProgressCancelParams) { - self.build_provider.cancel(params.token).await; - } - - #[jsonrpc_method("textDocument/completion", kind = "request")] - pub async fn completion(&self, params: CompletionParams) -> Result<CompletionList> { - let req = self - .make_feature_request(params.text_document_position.text_document.as_uri(), params) - .await?; - - self.last_position_by_uri.insert( - req.current().uri.clone(), - req.params.text_document_position.position, + fn did_open(&self, params: DidOpenTextDocumentParams) -> Result<()> { + let language_id = ¶ms.text_document.language_id; + let language = DocumentLanguage::by_language_id(language_id); + let document = self.workspace.open( + Arc::new(params.text_document.uri.into()), + params.text_document.text, + language.unwrap_or(DocumentLanguage::Latex), + WorkspaceSource::Client, ); - let items = self.completion_provider.execute(&req).await; - let is_incomplete = if self - .client_info - .get() - .and_then(|info| info.as_ref()) - .map(|info| info.name.as_str()) - .unwrap_or_default() - == "vscode" + let should_lint = { self.context.options.read().unwrap().chktex.on_open_and_save }; + if let Some(document) = self + .workspace + .get(document.uri.as_ref()) + .filter(|_| should_lint) { - true - } else { - items.len() >= COMPLETION_LIMIT + self.chktex_debouncer + .sender + .send(DiagnosticsMessage::Analyze { + workspace: Arc::clone(&self.workspace), + document, + })?; }; + Ok(()) + } - Ok(CompletionList { - is_incomplete, - items, - }) + fn did_change(&self, mut params: DidChangeTextDocumentParams) -> Result<()> { + let uri = params.text_document.uri.into(); + assert_eq!(params.content_changes.len(), 1); + let text = params.content_changes.pop().unwrap().text; + let language = self + .workspace + .get(&uri) + .map(|document| document.data.language()) + .unwrap_or(DocumentLanguage::Latex); + + let document = self + .workspace + .open(Arc::new(uri), text, language, WorkspaceSource::Client); + + let should_lint = { self.context.options.read().unwrap().chktex.on_edit }; + if let Some(document) = self + .workspace + .get(document.uri.as_ref()) + .filter(|_| should_lint) + { + self.chktex_debouncer + .sender + .send(DiagnosticsMessage::Analyze { + workspace: Arc::clone(&self.workspace), + document, + })?; + }; + + Ok(()) } - #[jsonrpc_method("completionItem/resolve", kind = "request")] - pub async fn completion_resolve(&self, mut item: CompletionItem) -> Result<CompletionItem> { - let data: CompletionItemData = serde_json::from_value(item.data.clone().unwrap()).unwrap(); - match data { - CompletionItemData::Package | CompletionItemData::Class => { - item.documentation = COMPONENT_DATABASE - .documentation(&item.label) - .map(Documentation::MarkupContent); - } - #[cfg(feature = "citation")] - CompletionItemData::Citation { uri, key } => { - let snapshot = self.workspace.get().await; - if let Some(doc) = snapshot.find(&uri) { - if let DocumentContent::Bibtex(tree) = &doc.content { - let markup = render_citation(&tree, &key); - item.documentation = markup.map(Documentation::MarkupContent); - } - } - } - _ => {} + fn did_save(&self, params: DidSaveTextDocumentParams) -> Result<()> { + let uri = params.text_document.uri.into(); + let should_lint = { self.context.options.read().unwrap().chktex.on_open_and_save }; + if let Some(document) = self.workspace.get(&uri).filter(|_| should_lint) { + self.chktex_debouncer + .sender + .send(DiagnosticsMessage::Analyze { + workspace: Arc::clone(&self.workspace), + document, + })?; }; - Ok(item) + Ok(()) } - #[jsonrpc_method("textDocument/hover", kind = "request")] - pub async fn hover(&self, params: TextDocumentPositionParams) -> Result<Option<Hover>> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; + fn did_close(&self, params: DidCloseTextDocumentParams) -> Result<()> { + let uri = params.text_document.uri.into(); + self.workspace.close(&uri); + Ok(()) + } - self.last_position_by_uri - .insert(req.current().uri.clone(), req.params.position); + fn feature_request<P>(&self, uri: Arc<Uri>, params: P) -> Option<FeatureRequest<P>> { + let req_queue = Arc::clone(&self.req_queue); + let sender = self.connection.sender.clone(); + let cx = Arc::clone(&self.context); + self.pool.execute(move || { + pull_config( + &req_queue, + &sender, + &cx.options, + &cx.client_capabilities.lock().unwrap(), + ); + }); + + Some(FeatureRequest { + context: Arc::clone(&self.context), + params, + workspace: Arc::clone(&self.workspace), + subset: self.workspace.subset(uri)?, + }) + } - Ok(self.hover_provider.execute(&req).await) + fn send_feature_error(&self, id: RequestId) -> Result<()> { + let resp = lsp_server::Response::new_err( + id, + ErrorCode::InternalError as i32, + "unknown document URI".to_string(), + ); + self.connection.sender.send(resp.into())?; + Ok(()) } - #[jsonrpc_method("textDocument/definition", kind = "request")] - pub async fn definition( + fn handle_feature_request<P, R, H>( &self, - params: TextDocumentPositionParams, - ) -> Result<DefinitionResponse> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - let results = self.definition_provider.execute(&req).await; - let response = if req.client_capabilities.has_definition_link_support() { - DefinitionResponse::LocationLinks(results) - } else { - DefinitionResponse::Locations( - results - .into_iter() - .map(|link| Location::new(link.target_uri, link.target_selection_range)) - .collect(), - ) + id: RequestId, + params: P, + uri: Arc<Uri>, + token: &Arc<CancellationToken>, + handler: H, + ) -> Result<()> + where + P: Send + 'static, + R: Serialize, + H: FnOnce(FeatureRequest<P>, &CancellationToken) -> R + Send + 'static, + { + match self.feature_request(uri, params) { + Some(req) => { + let sender = self.connection.sender.clone(); + let token = Arc::clone(token); + self.pool.execute(move || { + let result = handler(req, &token); + if token.is_canceled() { + sender.send(cancel_response(id).into()).unwrap(); + } else { + sender + .send(lsp_server::Response::new_ok(id, result).into()) + .unwrap(); + } + }); + } + None => { + self.send_feature_error(id)?; + } }; - - Ok(response) + Ok(()) } - #[jsonrpc_method("textDocument/references", kind = "request")] - pub async fn references(&self, params: ReferenceParams) -> Result<Vec<Location>> { - let req = self - .make_feature_request(params.text_document_position.as_uri(), params) - .await?; - Ok(self.reference_provider.execute(&req).await) + fn document_link( + &self, + id: RequestId, + params: DocumentLinkParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new(params.text_document.uri.clone().into()); + self.handle_feature_request(id, params, uri, token, find_document_links)?; + Ok(()) } - #[jsonrpc_method("textDocument/documentHighlight", kind = "request")] - pub async fn document_highlight( + fn document_symbols( &self, - params: TextDocumentPositionParams, - ) -> Result<Vec<DocumentHighlight>> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - Ok(self.highlight_provider.execute(&req).await) + id: RequestId, + params: DocumentSymbolParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new(params.text_document.uri.clone().into()); + self.handle_feature_request(id, params, uri, token, find_document_symbols)?; + Ok(()) } - #[jsonrpc_method("workspace/symbol", kind = "request")] - pub async fn workspace_symbol( + fn workspace_symbols( &self, + id: RequestId, params: WorkspaceSymbolParams, - ) -> Result<Vec<SymbolInformation>> { - let distro = self.distro.clone(); - let client_capabilities = self.client_capabilities(); - let snapshot = self.workspace.get().await; - let options = self.config_manager().get().await; - let symbols = workspace_symbols( - distro, - client_capabilities, - snapshot, - &options, - Arc::clone(&self.current_dir), - ¶ms, - ) - .await; - Ok(symbols) - } - - #[jsonrpc_method("textDocument/documentSymbol", kind = "request")] - pub async fn document_symbol( + token: &Arc<CancellationToken>, + ) -> Result<()> { + let sender = self.connection.sender.clone(); + let workspace = Arc::clone(&self.workspace); + let token = Arc::clone(token); + self.pool.execute(move || { + let result = find_workspace_symbols(workspace.as_ref(), ¶ms, &token); + if token.is_canceled() { + sender.send(cancel_response(id).into()).unwrap(); + } else { + sender + .send(lsp_server::Response::new_ok(id, result).into()) + .unwrap(); + } + }); + Ok(()) + } + + #[cfg(feature = "completion")] + fn completion( &self, - params: DocumentSymbolParams, - ) -> Result<DocumentSymbolResponse> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - - let symbols = self.symbol_provider.execute(&req).await; - let response = document_symbols( - &req.client_capabilities, - req.snapshot(), - &req.current().uri, - &req.options, - &req.current_dir, - symbols.into_iter().map(Into::into).collect(), + id: RequestId, + params: CompletionParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new( + params + .text_document_position + .text_document + .uri + .clone() + .into(), ); - Ok(response) - } - - #[jsonrpc_method("textDocument/documentLink", kind = "request")] - pub async fn document_link(&self, params: DocumentLinkParams) -> Result<Vec<DocumentLink>> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - Ok(self.link_provider.execute(&req).await) - } - - #[jsonrpc_method("textDocument/formatting", kind = "request")] - pub async fn formatting(&self, params: DocumentFormattingParams) -> Result<Vec<TextEdit>> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - let mut edits = Vec::new(); - match &req.current().content { - DocumentContent::Latex(_) => { - Self::run_latexindent(&req.current().text, "tex", &mut edits).await; - } - DocumentContent::Bibtex(tree) => { - let options = req - .options - .bibtex - .clone() - .and_then(|opts| opts.formatting) - .unwrap_or_default(); - - match options.formatter.unwrap_or_default() { - BibtexFormatter::Texlab => { - let params = bibtex::FormattingParams { - tab_size: req.params.options.tab_size as usize, - insert_spaces: req.params.options.insert_spaces, - options: &options, - }; - - for node in tree.children(tree.root) { - let should_format = match &tree.graph[node] { - bibtex::Node::Preamble(_) | bibtex::Node::String(_) => true, - bibtex::Node::Entry(entry) => !entry.is_comment(), - _ => false, - }; - if should_format { - let text = bibtex::format(&tree, node, params); - edits.push(TextEdit::new(tree.graph[node].range(), text)); - } + self.handle_feature_request(id, params, uri, token, crate::features::complete)?; + Ok(()) + } + + #[cfg(feature = "completion")] + fn completion_resolve( + &self, + id: RequestId, + mut item: CompletionItem, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let sender = self.connection.sender.clone(); + let token = Arc::clone(token); + let workspace = Arc::clone(&self.workspace); + self.pool.execute(move || { + match serde_json::from_value(item.data.clone().unwrap()).unwrap() { + crate::features::CompletionItemData::Package + | crate::features::CompletionItemData::Class => { + item.documentation = COMPONENT_DATABASE + .documentation(&item.label) + .map(Documentation::MarkupContent); + } + #[cfg(feature = "citeproc")] + crate::features::CompletionItemData::Citation { uri, key } => { + if let Some(document) = workspace.get(&uri) { + if let Some(data) = document.data.as_bibtex() { + let markup = crate::citation::render_citation(&data.root, &key); + item.documentation = markup.map(Documentation::MarkupContent); } } - BibtexFormatter::Latexindent => { - Self::run_latexindent(&req.current().text, "bib", &mut edits).await; - } } + _ => {} + }; + + drop(workspace); + if token.is_canceled() { + sender.send(cancel_response(id).into()).unwrap(); + } else { + sender + .send(lsp_server::Response::new_ok(id, item).into()) + .unwrap(); } - } - Ok(edits) + }); + Ok(()) } - async fn run_latexindent(old_text: &str, extension: &str, edits: &mut Vec<TextEdit>) { - match latexindent::format(old_text, extension).await { - Ok(new_text) => { - let mut stream = CharStream::new(&old_text); - while stream.next().is_some() {} - let range = Range::new(Position::new(0, 0), stream.current_position); - edits.push(TextEdit::new(range, new_text)); - } - Err(why) => { - debug!("Failed to run latexindent.pl: {}", why); - } - } + fn folding_range( + &self, + id: RequestId, + params: FoldingRangeParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new(params.text_document.uri.clone().into()); + self.handle_feature_request(id, params, uri, token, find_foldings)?; + Ok(()) } - #[jsonrpc_method("textDocument/prepareRename", kind = "request")] - pub async fn prepare_rename( + fn references( &self, - params: TextDocumentPositionParams, - ) -> Result<Option<Range>> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - Ok(self.prepare_rename_provider.execute(&req).await) - } - - #[jsonrpc_method("textDocument/rename", kind = "request")] - pub async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> { - let req = self - .make_feature_request(params.text_document_position.text_document.as_uri(), params) - .await?; - Ok(self.rename_provider.execute(&req).await) - } - - #[jsonrpc_method("textDocument/foldingRange", kind = "request")] - pub async fn folding_range(&self, params: FoldingRangeParams) -> Result<Vec<FoldingRange>> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - Ok(self.folding_provider.execute(&req).await) - } - - #[jsonrpc_method("textDocument/build", kind = "request")] - pub async fn build(&self, params: BuildParams) -> Result<BuildResult> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - - let pos = self - .last_position_by_uri - .get(&req.current().uri) - .map(|pos| *pos) - .unwrap_or_default(); - - let res = self.build_provider.execute(&req).await; - - if req - .options - .latex - .and_then(|opts| opts.build) - .unwrap_or_default() - .forward_search_after() - && !self.build_provider.is_building() - { - let params = TextDocumentPositionParams::new(req.params.text_document, pos); - self.forward_search(params).await?; - } + id: RequestId, + params: ReferenceParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new( + params + .text_document_position + .text_document + .uri + .clone() + .into(), + ); + self.handle_feature_request(id, params, uri, token, find_all_references)?; + Ok(()) + } + + fn hover( + &self, + id: RequestId, + params: HoverParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new( + params + .text_document_position_params + .text_document + .uri + .clone() + .into(), + ); + self.handle_feature_request(id, params, uri, token, find_hover)?; + Ok(()) + } - Ok(res) + fn goto_definition( + &self, + id: RequestId, + params: GotoDefinitionParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new( + params + .text_document_position_params + .text_document + .uri + .clone() + .into(), + ); + self.handle_feature_request(id, params, uri, token, goto_definition)?; + Ok(()) } - #[jsonrpc_method("textDocument/forwardSearch", kind = "request")] - pub async fn forward_search( + fn prepare_rename( &self, + id: RequestId, params: TextDocumentPositionParams, - ) -> Result<ForwardSearchResult> { - let req = self - .make_feature_request(params.text_document.as_uri(), params) - .await?; - - forward_search::search( - &req.view.snapshot, - &req.current().uri, - req.params.position.line, - &req.options, - &self.current_dir, - ) - .await - .ok_or_else(|| "Unable to execute forward search".into()) - } - - #[jsonrpc_method("$/detectRoot", kind = "request")] - pub async fn detect_root(&self, params: TextDocumentIdentifier) -> Result<()> { - let options = self.config_manager().get().await; - let _ = self.workspace.detect_root(¶ms.as_uri(), &options).await; + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new(params.text_document.uri.clone().into()); + self.handle_feature_request(id, params, uri, token, prepare_rename_all)?; Ok(()) } - async fn make_feature_request<P>(&self, uri: Uri, params: P) -> Result<FeatureRequest<P>> { - let options = self.pull_configuration().await; - let snapshot = self.workspace.get().await; - let client_capabilities = self.client_capabilities(); - match snapshot.find(&uri) { - Some(current) => Ok(FeatureRequest { - params, - view: DocumentView::analyze(snapshot, current, &options, &self.current_dir), - distro: self.distro.clone(), - client_capabilities, - options, - current_dir: Arc::clone(&self.current_dir), - }), - None => { - let msg = format!("Unknown document: {}", uri); - Err(msg) - } - } + fn rename( + &self, + id: RequestId, + params: RenameParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new( + params + .text_document_position + .text_document + .uri + .clone() + .into(), + ); + self.handle_feature_request(id, params, uri, token, rename_all)?; + Ok(()) } - async fn pull_configuration(&self) -> Options { - let config_manager = self.config_manager(); - let has_changed = config_manager.pull().await; - let options = config_manager.get().await; - if has_changed { - self.workspace.reparse(&options).await; - } - options + fn document_highlight( + &self, + id: RequestId, + params: DocumentHighlightParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new( + params + .text_document_position_params + .text_document + .uri + .clone() + .into(), + ); + self.handle_feature_request(id, params, uri, token, find_document_highlights)?; + Ok(()) } - async fn update_build_diagnostics(&self) { - let snapshot = self.workspace.get().await; - let options = self.config_manager().get().await; + fn formatting( + &self, + id: RequestId, + params: DocumentFormattingParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new(params.text_document.uri.clone().into()); + self.handle_feature_request(id, params, uri, token, format_source_code)?; + Ok(()) + } - for doc in snapshot.0.iter().filter(|doc| doc.uri.scheme() == "file") { - if let DocumentContent::Latex(table) = &doc.content { - if table.is_standalone { - match self - .diagnostics_manager - .build - .update(&snapshot, &doc.uri, &options, &self.current_dir) - .await - { - Ok(true) => self.action_manager.push(Action::PublishDiagnostics).await, - Ok(false) => (), - Err(why) => { - warn!("Unable to read log file ({}): {}", why, doc.uri.as_str()) - } - } - } - } - } + #[cfg(feature = "semantic")] + fn semantic_tokens_range( + &self, + id: RequestId, + params: SemanticTokensRangeParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new(params.text_document.uri.clone().into()); + self.handle_feature_request( + id, + params, + uri, + token, + crate::features::find_semantic_tokens_range, + )?; + Ok(()) } - async fn load_distribution(&self) { - info!("Detected TeX distribution: {}", self.distro.kind()); - if self.distro.kind() == DistributionKind::Unknown { - let params = ShowMessageParams { - message: "Your TeX distribution could not be detected. \ - Please make sure that your distribution is in your PATH." - .into(), - typ: MessageType::Error, - }; - self.client.show_message(params).await; - } + #[cfg(not(feature = "semantic"))] + fn semantic_tokens_range( + &self, + _id: RequestId, + _params: SemanticTokensRangeParams, + _token: &Arc<CancellationToken>, + ) -> Result<()> { + Ok(()) + } - if let Err(why) = self.distro.load().await { - let message = match why { - KpsewhichError::NotInstalled | KpsewhichError::InvalidOutput => { - "An error occurred while executing `kpsewhich`.\ - Please make sure that your distribution is in your PATH \ - environment variable and provides the `kpsewhich` tool." - } - KpsewhichError::CorruptDatabase | KpsewhichError::NoDatabase => { - "The file database of your TeX distribution seems \ - to be corrupt. Please rebuild it and try again." - } - KpsewhichError::Decode(_) => { - "An error occurred while decoding the output of `kpsewhich`." - } - KpsewhichError::IO(why) => { - error!("An I/O error occurred while executing 'kpsewhich': {}", why); - "An I/O error occurred while executing 'kpsewhich'" + fn build( + &self, + id: RequestId, + params: BuildParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new(params.text_document.uri.clone().into()); + let lsp_sender = self.connection.sender.clone(); + self.handle_feature_request(id, params, uri, token, |request, token| { + let (log_sender, log_receiver) = crossbeam_channel::unbounded(); + + thread::spawn(move || { + for message in &log_receiver { + send_notification::<LogMessage>( + &lsp_sender, + LogMessageParams { + message, + typ: MessageType::Log, + }, + ) + .unwrap(); } - }; - let params = ShowMessageParams { - message: message.into(), - typ: MessageType::Error, - }; - self.client.show_message(params).await; - }; + }); + + build_document(request, token, log_sender) + })?; + Ok(()) } -} -#[async_trait] -impl<C: LspClient + Send + Sync + 'static> Middleware for LatexLspServer<C> { - async fn before_message(&self) { - if let Some(config_manager) = self.config_manager.get() { - let options = config_manager.get().await; - self.workspace.detect_children(&options).await; - self.workspace.reparse_all_if_newer(&options).await; - } + fn forward_search( + &self, + id: RequestId, + params: TextDocumentPositionParams, + token: &Arc<CancellationToken>, + ) -> Result<()> { + let uri = Arc::new(params.text_document.uri.clone().into()); + self.handle_feature_request( + id, + params, + uri, + token, + crate::features::execute_forward_search, + )?; + Ok(()) } - async fn after_message(&self) { - self.update_build_diagnostics().await; - for action in self.action_manager.take().await { - match action { - Action::LoadDistribution => { - self.load_distribution().await; - } - Action::RegisterCapabilities => { - let config_manager = self.config_manager(); - config_manager.register().await; - } - Action::PullConfiguration => { - self.pull_configuration().await; - } - Action::DetectRoot(uri) => { - let options = self.config_manager().get().await; - let _ = self.workspace.detect_root(&uri, &options).await; - } - Action::PublishDiagnostics => { - let snapshot = self.workspace.get().await; - for doc in &snapshot.0 { - let diagnostics = self.diagnostics_manager.get(doc).await; - let params = PublishDiagnosticsParams { - uri: doc.uri.clone().into(), - diagnostics, - version: None, - }; - self.client.publish_diagnostics(params).await; + fn process_messages(&self) -> Result<()> { + for msg in &self.connection.receiver { + match msg { + Message::Request(request) => { + if self.connection.handle_shutdown(&request)? { + return Ok(()); } - } - Action::Build(uri) => { - let options = self - .config_manager() - .get() - .await - .latex - .and_then(|opts| opts.build) - .unwrap_or_default(); - - if options.on_save() { - let text_document = TextDocumentIdentifier::new(uri.into()); - self.build(BuildParams { text_document }).await.unwrap(); + + let token = self.register_incoming_request(request.id.clone()); + if let Some(response) = RequestDispatcher::new(request) + .on::<DocumentLinkRequest, _>(|id, params| { + self.document_link(id, params, &token) + })? + .on::<FoldingRangeRequest, _>(|id, params| { + self.folding_range(id, params, &token) + })? + .on::<References, _>(|id, params| self.references(id, params, &token))? + .on::<HoverRequest, _>(|id, params| self.hover(id, params, &token))? + .on::<DocumentSymbolRequest, _>(|id, params| { + self.document_symbols(id, params, &token) + })? + .on::<WorkspaceSymbol, _>(|id, params| { + self.workspace_symbols(id, params, &token) + })? + .on::<Completion, _>(|id, params| { + #[cfg(feature = "completion")] + self.completion(id, params, &token)?; + Ok(()) + })? + .on::<ResolveCompletionItem, _>(|id, params| { + #[cfg(feature = "completion")] + self.completion_resolve(id, params, &token)?; + Ok(()) + })? + .on::<GotoDefinition, _>(|id, params| { + self.goto_definition(id, params, &token) + })? + .on::<PrepareRenameRequest, _>(|id, params| { + self.prepare_rename(id, params, &token) + })? + .on::<Rename, _>(|id, params| self.rename(id, params, &token))? + .on::<DocumentHighlightRequest, _>(|id, params| { + self.document_highlight(id, params, &token) + })? + .on::<Formatting, _>(|id, params| self.formatting(id, params, &token))? + .on::<BuildRequest, _>(|id, params| self.build(id, params, &token))? + .on::<ForwardSearchRequest, _>(|id, params| { + self.forward_search(id, params, &token) + })? + .on::<SemanticTokensRangeRequest, _>(|id, params| { + self.semantic_tokens_range(id, params, &token) + })? + .default() + { + self.connection.sender.send(response.into())?; } } - Action::RunLinter(uri, reason) => { - let options = self - .config_manager() - .get() - .await - .latex - .and_then(|opts| opts.lint) - .unwrap_or_default(); - - let should_lint = match reason { - LintReason::Change => options.on_change(), - LintReason::Save => options.on_save() || options.on_change(), + Message::Notification(notification) => { + NotificationDispatcher::new(notification) + .on::<Cancel, _>(|params| self.cancel(params))? + .on::<DidChangeConfiguration, _>(|params| { + self.did_change_configuration(params) + })? + .on::<DidOpenTextDocument, _>(|params| self.did_open(params))? + .on::<DidChangeTextDocument, _>(|params| self.did_change(params))? + .on::<DidSaveTextDocument, _>(|params| self.did_save(params))? + .on::<DidCloseTextDocument, _>(|params| self.did_close(params))? + .default(); + } + Message::Response(response) => { + let mut req_queue = self.req_queue.lock().unwrap(); + let data = req_queue.outgoing.complete(response.id); + let result = match response.result { + Some(result) => Ok(result), + None => Err(response + .error + .expect("response without result or error received")), }; - - if should_lint { - let snapshot = self.workspace.get().await; - if let Some(doc) = snapshot.find(&uri) { - if let DocumentContent::Latex(_) = &doc.content { - self.diagnostics_manager.latex.update(&uri, &doc.text).await; - } - } - } + data.sender.send(result)?; } } } + Ok(()) + } + + pub fn run(mut self) -> Result<()> { + self.initialize()?; + self.process_messages()?; + drop(self.static_debouncer); + drop(self.chktex_debouncer); + self.pool.join(); + Ok(()) } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -enum LintReason { - Change, - Save, +fn create_static_debouncer( + manager: Arc<Mutex<DiagnosticsManager>>, + conn: &Connection, + context: Arc<ServerContext>, +) -> DiagnosticsDebouncer { + let sender = conn.sender.clone(); + DiagnosticsDebouncer::launch(context, move |workspace, document| { + let mut manager = manager.lock().unwrap(); + manager.update_static(workspace.as_ref(), Arc::clone(&document.uri)); + if let Err(why) = publish_diagnostics(&sender, workspace.as_ref(), &manager) { + warn!("Failed to publish diagnostics: {}", why); + } + }) } -#[derive(Debug, PartialEq, Clone)] -enum Action { - LoadDistribution, - RegisterCapabilities, - PullConfiguration, - DetectRoot(Uri), - PublishDiagnostics, - Build(Uri), - RunLinter(Uri, LintReason), +fn create_chktex_debouncer( + manager: Arc<Mutex<DiagnosticsManager>>, + conn: &Connection, + context: Arc<ServerContext>, +) -> DiagnosticsDebouncer { + let sender = conn.sender.clone(); + DiagnosticsDebouncer::launch(Arc::clone(&context), move |workspace, document| { + let options = { context.options.read().unwrap().clone() }; + let mut manager = manager.lock().unwrap(); + manager.update_chktex(workspace.as_ref(), Arc::clone(&document.uri), &options); + if let Err(why) = publish_diagnostics(&sender, workspace.as_ref(), &manager) { + warn!("Failed to publish diagnostics: {}", why); + } + }) } -#[derive(Debug, Default)] -struct ActionManager { - actions: Mutex<Vec<Action>>, +fn publish_diagnostics( + sender: &Sender<lsp_server::Message>, + workspace: &dyn Workspace, + diag_manager: &DiagnosticsManager, +) -> Result<()> { + for document in workspace.documents() { + let diagnostics = diag_manager.publish(Arc::clone(&document.uri)); + send_notification::<PublishDiagnostics>( + sender, + PublishDiagnosticsParams { + uri: document.uri.as_ref().clone().into(), + version: None, + diagnostics, + }, + )?; + } + Ok(()) } -impl ActionManager { - pub async fn push(&self, action: Action) { - let mut actions = self.actions.lock().await; - actions.push(action); - } +fn cancel_response(id: RequestId) -> lsp_server::Response { + lsp_server::Response::new_err( + id, + ErrorCode::RequestCanceled as i32, + "canceled by client".to_string(), + ) +} - pub async fn take(&self) -> Vec<Action> { - let mut actions = self.actions.lock().await; - mem::replace(&mut *actions, Vec::new()) - } +struct BuildRequest; + +impl lsp_types::request::Request for BuildRequest { + type Params = BuildParams; + + type Result = BuildResult; + + const METHOD: &'static str = "textDocument/build"; +} + +struct ForwardSearchRequest; + +impl lsp_types::request::Request for ForwardSearchRequest { + type Params = TextDocumentPositionParams; + + type Result = ForwardSearchResult; + + const METHOD: &'static str = "textDocument/forwardSearch"; } diff --git a/support/texlab/src/symbol/bibtex_entry.rs b/support/texlab/src/symbol/bibtex_entry.rs deleted file mode 100644 index f719937e80..0000000000 --- a/support/texlab/src/symbol/bibtex_entry.rs +++ /dev/null @@ -1,153 +0,0 @@ -use super::types::{LatexSymbol, LatexSymbolKind}; -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::DocumentSymbolParams, - syntax::{bibtex, BibtexEntryTypeCategory, SyntaxNode, LANGUAGE_DATA}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use petgraph::graph::NodeIndex; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexEntrySymbolProvider; - -#[async_trait] -impl FeatureProvider for BibtexEntrySymbolProvider { - type Params = DocumentSymbolParams; - type Output = Vec<LatexSymbol>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut symbols = Vec::new(); - if let DocumentContent::Bibtex(tree) = &req.current().content { - for entry_node in tree.children(tree.root) { - if let Some(entry) = tree - .as_entry(entry_node) - .filter(|entry| !entry.is_comment()) - .filter(|entry| entry.key.is_some()) - { - let category = LANGUAGE_DATA - .find_entry_type(&entry.ty.text()[1..]) - .map(|ty| ty.category) - .unwrap_or(BibtexEntryTypeCategory::Misc); - - let key = entry.key.as_ref().unwrap(); - let symbol = LatexSymbol { - name: key.text().to_owned(), - label: None, - kind: LatexSymbolKind::Entry(category), - deprecated: false, - full_range: entry.range(), - selection_range: key.range(), - children: Self::field_symbols(tree, entry_node), - }; - symbols.push(symbol); - } - } - } - symbols - } -} - -impl BibtexEntrySymbolProvider { - fn field_symbols(tree: &bibtex::Tree, entry_node: NodeIndex) -> Vec<LatexSymbol> { - let mut symbols = Vec::new(); - for field in tree - .children(entry_node) - .filter_map(|node| tree.as_field(node)) - { - let symbol = LatexSymbol { - name: field.name.text().to_owned(), - label: None, - kind: LatexSymbolKind::Field, - deprecated: false, - full_range: field.range(), - selection_range: field.name.range(), - children: Vec::new(), - }; - symbols.push(symbol); - } - symbols - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - - #[tokio::test] - async fn empty_latex_document() { - let actual_symbols = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .test_symbol(BibtexEntrySymbolProvider) - .await; - - assert!(actual_symbols.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_symbols = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .test_symbol(BibtexEntrySymbolProvider) - .await; - - assert!(actual_symbols.is_empty()); - } - - #[tokio::test] - async fn entry() { - let actual_symbols = FeatureTester::new() - .file("main.bib", r#"@article{key, foo = bar, baz = qux}"#) - .main("main.bib") - .test_symbol(BibtexEntrySymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "key".into(), - label: None, - kind: LatexSymbolKind::Entry(BibtexEntryTypeCategory::Article), - deprecated: false, - full_range: Range::new_simple(0, 0, 0, 35), - selection_range: Range::new_simple(0, 9, 0, 12), - children: vec![ - LatexSymbol { - name: "foo".into(), - label: None, - kind: LatexSymbolKind::Field, - deprecated: false, - full_range: Range::new_simple(0, 14, 0, 24), - selection_range: Range::new_simple(0, 14, 0, 17), - children: Vec::new(), - }, - LatexSymbol { - name: "baz".into(), - label: None, - kind: LatexSymbolKind::Field, - deprecated: false, - full_range: Range::new_simple(0, 25, 0, 34), - selection_range: Range::new_simple(0, 25, 0, 28), - children: Vec::new(), - }, - ], - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn comment() { - let actual_symbols = FeatureTester::new() - .file("main.bib", r#"@comment{key, foo = bar, baz = qux}"#) - .main("main.bib") - .test_symbol(BibtexEntrySymbolProvider) - .await; - - assert!(actual_symbols.is_empty()); - } -} diff --git a/support/texlab/src/symbol/bibtex_string.rs b/support/texlab/src/symbol/bibtex_string.rs deleted file mode 100644 index d2283f640b..0000000000 --- a/support/texlab/src/symbol/bibtex_string.rs +++ /dev/null @@ -1,102 +0,0 @@ -use super::types::{LatexSymbol, LatexSymbolKind}; -use crate::{ - feature::{FeatureProvider, FeatureRequest}, - protocol::DocumentSymbolParams, - syntax::SyntaxNode, - workspace::DocumentContent, -}; -use async_trait::async_trait; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct BibtexStringSymbolProvider; - -#[async_trait] -impl FeatureProvider for BibtexStringSymbolProvider { - type Params = DocumentSymbolParams; - type Output = Vec<LatexSymbol>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut symbols = Vec::new(); - if let DocumentContent::Bibtex(tree) = &req.current().content { - for string_node in tree.children(tree.root) { - if let Some(string) = &tree.as_string(string_node) { - if let Some(name) = &string.name { - symbols.push(LatexSymbol { - name: name.text().into(), - label: None, - kind: LatexSymbolKind::String, - deprecated: false, - full_range: string.range(), - selection_range: name.range(), - children: Vec::new(), - }); - } - } - } - } - symbols - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - feature::FeatureTester, - protocol::{Range, RangeExt}, - }; - - #[tokio::test] - async fn empty_latex_document() { - let actual_symbols = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .test_symbol(BibtexStringSymbolProvider) - .await; - - assert!(actual_symbols.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_symbols = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .test_symbol(BibtexStringSymbolProvider) - .await; - - assert!(actual_symbols.is_empty()); - } - - #[tokio::test] - async fn valid() { - let actual_symbols = FeatureTester::new() - .file("main.bib", r#"@string{key = "value"}"#) - .main("main.bib") - .test_symbol(BibtexStringSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "key".into(), - label: None, - kind: LatexSymbolKind::String, - deprecated: false, - full_range: Range::new_simple(0, 0, 0, 22), - selection_range: Range::new_simple(0, 8, 0, 11), - children: Vec::new(), - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn invalid() { - let actual_symbols = FeatureTester::new() - .file("main.bib", r#"@string{}"#) - .main("main.bib") - .test_symbol(BibtexStringSymbolProvider) - .await; - - assert!(actual_symbols.is_empty()); - } -} diff --git a/support/texlab/src/symbol/latex_section/enumeration.rs b/support/texlab/src/symbol/latex_section/enumeration.rs deleted file mode 100644 index e96a856eac..0000000000 --- a/support/texlab/src/symbol/latex_section/enumeration.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::{label_name, selection_range}; -use crate::{ - feature::DocumentView, - outline::OutlineContext, - protocol::{Range, RangeExt}, - symbol::types::{LatexSymbol, LatexSymbolKind}, - syntax::{latex, SyntaxNode}, -}; -use titlecase::titlecase; - -pub fn symbols(view: &DocumentView, table: &latex::SymbolTable) -> Vec<LatexSymbol> { - table - .environments - .iter() - .filter(|env| env.left.is_enum(&table)) - .map(|enumeration| make_symbol(view, table, *enumeration)) - .collect() -} - -fn make_symbol( - view: &DocumentView, - table: &latex::SymbolTable, - enumeration: latex::Environment, -) -> LatexSymbol { - let name = titlecase(enumeration.left.name(&table).unwrap().text()); - - let items: Vec<_> = table - .items - .iter() - .filter(|item| table.is_enum_item(enumeration, **item)) - .collect(); - - let mut children = Vec::new(); - for i in 0..items.len() { - let start = table[items[i].parent].start(); - let end = items - .get(i + 1) - .map(|item| table[item.parent].start()) - .unwrap_or_else(|| table[enumeration.right.parent].start()); - let range = Range::new(start, end); - - let label = find_item_label(table, range); - - let number = items[i] - .name(&table) - .or_else(|| label.and_then(|label| OutlineContext::find_number(view, table, *label))); - - let name = number.unwrap_or_else(|| "Item".into()); - children.push(LatexSymbol { - name, - label: label_name(table, label), - kind: LatexSymbolKind::EnumerationItem, - deprecated: false, - full_range: range, - selection_range: selection_range(table, table[items[i].parent].range(), label), - children: Vec::new(), - }); - } - - LatexSymbol { - name, - label: None, - kind: LatexSymbolKind::Enumeration, - deprecated: false, - full_range: enumeration.range(&table), - selection_range: enumeration.range(&table), - children, - } -} - -fn find_item_label(table: &latex::SymbolTable, item_range: Range) -> Option<&latex::Label> { - table.find_label_by_range(item_range).filter(|label| { - table - .environments - .iter() - .filter(|env| item_range.contains(env.range(&table).start)) - .all(|env| !env.range(&table).contains(table[label.parent].start())) - }) -} diff --git a/support/texlab/src/symbol/latex_section/equation.rs b/support/texlab/src/symbol/latex_section/equation.rs deleted file mode 100644 index 2889aefd4d..0000000000 --- a/support/texlab/src/symbol/latex_section/equation.rs +++ /dev/null @@ -1,41 +0,0 @@ -use super::{label_name, selection_range}; -use crate::{ - feature::DocumentView, - outline::OutlineContext, - protocol::Range, - symbol::types::{LatexSymbol, LatexSymbolKind}, - syntax::latex, -}; - -pub fn symbols(view: &DocumentView, table: &latex::SymbolTable) -> Vec<LatexSymbol> { - let mut symbols = Vec::new(); - for equation in &table.equations { - symbols.push(make_symbol(view, table, equation.range(&table))); - } - - for equation in &table.environments { - if equation.left.is_math(&table) { - symbols.push(make_symbol(view, table, equation.range(&table))); - } - } - symbols -} - -fn make_symbol(view: &DocumentView, table: &latex::SymbolTable, full_range: Range) -> LatexSymbol { - let label = table.find_label_by_range(full_range); - - let name = match label.and_then(|label| OutlineContext::find_number(view, table, *label)) { - Some(num) => format!("Equation ({})", num), - None => "Equation".to_owned(), - }; - - LatexSymbol { - name, - label: label_name(table, label), - kind: LatexSymbolKind::Equation, - deprecated: false, - full_range, - selection_range: selection_range(table, full_range, label), - children: Vec::new(), - } -} diff --git a/support/texlab/src/symbol/latex_section/float.rs b/support/texlab/src/symbol/latex_section/float.rs deleted file mode 100644 index 7ade3602cf..0000000000 --- a/support/texlab/src/symbol/latex_section/float.rs +++ /dev/null @@ -1,59 +0,0 @@ -use super::{label_name, selection_range}; -use crate::{ - feature::DocumentView, - outline::{OutlineCaptionKind, OutlineContext}, - symbol::types::{LatexSymbol, LatexSymbolKind}, - syntax::{latex, SyntaxNode}, -}; - -pub fn symbols(view: &DocumentView, table: &latex::SymbolTable) -> Vec<LatexSymbol> { - table - .captions - .iter() - .filter_map(|caption| make_symbol(view, table, *caption)) - .collect() -} - -fn make_symbol( - view: &DocumentView, - table: &latex::SymbolTable, - caption: latex::Caption, -) -> Option<LatexSymbol> { - let env = table - .environments - .iter() - .find(|env| table.is_direct_child(**env, table[caption.parent].start()))?; - - let text = - table.print_group_content(caption.parent, latex::GroupKind::Group, caption.arg_index)?; - - let kind = env - .left - .name(&table) - .map(latex::Token::text) - .and_then(OutlineCaptionKind::parse)?; - - let label = table.find_label_by_environment(*env); - let number = label.and_then(|label| OutlineContext::find_number(view, table, *label)); - - let name = match &number { - Some(number) => format!("{} {}: {}", kind.as_str(), number, text), - None => format!("{}: {}", kind.as_str(), text), - }; - - let symbol = LatexSymbol { - name, - label: label_name(table, label), - kind: match kind { - OutlineCaptionKind::Figure => LatexSymbolKind::Figure, - OutlineCaptionKind::Table => LatexSymbolKind::Table, - OutlineCaptionKind::Listing => LatexSymbolKind::Listing, - OutlineCaptionKind::Algorithm => LatexSymbolKind::Algorithm, - }, - deprecated: false, - full_range: env.range(&table), - selection_range: selection_range(table, env.range(&table), label), - children: Vec::new(), - }; - Some(symbol) -} diff --git a/support/texlab/src/symbol/latex_section/mod.rs b/support/texlab/src/symbol/latex_section/mod.rs deleted file mode 100644 index fc81695b50..0000000000 --- a/support/texlab/src/symbol/latex_section/mod.rs +++ /dev/null @@ -1,671 +0,0 @@ -mod enumeration; -mod equation; -mod float; -mod theorem; - -use super::types::{LatexSymbol, LatexSymbolKind}; -use crate::{ - feature::{DocumentView, FeatureProvider, FeatureRequest}, - outline::{Outline, OutlineContext, OutlineContextItem}, - protocol::{DocumentSymbolParams, Options, Position, Range, RangeExt}, - syntax::{latex, CharStream, LatexLabelKind, SyntaxNode}, - workspace::DocumentContent, -}; -use async_trait::async_trait; -use std::path::Path; - -fn label_name(table: &latex::SymbolTable, label: Option<&latex::Label>) -> Option<String> { - label.map(|label| label.names(&table)[0].text().to_owned()) -} - -fn selection_range( - table: &latex::SymbolTable, - full_range: Range, - label: Option<&latex::Label>, -) -> Range { - label - .map(|label| table[label.parent].range()) - .unwrap_or(full_range) -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] -pub struct LatexSectionSymbolProvider; - -#[async_trait] -impl FeatureProvider for LatexSectionSymbolProvider { - type Params = DocumentSymbolParams; - type Output = Vec<LatexSymbol>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - let mut symbols = Vec::new(); - if let DocumentContent::Latex(table) = &req.current().content { - let mut section_tree = - build_section_tree(&req.view, table, &req.options, &req.current_dir); - for symbol in enumeration::symbols(&req.view, table) { - section_tree.insert_symbol(&symbol); - } - - for symbol in equation::symbols(&req.view, table) { - section_tree.insert_symbol(&symbol); - } - - for symbol in float::symbols(&req.view, table) { - section_tree.insert_symbol(&symbol); - } - - for symbol in theorem::symbols(&req.view, table) { - section_tree.insert_symbol(&symbol); - } - - for symbol in section_tree.symbols { - symbols.push(symbol); - } - - for child in section_tree.children { - symbols.push(child.into()); - } - } - symbols - } -} - -pub fn build_section_tree<'a>( - view: &'a DocumentView, - table: &'a latex::SymbolTable, - options: &'a Options, - current_dir: &'a Path, -) -> LatexSectionTree<'a> { - let mut section_tree = LatexSectionTree::from(table); - section_tree.set_full_text(&view.current.text); - let end_position = compute_end_position(table, &view.current.text); - LatexSectionNode::set_full_range(&mut section_tree.children, table, end_position); - let outline = Outline::analyze(view, options, current_dir); - for child in &mut section_tree.children { - child.set_label(view, &outline); - } - section_tree -} - -fn compute_end_position(table: &latex::SymbolTable, text: &str) -> Position { - let mut stream = CharStream::new(text); - while stream.next().is_some() {} - table - .environments - .iter() - .find(|env| env.left.name(&table).map(latex::Token::text) == Some("document")) - .map(|env| table[env.right.parent].start()) - .unwrap_or(stream.current_position) -} - -#[derive(Debug, Clone)] -pub struct LatexSectionNode<'a> { - pub table: &'a latex::SymbolTable, - pub section: &'a latex::Section, - pub full_range: Range, - full_text: &'a str, - label: Option<String>, - number: Option<String>, - symbols: Vec<LatexSymbol>, - children: Vec<Self>, -} - -impl<'a> LatexSectionNode<'a> { - fn new(table: &'a latex::SymbolTable, section: &'a latex::Section) -> Self { - Self { - table, - section, - full_range: Range::default(), - full_text: "", - label: None, - number: None, - symbols: Vec::new(), - children: Vec::new(), - } - } - - fn set_full_text(&mut self, full_text: &'a str) { - self.full_text = full_text; - for child in &mut self.children { - child.set_full_text(full_text); - } - } - - fn name(&self) -> String { - self.table - .print_group_content( - self.section.parent, - latex::GroupKind::Group, - self.section.arg_index, - ) - .unwrap_or_else(|| "Unknown".into()) - } - - fn set_full_range( - children: &mut Vec<Self>, - table: &latex::SymbolTable, - end_position: Position, - ) { - for i in 0..children.len() { - let current_end = children - .get(i + 1) - .map(|next| table[next.section.parent].start()) - .unwrap_or(end_position); - - let mut current = &mut children[i]; - current.full_range = Range::new(table[current.section.parent].start(), current_end); - Self::set_full_range(&mut current.children, table, current_end); - } - } - - fn set_label(&mut self, view: &DocumentView, outline: &Outline) { - if let Some(label) = self - .table - .labels - .iter() - .filter(|label| label.kind == LatexLabelKind::Definition) - .find(|label| self.full_range.contains(self.table[label.parent].start())) - { - if let Some(ctx) = OutlineContext::parse(view, outline, *label) { - let mut is_section = false; - if let OutlineContextItem::Section { text, .. } = &ctx.item { - if self.name() == *text { - for name in label.names(&self.table) { - self.label = Some(name.text().to_owned()); - } - - is_section = true; - } - } - - if is_section { - self.number = ctx.number; - } - } - } - - for child in &mut self.children { - child.set_label(view, outline); - } - } - - fn insert_section( - nodes: &mut Vec<Self>, - table: &'a latex::SymbolTable, - section: &'a latex::Section, - ) { - match nodes.last_mut() { - Some(parent) => { - if parent.section.level < section.level { - Self::insert_section(&mut parent.children, table, section); - } else { - nodes.push(LatexSectionNode::new(table, section)); - } - } - None => { - nodes.push(LatexSectionNode::new(table, section)); - } - } - } - - fn insert_symbol(&mut self, symbol: &LatexSymbol) -> bool { - if !self.full_range.contains(symbol.selection_range.start) { - return false; - } - - for child in &mut self.children { - if child.insert_symbol(symbol) { - return true; - } - } - - self.symbols.push(symbol.clone()); - true - } - - fn find(&self, label: &str) -> Option<&Self> { - if self.label.as_ref().map(AsRef::as_ref) == Some(label) { - Some(self) - } else { - for child in &self.children { - let result = child.find(label); - if result.is_some() { - return result; - } - } - None - } - } -} - -impl<'a> Into<LatexSymbol> for LatexSectionNode<'a> { - fn into(self) -> LatexSymbol { - let name = self.name(); - - let mut children: Vec<LatexSymbol> = self.children.into_iter().map(Into::into).collect(); - - for symbol in self.symbols { - children.push(symbol); - } - - let full_name = match &self.number { - Some(number) => format!("{} {}", number, name), - None => name, - }; - - LatexSymbol { - name: full_name, - label: self.label, - kind: LatexSymbolKind::Section, - deprecated: false, - full_range: self.full_range, - selection_range: self.table[self.section.parent].range(), - children, - } - } -} - -#[derive(Debug, Clone)] -pub struct LatexSectionTree<'a> { - symbols: Vec<LatexSymbol>, - children: Vec<LatexSectionNode<'a>>, -} - -impl<'a> LatexSectionTree<'a> { - fn new() -> Self { - Self { - symbols: Vec::new(), - children: Vec::new(), - } - } - - fn set_full_text(&mut self, full_text: &'a str) { - for child in &mut self.children { - child.set_full_text(full_text); - } - } - - fn insert_symbol(&mut self, symbol: &LatexSymbol) { - for child in &mut self.children { - if child.insert_symbol(symbol) { - return; - } - } - self.symbols.push(symbol.clone()); - } - - pub fn find(&self, label: &str) -> Option<&LatexSectionNode<'a>> { - for child in &self.children { - let result = child.find(label); - if result.is_some() { - return result; - } - } - None - } -} - -impl<'a> From<&'a latex::SymbolTable> for LatexSectionTree<'a> { - fn from(table: &'a latex::SymbolTable) -> Self { - let mut root = Self::new(); - for section in &table.sections { - LatexSectionNode::insert_section(&mut root.children, table, section); - } - root - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::feature::FeatureTester; - use indoc::indoc; - - #[tokio::test] - async fn empty_latex_document() { - let actual_symbols = FeatureTester::new() - .file("main.tex", "") - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - assert!(actual_symbols.is_empty()); - } - - #[tokio::test] - async fn empty_bibtex_document() { - let actual_symbols = FeatureTester::new() - .file("main.bib", "") - .main("main.bib") - .test_symbol(LatexSectionSymbolProvider) - .await; - - assert!(actual_symbols.is_empty()); - } - - #[tokio::test] - async fn subsection() { - let actual_symbols = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \section{Foo} - \subsection{Bar}\label{sec:bar} - \subsection{Baz} - \section{Qux} - "# - ), - ) - .file( - "main.aux", - r#"\newlabel{sec:bar}{{\relax 2.1}{4}{Bar\relax }{figure.caption.4}{}}"#, - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![ - LatexSymbol { - name: "Foo".into(), - label: None, - kind: LatexSymbolKind::Section, - deprecated: false, - full_range: Range::new_simple(0, 0, 3, 0), - selection_range: Range::new_simple(0, 0, 0, 13), - children: vec![ - LatexSymbol { - name: "2.1 Bar".into(), - label: Some("sec:bar".into()), - kind: LatexSymbolKind::Section, - deprecated: false, - full_range: Range::new_simple(1, 0, 2, 0), - selection_range: Range::new_simple(1, 0, 1, 16), - children: Vec::new(), - }, - LatexSymbol { - name: "Baz".into(), - label: None, - kind: LatexSymbolKind::Section, - deprecated: false, - full_range: Range::new_simple(2, 0, 3, 0), - selection_range: Range::new_simple(2, 0, 2, 16), - children: Vec::new(), - }, - ], - }, - LatexSymbol { - name: "Qux".into(), - label: None, - kind: LatexSymbolKind::Section, - deprecated: false, - full_range: Range::new_simple(3, 0, 3, 13), - selection_range: Range::new_simple(3, 0, 3, 13), - children: Vec::new(), - }, - ]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn section_inside_document_environment() { - let actual_symbols = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \begin{document}\section{Foo}\relax - \end{document} - "# - ), - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "Foo".into(), - label: None, - kind: LatexSymbolKind::Section, - deprecated: false, - full_range: Range::new_simple(0, 16, 1, 0), - selection_range: Range::new_simple(0, 16, 0, 29), - children: Vec::new(), - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn enumeration() { - let actual_symbols = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \section{Foo} - \begin{enumerate} - \end{enumerate} - "# - ), - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "Foo".into(), - label: None, - kind: LatexSymbolKind::Section, - deprecated: false, - full_range: Range::new_simple(0, 0, 2, 15), - selection_range: Range::new_simple(0, 0, 0, 13), - children: vec![LatexSymbol { - name: "Enumerate".into(), - label: None, - kind: LatexSymbolKind::Enumeration, - deprecated: false, - full_range: Range::new_simple(1, 0, 2, 15), - selection_range: Range::new_simple(1, 0, 2, 15), - children: Vec::new(), - }], - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn equation() { - let actual_symbols = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \[Foo\] - \begin{equation}\label{eq:foo}\end{equation} - "# - ), - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![ - LatexSymbol { - name: "Equation".into(), - label: None, - kind: LatexSymbolKind::Equation, - deprecated: false, - full_range: Range::new_simple(0, 0, 0, 7), - selection_range: Range::new_simple(0, 0, 0, 7), - children: Vec::new(), - }, - LatexSymbol { - name: "Equation".into(), - label: Some("eq:foo".into()), - kind: LatexSymbolKind::Equation, - deprecated: false, - full_range: Range::new_simple(1, 0, 1, 44), - selection_range: Range::new_simple(1, 16, 1, 30), - children: Vec::new(), - }, - ]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn equation_number() { - let actual_symbols = FeatureTester::new() - .file("main.tex", r#"\[\label{eq:foo}\]"#) - .file( - "main.aux", - r#"\newlabel{eq:foo}{{\relax 2.1}{4}{Bar\relax }{figure.caption.4}{}}"#, - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "Equation (2.1)".into(), - label: Some("eq:foo".into()), - kind: LatexSymbolKind::Equation, - deprecated: false, - full_range: Range::new_simple(0, 0, 0, 18), - selection_range: Range::new_simple(0, 2, 0, 16), - children: Vec::new(), - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn table() { - let actual_symbols = FeatureTester::new() - .file("main.tex", r#"\begin{table}\caption{Foo}\end{table}"#) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "Table: Foo".into(), - label: None, - kind: LatexSymbolKind::Table, - deprecated: false, - full_range: Range::new_simple(0, 0, 0, 37), - selection_range: Range::new_simple(0, 0, 0, 37), - children: Vec::new(), - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn figure_number() { - let actual_symbols = FeatureTester::new() - .file( - "main.tex", - r#"\begin{figure}\caption{Foo}\label{fig:foo}\end{figure}"#, - ) - .file( - "main.aux", - r#"\newlabel{fig:foo}{{\relax 2.1}{4}{Bar\relax }{figure.caption.4}{}}"#, - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "Figure 2.1: Foo".into(), - label: Some("fig:foo".into()), - kind: LatexSymbolKind::Figure, - deprecated: false, - full_range: Range::new_simple(0, 0, 0, 54), - selection_range: Range::new_simple(0, 27, 0, 42), - children: Vec::new(), - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn lemma() { - let actual_symbols = FeatureTester::new() - .file( - "main.tex", - r#"\newtheorem{lemma}{Lemma}\begin{lemma}\end{lemma}"#, - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "Lemma".into(), - label: None, - kind: LatexSymbolKind::Theorem, - deprecated: false, - full_range: Range::new_simple(0, 25, 0, 49), - selection_range: Range::new_simple(0, 25, 0, 49), - children: Vec::new(), - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn lemma_number() { - let actual_symbols = FeatureTester::new() - .file( - "main.tex", - indoc!( - r#" - \newtheorem{lemma}{Lemma} - \begin{lemma}\label{thm:foo}\end{lemma} - "# - ), - ) - .file( - "main.aux", - r#"\newlabel{thm:foo}{{\relax 2.1}{4}{Bar\relax }{figure.caption.4}{}}"#, - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "Lemma 2.1".into(), - label: Some("thm:foo".into()), - kind: LatexSymbolKind::Theorem, - deprecated: false, - full_range: Range::new_simple(1, 0, 1, 39), - selection_range: Range::new_simple(1, 13, 1, 28), - children: Vec::new(), - }]; - - assert_eq!(actual_symbols, expected_symbols); - } - - #[tokio::test] - async fn lemma_description() { - let actual_symbols = FeatureTester::new() - .file( - "main.tex", - r#"\newtheorem{lemma}{Lemma}\begin{lemma}[Foo]\end{lemma}"#, - ) - .main("main.tex") - .test_symbol(LatexSectionSymbolProvider) - .await; - - let expected_symbols = vec![LatexSymbol { - name: "Lemma (Foo)".into(), - label: None, - kind: LatexSymbolKind::Theorem, - deprecated: false, - full_range: Range::new_simple(0, 25, 0, 54), - selection_range: Range::new_simple(0, 25, 0, 54), - children: Vec::new(), - }]; - - assert_eq!(actual_symbols, expected_symbols); - } -} diff --git a/support/texlab/src/symbol/latex_section/theorem.rs b/support/texlab/src/symbol/latex_section/theorem.rs deleted file mode 100644 index dac231fbfa..0000000000 --- a/support/texlab/src/symbol/latex_section/theorem.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::{label_name, selection_range}; -use crate::{ - feature::DocumentView, - outline::OutlineContext, - symbol::types::{LatexSymbol, LatexSymbolKind}, - syntax::latex, - workspace::DocumentContent, -}; -use titlecase::titlecase; - -pub fn symbols(view: &DocumentView, table: &latex::SymbolTable) -> Vec<LatexSymbol> { - table - .environments - .iter() - .filter_map(|env| make_symbol(view, table, *env)) - .collect() -} - -fn make_symbol( - view: &DocumentView, - main_table: &latex::SymbolTable, - env: latex::Environment, -) -> Option<LatexSymbol> { - let env_name = env.left.name(&main_table).map(latex::Token::text)?; - - for document in &view.related { - if let DocumentContent::Latex(table) = &document.content { - for definition in &table.theorem_definitions { - if definition.name(&table).text() == env_name { - let kind = table - .print_group_content( - definition.parent, - latex::GroupKind::Group, - definition.arg_index + 1, - ) - .unwrap_or_else(|| titlecase(env_name)); - - let desc = main_table.print_group_content( - env.left.parent, - latex::GroupKind::Options, - 0, - ); - - let label = main_table.find_label_by_environment(env); - let number = label - .and_then(|label| OutlineContext::find_number(view, &main_table, *label)); - - let name = match (desc, number) { - (Some(desc), Some(num)) => format!("{} {} ({})", kind, num, desc), - (Some(desc), None) => format!("{} ({})", kind, desc), - (None, Some(num)) => format!("{} {}", kind, num), - (None, None) => kind, - }; - - let symbol = LatexSymbol { - name, - label: label_name(main_table, label), - kind: LatexSymbolKind::Theorem, - deprecated: false, - full_range: env.range(&main_table), - selection_range: selection_range(main_table, env.range(&main_table), label), - children: Vec::new(), - }; - return Some(symbol); - } - } - } - } - None -} diff --git a/support/texlab/src/symbol/mod.rs b/support/texlab/src/symbol/mod.rs deleted file mode 100644 index b292f15bb2..0000000000 --- a/support/texlab/src/symbol/mod.rs +++ /dev/null @@ -1,178 +0,0 @@ -mod bibtex_entry; -mod bibtex_string; -mod latex_section; -mod project_order; -mod types; - -pub use self::latex_section::{build_section_tree, LatexSectionNode, LatexSectionTree}; - -use self::{ - bibtex_entry::BibtexEntrySymbolProvider, bibtex_string::BibtexStringSymbolProvider, - latex_section::LatexSectionSymbolProvider, project_order::ProjectOrdering, types::LatexSymbol, -}; -use crate::{ - feature::{ConcatProvider, DocumentView, FeatureProvider, FeatureRequest}, - protocol::{ - ClientCapabilities, ClientCapabilitiesExt, DocumentSymbolParams, DocumentSymbolResponse, - Options, PartialResultParams, SymbolInformation, TextDocumentIdentifier, Uri, - WorkDoneProgressParams, WorkspaceSymbolParams, - }, - tex::Distribution, - workspace::Snapshot, -}; -use async_trait::async_trait; -use std::{ - cmp::Reverse, - path::{Path, PathBuf}, - sync::Arc, -}; - -pub struct SymbolProvider { - provider: ConcatProvider<DocumentSymbolParams, LatexSymbol>, -} - -impl SymbolProvider { - pub fn new() -> Self { - Self { - provider: ConcatProvider::new(vec![ - Box::new(BibtexEntrySymbolProvider), - Box::new(BibtexStringSymbolProvider), - Box::new(LatexSectionSymbolProvider), - ]), - } - } -} - -impl Default for SymbolProvider { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl FeatureProvider for SymbolProvider { - type Params = DocumentSymbolParams; - type Output = Vec<LatexSymbol>; - - async fn execute<'a>(&'a self, req: &'a FeatureRequest<Self::Params>) -> Self::Output { - self.provider.execute(req).await - } -} - -pub fn document_symbols( - client_capabilities: &ClientCapabilities, - snapshot: &Snapshot, - uri: &Uri, - options: &Options, - current_dir: &Path, - symbols: Vec<LatexSymbol>, -) -> DocumentSymbolResponse { - if client_capabilities.has_hierarchical_document_symbol_support() { - DocumentSymbolResponse::Nested(symbols.into_iter().map(Into::into).collect()) - } else { - let mut buffer = Vec::new(); - for symbol in symbols { - symbol.flatten(&mut buffer); - } - let mut buffer = buffer - .into_iter() - .map(|symbol| symbol.into_symbol_info(uri.clone())) - .collect(); - sort_symbols(snapshot, options, ¤t_dir, &mut buffer); - DocumentSymbolResponse::Flat(buffer) - } -} - -struct WorkspaceSymbol { - info: SymbolInformation, - search_text: String, -} - -pub async fn workspace_symbols<'a>( - distro: Arc<dyn Distribution>, - client_capabilities: Arc<ClientCapabilities>, - snapshot: Arc<Snapshot>, - options: &'a Options, - current_dir: Arc<PathBuf>, - params: &'a WorkspaceSymbolParams, -) -> Vec<SymbolInformation> { - let provider = SymbolProvider::new(); - let mut symbols = Vec::new(); - - for doc in &snapshot.0 { - let uri: Uri = doc.uri.clone(); - let req = FeatureRequest { - params: DocumentSymbolParams { - text_document: TextDocumentIdentifier::new(uri.clone().into()), - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }, - view: DocumentView::analyze( - Arc::clone(&snapshot), - Arc::clone(&doc), - &options, - ¤t_dir, - ), - distro: distro.clone(), - client_capabilities: Arc::clone(&client_capabilities), - options: options.clone(), - current_dir: Arc::clone(¤t_dir), - }; - - let mut buffer = Vec::new(); - for symbol in provider.execute(&req).await { - symbol.flatten(&mut buffer); - } - - for symbol in buffer { - symbols.push(WorkspaceSymbol { - search_text: symbol.search_text(), - info: symbol.into_symbol_info(uri.clone()), - }); - } - } - - let query_words: Vec<_> = params - .query - .split_whitespace() - .map(str::to_lowercase) - .collect(); - let mut filtered = Vec::new(); - for symbol in symbols { - let mut included = true; - for word in &query_words { - if !symbol.search_text.contains(word) { - included = false; - break; - } - } - - if included { - filtered.push(symbol.info); - } - } - sort_symbols(&snapshot, options, ¤t_dir, &mut filtered); - filtered -} - -fn sort_symbols( - snapshot: &Snapshot, - options: &Options, - current_dir: &Path, - symbols: &mut Vec<SymbolInformation>, -) { - let ordering = ProjectOrdering::analyze(snapshot, options, current_dir); - symbols.sort_by(|left, right| { - let left_key = ( - ordering.get(&Uri::from(left.location.uri.clone())), - left.location.range.start, - Reverse(left.location.range.end), - ); - let right_key = ( - ordering.get(&Uri::from(right.location.uri.clone())), - right.location.range.start, - Reverse(right.location.range.end), - ); - left_key.cmp(&right_key) - }); -} diff --git a/support/texlab/src/symbol/project_order.rs b/support/texlab/src/symbol/project_order.rs deleted file mode 100644 index 8a2ec5f6d0..0000000000 --- a/support/texlab/src/symbol/project_order.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::{ - protocol::{Options, Uri}, - workspace::{Document, DocumentContent, Snapshot}, -}; -use petgraph::{algo::tarjan_scc, Directed, Graph}; -use std::{collections::HashSet, path::Path, sync::Arc, usize}; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct ProjectOrdering { - ordering: Vec<Arc<Document>>, -} - -impl ProjectOrdering { - pub fn get(&self, uri: &Uri) -> usize { - self.ordering - .iter() - .position(|doc| doc.uri == *uri) - .unwrap_or(usize::MAX) - } - - pub fn analyze(snapshot: &Snapshot, options: &Options, current_dir: &Path) -> Self { - let mut ordering = Vec::new(); - let comps = Self::connected_components(snapshot, options, current_dir); - for comp in comps { - let graph = Self::build_dependency_graph(&comp); - - let mut visited = HashSet::new(); - let root_index = *graph.node_weight(tarjan_scc(&graph)[0][0]).unwrap(); - let mut stack = vec![Arc::clone(&comp[root_index])]; - - while let Some(doc) = stack.pop() { - if !visited.insert(doc.uri.as_str().to_owned()) { - continue; - } - - ordering.push(Arc::clone(&doc)); - if let DocumentContent::Latex(tree) = &doc.content { - for include in tree.includes.iter().rev() { - for targets in &include.all_targets { - for target in targets { - if let Some(child) = snapshot.find(target) { - stack.push(child); - } - } - } - } - } - } - } - - Self { ordering } - } - - fn connected_components( - snapshot: &Snapshot, - options: &Options, - current_dir: &Path, - ) -> Vec<Vec<Arc<Document>>> { - let mut comps = Vec::new(); - let mut visited = HashSet::new(); - for root in &snapshot.0 { - if !visited.insert(root.uri.clone()) { - continue; - } - - let comp = snapshot.relations(&root.uri, options, current_dir); - for document in &comp { - visited.insert(document.uri.clone()); - } - comps.push(comp); - } - comps - } - - fn build_dependency_graph(docs: &[Arc<Document>]) -> Graph<usize, (), Directed> { - let mut graph = Graph::new(); - let nodes: Vec<_> = (0..docs.len()).map(|i| graph.add_node(i)).collect(); - - for (i, doc) in docs.iter().enumerate() { - if let DocumentContent::Latex(tree) = &doc.content { - for targets in tree - .includes - .iter() - .flat_map(|include| &include.all_targets) - { - for target in targets { - if let Some(j) = docs.iter().position(|doc| doc.uri == *target) { - graph.add_edge(nodes[j], nodes[i], ()); - break; - } - } - } - } - } - graph - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - tex::{Language, Resolver}, - workspace::DocumentParams, - }; - use std::env; - - fn create_simple_document(uri: &Uri, language: Language, text: &str) -> Arc<Document> { - Arc::new(Document::open(DocumentParams { - uri: uri.clone(), - text: text.into(), - language, - resolver: &Resolver::default(), - options: &Options::default(), - current_dir: &env::current_dir().unwrap(), - })) - } - - #[test] - fn no_cycles() { - let a = Uri::parse("http://example.com/a.tex").unwrap(); - let b = Uri::parse("http://example.com/b.tex").unwrap(); - let c = Uri::parse("http://example.com/c.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&a, Language::Latex, ""), - create_simple_document(&b, Language::Latex, ""), - create_simple_document(&c, Language::Latex, r#"\include{b}\include{a}"#), - ]; - - let current_dir = env::current_dir().unwrap(); - let ordering = ProjectOrdering::analyze(&snapshot, &Options::default(), ¤t_dir); - - assert_eq!(ordering.get(&a), 2); - assert_eq!(ordering.get(&b), 1); - assert_eq!(ordering.get(&c), 0); - } - - #[test] - fn cycles() { - let a = Uri::parse("http://example.com/a.tex").unwrap(); - let b = Uri::parse("http://example.com/b.tex").unwrap(); - let c = Uri::parse("http://example.com/c.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&a, Language::Latex, r#"\include{b}"#), - create_simple_document(&b, Language::Latex, r#"\include{a}"#), - create_simple_document(&c, Language::Latex, r#"\include{a}"#), - ]; - - let current_dir = env::current_dir().unwrap(); - let ordering = ProjectOrdering::analyze(&snapshot, &Options::default(), ¤t_dir); - - assert_eq!(ordering.get(&a), 1); - assert_eq!(ordering.get(&b), 2); - assert_eq!(ordering.get(&c), 0); - } - - #[test] - fn multiple_roots() { - let a = Uri::parse("http://example.com/a.tex").unwrap(); - let b = Uri::parse("http://example.com/b.tex").unwrap(); - let c = Uri::parse("http://example.com/c.tex").unwrap(); - let d = Uri::parse("http://example.com/d.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&a, Language::Latex, r#"\include{b}"#), - create_simple_document(&b, Language::Latex, ""), - create_simple_document(&c, Language::Latex, ""), - create_simple_document(&d, Language::Latex, r#"\include{c}"#), - ]; - - let current_dir = env::current_dir().unwrap(); - let ordering = ProjectOrdering::analyze(&snapshot, &Options::default(), ¤t_dir); - - assert_eq!(ordering.get(&a), 0); - assert_eq!(ordering.get(&b), 1); - assert_eq!(ordering.get(&d), 2); - assert_eq!(ordering.get(&c), 3); - } -} diff --git a/support/texlab/src/syntax.rs b/support/texlab/src/syntax.rs new file mode 100644 index 0000000000..5a906c0ea7 --- /dev/null +++ b/support/texlab/src/syntax.rs @@ -0,0 +1,17 @@ +use cstree::TextRange; + +pub mod bibtex; +pub mod build_log; +pub mod latex; + +pub trait CstNode<'a> { + type Lang: cstree::Language; + + fn cast(node: &'a cstree::ResolvedNode<Self::Lang>) -> Option<Self> + where + Self: Sized; + + fn syntax(&self) -> &'a cstree::ResolvedNode<Self::Lang>; + + fn small_range(&self) -> TextRange; +} diff --git a/support/texlab/src/syntax/bibtex.rs b/support/texlab/src/syntax/bibtex.rs new file mode 100644 index 0000000000..c6e8bb3760 --- /dev/null +++ b/support/texlab/src/syntax/bibtex.rs @@ -0,0 +1,34 @@ +mod cst; +mod kind; +mod lexer; +mod parser; + +pub use self::{ + cst::*, + kind::SyntaxKind::{self, *}, + parser::{parse, Parse}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Language {} + +impl cstree::Language for Language { + type Kind = SyntaxKind; + + fn kind_from_raw(raw: cstree::SyntaxKind) -> Self::Kind { + assert!(raw.0 <= ROOT as u16); + unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) } + } + + fn kind_to_raw(kind: Self::Kind) -> cstree::SyntaxKind { + kind.into() + } +} + +pub type SyntaxNode = cstree::ResolvedNode<Language>; + +pub type SyntaxToken = cstree::ResolvedToken<Language>; + +pub type SyntaxElement = cstree::ResolvedElement<Language>; + +pub type SyntaxElementRef<'a> = cstree::ResolvedElementRef<'a, Language>; diff --git a/support/texlab/src/syntax/bibtex/ast.rs b/support/texlab/src/syntax/bibtex/ast.rs deleted file mode 100644 index 77ee98e26d..0000000000 --- a/support/texlab/src/syntax/bibtex/ast.rs +++ /dev/null @@ -1,431 +0,0 @@ -use crate::{ - protocol::{Position, Range, RangeExt}, - syntax::{Span, SyntaxNode}, -}; -use itertools::Itertools; -use petgraph::graph::{Graph, NodeIndex}; -use serde::{Deserialize, Serialize}; -use std::fmt; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub enum TokenKind { - PreambleKind, - StringKind, - EntryKind, - Word, - Command, - Assign, - Comma, - Concat, - Quote, - BeginBrace, - EndBrace, - BeginParen, - EndParen, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Token { - pub span: Span, - pub kind: TokenKind, -} - -impl SyntaxNode for Token { - fn range(&self) -> Range { - self.span.range() - } -} - -impl Token { - pub fn new(span: Span, kind: TokenKind) -> Self { - Self { span, kind } - } - - pub fn text(&self) -> &str { - &self.span.text - } -} - -#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct Root { - pub range: Range, -} - -impl SyntaxNode for Root { - fn range(&self) -> Range { - self.range - } -} - -impl fmt::Debug for Root { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Root") - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Comment { - pub token: Token, -} - -impl SyntaxNode for Comment { - fn range(&self) -> Range { - self.token.range() - } -} - -impl fmt::Debug for Comment { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Comment({})", self.token.text()) - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Preamble { - pub range: Range, - pub ty: Token, - pub left: Option<Token>, - pub right: Option<Token>, -} - -impl SyntaxNode for Preamble { - fn range(&self) -> Range { - self.range - } -} - -impl fmt::Debug for Preamble { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Preamble") - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct String { - pub range: Range, - pub ty: Token, - pub left: Option<Token>, - pub name: Option<Token>, - pub assign: Option<Token>, - pub right: Option<Token>, -} - -impl SyntaxNode for String { - fn range(&self) -> Range { - self.range - } -} - -impl fmt::Debug for String { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "String({:?})", self.name.as_ref().map(Token::text)) - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Entry { - pub range: Range, - pub ty: Token, - pub left: Option<Token>, - pub key: Option<Token>, - pub comma: Option<Token>, - pub right: Option<Token>, -} - -impl SyntaxNode for Entry { - fn range(&self) -> Range { - self.range - } -} - -impl fmt::Debug for Entry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Entry({:?})", self.key.as_ref().map(Token::text)) - } -} - -impl Entry { - pub fn is_comment(&self) -> bool { - self.ty.text().to_lowercase() == "@comment" - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Field { - pub range: Range, - pub name: Token, - pub assign: Option<Token>, - pub comma: Option<Token>, -} - -impl SyntaxNode for Field { - fn range(&self) -> Range { - self.range - } -} - -impl fmt::Debug for Field { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Field({})", self.name.text()) - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Word { - pub token: Token, -} - -impl SyntaxNode for Word { - fn range(&self) -> Range { - self.token.range() - } -} - -impl fmt::Debug for Word { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Word({})", self.token.text()) - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Command { - pub token: Token, -} - -impl SyntaxNode for Command { - fn range(&self) -> Range { - self.token.range() - } -} - -impl fmt::Debug for Command { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Command({})", self.token.text()) - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct QuotedContent { - pub range: Range, - pub left: Token, - pub right: Option<Token>, -} - -impl SyntaxNode for QuotedContent { - fn range(&self) -> Range { - self.range - } -} - -impl fmt::Debug for QuotedContent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "QuotedContent") - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct BracedContent { - pub range: Range, - pub left: Token, - pub right: Option<Token>, -} - -impl SyntaxNode for BracedContent { - fn range(&self) -> Range { - self.range - } -} - -impl fmt::Debug for BracedContent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "BracedContent") - } -} - -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Concat { - pub range: Range, - pub operator: Token, -} - -impl SyntaxNode for Concat { - fn range(&self) -> Range { - self.range - } -} - -impl fmt::Debug for Concat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Concat") - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub enum Node { - Root(Root), - Comment(Comment), - Preamble(Box<Preamble>), - String(Box<String>), - Entry(Box<Entry>), - Field(Box<Field>), - Word(Word), - Command(Command), - QuotedContent(QuotedContent), - BracedContent(BracedContent), - Concat(Concat), -} - -impl SyntaxNode for Node { - fn range(&self) -> Range { - match self { - Self::Root(root) => root.range(), - Self::Comment(comment) => comment.range(), - Self::Preamble(preamble) => preamble.range(), - Self::String(string) => string.range(), - Self::Entry(entry) => entry.range(), - Self::Field(field) => field.range(), - Self::Word(word) => word.range(), - Self::Command(cmd) => cmd.range(), - Self::QuotedContent(content) => content.range(), - Self::BracedContent(content) => content.range(), - Self::Concat(concat) => concat.range(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Tree { - pub graph: Graph<Node, ()>, - pub root: NodeIndex, -} - -impl Tree { - pub fn children(&self, parent: NodeIndex) -> impl Iterator<Item = NodeIndex> { - self.graph - .neighbors(parent) - .sorted_by_key(|child| self.graph[*child].start()) - } - - pub fn has_children(&self, parent: NodeIndex) -> bool { - self.children(parent).next().is_some() - } - - pub fn walk<'a, V: Visitor<'a>>(&'a self, visitor: &mut V, parent: NodeIndex) { - for child in self.children(parent) { - visitor.visit(self, child); - } - } - - pub fn find(&self, pos: Position) -> Vec<NodeIndex> { - let mut finder = Finder::new(pos); - finder.visit(self, self.root); - finder.results - } - - pub fn as_preamble(&self, node: NodeIndex) -> Option<&Preamble> { - if let Node::Preamble(preamble) = &self.graph[node] { - Some(preamble) - } else { - None - } - } - - pub fn as_string(&self, node: NodeIndex) -> Option<&String> { - if let Node::String(string) = &self.graph[node] { - Some(string) - } else { - None - } - } - - pub fn as_entry(&self, node: NodeIndex) -> Option<&Entry> { - if let Node::Entry(entry) = &self.graph[node] { - Some(entry) - } else { - None - } - } - - pub fn as_field(&self, node: NodeIndex) -> Option<&Field> { - if let Node::Field(field) = &self.graph[node] { - Some(field) - } else { - None - } - } - - pub fn as_command(&self, node: NodeIndex) -> Option<&Command> { - if let Node::Command(cmd) = &self.graph[node] { - Some(cmd) - } else { - None - } - } - - pub fn as_word(&self, node: NodeIndex) -> Option<&Word> { - if let Node::Word(word) = &self.graph[node] { - Some(word) - } else { - None - } - } - - pub fn entry_by_key(&self, key: &str) -> Option<NodeIndex> { - for node in self.children(self.root) { - if let Some(entry) = self.as_entry(node) { - if entry.key.as_ref().map(Token::text) == Some(key) { - return Some(node); - } - } - } - None - } - - pub fn field_by_name(&self, parent: NodeIndex, name: &str) -> Option<NodeIndex> { - let name = name.to_lowercase(); - self.as_entry(parent)?; - for node in self.children(parent) { - if let Some(field) = self.as_field(node) { - if field.name.text() == name { - return Some(node); - } - } - } - None - } - - pub fn crossref(&self, entry: NodeIndex) -> Option<NodeIndex> { - let field = self.field_by_name(entry, "crossref")?; - let content = self.children(field).next()?; - let key = self.as_word(self.children(content).next()?)?; - self.entry_by_key(key.token.text()) - } -} - -pub trait Visitor<'a> { - fn visit(&mut self, tree: &'a Tree, node: NodeIndex); -} - -#[derive(Debug)] -struct Finder { - position: Position, - results: Vec<NodeIndex>, -} - -impl Finder { - fn new(position: Position) -> Self { - Self { - position, - results: Vec::new(), - } - } -} - -impl<'a> Visitor<'a> for Finder { - fn visit(&mut self, tree: &'a Tree, node: NodeIndex) { - if tree.graph[node].range().contains(self.position) { - self.results.push(node); - tree.walk(self, node); - } - } -} diff --git a/support/texlab/src/syntax/bibtex/cst.rs b/support/texlab/src/syntax/bibtex/cst.rs new file mode 100644 index 0000000000..7f824681dc --- /dev/null +++ b/support/texlab/src/syntax/bibtex/cst.rs @@ -0,0 +1,212 @@ +use cstree::TextRange; + +use crate::syntax::CstNode; + +use super::{Language, SyntaxKind::*, SyntaxNode, SyntaxToken}; + +macro_rules! cst_node { + ($name:ident, $($kind:pat),+) => { + #[derive(Clone)] + #[repr(transparent)] + pub struct $name<'a>(&'a SyntaxNode); + + impl<'a> CstNode<'a> for $name<'a> { + type Lang = Language; + + fn cast(node: &'a cstree::ResolvedNode<Self::Lang>) -> Option<Self> + where + Self: Sized, + { + match node.kind() { + $($kind => Some(Self(node)),)+ + _ => None, + } + } + + fn syntax(&self) -> &'a cstree::ResolvedNode<Self::Lang> { + &self.0 + } + + fn small_range(&self) -> TextRange { + let full_range = self.syntax().text_range(); + let start = full_range.start(); + let mut token = self.syntax().last_token(); + while let Some(current) = token { + if !matches!(current.kind(), WHITESPACE | JUNK) { + return TextRange::new(start, current.text_range().end()); + } + token = current.prev_token(); + } + TextRange::new(start, start) + } + } + }; +} + +pub trait HasCurly<'a>: CstNode<'a, Lang = Language> { + fn left_curly(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == L_CURLY.into()) + } + + fn right_curly(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == R_CURLY.into()) + } +} + +pub trait HasQuotes<'a>: CstNode<'a, Lang = Language> { + fn left_quote(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == QUOTE.into()) + } + + fn right_quote(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .filter(|node| node.kind() == QUOTE.into()) + .nth(1) + } +} + +pub trait HasDelimiters<'a>: CstNode<'a, Lang = Language> { + fn left_delimiter(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| matches!(node.kind(), L_CURLY | L_PAREN)) + } + + fn right_delimiter(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| matches!(node.kind(), R_CURLY | R_PAREN)) + } +} + +pub trait HasType<'a>: CstNode<'a, Lang = Language> { + fn ty(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| { + matches!( + node.kind(), + PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE + ) + }) + } +} + +cst_node!(Root, ROOT); + +cst_node!(Junk, JUNK); + +cst_node!(Comment, COMMENT); + +impl<'a> HasType<'a> for Comment<'a> {} + +cst_node!(Preamble, PREAMBLE); + +impl<'a> HasType<'a> for Preamble<'a> {} + +impl<'a> HasDelimiters<'a> for Preamble<'a> {} + +impl<'a> Preamble<'a> { + pub fn value(&self) -> Option<Value<'a>> { + self.syntax().children().find_map(Value::cast) + } +} + +cst_node!(String, STRING); + +impl<'a> HasType<'a> for String<'a> {} + +impl<'a> HasDelimiters<'a> for String<'a> {} + +impl<'a> String<'a> { + pub fn name(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == WORD) + } + + pub fn equality_sign(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == EQUALITY_SIGN) + } + + pub fn value(&self) -> Option<Value<'a>> { + self.syntax().children().find_map(Value::cast) + } +} + +cst_node!(Entry, ENTRY); + +impl<'a> HasType<'a> for Entry<'a> {} + +impl<'a> HasDelimiters<'a> for Entry<'a> {} + +impl<'a> Entry<'a> { + pub fn key(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == WORD) + } + + pub fn fields(&self) -> impl Iterator<Item = Field<'a>> { + self.syntax().children().filter_map(Field::cast) + } +} + +cst_node!(Field, FIELD); + +impl<'a> Field<'a> { + pub fn name(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == WORD) + } + + pub fn equality_sign(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == EQUALITY_SIGN) + } + + pub fn value(&self) -> Option<Value<'a>> { + self.syntax().children().find_map(Value::cast) + } +} + +cst_node!(Value, VALUE); + +impl<'a> Value<'a> { + pub fn tokens(&self) -> impl Iterator<Item = Token<'a>> { + self.syntax().children().filter_map(Token::cast) + } +} + +cst_node!(Token, TOKEN); + +cst_node!(BraceGroup, BRACE_GROUP); + +impl<'a> HasCurly<'a> for BraceGroup<'a> {} + +cst_node!(QuoteGroup, QUOTE_GROUP); + +impl<'a> HasQuotes<'a> for QuoteGroup<'a> {} diff --git a/support/texlab/src/syntax/bibtex/formatter.rs b/support/texlab/src/syntax/bibtex/formatter.rs deleted file mode 100644 index d4965e7cd4..0000000000 --- a/support/texlab/src/syntax/bibtex/formatter.rs +++ /dev/null @@ -1,329 +0,0 @@ -use super::ast::*; -use crate::{protocol::BibtexFormattingOptions, syntax::text::SyntaxNode}; -use petgraph::graph::NodeIndex; -use std::{i32, string::String as StdString}; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct FormattingParams<'a> { - pub tab_size: usize, - pub insert_spaces: bool, - pub options: &'a BibtexFormattingOptions, -} - -impl<'a> FormattingParams<'a> { - fn line_length(self) -> i32 { - let line_length = self.options.line_length.unwrap_or(120); - if line_length <= 0 { - i32::MAX - } else { - line_length - } - } - - fn indent(self) -> StdString { - if self.insert_spaces { - let mut buffer = StdString::new(); - for _ in 0..self.tab_size { - buffer.push(' '); - } - buffer - } else { - "\t".into() - } - } -} - -#[derive(Debug, Clone)] -struct Formatter<'a> { - params: FormattingParams<'a>, - indent: StdString, - output: StdString, - align: Vec<usize>, -} - -impl<'a> Formatter<'a> { - fn new(params: FormattingParams<'a>) -> Self { - Self { - params, - indent: params.indent(), - output: StdString::new(), - align: Vec::new(), - } - } - - fn visit_token_lowercase(&mut self, token: &Token) { - self.output.push_str(token.text().to_lowercase().as_ref()); - } - - fn should_insert_space(previous: &Token, current: &Token) -> bool { - previous.start().line != current.start().line - || previous.end().character < current.start().character - } -} - -impl<'a, 'b> Visitor<'b> for Formatter<'a> { - fn visit(&mut self, tree: &'b Tree, node: NodeIndex) { - match &tree.graph[node] { - Node::Root(_) => tree.walk(self, node), - Node::Comment(comment) => self.output.push_str(comment.token.text()), - Node::Preamble(preamble) => { - self.visit_token_lowercase(&preamble.ty); - self.output.push('{'); - if tree.has_children(node) { - self.align.push(self.output.chars().count()); - tree.walk(self, node); - self.output.push('}'); - } - } - Node::String(string) => { - self.visit_token_lowercase(&string.ty); - self.output.push('{'); - if let Some(name) = &string.name { - self.output.push_str(name.text()); - self.output.push_str(" = "); - if tree.has_children(node) { - self.align.push(self.output.chars().count()); - tree.walk(self, node); - self.output.push('}'); - } - } - } - Node::Entry(entry) => { - self.visit_token_lowercase(&entry.ty); - self.output.push('{'); - if let Some(key) = &entry.key { - self.output.push_str(key.text()); - self.output.push(','); - self.output.push('\n'); - tree.walk(self, node); - self.output.push('}'); - } - } - Node::Field(field) => { - self.output.push_str(&self.indent); - self.visit_token_lowercase(&field.name); - self.output.push_str(" = "); - if tree.has_children(node) { - let count = field.name.text().chars().count(); - self.align.push(self.params.tab_size as usize + count + 3); - tree.walk(self, node); - self.output.push(','); - self.output.push('\n'); - } - } - Node::Word(_) - | Node::Command(_) - | Node::BracedContent(_) - | Node::QuotedContent(_) - | Node::Concat(_) => { - let mut analyzer = ContentAnalyzer::default(); - analyzer.visit(tree, node); - let tokens = analyzer.tokens; - self.output.push_str(tokens[0].text()); - - let align = self.align.pop().unwrap_or_default(); - let mut length = align + tokens[0].text().chars().count(); - for i in 1..tokens.len() { - let previous = tokens[i - 1]; - let current = tokens[i]; - let current_length = current.text().chars().count(); - - let insert_space = Self::should_insert_space(previous, current); - let space_length = if insert_space { 1 } else { 0 }; - - if length + current_length + space_length > self.params.line_length() as usize { - self.output.push('\n'); - self.output.push_str(self.indent.as_ref()); - for _ in 0..=align - self.params.tab_size { - self.output.push(' '); - } - length = align; - } else if insert_space { - self.output.push(' '); - length += 1; - } - self.output.push_str(current.text()); - length += current_length; - } - } - } - } -} - -#[derive(Debug, Default)] -struct ContentAnalyzer<'a> { - tokens: Vec<&'a Token>, -} - -impl<'a> Visitor<'a> for ContentAnalyzer<'a> { - fn visit(&mut self, tree: &'a Tree, node: NodeIndex) { - match &tree.graph[node] { - Node::Root(_) - | Node::Comment(_) - | Node::Preamble(_) - | Node::String(_) - | Node::Entry(_) - | Node::Field(_) => tree.walk(self, node), - Node::Word(word) => self.tokens.push(&word.token), - Node::Command(cmd) => self.tokens.push(&cmd.token), - Node::QuotedContent(content) => { - self.tokens.push(&content.left); - tree.walk(self, node); - if let Some(right) = &content.right { - self.tokens.push(right); - } - } - Node::BracedContent(content) => { - self.tokens.push(&content.left); - tree.walk(self, node); - if let Some(right) = &content.right { - self.tokens.push(right); - } - } - Node::Concat(concat) => { - let mut children = tree.children(node); - let left = children.next().unwrap(); - self.visit(tree, left); - self.tokens.push(&concat.operator); - children.for_each(|right| self.visit(tree, right)); - } - } - } -} - -pub fn format(tree: &Tree, node: NodeIndex, params: FormattingParams) -> StdString { - let mut formatter = Formatter::new(params); - formatter.visit(tree, node); - formatter.output -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::syntax::bibtex; - use indoc::indoc; - - fn verify(source: &str, expected: &str, line_length: i32) { - let tree = bibtex::open(source); - let options = BibtexFormattingOptions { - line_length: Some(line_length), - formatter: None, - }; - - let mut children = tree.children(tree.root); - let declaration = children.next().unwrap(); - assert_eq!(children.next(), None); - - let actual = format( - &tree, - declaration, - FormattingParams { - tab_size: 4, - insert_spaces: true, - options: &options, - }, - ); - assert_eq!(actual, expected); - } - - #[test] - fn wrap_long_lines() { - let source = - "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}"; - let expected = indoc!( - " - @article{foo, - bar = {Lorem ipsum dolor - sit amet, - consectetur - adipiscing elit.}, - }" - ); - verify(source, expected, 30); - } - - #[test] - fn line_length_zero() { - let source = - "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}"; - let expected = indoc!( - " - @article{foo, - bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.}, - }" - ); - verify(source, expected, 0); - } - - #[test] - fn trailing_commas() { - let source = "@article{foo, bar = baz}"; - let expected = indoc!( - " - @article{foo, - bar = baz, - }" - ); - verify(source, expected, 30); - } - - #[test] - fn insert_braces() { - let source = "@article{foo, bar = baz,"; - let expected = indoc!( - " - @article{foo, - bar = baz, - }" - ); - verify(source, expected, 30); - } - - #[test] - fn commands() { - let source = "@article{foo, bar = \"\\baz\",}"; - let expected = indoc!( - "@article{foo, - bar = \"\\baz\", - }" - ); - verify(source, expected, 30); - } - - #[test] - fn concatenation() { - let source = "@article{foo, bar = \"baz\" # \"qux\"}"; - let expected = indoc!( - " - @article{foo, - bar = \"baz\" # \"qux\", - }" - ); - verify(source, expected, 30); - } - - #[test] - fn parentheses() { - let source = "@article(foo,)"; - let expected = indoc!( - " - @article{foo, - }" - ); - verify(source, expected, 30); - } - - #[test] - fn string() { - let source = "@string{foo=\"bar\"}"; - let expected = "@string{foo = \"bar\"}"; - verify(source, expected, 30); - } - - #[test] - fn preamble() { - let source = "@preamble{\n\"foo bar baz\"}"; - let expected = "@preamble{\"foo bar baz\"}"; - verify(source, expected, 30); - } -} diff --git a/support/texlab/src/syntax/bibtex/kind.rs b/support/texlab/src/syntax/bibtex/kind.rs new file mode 100644 index 0000000000..fcb26b69bd --- /dev/null +++ b/support/texlab/src/syntax/bibtex/kind.rs @@ -0,0 +1,51 @@ +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +#[allow(non_camel_case_types)] +#[repr(u16)] +pub enum SyntaxKind { + ERROR = 0, + MISSING, + + WHITESPACE, + PREAMBLE_TYPE, + STRING_TYPE, + COMMENT_TYPE, + ENTRY_TYPE, + WORD, + L_CURLY, + R_CURLY, + L_PAREN, + R_PAREN, + COMMA, + HASH, + QUOTE, + EQUALITY_SIGN, + COMMAND_NAME, + + JUNK, + PREAMBLE, + STRING, + COMMENT, + ENTRY, + FIELD, + VALUE, + TOKEN, + BRACE_GROUP, + QUOTE_GROUP, + ROOT, +} + +impl SyntaxKind { + pub fn is_type(&self) -> bool { + use SyntaxKind::*; + matches!( + self, + PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE + ) + } +} + +impl From<SyntaxKind> for cstree::SyntaxKind { + fn from(kind: SyntaxKind) -> Self { + Self(kind as u16) + } +} diff --git a/support/texlab/src/syntax/bibtex/lexer.rs b/support/texlab/src/syntax/bibtex/lexer.rs index e089b15864..dff111d645 100644 --- a/support/texlab/src/syntax/bibtex/lexer.rs +++ b/support/texlab/src/syntax/bibtex/lexer.rs @@ -1,173 +1,82 @@ -use super::ast::{Token, TokenKind}; -use crate::syntax::text::CharStream; +use logos::Logos; -#[derive(Debug)] -pub struct Lexer<'a> { - stream: CharStream<'a>, -} +use super::kind::SyntaxKind; -impl<'a> Lexer<'a> { - pub fn new(text: &'a str) -> Self { - Self { - stream: CharStream::new(text), - } - } +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord, Logos)] +#[allow(non_camel_case_types)] +#[repr(u16)] +enum LogosToken { + #[regex(r"\s+")] + WHITESPACE = 2, - fn kind(&mut self) -> Token { - fn is_type_char(c: char) -> bool { - c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' - } + #[regex(r"@[Pp][Rr][Ee][Aa][Mm][Bb][Ll][Ee]")] + PREAMBLE_TYPE, - self.stream.start_span(); - self.stream.next().unwrap(); - while self.stream.satifies(|c| is_type_char(*c)) { - self.stream.next(); - } - let span = self.stream.end_span(); - let kind = match span.text.to_lowercase().as_ref() { - "@preamble" => TokenKind::PreambleKind, - "@string" => TokenKind::StringKind, - _ => TokenKind::EntryKind, - }; - Token::new(span, kind) - } + #[regex(r"@[Ss][Tt][Rr][Ii][Nn][Gg]")] + STRING_TYPE, - fn single_character(&mut self, kind: TokenKind) -> Token { - self.stream.start_span(); - self.stream.next(); - let span = self.stream.end_span(); - Token::new(span, kind) - } + #[regex(r"@[Cc][Oo][Mm][Mm][Ee][Nn][Tt]")] + COMMENT_TYPE, - fn command(&mut self) -> Token { - let span = self.stream.command(); - Token::new(span, TokenKind::Command) - } + #[regex(r"@[!\$\&\*\+\-\./:;<>\?@\[\]\\\^_`\|\~a-zA-Z][!\$\&\*\+\-\./:;<>\?@\[\]\\\^_`\|\~a-zA-Z0-9]*|@")] + ENTRY_TYPE, - fn word(&mut self) -> Token { - fn is_word_char(c: char) -> bool { - !c.is_whitespace() - && c != '@' - && c != '=' - && c != ',' - && c != '#' - && c != '"' - && c != '{' - && c != '}' - && c != '(' - && c != ')' - } + #[regex(r#"[^\s\{\}\(\),#"=\\]+"#)] + #[error] + WORD, - self.stream.start_span(); - while self.stream.satifies(|c| is_word_char(*c)) { - self.stream.next(); - } - let span = self.stream.end_span(); - Token::new(span, TokenKind::Word) - } -} + #[token("{")] + L_CURLY, -impl<'a> Iterator for Lexer<'a> { - type Item = Token; - - fn next(&mut self) -> Option<Token> { - loop { - match self.stream.peek() { - Some('@') => return Some(self.kind()), - Some('=') => return Some(self.single_character(TokenKind::Assign)), - Some(',') => return Some(self.single_character(TokenKind::Comma)), - Some('#') => return Some(self.single_character(TokenKind::Concat)), - Some('"') => return Some(self.single_character(TokenKind::Quote)), - Some('{') => return Some(self.single_character(TokenKind::BeginBrace)), - Some('}') => return Some(self.single_character(TokenKind::EndBrace)), - Some('(') => return Some(self.single_character(TokenKind::BeginParen)), - Some(')') => return Some(self.single_character(TokenKind::EndParen)), - Some('\\') => return Some(self.command()), - Some(c) => { - if c.is_whitespace() { - self.stream.next(); - } else { - return Some(self.word()); - } - } - None => { - return None; - } - } - } - } -} + #[token("}")] + R_CURLY, -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - protocol::{Position, Range}, - syntax::text::Span, - }; - - fn verify<'a>(lexer: &mut Lexer<'a>, line: u64, character: u64, text: &str, kind: TokenKind) { - let start = Position::new(line, character); - let end = Position::new(line, character + text.chars().count() as u64); - let range = Range::new(start, end); - let span = Span::new(range, text.to_owned()); - let token = Token::new(span, kind); - assert_eq!(Some(token), lexer.next()); - } + #[token("(")] + L_PAREN, - #[test] - fn word() { - let mut lexer = Lexer::new("foo bar baz"); - verify(&mut lexer, 0, 0, "foo", TokenKind::Word); - verify(&mut lexer, 0, 4, "bar", TokenKind::Word); - verify(&mut lexer, 0, 8, "baz", TokenKind::Word); - assert_eq!(None, lexer.next()); - } + #[token(")")] + R_PAREN, - #[test] - fn command() { - let mut lexer = Lexer::new("\\foo\\bar@baz"); - verify(&mut lexer, 0, 0, "\\foo", TokenKind::Command); - verify(&mut lexer, 0, 4, "\\bar@baz", TokenKind::Command); - assert_eq!(None, lexer.next()); - } + #[token(",")] + COMMA, - #[test] - fn escape_sequence() { - let mut lexer = Lexer::new("\\foo*\n\\%\\**"); - verify(&mut lexer, 0, 0, "\\foo*", TokenKind::Command); - verify(&mut lexer, 1, 0, "\\%", TokenKind::Command); - verify(&mut lexer, 1, 2, "\\*", TokenKind::Command); - verify(&mut lexer, 1, 4, "*", TokenKind::Word); - assert_eq!(None, lexer.next()); - } + #[token("#")] + HASH, + + #[token("\"")] + QUOTE, + + #[token("=")] + EQUALITY_SIGN, - #[test] - fn delimiter() { - let mut lexer = Lexer::new("{}()\""); - verify(&mut lexer, 0, 0, "{", TokenKind::BeginBrace); - verify(&mut lexer, 0, 1, "}", TokenKind::EndBrace); - verify(&mut lexer, 0, 2, "(", TokenKind::BeginParen); - verify(&mut lexer, 0, 3, ")", TokenKind::EndParen); - verify(&mut lexer, 0, 4, "\"", TokenKind::Quote); - assert_eq!(None, lexer.next()); + #[regex(r"\\([^\r\n]|[@a-zA-Z:_]+\*?)?")] + COMMAND_NAME, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Lexer<'a> { + tokens: Vec<(SyntaxKind, &'a str)>, +} + +impl<'a> Lexer<'a> { + pub fn new(text: &'a str) -> Self { + let mut tokens = Vec::new(); + let mut lexer = LogosToken::lexer(text); + while let Some(kind) = lexer.next() { + tokens.push(( + unsafe { std::mem::transmute::<LogosToken, SyntaxKind>(kind) }, + lexer.slice(), + )); + } + tokens.reverse(); + Self { tokens } } - #[test] - fn kind() { - let mut lexer = Lexer::new("@pReAmBlE\n@article\n@string"); - verify(&mut lexer, 0, 0, "@pReAmBlE", TokenKind::PreambleKind); - verify(&mut lexer, 1, 0, "@article", TokenKind::EntryKind); - verify(&mut lexer, 2, 0, "@string", TokenKind::StringKind); - assert_eq!(None, lexer.next()); + pub fn peek(&self) -> Option<SyntaxKind> { + self.tokens.last().map(|(kind, _)| *kind) } - #[test] - fn operator() { - let mut lexer = Lexer::new("=,#"); - verify(&mut lexer, 0, 0, "=", TokenKind::Assign); - verify(&mut lexer, 0, 1, ",", TokenKind::Comma); - verify(&mut lexer, 0, 2, "#", TokenKind::Concat); - assert_eq!(None, lexer.next()); + pub fn consume(&mut self) -> Option<(SyntaxKind, &'a str)> { + self.tokens.pop() } } diff --git a/support/texlab/src/syntax/bibtex/mod.rs b/support/texlab/src/syntax/bibtex/mod.rs deleted file mode 100644 index aa7242c1f7..0000000000 --- a/support/texlab/src/syntax/bibtex/mod.rs +++ /dev/null @@ -1,396 +0,0 @@ -mod ast; -mod formatter; -mod lexer; -mod parser; - -pub use self::{ast::*, formatter::*}; - -use self::{lexer::Lexer, parser::Parser}; - -pub fn open(text: &str) -> Tree { - let lexer = Lexer::new(text); - let parser = Parser::new(lexer); - parser.parse() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - protocol::{Range, RangeExt}, - syntax::text::SyntaxNode, - }; - use petgraph::graph::NodeIndex; - - #[derive(Debug, Default)] - struct TreeTraversal { - nodes: Vec<NodeIndex>, - } - - impl<'a> Visitor<'a> for TreeTraversal { - fn visit(&mut self, tree: &Tree, node: NodeIndex) { - self.nodes.push(node); - tree.walk(self, node); - } - } - - mod range { - use super::*; - use indoc::indoc; - - fn verify(expected_ranges: Vec<Range>, text: &str) { - let tree = open(text.trim()); - - let mut traversal = TreeTraversal::default(); - traversal.visit(&tree, tree.root); - let actual_ranges: Vec<_> = traversal - .nodes - .into_iter() - .map(|node| tree.graph[node].range()) - .collect(); - - assert_eq!(actual_ranges, expected_ranges); - } - - #[test] - fn empty_document() { - verify(vec![Range::new_simple(0, 0, 0, 0)], ""); - } - - #[test] - fn comment() { - verify( - vec![Range::new_simple(0, 0, 0, 3), Range::new_simple(0, 0, 0, 3)], - "foo", - ); - } - - #[test] - fn preamble_no_left() { - verify( - vec![Range::new_simple(0, 0, 0, 9), Range::new_simple(0, 0, 0, 9)], - "@preamble", - ); - } - - #[test] - fn preamble_no_content() { - verify( - vec![ - Range::new_simple(0, 0, 0, 10), - Range::new_simple(0, 0, 0, 10), - ], - "@preamble{", - ); - } - - #[test] - fn preamble_no_right() { - verify( - vec![ - Range::new_simple(0, 0, 0, 15), - Range::new_simple(0, 0, 0, 15), - Range::new_simple(0, 10, 0, 15), - Range::new_simple(0, 11, 0, 14), - ], - r#"@preamble{"foo""#, - ); - } - - #[test] - fn preamble_complete() { - verify( - vec![ - Range::new_simple(0, 0, 0, 16), - Range::new_simple(0, 0, 0, 16), - Range::new_simple(0, 10, 0, 15), - Range::new_simple(0, 11, 0, 14), - ], - r#"@preamble{"foo"}"#, - ); - } - - #[test] - fn string_no_left() { - verify( - vec![Range::new_simple(0, 0, 0, 7), Range::new_simple(0, 0, 0, 7)], - r#"@string"#, - ); - } - - #[test] - fn string_no_name() { - verify( - vec![Range::new_simple(0, 0, 0, 8), Range::new_simple(0, 0, 0, 8)], - r#"@string{"#, - ); - } - - #[test] - fn string_no_assign() { - verify( - vec![ - Range::new_simple(0, 0, 0, 11), - Range::new_simple(0, 0, 0, 11), - ], - r#"@string{foo"#, - ); - } - - #[test] - fn string_no_value() { - verify( - vec![ - Range::new_simple(0, 0, 0, 13), - Range::new_simple(0, 0, 0, 13), - ], - r#"@string{foo ="#, - ); - } - - #[test] - fn string_no_right() { - verify( - vec![ - Range::new_simple(0, 0, 0, 19), - Range::new_simple(0, 0, 0, 19), - Range::new_simple(0, 14, 0, 19), - Range::new_simple(0, 15, 0, 18), - ], - r#"@string{foo = "bar""#, - ); - } - - #[test] - fn string_complete() { - verify( - vec![ - Range::new_simple(0, 0, 0, 20), - Range::new_simple(0, 0, 0, 20), - Range::new_simple(0, 14, 0, 19), - Range::new_simple(0, 15, 0, 18), - ], - r#"@string{foo = "bar"}"#, - ); - } - - #[test] - fn entry_no_left() { - verify( - vec![Range::new_simple(0, 0, 0, 8), Range::new_simple(0, 0, 0, 8)], - r#"@article"#, - ); - } - - #[test] - fn entry_no_key() { - verify( - vec![Range::new_simple(0, 0, 0, 9), Range::new_simple(0, 0, 0, 9)], - r#"@article{"#, - ); - } - - #[test] - fn entry_no_comma() { - verify( - vec![ - Range::new_simple(0, 0, 0, 12), - Range::new_simple(0, 0, 0, 12), - ], - r#"@article{foo"#, - ); - } - - #[test] - fn entry_no_right() { - verify( - vec![ - Range::new_simple(0, 0, 0, 13), - Range::new_simple(0, 0, 0, 13), - ], - r#"@article{foo,"#, - ); - } - - #[test] - fn entry_parentheses() { - verify( - vec![ - Range::new_simple(0, 0, 0, 14), - Range::new_simple(0, 0, 0, 14), - ], - r#"@article(foo,)"#, - ); - } - - #[test] - fn field_no_assign() { - verify( - vec![ - Range::new_simple(0, 0, 1, 10), - Range::new_simple(0, 0, 1, 10), - Range::new_simple(1, 4, 1, 10), - ], - indoc!( - r#" - @article{foo, - author - "# - ), - ); - } - - #[test] - fn field_no_value() { - verify( - vec![ - Range::new_simple(0, 0, 1, 12), - Range::new_simple(0, 0, 1, 12), - Range::new_simple(1, 4, 1, 12), - ], - indoc!( - r#" - @article{foo, - author = - "# - ), - ); - } - - #[test] - fn field_no_comma() { - verify( - vec![ - Range::new_simple(0, 0, 1, 16), - Range::new_simple(0, 0, 1, 16), - Range::new_simple(1, 4, 1, 16), - Range::new_simple(1, 13, 1, 16), - ], - indoc!( - r#" - @article{foo, - author = bar - "# - ), - ); - } - - #[test] - fn field_complete() { - verify( - vec![ - Range::new_simple(0, 0, 2, 1), - Range::new_simple(0, 0, 2, 1), - Range::new_simple(1, 4, 1, 17), - Range::new_simple(1, 13, 1, 16), - ], - indoc!( - r#" - @article{foo, - author = bar, - } - "# - ), - ); - } - - #[test] - fn entry_two_fields() { - verify( - vec![ - Range::new_simple(0, 0, 3, 1), - Range::new_simple(0, 0, 3, 1), - Range::new_simple(1, 4, 1, 17), - Range::new_simple(1, 13, 1, 16), - Range::new_simple(2, 4, 2, 16), - Range::new_simple(2, 12, 2, 15), - ], - indoc!( - r#" - @article{foo, - author = bar, - title = baz, - } - "# - ), - ); - } - - #[test] - fn quoted_content_no_children() { - verify( - vec![ - Range::new_simple(0, 0, 0, 12), - Range::new_simple(0, 0, 0, 12), - Range::new_simple(0, 10, 0, 11), - ], - r#"@preamble{"}"#, - ); - } - - #[test] - fn quoted_content_no_right() { - verify( - vec![ - Range::new_simple(0, 0, 0, 16), - Range::new_simple(0, 0, 0, 16), - Range::new_simple(0, 10, 0, 15), - Range::new_simple(0, 11, 0, 15), - ], - r#"@preamble{"word}"#, - ); - } - - #[test] - fn braced_content_no_children() { - verify( - vec![ - Range::new_simple(0, 0, 0, 11), - Range::new_simple(0, 0, 0, 11), - Range::new_simple(0, 10, 0, 11), - ], - r#"@preamble{{"#, - ); - } - - #[test] - fn braced_content_no_right() { - verify( - vec![ - Range::new_simple(0, 0, 0, 15), - Range::new_simple(0, 0, 0, 15), - Range::new_simple(0, 10, 0, 15), - Range::new_simple(0, 11, 0, 15), - ], - r#"@preamble{{word"#, - ); - } - - #[test] - fn concat_no_right() { - verify( - vec![ - Range::new_simple(0, 0, 0, 16), - Range::new_simple(0, 0, 0, 16), - Range::new_simple(0, 10, 0, 15), - Range::new_simple(0, 10, 0, 13), - ], - r#"@preamble{foo #}"#, - ); - } - - #[test] - fn concat_complete() { - verify( - vec![ - Range::new_simple(0, 0, 0, 20), - Range::new_simple(0, 0, 0, 20), - Range::new_simple(0, 10, 0, 19), - Range::new_simple(0, 10, 0, 13), - Range::new_simple(0, 16, 0, 19), - ], - r#"@preamble{foo # bar}"#, - ); - } - } -} diff --git a/support/texlab/src/syntax/bibtex/parser.rs b/support/texlab/src/syntax/bibtex/parser.rs index b23903aeb3..ffdb1c94a0 100644 --- a/support/texlab/src/syntax/bibtex/parser.rs +++ b/support/texlab/src/syntax/bibtex/parser.rs @@ -1,401 +1,413 @@ -use super::ast::*; -use crate::{ - protocol::{Range, RangeExt}, - syntax::text::SyntaxNode, +use cstree::GreenNodeBuilder; + +use super::{ + lexer::Lexer, + SyntaxKind::{self, *}, + SyntaxNode, }; -use petgraph::graph::{Graph, NodeIndex}; -use std::iter::Peekable; -pub struct Parser<I: Iterator<Item = Token>> { - graph: Graph<Node, ()>, - tokens: Peekable<I>, +#[derive(Debug, Clone)] +pub struct Parse { + pub root: SyntaxNode, +} + +struct Parser<'a> { + lexer: Lexer<'a>, + builder: GreenNodeBuilder<'static, 'static>, } -impl<I: Iterator<Item = Token>> Parser<I> { - pub fn new(tokens: I) -> Self { +impl<'a> Parser<'a> { + pub fn new(lexer: Lexer<'a>) -> Self { Self { - tokens: tokens.peekable(), - graph: Graph::new(), + lexer, + builder: GreenNodeBuilder::new(), } } - pub fn parse(mut self) -> Tree { - let mut children = Vec::new(); + fn peek(&self) -> Option<SyntaxKind> { + self.lexer.peek() + } - while let Some(ref token) = self.tokens.peek() { - match token.kind { - TokenKind::PreambleKind => children.push(self.preamble()), - TokenKind::StringKind => children.push(self.string()), - TokenKind::EntryKind => children.push(self.entry()), - _ => children.push(self.comment()), - } - } + fn eat(&mut self) { + let (kind, text) = self.lexer.consume().unwrap(); + self.builder.token(kind.into(), text); + } - let range = if children.is_empty() { - Range::new_simple(0, 0, 0, 0) + fn expect(&mut self, kind: SyntaxKind) { + if self.peek() == Some(kind) { + self.eat(); + self.trivia(); } else { - let start = self.graph[children[0]].start(); - let end = self.graph[children[children.len() - 1]].end(); - Range::new(start, end) - }; + self.builder.token(MISSING.into(), ""); + } + } - let root = self.graph.add_node(Node::Root(Root { range })); - self.connect(root, &children); - Tree { - graph: self.graph, - root, + pub fn parse(mut self) -> Parse { + self.builder.start_node(ROOT.into()); + while let Some(kind) = self.peek() { + match kind { + PREAMBLE_TYPE => self.preamble(), + STRING_TYPE => self.string(), + COMMENT_TYPE => self.comment(), + ENTRY_TYPE => self.entry(), + _ => self.junk(), + } + } + self.builder.finish_node(); + let (green, resolver) = self.builder.finish(); + Parse { + root: SyntaxNode::new_root_with_resolver(green, resolver.unwrap()), } } - fn preamble(&mut self) -> NodeIndex { - let ty = self.tokens.next().unwrap(); + fn trivia(&mut self) { + while self.peek() == Some(WHITESPACE) { + self.eat(); + } + } - let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen); - if left.is_none() { - return self.graph.add_node(Node::Preamble(Box::new(Preamble { - range: ty.range(), - ty, - left: None, - right: None, - }))); + fn junk(&mut self) { + self.builder.start_node(JUNK.into()); + while self + .lexer + .peek() + .filter(|&kind| { + !matches!( + kind, + PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE, + ) + }) + .is_some() + { + self.eat(); } + self.builder.finish_node(); + } - if !self.can_match_content() { - return self.graph.add_node(Node::Preamble(Box::new(Preamble { - range: Range::new(ty.start(), left.as_ref().unwrap().end()), - ty, - left, - right: None, - }))); + fn left_delimiter_or_missing(&mut self) { + if self + .lexer + .peek() + .filter(|&kind| matches!(kind, L_CURLY | L_PAREN)) + .is_some() + { + self.eat(); + self.trivia(); + } else { + self.builder.token(MISSING.into(), ""); } + } - let content = self.content(); - - let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen); - let end = right - .as_ref() - .map(Token::end) - .unwrap_or_else(|| self.graph[content].end()); - - let parent = self.graph.add_node(Node::Preamble(Box::new(Preamble { - range: Range::new(ty.start(), end), - ty, - left, - right, - }))); - self.graph.add_edge(parent, content, ()); - parent - } - - fn string(&mut self) -> NodeIndex { - let ty = self.tokens.next().unwrap(); - - let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen); - if left.is_none() { - return self.graph.add_node(Node::String(Box::new(String { - range: ty.range(), - ty, - left: None, - name: None, - assign: None, - right: None, - }))); + fn right_delimiter_or_missing(&mut self) { + if self + .lexer + .peek() + .filter(|&kind| matches!(kind, R_CURLY | R_PAREN)) + .is_some() + { + self.eat(); + self.trivia(); + } else { + self.builder.token(MISSING.into(), ""); } + } - let name = self.expect1(TokenKind::Word); - if name.is_none() { - return self.graph.add_node(Node::String(Box::new(String { - range: Range::new(ty.start(), left.as_ref().unwrap().end()), - ty, - left, - name: None, - assign: None, - right: None, - }))); + fn value_or_missing(&mut self) { + if self + .lexer + .peek() + .filter(|&kind| matches!(kind, L_CURLY | QUOTE | WORD)) + .is_some() + { + self.value(); + } else { + self.builder.token(MISSING.into(), ""); } + } + + fn preamble(&mut self) { + self.builder.start_node(PREAMBLE.into()); + self.eat(); + self.trivia(); + self.left_delimiter_or_missing(); + self.value_or_missing(); + self.right_delimiter_or_missing(); + self.builder.finish_node(); + } - let assign = self.expect1(TokenKind::Assign); - if assign.is_none() { - return self.graph.add_node(Node::String(Box::new(String { - range: Range::new(ty.start(), name.as_ref().unwrap().end()), - ty, - left, - name, - assign: None, - right: None, - }))); + fn string(&mut self) { + self.builder.start_node(STRING.into()); + self.eat(); + self.trivia(); + self.left_delimiter_or_missing(); + + if self.peek() != Some(WORD) { + self.builder.token(MISSING.into(), ""); + self.builder.finish_node(); + return; } + self.eat(); + self.trivia(); + self.expect(EQUALITY_SIGN); + + self.value_or_missing(); + + self.right_delimiter_or_missing(); + self.builder.finish_node(); + } + + fn comment(&mut self) { + self.builder.start_node(COMMENT.into()); + self.eat(); + self.builder.finish_node(); + } - if !self.can_match_content() { - return self.graph.add_node(Node::String(Box::new(String { - range: Range::new(ty.start(), assign.as_ref().unwrap().end()), - ty, - left, - name, - assign, - right: None, - }))); + fn entry(&mut self) { + self.builder.start_node(ENTRY.into()); + self.eat(); + self.trivia(); + + self.left_delimiter_or_missing(); + + if self.peek() != Some(WORD) { + self.builder.token(MISSING.into(), ""); + self.builder.finish_node(); + return; } - let value = self.content(); - - let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen); - let end = right - .as_ref() - .map(Token::end) - .unwrap_or_else(|| self.graph[value].end()); - - let parent = self.graph.add_node(Node::String(Box::new(String { - range: Range::new(ty.start(), end), - ty, - left, - name, - assign, - right, - }))); - self.graph.add_edge(parent, value, ()); - parent - } - - fn entry(&mut self) -> NodeIndex { - let ty = self.tokens.next().unwrap(); - - let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen); - if left.is_none() { - return self.graph.add_node(Node::Entry(Box::new(Entry { - range: ty.range(), - ty, - left: None, - key: None, - comma: None, - right: None, - }))); + self.eat(); + + while let Some(kind) = self.peek() { + match kind { + WHITESPACE => self.eat(), + WORD => self.field(), + COMMA => self.eat(), + _ => break, + }; } - let key = self.expect1(TokenKind::Word); - if key.is_none() { - return self.graph.add_node(Node::Entry(Box::new(Entry { - range: Range::new(ty.start(), left.as_ref().unwrap().end()), - ty, - left, - key: None, - comma: None, - right: None, - }))); - } + self.right_delimiter_or_missing(); - let comma = self.expect1(TokenKind::Comma); - if comma.is_none() { - return self.graph.add_node(Node::Entry(Box::new(Entry { - range: Range::new(ty.start(), key.as_ref().unwrap().end()), - ty, - left, - key, - comma: None, - right: None, - }))); - } + self.builder.finish_node(); + } - let mut fields = Vec::new(); - while self.next_of_kind(TokenKind::Word) { - fields.push(self.field()); - } + fn field(&mut self) { + self.builder.start_node(FIELD.into()); + self.eat(); + self.trivia(); - let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen); - - let end = right - .as_ref() - .map(Token::end) - .or_else(|| fields.last().map(|field| self.graph[*field].end())) - .unwrap_or_else(|| comma.as_ref().unwrap().end()); - let parent = self.graph.add_node(Node::Entry(Box::new(Entry { - range: Range::new(ty.start(), end), - ty, - left, - key, - comma, - right, - }))); - self.connect(parent, &fields); - parent - } - - fn comment(&mut self) -> NodeIndex { - let token = self.tokens.next().unwrap(); - self.graph.add_node(Node::Comment(Comment { token })) - } - - fn field(&mut self) -> NodeIndex { - let name = self.tokens.next().unwrap(); - - let assign = self.expect1(TokenKind::Assign); - if assign.is_none() { - return self.graph.add_node(Node::Field(Box::new(Field { - range: name.range(), - name, - assign: None, - comma: None, - }))); + if self.peek() == Some(EQUALITY_SIGN) { + self.eat(); + self.trivia(); + } else { + self.builder.token(MISSING.into(), ""); } - if !self.can_match_content() { - return self.graph.add_node(Node::Field(Box::new(Field { - range: Range::new(name.start(), assign.as_ref().unwrap().end()), - name, - assign, - comma: None, - }))); + if self + .lexer + .peek() + .filter(|&kind| matches!(kind, L_CURLY | QUOTE | WORD)) + .is_some() + { + self.value(); + } else { + self.builder.token(MISSING.into(), ""); } - let content = self.content(); - - let comma = self.expect1(TokenKind::Comma); - - let end = comma - .as_ref() - .map(Token::end) - .unwrap_or_else(|| self.graph[content].end()); - let parent = self.graph.add_node(Node::Field(Box::new(Field { - range: Range::new(name.start(), end), - name, - assign, - comma, - }))); - self.graph.add_edge(parent, content, ()); - parent - } - - fn content(&mut self) -> NodeIndex { - let token = self.tokens.next().unwrap(); - let left = match token.kind { - TokenKind::PreambleKind - | TokenKind::StringKind - | TokenKind::EntryKind - | TokenKind::Word - | TokenKind::Assign - | TokenKind::Comma - | TokenKind::BeginParen - | TokenKind::EndParen => self.graph.add_node(Node::Word(Word { token })), - TokenKind::Command => self.graph.add_node(Node::Command(Command { token })), - TokenKind::Quote => { - let mut children = Vec::new(); - while self.can_match_content() { - if self.next_of_kind(TokenKind::Quote) { - break; - } - children.push(self.content()); - } - let right = self.expect1(TokenKind::Quote); - - let end = right - .as_ref() - .map(Token::end) - .or_else(|| children.last().map(|child| self.graph[*child].end())) - .unwrap_or_else(|| token.end()); - let parent = self.graph.add_node(Node::QuotedContent(QuotedContent { - range: Range::new(token.start(), end), - left: token, - right, - })); - self.connect(parent, &children); - parent - } - TokenKind::BeginBrace => { - let mut children = Vec::new(); - while self.can_match_content() { - children.push(self.content()); - } - let right = self.expect1(TokenKind::EndBrace); - - let end = right - .as_ref() - .map(Token::end) - .or_else(|| children.last().map(|child| self.graph[*child].end())) - .unwrap_or_else(|| token.end()); - let parent = self.graph.add_node(Node::BracedContent(BracedContent { - range: Range::new(token.start(), end), - left: token, - right, - })); - self.connect(parent, &children); - parent + self.builder.finish_node(); + } + + fn value(&mut self) { + self.builder.start_node(VALUE.into()); + self.token(); + while let Some(kind) = self.peek() { + match kind { + WHITESPACE => self.eat(), + L_CURLY | QUOTE | WORD => self.token(), + HASH => self.eat(), + _ => break, } + } + self.builder.finish_node(); + } + + fn token(&mut self) { + self.builder.start_node(TOKEN.into()); + match self.peek().unwrap() { + L_CURLY => self.brace_group(), + QUOTE => self.quote_group(), + WORD => self.eat(), _ => unreachable!(), }; + self.builder.finish_node(); + } - match self.expect1(TokenKind::Concat) { - Some(operator) => { - if self.can_match_content() { - let right = self.content(); - let parent = self.graph.add_node(Node::Concat(Concat { - range: Range::new(self.graph[left].start(), self.graph[right].end()), - operator, - })); - self.graph.add_edge(parent, left, ()); - self.graph.add_edge(parent, right, ()); - parent - } else { - let parent = self.graph.add_node(Node::Concat(Concat { - range: Range::new(self.graph[left].start(), operator.end()), - operator, - })); - self.graph.add_edge(parent, left, ()); - parent - } - } - None => left, + fn brace_group(&mut self) { + self.builder.start_node(BRACE_GROUP.into()); + self.eat(); + + while let Some(kind) = self.peek() { + match kind { + WHITESPACE => self.eat(), + PREAMBLE_TYPE => break, + STRING_TYPE => break, + COMMENT_TYPE => break, + ENTRY_TYPE => break, + WORD => self.eat(), + L_CURLY => self.brace_group(), + R_CURLY => break, + L_PAREN => self.eat(), + R_PAREN => self.eat(), + COMMA => self.eat(), + HASH => self.eat(), + QUOTE => self.eat(), + EQUALITY_SIGN => self.eat(), + COMMAND_NAME => self.eat(), + _ => unreachable!(), + }; } + + self.expect(R_CURLY); + + self.builder.finish_node(); } - fn connect(&mut self, parent: NodeIndex, children: &[NodeIndex]) { - for child in children { - self.graph.add_edge(parent, *child, ()); + fn quote_group(&mut self) { + self.builder.start_node(QUOTE_GROUP.into()); + self.eat(); + + while let Some(kind) = self.peek() { + match kind { + WHITESPACE => self.eat(), + PREAMBLE_TYPE => break, + STRING_TYPE => break, + COMMENT_TYPE => break, + ENTRY_TYPE => break, + WORD => self.eat(), + L_CURLY => self.brace_group(), + R_CURLY => break, + L_PAREN => self.eat(), + R_PAREN => self.eat(), + COMMA => self.eat(), + HASH => self.eat(), + QUOTE => break, + EQUALITY_SIGN => self.eat(), + COMMAND_NAME => self.eat(), + _ => unreachable!(), + }; } + + self.expect(QUOTE); + self.builder.finish_node(); } +} - fn can_match_content(&mut self) -> bool { - if let Some(ref token) = self.tokens.peek() { - match token.kind { - TokenKind::PreambleKind - | TokenKind::StringKind - | TokenKind::EntryKind - | TokenKind::Word - | TokenKind::Command - | TokenKind::Assign - | TokenKind::Comma - | TokenKind::Quote - | TokenKind::BeginBrace - | TokenKind::BeginParen - | TokenKind::EndParen => true, - TokenKind::Concat | TokenKind::EndBrace => false, - } - } else { - false - } +pub fn parse(text: &str) -> Parse { + Parser::new(Lexer::new(text)).parse() +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::*; + + fn setup(text: &str) -> SyntaxNode { + parse(&text.trim().replace("\r", "")).root } - fn expect1(&mut self, kind: TokenKind) -> Option<Token> { - if let Some(ref token) = self.tokens.peek() { - if token.kind == kind { - return self.tokens.next(); - } - } - None + #[test] + fn test_empty() { + assert_debug_snapshot!(setup(r#""#)); } - fn expect2(&mut self, kind1: TokenKind, kind2: TokenKind) -> Option<Token> { - if let Some(ref token) = self.tokens.peek() { - if token.kind == kind1 || token.kind == kind2 { - return self.tokens.next(); - } - } - None + #[test] + fn test_junk() { + assert_debug_snapshot!(setup(r#"Hello World!"#)); } - fn next_of_kind(&mut self, kind: TokenKind) -> bool { - if let Some(token) = self.tokens.peek() { - token.kind == kind - } else { - false - } + #[test] + fn test_preamble_complete() { + assert_debug_snapshot!(setup(r#"@preamble{ "Hello World" }"#)); + } + + #[test] + fn test_preamble_missing_end() { + assert_debug_snapshot!(setup(r#"@preamble{ "Hello World" "#)); + } + + #[test] + fn test_preamble_casing() { + assert_debug_snapshot!(setup(r#"@preAmbLe{ "Hello World" }"#)); + } + + #[test] + fn test_string_complete() { + assert_debug_snapshot!(setup(r#"@string{foo = {Hello World}}"#)); + } + + #[test] + fn test_string_incomplete() { + assert_debug_snapshot!(setup(r#"@string{foo = {Hello World}"#)); + } + + #[test] + fn test_string_concatenation() { + assert_debug_snapshot!(setup( + r#"@string{foo = {Hello World}} @string{bar = foo # "!"}"# + )); + } + + #[test] + fn test_string_casing() { + assert_debug_snapshot!(setup(r#"@STRING{foo = "Hello World"}"#)); + } + + #[test] + fn test_string_missing_quote() { + assert_debug_snapshot!(setup(r#"@STRING{foo = "Hello World}"#)); + } + + #[test] + fn test_entry_no_fields() { + assert_debug_snapshot!(setup(r#"@article{foo,}"#)); + } + + #[test] + fn test_entry_no_fields_missing_comma() { + assert_debug_snapshot!(setup(r#"@article{foo}"#)); + } + + #[test] + fn test_entry_one_field() { + assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar}}"#)); + } + + #[test] + fn test_entry_one_field_number_key() { + assert_debug_snapshot!(setup(r#"@article{foo2021, author = {Foo Bar}}"#)); + } + + #[test] + fn test_entry_one_field_trailing_comma() { + assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar},}"#)); + } + + #[test] + fn test_entry_two_fields() { + assert_debug_snapshot!(setup( + r#"@article{foo, author = {Foo Bar}, title = {Hello World}}"# + )); + } + + #[test] + fn test_entry_two_fields_incomplete() { + assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar}, t}"#)); + } + + #[test] + fn test_entry_complete_parens() { + assert_debug_snapshot!(setup( + r#"@article(foo, author = {Foo Bar}, title = {Hello})"# + )); } } diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap new file mode 100644 index 0000000000..55e1954d2f --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap @@ -0,0 +1,7 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"\"#)" + +--- +ROOT@0..0 + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap new file mode 100644 index 0000000000..cfd469f161 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap @@ -0,0 +1,40 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@article(foo, author = {Foo Bar}, title = {Hello})\"#)" + +--- +ROOT@0..50 + ENTRY@0..50 + ENTRY_TYPE@0..8 "@article" + L_PAREN@8..9 "(" + WORD@9..12 "foo" + COMMA@12..13 "," + WHITESPACE@13..14 " " + FIELD@14..32 + WORD@14..20 "author" + WHITESPACE@20..21 " " + EQUALITY_SIGN@21..22 "=" + WHITESPACE@22..23 " " + VALUE@23..32 + TOKEN@23..32 + BRACE_GROUP@23..32 + L_CURLY@23..24 "{" + WORD@24..27 "Foo" + WHITESPACE@27..28 " " + WORD@28..31 "Bar" + R_CURLY@31..32 "}" + COMMA@32..33 "," + WHITESPACE@33..34 " " + FIELD@34..49 + WORD@34..39 "title" + WHITESPACE@39..40 " " + EQUALITY_SIGN@40..41 "=" + WHITESPACE@41..42 " " + VALUE@42..49 + TOKEN@42..49 + BRACE_GROUP@42..49 + L_CURLY@42..43 "{" + WORD@43..48 "Hello" + R_CURLY@48..49 "}" + R_PAREN@49..50 ")" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap new file mode 100644 index 0000000000..a98df7e382 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap @@ -0,0 +1,13 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@article{foo,}\"#)" + +--- +ROOT@0..14 + ENTRY@0..14 + ENTRY_TYPE@0..8 "@article" + L_CURLY@8..9 "{" + WORD@9..12 "foo" + COMMA@12..13 "," + R_CURLY@13..14 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap new file mode 100644 index 0000000000..b83037aae0 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap @@ -0,0 +1,12 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@article{foo}\"#)" + +--- +ROOT@0..13 + ENTRY@0..13 + ENTRY_TYPE@0..8 "@article" + L_CURLY@8..9 "{" + WORD@9..12 "foo" + R_CURLY@12..13 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap new file mode 100644 index 0000000000..704cbbbd47 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap @@ -0,0 +1,27 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@article{foo, author = {Foo Bar}}\"#)" + +--- +ROOT@0..33 + ENTRY@0..33 + ENTRY_TYPE@0..8 "@article" + L_CURLY@8..9 "{" + WORD@9..12 "foo" + COMMA@12..13 "," + WHITESPACE@13..14 " " + FIELD@14..32 + WORD@14..20 "author" + WHITESPACE@20..21 " " + EQUALITY_SIGN@21..22 "=" + WHITESPACE@22..23 " " + VALUE@23..32 + TOKEN@23..32 + BRACE_GROUP@23..32 + L_CURLY@23..24 "{" + WORD@24..27 "Foo" + WHITESPACE@27..28 " " + WORD@28..31 "Bar" + R_CURLY@31..32 "}" + R_CURLY@32..33 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap new file mode 100644 index 0000000000..8f00c5ee1c --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap @@ -0,0 +1,27 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@article{foo2021, author = {Foo Bar}}\"#)" + +--- +ROOT@0..37 + ENTRY@0..37 + ENTRY_TYPE@0..8 "@article" + L_CURLY@8..9 "{" + WORD@9..16 "foo2021" + COMMA@16..17 "," + WHITESPACE@17..18 " " + FIELD@18..36 + WORD@18..24 "author" + WHITESPACE@24..25 " " + EQUALITY_SIGN@25..26 "=" + WHITESPACE@26..27 " " + VALUE@27..36 + TOKEN@27..36 + BRACE_GROUP@27..36 + L_CURLY@27..28 "{" + WORD@28..31 "Foo" + WHITESPACE@31..32 " " + WORD@32..35 "Bar" + R_CURLY@35..36 "}" + R_CURLY@36..37 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap new file mode 100644 index 0000000000..af8b1593df --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap @@ -0,0 +1,28 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@article{foo, author = {Foo Bar},}\"#)" + +--- +ROOT@0..34 + ENTRY@0..34 + ENTRY_TYPE@0..8 "@article" + L_CURLY@8..9 "{" + WORD@9..12 "foo" + COMMA@12..13 "," + WHITESPACE@13..14 " " + FIELD@14..32 + WORD@14..20 "author" + WHITESPACE@20..21 " " + EQUALITY_SIGN@21..22 "=" + WHITESPACE@22..23 " " + VALUE@23..32 + TOKEN@23..32 + BRACE_GROUP@23..32 + L_CURLY@23..24 "{" + WORD@24..27 "Foo" + WHITESPACE@27..28 " " + WORD@28..31 "Bar" + R_CURLY@31..32 "}" + COMMA@32..33 "," + R_CURLY@33..34 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap new file mode 100644 index 0000000000..f47b3b6917 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap @@ -0,0 +1,42 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@article{foo, author = {Foo Bar}, title = {Hello World}}\"#)" + +--- +ROOT@0..56 + ENTRY@0..56 + ENTRY_TYPE@0..8 "@article" + L_CURLY@8..9 "{" + WORD@9..12 "foo" + COMMA@12..13 "," + WHITESPACE@13..14 " " + FIELD@14..32 + WORD@14..20 "author" + WHITESPACE@20..21 " " + EQUALITY_SIGN@21..22 "=" + WHITESPACE@22..23 " " + VALUE@23..32 + TOKEN@23..32 + BRACE_GROUP@23..32 + L_CURLY@23..24 "{" + WORD@24..27 "Foo" + WHITESPACE@27..28 " " + WORD@28..31 "Bar" + R_CURLY@31..32 "}" + COMMA@32..33 "," + WHITESPACE@33..34 " " + FIELD@34..55 + WORD@34..39 "title" + WHITESPACE@39..40 " " + EQUALITY_SIGN@40..41 "=" + WHITESPACE@41..42 " " + VALUE@42..55 + TOKEN@42..55 + BRACE_GROUP@42..55 + L_CURLY@42..43 "{" + WORD@43..48 "Hello" + WHITESPACE@48..49 " " + WORD@49..54 "World" + R_CURLY@54..55 "}" + R_CURLY@55..56 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap new file mode 100644 index 0000000000..34a9019582 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap @@ -0,0 +1,33 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@article{foo, author = {Foo Bar}, t}\"#)" + +--- +ROOT@0..36 + ENTRY@0..36 + ENTRY_TYPE@0..8 "@article" + L_CURLY@8..9 "{" + WORD@9..12 "foo" + COMMA@12..13 "," + WHITESPACE@13..14 " " + FIELD@14..32 + WORD@14..20 "author" + WHITESPACE@20..21 " " + EQUALITY_SIGN@21..22 "=" + WHITESPACE@22..23 " " + VALUE@23..32 + TOKEN@23..32 + BRACE_GROUP@23..32 + L_CURLY@23..24 "{" + WORD@24..27 "Foo" + WHITESPACE@27..28 " " + WORD@28..31 "Bar" + R_CURLY@31..32 "}" + COMMA@32..33 "," + WHITESPACE@33..34 " " + FIELD@34..35 + WORD@34..35 "t" + MISSING@35..35 "" + MISSING@35..35 "" + R_CURLY@35..36 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap new file mode 100644 index 0000000000..e77313cabf --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap @@ -0,0 +1,11 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"Hello World!\"#)" + +--- +ROOT@0..12 + JUNK@0..12 + WORD@0..5 "Hello" + WHITESPACE@5..6 " " + WORD@6..12 "World!" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap new file mode 100644 index 0000000000..f4669b8a01 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap @@ -0,0 +1,21 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@preAmbLe{ \"Hello World\" }\"#)" + +--- +ROOT@0..26 + PREAMBLE@0..26 + PREAMBLE_TYPE@0..9 "@preAmbLe" + L_CURLY@9..10 "{" + WHITESPACE@10..11 " " + VALUE@11..25 + TOKEN@11..25 + QUOTE_GROUP@11..25 + QUOTE@11..12 "\"" + WORD@12..17 "Hello" + WHITESPACE@17..18 " " + WORD@18..23 "World" + QUOTE@23..24 "\"" + WHITESPACE@24..25 " " + R_CURLY@25..26 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap new file mode 100644 index 0000000000..d1977f2e76 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap @@ -0,0 +1,21 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@preamble{ \"Hello World\" }\"#)" + +--- +ROOT@0..26 + PREAMBLE@0..26 + PREAMBLE_TYPE@0..9 "@preamble" + L_CURLY@9..10 "{" + WHITESPACE@10..11 " " + VALUE@11..25 + TOKEN@11..25 + QUOTE_GROUP@11..25 + QUOTE@11..12 "\"" + WORD@12..17 "Hello" + WHITESPACE@17..18 " " + WORD@18..23 "World" + QUOTE@23..24 "\"" + WHITESPACE@24..25 " " + R_CURLY@25..26 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap new file mode 100644 index 0000000000..4ba447a2ae --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap @@ -0,0 +1,20 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@preamble{ \"Hello World\" \"#)" + +--- +ROOT@0..24 + PREAMBLE@0..24 + PREAMBLE_TYPE@0..9 "@preamble" + L_CURLY@9..10 "{" + WHITESPACE@10..11 " " + VALUE@11..24 + TOKEN@11..24 + QUOTE_GROUP@11..24 + QUOTE@11..12 "\"" + WORD@12..17 "Hello" + WHITESPACE@17..18 " " + WORD@18..23 "World" + QUOTE@23..24 "\"" + MISSING@24..24 "" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap new file mode 100644 index 0000000000..1c8b1dbea4 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap @@ -0,0 +1,23 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@STRING{foo = \"Hello World\"}\"#)" + +--- +ROOT@0..28 + STRING@0..28 + STRING_TYPE@0..7 "@STRING" + L_CURLY@7..8 "{" + WORD@8..11 "foo" + WHITESPACE@11..12 " " + EQUALITY_SIGN@12..13 "=" + WHITESPACE@13..14 " " + VALUE@14..27 + TOKEN@14..27 + QUOTE_GROUP@14..27 + QUOTE@14..15 "\"" + WORD@15..20 "Hello" + WHITESPACE@20..21 " " + WORD@21..26 "World" + QUOTE@26..27 "\"" + R_CURLY@27..28 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap new file mode 100644 index 0000000000..506452894d --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap @@ -0,0 +1,23 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@string{foo = {Hello World}}\"#)" + +--- +ROOT@0..28 + STRING@0..28 + STRING_TYPE@0..7 "@string" + L_CURLY@7..8 "{" + WORD@8..11 "foo" + WHITESPACE@11..12 " " + EQUALITY_SIGN@12..13 "=" + WHITESPACE@13..14 " " + VALUE@14..27 + TOKEN@14..27 + BRACE_GROUP@14..27 + L_CURLY@14..15 "{" + WORD@15..20 "Hello" + WHITESPACE@20..21 " " + WORD@21..26 "World" + R_CURLY@26..27 "}" + R_CURLY@27..28 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap new file mode 100644 index 0000000000..7f457d6964 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap @@ -0,0 +1,43 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@string{foo = {Hello World}} @string{bar = foo # \"!\"}\"#)" + +--- +ROOT@0..53 + STRING@0..29 + STRING_TYPE@0..7 "@string" + L_CURLY@7..8 "{" + WORD@8..11 "foo" + WHITESPACE@11..12 " " + EQUALITY_SIGN@12..13 "=" + WHITESPACE@13..14 " " + VALUE@14..27 + TOKEN@14..27 + BRACE_GROUP@14..27 + L_CURLY@14..15 "{" + WORD@15..20 "Hello" + WHITESPACE@20..21 " " + WORD@21..26 "World" + R_CURLY@26..27 "}" + R_CURLY@27..28 "}" + WHITESPACE@28..29 " " + STRING@29..53 + STRING_TYPE@29..36 "@string" + L_CURLY@36..37 "{" + WORD@37..40 "bar" + WHITESPACE@40..41 " " + EQUALITY_SIGN@41..42 "=" + WHITESPACE@42..43 " " + VALUE@43..52 + TOKEN@43..46 + WORD@43..46 "foo" + WHITESPACE@46..47 " " + HASH@47..48 "#" + WHITESPACE@48..49 " " + TOKEN@49..52 + QUOTE_GROUP@49..52 + QUOTE@49..50 "\"" + WORD@50..51 "!" + QUOTE@51..52 "\"" + R_CURLY@52..53 "}" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap new file mode 100644 index 0000000000..685d02a179 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap @@ -0,0 +1,23 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@string{foo = {Hello World}\"#)" + +--- +ROOT@0..27 + STRING@0..27 + STRING_TYPE@0..7 "@string" + L_CURLY@7..8 "{" + WORD@8..11 "foo" + WHITESPACE@11..12 " " + EQUALITY_SIGN@12..13 "=" + WHITESPACE@13..14 " " + VALUE@14..27 + TOKEN@14..27 + BRACE_GROUP@14..27 + L_CURLY@14..15 "{" + WORD@15..20 "Hello" + WHITESPACE@20..21 " " + WORD@21..26 "World" + R_CURLY@26..27 "}" + MISSING@27..27 "" + diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap new file mode 100644 index 0000000000..8eeefa9bf9 --- /dev/null +++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap @@ -0,0 +1,23 @@ +--- +source: src/syntax/bibtex/parser.rs +expression: "setup(r#\"@STRING{foo = \"Hello World}\"#)" + +--- +ROOT@0..27 + STRING@0..27 + STRING_TYPE@0..7 "@STRING" + L_CURLY@7..8 "{" + WORD@8..11 "foo" + WHITESPACE@11..12 " " + EQUALITY_SIGN@12..13 "=" + WHITESPACE@13..14 " " + VALUE@14..26 + TOKEN@14..26 + QUOTE_GROUP@14..26 + QUOTE@14..15 "\"" + WORD@15..20 "Hello" + WHITESPACE@20..21 " " + WORD@21..26 "World" + MISSING@26..26 "" + R_CURLY@26..27 "}" + diff --git a/support/texlab/src/diagnostics/build.rs b/support/texlab/src/syntax/build_log.rs index 16870e8fd8..65a5938262 100644 --- a/support/texlab/src/diagnostics/build.rs +++ b/support/texlab/src/syntax/build_log.rs @@ -1,136 +1,20 @@ -use crate::{ - protocol::{Diagnostic, DiagnosticSeverity, Options, Position, Range, Uri}, - workspace::{Document, Snapshot}, -}; -use futures::lock::Mutex; +use std::{cmp::Ordering, path::PathBuf}; + use once_cell::sync::Lazy; use regex::{Match, Regex}; -use std::{ - cmp::Ordering, - collections::HashMap, - io, - path::{Path, PathBuf}, - str, - time::SystemTime, -}; -use tokio::fs; - -#[derive(Debug, PartialEq, Eq, Clone)] -struct LogFile { - path: PathBuf, - modified: SystemTime, -} - -#[derive(Debug, Default)] -pub struct BuildDiagnosticsProvider { - diagnostics_by_uri: Mutex<HashMap<Uri, Vec<Diagnostic>>>, - log_files: Mutex<Vec<LogFile>>, -} - -impl BuildDiagnosticsProvider { - pub async fn get(&self, doc: &Document) -> Vec<Diagnostic> { - match self.diagnostics_by_uri.lock().await.get(&doc.uri) { - Some(diagnostics) => diagnostics.to_owned(), - None => Vec::new(), - } - } - - pub async fn update( - &self, - snapshot: &Snapshot, - tex_uri: &Uri, - options: &Options, - current_dir: &Path, - ) -> io::Result<bool> { - let log_uris = snapshot - .resolve_aux_targets(tex_uri, options, current_dir, "log") - .unwrap_or_default(); - if let Some(log_path) = log_uris - .into_iter() - .filter(|uri| uri.scheme() == "file") - .filter_map(|uri| uri.to_file_path().ok()) - .find(|path| path.exists()) - { - let modified = fs::metadata(&log_path).await?.modified()?; - let mut log_files = self.log_files.lock().await; - for log_file in log_files.iter_mut() { - if log_file.path == log_path { - return if modified > log_file.modified { - log_file.modified = modified; - self.update_diagnostics(tex_uri, &log_path).await - } else { - Ok(false) - }; - } - } - - self.update_diagnostics(tex_uri, &log_path).await?; - log_files.push(LogFile { - path: log_path, - modified, - }); - } - - Ok(true) - } - - async fn update_diagnostics(&self, tex_uri: &Uri, log_path: &Path) -> io::Result<bool> { - let log = String::from_utf8_lossy(&fs::read(log_path).await?).into_owned(); - let mut diagnostics_by_uri = self.diagnostics_by_uri.lock().await; - diagnostics_by_uri.clear(); - for error in parse_build_log(tex_uri, &log) { - let diagnostics = diagnostics_by_uri - .entry(error.uri.clone()) - .or_insert_with(Vec::new); - diagnostics.push(error.into()); - } - Ok(true) - } -} -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum BuildErrorKind { +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Hash)] +pub enum BuildErrorLevel { Error, Warning, } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub struct BuildError { - pub uri: Uri, - pub kind: BuildErrorKind, + pub relative_path: PathBuf, + pub level: BuildErrorLevel, pub message: String, - pub line: Option<u64>, -} - -impl BuildError { - pub fn new(uri: Uri, kind: BuildErrorKind, message: String, line: Option<u64>) -> Self { - Self { - uri, - kind, - message, - line, - } - } -} - -impl Into<Diagnostic> for BuildError { - fn into(self) -> Diagnostic { - let position = Position::new(self.line.unwrap_or(0), 0); - let severity = match self.kind { - BuildErrorKind::Error => DiagnosticSeverity::Error, - BuildErrorKind::Warning => DiagnosticSeverity::Warning, - }; - let range = Range::new(position, position); - Diagnostic { - range, - severity: Some(severity), - source: Some("latex".into()), - code: None, - message: self.message, - tags: None, - related_information: None, - } - } + pub line: Option<u32>, } const MAX_LINE_LENGTH: usize = 79; @@ -152,27 +36,33 @@ pub static BAD_BOX_REGEX: Lazy<Regex> = Lazy::new(|| { Regex::new("(?P<msg>(Ov|Und)erfull \\\\[hv]box[^\r\n]*lines? (?P<line>\\d+)[^\r\n]*)").unwrap() }); -fn parse_build_log(uri: &Uri, log: &str) -> Vec<BuildError> { +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct Parse { + pub errors: Vec<BuildError>, +} + +pub fn parse(log: &str) -> Parse { let log = prepare_log(log); let mut ranges: Vec<FileRange> = FILE_REGEX .find_iter(&log) - .map(|result| create_file_range(uri.clone(), &log, result)) + .map(|result| FileRange::create(&log, result)) .collect(); ranges.sort(); - let tex_errors = extract_matches(&log, &uri, &ranges, &TEX_ERROR_REGEX, BuildErrorKind::Error); - let warnings = extract_matches(&log, &uri, &ranges, &WARNING_REGEX, BuildErrorKind::Warning); - let bad_boxes = extract_matches(&log, &uri, &ranges, &BAD_BOX_REGEX, BuildErrorKind::Warning); + let tex_errors = extract_matches(&log, &ranges, &TEX_ERROR_REGEX, BuildErrorLevel::Error); + let warnings = extract_matches(&log, &ranges, &WARNING_REGEX, BuildErrorLevel::Warning); + let bad_boxes = extract_matches(&log, &ranges, &BAD_BOX_REGEX, BuildErrorLevel::Warning); - vec![tex_errors, warnings, bad_boxes].concat() + Parse { + errors: vec![tex_errors, warnings, bad_boxes].concat(), + } } fn extract_matches( log: &str, - parent_uri: &Uri, ranges: &[FileRange], regex: &Regex, - kind: BuildErrorKind, + level: BuildErrorLevel, ) -> Vec<BuildError> { let mut errors = Vec::new(); for result in regex.find_iter(&log) { @@ -191,10 +81,14 @@ fn extract_matches( if let Some(range) = ranges.iter().find(|range| range.contains(result.start())) { let line = captures .name("line") - .map(|result| u64::from_str_radix(result.as_str(), 10).unwrap() - 1); + .map(|result| u32::from_str_radix(result.as_str(), 10).unwrap() - 1); - let uri = range.uri.as_ref().unwrap_or(parent_uri); - errors.push(BuildError::new(uri.clone(), kind, message, line)); + errors.push(BuildError { + relative_path: range.path.clone(), + level, + message, + line, + }); } } errors @@ -230,52 +124,41 @@ fn prepare_log(log: &str) -> String { new_lines.join("\n") } -fn create_file_range(parent: Uri, log: &str, result: Match) -> FileRange { - let mut balance = 1; - let mut end = result.start() + 1; - let chars = (&log[result.start() + 1..]).chars(); - for c in chars { - if balance <= 0 { - break; - } - - if c == '(' { - balance += 1; - } else if c == ')' { - balance -= 1; - } - end += c.len_utf8(); - } - - let captures = FILE_REGEX.captures(result.as_str()).unwrap(); - let base_path = PathBuf::from(parent.path()); - let mut full_path = base_path.clone(); - full_path.push(captures.name("file").unwrap().as_str()); - let uri = if full_path.starts_with(base_path) { - parent - .join(captures.name("file").unwrap().as_str()) - .map(Into::into) - .ok() - } else { - None - }; - - FileRange::new(uri, result.start(), end) -} - #[derive(Debug, PartialEq, Eq, Clone)] struct FileRange { - pub uri: Option<Uri>, + pub path: PathBuf, pub start: usize, pub end: usize, } impl FileRange { - fn new(uri: Option<Uri>, start: usize, end: usize) -> Self { - Self { uri, start, end } + fn create(log: &str, result: Match) -> Self { + let mut balance = 1; + let mut end = result.start() + 1; + let chars = (&log[result.start() + 1..]).chars(); + for c in chars { + if balance <= 0 { + break; + } + + if c == '(' { + balance += 1; + } else if c == ')' { + balance -= 1; + } + end += c.len_utf8(); + } + + let captures = FILE_REGEX.captures(result.as_str()).unwrap(); + let path = PathBuf::from(captures.name("file").unwrap().as_str()); + Self { + path, + start: result.start(), + end, + } } - fn length(&self) -> usize { + fn len(&self) -> usize { self.end - self.start + 1 } @@ -285,13 +168,13 @@ impl FileRange { } impl Ord for FileRange { - fn cmp(&self, other: &FileRange) -> Ordering { - self.length().cmp(&other.length()) + fn cmp(&self, other: &Self) -> Ordering { + self.len().cmp(&other.len()) } } impl PartialOrd for FileRange { - fn partial_cmp(&self, other: &FileRange) -> Option<Ordering> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } @@ -301,15 +184,10 @@ mod tests { use super::*; use indoc::indoc; - fn create_uri(name: &str) -> Uri { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(name); - Uri::from_file_path(path.to_str().unwrap()).unwrap() - } - #[test] - fn bad_box() { - let log = indoc!( - r#" + fn test_parse_001() { + let log = indoc! { + r#" This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:50 entering extended mode restricted \write18 enabled. @@ -362,7 +240,7 @@ mod tests { Overfull \hbox (200.00162pt too wide) in paragraph at lines 8--9 []\OT1/cmr/m/n/10 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa [] LaTeX Font Info: External font `cmex10' loaded for size @@ -392,30 +270,33 @@ mod tests { 16 PDF objects out of 1000 (max. 8388607) 10 compressed objects within 1 object stream 0 named destinations out of 1000 (max. 500000) - 1 words of extra memory for PDF output out of 10000 (max. 10000000)"# - ); - - let actual_errors = parse_build_log(&create_uri("parent.tex"), log); + 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#, + }; - let error1 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Warning, - "Overfull \\hbox (200.00162pt too wide) in paragraph at lines 8--9".into(), - Some(7), - ); - let error2 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Warning, - "Overfull \\vbox (3.19998pt too high) detected at line 23".into(), - Some(22), + assert_eq!( + parse(&log).errors, + vec![ + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Warning, + message: "Overfull \\hbox (200.00162pt too wide) in paragraph at lines 8--9" + .into(), + line: Some(7), + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Warning, + message: "Overfull \\vbox (3.19998pt too high) detected at line 23".into(), + line: Some(22), + } + ] ); - assert_eq!(actual_errors, vec![error1, error2]); } #[test] - fn related() { - let log = indoc!( - r#" + fn test_parse_002() { + let log = indoc! { + r#" This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:52 entering extended mode restricted \write18 enabled. @@ -442,7 +323,7 @@ mod tests { \bibindent=\dimen102 ) (./parent.aux (./child.aux)) \openout1 = `parent.aux'. - + LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 4. LaTeX Font Info: ... okay on input line 4. LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 4. @@ -458,14 +339,14 @@ mod tests { (./child.tex ! Undefined control sequence. l.1 \foo - + The control sequence at the end of the top line of your error message was never \def'ed. If you have misspelled it (e.g., `\hobx'), type `I' and the correct spelling (e.g., `I\hbox'). Otherwise just continue, and I'll forget about whatever was undefined. - - ) (./parent.aux) ) + + ) (./parent.aux) ) Here is how much of TeX's memory you used: 205 strings out of 492995 2149 string characters out of 6138727 @@ -474,29 +355,29 @@ mod tests { 3640 words of font info for 14 fonts, out of 8000000 for 9000 1141 hyphenation exceptions out of 8191 23i,1n,17p,116b,36s stack positions out of 5000i,500n,10000p,200000b,80000s - + No pages of output. PDF statistics: 0 PDF objects out of 1000 (max. 8388607) 0 named destinations out of 1000 (max. 500000) - 1 words of extra memory for PDF output out of 10000 (max. 10000000)"# - ); - - let actual_errors = parse_build_log(&create_uri("parent.tex"), log); + 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#, + }; - let error = BuildError::new( - create_uri("child.tex"), - BuildErrorKind::Error, - "Undefined control sequence.".into(), - Some(0), + assert_eq!( + parse(&log).errors, + vec![BuildError { + relative_path: "./child.tex".into(), + level: BuildErrorLevel::Error, + message: "Undefined control sequence.".into(), + line: Some(0) + }] ); - assert_eq!(actual_errors, vec![error]); } #[test] - fn citation_warning() { - let log = indoc!( - r#" + fn test_parse_003() { + let log = indoc! { + r#" This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:51 entering extended mode restricted \write18 enabled. @@ -523,7 +404,7 @@ mod tests { \bibindent=\dimen102 ) (./parent.aux) \openout1 = `parent.aux'. - + LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 4. LaTeX Font Info: ... okay on input line 4. LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 4. @@ -536,17 +417,16 @@ mod tests { LaTeX Font Info: ... okay on input line 4. LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 4. LaTeX Font Info: ... okay on input line 4. - - + LaTeX Warning: Citation `foo' on page 1 undefined on input line 6. - + [1 - + {/TexLive/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (./parent.aux) - + LaTeX Warning: There were undefined references. - - ) + + ) Here is how much of TeX's memory you used: 204 strings out of 492995 2142 string characters out of 6138727 @@ -562,30 +442,32 @@ mod tests { 16 PDF objects out of 1000 (max. 8388607) 10 compressed objects within 1 object stream 0 named destinations out of 1000 (max. 500000) - 1 words of extra memory for PDF output out of 10000 (max. 10000000)"# - ); - - let actual_errors = parse_build_log(&create_uri("parent.tex"), log); + 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#, + }; - let error1 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Warning, - "Citation `foo' on page 1 undefined on input line 6.".into(), - None, - ); - let error2 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Warning, - "There were undefined references.".into(), - None, + assert_eq!( + parse(&log).errors, + vec![ + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Warning, + message: "Citation `foo' on page 1 undefined on input line 6.".into(), + line: None + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Warning, + message: "There were undefined references.".into(), + line: None, + } + ] ); - assert_eq!(actual_errors, vec![error1, error2]); } #[test] - fn package_error() { - let log = indoc!( - r#" + fn test_parse_004() { + let log = indoc! { + r#" This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:40 entering extended mode restricted \write18 enabled. @@ -613,37 +495,35 @@ mod tests { ) (/TexLive/texmf-dist/tex/generic/babel/babel.sty Package: babel 2017/05/19 3.10 The Babel package - - + ! Package babel Error: Unknown option `foo'. Either you misspelled it (babel) or the language definition file foo.ldf was not found. - + See the babel package documentation for explanation. Type H <return> for immediate help. - ... - + ... + l.393 \ProcessOptions* - + Valid options are: shorthands=, KeepShorthandsActive, activeacute, activegrave, noconfigs, safe=, main=, math= headfoot=, strings=, config=, hyphenmap=, or a language name. - - + ! Package babel Error: You haven't specified a language option. - + See the babel package documentation for explanation. Type H <return> for immediate help. - ... - + ... + l.426 ...ry to proceed from here, type x to quit.} - + You need to specify a language, either as a global option or as an optional argument to the \usepackage command; You shouldn't try to proceed from here, type x to quit. - + ) (./parent.aux) \openout1 = `parent.aux'. - + LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 5. LaTeX Font Info: ... okay on input line 5. LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 5. @@ -656,7 +536,7 @@ mod tests { LaTeX Font Info: ... okay on input line 5. LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 5. LaTeX Font Info: ... okay on input line 5. - (./parent.aux) ) + (./parent.aux) ) Here is how much of TeX's memory you used: 304 strings out of 492995 3421 string characters out of 6138727 @@ -665,36 +545,37 @@ mod tests { 3640 words of font info for 14 fonts, out of 8000000 for 9000 1141 hyphenation exceptions out of 8191 23i,1n,17p,116b,36s stack positions out of 5000i,500n,10000p,200000b,80000s - + No pages of output. PDF statistics: 0 PDF objects out of 1000 (max. 8388607) 0 named destinations out of 1000 (max. 500000) - 1 words of extra memory for PDF output out of 10000 (max. 10000000)"# - ); - - let actual_errors = parse_build_log(&create_uri("parent.tex"), log); + 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#, + }; - let error1 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Error, - "Package babel Error: Unknown option `foo'. Either you misspelled it or the language definition file foo.ldf was not found." - .into(), - Some(392), - ); - let error2 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Error, - "Package babel Error: You haven't specified a language option.".into(), - Some(425), + assert_eq!( + parse(&log).errors, + vec![ + BuildError { + relative_path: "/TexLive/texmf-dist/tex/generic/babel/babel.sty".into(), + level: BuildErrorLevel::Error, + message: "Package babel Error: Unknown option `foo'. Either you misspelled it or the language definition file foo.ldf was not found.".into(), + line: Some(392) + }, + BuildError { + relative_path: "/TexLive/texmf-dist/tex/generic/babel/babel.sty".into(), + level: BuildErrorLevel::Error, + message: "Package babel Error: You haven't specified a language option.".into(), + line: Some(425) + } + ] ); - assert_eq!(actual_errors, vec![error1, error2]); } #[test] - fn package_warning() { - let log = indoc!( - r#" + fn test_parse_005() { + let log = indoc! { + r#" This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:45 entering extended mode restricted \write18 enabled. @@ -722,10 +603,10 @@ mod tests { ) (/TexLive/texmf-dist/tex/generic/babel/babel.sty Package: babel 2017/05/19 3.10 The Babel package - + (/TexLive/texmf-dist/tex/generic/babel-spanish/spanish.ldf Language: spanish.ldf 2016/03/03 v5.0p Spanish support from the babel system - + (/TexLive/texmf-dist/tex/generic/babel/babel.def File: babel.def 2017/05/19 3.10 Babel common definitions \babel@savecnt=\count87 @@ -740,11 +621,11 @@ mod tests { )) (/TexLive/texmf-dist/tex/latex/biblatex/biblatex.sty Package: biblatex 2016/12/05 v3.7 programmable bibliographies (PK/JW/AB) - + (/TexLive/texmf-dist/tex/generic/oberdiek/pdftexcmds.sty Package: pdftexcmds 2017/03/19 v0.25 Utility functions of pdfTeX for LuaTeX (HO ) - + (/TexLive/texmf-dist/tex/generic/oberdiek/infwarerr.sty Package: infwarerr 2016/05/16 v1.4 Providing info/warning/error messages (HO) ) @@ -773,10 +654,10 @@ mod tests { ) (/TexLive/texmf-dist/tex/latex/oberdiek/kvoptions.sty Package: kvoptions 2016/05/16 v3.12 Key value format for package options (HO) - + (/TexLive/texmf-dist/tex/generic/oberdiek/kvsetkeys.sty Package: kvsetkeys 2016/05/16 v1.17 Key value parser (HO) - + (/TexLive/texmf-dist/tex/generic/oberdiek/etexcmds.sty Package: etexcmds 2016/05/16 v1.6 Avoid name clashes with e-TeX commands (HO) Package etexcmds Info: Could not find \expanded. @@ -787,7 +668,7 @@ mod tests { (/TexLive/texmf-dist/tex/latex/logreq/logreq.sty Package: logreq 2010/08/04 v1.0 xml request logger \lrq@indent=\count90 - + (/TexLive/texmf-dist/tex/latex/logreq/logreq.def File: logreq.def 2010/08/04 v1.0 logreq spec v1.0 )) @@ -859,7 +740,7 @@ mod tests { \c@mincompwidth=\count133 Package biblatex Info: Trying to load biblatex default data model... Package biblatex Info: ... file 'blx-dm.def' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/blx-dm.def) Package biblatex Info: Trying to load biblatex style data model... Package biblatex Info: ... file 'ieee.dbx' not found. @@ -939,13 +820,13 @@ mod tests { \shorteditorwidth=\skip56 Package biblatex Info: Trying to load compatibility code... Package biblatex Info: ... file 'blx-compat.def' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/blx-compat.def File: blx-compat.def 2016/12/05 v3.7 biblatex compatibility (PK/JW/AB) ) Package biblatex Info: Trying to load generic definitions... Package biblatex Info: ... file 'biblatex.def' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/biblatex.def File: biblatex.def 2016/12/05 v3.7 biblatex compatibility (PK/JW/AB) \c@textcitecount=\count266 @@ -958,22 +839,22 @@ mod tests { ) Package biblatex Info: Trying to load bibliography style 'ieee'... Package biblatex Info: ... file 'ieee.bbx' found. - + (/TexLive/texmf-dist/tex/latex/biblatex-ieee/ieee.bbx File: ieee.bbx 2017/03/27 v1.2d biblatex bibliography style Package biblatex Info: Trying to load bibliography style 'numeric-comp'... Package biblatex Info: ... file 'numeric-comp.bbx' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/bbx/numeric-comp.bbx File: numeric-comp.bbx 2016/12/05 v3.7 biblatex bibliography style (PK/JW/AB) Package biblatex Info: Trying to load bibliography style 'numeric'... Package biblatex Info: ... file 'numeric.bbx' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/bbx/numeric.bbx File: numeric.bbx 2016/12/05 v3.7 biblatex bibliography style (PK/JW/AB) Package biblatex Info: Trying to load bibliography style 'standard'... Package biblatex Info: ... file 'standard.bbx' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/bbx/standard.bbx File: standard.bbx 2016/12/05 v3.7 biblatex bibliography style (PK/JW/AB) \c@bbx:relatedcount=\count273 @@ -981,12 +862,12 @@ mod tests { )))) Package biblatex Info: Trying to load citation style 'ieee'... Package biblatex Info: ... file 'ieee.cbx' found. - + (/TexLive/texmf-dist/tex/latex/biblatex-ieee/ieee.cbx File: ieee.cbx 2017/03/27 v1.2d biblatex citation style Package biblatex Info: Trying to load citation style 'numeric-comp'... Package biblatex Info: ... file 'numeric-comp.cbx' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/cbx/numeric-comp.cbx File: numeric-comp.cbx 2016/12/05 v3.7 biblatex citation style (PK/JW/AB) \c@cbx@tempcnta=\count275 @@ -1008,25 +889,25 @@ mod tests { ) Package biblatex Info: Trying to load configuration file... Package biblatex Info: ... file 'biblatex.cfg' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/biblatex.cfg - File: biblatex.cfg + File: biblatex.cfg )) Package biblatex Info: Trying to load language 'spanish'... Package biblatex Info: ... file 'spanish.lbx' found. - + (/TexLive/texmf-dist/tex/latex/biblatex/lbx/spanish.lbx File: spanish.lbx 2016/12/05 v3.7 biblatex localization (PK/JW/AB) ) - + Package biblatex Warning: 'babel/polyglossia' detected but 'csquotes' missing. (biblatex) Loading 'csquotes' recommended. - + \@quotelevel=\count277 \@quotereset=\count278 (./parent.aux) \openout1 = `parent.aux'. - + LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 6. LaTeX Font Info: ... okay on input line 6. LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 6. @@ -1047,26 +928,25 @@ mod tests { Package biblatex Info: Automatic encoding selection. (biblatex) Assuming data encoding 'ascii'. \openout4 = `parent.bcf'. - + Package biblatex Info: Trying to load bibliographic data... Package biblatex Info: ... file 'parent.bbl' not found. - + No file parent.bbl. Package biblatex Info: Reference section=0 on input line 6. Package biblatex Info: Reference segment=0 on input line 6. (./parent.aux) - + LaTeX Warning: There were undefined references. - - + Package biblatex Warning: Please (re)run Biber on the file: (biblatex) parent (biblatex) and rerun LaTeX afterwards. - + Package logreq Info: Writing requests to 'parent.run.xml'. \openout1 = `parent.run.xml'. - - ) + + ) Here is how much of TeX's memory you used: 7717 strings out of 492995 133301 string characters out of 6138727 @@ -1075,41 +955,42 @@ mod tests { 3640 words of font info for 14 fonts, out of 8000000 for 9000 1141 hyphenation exceptions out of 8191 35i,1n,30p,856b,700s stack positions out of 5000i,500n,10000p,200000b,80000s - + No pages of output. PDF statistics: 0 PDF objects out of 1000 (max. 8388607) 0 named destinations out of 1000 (max. 500000) - 1 words of extra memory for PDF output out of 10000 (max. 10000000)"# - ); - - let actual_errors = parse_build_log(&create_uri("parent.tex"), log); + 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#, + }; - let error1 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Warning, - "'babel/polyglossia' detected but 'csquotes' missing. Loading 'csquotes' recommended." - .to_owned(), - None, - ); - let error2 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Warning, - "There were undefined references.".into(), - None, - ); - let error3 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Warning, - "Please (re)run Biber on the file: parent and rerun LaTeX afterwards.".into(), - None, + assert_eq!( + parse(&log).errors, + vec![ + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Warning, + message: "'babel/polyglossia' detected but 'csquotes' missing. Loading 'csquotes' recommended.".into(), + line: None + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Warning, + message: "There were undefined references.".into(), + line: None, + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Warning, + message: "Please (re)run Biber on the file: parent and rerun LaTeX afterwards.".into(), + line: None + } + ] ); - assert_eq!(actual_errors, vec![error1, error2, error3]); } #[test] - fn tex_error() { - let log = indoc!( + fn test_parse_006() { + let log = indoc! { r#" This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/W32TeX) (preloaded format=pdflatex 2018.3.30) 26 DEC 2018 16:33 entering extended mode @@ -1138,7 +1019,7 @@ mod tests { ) (./parent.aux (./child.tex.aux)) \openout1 = `parent.aux'. - + LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 3. LaTeX Font Info: ... okay on input line 3. LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 3. @@ -1152,73 +1033,71 @@ mod tests { LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 3. LaTeX Font Info: ... okay on input line 3. \openout2 = `child.aux'. - + (./child.tex) ! Undefined control sequence. l.7 \foo - + The control sequence at the end of the top line of your error message was never \def'ed. If you have misspelled it (e.g., `\hobx'), type `I' and the correct spelling (e.g., `I\hbox'). Otherwise just continue, and I'll forget about whatever was undefined. - + ! Missing $ inserted. - <inserted text> + <inserted text> $ l.8 \bar - + I've inserted a begin-math/end-math symbol since I think you left one out. Proceed, with fingers crossed. - + LaTeX Font Info: External font `cmex10' loaded for size (Font) <7> on input line 8. LaTeX Font Info: External font `cmex10' loaded for size (Font) <5> on input line 8. ! Undefined control sequence. l.9 \baz - + The control sequence at the end of the top line of your error message was never \def'ed. If you have misspelled it (e.g., `\hobx'), type `I' and the correct spelling (e.g., `I\hbox'). Otherwise just continue, and I'll forget about whatever was undefined. - + ! Missing { inserted. - <to be read again> - \par - l.10 - + <to be read again> + \par + l.10 + A left brace was mandatory here, so I've put one in. You might want to delete and/or insert some corrections so that I will find a matching right brace soon. (If you're confused by all this, try typing `I}' now.) - + ! Missing $ inserted. - <inserted text> + <inserted text> $ - l.10 - + l.10 + I've inserted a begin-math/end-math symbol since I think you left one out. Proceed, with fingers crossed. - + ! Missing } inserted. - <inserted text> + <inserted text> } - l.10 - + l.10 + I've inserted something that you may have forgotten. (See the <inserted text> above.) With luck, this will get me unwedged. But if you really didn't forget anything, try typing `2' now; then my insertion and my current dilemma will both disappear. - + [1 - - - + {/TexLive/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (./parent.aux - (./child.aux)) ) + (./child.aux)) ) Here is how much of TeX's memory you used: 212 strings out of 492995 2238 string characters out of 6138727 @@ -1234,50 +1113,49 @@ mod tests { 12 PDF objects out of 1000 (max. 8388607) 7 compressed objects within 1 object stream 0 named destinations out of 1000 (max. 500000) - 1 words of extra memory for PDF output out of 10000 (max. 10000000)"# - ); - - let actual_errors = parse_build_log(&create_uri("parent.tex"), log); + 1 words of extra memory for PDF output out of 10000 (max. 10000000)"#, + }; - let error1 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Error, - "Undefined control sequence.".into(), - Some(6), - ); - let error2 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Error, - "Missing $ inserted.".into(), - Some(7), - ); - let error3 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Error, - "Undefined control sequence.".into(), - Some(8), - ); - let error4 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Error, - "Missing { inserted.".into(), - Some(9), - ); - let error5 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Error, - "Missing $ inserted.".into(), - Some(9), - ); - let error6 = BuildError::new( - create_uri("parent.tex"), - BuildErrorKind::Error, - "Missing } inserted.".into(), - Some(9), - ); assert_eq!( - actual_errors, - vec![error1, error2, error3, error4, error5, error6] + parse(&log).errors, + vec![ + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Error, + message: "Undefined control sequence.".into(), + line: Some(6) + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Error, + message: "Missing $ inserted.".into(), + line: Some(7) + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Error, + message: "Undefined control sequence.".into(), + line: Some(8) + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Error, + message: "Missing { inserted.".into(), + line: Some(9) + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Error, + message: "Missing $ inserted.".into(), + line: Some(9) + }, + BuildError { + relative_path: "./parent.tex".into(), + level: BuildErrorLevel::Error, + message: "Missing } inserted.".into(), + line: Some(9) + }, + ] ); } } diff --git a/support/texlab/src/syntax/generic_ast.rs b/support/texlab/src/syntax/generic_ast.rs deleted file mode 100644 index 3311460c0a..0000000000 --- a/support/texlab/src/syntax/generic_ast.rs +++ /dev/null @@ -1,51 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::ops::Index; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct AstNodeIndex(usize); - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct Ast<T> { - nodes: Vec<T>, - edges: Vec<Vec<AstNodeIndex>>, -} - -impl<T> Ast<T> { - pub fn new() -> Self { - Self { - nodes: Vec::new(), - edges: Vec::new(), - } - } - - pub fn nodes(&self) -> Vec<AstNodeIndex> { - let mut nodes = Vec::new(); - for i in 0..self.nodes.len() { - nodes.push(AstNodeIndex(i)); - } - nodes - } - - pub fn add_node(&mut self, value: T) -> AstNodeIndex { - let node = AstNodeIndex(self.nodes.len()); - self.nodes.push(value); - self.edges.push(Vec::new()); - node - } - - pub fn add_edge(&mut self, parent: AstNodeIndex, child: AstNodeIndex) { - self.edges[parent.0].push(child); - } - - pub fn children<'a>(&'a self, parent: AstNodeIndex) -> impl Iterator<Item = AstNodeIndex> + 'a { - self.edges[parent.0].iter().map(|child| *child) - } -} - -impl<T> Index<AstNodeIndex> for Ast<T> { - type Output = T; - - fn index(&self, index: AstNodeIndex) -> &Self::Output { - &self.nodes[index.0] - } -} diff --git a/support/texlab/src/syntax/lang_data.rs b/support/texlab/src/syntax/lang_data.rs deleted file mode 100644 index 4ea7cc44da..0000000000 --- a/support/texlab/src/syntax/lang_data.rs +++ /dev/null @@ -1,231 +0,0 @@ -use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexEnvironmentCommand { - pub name: String, - pub index: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexCitationCommand { - pub name: String, - pub index: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum LatexLabelReferenceSource { - Everything, - Math, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum LatexLabelKind { - Definition, - Reference(LatexLabelReferenceSource), -} - -impl LatexLabelKind { - pub fn is_reference(self) -> bool { - match self { - LatexLabelKind::Definition => false, - LatexLabelKind::Reference(_) => true, - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexLabelCommand { - pub name: String, - pub index: usize, - pub kind: LatexLabelKind, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexSectionCommand { - pub name: String, - pub index: usize, - pub level: i32, - pub prefix: String, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum LatexIncludeKind { - Package, - Class, - Latex, - Bibliography, - Image, - Svg, - Pdf, - Everything, -} - -impl LatexIncludeKind { - pub fn extensions(self) -> Option<&'static [&'static str]> { - match self { - LatexIncludeKind::Package => Some(&["sty"]), - LatexIncludeKind::Class => Some(&["cls"]), - LatexIncludeKind::Latex => Some(&["tex"]), - LatexIncludeKind::Bibliography => Some(&["bib"]), - LatexIncludeKind::Image => Some(&["pdf", "png", "jpg", "jpeg", "bmp"]), - LatexIncludeKind::Svg => Some(&["svg"]), - LatexIncludeKind::Pdf => Some(&["pdf"]), - LatexIncludeKind::Everything => None, - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexIncludeCommand { - pub name: String, - pub index: usize, - pub kind: LatexIncludeKind, - pub include_extension: bool, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexCommandDefinitionCommand { - pub name: String, - pub definition_index: usize, - pub arg_count_index: usize, - pub implementation_index: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexMathOperatorCommand { - pub name: String, - pub definition_index: usize, - pub implementation_index: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexTheoremDefinitionCommand { - pub name: String, - pub index: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexColorCommand { - pub name: String, - pub index: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexColorModelCommand { - pub name: String, - pub index: usize, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum LatexGlossaryEntryKind { - General, - Acronym, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexGlossaryEntryDefinitionCommand { - pub name: String, - pub label_index: usize, - pub kind: LatexGlossaryEntryKind, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LatexGlossaryEntryReferenceCommand { - pub name: String, - pub index: usize, - pub kind: LatexGlossaryEntryKind, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum BibtexEntryTypeCategory { - Misc, - String, - Article, - Book, - Collection, - Part, - Thesis, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BibtexEntryTypeDoc { - pub name: String, - pub category: BibtexEntryTypeCategory, - pub documentation: Option<String>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BibtexFieldDoc { - pub name: String, - pub documentation: String, -} - -#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LanguageData { - pub environment_commands: Vec<LatexEnvironmentCommand>, - pub citation_commands: Vec<LatexCitationCommand>, - pub label_commands: Vec<LatexLabelCommand>, - pub section_commands: Vec<LatexSectionCommand>, - pub include_commands: Vec<LatexIncludeCommand>, - pub command_definition_commands: Vec<LatexCommandDefinitionCommand>, - pub math_operator_commands: Vec<LatexMathOperatorCommand>, - pub theorem_definition_commands: Vec<LatexTheoremDefinitionCommand>, - pub colors: Vec<String>, - pub color_commands: Vec<LatexColorCommand>, - pub color_model_commands: Vec<LatexColorModelCommand>, - pub glossary_entry_definition_commands: Vec<LatexGlossaryEntryDefinitionCommand>, - pub glossary_entry_reference_commands: Vec<LatexGlossaryEntryReferenceCommand>, - pub entry_types: Vec<BibtexEntryTypeDoc>, - pub fields: Vec<BibtexFieldDoc>, - pub pgf_libraries: Vec<String>, - pub tikz_libraries: Vec<String>, - pub math_environments: Vec<String>, - pub enum_environments: Vec<String>, -} - -impl LanguageData { - pub fn find_entry_type(&self, name: &str) -> Option<&BibtexEntryTypeDoc> { - let name = name.to_lowercase(); - self.entry_types - .iter() - .find(|ty| ty.name.to_lowercase() == name) - } - - pub fn entry_type_documentation(&self, name: &str) -> Option<&str> { - self.find_entry_type(name) - .and_then(|ty| ty.documentation.as_ref().map(AsRef::as_ref)) - } - - pub fn field_documentation(&self, name: &str) -> Option<&str> { - self.fields - .iter() - .find(|field| field.name.to_lowercase() == name.to_lowercase()) - .map(|field| field.documentation.as_ref()) - } -} - -pub static LANGUAGE_DATA: Lazy<LanguageData> = Lazy::new(|| { - const JSON: &str = include_str!("../../data/lang_data.json"); - serde_json::from_str(JSON).expect("Failed to deserialize language.json") -}); diff --git a/support/texlab/src/syntax/latex.rs b/support/texlab/src/syntax/latex.rs new file mode 100644 index 0000000000..7839c1795b --- /dev/null +++ b/support/texlab/src/syntax/latex.rs @@ -0,0 +1,36 @@ +mod analysis; +mod cst; +mod kind; +mod lexer; +mod parser; + +pub use self::{ + analysis::*, + cst::*, + kind::SyntaxKind::{self, *}, + parser::{parse, Parse}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Language {} + +impl cstree::Language for Language { + type Kind = SyntaxKind; + + fn kind_from_raw(raw: cstree::SyntaxKind) -> Self::Kind { + assert!(raw.0 <= ROOT as u16); + unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) } + } + + fn kind_to_raw(kind: Self::Kind) -> cstree::SyntaxKind { + kind.into() + } +} + +pub type SyntaxNode = cstree::ResolvedNode<Language>; + +pub type SyntaxToken = cstree::ResolvedToken<Language>; + +pub type SyntaxElement = cstree::ResolvedElement<Language>; + +pub type SyntaxElementRef<'a> = cstree::ResolvedElementRef<'a, Language>; diff --git a/support/texlab/src/syntax/latex/analysis.rs b/support/texlab/src/syntax/latex/analysis.rs index af8b756f31..063d8e95f3 100644 --- a/support/texlab/src/syntax/latex/analysis.rs +++ b/support/texlab/src/syntax/latex/analysis.rs @@ -1,913 +1,36 @@ -use crate::{ - protocol::{Options, Position, Range, RangeExt, Uri}, - syntax::{generic_ast::AstNodeIndex, lang_data::*, latex::ast::*, text::SyntaxNode}, - tex::Resolver, +mod command; +mod distro_file; +mod environment; +mod explicit_link; +mod implicit_link; +mod label_name; +mod label_number; +mod theorem; +mod types; + +use crate::syntax::latex; + +pub use self::types::*; +use self::{ + command::analyze_command, + environment::analyze_begin, + explicit_link::{analyze_import, analyze_include}, + implicit_link::analyze_implicit_links, + label_name::analyze_label_name, + label_number::analyze_label_number, + theorem::analyze_theorem_definition, }; -use itertools::{iproduct, Itertools}; -use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, ops::Deref, path::Path}; -#[derive(Debug, Clone)] -pub struct SymbolTableParams<'a> { - pub tree: Tree, - pub uri: &'a Uri, - pub resolver: &'a Resolver, - pub options: &'a Options, - pub current_dir: &'a Path, -} - -#[derive(Debug, Clone)] -pub struct SymbolTable { - pub(crate) tree: Tree, - pub commands: Vec<AstNodeIndex>, - pub environments: Vec<Environment>, - pub is_standalone: bool, - pub includes: Vec<Include>, - pub imports: Vec<Import>, - pub components: Vec<String>, - pub citations: Vec<Citation>, - pub command_definitions: Vec<CommandDefinition>, - pub glossary_entries: Vec<GlossaryEntry>, - pub equations: Vec<Equation>, - pub inlines: Vec<Inline>, - pub math_operators: Vec<MathOperator>, - pub theorem_definitions: Vec<TheoremDefinition>, - pub sections: Vec<Section>, - pub labels: Vec<Label>, - pub label_numberings: Vec<LabelNumbering>, - pub captions: Vec<Caption>, - pub items: Vec<Item>, -} - -impl SymbolTable { - pub fn analyze(params: SymbolTableParams) -> Self { - let SymbolTableParams { - tree, - uri, - resolver, - options, - current_dir, - } = params; - - let commands: Vec<_> = tree.commands().collect(); - let ctx = SymbolContext { - tree: &tree, - commands: &commands, - uri, - resolver, - options, - current_dir, - }; - - let mut environments = None; - let mut includes = None; - let mut imports = None; - let mut citations = None; - let mut command_definitions = None; - let mut glossary_entries = None; - let mut equations = None; - let mut inlines = None; - let mut math_operators = None; - let mut theorem_definitions = None; - let mut sections = None; - let mut labels = None; - let mut label_numberings = None; - let mut captions = None; - let mut items = None; - - rayon::scope(|s| { - s.spawn(|_| environments = Some(Environment::parse(ctx))); - s.spawn(|_| includes = Some(Include::parse(ctx))); - s.spawn(|_| imports = Some(Import::parse(ctx))); - s.spawn(|_| citations = Some(Citation::parse(ctx))); - s.spawn(|_| command_definitions = Some(CommandDefinition::parse(ctx))); - s.spawn(|_| glossary_entries = Some(GlossaryEntry::parse(ctx))); - s.spawn(|_| equations = Some(Equation::parse(ctx))); - s.spawn(|_| inlines = Some(Inline::parse(ctx))); - s.spawn(|_| math_operators = Some(MathOperator::parse(ctx))); - s.spawn(|_| theorem_definitions = Some(TheoremDefinition::parse(ctx))); - s.spawn(|_| sections = Some(Section::parse(ctx))); - s.spawn(|_| labels = Some(Label::parse(ctx))); - s.spawn(|_| label_numberings = Some(LabelNumbering::parse(ctx))); - s.spawn(|_| captions = Some(Caption::parse(ctx))); - s.spawn(|_| items = Some(Item::parse(ctx))); - }); - - let is_standalone = environments - .as_ref() - .unwrap() - .iter() - .any(|env| env.is_root(&tree)); - - let components = includes - .as_ref() - .unwrap() - .iter() - .flat_map(|include| include.components(&tree)) - .collect(); - - Self { - tree, - commands, - environments: environments.unwrap(), - is_standalone, - includes: includes.unwrap(), - imports: imports.unwrap(), - components, - citations: citations.unwrap(), - command_definitions: command_definitions.unwrap(), - glossary_entries: glossary_entries.unwrap(), - equations: equations.unwrap(), - inlines: inlines.unwrap(), - math_operators: math_operators.unwrap(), - theorem_definitions: theorem_definitions.unwrap(), - sections: sections.unwrap(), - labels: labels.unwrap(), - label_numberings: label_numberings.unwrap(), - captions: captions.unwrap(), - items: items.unwrap(), - } - } - - pub fn is_direct_child(&self, env: Environment, pos: Position) -> bool { - env.range(&self.tree).contains(pos) - && !self - .environments - .iter() - .filter(|e| e.left.parent != env.left.parent) - .filter(|e| env.range(&self.tree).contains(e.range(&self.tree).start)) - .any(|e| e.range(&self.tree).contains(pos)) - } - - pub fn is_enum_item(&self, enumeration: Environment, item: Item) -> bool { - let item_range = self.tree[item.parent].range(); - enumeration.range(&self.tree).contains(item_range.start) - && !self - .environments - .iter() - .filter(|env| env.left.parent != enumeration.left.parent) - .filter(|env| env.left.is_enum(&self.tree)) - .filter(|env| { - enumeration - .range(&self.tree) - .contains(env.range(&self.tree).start) - }) - .any(|env| env.range(&self.tree).contains(item_range.start)) - } - - pub fn find_label_by_range(&self, range: Range) -> Option<&Label> { - self.labels - .iter() - .filter(|label| label.kind == LatexLabelKind::Definition) - .filter(|label| label.names(&self).len() == 1) - .find(|label| range.contains(self[label.parent].range().start)) - } - - pub fn find_label_by_environment(&self, env: Environment) -> Option<&Label> { - self.labels - .iter() - .filter(|label| label.kind == LatexLabelKind::Definition) - .filter(|label| label.names(&self.tree).len() == 1) - .find(|label| self.is_direct_child(env, self.tree[label.parent].start())) - } -} - -impl Deref for SymbolTable { - type Target = Tree; - - fn deref(&self) -> &Self::Target { - &self.tree - } -} - -#[derive(Debug, Clone, Copy)] -pub struct SymbolContext<'a> { - tree: &'a Tree, - commands: &'a [AstNodeIndex], - uri: &'a Uri, - resolver: &'a Resolver, - options: &'a Options, - current_dir: &'a Path, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct EnvironmentDelimiter { - pub parent: AstNodeIndex, -} - -impl EnvironmentDelimiter { - pub fn name(self, tree: &Tree) -> Option<&Token> { - tree.extract_word(self.parent, GroupKind::Group, 0) - } - - pub fn is_math(self, tree: &Tree) -> bool { - self.is_special(tree, LANGUAGE_DATA.math_environments.iter()) - } - - pub fn is_enum(self, tree: &Tree) -> bool { - self.is_special(tree, LANGUAGE_DATA.enum_environments.iter()) - } - - fn is_special<'a, I: Iterator<Item = &'a String>>(self, tree: &Tree, mut values: I) -> bool { - match self.name(tree) { - Some(name) => values.any(|env| env == name.text()), - None => false, - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Environment { - pub left: EnvironmentDelimiter, - pub right: EnvironmentDelimiter, -} - -impl Environment { - pub fn is_root(self, tree: &Tree) -> bool { - self.left - .name(tree) - .iter() - .chain(self.right.name(tree).iter()) - .any(|name| name.text() == "document") - } - - pub fn range(self, tree: &Tree) -> Range { - let start = tree[self.left.parent].start(); - let end = tree[self.right.parent].end(); - Range::new(start, end) - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - let mut stack = Vec::new(); - let mut envs = Vec::new(); - for parent in ctx.commands { - if let Some((delim, delim_cmd)) = Self::parse_delimiter(ctx.tree, *parent) { - if delim_cmd.name.text() == "\\begin" { - stack.push(delim); - } else if let Some(left) = stack.pop() { - envs.push(Self { left, right: delim }); - } - } - } - envs - } - - fn parse_delimiter( - tree: &Tree, - parent: AstNodeIndex, - ) -> Option<(EnvironmentDelimiter, &Command)> { - let cmd = tree.as_command(parent)?; - if cmd.name.text() != "\\begin" && cmd.name.text() != "\\end" { - return None; - } - - let group = tree.extract_group(parent, GroupKind::Group, 0)?; - if tree.extract_word(parent, GroupKind::Group, 0).is_some() - || tree.children(group).next().is_none() - || tree.as_group(group)?.right.is_none() - { - Some((EnvironmentDelimiter { parent }, cmd)) - } else { - None - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Include { - pub parent: AstNodeIndex, - pub arg_index: usize, - pub kind: LatexIncludeKind, - pub all_targets: Vec<Vec<Uri>>, - pub include_extension: bool, -} - -impl Include { - pub fn paths<'a>(&self, tree: &'a Tree) -> Vec<&'a Token> { - tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index) - .unwrap() - } - - pub fn components<'a>(&self, tree: &'a Tree) -> impl Iterator<Item = String> + 'a { - let kind = self.kind; - self.paths(tree) - .into_iter() - .filter_map(move |path| match kind { - LatexIncludeKind::Package => Some(format!("{}.sty", path.text())), - LatexIncludeKind::Class => Some(format!("{}.cls", path.text())), - LatexIncludeKind::Latex - | LatexIncludeKind::Bibliography - | LatexIncludeKind::Image - | LatexIncludeKind::Svg - | LatexIncludeKind::Pdf - | LatexIncludeKind::Everything => None, - }) - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - iproduct!(ctx.commands, LANGUAGE_DATA.include_commands.iter()) - .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) - .collect() - } - - fn parse_single( - ctx: SymbolContext, - parent: AstNodeIndex, - desc: &LatexIncludeCommand, - ) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != desc.name { - return None; - } - - let mut all_targets = Vec::new(); - let paths = ctx - .tree - .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?; - for path in paths { - let mut targets = Vec::new(); - let base_url = base_url(ctx)?; - targets.push(base_url.join(path.text()).ok()?.into()); - - if let Some(extensions) = desc.kind.extensions() { - for extension in extensions { - let path = format!("{}.{}", path.text(), extension); - targets.push(base_url.join(&path).ok()?.into()); - } - } - - if let Some(target) = Self::resolve_distro_file(ctx, desc, path.text()) { - targets.push(target); - } - all_targets.push(targets); - } - - let include = Self { - parent, - arg_index: desc.index, - kind: desc.kind, - all_targets, - include_extension: desc.include_extension, - }; - Some(include) - } - - fn resolve_distro_file( - ctx: SymbolContext, - desc: &LatexIncludeCommand, - name: &str, - ) -> Option<Uri> { - let mut path = ctx.resolver.files_by_name.get(name); - if let Some(extensions) = desc.kind.extensions() { - for extension in extensions { - path = path.or_else(|| { - let full_name = format!("{}.{}", name, extension); - ctx.resolver.files_by_name.get(&full_name) - }); - } - } - path.and_then(|p| Uri::from_file_path(p).ok()) - } -} - -fn base_url(ctx: SymbolContext) -> Option<Uri> { - if let Some(root_directory) = ctx - .options - .latex - .as_ref() - .and_then(|opts| opts.root_directory.as_ref()) - { - let file_name = ctx.uri.path_segments()?.last()?; - let path = ctx.current_dir.join(root_directory).join(file_name); - Uri::from_file_path(path).ok() - } else { - Some(ctx.uri.clone()) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Import { - pub parent: AstNodeIndex, - pub targets: Vec<Uri>, -} - -impl Import { - pub fn dir<'a>(&self, tree: &'a Tree) -> &'a Token { - tree.extract_word(self.parent, GroupKind::Group, 0).unwrap() - } - - pub fn file<'a>(&self, tree: &'a Tree) -> &'a Token { - tree.extract_word(self.parent, GroupKind::Group, 1).unwrap() - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - ctx.commands - .iter() - .filter_map(|parent| Self::parse_single(ctx, *parent)) - .collect() - } - - fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != "\\import" && cmd.name.text() != "\\subimport" { - return None; - } - - let dir = ctx.tree.extract_word(parent, GroupKind::Group, 0)?; - let file = ctx.tree.extract_word(parent, GroupKind::Group, 1)?; - - let mut targets = Vec::new(); - let base_url = base_url(ctx)?.join(dir.text()).ok()?; - targets.push(base_url.join(file.text()).ok()?.into()); - targets.push(base_url.join(&format!("{}.tex", file.text())).ok()?.into()); - Some(Self { parent, targets }) - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Citation { - parent: AstNodeIndex, - arg_index: usize, -} - -impl Citation { - pub fn keys(self, tree: &Tree) -> Vec<&Token> { - tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index) - .unwrap() - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - iproduct!(ctx.commands, LANGUAGE_DATA.citation_commands.iter()) - .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) - .collect() - } - - fn parse_single( - ctx: SymbolContext, - parent: AstNodeIndex, - desc: &LatexCitationCommand, - ) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != desc.name { - return None; - } - - ctx.tree - .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?; - - Some(Self { - parent, - arg_index: desc.index, - }) - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct CommandDefinition { - pub parent: AstNodeIndex, - pub definition: AstNodeIndex, - pub definition_index: usize, - pub implementation: AstNodeIndex, - pub implementation_index: usize, - pub arg_count_index: usize, -} - -impl CommandDefinition { - pub fn definition_name(self, tree: &Tree) -> &str { - tree.as_command(self.definition).unwrap().name.text() - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - let def = LANGUAGE_DATA.command_definition_commands.iter(); - iproduct!(ctx.commands, def) - .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) - .collect() - } - - fn parse_single( - ctx: SymbolContext, - parent: AstNodeIndex, - desc: &LatexCommandDefinitionCommand, - ) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != desc.name { - return None; - } - - let group_kind = GroupKind::Group; - let implementation = - ctx.tree - .extract_group(parent, group_kind, desc.implementation_index)?; - - let def_group = ctx - .tree - .extract_group(parent, group_kind, desc.definition_index)?; - - let mut def_children = ctx.tree.children(def_group); - let definition = def_children.next()?; - ctx.tree.as_command(definition)?; - Some(Self { - parent, - definition, - definition_index: desc.definition_index, - implementation, - implementation_index: desc.implementation_index, - arg_count_index: desc.arg_count_index, - }) - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct GlossaryEntry { - pub parent: AstNodeIndex, - pub label_index: usize, - pub kind: LatexGlossaryEntryKind, -} - -impl GlossaryEntry { - pub fn label(self, tree: &Tree) -> &Token { - tree.extract_word(self.parent, GroupKind::Group, self.label_index) - .unwrap() - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - let entry = LANGUAGE_DATA.glossary_entry_definition_commands.iter(); - iproduct!(ctx.commands, entry) - .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) - .collect() - } - - fn parse_single( - ctx: SymbolContext, - parent: AstNodeIndex, - desc: &LatexGlossaryEntryDefinitionCommand, - ) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != desc.name { - return None; - } - - ctx.tree - .extract_word(parent, GroupKind::Group, desc.label_index)?; - - Some(Self { - parent, - label_index: desc.label_index, - kind: desc.kind, - }) - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Equation { - pub left: AstNodeIndex, - pub right: AstNodeIndex, -} - -impl Equation { - pub fn range(self, tree: &Tree) -> Range { - let start = tree[self.left].start(); - let end = tree[self.right].end(); - Range::new(start, end) - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - let mut equations = Vec::new(); - let mut left = None; - for node in ctx.commands { - let cmd = ctx.tree.as_command(*node).unwrap(); - let name = cmd.name.text(); - if name == "\\[" || name == "\\(" { - left = Some(node); - } else if name == "\\]" || name == "\\)" { - if let Some(begin) = left { - equations.push(Self { - left: *begin, - right: *node, - }); - left = None; - } - } - } - equations - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Inline { - pub left: AstNodeIndex, - pub right: AstNodeIndex, -} - -impl Inline { - pub fn range(self, tree: &Tree) -> Range { - let start = tree[self.left].start(); - let end = tree[self.right].end(); - Range::new(start, end) - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - let mut inlines = Vec::new(); - let mut left = None; - for node in ctx - .tree - .nodes() - .into_iter() - .filter(|node| ctx.tree.as_math(*node).is_some()) - .sorted_by_key(|node| ctx.tree[*node].start()) - { - if let Some(l) = left { - inlines.push(Inline { - left: l, - right: node, - }); - left = None; - } else { - left = Some(node); - } - } - inlines - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct MathOperator { - pub parent: AstNodeIndex, - pub definition: AstNodeIndex, - pub definition_index: usize, - pub implementation: AstNodeIndex, - pub implementation_index: usize, -} - -impl MathOperator { - pub fn definition_name(self, tree: &Tree) -> &str { - tree.as_command(self.definition).unwrap().name.text() - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - iproduct!(ctx.commands, LANGUAGE_DATA.math_operator_commands.iter()) - .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) - .collect() - } - - fn parse_single( - ctx: SymbolContext, - parent: AstNodeIndex, - desc: &LatexMathOperatorCommand, - ) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != desc.name { - return None; - } - - let group_kind = GroupKind::Group; - let def_group = ctx - .tree - .extract_group(parent, group_kind, desc.definition_index)?; - let implementation = - ctx.tree - .extract_group(parent, group_kind, desc.implementation_index)?; - - let mut def_children = ctx.tree.children(def_group); - let definition = def_children.next()?; - Some(Self { - parent, - definition, - definition_index: desc.definition_index, - implementation, - implementation_index: desc.implementation_index, - }) - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct TheoremDefinition { - pub parent: AstNodeIndex, - pub arg_index: usize, -} - -impl TheoremDefinition { - pub fn name(self, tree: &Tree) -> &Token { - tree.extract_word(self.parent, GroupKind::Group, self.arg_index) - .unwrap() - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - let thm = LANGUAGE_DATA.theorem_definition_commands.iter(); - iproduct!(ctx.commands, thm) - .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) - .collect() - } - - fn parse_single( - ctx: SymbolContext, - parent: AstNodeIndex, - desc: &LatexTheoremDefinitionCommand, - ) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != desc.name { - return None; - } - - let group_kind = GroupKind::Group; - ctx.tree.extract_word(parent, group_kind, desc.index)?; - - Some(Self { - parent, - arg_index: desc.index, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Section { - pub parent: AstNodeIndex, - pub arg_index: usize, - pub level: i32, - pub prefix: Cow<'static, str>, -} - -impl Section { - pub fn print(&self, tree: &Tree) -> Option<String> { - tree.print_group_content(self.parent, GroupKind::Group, self.arg_index) - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - iproduct!(ctx.commands, LANGUAGE_DATA.section_commands.iter()) - .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) - .collect() - } - - fn parse_single( - ctx: SymbolContext, - parent: AstNodeIndex, - desc: &'static LatexSectionCommand, - ) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != desc.name { - return None; - } - - let group_kind = GroupKind::Group; - ctx.tree.extract_group(parent, group_kind, desc.index)?; - - Some(Self { - parent, - arg_index: desc.index, - level: desc.level, - prefix: Cow::from(&desc.prefix), - }) - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Label { - pub parent: AstNodeIndex, - pub arg_index: usize, - pub kind: LatexLabelKind, -} - -impl Label { - pub fn names(self, tree: &Tree) -> Vec<&Token> { - tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index) - .unwrap() - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - iproduct!(ctx.commands, LANGUAGE_DATA.label_commands.iter()) - .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) - .collect() - } - - fn parse_single( - ctx: SymbolContext, - parent: AstNodeIndex, - desc: &LatexLabelCommand, - ) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != desc.name { - return None; - } - - ctx.tree - .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?; - - Some(Self { - parent, - arg_index: desc.index, - kind: desc.kind, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LabelNumbering { - pub parent: AstNodeIndex, - pub number: String, -} - -impl LabelNumbering { - pub fn name<'a>(&self, tree: &'a Tree) -> &'a Token { - tree.extract_word(self.parent, GroupKind::Group, 0).unwrap() - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - ctx.commands - .iter() - .filter_map(|parent| Self::parse_single(ctx, *parent)) - .collect() - } - - fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != "\\newlabel" { - return None; - } - - ctx.tree.extract_word(parent, GroupKind::Group, 0)?; - - let arg = ctx.tree.extract_group(parent, GroupKind::Group, 1)?; - let mut analyzer = FirstText::default(); - analyzer.visit(ctx.tree, arg); - Some(Self { - parent, - number: analyzer.text?, - }) - } -} - -#[derive(Debug, Default)] -struct FirstText { - text: Option<String>, -} - -impl Visitor for FirstText { - fn visit(&mut self, tree: &Tree, node: AstNodeIndex) { - if let Some(text) = tree.as_text(node) { - self.text = Some(text.words.iter().map(Token::text).join(" ")); - } - - if self.text.is_none() { - tree.walk(self, node); - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Caption { - pub parent: AstNodeIndex, - pub arg_index: usize, -} - -impl Caption { - pub fn print(self, tree: &Tree) -> Option<String> { - tree.print_group_content(self.parent, GroupKind::Group, self.arg_index) - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - ctx.commands - .iter() - .flat_map(|parent| Self::parse_single(ctx, *parent)) - .collect() - } - - fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != "\\caption" { - return None; - } - - ctx.tree.extract_group(parent, GroupKind::Group, 0)?; - Some(Self { - parent, - arg_index: 0, - }) - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Item { - pub parent: AstNodeIndex, -} - -impl Item { - pub fn name(self, tree: &Tree) -> Option<String> { - tree.print_group_content(self.parent, GroupKind::Options, 0) - } - - fn parse(ctx: SymbolContext) -> Vec<Self> { - ctx.commands - .iter() - .filter_map(|parent| Self::parse_single(ctx, *parent)) - .collect() - } - - fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> { - let cmd = ctx.tree.as_command(parent)?; - if cmd.name.text() != "\\item" { - return None; - } - - Some(Self { parent }) - } +pub fn analyze(context: &mut LatexAnalyzerContext, root: &latex::SyntaxNode) { + analyze_implicit_links(context); + for node in root.descendants() { + analyze_command(context, node) + .or_else(|| analyze_begin(context, node)) + .or_else(|| analyze_include(context, node)) + .or_else(|| analyze_import(context, node)) + .or_else(|| analyze_label_name(context, node)) + .or_else(|| analyze_label_number(context, node)) + .or_else(|| analyze_theorem_definition(context, node)); + } + context.extras.has_document_environment = context.extras.environment_names.contains("document"); } diff --git a/support/texlab/src/syntax/latex/analysis/command.rs b/support/texlab/src/syntax/latex/analysis/command.rs new file mode 100644 index 0000000000..9dea4d84ee --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/command.rs @@ -0,0 +1,12 @@ +use crate::syntax::{latex, CstNode}; + +use super::LatexAnalyzerContext; + +pub fn analyze_command(context: &mut LatexAnalyzerContext, node: &latex::SyntaxNode) -> Option<()> { + let command = latex::GenericCommand::cast(node)?; + context + .extras + .command_names + .insert(command.name()?.text().into()); + Some(()) +} diff --git a/support/texlab/src/syntax/latex/analysis/distro_file.rs b/support/texlab/src/syntax/latex/analysis/distro_file.rs new file mode 100644 index 0000000000..4c747ce760 --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/distro_file.rs @@ -0,0 +1,71 @@ +use crate::{distro::Resolver, Uri}; + +pub fn resolve_distro_file(resolver: &Resolver, stem: &str, extensions: &[&str]) -> Option<Uri> { + let mut document = resolver.files_by_name.get(stem); + for extension in extensions { + document = document.or_else(|| { + let full_name = format!("{}.{}", stem, extension); + resolver.files_by_name.get(full_name.as_str()) + }); + } + document.and_then(|path| Uri::from_file_path(path).ok()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(windows)] + fn test() { + let mut resolver = Resolver::default(); + resolver + .files_by_name + .insert("foo.tex".into(), "C:/distro/foo.tex".into()); + resolver + .files_by_name + .insert("foo.sty".into(), "C:/distro/foo.sty".into()); + resolver + .files_by_name + .insert("bar.tex".into(), "C:/distro/bar.tex".into()); + + assert_eq!( + resolve_distro_file(&resolver, "foo", &["tex"]), + Some(Uri::from_file_path("C:/distro/foo.tex").unwrap()) + ); + + assert_eq!( + resolve_distro_file(&resolver, "foo", &["sty"]), + Some(Uri::from_file_path("C:/distro/foo.sty").unwrap()) + ); + + assert_eq!(resolve_distro_file(&resolver, "foo", &["cls"]), None); + } + + #[test] + #[cfg(unix)] + fn test() { + let mut resolver = Resolver::default(); + resolver + .files_by_name + .insert("foo.tex".into(), "/distro/foo.tex".into()); + resolver + .files_by_name + .insert("foo.sty".into(), "/distro/foo.sty".into()); + resolver + .files_by_name + .insert("bar.tex".into(), "/distro/bar.tex".into()); + + assert_eq!( + resolve_distro_file(&resolver, "foo", &["tex"]), + Some(Uri::from_file_path("/distro/foo.tex").unwrap()) + ); + + assert_eq!( + resolve_distro_file(&resolver, "foo", &["sty"]), + Some(Uri::from_file_path("/distro/foo.sty").unwrap()) + ); + + assert_eq!(resolve_distro_file(&resolver, "foo", &["cls"]), None); + } +} diff --git a/support/texlab/src/syntax/latex/analysis/environment.rs b/support/texlab/src/syntax/latex/analysis/environment.rs new file mode 100644 index 0000000000..3b83b40b2f --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/environment.rs @@ -0,0 +1,11 @@ +use crate::syntax::{latex, CstNode}; + +use super::LatexAnalyzerContext; + +pub fn analyze_begin(context: &mut LatexAnalyzerContext, node: &latex::SyntaxNode) -> Option<()> { + let begin = latex::Begin::cast(node)?; + let name = begin.name()?.word()?.text(); + let extras = &mut context.extras; + extras.environment_names.insert(name.into()); + Some(()) +} diff --git a/support/texlab/src/syntax/latex/analysis/explicit_link.rs b/support/texlab/src/syntax/latex/analysis/explicit_link.rs new file mode 100644 index 0000000000..363a576aaa --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/explicit_link.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use crate::syntax::{latex, CstNode}; + +use super::{ + distro_file::resolve_distro_file, ExplicitLink, ExplicitLinkKind, LatexAnalyzerContext, +}; + +pub fn analyze_include(context: &mut LatexAnalyzerContext, node: &latex::SyntaxNode) -> Option<()> { + let include = latex::Include::cast(node)?; + let kind = match include.syntax().kind() { + latex::LATEX_INCLUDE => ExplicitLinkKind::Latex, + latex::BIBLATEX_INCLUDE | latex::BIBTEX_INCLUDE => ExplicitLinkKind::Bibtex, + latex::PACKAGE_INCLUDE => ExplicitLinkKind::Package, + latex::CLASS_INCLUDE => ExplicitLinkKind::Class, + _ => return None, + }; + + let extensions = match kind { + ExplicitLinkKind::Latex => &["tex"], + ExplicitLinkKind::Bibtex => &["bib"], + ExplicitLinkKind::Package => &["sty"], + ExplicitLinkKind::Class => &["cls"], + }; + + for path in include.path_list()?.words() { + let stem = path.text(); + let mut targets = vec![Arc::new(context.base_uri.join(stem).ok()?.into())]; + for extension in extensions { + let path = format!("{}.{}", stem, extension); + targets.push(Arc::new(context.base_uri.join(&path).ok()?.into())); + } + + resolve_distro_file(&context.inner.resolver.lock().unwrap(), stem, extensions) + .into_iter() + .for_each(|target| targets.push(Arc::new(target))); + + context.extras.explicit_links.push(ExplicitLink { + kind, + stem: stem.into(), + stem_range: path.text_range(), + targets, + }); + } + + Some(()) +} + +pub fn analyze_import(context: &mut LatexAnalyzerContext, node: &latex::SyntaxNode) -> Option<()> { + let import = latex::Import::cast(node)?; + + let mut targets = Vec::new(); + let directory = context + .base_uri + .join(import.directory()?.word()?.text()) + .ok()?; + + let file = import.file()?.word()?; + let stem = file.text(); + targets.push(Arc::new(directory.join(stem).ok()?.into())); + targets.push(Arc::new( + directory.join(&format!("{}.tex", stem)).ok()?.into(), + )); + + context.extras.explicit_links.push(ExplicitLink { + stem: stem.into(), + stem_range: file.text_range(), + targets, + kind: ExplicitLinkKind::Latex, + }); + Some(()) +} diff --git a/support/texlab/src/syntax/latex/analysis/implicit_link.rs b/support/texlab/src/syntax/latex/analysis/implicit_link.rs new file mode 100644 index 0000000000..74055b3e31 --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/implicit_link.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use crate::Uri; + +use super::LatexAnalyzerContext; + +pub fn analyze_implicit_links(context: &mut LatexAnalyzerContext) { + context.extras.implicit_links.aux = find_by_extension(context, "aux").unwrap_or_default(); + context.extras.implicit_links.log = find_by_extension(context, "log").unwrap_or_default(); + context.extras.implicit_links.pdf = find_by_extension(context, "pdf").unwrap_or_default(); +} + +fn find_by_extension(context: &LatexAnalyzerContext, extension: &str) -> Option<Vec<Arc<Uri>>> { + let mut targets: Vec<Arc<Uri>> = Vec::new(); + targets.push(Arc::new(context.document_uri.with_extension(extension)?)); + if context.document_uri.scheme() == "file" { + let file_path = context.document_uri.to_file_path().ok()?; + let file_stem = file_path.file_stem()?; + let aux_name = format!("{}.{}", file_stem.to_str()?, extension); + + let options = context.inner.options.read().unwrap(); + if let Some(root_dir) = options.root_directory.as_ref() { + let path = context + .inner + .current_directory + .join(root_dir) + .join(&aux_name); + targets.push(Arc::new(Uri::from_file_path(path).ok()?)); + } + + if let Some(build_dir) = options.aux_directory.as_ref() { + let path = context + .inner + .current_directory + .join(build_dir) + .join(&aux_name); + targets.push(Arc::new(Uri::from_file_path(path).ok()?)); + } + } + Some(targets) +} diff --git a/support/texlab/src/syntax/latex/analysis/label_name.rs b/support/texlab/src/syntax/latex/analysis/label_name.rs new file mode 100644 index 0000000000..d0410b953c --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/label_name.rs @@ -0,0 +1,66 @@ +use latex::LabelReferenceRange; + +use crate::syntax::{latex, CstNode}; + +use super::{LabelName, LatexAnalyzerContext}; + +pub fn analyze_label_name( + context: &mut LatexAnalyzerContext, + node: &latex::SyntaxNode, +) -> Option<()> { + analyze_label_definition_name(context, node) + .or_else(|| analyze_label_reference_name(context, node)) + .or_else(|| analyze_label_reference_range_name(context, node)) +} + +fn analyze_label_definition_name( + context: &mut LatexAnalyzerContext, + node: &latex::SyntaxNode, +) -> Option<()> { + let label = latex::LabelDefinition::cast(node)?; + let name = label.name()?.word()?; + context.extras.label_names.push(LabelName { + text: name.text().into(), + range: name.text_range(), + is_definition: true, + }); + Some(()) +} + +fn analyze_label_reference_name( + context: &mut LatexAnalyzerContext, + node: &latex::SyntaxNode, +) -> Option<()> { + let label = latex::LabelReference::cast(node)?; + for name in label.name_list()?.words() { + context.extras.label_names.push(LabelName { + text: name.text().into(), + range: name.text_range(), + is_definition: false, + }); + } + Some(()) +} + +fn analyze_label_reference_range_name( + context: &mut LatexAnalyzerContext, + node: &latex::SyntaxNode, +) -> Option<()> { + let label = LabelReferenceRange::cast(node)?; + if let Some(name1) = label.from().and_then(|name| name.word()) { + context.extras.label_names.push(LabelName { + text: name1.text().into(), + range: name1.text_range(), + is_definition: false, + }); + } + + if let Some(name2) = label.to().and_then(|name| name.word()) { + context.extras.label_names.push(LabelName { + text: name2.text().into(), + range: name2.text_range(), + is_definition: false, + }); + } + Some(()) +} diff --git a/support/texlab/src/syntax/latex/analysis/label_number.rs b/support/texlab/src/syntax/latex/analysis/label_number.rs new file mode 100644 index 0000000000..de8d0dc1e1 --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/label_number.rs @@ -0,0 +1,22 @@ +use crate::syntax::{latex, CstNode}; + +use super::LatexAnalyzerContext; + +pub fn analyze_label_number( + context: &mut LatexAnalyzerContext, + node: &latex::SyntaxNode, +) -> Option<()> { + let number = latex::LabelNumber::cast(node)?; + let name = number.name()?.word()?.text().into(); + let text = number + .text()? + .syntax() + .descendants_with_tokens() + .filter_map(|element| element.into_node()) + .find(|node| node.kind() == latex::TEXT || node.kind() == latex::MIXED_GROUP)? + .text() + .to_string(); + + context.extras.label_numbers_by_name.insert(name, text); + Some(()) +} diff --git a/support/texlab/src/syntax/latex/analysis/theorem.rs b/support/texlab/src/syntax/latex/analysis/theorem.rs new file mode 100644 index 0000000000..0c365f0ef5 --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/theorem.rs @@ -0,0 +1,23 @@ +use crate::syntax::{ + latex::{self, HasCurly}, + CstNode, +}; + +use super::{LatexAnalyzerContext, TheoremEnvironment}; + +pub fn analyze_theorem_definition( + context: &mut LatexAnalyzerContext, + node: &latex::SyntaxNode, +) -> Option<()> { + let theorem = latex::TheoremDefinition::cast(node)?; + let name = theorem.name()?.word()?.text().into(); + let description = theorem.description()?; + let description = description.content_text()?; + + context + .extras + .theorem_environments + .push(TheoremEnvironment { name, description }); + + Some(()) +} diff --git a/support/texlab/src/syntax/latex/analysis/types.rs b/support/texlab/src/syntax/latex/analysis/types.rs new file mode 100644 index 0000000000..f089721397 --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis/types.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use cstree::TextRange; +use rustc_hash::{FxHashMap, FxHashSet}; +use smol_str::SmolStr; + +use crate::{ServerContext, Uri}; + +#[derive(Debug)] +pub struct LatexAnalyzerContext { + pub inner: Arc<ServerContext>, + pub document_uri: Arc<Uri>, + pub base_uri: Arc<Uri>, + pub extras: Extras, +} + +#[derive(Debug, Clone, Default)] +pub struct Extras { + pub implicit_links: ImplicitLinks, + pub explicit_links: Vec<ExplicitLink>, + pub has_document_environment: bool, + pub command_names: FxHashSet<SmolStr>, + pub environment_names: FxHashSet<SmolStr>, + pub label_names: Vec<LabelName>, + pub label_numbers_by_name: FxHashMap<SmolStr, String>, + pub theorem_environments: Vec<TheoremEnvironment>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)] +pub struct ImplicitLinks { + pub aux: Vec<Arc<Uri>>, + pub log: Vec<Arc<Uri>>, + pub pdf: Vec<Arc<Uri>>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Hash)] +pub enum ExplicitLinkKind { + Package, + Class, + Latex, + Bibtex, +} + +#[derive(Debug, Clone)] +pub struct ExplicitLink { + pub stem: SmolStr, + pub stem_range: TextRange, + pub targets: Vec<Arc<Uri>>, + pub kind: ExplicitLinkKind, +} + +impl ExplicitLink { + pub fn as_component_name(&self) -> Option<String> { + match self.kind { + ExplicitLinkKind::Package => Some(format!("{}.sty", self.stem)), + ExplicitLinkKind::Class => Some(format!("{}.cls", self.stem)), + ExplicitLinkKind::Latex | ExplicitLinkKind::Bibtex => None, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)] +pub struct TheoremEnvironment { + pub name: SmolStr, + pub description: String, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)] +pub struct LabelName { + pub text: SmolStr, + pub range: TextRange, + pub is_definition: bool, +} diff --git a/support/texlab/src/syntax/latex/ast.rs b/support/texlab/src/syntax/latex/ast.rs deleted file mode 100644 index ab7c6863e1..0000000000 --- a/support/texlab/src/syntax/latex/ast.rs +++ /dev/null @@ -1,414 +0,0 @@ -use crate::{ - protocol::{Position, Range, RangeExt}, - syntax::{ - generic_ast::{Ast, AstNodeIndex}, - text::{Span, SyntaxNode}, - }, -}; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub enum TokenKind { - Word, - Command, - Math, - Comma, - BeginGroup, - EndGroup, - BeginOptions, - EndOptions, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Token { - pub span: Span, - pub kind: TokenKind, -} - -impl Token { - pub fn new(span: Span, kind: TokenKind) -> Self { - Self { span, kind } - } - - pub fn text(&self) -> &str { - &self.span.text - } -} - -impl SyntaxNode for Token { - fn range(&self) -> Range { - self.span.range() - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct Root { - pub range: Range, -} - -impl SyntaxNode for Root { - fn range(&self) -> Range { - self.range - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub enum GroupKind { - Group, - Options, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Group { - pub range: Range, - pub left: Token, - pub right: Option<Token>, - pub kind: GroupKind, -} - -impl SyntaxNode for Group { - fn range(&self) -> Range { - self.range - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Command { - pub range: Range, - pub name: Token, -} - -impl Command { - pub fn short_name_range(&self) -> Range { - Range::new_simple( - self.name.start().line, - self.name.start().character + 1, - self.name.end().line, - self.name.end().character, - ) - } -} - -impl SyntaxNode for Command { - fn range(&self) -> Range { - self.range - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Text { - pub range: Range, - pub words: Vec<Token>, -} - -impl SyntaxNode for Text { - fn range(&self) -> Range { - self.range - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Comma { - pub range: Range, - pub token: Token, -} - -impl SyntaxNode for Comma { - fn range(&self) -> Range { - self.range - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Math { - pub range: Range, - pub token: Token, -} - -impl SyntaxNode for Math { - fn range(&self) -> Range { - self.range - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub enum Node { - Root(Root), - Group(Group), - Command(Command), - Text(Text), - Comma(Comma), - Math(Math), -} - -impl SyntaxNode for Node { - fn range(&self) -> Range { - match self { - Self::Root(root) => root.range(), - Self::Group(group) => group.range(), - Self::Command(cmd) => cmd.range(), - Self::Text(text) => text.range(), - Self::Comma(comma) => comma.range(), - Self::Math(math) => math.range(), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Tree { - pub inner: Ast<Node>, - pub root: AstNodeIndex, -} - -impl Deref for Tree { - type Target = Ast<Node>; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl Tree { - pub fn walk<V: Visitor>(&self, visitor: &mut V, parent: AstNodeIndex) { - for child in self.children(parent) { - visitor.visit(self, child); - } - } - - pub fn find(&self, pos: Position) -> Vec<AstNodeIndex> { - let mut finder = Finder::new(pos); - finder.visit(self, self.root); - finder.results - } - - pub fn find_command_by_short_name_range(&self, pos: Position) -> Option<AstNodeIndex> { - self.find(pos).into_iter().find(|node| { - self.as_command(*node) - .filter(|cmd| { - cmd.name.range().contains(pos) && cmd.name.start().character != pos.character - }) - .is_some() - }) - } - - pub fn print(&self, node: AstNodeIndex) -> String { - let start_position = self[node].start(); - let mut printer = Printer::new(start_position); - printer.visit(self, node); - printer.output - } - - pub fn commands<'a>(&'a self) -> impl Iterator<Item = AstNodeIndex> + 'a { - self.inner - .nodes() - .into_iter() - .filter(move |node| self.as_command(*node).is_some()) - } - - pub fn as_group(&self, node: AstNodeIndex) -> Option<&Group> { - if let Node::Group(group) = &self[node] { - Some(group) - } else { - None - } - } - - pub fn as_command(&self, node: AstNodeIndex) -> Option<&Command> { - if let Node::Command(cmd) = &self[node] { - Some(cmd) - } else { - None - } - } - - pub fn as_text(&self, node: AstNodeIndex) -> Option<&Text> { - if let Node::Text(text) = &self[node] { - Some(text) - } else { - None - } - } - - pub fn as_math(&self, node: AstNodeIndex) -> Option<&Math> { - if let Node::Math(math) = &self[node] { - Some(math) - } else { - None - } - } - - pub fn extract_group( - &self, - parent: AstNodeIndex, - group_kind: GroupKind, - index: usize, - ) -> Option<AstNodeIndex> { - self.children(parent) - .filter(|child| { - self.as_group(*child) - .filter(|group| group.kind == group_kind) - .is_some() - }) - .nth(index) - } - - pub fn extract_text( - &self, - parent: AstNodeIndex, - group_kind: GroupKind, - index: usize, - ) -> Option<&Text> { - let group = self.extract_group(parent, group_kind, index)?; - let mut contents = self.children(group); - let text = self.as_text(contents.next()?); - if contents.next().is_none() { - text - } else { - None - } - } - - pub fn extract_word( - &self, - parent: AstNodeIndex, - group_kind: GroupKind, - index: usize, - ) -> Option<&Token> { - let text = self.extract_text(parent, group_kind, index)?; - if text.words.len() == 1 { - Some(&text.words[0]) - } else { - None - } - } - - pub fn extract_comma_separated_words( - &self, - parent: AstNodeIndex, - group_kind: GroupKind, - index: usize, - ) -> Option<Vec<&Token>> { - let group = self.extract_group(parent, group_kind, index)?; - let mut words = Vec::new(); - for child in self.children(group) { - match &self[child] { - Node::Root(_) | Node::Group(_) | Node::Command(_) | Node::Math(_) => return None, - Node::Text(text) => { - for word in &text.words { - words.push(word); - } - } - Node::Comma(_) => (), - } - } - Some(words) - } - - pub fn print_group_content( - &self, - parent: AstNodeIndex, - group_kind: GroupKind, - index: usize, - ) -> Option<String> { - let arg = self.extract_group(parent, group_kind, index)?; - let text = self.print(arg); - self.as_group(arg)?.right.as_ref()?; - Some(text[1..text.len() - 1].trim().into()) - } -} - -pub trait Visitor { - fn visit(&mut self, tree: &Tree, node: AstNodeIndex); -} - -#[derive(Debug)] -struct Finder { - position: Position, - results: Vec<AstNodeIndex>, -} - -impl Finder { - fn new(position: Position) -> Self { - Self { - position, - results: Vec::new(), - } - } -} - -impl Visitor for Finder { - fn visit(&mut self, tree: &Tree, node: AstNodeIndex) { - if tree[node].range().contains(self.position) { - self.results.push(node); - tree.walk(self, node); - } - } -} - -#[derive(Debug)] -struct Printer { - output: String, - position: Position, -} - -impl Printer { - fn new(start_position: Position) -> Self { - Self { - output: String::new(), - position: start_position, - } - } - - fn synchronize(&mut self, position: Position) { - while self.position.line < position.line { - self.output.push('\n'); - self.position.line += 1; - self.position.character = 0; - } - - while self.position.character < position.character { - self.output.push(' '); - self.position.character += 1; - } - - assert_eq!(self.position, position); - } - - fn print_token(&mut self, token: &Token) { - self.synchronize(token.start()); - self.output.push_str(token.text()); - self.position.character += token.end().character - token.start().character; - self.synchronize(token.end()); - } -} - -impl Visitor for Printer { - fn visit(&mut self, tree: &Tree, node: AstNodeIndex) { - match &tree[node] { - Node::Root(_) => tree.walk(self, node), - Node::Group(group) => { - self.print_token(&group.left); - tree.walk(self, node); - if let Some(right) = &group.right { - self.print_token(right); - } - } - Node::Command(cmd) => { - self.print_token(&cmd.name); - tree.walk(self, node); - } - Node::Text(text) => { - for word in &text.words { - self.print_token(word); - } - } - Node::Comma(comma) => { - self.print_token(&comma.token); - } - Node::Math(math) => { - self.print_token(&math.token); - } - } - } -} diff --git a/support/texlab/src/syntax/latex/cst.rs b/support/texlab/src/syntax/latex/cst.rs new file mode 100644 index 0000000000..0b839b706d --- /dev/null +++ b/support/texlab/src/syntax/latex/cst.rs @@ -0,0 +1,639 @@ +use cstree::TextRange; + +use crate::syntax::CstNode; + +use super::{Language, SyntaxKind::*, SyntaxNode, SyntaxToken}; + +macro_rules! cst_node { + ($name:ident, $($kind:pat),+) => { + #[derive(Clone)] + #[repr(transparent)] + pub struct $name<'a>(&'a SyntaxNode); + + impl<'a> CstNode<'a> for $name<'a> { + type Lang = Language; + + fn cast(node: &'a cstree::ResolvedNode<Self::Lang>) -> Option<Self> + where + Self: Sized, + { + match node.kind() { + $($kind => Some(Self(node)),)+ + _ => None, + } + } + + fn syntax(&self) -> &'a cstree::ResolvedNode<Self::Lang> { + &self.0 + } + + fn small_range(&self) -> TextRange { + let full_range = self.syntax().text_range(); + let start = full_range.start(); + let mut token = self.syntax().last_token(); + while let Some(current) = token { + if !matches!(current.kind(), WHITESPACE | COMMENT) { + return TextRange::new(start, current.text_range().end()); + } + token = current.prev_token(); + } + TextRange::new(start, start) + } + } + }; +} + +cst_node!(Text, TEXT); + +pub trait HasCurly<'a>: CstNode<'a, Lang = Language> { + fn left_curly(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == L_CURLY) + } + + fn right_curly(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == R_CURLY) + } + + fn content_text(&self) -> Option<String> { + self.left_curly()?; + self.right_curly()?; + let mut text = String::new(); + for child in self + .syntax() + .descendants_with_tokens() + .filter_map(|child| child.into_token()) + .filter(|token| !matches!(token.kind(), COMMENT)) + { + text.push_str(child.text()); + } + let text = text.trim_end(); + let text = text[1..text.len() - 1].trim().to_string(); + + Some(text) + } +} + +cst_node!(CurlyGroup, CURLY_GROUP); + +impl<'a> HasCurly<'a> for CurlyGroup<'a> {} + +impl<'a> CurlyGroup<'a> {} + +pub trait HasBrack<'a>: CstNode<'a, Lang = Language> { + fn left_brack(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == L_BRACK) + } + + fn right_brack(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == R_BRACK) + } + + fn content_text(&self) -> Option<String> { + self.left_brack()?; + self.right_brack()?; + let mut text = String::new(); + for child in self + .syntax() + .descendants_with_tokens() + .filter_map(|child| child.into_token()) + .filter(|token| !matches!(token.kind(), COMMENT)) + { + text.push_str(child.text()); + } + let text = text.trim_end(); + let text = text[1..text.len() - 1].trim().to_string(); + + Some(text) + } +} + +cst_node!(BrackGroup, BRACK_GROUP); + +impl<'a> BrackGroup<'a> {} + +impl<'a> HasBrack<'a> for BrackGroup<'a> {} + +pub trait HasParen<'a>: CstNode<'a, Lang = Language> { + fn left_paren(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == L_PAREN) + } + + fn right_paren(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == R_PAREN) + } +} + +cst_node!(ParenGroup, PAREN_GROUP); + +impl<'a> HasParen<'a> for ParenGroup<'a> {} + +cst_node!(MixedGroup, MIXED_GROUP); + +impl<'a> MixedGroup<'a> { + pub fn left_delim(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| matches!(node.kind().into(), L_BRACK | L_PAREN)) + } + + pub fn right_delim(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| matches!(node.kind().into(), R_BRACK | R_PAREN)) + } +} + +cst_node!(CurlyGroupWord, CURLY_GROUP_WORD); + +impl<'a> HasCurly<'a> for CurlyGroupWord<'a> {} + +impl<'a> CurlyGroupWord<'a> { + pub fn word(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == WORD) + } +} + +cst_node!(BrackGroupWord, BRACK_GROUP_WORD); + +impl<'a> HasBrack<'a> for BrackGroupWord<'a> {} + +impl<'a> BrackGroupWord<'a> { + pub fn word(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == WORD) + } +} + +cst_node!(CurlyGroupWordList, CURLY_GROUP_WORD_LIST); + +impl<'a> HasCurly<'a> for CurlyGroupWordList<'a> {} + +impl<'a> CurlyGroupWordList<'a> { + pub fn words(&self) -> impl Iterator<Item = &'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .filter(|node| node.kind() == WORD) + } +} + +cst_node!(CurlyGroupCommand, CURLY_GROUP_COMMAND); + +impl<'a> HasCurly<'a> for CurlyGroupCommand<'a> {} + +impl<'a> CurlyGroupCommand<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == GENERIC_COMMAND_NAME) + } +} + +cst_node!(Key, KEY); + +impl<'a> Key<'a> { + pub fn words(&self) -> impl Iterator<Item = &'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .filter(|node| node.kind() == WORD) + } +} + +cst_node!(Value, VALUE); + +cst_node!(KeyValuePair, KEY_VALUE_PAIR); + +impl<'a> KeyValuePair<'a> { + pub fn key(&self) -> Option<Key<'a>> { + self.syntax().children().find_map(Key::cast) + } + + pub fn value(&self) -> Option<Value<'a>> { + self.syntax().children().find_map(Value::cast) + } +} + +cst_node!(KeyValueBody, KEY_VALUE_BODY); + +impl<'a> KeyValueBody<'a> { + pub fn pairs(&self) -> impl Iterator<Item = KeyValuePair<'a>> { + self.syntax().children().filter_map(KeyValuePair::cast) + } +} + +pub trait HasKeyValueBody<'a>: CstNode<'a, Lang = Language> { + fn body(&self) -> Option<KeyValueBody<'a>> { + self.syntax().children().find_map(KeyValueBody::cast) + } +} + +cst_node!(CurlyGroupKeyValue, CURLY_GROUP_KEY_VALUE); + +impl<'a> HasCurly<'a> for CurlyGroupKeyValue<'a> {} + +impl<'a> HasKeyValueBody<'a> for CurlyGroupKeyValue<'a> {} + +cst_node!(BrackGroupKeyValue, BRACK_GROUP_KEY_VALUE); + +impl<'a> HasBrack<'a> for BrackGroupKeyValue<'a> {} + +impl<'a> HasKeyValueBody<'a> for BrackGroupKeyValue<'a> {} + +cst_node!(Formula, FORMULA); + +cst_node!(GenericCommand, GENERIC_COMMAND); + +impl<'a> GenericCommand<'a> { + pub fn name(&self) -> Option<&'a SyntaxToken> { + self.syntax() + .children_with_tokens() + .filter_map(|node| node.into_token()) + .find(|node| node.kind() == GENERIC_COMMAND_NAME) + } +} + +cst_node!(Equation, EQUATION); + +cst_node!(Begin, BEGIN); + +impl<'a> Begin<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } + + pub fn options(&self) -> Option<BrackGroup<'a>> { + self.syntax().children().find_map(BrackGroup::cast) + } +} + +cst_node!(End, END); + +impl<'a> End<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } +} + +cst_node!(Environment, ENVIRONMENT); + +impl<'a> Environment<'a> { + pub fn begin(&self) -> Option<Begin<'a>> { + self.syntax().children().find_map(Begin::cast) + } + + pub fn end(&self) -> Option<End<'a>> { + self.syntax().children().find_map(End::cast) + } +} + +cst_node!( + Section, + PART, + CHAPTER, + SECTION, + SUBSECTION, + SUBSUBSECTION, + PARAGRAPH, + SUBPARAGRAPH +); + +impl<'a> Section<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroup<'a>> { + self.syntax().children().find_map(CurlyGroup::cast) + } +} + +cst_node!(EnumItem, ENUM_ITEM); + +impl<'a> EnumItem<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn label(&self) -> Option<BrackGroup<'a>> { + self.syntax().children().find_map(BrackGroup::cast) + } +} + +cst_node!(Caption, CAPTION); + +impl<'a> Caption<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn short(&self) -> Option<BrackGroup<'a>> { + self.syntax().children().find_map(BrackGroup::cast) + } + + pub fn long(&self) -> Option<CurlyGroup<'a>> { + self.syntax().children().find_map(CurlyGroup::cast) + } +} + +cst_node!(Citation, CITATION); + +impl<'a> Citation<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn prenote(&self) -> Option<BrackGroup<'a>> { + self.syntax().children().find_map(BrackGroup::cast) + } + + pub fn postnote(&self) -> Option<BrackGroup<'a>> { + self.syntax().children().filter_map(BrackGroup::cast).nth(1) + } + + pub fn key_list(&self) -> Option<CurlyGroupWordList<'a>> { + self.syntax().children().find_map(CurlyGroupWordList::cast) + } +} + +cst_node!( + Include, + PACKAGE_INCLUDE, + CLASS_INCLUDE, + LATEX_INCLUDE, + BIBLATEX_INCLUDE, + BIBTEX_INCLUDE, + GRAPHICS_INCLUDE, + SVG_INCLUDE, + INKSCAPE_INCLUDE, + VERBATIM_INCLUDE +); + +impl<'a> Include<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn path_list(&self) -> Option<CurlyGroupWordList<'a>> { + self.syntax().children().find_map(CurlyGroupWordList::cast) + } +} + +cst_node!(Import, IMPORT); + +impl<'a> Import<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn directory(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } + + pub fn file(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax() + .children() + .filter_map(CurlyGroupWord::cast) + .nth(1) + } +} + +cst_node!(LabelDefinition, LABEL_DEFINITION); + +impl<'a> LabelDefinition<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } +} + +cst_node!(LabelReference, LABEL_REFERENCE); + +impl<'a> LabelReference<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name_list(&self) -> Option<CurlyGroupWordList<'a>> { + self.syntax().children().find_map(CurlyGroupWordList::cast) + } +} + +cst_node!(LabelReferenceRange, LABEL_REFERENCE_RANGE); + +impl<'a> LabelReferenceRange<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn from(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } + + pub fn to(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax() + .children() + .filter_map(CurlyGroupWord::cast) + .nth(1) + } +} + +cst_node!(LabelNumber, LABEL_NUMBER); + +impl<'a> LabelNumber<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } + + pub fn text(&self) -> Option<CurlyGroup<'a>> { + self.syntax().children().find_map(CurlyGroup::cast) + } +} + +cst_node!(TheoremDefinition, THEOREM_DEFINITION); + +impl<'a> TheoremDefinition<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } + + pub fn description(&self) -> Option<CurlyGroup<'a>> { + self.syntax().children().find_map(CurlyGroup::cast) + } +} + +cst_node!(CommandDefinition, COMMAND_DEFINITION, MATH_OPERATOR); + +impl<'a> CommandDefinition<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupCommand<'a>> { + self.syntax().children().find_map(CurlyGroupCommand::cast) + } + + pub fn implementation(&self) -> Option<CurlyGroup<'a>> { + self.syntax().children().find_map(CurlyGroup::cast) + } +} + +cst_node!(AcronymReference, ACRONYM_REFERENCE); + +impl<'a> AcronymReference<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } +} + +cst_node!(AcronymDefinition, ACRONYM_DEFINITION); + +impl<'a> AcronymDefinition<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } +} + +cst_node!(AcronymDeclaration, ACRONYM_DECLARATION); + +impl<'a> AcronymDeclaration<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } +} + +cst_node!(ColorDefinition, COLOR_DEFINITION); + +impl<'a> ColorDefinition<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } + + pub fn model(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax() + .children() + .filter_map(CurlyGroupWord::cast) + .nth(1) + } + + pub fn spec(&self) -> Option<CurlyGroup<'a>> { + self.syntax().children().find_map(CurlyGroup::cast) + } +} + +cst_node!(ColorSetDefinition, COLOR_SET_DEFINITION); + +impl<'a> ColorSetDefinition<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn model_list(&self) -> Option<CurlyGroupWordList<'a>> { + self.syntax().children().find_map(CurlyGroupWordList::cast) + } +} + +cst_node!(ColorReference, COLOR_REFERENCE); + +impl<'a> ColorReference<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } +} + +cst_node!(GlossaryEntryReference, GLOSSARY_ENTRY_REFERENCE); + +impl<'a> GlossaryEntryReference<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } +} + +cst_node!(GlossaryEntryDefinition, GLOSSARY_ENTRY_DEFINITION); + +impl<'a> GlossaryEntryDefinition<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name(&self) -> Option<CurlyGroupWord<'a>> { + self.syntax().children().find_map(CurlyGroupWord::cast) + } +} + +cst_node!(TikzLibraryImport, TIKZ_LIBRARY_IMPORT); + +impl<'a> TikzLibraryImport<'a> { + pub fn command(&self) -> Option<&'a SyntaxToken> { + self.syntax().first_token() + } + + pub fn name_list(&self) -> Option<CurlyGroupWordList<'a>> { + self.syntax().children().find_map(CurlyGroupWordList::cast) + } +} diff --git a/support/texlab/src/syntax/latex/kind.rs b/support/texlab/src/syntax/latex/kind.rs new file mode 100644 index 0000000000..fcab393380 --- /dev/null +++ b/support/texlab/src/syntax/latex/kind.rs @@ -0,0 +1,180 @@ +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +#[allow(non_camel_case_types)] +#[repr(u16)] +pub enum SyntaxKind { + ERROR = 0, + MISSING, + + WHITESPACE, + COMMENT, + L_CURLY, + R_CURLY, + L_BRACK, + R_BRACK, + L_PAREN, + R_PAREN, + PARAMETER, + COMMA, + EQUALITY_SIGN, + WORD, + DOLLAR, + GENERIC_COMMAND_NAME, + BEGIN_ENVIRONMENT_NAME, + END_ENVIRONMENT_NAME, + BEGIN_EQUATION_NAME, + END_EQUATION_NAME, + PART_NAME, + CHAPTER_NAME, + SECTION_NAME, + SUBSECTION_NAME, + SUBSUBSECTION_NAME, + PARAGRAPH_NAME, + SUBPARAGRAPH_NAME, + ENUM_ITEM_NAME, + CAPTION_NAME, + CITATION_NAME, + PACKAGE_INCLUDE_NAME, + CLASS_INCLUDE_NAME, + LATEX_INCLUDE_NAME, + BIBLATEX_INCLUDE_NAME, + BIBTEX_INCLUDE_NAME, + GRAPHICS_INCLUDE_NAME, + SVG_INCLUDE_NAME, + INKSCAPE_INCLUDE_NAME, + VERBATIM_INCLUDE_NAME, + IMPORT_NAME, + LABEL_DEFINITION_NAME, + LABEL_REFERENCE_NAME, + LABEL_REFERENCE_RANGE_NAME, + LABEL_NUMBER_NAME, + COMMAND_DEFINITION_NAME, + MATH_OPERATOR_NAME, + GLOSSARY_ENTRY_DEFINITION_NAME, + GLOSSARY_ENTRY_REFERENCE_NAME, + ACRONYM_DEFINITION_NAME, + ACRONYM_DECLARATION_NAME, + ACRONYM_REFERENCE_NAME, + THEOREM_DEFINITION_NAME, + COLOR_REFERENCE_NAME, + COLOR_DEFINITION_NAME, + COLOR_SET_DEFINITION_NAME, + TIKZ_LIBRARY_IMPORT_NAME, + ENVIRONMENT_DEFINIITION_NAME, + + PREAMBLE, + TEXT, + KEY, + VALUE, + KEY_VALUE_PAIR, + KEY_VALUE_BODY, + CURLY_GROUP, + CURLY_GROUP_WORD, + CURLY_GROUP_WORD_LIST, + CURLY_GROUP_COMMAND, + CURLY_GROUP_KEY_VALUE, + BRACK_GROUP, + BRACK_GROUP_WORD, + BRACK_GROUP_KEY_VALUE, + PAREN_GROUP, + MIXED_GROUP, + GENERIC_COMMAND, + ENVIRONMENT, + BEGIN, + END, + EQUATION, + PART, + CHAPTER, + SECTION, + SUBSECTION, + SUBSUBSECTION, + PARAGRAPH, + SUBPARAGRAPH, + ENUM_ITEM, + FORMULA, + CAPTION, + CITATION, + PACKAGE_INCLUDE, + CLASS_INCLUDE, + LATEX_INCLUDE, + BIBLATEX_INCLUDE, + BIBTEX_INCLUDE, + GRAPHICS_INCLUDE, + SVG_INCLUDE, + INKSCAPE_INCLUDE, + VERBATIM_INCLUDE, + IMPORT, + LABEL_DEFINITION, + LABEL_REFERENCE, + LABEL_REFERENCE_RANGE, + LABEL_NUMBER, + COMMAND_DEFINITION, + MATH_OPERATOR, + GLOSSARY_ENTRY_DEFINITION, + GLOSSARY_ENTRY_REFERENCE, + ACRONYM_DEFINITION, + ACRONYM_DECLARATION, + ACRONYM_REFERENCE, + THEOREM_DEFINITION, + COLOR_REFERENCE, + COLOR_DEFINITION, + COLOR_SET_DEFINITION, + TIKZ_LIBRARY_IMPORT, + ENVIRONMENT_DEFINITION, + ROOT, +} + +impl SyntaxKind { + pub fn is_command_name(&self) -> bool { + use SyntaxKind::*; + matches!( + self, + GENERIC_COMMAND_NAME + | BEGIN_ENVIRONMENT_NAME + | END_ENVIRONMENT_NAME + | BEGIN_EQUATION_NAME + | END_EQUATION_NAME + | PART_NAME + | CHAPTER_NAME + | SECTION_NAME + | SUBSECTION_NAME + | SUBSUBSECTION_NAME + | PARAGRAPH_NAME + | SUBPARAGRAPH_NAME + | ENUM_ITEM_NAME + | CAPTION_NAME + | CITATION_NAME + | PACKAGE_INCLUDE_NAME + | CLASS_INCLUDE_NAME + | LATEX_INCLUDE_NAME + | BIBLATEX_INCLUDE_NAME + | BIBTEX_INCLUDE_NAME + | GRAPHICS_INCLUDE_NAME + | SVG_INCLUDE_NAME + | INKSCAPE_INCLUDE_NAME + | VERBATIM_INCLUDE_NAME + | IMPORT_NAME + | LABEL_DEFINITION_NAME + | LABEL_REFERENCE_NAME + | LABEL_REFERENCE_RANGE_NAME + | LABEL_NUMBER_NAME + | COMMAND_DEFINITION_NAME + | MATH_OPERATOR_NAME + | GLOSSARY_ENTRY_DEFINITION_NAME + | GLOSSARY_ENTRY_REFERENCE_NAME + | ACRONYM_DEFINITION_NAME + | ACRONYM_DECLARATION_NAME + | ACRONYM_REFERENCE_NAME + | THEOREM_DEFINITION_NAME + | COLOR_REFERENCE_NAME + | COLOR_DEFINITION_NAME + | COLOR_SET_DEFINITION_NAME + | TIKZ_LIBRARY_IMPORT_NAME + ) + } +} + +impl From<SyntaxKind> for cstree::SyntaxKind { + fn from(kind: SyntaxKind) -> Self { + Self(kind as u16) + } +} diff --git a/support/texlab/src/syntax/latex/lexer.rs b/support/texlab/src/syntax/latex/lexer.rs index e14f1f6d85..7cd94ec897 100644 --- a/support/texlab/src/syntax/latex/lexer.rs +++ b/support/texlab/src/syntax/latex/lexer.rs @@ -1,176 +1,255 @@ -use super::ast::{Token, TokenKind}; -use crate::syntax::text::CharStream; +use logos::Logos; -#[derive(Debug)] +use super::kind::SyntaxKind; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord, Logos)] +#[allow(non_camel_case_types)] +#[repr(u16)] +enum Token { + #[regex(r"\s+")] + WHITESPACE = 2, + + #[regex(r"%[^\r\n]*")] + COMMENT, + + #[token("{")] + L_CURLY, + + #[token("}")] + R_CURLY, + + #[token("[")] + L_BRACK, + + #[token("]")] + R_BRACK, + + #[token("(")] + L_PAREN, + + #[token(")")] + R_PAREN, + + #[regex(r"#\d?")] + PARAMETER, + + #[token(",")] + COMMA, + + #[token("=")] + EQUALITY_SIGN, + + #[regex(r"[^\s\\%\{\},\$\[\]\(\)=\#]+")] + #[error] + WORD, + + #[regex(r"\$\$?")] + DOLLAR, + + #[regex(r"\\([^\r\n]|[@a-zA-Z:_]+\*?)?")] + GENERIC_COMMAND_NAME, + + #[regex(r"\\begin")] + BEGIN_ENVIRONMENT_NAME, + + #[regex(r"\\end")] + END_ENVIRONMENT_NAME, + + #[regex(r"\\\[")] + BEGIN_EQUATION_NAME, + + #[regex(r"\\\]")] + END_EQUATION_NAME, + + #[regex(r"\\part\*?")] + PART_NAME, + + #[regex(r"\\chapter\*?")] + CHAPTER_NAME, + + #[regex(r"\\section\*?")] + SECTION_NAME, + + #[regex(r"\\subsection\*?")] + SUBSECTION_NAME, + + #[regex(r"\\subsubsection\*?")] + SUBSUBSECTION_NAME, + + #[regex(r"\\paragraph\*?")] + PARAGRAPH_NAME, + + #[regex(r"\\subparagraph\*?")] + SUBPARAGRAPH_NAME, + + #[regex(r"\\item")] + ENUM_ITEM_NAME, + + #[regex(r"\\caption")] + CAPTION_NAME, + + #[regex(r"\\cite|\\cite\*|\\Cite|\\nocite|\\citet|\\citep|\\citet\*|\\citep\*|\\citeauthor|\\citeauthor\*|\\Citeauthor|\\Citeauthor\*|\\citetitle|\\citetitle\*|\\citeyear|\\citeyear\*|\\citedate|\\citedate\*|\\citeurl|\\fullcite|\\citeyearpar|\\citealt|\\citealp|\\citetext|\\parencite|\\parencite\*|\\Parencite|\\footcite|\\footfullcite|\\footcitetext|\\textcite|\\Textcite|\\smartcite|\\Smartcite|\\supercite|\\autocite|\\Autocite|\\autocite\*|\\Autocite\*|\\volcite|\\Volcite|\\pvolcite|\\Pvolcite|\\fvolcite|\\ftvolcite|\\svolcite|\\Svolcite|\\tvolcite|\\Tvolcite|\\avolcite|\\Avolcite|\\notecite|\\notecite|\\pnotecite|\\Pnotecite|\\fnotecite|\\citeA|\\citeA\*")] + CITATION_NAME, + + #[regex(r"\\usepackage|\\RequirePackage")] + PACKAGE_INCLUDE_NAME, + + #[regex(r"\\documentclass")] + CLASS_INCLUDE_NAME, + + #[regex(r"\\include|\\subfileinclude|\\input|\\subfile")] + LATEX_INCLUDE_NAME, + + #[regex(r"\\addbibresource")] + BIBLATEX_INCLUDE_NAME, + + #[regex(r"\\bibliography")] + BIBTEX_INCLUDE_NAME, + + #[regex(r"\\includegraphics")] + GRAPHICS_INCLUDE_NAME, + + #[regex(r"\\includesvg")] + SVG_INCLUDE_NAME, + + #[regex(r"\\includeinkscape")] + INKSCAPE_INCLUDE_NAME, + + #[regex(r"\\verbatiminput|\\VerbatimInput")] + VERBATIM_INCLUDE_NAME, + + #[regex(r"\\import|\\subimport|\\inputfrom|\\subimportfrom|\\includefrom|\\subincludefrom")] + IMPORT_NAME, + + #[regex(r"\\label")] + LABEL_DEFINITION_NAME, + + #[regex(r"\\ref|\\vref|\\Vref|\\autoref|\\pageref|\\cref|\\Cref|\\cref*|\\Cref*|\\namecref|\\nameCref|\\lcnamecref|\\namecrefs|\\nameCrefs|\\lcnamecrefs|\\labelcref|\\labelcpageref|\\eqref")] + LABEL_REFERENCE_NAME, + + #[regex(r"\\crefrange\*?|\\Crefrange\*?")] + LABEL_REFERENCE_RANGE_NAME, + + #[regex(r"\\newlabel")] + LABEL_NUMBER_NAME, + + #[regex(r"\\newcommand\*?|\\renewcommand|\\DeclareRobustCommand")] + COMMAND_DEFINITION_NAME, + + #[regex(r"\\DeclareMathOperator\*?")] + MATH_OPERATOR_NAME, + + #[regex(r"\\newglossaryentry")] + GLOSSARY_ENTRY_DEFINITION_NAME, + + #[regex(r"\\gls|\\Gls|\\GLS|\\glspl|\\Glspl|\\GLSpl|\\glsdisp|\\glslink|\\glstext|\\Glstext|\\GLStext|\\glsfirst|\\Glsfirst|\\GLSfirst|\\glsplural|\\Glsplural|\\GLSplural|\\glsfirstplural|\\Glsfirstplural|\\GLSfirstplural|\\glsname|\\Glsname|\\GLSname|\\glssymbol|\\Glssymbol|\\glsdesc|\\Glsdesc|\\GLSdesc|\\glsuseri|\\Glsuseri|\\GLSuseri|\\glsuserii|\\Glsuserii|\\GLSuserii|\\glsuseriii|\\Glsuseriii|\\GLSuseriii|\\glsuseriv|\\Glsuseriv|\\GLSuseriv|\\glsuserv|\\Glsuserv|\\GLSuserv|\\glsuservi|\\Glsuservi|\\GLSuservi")] + GLOSSARY_ENTRY_REFERENCE_NAME, + + #[regex(r"\\newacronym")] + ACRONYM_DEFINITION_NAME, + + #[regex(r"\\DeclareAcronym")] + ACRONYM_DECLARATION_NAME, + + #[regex(r"\\acrshort|\\Acrshort|\\ACRshort|\\acrshortpl|\\Acrshortpl|\\ACRshortpl|\\acrlong|\\Acrlong|\\ACRlong|\\acrlongpl|\\Acrlongpl|\\ACRlongpl|\\acrfull|\\Acrfull|\\ACRfull|\\acrfullpl|\\Acrfullpl|\\ACRfullpl|\\acs|\\Acs|\\acsp|\\Acsp|\\acl|\\Acl|\\aclp|\\Aclp|\\acf|\\Acf|\\acfp|\\Acfp|\\ac|\\Ac|\\acp|\\glsentrylong|\\Glsentrylong|\\glsentrylongpl|\\Glsentrylongpl|\\glsentryshort|\\Glsentryshort|\\glsentryshortpl|\\Glsentryshortpl|\\glsentryfullpl|\\Glsentryfullpl")] + ACRONYM_REFERENCE_NAME, + + #[regex(r"\\newtheorem|\\declaretheorem")] + THEOREM_DEFINITION_NAME, + + #[regex(r"\\color|\\colorbox|\\textcolor|\\pagecolor")] + COLOR_REFERENCE_NAME, + + #[regex(r"\\definecolor")] + COLOR_DEFINITION_NAME, + + #[regex(r"\\definecolorset")] + COLOR_SET_DEFINITION_NAME, + + #[regex(r"\\usepgflibrary|\\usetikzlibrary")] + TIKZ_LIBRARY_IMPORT_NAME, + + #[regex(r"\\newenvironment|\\newenvironment*")] + ENVIRONMENT_DEFINITION_NAME, +} + +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Lexer<'a> { - stream: CharStream<'a>, + tokens: Vec<(SyntaxKind, &'a str)>, } impl<'a> Lexer<'a> { pub fn new(text: &'a str) -> Self { - Self { - stream: CharStream::new(text), + let mut tokens = Vec::new(); + let mut lexer = Token::lexer(text); + while let Some(kind) = lexer.next() { + tokens.push(( + unsafe { std::mem::transmute::<Token, SyntaxKind>(kind) }, + lexer.slice(), + )); } + tokens.reverse(); + Self { tokens } } - fn single_char(&mut self, kind: TokenKind) -> Token { - self.stream.start_span(); - self.stream.next(); - let span = self.stream.end_span(); - Token::new(span, kind) + pub fn peek(&self) -> Option<SyntaxKind> { + self.tokens.last().map(|(kind, _)| *kind) } - fn math(&mut self) -> Token { - self.stream.start_span(); - self.stream.next(); - if self.stream.satifies(|c| *c == '$') { - self.stream.next(); - } - let span = self.stream.end_span(); - Token::new(span, TokenKind::Math) + pub fn eat(&mut self) -> Option<(SyntaxKind, &'a str)> { + self.tokens.pop() } +} - fn command(&mut self) -> Token { - let span = self.stream.command(); - Token::new(span, TokenKind::Command) - } +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; - fn word(&mut self) -> Token { - self.stream.start_span(); - self.stream.next(); - while self.stream.satifies(|c| is_word_char(*c)) { - self.stream.next(); - } + use super::*; - let span = self.stream.end_span(); - Token::new(span, TokenKind::Word) + fn verify(text: &str) -> Vec<(SyntaxKind, &str)> { + let mut tokens = Lexer::new(text).tokens; + tokens.reverse(); + tokens } -} -impl<'a> Iterator for Lexer<'a> { - type Item = Token; - - fn next(&mut self) -> Option<Token> { - loop { - match self.stream.peek() { - Some('%') => { - self.stream.skip_rest_of_line(); - } - Some('{') => { - return Some(self.single_char(TokenKind::BeginGroup)); - } - Some('}') => { - return Some(self.single_char(TokenKind::EndGroup)); - } - Some('[') => { - return Some(self.single_char(TokenKind::BeginOptions)); - } - Some(']') => { - return Some(self.single_char(TokenKind::EndOptions)); - } - Some('$') => { - return Some(self.math()); - } - Some(',') => { - return Some(self.single_char(TokenKind::Comma)); - } - Some('\\') => { - return Some(self.command()); - } - Some(c) => { - if c.is_whitespace() { - self.stream.next(); - } else { - return Some(self.word()); - } - } - None => { - return None; - } - } - } + #[test] + fn test_empty() { + assert_debug_snapshot!(verify(r#""#)); } -} - -fn is_word_char(c: char) -> bool { - !c.is_whitespace() - && c != '%' - && c != '{' - && c != '}' - && c != '[' - && c != ']' - && c != '\\' - && c != '$' - && c != ',' -} -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - protocol::{Position, Range}, - syntax::text::Span, - }; - - fn verify<'a>(lexer: &mut Lexer<'a>, line: u64, character: u64, text: &str, kind: TokenKind) { - let start = Position::new(line, character); - let end = Position::new(line, character + text.chars().count() as u64); - let range = Range::new(start, end); - let span = Span::new(range, text.to_owned()); - let token = Token::new(span, kind); - assert_eq!(Some(token), lexer.next()); + #[test] + fn test_delimiters() { + assert_debug_snapshot!(verify(r#"{foo} (bar) [baz, qux = foo-bar]"#)); } #[test] - fn word() { - let mut lexer = Lexer::new("foo bar baz"); - verify(&mut lexer, 0, 0, "foo", TokenKind::Word); - verify(&mut lexer, 0, 4, "bar", TokenKind::Word); - verify(&mut lexer, 0, 8, "baz", TokenKind::Word); - assert_eq!(None, lexer.next()); + fn test_command_with_parameter() { + assert_debug_snapshot!(verify(r#"\newcommand{\id}[1]{#1}"#)); } #[test] - fn command() { - let mut lexer = Lexer::new("\\foo\\bar@baz\n\\foo*"); - verify(&mut lexer, 0, 0, "\\foo", TokenKind::Command); - verify(&mut lexer, 0, 4, "\\bar@baz", TokenKind::Command); - verify(&mut lexer, 1, 0, "\\foo*", TokenKind::Command); - assert_eq!(None, lexer.next()); + fn test_command_with_star() { + assert_debug_snapshot!(verify(r#"\section*{Foo}"#)); } #[test] - fn escape_sequence() { - let mut lexer = Lexer::new("\\%\\**"); - verify(&mut lexer, 0, 0, "\\%", TokenKind::Command); - verify(&mut lexer, 0, 2, "\\*", TokenKind::Command); - verify(&mut lexer, 0, 4, "*", TokenKind::Word); - assert_eq!(None, lexer.next()); + fn test_escape_sequence() { + assert_debug_snapshot!(verify(r#"\% hello"#)); } #[test] - fn group_delimiter() { - let mut lexer = Lexer::new("{}[]"); - verify(&mut lexer, 0, 0, "{", TokenKind::BeginGroup); - verify(&mut lexer, 0, 1, "}", TokenKind::EndGroup); - verify(&mut lexer, 0, 2, "[", TokenKind::BeginOptions); - verify(&mut lexer, 0, 3, "]", TokenKind::EndOptions); - assert_eq!(None, lexer.next()); + fn test_formula() { + assert_debug_snapshot!(verify(r#"$ f(x) = y $$"#)); } #[test] - fn math() { - let mut lexer = Lexer::new("$$ $ $"); - verify(&mut lexer, 0, 0, "$$", TokenKind::Math); - verify(&mut lexer, 0, 3, "$", TokenKind::Math); - verify(&mut lexer, 0, 5, "$", TokenKind::Math); - assert_eq!(None, lexer.next()); + fn test_comment() { + assert_debug_snapshot!(verify("hello %world\r\ntest %test")); } #[test] - fn line_comment() { - let mut lexer = Lexer::new(" %foo \nfoo"); - verify(&mut lexer, 1, 0, "foo", TokenKind::Word); - assert_eq!(None, lexer.next()); + fn test_invalid_parameter() { + assert_debug_snapshot!(verify(r#"#"#)) } } diff --git a/support/texlab/src/syntax/latex/mod.rs b/support/texlab/src/syntax/latex/mod.rs deleted file mode 100644 index cee83ab04e..0000000000 --- a/support/texlab/src/syntax/latex/mod.rs +++ /dev/null @@ -1,686 +0,0 @@ -mod analysis; -mod ast; -mod lexer; -mod parser; - -pub use self::{analysis::*, ast::*}; - -use self::{lexer::Lexer, parser::Parser}; -use crate::{ - protocol::{Options, Uri}, - tex::Resolver, -}; -use std::path::Path; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct OpenParams<'a> { - pub text: &'a str, - pub uri: &'a Uri, - pub resolver: &'a Resolver, - pub options: &'a Options, - pub current_dir: &'a Path, -} - -pub fn open(params: OpenParams) -> SymbolTable { - let OpenParams { - text, - uri, - resolver, - options, - current_dir, - } = params; - - let lexer = Lexer::new(text); - let parser = Parser::new(lexer); - let tree = parser.parse(); - - let params = SymbolTableParams { - tree, - uri, - resolver, - options, - current_dir, - }; - SymbolTable::analyze(params) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - protocol::{Options, Range, RangeExt, Uri}, - syntax::{generic_ast::AstNodeIndex, text::SyntaxNode}, - tex::Resolver, - }; - use indoc::indoc; - use std::env; - - fn open_simple(text: &str) -> SymbolTable { - open(OpenParams { - text: text.trim(), - uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(), - resolver: &Resolver::default(), - options: &Options::default(), - current_dir: &env::current_dir().unwrap(), - }) - } - - #[derive(Debug, Default)] - struct TreeTraversal { - nodes: Vec<AstNodeIndex>, - } - - impl Visitor for TreeTraversal { - fn visit(&mut self, tree: &Tree, node: AstNodeIndex) { - self.nodes.push(node); - tree.walk(self, node); - } - } - - mod range { - use super::*; - - fn verify(expected_ranges: Vec<Range>, text: &str) { - let table = open_simple(text); - - let mut traversal = TreeTraversal::default(); - traversal.visit(&table.tree, table.tree.root); - let actual_ranges: Vec<_> = traversal - .nodes - .into_iter() - .map(|node| table[node].range()) - .collect(); - assert_eq!(actual_ranges, expected_ranges); - } - - #[test] - fn command() { - verify( - vec![ - Range::new_simple(0, 0, 2, 14), - Range::new_simple(0, 0, 0, 23), - Range::new_simple(0, 14, 0, 23), - Range::new_simple(0, 15, 0, 22), - Range::new_simple(1, 0, 1, 20), - Range::new_simple(1, 11, 1, 20), - Range::new_simple(1, 12, 1, 19), - Range::new_simple(2, 0, 2, 14), - Range::new_simple(2, 4, 2, 9), - Range::new_simple(2, 5, 2, 8), - Range::new_simple(2, 9, 2, 14), - Range::new_simple(2, 10, 2, 13), - ], - indoc!( - r#" - \documentclass{article} - \usepackage{amsmath} - \foo[bar]{baz} - "# - ), - ); - } - - #[test] - fn text() { - verify( - vec![ - Range::new_simple(0, 0, 0, 11), - Range::new_simple(0, 0, 0, 11), - ], - indoc!( - r#" - foo bar baz - "# - ), - ); - } - - #[test] - fn text_bracket() { - verify( - vec![Range::new_simple(0, 0, 0, 5), Range::new_simple(0, 0, 0, 5)], - indoc!( - r#" - ]foo[ - "# - ), - ); - } - - #[test] - fn group() { - verify( - vec![ - Range::new_simple(0, 0, 0, 15), - Range::new_simple(0, 0, 0, 15), - Range::new_simple(0, 2, 0, 5), - Range::new_simple(0, 6, 0, 13), - Range::new_simple(0, 8, 0, 11), - ], - indoc!( - r#" - { foo { bar } } - "# - ), - ); - } - - #[test] - fn group_incomplete() { - verify( - vec![Range::new_simple(0, 1, 0, 2), Range::new_simple(0, 1, 0, 2)], - indoc!( - r#" - }{ - "# - ), - ); - } - - #[test] - fn math() { - verify( - vec![ - Range::new_simple(0, 0, 0, 9), - Range::new_simple(0, 0, 0, 1), - Range::new_simple(0, 2, 0, 7), - Range::new_simple(0, 8, 0, 9), - ], - indoc!( - r#" - $ x = 1 $ - "# - ), - ); - } - - #[test] - fn comma() { - verify( - vec![ - Range::new_simple(0, 0, 0, 8), - Range::new_simple(0, 0, 0, 3), - Range::new_simple(0, 3, 0, 4), - Range::new_simple(0, 5, 0, 8), - ], - indoc!( - r#" - foo, bar - "# - ), - ); - } - } - - mod command { - use super::*; - - fn verify(expected_names: Vec<&str>, text: &str) { - let table = open(OpenParams { - text, - uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(), - resolver: &Resolver::default(), - options: &Options::default(), - current_dir: &env::current_dir().unwrap(), - }); - - let actual_names: Vec<_> = table - .commands - .iter() - .map(|node| table.tree.as_command(*node).unwrap().name.text()) - .collect(); - - assert_eq!(actual_names, expected_names); - } - - #[test] - fn basic() { - verify( - vec!["\\documentclass", "\\usepackage", "\\begin", "\\end"], - indoc!( - r#" - \documentclass{article} - \usepackage{amsmath} - \begin{document} - Hello World - \end{document} - "# - ), - ); - } - - #[test] - fn star() { - verify( - vec!["\\section*", "\\subsection*"], - indoc!( - r#" - \section*{Foo} - \subsection**{Bar} - "# - ), - ); - } - - #[test] - fn at() { - verify(vec!["\\foo@bar"], indoc!(r#"\foo@bar"#)); - } - - #[test] - fn escape() { - verify(vec!["\\%"], indoc!(r#"\%foo"#)) - } - } - - mod environment { - use super::*; - - fn verify(expected_names: Vec<(&str, &str)>, text: &str) { - let table = open_simple(text); - let actual_names: Vec<_> = table - .environments - .iter() - .map(|env| { - ( - env.left - .name(&table.tree) - .map(Token::text) - .unwrap_or_default(), - env.right - .name(&table.tree) - .map(Token::text) - .unwrap_or_default(), - ) - }) - .collect(); - - assert_eq!(actual_names, expected_names); - } - - #[test] - fn nested() { - verify( - vec![("b", "b"), ("a", "a")], - indoc!( - r#" - \begin{a} - \begin{b} - \end{b} - \end{a} - "# - ), - ); - } - - #[test] - fn empty_name() { - verify( - vec![("a", ""), ("", "b")], - indoc!( - r#" - \begin{a} - \end{} - \begin{} - \end{b} - "# - ), - ); - } - - #[test] - fn incomplete() { - verify( - Vec::new(), - indoc!( - r#" - \end{a} - \begin{a} - "# - ), - ); - } - - #[test] - fn standalone_true() { - let table = open_simple(r#"\begin{document}\end{document}"#); - assert!(table.is_standalone); - } - - #[test] - fn standalone_false() { - let table = open_simple(r#"\begin{doc}\end{doc}"#); - assert!(!table.is_standalone); - } - } - - mod include { - use super::*; - - fn verify(expected_targets: Vec<Vec<&str>>, resolver: Resolver, text: &str) { - let table = open(OpenParams { - text, - uri: &Uri::parse("http://www.foo.com/dir1/dir2/foo.tex").unwrap(), - resolver: &resolver, - options: &Options::default(), - current_dir: &env::current_dir().unwrap(), - }); - - assert_eq!(table.includes.len(), 1); - let include = &table.includes[0]; - let actual_targets: Vec<Vec<&str>> = include - .all_targets - .iter() - .map(|targets| targets.iter().map(|target| target.as_str()).collect()) - .collect(); - - assert_eq!(actual_targets, expected_targets); - } - - #[test] - fn same_directory() { - verify( - vec![vec![ - "http://www.foo.com/dir1/dir2/bar", - "http://www.foo.com/dir1/dir2/bar.tex", - ]], - Resolver::default(), - indoc!(r#"\include{bar}"#), - ); - } - - #[test] - fn two_paths() { - verify( - vec![ - vec![ - "http://www.foo.com/dir1/dir2/bar.tex", - "http://www.foo.com/dir1/dir2/bar.tex.tex", - ], - vec![ - "http://www.foo.com/dir1/dir2/baz.tex", - "http://www.foo.com/dir1/dir2/baz.tex.tex", - ], - ], - Resolver::default(), - indoc!(r#"\input{bar.tex, ./baz.tex}"#), - ); - } - - #[test] - fn sub_directory() { - verify( - vec![vec![ - "http://www.foo.com/dir1/dir2/dir3/bar", - "http://www.foo.com/dir1/dir2/dir3/bar.tex", - ]], - Resolver::default(), - indoc!(r#"\include{dir3/bar}"#), - ); - } - - #[test] - fn parent_directory() { - verify( - vec![vec![ - "http://www.foo.com/dir1/bar", - "http://www.foo.com/dir1/bar.tex", - ]], - Resolver::default(), - indoc!(r#"\include{../bar}"#), - ); - } - - #[test] - fn distro_file() { - let mut resolver = Resolver::default(); - let path = env::current_dir().unwrap().join("biblatex-examples.bib"); - resolver - .files_by_name - .insert("biblatex-examples.bib".into(), path.clone()); - verify( - vec![vec![ - "http://www.foo.com/dir1/dir2/biblatex-examples.bib", - "http://www.foo.com/dir1/dir2/biblatex-examples.bib.bib", - Uri::from_file_path(&path).unwrap().as_str(), - ]], - resolver, - indoc!(r#"\addbibresource{biblatex-examples.bib}"#), - ); - } - - #[test] - fn component() { - let table = open(OpenParams { - text: indoc!( - r#" - \documentclass{article} - \usepackage{amsmath} - \usepackage{geometry, lipsum} - "# - ), - uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(), - resolver: &Resolver::default(), - options: &Options::default(), - current_dir: &env::current_dir().unwrap(), - }); - assert_eq!( - table.components, - vec!["article.cls", "amsmath.sty", "geometry.sty", "lipsum.sty"] - ); - } - } - - #[test] - fn citation() { - let table = open_simple(indoc!( - r#" - \cite{key1} - \cite{key2, key3} - \nocite{*} - "# - )); - - let expected_keys = vec![vec!["key1"], vec!["key2", "key3"], vec!["*"]]; - - let actual_keys: Vec<Vec<&str>> = table - .citations - .iter() - .map(|cit| cit.keys(&table.tree).into_iter().map(Token::text).collect()) - .collect(); - - assert_eq!(actual_keys, expected_keys); - } - - #[test] - fn command_definition() { - let table = open_simple(indoc!( - r#" - \newcommand{\foo}{Foo} - \newcommand[2]{\bar}{Bar} - \renewcommand{\baz}{Baz} - \qux - "# - )); - - let expected_cmds = vec!["\\foo", "\\bar", "\\baz"]; - - let actual_cmds: Vec<&str> = table - .command_definitions - .iter() - .map(|def| def.definition_name(&table.tree)) - .collect(); - - assert_eq!(actual_cmds, expected_cmds); - } - - #[test] - fn glossary_entry() { - let table = open_simple(indoc!( - r#" - \newglossaryentry{foo}{...} - \newacronym{bar}{...} - "# - )); - - let expected_entries = vec!["foo", "bar"]; - - let actual_entries: Vec<&str> = table - .glossary_entries - .iter() - .map(|entry| entry.label(&table.tree).text()) - .collect(); - - assert_eq!(actual_entries, expected_entries); - } - - #[test] - fn equation() { - let table = open_simple(indoc!( - r#" - \[ - e^{i \pi} + 1 = 0 - \] - \] \[ - "# - )); - - assert_eq!(table.equations.len(), 1); - } - - #[test] - fn inline() { - let table = open_simple(indoc!( - r#" - $ x $ - $ - "# - )); - - assert_eq!(table.inlines.len(), 1); - } - - #[test] - fn math_operator() { - let table = open_simple(indoc!( - r#" - \DeclareMathOperator{\foo}{foo} - "# - )); - - assert_eq!(table.math_operators.len(), 1); - assert_eq!( - table.math_operators[0].definition_name(&table.tree), - "\\foo" - ); - } - - #[test] - fn theorem_definition() { - let table = open_simple(indoc!( - r#" - \newtheorem{lemma}{Lemma} - "# - )); - - assert_eq!(table.theorem_definitions.len(), 1); - assert_eq!( - table.theorem_definitions[0].name(&table.tree).text(), - "lemma" - ); - } - - #[test] - fn section() { - let table = open_simple(indoc!( - r#" - \section{Introduction to \LaTeX} - \subsection*{Foo - "# - )); - assert_eq!(table.sections.len(), 2); - assert_eq!( - table.sections[0].print(&table.tree).unwrap(), - "Introduction to \\LaTeX" - ); - assert_eq!(table.sections[1].print(&table.tree), None); - } - - #[test] - fn label() { - let table = open_simple(indoc!( - r#" - \label{foo} - \ref{bar, baz} - "# - )); - - let expected_names = vec![vec!["foo"], vec!["bar", "baz"]]; - - let actual_names: Vec<Vec<&str>> = table - .labels - .iter() - .map(|label| { - label - .names(&table.tree) - .into_iter() - .map(Token::text) - .collect() - }) - .collect(); - - assert_eq!(actual_names, expected_names); - } - - #[test] - fn label_numbering() { - let table = open_simple(indoc!( - r#" - \newlabel{foo}{{1}{1}} - "# - )); - - assert_eq!(table.label_numberings.len(), 1); - assert_eq!(table.label_numberings[0].name(&table.tree).text(), "foo"); - assert_eq!(table.label_numberings[0].number, "1"); - } - - #[test] - fn caption() { - let table = open_simple(indoc!( - r#" - \caption{Foo \LaTeX Bar} - "# - )); - - assert_eq!(table.captions.len(), 1); - assert_eq!( - table.captions[0].print(&table.tree).unwrap(), - "Foo \\LaTeX Bar" - ); - } - - #[test] - fn item_without_name() { - let table = open_simple(indoc!( - r#" - \item - "# - )); - - assert_eq!(table.items.len(), 1); - assert_eq!(table.items[0].name(&table.tree), None); - } - - #[test] - fn item_with_name() { - let table = open_simple(indoc!( - r#" - \item[foo bar] - "# - )); - - assert_eq!(table.items.len(), 1); - assert_eq!(table.items[0].name(&table.tree).unwrap(), "foo bar"); - } -} diff --git a/support/texlab/src/syntax/latex/parser.rs b/support/texlab/src/syntax/latex/parser.rs index 4ace4fbcd6..4333235ba5 100644 --- a/support/texlab/src/syntax/latex/parser.rs +++ b/support/texlab/src/syntax/latex/parser.rs @@ -1,186 +1,1709 @@ -use super::ast::*; -use crate::{ - protocol::{Range, RangeExt}, - syntax::{ - generic_ast::{Ast, AstNodeIndex}, - text::SyntaxNode, - }, +use cstree::GreenNodeBuilder; + +use super::{ + lexer::Lexer, + SyntaxKind::{self, *}, + SyntaxNode, }; -use std::iter::Peekable; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -enum Scope { - Root, - Group, - Options, +#[derive(Clone)] +pub struct Parse { + pub root: SyntaxNode, +} + +#[derive(Debug, Clone, Copy)] +struct ParserContext { + allow_environment: bool, + allow_comma: bool, +} + +impl Default for ParserContext { + fn default() -> Self { + Self { + allow_environment: true, + allow_comma: true, + } + } } #[derive(Debug)] -pub struct Parser<I: Iterator<Item = Token>> { - tree: Ast<Node>, - tokens: Peekable<I>, +struct Parser<'a> { + lexer: Lexer<'a>, + builder: GreenNodeBuilder<'static, 'static>, } -impl<I: Iterator<Item = Token>> Parser<I> { - pub fn new(tokens: I) -> Self { +impl<'a> Parser<'a> { + pub fn new(text: &'a str) -> Self { Self { - tree: Ast::new(), - tokens: tokens.peekable(), + lexer: Lexer::new(text), + builder: GreenNodeBuilder::new(), } } - pub fn parse(mut self) -> Tree { - let children = self.content(Scope::Root); + fn eat(&mut self) { + let (kind, text) = self.lexer.eat().unwrap(); + self.builder.token(kind.into(), text); + } - let range = if children.is_empty() { - Range::new_simple(0, 0, 0, 0) + fn peek(&self) -> Option<SyntaxKind> { + self.lexer.peek() + } + + fn expect(&mut self, kind: SyntaxKind) { + if self.peek() == Some(kind) { + self.eat(); + self.trivia(); } else { - let start = self.tree[children[0]].start(); - let end = self.tree[children[children.len() - 1]].end(); - Range::new(start, end) - }; + self.builder.token(MISSING.into(), ""); + } + } - let root = self.tree.add_node(Node::Root(Root { range })); - self.connect(root, &children); - Tree { - inner: self.tree, - root, + fn expect2(&mut self, kind1: SyntaxKind, kind2: SyntaxKind) { + if self + .peek() + .filter(|&kind| kind == kind1 || kind == kind2) + .is_some() + { + self.eat(); + self.trivia(); + } else { + self.builder.token(MISSING.into(), ""); } } - fn content(&mut self, scope: Scope) -> Vec<AstNodeIndex> { - let mut children = Vec::new(); - while let Some(ref token) = self.tokens.peek() { - match token.kind { - TokenKind::Word | TokenKind::BeginOptions => { - children.push(self.text(scope)); - } - TokenKind::Command => { - children.push(self.command()); - } - TokenKind::Comma => { - children.push(self.comma()); - } - TokenKind::Math => { - children.push(self.math()); - } - TokenKind::BeginGroup => { - children.push(self.group(GroupKind::Group)); - } - TokenKind::EndGroup => { - if scope == Scope::Root { - self.tokens.next(); - } else { - return children; - } - } - TokenKind::EndOptions => { - if scope == Scope::Options { - return children; + fn trivia(&mut self) { + while self + .peek() + .filter(|&kind| matches!(kind, WHITESPACE | COMMENT)) + .is_some() + { + self.eat(); + } + } + + pub fn parse(mut self) -> Parse { + self.builder.start_node(ROOT.into()); + self.preamble(); + while self.peek().is_some() { + self.content(ParserContext::default()); + } + self.builder.finish_node(); + let (green_node, interner) = self.builder.finish(); + Parse { + root: SyntaxNode::new_root_with_resolver(green_node, interner.unwrap()), + } + } + + fn content(&mut self, context: ParserContext) { + match self.peek().unwrap() { + WHITESPACE | COMMENT => self.eat(), + L_CURLY if context.allow_environment => self.curly_group(), + L_CURLY => self.curly_group_without_environments(), + L_BRACK | L_PAREN => self.mixed_group(), + R_CURLY | R_BRACK | R_PAREN => { + self.builder.start_node(ERROR.into()); + self.eat(); + self.builder.finish_node(); + } + PARAMETER => self.eat(), + WORD | COMMA => self.text(context), + EQUALITY_SIGN => self.eat(), + DOLLAR => self.formula(), + GENERIC_COMMAND_NAME => self.generic_command(), + BEGIN_ENVIRONMENT_NAME if context.allow_environment => self.environment(), + BEGIN_ENVIRONMENT_NAME => self.generic_command(), + END_ENVIRONMENT_NAME => self.generic_command(), + BEGIN_EQUATION_NAME => self.equation(), + END_EQUATION_NAME => self.generic_command(), + MISSING | ERROR => self.eat(), + PART_NAME => self.part(), + CHAPTER_NAME => self.chapter(), + SECTION_NAME => self.section(), + SUBSECTION_NAME => self.subsection(), + SUBSUBSECTION_NAME => self.subsubsection(), + PARAGRAPH_NAME => self.paragraph(), + SUBPARAGRAPH_NAME => self.subparagraph(), + ENUM_ITEM_NAME => self.enum_item(), + CAPTION_NAME => self.caption(), + CITATION_NAME => self.citation(), + PACKAGE_INCLUDE_NAME => self.package_include(), + CLASS_INCLUDE_NAME => self.class_include(), + LATEX_INCLUDE_NAME => self.latex_include(), + BIBLATEX_INCLUDE_NAME => self.biblatex_include(), + BIBTEX_INCLUDE_NAME => self.bibtex_include(), + GRAPHICS_INCLUDE_NAME => self.graphics_include(), + SVG_INCLUDE_NAME => self.svg_include(), + INKSCAPE_INCLUDE_NAME => self.inkscape_include(), + VERBATIM_INCLUDE_NAME => self.verbatim_include(), + IMPORT_NAME => self.import(), + LABEL_DEFINITION_NAME => self.label_definition(), + LABEL_REFERENCE_NAME => self.label_reference(), + LABEL_REFERENCE_RANGE_NAME => self.label_reference_range(), + LABEL_NUMBER_NAME => self.label_number(), + COMMAND_DEFINITION_NAME => self.command_definition(), + MATH_OPERATOR_NAME => self.math_operator(), + GLOSSARY_ENTRY_DEFINITION_NAME => self.glossary_entry_definition(), + GLOSSARY_ENTRY_REFERENCE_NAME => self.glossary_entry_reference(), + ACRONYM_DEFINITION_NAME => self.acronym_definition(), + ACRONYM_DECLARATION_NAME => self.acronym_declaration(), + ACRONYM_REFERENCE_NAME => self.acronym_reference(), + THEOREM_DEFINITION_NAME => self.theorem_definition(), + COLOR_REFERENCE_NAME => self.color_reference(), + COLOR_DEFINITION_NAME => self.color_definition(), + COLOR_SET_DEFINITION_NAME => self.color_set_definition(), + TIKZ_LIBRARY_IMPORT_NAME => self.tikz_library_import(), + ENVIRONMENT_DEFINIITION_NAME => self.environment_definition(), + _ => unreachable!(), + } + } + + fn text(&mut self, context: ParserContext) { + self.builder.start_node(TEXT.into()); + self.eat(); + while self + .peek() + .filter(|&kind| { + matches!(kind, WHITESPACE | COMMENT | WORD | COMMA) + && (context.allow_comma || kind != COMMA) + }) + .is_some() + { + self.eat(); + } + self.builder.finish_node(); + } + + fn group_with_token( + &mut self, + node_kind: SyntaxKind, + content_kind: SyntaxKind, + right_kind: SyntaxKind, + ) { + self.builder.start_node(node_kind.into()); + self.eat(); + self.trivia(); + self.expect(content_kind); + self.expect(right_kind); + self.builder.finish_node(); + } + + fn curly_group(&mut self) { + self.builder.start_node(CURLY_GROUP.into()); + self.eat(); + while self + .peek() + .filter(|&kind| !matches!(kind, R_CURLY | END_ENVIRONMENT_NAME)) + .is_some() + { + self.content(ParserContext::default()); + } + self.expect(R_CURLY); + self.builder.finish_node(); + } + + fn curly_group_without_environments(&mut self) { + self.builder.start_node(CURLY_GROUP.into()); + self.eat(); + while self + .peek() + .filter(|&kind| !matches!(kind, R_CURLY)) + .is_some() + { + self.content(ParserContext { + allow_environment: false, + allow_comma: true, + }); + } + self.expect(R_CURLY); + self.builder.finish_node(); + } + + fn curly_group_word(&mut self) { + self.group_with_token(CURLY_GROUP_WORD, WORD, R_CURLY); + } + + fn curly_group_word_list(&mut self) { + self.builder.start_node(CURLY_GROUP_WORD_LIST.into()); + self.eat(); + + while self + .peek() + .filter(|&kind| matches!(kind, WHITESPACE | COMMENT | WORD | COMMA)) + .is_some() + { + self.eat(); + } + + self.expect(R_CURLY); + self.builder.finish_node(); + } + + fn curly_group_command(&mut self) { + self.group_with_token(CURLY_GROUP_COMMAND, GENERIC_COMMAND_NAME, R_CURLY); + } + + fn brack_group(&mut self) { + self.builder.start_node(BRACK_GROUP.into()); + self.eat(); + while self + .peek() + .filter(|&kind| { + !matches!( + kind, + R_CURLY + | R_BRACK + | PART_NAME + | CHAPTER_NAME + | SECTION_NAME + | SUBSECTION_NAME + | PARAGRAPH_NAME + | SUBPARAGRAPH_NAME + | ENUM_ITEM_NAME + | END_ENVIRONMENT_NAME + ) + }) + .is_some() + { + self.content(ParserContext::default()); + } + self.expect(R_BRACK); + self.builder.finish_node(); + } + + fn brack_group_word(&mut self) { + self.group_with_token(BRACK_GROUP_WORD, WORD, R_BRACK); + } + + // fn paren_group(&mut self) { + // self.builder.start_node(PAREN_GROUP.into()); + // self.eat(); + // while self + // .peek() + // .filter(|&kind| { + // !matches!( + // kind, + // R_CURLY + // | R_BRACK + // | R_PAREN + // | PART_NAME + // | CHAPTER_NAME + // | SECTION_NAME + // | SUBSECTION_NAME + // | PARAGRAPH_NAME + // | SUBPARAGRAPH_NAME + // | ENUM_ITEM_NAME + // | END_ENVIRONMENT_NAME + // ) + // }) + // .is_some() + // { + // self.content(); + // } + // self.expect(R_PAREN); + // self.builder.finish_node(); + // } + + fn mixed_group(&mut self) { + self.builder.start_node(MIXED_GROUP.into()); + self.eat(); + self.trivia(); + while self + .peek() + .filter(|&kind| { + !matches!( + kind, + R_CURLY + | R_BRACK + | R_PAREN + | PART_NAME + | CHAPTER_NAME + | SECTION_NAME + | SUBSECTION_NAME + | PARAGRAPH_NAME + | SUBPARAGRAPH_NAME + | ENUM_ITEM_NAME + | END_ENVIRONMENT_NAME + ) + }) + .is_some() + { + self.content(ParserContext::default()); + } + self.expect2(R_BRACK, R_PAREN); + self.builder.finish_node(); + } + + fn key(&mut self) { + self.builder.start_node(KEY.into()); + self.eat(); + while self + .peek() + .filter(|&kind| matches!(kind, WHITESPACE | COMMENT | WORD)) + .is_some() + { + self.eat(); + } + self.builder.finish_node(); + } + + fn value(&mut self) { + self.builder.start_node(VALUE.into()); + self.content(ParserContext { + allow_environment: true, + allow_comma: false, + }); + self.builder.finish_node(); + } + + fn key_value_pair(&mut self) { + self.builder.start_node(KEY_VALUE_PAIR.into()); + self.key(); + if self.peek() == Some(EQUALITY_SIGN) { + self.eat(); + self.trivia(); + if self + .peek() + .filter(|&kind| { + !matches!( + kind, + END_ENVIRONMENT_NAME | R_CURLY | R_BRACK | R_PAREN | COMMA + ) + }) + .is_some() + { + self.value(); + } else { + self.builder.token(MISSING.into(), ""); + } + } + + self.builder.finish_node(); + } + + fn key_value_body(&mut self) { + self.builder.start_node(KEY_VALUE_BODY.into()); + while let Some(kind) = self.peek() { + match kind { + WHITESPACE | COMMENT => self.eat(), + WORD => { + self.key_value_pair(); + if self.peek() == Some(COMMA) { + self.eat(); } else { - children.push(self.text(scope)); + break; } } + _ => break, } } - children - } - - fn group(&mut self, kind: GroupKind) -> AstNodeIndex { - let left = self.tokens.next().unwrap(); - let scope = match kind { - GroupKind::Group => Scope::Group, - GroupKind::Options => Scope::Options, - }; - - let children = self.content(scope); - let right_kind = match kind { - GroupKind::Group => TokenKind::EndGroup, - GroupKind::Options => TokenKind::EndOptions, - }; - - let right = if self.next_of_kind(right_kind) { - self.tokens.next() - } else { - None - }; - - let end = right - .as_ref() - .map(SyntaxNode::end) - .or_else(|| children.last().map(|child| self.tree[*child].end())) - .unwrap_or_else(|| left.end()); - let range = Range::new(left.start(), end); - - let node = self.tree.add_node(Node::Group(Group { - range, - left, - kind, - right, - })); - self.connect(node, &children); - node - } - - fn command(&mut self) -> AstNodeIndex { - let name = self.tokens.next().unwrap(); - let mut children = Vec::new(); - while let Some(token) = self.tokens.peek() { - match token.kind { - TokenKind::BeginGroup => children.push(self.group(GroupKind::Group)), - TokenKind::BeginOptions => children.push(self.group(GroupKind::Options)), + self.builder.finish_node(); + } + + fn group_key_value(&mut self, node_kind: SyntaxKind, right_kind: SyntaxKind) { + self.builder.start_node(node_kind.into()); + self.eat(); + self.trivia(); + self.key_value_body(); + self.expect(right_kind); + self.builder.finish_node(); + } + + fn curly_group_key_value(&mut self) { + self.group_key_value(CURLY_GROUP_KEY_VALUE, R_CURLY); + } + + fn brack_group_key_value(&mut self) { + self.group_key_value(BRACK_GROUP_KEY_VALUE, R_BRACK); + } + + fn formula(&mut self) { + self.builder.start_node(FORMULA.into()); + self.eat(); + self.trivia(); + while self + .peek() + .filter(|&kind| !matches!(kind, R_CURLY | END_ENVIRONMENT_NAME | DOLLAR)) + .is_some() + { + self.content(ParserContext::default()); + } + self.expect(DOLLAR); + self.builder.finish_node(); + } + + fn generic_command(&mut self) { + self.builder.start_node(GENERIC_COMMAND.into()); + self.eat(); + while let Some(kind) = self.peek() { + match kind { + WHITESPACE | COMMENT => self.eat(), + L_CURLY => self.curly_group(), + L_BRACK | L_PAREN => self.mixed_group(), _ => break, } } + self.builder.finish_node(); + } + + fn equation(&mut self) { + self.builder.start_node(EQUATION.into()); + self.eat(); + while self + .peek() + .filter(|&kind| !matches!(kind, END_ENVIRONMENT_NAME | R_CURLY | END_EQUATION_NAME)) + .is_some() + { + self.content(ParserContext::default()); + } + self.expect(END_EQUATION_NAME); + self.builder.finish_node(); + } - let end = children - .last() - .map(|child| self.tree[*child].end()) - .unwrap_or_else(|| name.end()); - let range = Range::new(name.start(), end); + fn begin(&mut self) { + self.builder.start_node(BEGIN.into()); + self.eat(); + self.trivia(); - let node = self.tree.add_node(Node::Command(Command { range, name })); - self.connect(node, &children); - node + if self.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.peek() == Some(L_BRACK) { + self.brack_group(); + } + self.builder.finish_node(); } - fn text(&mut self, scope: Scope) -> AstNodeIndex { - let mut words = Vec::new(); - while let Some(ref token) = self.tokens.peek() { - let kind = token.kind; - let opts = kind == TokenKind::EndOptions && scope != Scope::Options; - if kind == TokenKind::Word || kind == TokenKind::BeginOptions || opts { - words.push(self.tokens.next().unwrap()); - } else { - break; - } + fn end(&mut self) { + self.builder.start_node(END.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); } - let range = Range::new(words[0].start(), words[words.len() - 1].end()); - self.tree.add_node(Node::Text(Text { range, words })) + + self.builder.finish_node(); } - fn comma(&mut self) -> AstNodeIndex { - let token = self.tokens.next().unwrap(); - let range = token.range(); - self.tree.add_node(Node::Comma(Comma { range, token })) + fn environment(&mut self) { + self.builder.start_node(ENVIRONMENT.into()); + self.begin(); + + while self + .peek() + .filter(|&kind| kind != END_ENVIRONMENT_NAME) + .is_some() + { + self.content(ParserContext::default()); + } + + if self.peek() == Some(END_ENVIRONMENT_NAME) { + self.end(); + } else { + self.builder.token(MISSING.into(), ""); + } + self.builder.finish_node(); } - fn math(&mut self) -> AstNodeIndex { - let token = self.tokens.next().unwrap(); - let range = token.range(); - self.tree.add_node(Node::Math(Math { range, token })) + fn preamble(&mut self) { + self.builder.start_node(PREAMBLE.into()); + while self + .peek() + .filter(|&kind| kind != END_ENVIRONMENT_NAME) + .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); } - fn connect(&mut self, parent: AstNodeIndex, children: &[AstNodeIndex]) { - for child in children { - self.tree.add_edge(parent, *child); + fn part(&mut self) { + self.builder.start_node(PART.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); } + + while self + .peek() + .filter(|&kind| !matches!(kind, END_ENVIRONMENT_NAME | R_CURLY | PART_NAME)) + .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); } - fn next_of_kind(&mut self, kind: TokenKind) -> bool { - self.tokens + fn chapter(&mut self) { + self.builder.start_node(CHAPTER.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + while self .peek() - .filter(|token| token.kind == kind) + .filter(|&kind| { + !matches!( + kind, + END_ENVIRONMENT_NAME | R_CURLY | PART_NAME | CHAPTER_NAME + ) + }) .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); + } + + fn section(&mut self) { + self.builder.start_node(SECTION.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + while self + .peek() + .filter(|&kind| { + !matches!( + kind, + END_ENVIRONMENT_NAME | R_CURLY | PART_NAME | CHAPTER_NAME | SECTION_NAME + ) + }) + .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); + } + + fn subsection(&mut self) { + self.builder.start_node(SUBSECTION.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + while self + .peek() + .filter(|&kind| { + !matches!( + kind, + END_ENVIRONMENT_NAME + | R_CURLY + | PART_NAME + | CHAPTER_NAME + | SECTION_NAME + | SUBSECTION_NAME + ) + }) + .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); + } + + fn subsubsection(&mut self) { + self.builder.start_node(SUBSUBSECTION.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + while self + .peek() + .filter(|&kind| { + !matches!( + kind, + END_ENVIRONMENT_NAME + | R_CURLY + | PART_NAME + | CHAPTER_NAME + | SECTION_NAME + | SUBSECTION_NAME + | SUBSUBSECTION_NAME + ) + }) + .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); + } + + fn paragraph(&mut self) { + self.builder.start_node(PARAGRAPH.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + while self + .peek() + .filter(|&kind| { + !matches!( + kind, + END_ENVIRONMENT_NAME + | R_CURLY + | PART_NAME + | CHAPTER_NAME + | SECTION_NAME + | SUBSECTION_NAME + | SUBSUBSECTION_NAME + | PARAGRAPH_NAME + ) + }) + .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); + } + + fn subparagraph(&mut self) { + self.builder.start_node(SUBPARAGRAPH.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + while self + .peek() + .filter(|&kind| { + !matches!( + kind, + END_ENVIRONMENT_NAME + | R_CURLY + | PART_NAME + | CHAPTER_NAME + | SECTION_NAME + | SUBSECTION_NAME + | SUBSUBSECTION_NAME + | PARAGRAPH_NAME + | SUBPARAGRAPH_NAME + ) + }) + .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); + } + + fn enum_item(&mut self) { + self.builder.start_node(ENUM_ITEM.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_BRACK) { + self.brack_group(); + } + + while self + .peek() + .filter(|&kind| { + !matches!( + kind, + END_ENVIRONMENT_NAME + | R_CURLY + | PART_NAME + | CHAPTER_NAME + | SECTION_NAME + | SUBSECTION_NAME + | SUBSUBSECTION_NAME + | PARAGRAPH_NAME + | SUBPARAGRAPH_NAME + | ENUM_ITEM_NAME + ) + }) + .is_some() + { + self.content(ParserContext::default()); + } + self.builder.finish_node(); + } + + fn caption(&mut self) { + self.builder.start_node(CAPTION.into()); + self.eat(); + self.trivia(); + + if self.peek() == Some(L_BRACK) { + self.brack_group(); + } + + if self.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn citation(&mut self) { + self.builder.start_node(CITATION.into()); + self.eat(); + self.trivia(); + for _ in 0..2 { + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group(); + } + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word_list(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn generic_include(&mut self, kind: SyntaxKind, options: bool) { + self.builder.start_node(kind.into()); + self.eat(); + self.trivia(); + if options && self.lexer.peek() == Some(L_BRACK) { + self.brack_group_key_value(); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word_list(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn package_include(&mut self) { + self.generic_include(PACKAGE_INCLUDE, true); + } + + fn class_include(&mut self) { + self.generic_include(CLASS_INCLUDE, true); + } + + fn latex_include(&mut self) { + self.generic_include(LATEX_INCLUDE, false); + } + + fn biblatex_include(&mut self) { + self.generic_include(BIBLATEX_INCLUDE, true); + } + + fn bibtex_include(&mut self) { + self.generic_include(BIBTEX_INCLUDE, false); + } + + fn graphics_include(&mut self) { + self.generic_include(GRAPHICS_INCLUDE, true); + } + + fn svg_include(&mut self) { + self.generic_include(SVG_INCLUDE, true); + } + + fn inkscape_include(&mut self) { + self.generic_include(INKSCAPE_INCLUDE, true); + } + + fn verbatim_include(&mut self) { + self.generic_include(VERBATIM_INCLUDE, false); + } + + fn import(&mut self) { + self.builder.start_node(IMPORT.into()); + self.eat(); + self.trivia(); + + for _ in 0..2 { + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + } + + self.builder.finish_node(); + } + + fn label_definition(&mut self) { + self.builder.start_node(LABEL_DEFINITION.into()); + self.eat(); + self.trivia(); + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + self.builder.finish_node(); + } + + fn label_reference(&mut self) { + self.builder.start_node(LABEL_REFERENCE.into()); + self.eat(); + self.trivia(); + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word_list(); + } else { + self.builder.token(MISSING.into(), ""); + } + self.builder.finish_node(); + } + + fn label_reference_range(&mut self) { + self.builder.start_node(LABEL_REFERENCE_RANGE.into()); + self.eat(); + self.trivia(); + + for _ in 0..2 { + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + } + + self.builder.finish_node(); + } + + fn label_number(&mut self) { + self.builder.start_node(LABEL_NUMBER.into()); + self.eat(); + self.trivia(); + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group(); + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn command_definition(&mut self) { + self.builder.start_node(COMMAND_DEFINITION.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_command(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group_word(); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn math_operator(&mut self) { + self.builder.start_node(MATH_OPERATOR.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_command(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn glossary_entry_definition(&mut self) { + self.builder.start_node(GLOSSARY_ENTRY_DEFINITION.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_key_value(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn glossary_entry_reference(&mut self) { + self.builder.start_node(GLOSSARY_ENTRY_REFERENCE.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group_key_value(); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn acronym_definition(&mut self) { + self.builder.start_node(ACRONYM_DEFINITION.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group_key_value(); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + for _ in 0..2 { + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + } + + self.builder.finish_node(); + } + + fn acronym_declaration(&mut self) { + self.builder.start_node(ACRONYM_DECLARATION.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_key_value(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn acronym_reference(&mut self) { + self.builder.start_node(ACRONYM_REFERENCE.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group_key_value(); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn theorem_definition(&mut self) { + self.builder.start_node(THEOREM_DEFINITION.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group_word(); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group_word(); + } + + self.builder.finish_node(); + } + + fn color_reference(&mut self) { + self.builder.start_node(COLOR_REFERENCE.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn color_definition(&mut self) { + self.builder.start_node(COLOR_DEFINITION.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn color_set_definition(&mut self) { + self.builder.start_node(COLOR_SET_DEFINITION.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group_word(); + } + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word_list(); + } else { + self.builder.token(MISSING.into(), ""); + } + + for _ in 0..3 { + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + } + + self.builder.finish_node(); + } + + fn tikz_library_import(&mut self) { + self.builder.start_node(TIKZ_LIBRARY_IMPORT.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word_list(); + } else { + self.builder.token(MISSING.into(), ""); + } + + self.builder.finish_node(); + } + + fn environment_definition(&mut self) { + self.builder.start_node(ENVIRONMENT_DEFINITION.into()); + self.eat(); + self.trivia(); + + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_word(); + } else { + self.builder.token(MISSING.into(), ""); + } + + if self.lexer.peek() == Some(L_BRACK) { + self.brack_group_word(); + } + + for _ in 0..2 { + if self.lexer.peek() == Some(L_CURLY) { + self.curly_group_without_environments(); + } else { + self.builder.token(MISSING.into(), ""); + } + } + + self.builder.finish_node(); + } +} + +pub fn parse(text: &str) -> Parse { + Parser::new(text).parse() +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::*; + + fn setup(text: &str) -> SyntaxNode { + parse(&text.trim().replace("\r", "")).root + } + + #[test] + fn test_empty() { + assert_debug_snapshot!(setup(r#""#)); + } + + #[test] + fn test_hello_world() { + assert_debug_snapshot!(setup(r#"Hello World!"#)); + } + + #[test] + fn test_generic_command_empty() { + assert_debug_snapshot!(setup(r#"\foo"#)); + } + + #[test] + fn test_generic_command_escape() { + assert_debug_snapshot!(setup(r#"\#"#)); + } + + #[test] + fn test_generic_command_args() { + assert_debug_snapshot!(setup(r#"\foo{bar}[qux]"#)); + } + + #[test] + fn test_inline() { + assert_debug_snapshot!(setup(r#"$x \in [0, \infty)$"#)); + } + + #[test] + fn test_inline_double_dollar() { + assert_debug_snapshot!(setup(r#"$$x \in [0, \infty)$$"#)); + } + + #[test] + fn test_brace_group_simple() { + assert_debug_snapshot!(setup(r#"{hello world}"#)); + } + + #[test] + fn test_brace_group_missing_end() { + assert_debug_snapshot!(setup(r#"{hello world"#)); + } + + #[test] + fn test_unmatched_braces() { + assert_debug_snapshot!(setup(r#"}{"#)); + } + + #[test] + fn test_unmatched_brackets() { + assert_debug_snapshot!(setup(r#"]["#)); + } + + #[test] + fn test_unmatched_brackets_with_group() { + assert_debug_snapshot!(setup(r#"{][}"#)); + } + + #[test] + fn test_escaped_brackets() { + assert_debug_snapshot!(setup(r#"{[}{]}"#)); + } + + #[test] + fn test_parameter() { + assert_debug_snapshot!(setup(r#"#1"#)); + } + + #[test] + fn test_parameter_error() { + assert_debug_snapshot!(setup(r#"#"#)); + } + + #[test] + fn test_environment_simple() { + assert_debug_snapshot!(setup(r#"\begin{foo} Hello World \end{bar}"#)); + } + + #[test] + fn test_environment_nested() { + assert_debug_snapshot!(setup(r#"\begin{foo} \begin{qux} \end{baz} \end{bar}"#)); + } + + #[test] + fn test_environment_nested_missing_braces() { + assert_debug_snapshot!(setup( + r#"\begin{foo \begin{qux Hello World \end{baz} \end{bar"# + )); + } + + #[test] + fn test_structure_siblings() { + assert_debug_snapshot!(setup(r#"\section{Foo} Foo \section{Bar} Bar"#)); + } + + #[test] + fn test_structure_nested() { + assert_debug_snapshot!(setup( + r#"\part{1}\chapter{2}\section{3}\subsection{4}\subsubsection{5}\paragraph{6}\subparagraph{7}"# + )); + } + + #[test] + fn test_structure_enum_item() { + assert_debug_snapshot!(setup( + r#"\begin{enumerate} \item 1 \item[2] 2 \item 3 \end{enumerate}"# + )); + } + + #[test] + fn test_structure_invalid_nesting() { + assert_debug_snapshot!(setup(r#"\section{Foo} \chapter{Bar}"#)); + } + + #[test] + fn test_equation() { + assert_debug_snapshot!(setup(r#"\[ foo bar \]"#)); + } + + #[test] + fn test_equation_missing_end() { + assert_debug_snapshot!(setup(r#"\begin{a} \[ foo bar \end{b}"#)); + } + + #[test] + fn test_equation_missing_begin() { + assert_debug_snapshot!(setup(r#"\begin{a} foo bar \] \end{b}"#)); + } + + #[test] + fn test_caption_minimal() { + assert_debug_snapshot!(setup(r#"\caption{Foo \Bar Baz}"#)); + } + + #[test] + fn test_caption_minimal_error() { + assert_debug_snapshot!(setup(r#"\caption{Foo \Bar Baz"#)); + } + + #[test] + fn test_caption() { + assert_debug_snapshot!(setup(r#"\caption[qux]{Foo \Bar Baz}"#)); + } + + #[test] + fn test_caption_error() { + assert_debug_snapshot!(setup(r#"\caption[qux]{Foo \Bar Baz"#)); + } + + #[test] + fn test_caption_figure() { + assert_debug_snapshot!(setup(r#"\begin{figure}\caption{Foo}\end{figure}"#)); + } + + #[test] + fn test_citation_empty() { + assert_debug_snapshot!(setup(r#"\cite{}"#)); + } + + #[test] + fn test_citation_simple() { + assert_debug_snapshot!(setup(r#"\cite{foo}"#)); + } + + #[test] + fn test_citation_multiple_keys() { + assert_debug_snapshot!(setup(r#"\cite{foo, bar}"#)); + } + + #[test] + fn test_citation_star() { + assert_debug_snapshot!(setup(r#"\nocite{*}"#)); + } + + #[test] + fn test_citation_prenote() { + assert_debug_snapshot!(setup(r#"\cite[foo]{bar}"#)); + } + + #[test] + fn test_citation_prenote_postnote() { + assert_debug_snapshot!(setup(r#"\cite[foo][bar]{baz}"#)); + } + + #[test] + fn test_citation_missing_brace() { + assert_debug_snapshot!(setup(r#"\cite{foo"#)); + } + + #[test] + fn test_citation_redundant_comma() { + assert_debug_snapshot!(setup(r#"\cite{,foo,}"#)); + } + + #[test] + fn test_package_include_empty() { + assert_debug_snapshot!(setup(r#"\usepackage{}"#)); + } + + #[test] + fn test_package_include_simple() { + assert_debug_snapshot!(setup(r#"\usepackage{amsmath}"#)); + } + + #[test] + fn test_package_include_multiple() { + assert_debug_snapshot!(setup(r#"\usepackage{amsmath, lipsum}"#)); + } + + #[test] + fn test_package_include_options() { + assert_debug_snapshot!(setup(r#"\usepackage[foo = bar, baz, qux]{amsmath}"#)); + } + + #[test] + fn test_class_include_empty() { + assert_debug_snapshot!(setup(r#"\documentclass{}"#)); + } + + #[test] + fn test_class_include_simple() { + assert_debug_snapshot!(setup(r#"\documentclass{article}"#)); + } + + #[test] + fn test_class_include_options() { + assert_debug_snapshot!(setup(r#"\documentclass[foo = bar, baz, qux]{article}"#)); + } + + #[test] + fn test_latex_include_simple() { + assert_debug_snapshot!(setup(r#"\include{foo/bar}"#)); + } + + #[test] + fn test_latex_input_simple() { + assert_debug_snapshot!(setup(r#"\input{foo/bar.tex}"#)); + } + + #[test] + fn test_biblatex_include_simple() { + assert_debug_snapshot!(setup(r#"\addbibresource{foo/bar.bib}"#)); + } + + #[test] + fn test_biblatex_include_options() { + assert_debug_snapshot!(setup(r#"\addbibresource[foo=bar, baz]{foo/bar.bib}"#)); + } + + #[test] + fn test_bibtex_include_simple() { + assert_debug_snapshot!(setup(r#"\bibliography{foo/bar}"#)); + } + + #[test] + fn test_graphics_include_simple() { + assert_debug_snapshot!(setup(r#"\includegraphics{foo/bar.pdf}"#)); + } + + #[test] + fn test_graphics_include_options() { + assert_debug_snapshot!(setup(r#"\includegraphics[scale=.5]{foo/bar.pdf}"#)); + } + + #[test] + fn test_svg_include_simple() { + assert_debug_snapshot!(setup(r#"\includesvg{foo/bar.svg}"#)); + } + + #[test] + fn test_svg_include_options() { + assert_debug_snapshot!(setup(r#"\includesvg[scale=.5]{foo/bar.svg}"#)); + } + + #[test] + fn test_inkscape_include_simple() { + assert_debug_snapshot!(setup(r#"\includesvg{foo/bar}"#)); + } + + #[test] + fn test_inkscape_include_options() { + assert_debug_snapshot!(setup(r#"\includesvg[scale=.5]{foo/bar}"#)); + } + + #[test] + fn test_verbatim_include_simple() { + assert_debug_snapshot!(setup(r#"\verbatiminput{foo/bar.txt}"#)); + } + + #[test] + fn test_import_simple() { + assert_debug_snapshot!(setup(r#"\import{foo}{bar}"#)); + } + + #[test] + fn test_import_incomplete() { + assert_debug_snapshot!(setup(r#"\import{foo"#)); + } + + #[test] + fn test_label_definition_simple() { + assert_debug_snapshot!(setup(r#"\label{foo}"#)); + } + + #[test] + fn test_label_reference_simple() { + assert_debug_snapshot!(setup(r#"\ref{foo}"#)); + } + + #[test] + fn test_label_reference_multiple() { + assert_debug_snapshot!(setup(r#"\ref{foo, bar}"#)); + } + + #[test] + fn test_equation_label_reference_simple() { + assert_debug_snapshot!(setup(r#"\eqref{foo}"#)); + } + + #[test] + fn test_label_reference_range_simple() { + assert_debug_snapshot!(setup(r#"\crefrange{foo}{bar}"#)); + } + + #[test] + fn test_label_reference_range_incomplete() { + assert_debug_snapshot!(setup(r#"\crefrange{foo}"#)); + } + + #[test] + fn test_label_reference_range_error() { + assert_debug_snapshot!(setup(r#"\crefrange{foo{bar}"#)); + } + + #[test] + fn test_label_number() { + assert_debug_snapshot!(setup(r#"\newlabel{foo}{{1.1}}"#)); + } + + #[test] + fn test_command_definition_simple() { + assert_debug_snapshot!(setup(r#"\newcommand[1]{\id}{#1}"#)); + } + + #[test] + fn test_command_definition_no_argc() { + assert_debug_snapshot!(setup(r#"\newcommand{\foo}{foo}"#)); + } + + #[test] + fn test_command_definition_no_impl() { + assert_debug_snapshot!(setup(r#"\newcommand{\foo}"#)); + } + + #[test] + fn test_command_definition_no_impl_error() { + assert_debug_snapshot!(setup(r#"\newcommand{\foo"#)); + } + + #[test] + fn test_math_operator_simple() { + assert_debug_snapshot!(setup(r#"\DeclareMathOperator{\foo}{foo}"#)); + } + + #[test] + fn test_math_operator_no_impl() { + assert_debug_snapshot!(setup(r#"\DeclareMathOperator{\foo}"#)); + } + + #[test] + fn test_glossary_entry_definition_simple() { + assert_debug_snapshot!(setup(r#"\newglossaryentry{foo}{bar = baz, qux,}"#)); + } + + #[test] + fn test_glossary_entry_reference_simple() { + assert_debug_snapshot!(setup(r#"\gls{foo}"#)); + } + + #[test] + fn test_glossary_entry_reference_options() { + assert_debug_snapshot!(setup(r#"\gls[foo = bar, qux]{baz}"#)); + } + + #[test] + fn test_acroynm_definition_simple() { + assert_debug_snapshot!(setup(r#"\newacronym{fpsLabel}{FPS}{Frame per Second}"#)); + } + + #[test] + fn test_acroynm_definition_options() { + assert_debug_snapshot!(setup( + r#"\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}"# + )); + } + + #[test] + fn test_acroynm_reference_simple() { + assert_debug_snapshot!(setup(r#"\acrshort{fpsLabel}"#)); + } + + #[test] + fn test_acroynm_reference_options() { + assert_debug_snapshot!(setup(r#"\acrshort[foo=bar,baz]{fpsLabel}"#)); + } + + #[test] + fn test_theorem_definition_only_name() { + assert_debug_snapshot!(setup(r#"\newtheorem{foo}"#)); + } + + #[test] + fn test_theorem_definition_name_with_description() { + assert_debug_snapshot!(setup(r#"\newtheorem{foo}{Foo}"#)); + } + + #[test] + fn test_theorem_definition_name_with_description_and_counter() { + assert_debug_snapshot!(setup(r#"\newtheorem{foo}[bar]{Foo}"#)); + } + + #[test] + fn test_theorem_definition_name_with_counter() { + assert_debug_snapshot!(setup(r#"\newtheorem{foo}[bar]"#)); + } + + #[test] + fn test_theorem_definition_full() { + assert_debug_snapshot!(setup(r#"\newtheorem{foo}[bar]{Foo}[baz]"#)); + } + + #[test] + fn test_color_reference_simple() { + assert_debug_snapshot!(setup(r#"\color{black}"#)); + } + + #[test] + fn test_color_definition_simple() { + assert_debug_snapshot!(setup(r#"\definecolor{foo}{rgb}{255,168,0}"#)); + } + + #[test] + fn test_color_set_definition_simple() { + assert_debug_snapshot!(setup(r#"\definecolorset[ty]{rgb,HTML}{foo}{bar}{baz}"#)); + } + + #[test] + fn test_color_set_definition_error1() { + assert_debug_snapshot!(setup(r#"\definecolorset[ty]{rgb,HTML}{foo}{bar}"#)); + } + + #[test] + fn test_color_set_definition_error2() { + assert_debug_snapshot!(setup(r#"\definecolorset{rgb,HTML}{foo}"#)); + } + + #[test] + fn test_color_set_definition_error3() { + assert_debug_snapshot!(setup(r#"\definecolorset{rgb,HTML}"#)); + } + + #[test] + fn test_color_set_definition_error4() { + assert_debug_snapshot!(setup(r#"\definecolorset"#)); + } + + #[test] + fn test_pgf_library_import_simple() { + assert_debug_snapshot!(setup(r#"\usepgflibrary{foo}"#)); + } + + #[test] + fn test_tikz_library_import_simple() { + assert_debug_snapshot!(setup(r#"\usetikzlibrary{foo}"#)); + } + + #[test] + fn test_environment_definition() { + assert_debug_snapshot!(setup(r#"\newenvironment{bar}[1]{\begin{foo}}{\end{foo}}"#)); + } + + #[test] + fn test_acronym_declaration() { + assert_debug_snapshot!(setup( + r#"\DeclareAcronym{eg}{short = e.g,long = for example,tag = abbrev}"# + )); } } diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_parameter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_parameter.snap new file mode 100644 index 0000000000..9cff735e99 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_parameter.snap @@ -0,0 +1,47 @@ +--- +source: src/syntax/latex/lexer.rs +expression: "verify(r#\"\\newcommand{\\id}[1]{#1}\"#)" + +--- +[ + ( + COMMAND_DEFINITION_NAME, + "\\newcommand", + ), + ( + L_CURLY, + "{", + ), + ( + GENERIC_COMMAND_NAME, + "\\id", + ), + ( + R_CURLY, + "}", + ), + ( + L_BRACK, + "[", + ), + ( + WORD, + "1", + ), + ( + R_BRACK, + "]", + ), + ( + L_CURLY, + "{", + ), + ( + PARAMETER, + "#1", + ), + ( + R_CURLY, + "}", + ), +] diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_star.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_star.snap new file mode 100644 index 0000000000..41310edaa1 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__command_with_star.snap @@ -0,0 +1,23 @@ +--- +source: src/syntax/latex/lexer.rs +expression: "verify(r#\"\\section*{Foo}\"#)" + +--- +[ + ( + SECTION_NAME, + "\\section*", + ), + ( + L_CURLY, + "{", + ), + ( + WORD, + "Foo", + ), + ( + R_CURLY, + "}", + ), +] diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__comment.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__comment.snap new file mode 100644 index 0000000000..2186c957c5 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__comment.snap @@ -0,0 +1,35 @@ +--- +source: src/syntax/latex/lexer.rs +expression: "verify(\"hello %world\\r\\ntest %test\")" + +--- +[ + ( + WORD, + "hello", + ), + ( + WHITESPACE, + " ", + ), + ( + COMMENT, + "%world", + ), + ( + WHITESPACE, + "\r\n", + ), + ( + WORD, + "test", + ), + ( + WHITESPACE, + " ", + ), + ( + COMMENT, + "%test", + ), +] diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__delimiters.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__delimiters.snap new file mode 100644 index 0000000000..36bec0a276 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__delimiters.snap @@ -0,0 +1,79 @@ +--- +source: src/syntax/latex/lexer.rs +expression: "verify(r#\"{foo} (bar) [baz, qux = foo-bar]\"#)" + +--- +[ + ( + L_CURLY, + "{", + ), + ( + WORD, + "foo", + ), + ( + R_CURLY, + "}", + ), + ( + WHITESPACE, + " ", + ), + ( + L_PAREN, + "(", + ), + ( + WORD, + "bar", + ), + ( + R_PAREN, + ")", + ), + ( + WHITESPACE, + " ", + ), + ( + L_BRACK, + "[", + ), + ( + WORD, + "baz", + ), + ( + COMMA, + ",", + ), + ( + WHITESPACE, + " ", + ), + ( + WORD, + "qux", + ), + ( + WHITESPACE, + " ", + ), + ( + EQUALITY_SIGN, + "=", + ), + ( + WHITESPACE, + " ", + ), + ( + WORD, + "foo-bar", + ), + ( + R_BRACK, + "]", + ), +] diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__empty.snap new file mode 100644 index 0000000000..61735b5087 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__empty.snap @@ -0,0 +1,6 @@ +--- +source: src/syntax/latex/lexer.rs +expression: "verify(r#\"\"#)" + +--- +[] diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__escape_sequence.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__escape_sequence.snap new file mode 100644 index 0000000000..4dbc1bb8f5 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__escape_sequence.snap @@ -0,0 +1,19 @@ +--- +source: src/syntax/latex/lexer.rs +expression: "verify(r#\"\\% hello\"#)" + +--- +[ + ( + GENERIC_COMMAND_NAME, + "\\%", + ), + ( + WHITESPACE, + " ", + ), + ( + WORD, + "hello", + ), +] diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__formula.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__formula.snap new file mode 100644 index 0000000000..3fb7079384 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__formula.snap @@ -0,0 +1,55 @@ +--- +source: src/syntax/latex/lexer.rs +expression: "verify(r#\"$ f(x) = y $$\"#)" + +--- +[ + ( + DOLLAR, + "$", + ), + ( + WHITESPACE, + " ", + ), + ( + WORD, + "f", + ), + ( + L_PAREN, + "(", + ), + ( + WORD, + "x", + ), + ( + R_PAREN, + ")", + ), + ( + WHITESPACE, + " ", + ), + ( + EQUALITY_SIGN, + "=", + ), + ( + WHITESPACE, + " ", + ), + ( + WORD, + "y", + ), + ( + WHITESPACE, + " ", + ), + ( + DOLLAR, + "$$", + ), +] diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__invalid_parameter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__invalid_parameter.snap new file mode 100644 index 0000000000..adadfcf0ef --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__lexer__tests__invalid_parameter.snap @@ -0,0 +1,11 @@ +--- +source: src/syntax/latex/lexer.rs +expression: "verify(r#\"#\"#)" + +--- +[ + ( + PARAMETER, + "#", + ), +] diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acronym_declaration.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acronym_declaration.snap new file mode 100644 index 0000000000..dfd05750d3 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acronym_declaration.snap @@ -0,0 +1,49 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\DeclareAcronym{eg}{short = e.g,long = for example,tag = abbrev}\"#)" + +--- +ROOT@0..64 + PREAMBLE@0..64 + ACRONYM_DECLARATION@0..64 + ACRONYM_DECLARATION_NAME@0..15 "\\DeclareAcronym" + CURLY_GROUP_WORD@15..19 + L_CURLY@15..16 "{" + WORD@16..18 "eg" + R_CURLY@18..19 "}" + CURLY_GROUP_KEY_VALUE@19..64 + L_CURLY@19..20 "{" + KEY_VALUE_BODY@20..63 + KEY_VALUE_PAIR@20..31 + KEY@20..26 + WORD@20..25 "short" + WHITESPACE@25..26 " " + EQUALITY_SIGN@26..27 "=" + WHITESPACE@27..28 " " + VALUE@28..31 + TEXT@28..31 + WORD@28..31 "e.g" + COMMA@31..32 "," + KEY_VALUE_PAIR@32..50 + KEY@32..37 + WORD@32..36 "long" + WHITESPACE@36..37 " " + EQUALITY_SIGN@37..38 "=" + WHITESPACE@38..39 " " + VALUE@39..50 + TEXT@39..50 + WORD@39..42 "for" + WHITESPACE@42..43 " " + WORD@43..50 "example" + COMMA@50..51 "," + KEY_VALUE_PAIR@51..63 + KEY@51..55 + WORD@51..54 "tag" + WHITESPACE@54..55 " " + EQUALITY_SIGN@55..56 "=" + WHITESPACE@56..57 " " + VALUE@57..63 + TEXT@57..63 + WORD@57..63 "abbrev" + R_CURLY@63..64 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_options.snap new file mode 100644 index 0000000000..1838e80f9a --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_options.snap @@ -0,0 +1,46 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second}\"#)" + +--- +ROOT@0..76 + PREAMBLE@0..76 + ACRONYM_DEFINITION@0..76 + ACRONYM_DEFINITION_NAME@0..11 "\\newacronym" + BRACK_GROUP_KEY_VALUE@11..43 + L_BRACK@11..12 "[" + KEY_VALUE_BODY@12..42 + KEY_VALUE_PAIR@12..42 + KEY@12..22 + WORD@12..22 "longplural" + EQUALITY_SIGN@22..23 "=" + VALUE@23..42 + CURLY_GROUP@23..42 + L_CURLY@23..24 "{" + TEXT@24..41 + WORD@24..30 "Frames" + WHITESPACE@30..31 " " + WORD@31..34 "per" + WHITESPACE@34..35 " " + WORD@35..41 "Second" + R_CURLY@41..42 "}" + R_BRACK@42..43 "]" + CURLY_GROUP_WORD@43..53 + L_CURLY@43..44 "{" + WORD@44..52 "fpsLabel" + R_CURLY@52..53 "}" + CURLY_GROUP@53..58 + L_CURLY@53..54 "{" + TEXT@54..57 + WORD@54..57 "FPS" + R_CURLY@57..58 "}" + CURLY_GROUP@58..76 + L_CURLY@58..59 "{" + TEXT@59..75 + WORD@59..64 "Frame" + WHITESPACE@64..65 " " + WORD@65..68 "per" + WHITESPACE@68..69 " " + WORD@69..75 "Second" + R_CURLY@75..76 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_simple.snap new file mode 100644 index 0000000000..1d32bed249 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_definition_simple.snap @@ -0,0 +1,28 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newacronym{fpsLabel}{FPS}{Frame per Second}\"#)" + +--- +ROOT@0..44 + PREAMBLE@0..44 + ACRONYM_DEFINITION@0..44 + ACRONYM_DEFINITION_NAME@0..11 "\\newacronym" + CURLY_GROUP_WORD@11..21 + L_CURLY@11..12 "{" + WORD@12..20 "fpsLabel" + R_CURLY@20..21 "}" + CURLY_GROUP@21..26 + L_CURLY@21..22 "{" + TEXT@22..25 + WORD@22..25 "FPS" + R_CURLY@25..26 "}" + CURLY_GROUP@26..44 + L_CURLY@26..27 "{" + TEXT@27..43 + WORD@27..32 "Frame" + WHITESPACE@32..33 " " + WORD@33..36 "per" + WHITESPACE@36..37 " " + WORD@37..43 "Second" + R_CURLY@43..44 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_options.snap new file mode 100644 index 0000000000..ce5b85c1cf --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_options.snap @@ -0,0 +1,29 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\acrshort[foo=bar,baz]{fpsLabel}\"#)" + +--- +ROOT@0..32 + PREAMBLE@0..32 + ACRONYM_REFERENCE@0..32 + ACRONYM_REFERENCE_NAME@0..9 "\\acrshort" + BRACK_GROUP_KEY_VALUE@9..22 + L_BRACK@9..10 "[" + KEY_VALUE_BODY@10..21 + KEY_VALUE_PAIR@10..17 + KEY@10..13 + WORD@10..13 "foo" + EQUALITY_SIGN@13..14 "=" + VALUE@14..17 + TEXT@14..17 + WORD@14..17 "bar" + COMMA@17..18 "," + KEY_VALUE_PAIR@18..21 + KEY@18..21 + WORD@18..21 "baz" + R_BRACK@21..22 "]" + CURLY_GROUP_WORD@22..32 + L_CURLY@22..23 "{" + WORD@23..31 "fpsLabel" + R_CURLY@31..32 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_simple.snap new file mode 100644 index 0000000000..5df8059f1b --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__acroynm_reference_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\acrshort{fpsLabel}\"#)" + +--- +ROOT@0..19 + PREAMBLE@0..19 + ACRONYM_REFERENCE@0..19 + ACRONYM_REFERENCE_NAME@0..9 "\\acrshort" + CURLY_GROUP_WORD@9..19 + L_CURLY@9..10 "{" + WORD@10..18 "fpsLabel" + R_CURLY@18..19 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_options.snap new file mode 100644 index 0000000000..c910699363 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_options.snap @@ -0,0 +1,30 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\addbibresource[foo=bar, baz]{foo/bar.bib}\"#)" + +--- +ROOT@0..42 + PREAMBLE@0..42 + BIBLATEX_INCLUDE@0..42 + BIBLATEX_INCLUDE_NAME@0..15 "\\addbibresource" + BRACK_GROUP_KEY_VALUE@15..29 + L_BRACK@15..16 "[" + KEY_VALUE_BODY@16..28 + KEY_VALUE_PAIR@16..23 + KEY@16..19 + WORD@16..19 "foo" + EQUALITY_SIGN@19..20 "=" + VALUE@20..23 + TEXT@20..23 + WORD@20..23 "bar" + COMMA@23..24 "," + WHITESPACE@24..25 " " + KEY_VALUE_PAIR@25..28 + KEY@25..28 + WORD@25..28 "baz" + R_BRACK@28..29 "]" + CURLY_GROUP_WORD_LIST@29..42 + L_CURLY@29..30 "{" + WORD@30..41 "foo/bar.bib" + R_CURLY@41..42 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_simple.snap new file mode 100644 index 0000000000..3549b985c4 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__biblatex_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\addbibresource{foo/bar.bib}\"#)" + +--- +ROOT@0..28 + PREAMBLE@0..28 + BIBLATEX_INCLUDE@0..28 + BIBLATEX_INCLUDE_NAME@0..15 "\\addbibresource" + CURLY_GROUP_WORD_LIST@15..28 + L_CURLY@15..16 "{" + WORD@16..27 "foo/bar.bib" + R_CURLY@27..28 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__bibtex_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__bibtex_include_simple.snap new file mode 100644 index 0000000000..f0d30ebfa3 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__bibtex_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\bibliography{foo/bar}\"#)" + +--- +ROOT@0..22 + PREAMBLE@0..22 + BIBTEX_INCLUDE@0..22 + BIBTEX_INCLUDE_NAME@0..13 "\\bibliography" + CURLY_GROUP_WORD_LIST@13..22 + L_CURLY@13..14 "{" + WORD@14..21 "foo/bar" + R_CURLY@21..22 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_missing_end.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_missing_end.snap new file mode 100644 index 0000000000..c5fc46ec4d --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_missing_end.snap @@ -0,0 +1,15 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"{hello world\"#)" + +--- +ROOT@0..12 + PREAMBLE@0..12 + CURLY_GROUP@0..12 + L_CURLY@0..1 "{" + TEXT@1..12 + WORD@1..6 "hello" + WHITESPACE@6..7 " " + WORD@7..12 "world" + MISSING@12..12 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_simple.snap new file mode 100644 index 0000000000..10ebc4418e --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__brace_group_simple.snap @@ -0,0 +1,15 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"{hello world}\"#)" + +--- +ROOT@0..13 + PREAMBLE@0..13 + CURLY_GROUP@0..13 + L_CURLY@0..1 "{" + TEXT@1..12 + WORD@1..6 "hello" + WHITESPACE@6..7 " " + WORD@7..12 "world" + R_CURLY@12..13 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption.snap new file mode 100644 index 0000000000..f598a8f7e9 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption.snap @@ -0,0 +1,26 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\caption[qux]{Foo \\Bar Baz}\"#)" + +--- +ROOT@0..27 + PREAMBLE@0..27 + CAPTION@0..27 + CAPTION_NAME@0..8 "\\caption" + BRACK_GROUP@8..13 + L_BRACK@8..9 "[" + TEXT@9..12 + WORD@9..12 "qux" + R_BRACK@12..13 "]" + CURLY_GROUP@13..27 + L_CURLY@13..14 "{" + TEXT@14..18 + WORD@14..17 "Foo" + WHITESPACE@17..18 " " + GENERIC_COMMAND@18..23 + GENERIC_COMMAND_NAME@18..22 "\\Bar" + WHITESPACE@22..23 " " + TEXT@23..26 + WORD@23..26 "Baz" + R_CURLY@26..27 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_error.snap new file mode 100644 index 0000000000..13697e882b --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_error.snap @@ -0,0 +1,26 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\caption[qux]{Foo \\Bar Baz\"#)" + +--- +ROOT@0..26 + PREAMBLE@0..26 + CAPTION@0..26 + CAPTION_NAME@0..8 "\\caption" + BRACK_GROUP@8..13 + L_BRACK@8..9 "[" + TEXT@9..12 + WORD@9..12 "qux" + R_BRACK@12..13 "]" + CURLY_GROUP@13..26 + L_CURLY@13..14 "{" + TEXT@14..18 + WORD@14..17 "Foo" + WHITESPACE@17..18 " " + GENERIC_COMMAND@18..23 + GENERIC_COMMAND_NAME@18..22 "\\Bar" + WHITESPACE@22..23 " " + TEXT@23..26 + WORD@23..26 "Baz" + MISSING@26..26 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_figure.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_figure.snap new file mode 100644 index 0000000000..4b963caabd --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_figure.snap @@ -0,0 +1,28 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\begin{figure}\\caption{Foo}\\end{figure}\"#)" + +--- +ROOT@0..39 + PREAMBLE@0..39 + ENVIRONMENT@0..39 + BEGIN@0..14 + BEGIN_ENVIRONMENT_NAME@0..6 "\\begin" + CURLY_GROUP_WORD@6..14 + L_CURLY@6..7 "{" + WORD@7..13 "figure" + R_CURLY@13..14 "}" + CAPTION@14..27 + CAPTION_NAME@14..22 "\\caption" + CURLY_GROUP@22..27 + L_CURLY@22..23 "{" + TEXT@23..26 + WORD@23..26 "Foo" + R_CURLY@26..27 "}" + END@27..39 + END_ENVIRONMENT_NAME@27..31 "\\end" + CURLY_GROUP_WORD@31..39 + L_CURLY@31..32 "{" + WORD@32..38 "figure" + R_CURLY@38..39 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal.snap new file mode 100644 index 0000000000..bf9e795e18 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal.snap @@ -0,0 +1,21 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\caption{Foo \\Bar Baz}\"#)" + +--- +ROOT@0..22 + PREAMBLE@0..22 + CAPTION@0..22 + CAPTION_NAME@0..8 "\\caption" + CURLY_GROUP@8..22 + L_CURLY@8..9 "{" + TEXT@9..13 + WORD@9..12 "Foo" + WHITESPACE@12..13 " " + GENERIC_COMMAND@13..18 + GENERIC_COMMAND_NAME@13..17 "\\Bar" + WHITESPACE@17..18 " " + TEXT@18..21 + WORD@18..21 "Baz" + R_CURLY@21..22 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal_error.snap new file mode 100644 index 0000000000..6c932ce883 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__caption_minimal_error.snap @@ -0,0 +1,21 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\caption{Foo \\Bar Baz\"#)" + +--- +ROOT@0..21 + PREAMBLE@0..21 + CAPTION@0..21 + CAPTION_NAME@0..8 "\\caption" + CURLY_GROUP@8..21 + L_CURLY@8..9 "{" + TEXT@9..13 + WORD@9..12 "Foo" + WHITESPACE@12..13 " " + GENERIC_COMMAND@13..18 + GENERIC_COMMAND_NAME@13..17 "\\Bar" + WHITESPACE@17..18 " " + TEXT@18..21 + WORD@18..21 "Baz" + MISSING@21..21 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_empty.snap new file mode 100644 index 0000000000..faad4c0039 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_empty.snap @@ -0,0 +1,13 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\cite{}\"#)" + +--- +ROOT@0..7 + PREAMBLE@0..7 + CITATION@0..7 + CITATION_NAME@0..5 "\\cite" + CURLY_GROUP_WORD_LIST@5..7 + L_CURLY@5..6 "{" + R_CURLY@6..7 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_missing_brace.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_missing_brace.snap new file mode 100644 index 0000000000..363f8b0142 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_missing_brace.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\cite{foo\"#)" + +--- +ROOT@0..9 + PREAMBLE@0..9 + CITATION@0..9 + CITATION_NAME@0..5 "\\cite" + CURLY_GROUP_WORD_LIST@5..9 + L_CURLY@5..6 "{" + WORD@6..9 "foo" + MISSING@9..9 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_multiple_keys.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_multiple_keys.snap new file mode 100644 index 0000000000..dafc826a97 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_multiple_keys.snap @@ -0,0 +1,17 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\cite{foo, bar}\"#)" + +--- +ROOT@0..15 + PREAMBLE@0..15 + CITATION@0..15 + CITATION_NAME@0..5 "\\cite" + CURLY_GROUP_WORD_LIST@5..15 + L_CURLY@5..6 "{" + WORD@6..9 "foo" + COMMA@9..10 "," + WHITESPACE@10..11 " " + WORD@11..14 "bar" + R_CURLY@14..15 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote.snap new file mode 100644 index 0000000000..93d83bb448 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote.snap @@ -0,0 +1,19 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\cite[foo]{bar}\"#)" + +--- +ROOT@0..15 + PREAMBLE@0..15 + CITATION@0..15 + CITATION_NAME@0..5 "\\cite" + BRACK_GROUP@5..10 + L_BRACK@5..6 "[" + TEXT@6..9 + WORD@6..9 "foo" + R_BRACK@9..10 "]" + CURLY_GROUP_WORD_LIST@10..15 + L_CURLY@10..11 "{" + WORD@11..14 "bar" + R_CURLY@14..15 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote_postnote.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote_postnote.snap new file mode 100644 index 0000000000..3acaafea3b --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_prenote_postnote.snap @@ -0,0 +1,24 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\cite[foo][bar]{baz}\"#)" + +--- +ROOT@0..20 + PREAMBLE@0..20 + CITATION@0..20 + CITATION_NAME@0..5 "\\cite" + BRACK_GROUP@5..10 + L_BRACK@5..6 "[" + TEXT@6..9 + WORD@6..9 "foo" + R_BRACK@9..10 "]" + BRACK_GROUP@10..15 + L_BRACK@10..11 "[" + TEXT@11..14 + WORD@11..14 "bar" + R_BRACK@14..15 "]" + CURLY_GROUP_WORD_LIST@15..20 + L_CURLY@15..16 "{" + WORD@16..19 "baz" + R_CURLY@19..20 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_redundant_comma.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_redundant_comma.snap new file mode 100644 index 0000000000..12f34f16ae --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_redundant_comma.snap @@ -0,0 +1,16 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\cite{,foo,}\"#)" + +--- +ROOT@0..12 + PREAMBLE@0..12 + CITATION@0..12 + CITATION_NAME@0..5 "\\cite" + CURLY_GROUP_WORD_LIST@5..12 + L_CURLY@5..6 "{" + COMMA@6..7 "," + WORD@7..10 "foo" + COMMA@10..11 "," + R_CURLY@11..12 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_simple.snap new file mode 100644 index 0000000000..a22129f85f --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\cite{foo}\"#)" + +--- +ROOT@0..10 + PREAMBLE@0..10 + CITATION@0..10 + CITATION_NAME@0..5 "\\cite" + CURLY_GROUP_WORD_LIST@5..10 + L_CURLY@5..6 "{" + WORD@6..9 "foo" + R_CURLY@9..10 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_star.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_star.snap new file mode 100644 index 0000000000..4388596dfd --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__citation_star.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\nocite{*}\"#)" + +--- +ROOT@0..10 + PREAMBLE@0..10 + CITATION@0..10 + CITATION_NAME@0..7 "\\nocite" + CURLY_GROUP_WORD_LIST@7..10 + L_CURLY@7..8 "{" + WORD@8..9 "*" + R_CURLY@9..10 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_empty.snap new file mode 100644 index 0000000000..7d84653225 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_empty.snap @@ -0,0 +1,13 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\documentclass{}\"#)" + +--- +ROOT@0..16 + PREAMBLE@0..16 + CLASS_INCLUDE@0..16 + CLASS_INCLUDE_NAME@0..14 "\\documentclass" + CURLY_GROUP_WORD_LIST@14..16 + L_CURLY@14..15 "{" + R_CURLY@15..16 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_options.snap new file mode 100644 index 0000000000..0d0eb1b250 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_options.snap @@ -0,0 +1,37 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\documentclass[foo = bar, baz, qux]{article}\"#)" + +--- +ROOT@0..44 + PREAMBLE@0..44 + CLASS_INCLUDE@0..44 + CLASS_INCLUDE_NAME@0..14 "\\documentclass" + BRACK_GROUP_KEY_VALUE@14..35 + L_BRACK@14..15 "[" + KEY_VALUE_BODY@15..34 + KEY_VALUE_PAIR@15..24 + KEY@15..19 + WORD@15..18 "foo" + WHITESPACE@18..19 " " + EQUALITY_SIGN@19..20 "=" + WHITESPACE@20..21 " " + VALUE@21..24 + TEXT@21..24 + WORD@21..24 "bar" + COMMA@24..25 "," + WHITESPACE@25..26 " " + KEY_VALUE_PAIR@26..29 + KEY@26..29 + WORD@26..29 "baz" + COMMA@29..30 "," + WHITESPACE@30..31 " " + KEY_VALUE_PAIR@31..34 + KEY@31..34 + WORD@31..34 "qux" + R_BRACK@34..35 "]" + CURLY_GROUP_WORD_LIST@35..44 + L_CURLY@35..36 "{" + WORD@36..43 "article" + R_CURLY@43..44 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_simple.snap new file mode 100644 index 0000000000..85e21b47a8 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__class_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\documentclass{article}\"#)" + +--- +ROOT@0..23 + PREAMBLE@0..23 + CLASS_INCLUDE@0..23 + CLASS_INCLUDE_NAME@0..14 "\\documentclass" + CURLY_GROUP_WORD_LIST@14..23 + L_CURLY@14..15 "{" + WORD@15..22 "article" + R_CURLY@22..23 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_definition_simple.snap new file mode 100644 index 0000000000..e4adfd534c --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_definition_simple.snap @@ -0,0 +1,27 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\definecolor{foo}{rgb}{255,168,0}\"#)" + +--- +ROOT@0..33 + PREAMBLE@0..33 + COLOR_DEFINITION@0..33 + COLOR_DEFINITION_NAME@0..12 "\\definecolor" + CURLY_GROUP_WORD@12..17 + L_CURLY@12..13 "{" + WORD@13..16 "foo" + R_CURLY@16..17 "}" + CURLY_GROUP_WORD@17..22 + L_CURLY@17..18 "{" + WORD@18..21 "rgb" + R_CURLY@21..22 "}" + CURLY_GROUP@22..33 + L_CURLY@22..23 "{" + TEXT@23..32 + WORD@23..26 "255" + COMMA@26..27 "," + WORD@27..30 "168" + COMMA@30..31 "," + WORD@31..32 "0" + R_CURLY@32..33 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_reference_simple.snap new file mode 100644 index 0000000000..41a3700122 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_reference_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\color{black}\"#)" + +--- +ROOT@0..13 + PREAMBLE@0..13 + COLOR_REFERENCE@0..13 + COLOR_REFERENCE_NAME@0..6 "\\color" + CURLY_GROUP_WORD@6..13 + L_CURLY@6..7 "{" + WORD@7..12 "black" + R_CURLY@12..13 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error1.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error1.snap new file mode 100644 index 0000000000..42095ff974 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error1.snap @@ -0,0 +1,29 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\definecolorset[ty]{rgb,HTML}{foo}{bar}\"#)" + +--- +ROOT@0..39 + PREAMBLE@0..39 + COLOR_SET_DEFINITION@0..39 + COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset" + BRACK_GROUP_WORD@15..19 + L_BRACK@15..16 "[" + WORD@16..18 "ty" + R_BRACK@18..19 "]" + CURLY_GROUP_WORD_LIST@19..29 + L_CURLY@19..20 "{" + WORD@20..23 "rgb" + COMMA@23..24 "," + WORD@24..28 "HTML" + R_CURLY@28..29 "}" + CURLY_GROUP_WORD@29..34 + L_CURLY@29..30 "{" + WORD@30..33 "foo" + R_CURLY@33..34 "}" + CURLY_GROUP_WORD@34..39 + L_CURLY@34..35 "{" + WORD@35..38 "bar" + R_CURLY@38..39 "}" + MISSING@39..39 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error2.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error2.snap new file mode 100644 index 0000000000..718d49a3e3 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error2.snap @@ -0,0 +1,22 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\definecolorset{rgb,HTML}{foo}\"#)" + +--- +ROOT@0..30 + PREAMBLE@0..30 + COLOR_SET_DEFINITION@0..30 + COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset" + CURLY_GROUP_WORD_LIST@15..25 + L_CURLY@15..16 "{" + WORD@16..19 "rgb" + COMMA@19..20 "," + WORD@20..24 "HTML" + R_CURLY@24..25 "}" + CURLY_GROUP_WORD@25..30 + L_CURLY@25..26 "{" + WORD@26..29 "foo" + R_CURLY@29..30 "}" + MISSING@30..30 "" + MISSING@30..30 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error3.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error3.snap new file mode 100644 index 0000000000..f92d813123 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error3.snap @@ -0,0 +1,19 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\definecolorset{rgb,HTML}\"#)" + +--- +ROOT@0..25 + PREAMBLE@0..25 + COLOR_SET_DEFINITION@0..25 + COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset" + CURLY_GROUP_WORD_LIST@15..25 + L_CURLY@15..16 "{" + WORD@16..19 "rgb" + COMMA@19..20 "," + WORD@20..24 "HTML" + R_CURLY@24..25 "}" + MISSING@25..25 "" + MISSING@25..25 "" + MISSING@25..25 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error4.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error4.snap new file mode 100644 index 0000000000..3cafa81bae --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_error4.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\definecolorset\"#)" + +--- +ROOT@0..15 + PREAMBLE@0..15 + COLOR_SET_DEFINITION@0..15 + COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset" + MISSING@15..15 "" + MISSING@15..15 "" + MISSING@15..15 "" + MISSING@15..15 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_simple.snap new file mode 100644 index 0000000000..f9a736c65f --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__color_set_definition_simple.snap @@ -0,0 +1,32 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\definecolorset[ty]{rgb,HTML}{foo}{bar}{baz}\"#)" + +--- +ROOT@0..44 + PREAMBLE@0..44 + COLOR_SET_DEFINITION@0..44 + COLOR_SET_DEFINITION_NAME@0..15 "\\definecolorset" + BRACK_GROUP_WORD@15..19 + L_BRACK@15..16 "[" + WORD@16..18 "ty" + R_BRACK@18..19 "]" + CURLY_GROUP_WORD_LIST@19..29 + L_CURLY@19..20 "{" + WORD@20..23 "rgb" + COMMA@23..24 "," + WORD@24..28 "HTML" + R_CURLY@28..29 "}" + CURLY_GROUP_WORD@29..34 + L_CURLY@29..30 "{" + WORD@30..33 "foo" + R_CURLY@33..34 "}" + CURLY_GROUP_WORD@34..39 + L_CURLY@34..35 "{" + WORD@35..38 "bar" + R_CURLY@38..39 "}" + CURLY_GROUP_WORD@39..44 + L_CURLY@39..40 "{" + WORD@40..43 "baz" + R_CURLY@43..44 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_argc.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_argc.snap new file mode 100644 index 0000000000..2ff3896080 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_argc.snap @@ -0,0 +1,19 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newcommand{\\foo}{foo}\"#)" + +--- +ROOT@0..22 + PREAMBLE@0..22 + COMMAND_DEFINITION@0..22 + COMMAND_DEFINITION_NAME@0..11 "\\newcommand" + CURLY_GROUP_COMMAND@11..17 + L_CURLY@11..12 "{" + GENERIC_COMMAND_NAME@12..16 "\\foo" + R_CURLY@16..17 "}" + CURLY_GROUP@17..22 + L_CURLY@17..18 "{" + TEXT@18..21 + WORD@18..21 "foo" + R_CURLY@21..22 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl.snap new file mode 100644 index 0000000000..4602a7a688 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl.snap @@ -0,0 +1,15 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newcommand{\\foo}\"#)" + +--- +ROOT@0..17 + PREAMBLE@0..17 + COMMAND_DEFINITION@0..17 + COMMAND_DEFINITION_NAME@0..11 "\\newcommand" + CURLY_GROUP_COMMAND@11..17 + L_CURLY@11..12 "{" + GENERIC_COMMAND_NAME@12..16 "\\foo" + R_CURLY@16..17 "}" + MISSING@17..17 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl_error.snap new file mode 100644 index 0000000000..4a20be7d7c --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_no_impl_error.snap @@ -0,0 +1,15 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newcommand{\\foo\"#)" + +--- +ROOT@0..16 + PREAMBLE@0..16 + COMMAND_DEFINITION@0..16 + COMMAND_DEFINITION_NAME@0..11 "\\newcommand" + CURLY_GROUP_COMMAND@11..16 + L_CURLY@11..12 "{" + GENERIC_COMMAND_NAME@12..16 "\\foo" + MISSING@16..16 "" + MISSING@16..16 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_simple.snap new file mode 100644 index 0000000000..e5f2377fcf --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__command_definition_simple.snap @@ -0,0 +1,24 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newcommand[1]{\\id}{#1}\"#)" + +--- +ROOT@0..23 + PREAMBLE@0..23 + COMMAND_DEFINITION@0..19 + COMMAND_DEFINITION_NAME@0..11 "\\newcommand" + MISSING@11..11 "" + BRACK_GROUP_WORD@11..14 + L_BRACK@11..12 "[" + WORD@12..13 "1" + R_BRACK@13..14 "]" + CURLY_GROUP@14..19 + L_CURLY@14..15 "{" + GENERIC_COMMAND@15..18 + GENERIC_COMMAND_NAME@15..18 "\\id" + R_CURLY@18..19 "}" + CURLY_GROUP@19..23 + L_CURLY@19..20 "{" + PARAMETER@20..22 "#1" + R_CURLY@22..23 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__empty.snap new file mode 100644 index 0000000000..f01f7f9092 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__empty.snap @@ -0,0 +1,8 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\"#)" + +--- +ROOT@0..0 + PREAMBLE@0..0 + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_definition.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_definition.snap new file mode 100644 index 0000000000..df3c0d56fb --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_definition.snap @@ -0,0 +1,38 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newenvironment{bar}[1]{\\begin{foo}}{\\end{foo}}\"#)" + +--- +ROOT@0..47 + PREAMBLE@0..47 + ENVIRONMENT_DEFINITION@0..47 + ENVIRONMENT_DEFINIITION_NAME@0..15 "\\newenvironment" + CURLY_GROUP_WORD@15..20 + L_CURLY@15..16 "{" + WORD@16..19 "bar" + R_CURLY@19..20 "}" + BRACK_GROUP_WORD@20..23 + L_BRACK@20..21 "[" + WORD@21..22 "1" + R_BRACK@22..23 "]" + CURLY_GROUP@23..36 + L_CURLY@23..24 "{" + GENERIC_COMMAND@24..35 + BEGIN_ENVIRONMENT_NAME@24..30 "\\begin" + CURLY_GROUP@30..35 + L_CURLY@30..31 "{" + TEXT@31..34 + WORD@31..34 "foo" + R_CURLY@34..35 "}" + R_CURLY@35..36 "}" + CURLY_GROUP@36..47 + L_CURLY@36..37 "{" + GENERIC_COMMAND@37..46 + END_ENVIRONMENT_NAME@37..41 "\\end" + CURLY_GROUP@41..46 + L_CURLY@41..42 "{" + TEXT@42..45 + WORD@42..45 "foo" + R_CURLY@45..46 "}" + R_CURLY@46..47 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested.snap new file mode 100644 index 0000000000..a5c566f773 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested.snap @@ -0,0 +1,37 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\begin{foo} \\begin{qux} \\end{baz} \\end{bar}\"#)" + +--- +ROOT@0..43 + PREAMBLE@0..43 + ENVIRONMENT@0..43 + BEGIN@0..12 + BEGIN_ENVIRONMENT_NAME@0..6 "\\begin" + CURLY_GROUP_WORD@6..12 + L_CURLY@6..7 "{" + WORD@7..10 "foo" + R_CURLY@10..11 "}" + WHITESPACE@11..12 " " + ENVIRONMENT@12..34 + BEGIN@12..24 + BEGIN_ENVIRONMENT_NAME@12..18 "\\begin" + CURLY_GROUP_WORD@18..24 + L_CURLY@18..19 "{" + WORD@19..22 "qux" + R_CURLY@22..23 "}" + WHITESPACE@23..24 " " + END@24..34 + END_ENVIRONMENT_NAME@24..28 "\\end" + CURLY_GROUP_WORD@28..34 + L_CURLY@28..29 "{" + WORD@29..32 "baz" + R_CURLY@32..33 "}" + WHITESPACE@33..34 " " + END@34..43 + END_ENVIRONMENT_NAME@34..38 "\\end" + CURLY_GROUP_WORD@38..43 + L_CURLY@38..39 "{" + WORD@39..42 "bar" + R_CURLY@42..43 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested_missing_braces.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested_missing_braces.snap new file mode 100644 index 0000000000..9ff76605d6 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_nested_missing_braces.snap @@ -0,0 +1,42 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\begin{foo \\begin{qux Hello World \\end{baz} \\end{bar\"#)" + +--- +ROOT@0..52 + PREAMBLE@0..52 + ENVIRONMENT@0..52 + BEGIN@0..11 + BEGIN_ENVIRONMENT_NAME@0..6 "\\begin" + CURLY_GROUP_WORD@6..11 + L_CURLY@6..7 "{" + WORD@7..10 "foo" + WHITESPACE@10..11 " " + MISSING@11..11 "" + ENVIRONMENT@11..44 + BEGIN@11..22 + BEGIN_ENVIRONMENT_NAME@11..17 "\\begin" + CURLY_GROUP_WORD@17..22 + L_CURLY@17..18 "{" + WORD@18..21 "qux" + WHITESPACE@21..22 " " + MISSING@22..22 "" + TEXT@22..34 + WORD@22..27 "Hello" + WHITESPACE@27..28 " " + WORD@28..33 "World" + WHITESPACE@33..34 " " + END@34..44 + END_ENVIRONMENT_NAME@34..38 "\\end" + CURLY_GROUP_WORD@38..44 + L_CURLY@38..39 "{" + WORD@39..42 "baz" + R_CURLY@42..43 "}" + WHITESPACE@43..44 " " + END@44..52 + END_ENVIRONMENT_NAME@44..48 "\\end" + CURLY_GROUP_WORD@48..52 + L_CURLY@48..49 "{" + WORD@49..52 "bar" + MISSING@52..52 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_simple.snap new file mode 100644 index 0000000000..d58f5b26c4 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__environment_simple.snap @@ -0,0 +1,27 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\begin{foo} Hello World \\end{bar}\"#)" + +--- +ROOT@0..33 + PREAMBLE@0..33 + ENVIRONMENT@0..33 + BEGIN@0..12 + BEGIN_ENVIRONMENT_NAME@0..6 "\\begin" + CURLY_GROUP_WORD@6..12 + L_CURLY@6..7 "{" + WORD@7..10 "foo" + R_CURLY@10..11 "}" + WHITESPACE@11..12 " " + TEXT@12..24 + WORD@12..17 "Hello" + WHITESPACE@17..18 " " + WORD@18..23 "World" + WHITESPACE@23..24 " " + END@24..33 + END_ENVIRONMENT_NAME@24..28 "\\end" + CURLY_GROUP_WORD@28..33 + L_CURLY@28..29 "{" + WORD@29..32 "bar" + R_CURLY@32..33 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation.snap new file mode 100644 index 0000000000..e146fcaee6 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation.snap @@ -0,0 +1,17 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\[ foo bar \\]\"#)" + +--- +ROOT@0..13 + PREAMBLE@0..13 + EQUATION@0..13 + BEGIN_EQUATION_NAME@0..2 "\\[" + WHITESPACE@2..3 " " + TEXT@3..11 + WORD@3..6 "foo" + WHITESPACE@6..7 " " + WORD@7..10 "bar" + WHITESPACE@10..11 " " + END_EQUATION_NAME@11..13 "\\]" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_label_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_label_reference_simple.snap new file mode 100644 index 0000000000..7a851233ef --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_label_reference_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\eqref{foo}\"#)" + +--- +ROOT@0..11 + PREAMBLE@0..11 + LABEL_REFERENCE@0..11 + LABEL_REFERENCE_NAME@0..6 "\\eqref" + CURLY_GROUP_WORD_LIST@6..11 + L_CURLY@6..7 "{" + WORD@7..10 "foo" + R_CURLY@10..11 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_begin.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_begin.snap new file mode 100644 index 0000000000..fe065667c3 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_begin.snap @@ -0,0 +1,30 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\begin{a} foo bar \\] \\end{b}\"#)" + +--- +ROOT@0..28 + PREAMBLE@0..28 + ENVIRONMENT@0..28 + BEGIN@0..10 + BEGIN_ENVIRONMENT_NAME@0..6 "\\begin" + CURLY_GROUP_WORD@6..10 + L_CURLY@6..7 "{" + WORD@7..8 "a" + R_CURLY@8..9 "}" + WHITESPACE@9..10 " " + TEXT@10..18 + WORD@10..13 "foo" + WHITESPACE@13..14 " " + WORD@14..17 "bar" + WHITESPACE@17..18 " " + GENERIC_COMMAND@18..21 + END_EQUATION_NAME@18..20 "\\]" + WHITESPACE@20..21 " " + END@21..28 + END_ENVIRONMENT_NAME@21..25 "\\end" + CURLY_GROUP_WORD@25..28 + L_CURLY@25..26 "{" + WORD@26..27 "b" + R_CURLY@27..28 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_end.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_end.snap new file mode 100644 index 0000000000..f7b6205ffb --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__equation_missing_end.snap @@ -0,0 +1,31 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\begin{a} \\[ foo bar \\end{b}\"#)" + +--- +ROOT@0..28 + PREAMBLE@0..28 + ENVIRONMENT@0..28 + BEGIN@0..10 + BEGIN_ENVIRONMENT_NAME@0..6 "\\begin" + CURLY_GROUP_WORD@6..10 + L_CURLY@6..7 "{" + WORD@7..8 "a" + R_CURLY@8..9 "}" + WHITESPACE@9..10 " " + EQUATION@10..21 + BEGIN_EQUATION_NAME@10..12 "\\[" + WHITESPACE@12..13 " " + TEXT@13..21 + WORD@13..16 "foo" + WHITESPACE@16..17 " " + WORD@17..20 "bar" + WHITESPACE@20..21 " " + MISSING@21..21 "" + END@21..28 + END_ENVIRONMENT_NAME@21..25 "\\end" + CURLY_GROUP_WORD@25..28 + L_CURLY@25..26 "{" + WORD@26..27 "b" + R_CURLY@27..28 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__escaped_brackets.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__escaped_brackets.snap new file mode 100644 index 0000000000..c595828583 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__escaped_brackets.snap @@ -0,0 +1,19 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"{[}{]}\"#)" + +--- +ROOT@0..6 + PREAMBLE@0..6 + CURLY_GROUP@0..3 + L_CURLY@0..1 "{" + MIXED_GROUP@1..2 + L_BRACK@1..2 "[" + MISSING@2..2 "" + R_CURLY@2..3 "}" + CURLY_GROUP@3..6 + L_CURLY@3..4 "{" + ERROR@4..5 + R_BRACK@4..5 "]" + R_CURLY@5..6 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_args.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_args.snap new file mode 100644 index 0000000000..b62c8c2559 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_args.snap @@ -0,0 +1,20 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\foo{bar}[qux]\"#)" + +--- +ROOT@0..14 + PREAMBLE@0..14 + GENERIC_COMMAND@0..14 + GENERIC_COMMAND_NAME@0..4 "\\foo" + CURLY_GROUP@4..9 + L_CURLY@4..5 "{" + TEXT@5..8 + WORD@5..8 "bar" + R_CURLY@8..9 "}" + MIXED_GROUP@9..14 + L_BRACK@9..10 "[" + TEXT@10..13 + WORD@10..13 "qux" + R_BRACK@13..14 "]" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_empty.snap new file mode 100644 index 0000000000..84e62ea81d --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_empty.snap @@ -0,0 +1,10 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\foo\"#)" + +--- +ROOT@0..4 + PREAMBLE@0..4 + GENERIC_COMMAND@0..4 + GENERIC_COMMAND_NAME@0..4 "\\foo" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_escape.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_escape.snap new file mode 100644 index 0000000000..a0d5d2e29b --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__generic_command_escape.snap @@ -0,0 +1,10 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\#\"#)" + +--- +ROOT@0..2 + PREAMBLE@0..2 + GENERIC_COMMAND@0..2 + GENERIC_COMMAND_NAME@0..2 "\\#" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_definition_simple.snap new file mode 100644 index 0000000000..d1b31e275a --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_definition_simple.snap @@ -0,0 +1,33 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newglossaryentry{foo}{bar = baz, qux,}\"#)" + +--- +ROOT@0..39 + PREAMBLE@0..39 + GLOSSARY_ENTRY_DEFINITION@0..39 + GLOSSARY_ENTRY_DEFINITION_NAME@0..17 "\\newglossaryentry" + CURLY_GROUP_WORD@17..22 + L_CURLY@17..18 "{" + WORD@18..21 "foo" + R_CURLY@21..22 "}" + CURLY_GROUP_KEY_VALUE@22..39 + L_CURLY@22..23 "{" + KEY_VALUE_BODY@23..38 + KEY_VALUE_PAIR@23..32 + KEY@23..27 + WORD@23..26 "bar" + WHITESPACE@26..27 " " + EQUALITY_SIGN@27..28 "=" + WHITESPACE@28..29 " " + VALUE@29..32 + TEXT@29..32 + WORD@29..32 "baz" + COMMA@32..33 "," + WHITESPACE@33..34 " " + KEY_VALUE_PAIR@34..37 + KEY@34..37 + WORD@34..37 "qux" + COMMA@37..38 "," + R_CURLY@38..39 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_options.snap new file mode 100644 index 0000000000..ad5c44885a --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_options.snap @@ -0,0 +1,32 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\gls[foo = bar, qux]{baz}\"#)" + +--- +ROOT@0..25 + PREAMBLE@0..25 + GLOSSARY_ENTRY_REFERENCE@0..25 + GLOSSARY_ENTRY_REFERENCE_NAME@0..4 "\\gls" + BRACK_GROUP_KEY_VALUE@4..20 + L_BRACK@4..5 "[" + KEY_VALUE_BODY@5..19 + KEY_VALUE_PAIR@5..14 + KEY@5..9 + WORD@5..8 "foo" + WHITESPACE@8..9 " " + EQUALITY_SIGN@9..10 "=" + WHITESPACE@10..11 " " + VALUE@11..14 + TEXT@11..14 + WORD@11..14 "bar" + COMMA@14..15 "," + WHITESPACE@15..16 " " + KEY_VALUE_PAIR@16..19 + KEY@16..19 + WORD@16..19 "qux" + R_BRACK@19..20 "]" + CURLY_GROUP_WORD@20..25 + L_CURLY@20..21 "{" + WORD@21..24 "baz" + R_CURLY@24..25 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_simple.snap new file mode 100644 index 0000000000..2a6b845733 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__glossary_entry_reference_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\gls{foo}\"#)" + +--- +ROOT@0..9 + PREAMBLE@0..9 + GLOSSARY_ENTRY_REFERENCE@0..9 + GLOSSARY_ENTRY_REFERENCE_NAME@0..4 "\\gls" + CURLY_GROUP_WORD@4..9 + L_CURLY@4..5 "{" + WORD@5..8 "foo" + R_CURLY@8..9 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_options.snap new file mode 100644 index 0000000000..910a4848ef --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_options.snap @@ -0,0 +1,25 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\includegraphics[scale=.5]{foo/bar.pdf}\"#)" + +--- +ROOT@0..39 + PREAMBLE@0..39 + GRAPHICS_INCLUDE@0..39 + GRAPHICS_INCLUDE_NAME@0..16 "\\includegraphics" + BRACK_GROUP_KEY_VALUE@16..26 + L_BRACK@16..17 "[" + KEY_VALUE_BODY@17..25 + KEY_VALUE_PAIR@17..25 + KEY@17..22 + WORD@17..22 "scale" + EQUALITY_SIGN@22..23 "=" + VALUE@23..25 + TEXT@23..25 + WORD@23..25 ".5" + R_BRACK@25..26 "]" + CURLY_GROUP_WORD_LIST@26..39 + L_CURLY@26..27 "{" + WORD@27..38 "foo/bar.pdf" + R_CURLY@38..39 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_simple.snap new file mode 100644 index 0000000000..fa6fbf1a14 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__graphics_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\includegraphics{foo/bar.pdf}\"#)" + +--- +ROOT@0..29 + PREAMBLE@0..29 + GRAPHICS_INCLUDE@0..29 + GRAPHICS_INCLUDE_NAME@0..16 "\\includegraphics" + CURLY_GROUP_WORD_LIST@16..29 + L_CURLY@16..17 "{" + WORD@17..28 "foo/bar.pdf" + R_CURLY@28..29 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__hello_world.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__hello_world.snap new file mode 100644 index 0000000000..ee25cb5453 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__hello_world.snap @@ -0,0 +1,12 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"Hello World!\"#)" + +--- +ROOT@0..12 + PREAMBLE@0..12 + TEXT@0..12 + WORD@0..5 "Hello" + WHITESPACE@5..6 " " + WORD@6..12 "World!" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_incomplete.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_incomplete.snap new file mode 100644 index 0000000000..3dae1f433a --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_incomplete.snap @@ -0,0 +1,15 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\import{foo\"#)" + +--- +ROOT@0..11 + PREAMBLE@0..11 + IMPORT@0..11 + IMPORT_NAME@0..7 "\\import" + CURLY_GROUP_WORD@7..11 + L_CURLY@7..8 "{" + WORD@8..11 "foo" + MISSING@11..11 "" + MISSING@11..11 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_simple.snap new file mode 100644 index 0000000000..6e21f65cbf --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__import_simple.snap @@ -0,0 +1,18 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\import{foo}{bar}\"#)" + +--- +ROOT@0..17 + PREAMBLE@0..17 + IMPORT@0..17 + IMPORT_NAME@0..7 "\\import" + CURLY_GROUP_WORD@7..12 + L_CURLY@7..8 "{" + WORD@8..11 "foo" + R_CURLY@11..12 "}" + CURLY_GROUP_WORD@12..17 + L_CURLY@12..13 "{" + WORD@13..16 "bar" + R_CURLY@16..17 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_options.snap new file mode 100644 index 0000000000..d1fd2df652 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_options.snap @@ -0,0 +1,25 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\includesvg[scale=.5]{foo/bar}\"#)" + +--- +ROOT@0..30 + PREAMBLE@0..30 + SVG_INCLUDE@0..30 + SVG_INCLUDE_NAME@0..11 "\\includesvg" + BRACK_GROUP_KEY_VALUE@11..21 + L_BRACK@11..12 "[" + KEY_VALUE_BODY@12..20 + KEY_VALUE_PAIR@12..20 + KEY@12..17 + WORD@12..17 "scale" + EQUALITY_SIGN@17..18 "=" + VALUE@18..20 + TEXT@18..20 + WORD@18..20 ".5" + R_BRACK@20..21 "]" + CURLY_GROUP_WORD_LIST@21..30 + L_CURLY@21..22 "{" + WORD@22..29 "foo/bar" + R_CURLY@29..30 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_simple.snap new file mode 100644 index 0000000000..59f50c36d3 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inkscape_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\includesvg{foo/bar}\"#)" + +--- +ROOT@0..20 + PREAMBLE@0..20 + SVG_INCLUDE@0..20 + SVG_INCLUDE_NAME@0..11 "\\includesvg" + CURLY_GROUP_WORD_LIST@11..20 + L_CURLY@11..12 "{" + WORD@12..19 "foo/bar" + R_CURLY@19..20 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline.snap new file mode 100644 index 0000000000..b430de766e --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline.snap @@ -0,0 +1,26 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"$x \\in [0, \\infty)$\"#)" + +--- +ROOT@0..19 + PREAMBLE@0..19 + FORMULA@0..19 + DOLLAR@0..1 "$" + TEXT@1..3 + WORD@1..2 "x" + WHITESPACE@2..3 " " + GENERIC_COMMAND@3..18 + GENERIC_COMMAND_NAME@3..6 "\\in" + WHITESPACE@6..7 " " + MIXED_GROUP@7..18 + L_BRACK@7..8 "[" + TEXT@8..11 + WORD@8..9 "0" + COMMA@9..10 "," + WHITESPACE@10..11 " " + GENERIC_COMMAND@11..17 + GENERIC_COMMAND_NAME@11..17 "\\infty" + R_PAREN@17..18 ")" + DOLLAR@18..19 "$" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline_double_dollar.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline_double_dollar.snap new file mode 100644 index 0000000000..8da22f3ecb --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__inline_double_dollar.snap @@ -0,0 +1,26 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"$$x \\in [0, \\infty)$$\"#)" + +--- +ROOT@0..21 + PREAMBLE@0..21 + FORMULA@0..21 + DOLLAR@0..2 "$$" + TEXT@2..4 + WORD@2..3 "x" + WHITESPACE@3..4 " " + GENERIC_COMMAND@4..19 + GENERIC_COMMAND_NAME@4..7 "\\in" + WHITESPACE@7..8 " " + MIXED_GROUP@8..19 + L_BRACK@8..9 "[" + TEXT@9..12 + WORD@9..10 "0" + COMMA@10..11 "," + WHITESPACE@11..12 " " + GENERIC_COMMAND@12..18 + GENERIC_COMMAND_NAME@12..18 "\\infty" + R_PAREN@18..19 ")" + DOLLAR@19..21 "$$" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_definition_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_definition_simple.snap new file mode 100644 index 0000000000..7eacd65c59 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_definition_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\label{foo}\"#)" + +--- +ROOT@0..11 + PREAMBLE@0..11 + LABEL_DEFINITION@0..11 + LABEL_DEFINITION_NAME@0..6 "\\label" + CURLY_GROUP_WORD@6..11 + L_CURLY@6..7 "{" + WORD@7..10 "foo" + R_CURLY@10..11 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_number.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_number.snap new file mode 100644 index 0000000000..46392d7574 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_number.snap @@ -0,0 +1,23 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newlabel{foo}{{1.1}}\"#)" + +--- +ROOT@0..21 + PREAMBLE@0..21 + LABEL_NUMBER@0..21 + LABEL_NUMBER_NAME@0..9 "\\newlabel" + CURLY_GROUP_WORD@9..14 + L_CURLY@9..10 "{" + WORD@10..13 "foo" + R_CURLY@13..14 "}" + CURLY_GROUP@14..21 + L_CURLY@14..15 "{" + CURLY_GROUP@15..20 + L_CURLY@15..16 "{" + TEXT@16..19 + WORD@16..19 "1.1" + R_CURLY@19..20 "}" + R_CURLY@20..21 "}" + MISSING@21..21 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_multiple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_multiple.snap new file mode 100644 index 0000000000..e399397e09 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_multiple.snap @@ -0,0 +1,17 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\ref{foo, bar}\"#)" + +--- +ROOT@0..14 + PREAMBLE@0..14 + LABEL_REFERENCE@0..14 + LABEL_REFERENCE_NAME@0..4 "\\ref" + CURLY_GROUP_WORD_LIST@4..14 + L_CURLY@4..5 "{" + WORD@5..8 "foo" + COMMA@8..9 "," + WHITESPACE@9..10 " " + WORD@10..13 "bar" + R_CURLY@13..14 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_error.snap new file mode 100644 index 0000000000..2894a9c2ec --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_error.snap @@ -0,0 +1,18 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\crefrange{foo{bar}\"#)" + +--- +ROOT@0..19 + PREAMBLE@0..19 + LABEL_REFERENCE_RANGE@0..19 + LABEL_REFERENCE_RANGE_NAME@0..10 "\\crefrange" + CURLY_GROUP_WORD@10..14 + L_CURLY@10..11 "{" + WORD@11..14 "foo" + MISSING@14..14 "" + CURLY_GROUP_WORD@14..19 + L_CURLY@14..15 "{" + WORD@15..18 "bar" + R_CURLY@18..19 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_incomplete.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_incomplete.snap new file mode 100644 index 0000000000..0a1753867e --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_incomplete.snap @@ -0,0 +1,15 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\crefrange{foo}\"#)" + +--- +ROOT@0..15 + PREAMBLE@0..15 + LABEL_REFERENCE_RANGE@0..15 + LABEL_REFERENCE_RANGE_NAME@0..10 "\\crefrange" + CURLY_GROUP_WORD@10..15 + L_CURLY@10..11 "{" + WORD@11..14 "foo" + R_CURLY@14..15 "}" + MISSING@15..15 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_simple.snap new file mode 100644 index 0000000000..c06c31c1b7 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_range_simple.snap @@ -0,0 +1,18 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\crefrange{foo}{bar}\"#)" + +--- +ROOT@0..20 + PREAMBLE@0..20 + LABEL_REFERENCE_RANGE@0..20 + LABEL_REFERENCE_RANGE_NAME@0..10 "\\crefrange" + CURLY_GROUP_WORD@10..15 + L_CURLY@10..11 "{" + WORD@11..14 "foo" + R_CURLY@14..15 "}" + CURLY_GROUP_WORD@15..20 + L_CURLY@15..16 "{" + WORD@16..19 "bar" + R_CURLY@19..20 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_simple.snap new file mode 100644 index 0000000000..ed06f5dfc1 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__label_reference_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\ref{foo}\"#)" + +--- +ROOT@0..9 + PREAMBLE@0..9 + LABEL_REFERENCE@0..9 + LABEL_REFERENCE_NAME@0..4 "\\ref" + CURLY_GROUP_WORD_LIST@4..9 + L_CURLY@4..5 "{" + WORD@5..8 "foo" + R_CURLY@8..9 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_include_simple.snap new file mode 100644 index 0000000000..34565aa072 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\include{foo/bar}\"#)" + +--- +ROOT@0..17 + PREAMBLE@0..17 + LATEX_INCLUDE@0..17 + LATEX_INCLUDE_NAME@0..8 "\\include" + CURLY_GROUP_WORD_LIST@8..17 + L_CURLY@8..9 "{" + WORD@9..16 "foo/bar" + R_CURLY@16..17 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_input_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_input_simple.snap new file mode 100644 index 0000000000..208e898407 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__latex_input_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\input{foo/bar.tex}\"#)" + +--- +ROOT@0..19 + PREAMBLE@0..19 + LATEX_INCLUDE@0..19 + LATEX_INCLUDE_NAME@0..6 "\\input" + CURLY_GROUP_WORD_LIST@6..19 + L_CURLY@6..7 "{" + WORD@7..18 "foo/bar.tex" + R_CURLY@18..19 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_no_impl.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_no_impl.snap new file mode 100644 index 0000000000..2e39ee899c --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_no_impl.snap @@ -0,0 +1,15 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\DeclareMathOperator{\\foo}\"#)" + +--- +ROOT@0..26 + PREAMBLE@0..26 + MATH_OPERATOR@0..26 + MATH_OPERATOR_NAME@0..20 "\\DeclareMathOperator" + CURLY_GROUP_COMMAND@20..26 + L_CURLY@20..21 "{" + GENERIC_COMMAND_NAME@21..25 "\\foo" + R_CURLY@25..26 "}" + MISSING@26..26 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_simple.snap new file mode 100644 index 0000000000..bb44005153 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__math_operator_simple.snap @@ -0,0 +1,19 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\DeclareMathOperator{\\foo}{foo}\"#)" + +--- +ROOT@0..31 + PREAMBLE@0..31 + MATH_OPERATOR@0..31 + MATH_OPERATOR_NAME@0..20 "\\DeclareMathOperator" + CURLY_GROUP_COMMAND@20..26 + L_CURLY@20..21 "{" + GENERIC_COMMAND_NAME@21..25 "\\foo" + R_CURLY@25..26 "}" + CURLY_GROUP@26..31 + L_CURLY@26..27 "{" + TEXT@27..30 + WORD@27..30 "foo" + R_CURLY@30..31 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_empty.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_empty.snap new file mode 100644 index 0000000000..b8851b6e1d --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_empty.snap @@ -0,0 +1,13 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\usepackage{}\"#)" + +--- +ROOT@0..13 + PREAMBLE@0..13 + PACKAGE_INCLUDE@0..13 + PACKAGE_INCLUDE_NAME@0..11 "\\usepackage" + CURLY_GROUP_WORD_LIST@11..13 + L_CURLY@11..12 "{" + R_CURLY@12..13 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_multiple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_multiple.snap new file mode 100644 index 0000000000..e68b98d1b5 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_multiple.snap @@ -0,0 +1,17 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\usepackage{amsmath, lipsum}\"#)" + +--- +ROOT@0..28 + PREAMBLE@0..28 + PACKAGE_INCLUDE@0..28 + PACKAGE_INCLUDE_NAME@0..11 "\\usepackage" + CURLY_GROUP_WORD_LIST@11..28 + L_CURLY@11..12 "{" + WORD@12..19 "amsmath" + COMMA@19..20 "," + WHITESPACE@20..21 " " + WORD@21..27 "lipsum" + R_CURLY@27..28 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_options.snap new file mode 100644 index 0000000000..a33c4e0bd7 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_options.snap @@ -0,0 +1,37 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\usepackage[foo = bar, baz, qux]{amsmath}\"#)" + +--- +ROOT@0..41 + PREAMBLE@0..41 + PACKAGE_INCLUDE@0..41 + PACKAGE_INCLUDE_NAME@0..11 "\\usepackage" + BRACK_GROUP_KEY_VALUE@11..32 + L_BRACK@11..12 "[" + KEY_VALUE_BODY@12..31 + KEY_VALUE_PAIR@12..21 + KEY@12..16 + WORD@12..15 "foo" + WHITESPACE@15..16 " " + EQUALITY_SIGN@16..17 "=" + WHITESPACE@17..18 " " + VALUE@18..21 + TEXT@18..21 + WORD@18..21 "bar" + COMMA@21..22 "," + WHITESPACE@22..23 " " + KEY_VALUE_PAIR@23..26 + KEY@23..26 + WORD@23..26 "baz" + COMMA@26..27 "," + WHITESPACE@27..28 " " + KEY_VALUE_PAIR@28..31 + KEY@28..31 + WORD@28..31 "qux" + R_BRACK@31..32 "]" + CURLY_GROUP_WORD_LIST@32..41 + L_CURLY@32..33 "{" + WORD@33..40 "amsmath" + R_CURLY@40..41 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_simple.snap new file mode 100644 index 0000000000..00c075fdfe --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__package_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\usepackage{amsmath}\"#)" + +--- +ROOT@0..20 + PREAMBLE@0..20 + PACKAGE_INCLUDE@0..20 + PACKAGE_INCLUDE_NAME@0..11 "\\usepackage" + CURLY_GROUP_WORD_LIST@11..20 + L_CURLY@11..12 "{" + WORD@12..19 "amsmath" + R_CURLY@19..20 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter.snap new file mode 100644 index 0000000000..8c6bd90cb8 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter.snap @@ -0,0 +1,9 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"#1\"#)" + +--- +ROOT@0..2 + PREAMBLE@0..2 + PARAMETER@0..2 "#1" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter_error.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter_error.snap new file mode 100644 index 0000000000..1a9b68fa2d --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__parameter_error.snap @@ -0,0 +1,9 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"#\"#)" + +--- +ROOT@0..1 + PREAMBLE@0..1 + PARAMETER@0..1 "#" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__pgf_library_import_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__pgf_library_import_simple.snap new file mode 100644 index 0000000000..8f289f97a4 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__pgf_library_import_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\usepgflibrary{foo}\"#)" + +--- +ROOT@0..19 + PREAMBLE@0..19 + TIKZ_LIBRARY_IMPORT@0..19 + TIKZ_LIBRARY_IMPORT_NAME@0..14 "\\usepgflibrary" + CURLY_GROUP_WORD_LIST@14..19 + L_CURLY@14..15 "{" + WORD@15..18 "foo" + R_CURLY@18..19 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_enum_item.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_enum_item.snap new file mode 100644 index 0000000000..43dcae1987 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_enum_item.snap @@ -0,0 +1,45 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\begin{enumerate} \\item 1 \\item[2] 2 \\item 3 \\end{enumerate}\"#)" + +--- +ROOT@0..60 + PREAMBLE@0..60 + ENVIRONMENT@0..60 + BEGIN@0..18 + BEGIN_ENVIRONMENT_NAME@0..6 "\\begin" + CURLY_GROUP_WORD@6..18 + L_CURLY@6..7 "{" + WORD@7..16 "enumerate" + R_CURLY@16..17 "}" + WHITESPACE@17..18 " " + ENUM_ITEM@18..26 + ENUM_ITEM_NAME@18..23 "\\item" + WHITESPACE@23..24 " " + TEXT@24..26 + WORD@24..25 "1" + WHITESPACE@25..26 " " + ENUM_ITEM@26..37 + ENUM_ITEM_NAME@26..31 "\\item" + BRACK_GROUP@31..35 + L_BRACK@31..32 "[" + TEXT@32..33 + WORD@32..33 "2" + R_BRACK@33..34 "]" + WHITESPACE@34..35 " " + TEXT@35..37 + WORD@35..36 "2" + WHITESPACE@36..37 " " + ENUM_ITEM@37..45 + ENUM_ITEM_NAME@37..42 "\\item" + WHITESPACE@42..43 " " + TEXT@43..45 + WORD@43..44 "3" + WHITESPACE@44..45 " " + END@45..60 + END_ENVIRONMENT_NAME@45..49 "\\end" + CURLY_GROUP_WORD@49..60 + L_CURLY@49..50 "{" + WORD@50..59 "enumerate" + R_CURLY@59..60 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_invalid_nesting.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_invalid_nesting.snap new file mode 100644 index 0000000000..f999a50e9a --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_invalid_nesting.snap @@ -0,0 +1,23 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\section{Foo} \\chapter{Bar}\"#)" + +--- +ROOT@0..27 + PREAMBLE@0..27 + SECTION@0..14 + SECTION_NAME@0..8 "\\section" + CURLY_GROUP@8..14 + L_CURLY@8..9 "{" + TEXT@9..12 + WORD@9..12 "Foo" + R_CURLY@12..13 "}" + WHITESPACE@13..14 " " + CHAPTER@14..27 + CHAPTER_NAME@14..22 "\\chapter" + CURLY_GROUP@22..27 + L_CURLY@22..23 "{" + TEXT@23..26 + WORD@23..26 "Bar" + R_CURLY@26..27 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_nested.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_nested.snap new file mode 100644 index 0000000000..db5602e591 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_nested.snap @@ -0,0 +1,57 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\part{1}\\chapter{2}\\section{3}\\subsection{4}\\subsubsection{5}\\paragraph{6}\\subparagraph{7}\"#)" + +--- +ROOT@0..90 + PREAMBLE@0..90 + PART@0..90 + PART_NAME@0..5 "\\part" + CURLY_GROUP@5..8 + L_CURLY@5..6 "{" + TEXT@6..7 + WORD@6..7 "1" + R_CURLY@7..8 "}" + CHAPTER@8..90 + CHAPTER_NAME@8..16 "\\chapter" + CURLY_GROUP@16..19 + L_CURLY@16..17 "{" + TEXT@17..18 + WORD@17..18 "2" + R_CURLY@18..19 "}" + SECTION@19..90 + SECTION_NAME@19..27 "\\section" + CURLY_GROUP@27..30 + L_CURLY@27..28 "{" + TEXT@28..29 + WORD@28..29 "3" + R_CURLY@29..30 "}" + SUBSECTION@30..90 + SUBSECTION_NAME@30..41 "\\subsection" + CURLY_GROUP@41..44 + L_CURLY@41..42 "{" + TEXT@42..43 + WORD@42..43 "4" + R_CURLY@43..44 "}" + SUBSUBSECTION@44..90 + SUBSUBSECTION_NAME@44..58 "\\subsubsection" + CURLY_GROUP@58..61 + L_CURLY@58..59 "{" + TEXT@59..60 + WORD@59..60 "5" + R_CURLY@60..61 "}" + PARAGRAPH@61..90 + PARAGRAPH_NAME@61..71 "\\paragraph" + CURLY_GROUP@71..74 + L_CURLY@71..72 "{" + TEXT@72..73 + WORD@72..73 "6" + R_CURLY@73..74 "}" + SUBPARAGRAPH@74..90 + SUBPARAGRAPH_NAME@74..87 "\\subparagraph" + CURLY_GROUP@87..90 + L_CURLY@87..88 "{" + TEXT@88..89 + WORD@88..89 "7" + R_CURLY@89..90 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_siblings.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_siblings.snap new file mode 100644 index 0000000000..94d2ef40bd --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__structure_siblings.snap @@ -0,0 +1,29 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\section{Foo} Foo \\section{Bar} Bar\"#)" + +--- +ROOT@0..35 + PREAMBLE@0..35 + SECTION@0..18 + SECTION_NAME@0..8 "\\section" + CURLY_GROUP@8..14 + L_CURLY@8..9 "{" + TEXT@9..12 + WORD@9..12 "Foo" + R_CURLY@12..13 "}" + WHITESPACE@13..14 " " + TEXT@14..18 + WORD@14..17 "Foo" + WHITESPACE@17..18 " " + SECTION@18..35 + SECTION_NAME@18..26 "\\section" + CURLY_GROUP@26..32 + L_CURLY@26..27 "{" + TEXT@27..30 + WORD@27..30 "Bar" + R_CURLY@30..31 "}" + WHITESPACE@31..32 " " + TEXT@32..35 + WORD@32..35 "Bar" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_options.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_options.snap new file mode 100644 index 0000000000..863f27cf56 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_options.snap @@ -0,0 +1,25 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\includesvg[scale=.5]{foo/bar.svg}\"#)" + +--- +ROOT@0..34 + PREAMBLE@0..34 + SVG_INCLUDE@0..34 + SVG_INCLUDE_NAME@0..11 "\\includesvg" + BRACK_GROUP_KEY_VALUE@11..21 + L_BRACK@11..12 "[" + KEY_VALUE_BODY@12..20 + KEY_VALUE_PAIR@12..20 + KEY@12..17 + WORD@12..17 "scale" + EQUALITY_SIGN@17..18 "=" + VALUE@18..20 + TEXT@18..20 + WORD@18..20 ".5" + R_BRACK@20..21 "]" + CURLY_GROUP_WORD_LIST@21..34 + L_CURLY@21..22 "{" + WORD@22..33 "foo/bar.svg" + R_CURLY@33..34 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_simple.snap new file mode 100644 index 0000000000..02db942d99 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__svg_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\includesvg{foo/bar.svg}\"#)" + +--- +ROOT@0..24 + PREAMBLE@0..24 + SVG_INCLUDE@0..24 + SVG_INCLUDE_NAME@0..11 "\\includesvg" + CURLY_GROUP_WORD_LIST@11..24 + L_CURLY@11..12 "{" + WORD@12..23 "foo/bar.svg" + R_CURLY@23..24 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_full.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_full.snap new file mode 100644 index 0000000000..d8fe3816ad --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_full.snap @@ -0,0 +1,27 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newtheorem{foo}[bar]{Foo}[baz]\"#)" + +--- +ROOT@0..31 + PREAMBLE@0..31 + THEOREM_DEFINITION@0..31 + THEOREM_DEFINITION_NAME@0..11 "\\newtheorem" + CURLY_GROUP_WORD@11..16 + L_CURLY@11..12 "{" + WORD@12..15 "foo" + R_CURLY@15..16 "}" + BRACK_GROUP_WORD@16..21 + L_BRACK@16..17 "[" + WORD@17..20 "bar" + R_BRACK@20..21 "]" + CURLY_GROUP@21..26 + L_CURLY@21..22 "{" + TEXT@22..25 + WORD@22..25 "Foo" + R_CURLY@25..26 "}" + BRACK_GROUP_WORD@26..31 + L_BRACK@26..27 "[" + WORD@27..30 "baz" + R_BRACK@30..31 "]" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_counter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_counter.snap new file mode 100644 index 0000000000..9e550b19a4 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_counter.snap @@ -0,0 +1,19 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newtheorem{foo}[bar]\"#)" + +--- +ROOT@0..21 + PREAMBLE@0..21 + THEOREM_DEFINITION@0..21 + THEOREM_DEFINITION_NAME@0..11 "\\newtheorem" + CURLY_GROUP_WORD@11..16 + L_CURLY@11..12 "{" + WORD@12..15 "foo" + R_CURLY@15..16 "}" + BRACK_GROUP_WORD@16..21 + L_BRACK@16..17 "[" + WORD@17..20 "bar" + R_BRACK@20..21 "]" + MISSING@21..21 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description.snap new file mode 100644 index 0000000000..356dc079f1 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description.snap @@ -0,0 +1,19 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newtheorem{foo}{Foo}\"#)" + +--- +ROOT@0..21 + PREAMBLE@0..21 + THEOREM_DEFINITION@0..21 + THEOREM_DEFINITION_NAME@0..11 "\\newtheorem" + CURLY_GROUP_WORD@11..16 + L_CURLY@11..12 "{" + WORD@12..15 "foo" + R_CURLY@15..16 "}" + CURLY_GROUP@16..21 + L_CURLY@16..17 "{" + TEXT@17..20 + WORD@17..20 "Foo" + R_CURLY@20..21 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description_and_counter.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description_and_counter.snap new file mode 100644 index 0000000000..cc6e34d378 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_name_with_description_and_counter.snap @@ -0,0 +1,23 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newtheorem{foo}[bar]{Foo}\"#)" + +--- +ROOT@0..26 + PREAMBLE@0..26 + THEOREM_DEFINITION@0..26 + THEOREM_DEFINITION_NAME@0..11 "\\newtheorem" + CURLY_GROUP_WORD@11..16 + L_CURLY@11..12 "{" + WORD@12..15 "foo" + R_CURLY@15..16 "}" + BRACK_GROUP_WORD@16..21 + L_BRACK@16..17 "[" + WORD@17..20 "bar" + R_BRACK@20..21 "]" + CURLY_GROUP@21..26 + L_CURLY@21..22 "{" + TEXT@22..25 + WORD@22..25 "Foo" + R_CURLY@25..26 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_only_name.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_only_name.snap new file mode 100644 index 0000000000..27a040b7ff --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__theorem_definition_only_name.snap @@ -0,0 +1,15 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\newtheorem{foo}\"#)" + +--- +ROOT@0..16 + PREAMBLE@0..16 + THEOREM_DEFINITION@0..16 + THEOREM_DEFINITION_NAME@0..11 "\\newtheorem" + CURLY_GROUP_WORD@11..16 + L_CURLY@11..12 "{" + WORD@12..15 "foo" + R_CURLY@15..16 "}" + MISSING@16..16 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__tikz_library_import_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__tikz_library_import_simple.snap new file mode 100644 index 0000000000..4915306c55 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__tikz_library_import_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\usetikzlibrary{foo}\"#)" + +--- +ROOT@0..20 + PREAMBLE@0..20 + TIKZ_LIBRARY_IMPORT@0..20 + TIKZ_LIBRARY_IMPORT_NAME@0..15 "\\usetikzlibrary" + CURLY_GROUP_WORD_LIST@15..20 + L_CURLY@15..16 "{" + WORD@16..19 "foo" + R_CURLY@19..20 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_braces.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_braces.snap new file mode 100644 index 0000000000..df463f501a --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_braces.snap @@ -0,0 +1,13 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"}{\"#)" + +--- +ROOT@0..2 + PREAMBLE@0..2 + ERROR@0..1 + R_CURLY@0..1 "}" + CURLY_GROUP@1..2 + L_CURLY@1..2 "{" + MISSING@2..2 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets.snap new file mode 100644 index 0000000000..578cbf6099 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets.snap @@ -0,0 +1,13 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"][\"#)" + +--- +ROOT@0..2 + PREAMBLE@0..2 + ERROR@0..1 + R_BRACK@0..1 "]" + MIXED_GROUP@1..2 + L_BRACK@1..2 "[" + MISSING@2..2 "" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets_with_group.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets_with_group.snap new file mode 100644 index 0000000000..d929c98c46 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__unmatched_brackets_with_group.snap @@ -0,0 +1,16 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"{][}\"#)" + +--- +ROOT@0..4 + PREAMBLE@0..4 + CURLY_GROUP@0..4 + L_CURLY@0..1 "{" + ERROR@1..2 + R_BRACK@1..2 "]" + MIXED_GROUP@2..3 + L_BRACK@2..3 "[" + MISSING@3..3 "" + R_CURLY@3..4 "}" + diff --git a/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__verbatim_include_simple.snap b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__verbatim_include_simple.snap new file mode 100644 index 0000000000..399e16d791 --- /dev/null +++ b/support/texlab/src/syntax/latex/snapshots/texlab__syntax__latex__parser__tests__verbatim_include_simple.snap @@ -0,0 +1,14 @@ +--- +source: src/syntax/latex/parser.rs +expression: "setup(r#\"\\verbatiminput{foo/bar.txt}\"#)" + +--- +ROOT@0..27 + PREAMBLE@0..27 + VERBATIM_INCLUDE@0..27 + VERBATIM_INCLUDE_NAME@0..14 "\\verbatiminput" + CURLY_GROUP_WORD_LIST@14..27 + L_CURLY@14..15 "{" + WORD@15..26 "foo/bar.txt" + R_CURLY@26..27 "}" + diff --git a/support/texlab/src/syntax/latexindent.rs b/support/texlab/src/syntax/latexindent.rs deleted file mode 100644 index c45c95d4e5..0000000000 --- a/support/texlab/src/syntax/latexindent.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::{io, process::Stdio}; -use tempfile::tempdir; -use tokio::{fs, process::Command}; - -pub async fn format(text: &str, extension: &str) -> io::Result<String> { - let dir = tempdir()?; - let input = format!("input.{}", extension); - let output = format!("output.{}", extension); - fs::write(dir.path().join(&input), text).await?; - - Command::new("latexindent") - .args(&["-o", &output, &input]) - .current_dir(dir.path()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()? - .await?; - - fs::read_to_string(dir.path().join(&output)).await -} diff --git a/support/texlab/src/syntax/mod.rs b/support/texlab/src/syntax/mod.rs deleted file mode 100644 index 87b33792df..0000000000 --- a/support/texlab/src/syntax/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod bibtex; -mod generic_ast; -mod lang_data; -pub mod latex; -pub mod latexindent; -mod lsp_kind; -mod text; - -pub use self::{ - generic_ast::{Ast, AstNodeIndex}, - lang_data::*, - lsp_kind::Structure, - text::{CharStream, Span, SyntaxNode}, -}; diff --git a/support/texlab/src/syntax/text.rs b/support/texlab/src/syntax/text.rs deleted file mode 100644 index 17d6876126..0000000000 --- a/support/texlab/src/syntax/text.rs +++ /dev/null @@ -1,260 +0,0 @@ -use crate::protocol::{Position, Range}; -use serde::{Deserialize, Serialize}; -use std::{iter::Peekable, str::CharIndices}; - -pub trait SyntaxNode { - fn range(&self) -> Range; - - fn start(&self) -> Position { - self.range().start - } - - fn end(&self) -> Position { - self.range().end - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Span { - pub range: Range, - pub text: String, -} - -impl Span { - pub fn new(range: Range, text: String) -> Self { - Self { range, text } - } -} - -impl SyntaxNode for Span { - fn range(&self) -> Range { - self.range - } -} - -#[derive(Debug)] -pub struct CharStream<'a> { - text: &'a str, - chars: Peekable<CharIndices<'a>>, - pub current_position: Position, - pub current_index: usize, - start_position: Position, - start_index: usize, -} - -impl<'a> CharStream<'a> { - pub fn new(text: &'a str) -> Self { - Self { - text, - chars: text.char_indices().peekable(), - current_position: Position::new(0, 0), - current_index: 0, - start_position: Position::new(0, 0), - start_index: 0, - } - } - - pub fn peek(&mut self) -> Option<char> { - self.chars.peek().map(|(_, c)| *c) - } - - pub fn satifies<P: FnOnce(&char) -> bool>(&mut self, predicate: P) -> bool { - self.peek().filter(predicate).is_some() - } - - pub fn skip_rest_of_line(&mut self) { - loop { - match self.peek() { - Some('\n') => { - self.next(); - break; - } - Some(_) => { - self.next(); - } - None => { - break; - } - } - } - } - - pub fn start_span(&mut self) { - self.start_index = self.current_index; - self.start_position = self.current_position; - } - - pub fn end_span(&mut self) -> Span { - let range = Range::new(self.start_position, self.current_position); - let text = &self.text[self.start_index..self.current_index]; - Span::new(range, text.to_owned()) - } - - pub fn seek(&mut self, position: Position) { - while self.current_position < position { - self.next(); - } - } - - pub fn command(&mut self) -> Span { - self.start_span(); - self.next(); - let mut escape = true; - while self.satifies(|c| is_command_char(*c)) { - self.next(); - escape = false; - } - - if let Some(c) = self.peek() { - if c != '\r' && c != '\n' && (escape || c == '*') { - self.next(); - } - } - - self.end_span() - } - - fn update_position(&mut self, c: char) { - if c == '\n' { - self.current_position.line += 1; - self.current_position.character = 0; - } else { - self.current_position.character += 1; - } - } - - pub fn extract(text: &'a str, range: Range) -> String { - let mut stream = Self::new(text); - stream.seek(range.start); - stream.start_span(); - stream.seek(range.end); - stream.end_span().text - } -} - -impl<'a> Iterator for CharStream<'a> { - type Item = char; - - fn next(&mut self) -> Option<char> { - if let Some((i, c)) = self.chars.next() { - self.current_index = i + c.len_utf8(); - self.update_position(c); - Some(c) - } else { - None - } - } -} - -fn is_command_char(c: char) -> bool { - c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '@' -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::protocol::RangeExt; - - #[test] - fn peek() { - let mut stream = CharStream::new("ab\nc"); - assert_eq!(Some('a'), stream.peek()); - assert_eq!(Some('a'), stream.next()); - assert_eq!(Some('b'), stream.peek()); - assert_eq!(Some('b'), stream.next()); - assert_eq!(Some('\n'), stream.peek()); - assert_eq!(Some('\n'), stream.next()); - assert_eq!(Some('c'), stream.peek()); - assert_eq!(Some('c'), stream.next()); - assert_eq!(None, stream.peek()); - assert_eq!(None, stream.next()); - } - - #[test] - fn span() { - let mut stream = CharStream::new("abc\ndef"); - stream.next(); - stream.start_span(); - stream.next(); - stream.next(); - let span = stream.end_span(); - assert_eq!( - Span::new(Range::new_simple(0, 1, 0, 3), "bc".to_owned()), - span - ); - assert_eq!(Position::new(0, 1), span.start()); - assert_eq!(Position::new(0, 3), span.end()); - } - - #[test] - fn span_unicode() { - let mut stream = CharStream::new("😀😃😄😁"); - stream.next(); - stream.start_span(); - stream.next(); - stream.next(); - let span = stream.end_span(); - assert_eq!( - Span::new(Range::new_simple(0, 1, 0, 3), "😃😄".to_owned()), - span - ); - } - - #[test] - fn satifies() { - let mut stream = CharStream::new("aBc"); - assert_eq!(true, stream.satifies(|c| c.is_lowercase())); - stream.next(); - assert_eq!(false, stream.satifies(|c| c.is_lowercase())); - } - - #[test] - fn skip_rest_of_line() { - let mut stream = CharStream::new("abc\ndef"); - stream.skip_rest_of_line(); - assert_eq!(Some('d'), stream.next()); - stream.skip_rest_of_line(); - assert_eq!(None, stream.next()); - stream.skip_rest_of_line(); - assert_eq!(None, stream.next()); - } - - #[test] - fn seek() { - let mut stream = CharStream::new("abc\ndefghi"); - let pos = Position::new(1, 2); - stream.seek(pos); - assert_eq!(Some('f'), stream.peek()); - } - - #[test] - fn command_basic() { - let mut stream = CharStream::new("\\foo@bar"); - let span = stream.command(); - assert_eq!( - Span::new(Range::new_simple(0, 0, 0, 8), "\\foo@bar".to_owned()), - span - ); - } - - #[test] - fn command_star() { - let mut stream = CharStream::new("\\foo*"); - let span = stream.command(); - assert_eq!( - Span::new(Range::new_simple(0, 0, 0, 5), "\\foo*".to_owned()), - span - ); - } - - #[test] - fn command_escape() { - let mut stream = CharStream::new("\\**"); - let span = stream.command(); - assert_eq!( - Span::new(Range::new_simple(0, 0, 0, 2), "\\*".to_owned()), - span - ); - } -} diff --git a/support/texlab/src/test/client.rs b/support/texlab/src/test/client.rs deleted file mode 100644 index b8806cc933..0000000000 --- a/support/texlab/src/test/client.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::protocol::*; -use jsonrpc::client::Result; -use jsonrpc_derive::{jsonrpc_client, jsonrpc_method}; - -#[jsonrpc_client(TestLatexLspClient)] -pub trait TestLspClient { - #[jsonrpc_method("initialize", kind = "request")] - async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult>; - - #[jsonrpc_method("initialized", kind = "notification")] - async fn initialized(&self, params: InitializedParams); - - #[jsonrpc_method("shutdown", kind = "request")] - async fn shutdown(&self, params: ()) -> Result<()>; - - #[jsonrpc_method("exit", kind = "notification")] - async fn exit(&self, params: ()); - - #[jsonrpc_method("textDocument/didOpen", kind = "notification")] - async fn did_open(&self, params: DidOpenTextDocumentParams); - - #[jsonrpc_method("textDocument/didChange", kind = "notification")] - async fn did_change(&self, params: DidChangeTextDocumentParams); - - #[jsonrpc_method("workspace/didChangeConfiguration", kind = "notification")] - async fn did_change_configuration(&self, params: DidChangeConfigurationParams); - - #[jsonrpc_method("textDocument/definition", kind = "request")] - async fn definition(&self, params: TextDocumentPositionParams) -> Result<DefinitionResponse>; - - #[jsonrpc_method("textDocument/completion", kind = "request")] - async fn completion(&self, params: CompletionParams) -> Result<CompletionList>; - - #[jsonrpc_method("completionItem/resolve", kind = "request")] - async fn completion_resolve(&self, item: CompletionItem) -> Result<CompletionItem>; - - #[jsonrpc_method("textDocument/foldingRange", kind = "request")] - async fn folding_range(&self, params: FoldingRangeParams) -> Result<Vec<FoldingRange>>; - - #[jsonrpc_method("textDocument/documentHighlight", kind = "request")] - async fn document_highlight( - &self, - params: TextDocumentPositionParams, - ) -> Result<Vec<DocumentHighlight>>; - - #[jsonrpc_method("textDocument/documentLink", kind = "request")] - async fn document_link(&self, params: DocumentLinkParams) -> Result<Vec<DocumentLink>>; - - #[jsonrpc_method("textDocument/references", kind = "request")] - async fn references(&self, params: ReferenceParams) -> Result<Vec<Location>>; - - #[jsonrpc_method("textDocument/prepareRename", kind = "request")] - async fn prepare_rename(&self, params: TextDocumentPositionParams) -> Result<Option<Range>>; - - #[jsonrpc_method("textDocument/rename", kind = "request")] - async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>>; - - #[jsonrpc_method("textDocument/hover", kind = "request")] - async fn hover(&self, params: TextDocumentPositionParams) -> Result<Option<Hover>>; - - #[jsonrpc_method("workspace/symbol", kind = "request")] - async fn workspace_symbol( - &self, - params: WorkspaceSymbolParams, - ) -> Result<Vec<SymbolInformation>>; - - #[jsonrpc_method("textDocument/documentSymbol", kind = "request")] - async fn document_symbol(&self, params: DocumentSymbolParams) - -> Result<DocumentSymbolResponse>; - - #[jsonrpc_method("$/detectRoot", kind = "request")] - async fn detect_root(&self, params: TextDocumentIdentifier) -> Result<()>; -} diff --git a/support/texlab/src/test/mod.rs b/support/texlab/src/test/mod.rs deleted file mode 100644 index 5f26fb0dcc..0000000000 --- a/support/texlab/src/test/mod.rs +++ /dev/null @@ -1,632 +0,0 @@ -#![allow(deprecated)] - -mod client; -mod server; - -pub use self::{ - client::{TestLatexLspClient, TestLspClient}, - server::TestLatexLspServer, -}; - -use crate::{ - protocol::*, - server::LatexLspServer, - tex::{Distribution, UnknownDistribution}, -}; -use futures::{ - channel::mpsc, - future::{join, AbortHandle, Abortable}, - lock::Mutex, - prelude::*, -}; -use jsonrpc::MessageHandler; -use once_cell::sync::Lazy; -use std::{path::PathBuf, sync::Arc}; -use tempfile::{tempdir, TempDir}; -use tokio::fs; - -struct GlobalDistribution { - distro: Mutex<Option<Arc<dyn Distribution>>>, -} - -impl GlobalDistribution { - fn new() -> Self { - Self { - distro: Mutex::new(None), - } - } - - async fn get(&self, use_distro: bool) -> Arc<dyn Distribution> { - if use_distro { - let mut distro_lock = self.distro.lock().await; - match &*distro_lock { - Some(distro) => distro.clone(), - None => { - let distro = Distribution::detect().await; - *distro_lock = Some(distro.clone()); - distro - } - } - } else { - Arc::new(UnknownDistribution::default()) - } - } -} - -static DISTRO: Lazy<GlobalDistribution> = Lazy::new(GlobalDistribution::new); - -#[derive(Debug)] -struct Endpoint<S, C> { - receiver: mpsc::Receiver<String>, - client: Arc<C>, - server: Arc<S>, - handler: MessageHandler<S, C>, -} - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct TestBedBuilder { - files: Vec<(PathBuf, String)>, - use_distro: bool, - root_dir: Option<PathBuf>, - latex_build: Option<LatexBuildOptions>, - latex_lint: Option<LatexLintOptions>, - latex_forward_search: Option<LatexForwardSearchOptions>, - bibtex_formatting: Option<BibtexFormattingOptions>, -} - -impl TestBedBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn file<P: Into<PathBuf>>(&mut self, path: P, text: &str) -> &mut Self { - self.files.push((path.into(), text.trim().into())); - self - } - - pub fn use_distro(&mut self) -> &mut Self { - self.use_distro = true; - self - } - - pub fn root_dir<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self { - self.root_dir = Some(path.into()); - self - } - - pub fn latex_build(&mut self, options: LatexBuildOptions) -> &mut Self { - self.latex_build = Some(options); - self - } - - pub fn latex_lint(&mut self, options: LatexLintOptions) -> &mut Self { - self.latex_lint = Some(options); - self - } - - pub fn latex_forward_search(&mut self, options: LatexForwardSearchOptions) -> &mut Self { - self.latex_forward_search = Some(options); - self - } - - pub fn bibtex_formatting(&mut self, options: BibtexFormattingOptions) -> &mut Self { - self.bibtex_formatting = Some(options); - self - } - - pub async fn build(&self) -> TestBed { - let dir = tempdir().expect("failed to create temporary directory"); - for (path, text) in &self.files { - let full_path = dir.path().join(path); - fs::create_dir_all(full_path.parent().unwrap()) - .await - .unwrap(); - fs::write(&full_path, text).await.unwrap(); - } - - let (tx1, rx1) = mpsc::channel(0); - let (tx2, rx2) = mpsc::channel(0); - - let endpoint1 = self.build_endpoint1(&dir, tx2, rx1).await; - let endpoint2 = self.build_endpoint2(&dir, tx1, rx2).await; - let server = Arc::clone(&endpoint2.server); - let client = Arc::clone(&endpoint2.client); - TestBed { - dir, - server, - client, - endpoint1: Some(endpoint1), - endpoint2: Some(endpoint2), - handle: None, - } - } - - async fn build_endpoint1( - &self, - dir: &TempDir, - tx2: mpsc::Sender<String>, - rx1: mpsc::Receiver<String>, - ) -> Endpoint<LatexLspServer<LatexLspClient>, LatexLspClient> { - let client = Arc::new(LatexLspClient::new(tx2.clone())); - let server = Arc::new(LatexLspServer::new( - DISTRO.get(self.use_distro).await, - Arc::clone(&client), - Arc::new(dir.path().to_path_buf()), - )); - - Endpoint { - receiver: rx1, - client: Arc::clone(&client), - server: Arc::clone(&server), - handler: MessageHandler { - client, - server, - output: tx2.clone(), - }, - } - } - - async fn build_endpoint2( - &self, - dir: &TempDir, - tx1: mpsc::Sender<String>, - rx2: mpsc::Receiver<String>, - ) -> Endpoint<TestLatexLspServer, TestLatexLspClient> { - let options = Options { - latex: Some(LatexOptions { - root_directory: self.root_dir.as_ref().map(|path| dir.path().join(path)), - build: self.latex_build.clone(), - forward_search: self.latex_forward_search.clone(), - lint: self.latex_lint.clone(), - }), - bibtex: Some(BibtexOptions { - formatting: self.bibtex_formatting.clone(), - }), - }; - - let test_client = Arc::new(TestLatexLspClient::new(tx1.clone())); - let test_server = Arc::new(TestLatexLspServer::new(options)); - - Endpoint { - receiver: rx2, - client: Arc::clone(&test_client), - server: Arc::clone(&test_server), - handler: MessageHandler { - client: test_client, - server: test_server, - output: tx1, - }, - } - } -} - -pub struct TestBed { - pub dir: TempDir, - pub server: Arc<TestLatexLspServer>, - pub client: Arc<TestLatexLspClient>, - endpoint1: Option<Endpoint<LatexLspServer<LatexLspClient>, LatexLspClient>>, - endpoint2: Option<Endpoint<TestLatexLspServer, TestLatexLspClient>>, - handle: Option<AbortHandle>, -} - -impl TestBed { - pub fn spawn(&mut self) { - let (handle, reg) = AbortHandle::new_pair(); - - let endpoint1 = self.endpoint1.take().unwrap(); - let endpoint2 = self.endpoint2.take().unwrap(); - let mut rx1 = endpoint1.receiver; - let mut rx2 = endpoint2.receiver; - let mut handler1 = endpoint1.handler; - let mut handler2 = endpoint2.handler; - - tokio::spawn(Abortable::new( - async move { - let task1 = async move { - while let Some(json) = rx2.next().await { - handler2.handle(&json).await; - } - }; - - let task2 = async move { - while let Some(json) = rx1.next().await { - handler1.handle(&json).await; - } - }; - - join(task1, task2).await; - }, - reg, - )); - self.handle = Some(handle); - } - - pub async fn initialize(&self, capabilities: ClientCapabilities) { - self.client - .initialize(InitializeParams { - capabilities, - initialization_options: None, - process_id: None, - root_path: None, - root_uri: None, - trace: None, - workspace_folders: None, - client_info: None, - }) - .await - .unwrap(); - self.client.initialized(InitializedParams {}).await; - } - - pub fn path(&self, relative_path: &str) -> PathBuf { - self.dir.path().join(relative_path) - } - - pub fn uri(&self, relative_path: &str) -> Uri { - Uri::from_file_path(self.path(relative_path)).unwrap() - } - - pub fn identifier(&self, relative_path: &str) -> TextDocumentIdentifier { - TextDocumentIdentifier::new(self.uri(relative_path).into()) - } - - pub async fn open(&self, relative_path: &str) { - let full_path = self.path(relative_path); - let params = DidOpenTextDocumentParams { - text_document: TextDocumentItem { - language_id: if relative_path.ends_with("bib") { - "bibtex".into() - } else { - "latex".into() - }, - uri: Uri::from_file_path(&full_path).unwrap().into(), - version: 0, - text: fs::read_to_string(&full_path).await.unwrap(), - }, - }; - self.client.did_open(params).await; - } - - pub async fn edit<S: Into<String>>(&self, relative_path: &str, text: S) { - let uri = self.uri(relative_path).into(); - let params = DidChangeTextDocumentParams { - text_document: VersionedTextDocumentIdentifier::new(uri, 0), - content_changes: vec![TextDocumentContentChangeEvent { - range: None, - range_length: None, - text: text.into(), - }], - }; - self.client.did_change(params).await; - } - - pub async fn push_options(&self) { - let options = self.server.options.lock().await.clone(); - let params = DidChangeConfigurationParams { - settings: serde_json::to_value::<Options>(options).unwrap(), - }; - self.client.did_change_configuration(params).await - } - - pub async fn definition_link( - &self, - relative_path: &str, - line: u64, - character: u64, - ) -> Option<Vec<LocationLink>> { - let params = TextDocumentPositionParams { - text_document: self.identifier(relative_path), - position: Position::new(line, character), - }; - let response = self.client.definition(params).await.ok()?; - match response { - DefinitionResponse::LocationLinks(links) => Some(links), - DefinitionResponse::Locations(_) => unreachable!(), - } - } - - pub async fn definition_location( - &self, - relative_path: &str, - line: u64, - character: u64, - ) -> Option<Vec<Location>> { - let params = TextDocumentPositionParams { - text_document: self.identifier(relative_path), - position: Position::new(line, character), - }; - let response = self.client.definition(params).await.ok()?; - match response { - DefinitionResponse::LocationLinks(_) => unreachable!(), - DefinitionResponse::Locations(locations) => Some(locations), - } - } - - pub async fn completion( - &self, - relative_path: &str, - line: u64, - character: u64, - ) -> Option<Vec<CompletionItem>> { - let params = CompletionParams { - text_document_position: TextDocumentPositionParams { - text_document: self.identifier(relative_path), - position: Position::new(line, character), - }, - context: None, - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - self.client - .completion(params) - .await - .ok() - .map(|list| list.items) - } - - pub async fn folding_range(&self, relative_path: &str) -> Option<Vec<FoldingRange>> { - let params = FoldingRangeParams { - text_document: self.identifier(relative_path), - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - self.client.folding_range(params).await.ok() - } - - pub async fn document_highlight( - &self, - relative_path: &str, - line: u64, - character: u64, - ) -> Option<Vec<DocumentHighlight>> { - let params = TextDocumentPositionParams { - text_document: self.identifier(relative_path), - position: Position::new(line, character), - }; - self.client.document_highlight(params).await.ok() - } - - pub async fn document_link(&self, relative_path: &str) -> Option<Vec<DocumentLink>> { - let params = DocumentLinkParams { - text_document: self.identifier(relative_path), - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - self.client.document_link(params).await.ok() - } - - pub async fn references( - &self, - relative_path: &str, - line: u64, - character: u64, - include_declaration: bool, - ) -> Option<Vec<Location>> { - let params = ReferenceParams { - text_document_position: TextDocumentPositionParams::new( - self.identifier(relative_path), - Position::new(line, character), - ), - context: ReferenceContext { - include_declaration, - }, - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - self.client.references(params).await.ok() - } - - pub async fn prepare_rename( - &self, - relative_path: &str, - line: u64, - character: u64, - ) -> Option<Option<Range>> { - let pos = Position::new(line, character); - let params = TextDocumentPositionParams::new(self.identifier(relative_path), pos); - self.client.prepare_rename(params).await.ok() - } - - pub async fn rename<S: Into<String>>( - &self, - relative_path: &str, - line: u64, - character: u64, - new_name: S, - ) -> Option<Option<WorkspaceEdit>> { - let params = RenameParams { - text_document_position: TextDocumentPositionParams::new( - self.identifier(relative_path), - Position::new(line, character), - ), - new_name: new_name.into(), - work_done_progress_params: WorkDoneProgressParams::default(), - }; - self.client.rename(params).await.ok() - } - - pub async fn document_symbol_flat( - &self, - relative_path: &str, - ) -> Option<Vec<SymbolInformation>> { - let params = DocumentSymbolParams { - text_document: self.identifier(relative_path), - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - match self.client.document_symbol(params).await.ok()? { - DocumentSymbolResponse::Flat(symbols) => Some(symbols), - DocumentSymbolResponse::Nested(_) => unreachable!(), - } - } - - pub async fn document_symbol_nested(&self, relative_path: &str) -> Option<Vec<DocumentSymbol>> { - let params = DocumentSymbolParams { - text_document: self.identifier(relative_path), - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }; - match self.client.document_symbol(params).await.ok()? { - DocumentSymbolResponse::Flat(_) => unreachable!(), - DocumentSymbolResponse::Nested(symbols) => Some(symbols), - } - } - - pub async fn hover( - &self, - relative_path: &str, - line: u64, - character: u64, - ) -> Option<Option<Hover>> { - let params = TextDocumentPositionParams { - text_document: self.identifier(relative_path), - position: Position::new(line, character), - }; - self.client.hover(params).await.ok() - } - - pub async fn detect_root(&self, relative_path: &str) { - self.client - .detect_root(self.identifier(relative_path)) - .await - .unwrap(); - } - - pub async fn shutdown(&self) { - self.client.shutdown(()).await.unwrap(); - self.client.exit(()).await; - } -} - -impl Drop for TestBed { - fn drop(&mut self) { - if let Some(handle) = self.handle.as_ref() { - handle.abort(); - } - } -} - -pub static PULL_CAPABILITIES: ClientCapabilities = { - ClientCapabilities { - experimental: None, - text_document: None, - window: None, - workspace: Some(WorkspaceClientCapabilities { - apply_edit: None, - configuration: Some(true), - did_change_configuration: None, - did_change_watched_files: None, - execute_command: None, - symbol: None, - workspace_edit: None, - workspace_folders: None, - }), - } -}; - -pub static PUSH_CAPABILITIES: ClientCapabilities = { - ClientCapabilities { - experimental: None, - text_document: None, - window: None, - workspace: Some(WorkspaceClientCapabilities { - apply_edit: None, - configuration: None, - did_change_configuration: Some(GenericCapability { - dynamic_registration: Some(true), - }), - did_change_watched_files: None, - execute_command: None, - symbol: None, - workspace_edit: None, - workspace_folders: None, - }), - } -}; - -pub static NESTED_SYMBOL_CAPABILITIES: ClientCapabilities = { - ClientCapabilities { - experimental: None, - text_document: Some(TextDocumentClientCapabilities { - code_action: None, - code_lens: None, - color_provider: None, - completion: None, - declaration: None, - definition: None, - document_highlight: None, - document_link: None, - document_symbol: Some(DocumentSymbolCapability { - hierarchical_document_symbol_support: Some(true), - dynamic_registration: None, - symbol_kind: None, - }), - folding_range: None, - formatting: None, - hover: None, - implementation: None, - on_type_formatting: None, - publish_diagnostics: None, - range_formatting: None, - references: None, - rename: None, - signature_help: None, - synchronization: None, - type_definition: None, - }), - window: None, - workspace: Some(WorkspaceClientCapabilities { - apply_edit: None, - configuration: Some(true), - did_change_configuration: None, - did_change_watched_files: None, - execute_command: None, - symbol: None, - workspace_edit: None, - workspace_folders: None, - }), - } -}; - -pub static LOCATION_LINK_CAPABILITIES: ClientCapabilities = { - ClientCapabilities { - experimental: None, - text_document: Some(TextDocumentClientCapabilities { - code_action: None, - code_lens: None, - color_provider: None, - completion: None, - declaration: None, - definition: Some(GotoCapability { - dynamic_registration: None, - link_support: Some(true), - }), - document_highlight: None, - document_link: None, - document_symbol: None, - folding_range: None, - formatting: None, - hover: None, - implementation: None, - on_type_formatting: None, - publish_diagnostics: None, - range_formatting: None, - references: None, - rename: None, - signature_help: None, - synchronization: None, - type_definition: None, - }), - window: None, - workspace: Some(WorkspaceClientCapabilities { - apply_edit: None, - configuration: Some(true), - did_change_configuration: None, - did_change_watched_files: None, - execute_command: None, - symbol: None, - workspace_edit: None, - workspace_folders: None, - }), - } -}; diff --git a/support/texlab/src/test/server.rs b/support/texlab/src/test/server.rs deleted file mode 100644 index 77bed68c07..0000000000 --- a/support/texlab/src/test/server.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::protocol::*; -use aovec::Aovec; -use async_trait::async_trait; -use chashmap::CHashMap; -use futures::lock::Mutex; -use jsonrpc::server::{Middleware, Result}; -use jsonrpc_derive::{jsonrpc_method, jsonrpc_server}; - -pub struct TestLatexLspServer { - pub options: Mutex<Options>, - pub show_message_buf: Aovec<ShowMessageParams>, - pub register_capability_buf: Aovec<RegistrationParams>, - pub diagnostics_by_uri: CHashMap<Uri, Vec<Diagnostic>>, - pub progress_buf: Aovec<ProgressParams>, - pub work_done_progress_create_buf: Aovec<WorkDoneProgressCreateParams>, - pub log_message_buf: Aovec<LogMessageParams>, -} - -#[jsonrpc_server] -impl TestLatexLspServer { - pub fn new(options: Options) -> Self { - let base = 16; - Self { - options: Mutex::new(options), - show_message_buf: Aovec::new(base), - register_capability_buf: Aovec::new(base), - diagnostics_by_uri: CHashMap::new(), - progress_buf: Aovec::new(base), - work_done_progress_create_buf: Aovec::new(base), - log_message_buf: Aovec::new(base), - } - } - - #[jsonrpc_method("workspace/configuration", kind = "request")] - pub async fn configuration(&self, params: ConfigurationParams) -> Result<serde_json::Value> { - let options = self.options.lock().await; - if params.items[0].section.as_ref().unwrap() == "latex" { - Ok(serde_json::to_value(vec![options.latex.clone().unwrap_or_default()]).unwrap()) - } else { - Ok(serde_json::to_value(vec![options.bibtex.clone().unwrap_or_default()]).unwrap()) - } - } - - #[jsonrpc_method("window/showMessage", kind = "notification")] - pub async fn show_message(&self, params: ShowMessageParams) { - self.show_message_buf.push(params); - } - - #[jsonrpc_method("client/registerCapability", kind = "request")] - pub async fn register_capability(&self, params: RegistrationParams) -> Result<()> { - self.register_capability_buf.push(params); - Ok(()) - } - - #[jsonrpc_method("textDocument/publishDiagnostics", kind = "notification")] - pub async fn publish_diagnostics(&self, params: PublishDiagnosticsParams) { - let _ = self - .diagnostics_by_uri - .insert(params.uri.into(), params.diagnostics); - } - - #[jsonrpc_method("$/progress", kind = "notification")] - pub async fn progress(&self, params: ProgressParams) { - self.progress_buf.push(params); - } - - #[jsonrpc_method("window/workDoneProgress/create", kind = "request")] - pub async fn work_done_progress_create( - &self, - params: WorkDoneProgressCreateParams, - ) -> Result<()> { - self.work_done_progress_create_buf.push(params); - Ok(()) - } - - #[jsonrpc_method("window/logMessage", kind = "notification")] - pub async fn log_message(&self, params: LogMessageParams) { - self.log_message_buf.push(params); - } -} - -#[async_trait] -impl Middleware for TestLatexLspServer { - async fn before_message(&self) {} - - async fn after_message(&self) {} -} diff --git a/support/texlab/src/tex/compile.rs b/support/texlab/src/tex/compile.rs deleted file mode 100644 index a2b0d2f37d..0000000000 --- a/support/texlab/src/tex/compile.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::{io, process::Stdio, time::Duration}; -use tempfile::{tempdir, TempDir}; -use thiserror::Error; -use tokio::{ - fs, - process::Command, - time::{timeout, Elapsed}, -}; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum Format { - Latex, - Pdflatex, - Xelatex, - Lualatex, -} - -impl Format { - pub fn executable(self) -> &'static str { - match self { - Self::Latex => "latex", - Self::Pdflatex => "pdflatex", - Self::Xelatex => "xelatex", - Self::Lualatex => "lualatex", - } - } -} - -#[derive(Debug)] -pub struct Artifacts { - pub dir: TempDir, - pub log: String, -} - -#[derive(Debug, Error)] -pub enum CompileError { - #[error("an I/O error occurred: `{0}`")] - IO(#[from] io::Error), - #[error("TeX engine is not installed")] - NotInstalled, - #[error("build timeout: `{0}`")] - Timeout(#[from] Elapsed), -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct CompileParams<'a> { - pub format: Format, - pub file_name: &'a str, - pub code: &'a str, - pub timeout: Duration, -} - -impl<'a> Default for CompileParams<'a> { - fn default() -> Self { - Self { - format: Format::Lualatex, - file_name: "code.tex", - code: "", - timeout: Duration::from_secs(15), - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Compiler<'a> { - pub executable: &'a str, - pub args: &'a [&'a str], - pub file_name: &'a str, - pub timeout: Duration, -} - -impl<'a> Compiler<'a> { - pub async fn compile<'b>(&'a self, code: &'b str) -> Result<Artifacts, CompileError> { - let directory = tempdir()?; - let tex_file = directory.path().join(self.file_name); - fs::write(&tex_file, code).await?; - - let child = Command::new(self.executable) - .args(self.args) - .current_dir(&directory) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - - timeout(self.timeout, child) - .await? - .map_err(|_| CompileError::NotInstalled)?; - - let log_file = tex_file.with_extension("log"); - let log_bytes = fs::read(log_file).await?; - let log = String::from_utf8_lossy(&log_bytes).into_owned(); - Ok(Artifacts { - dir: directory, - log, - }) - } -} diff --git a/support/texlab/src/tex/kpsewhich.rs b/support/texlab/src/tex/kpsewhich.rs deleted file mode 100644 index cb2ab89de5..0000000000 --- a/support/texlab/src/tex/kpsewhich.rs +++ /dev/null @@ -1,112 +0,0 @@ -use super::Language; -use futures::Future; -use std::{ - collections::HashMap, - env, - ffi::OsStr, - io, - path::{Path, PathBuf}, - string::FromUtf8Error, -}; -use thiserror::Error; -use tokio::{fs, process::Command}; - -#[derive(Debug, Error)] -pub enum KpsewhichError { - #[error("an I/O error occurred: `{0}`")] - IO(#[from] io::Error), - #[error("an utf8 error occurred: `{0}`")] - Decode(#[from] FromUtf8Error), - #[error("invalid output from kpsewhich")] - InvalidOutput, - #[error("kpsewhich not installed")] - NotInstalled, - #[error("no kpsewhich database")] - NoDatabase, - #[error("corrupt kpsewhich database")] - CorruptDatabase, -} - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct Resolver { - pub files_by_name: HashMap<String, PathBuf>, -} - -impl Resolver { - pub fn new(files_by_name: HashMap<String, PathBuf>) -> Self { - Self { files_by_name } - } -} - -pub async fn parse_database<'a, R, F>( - root_directories: &'a [PathBuf], - reader: R, -) -> Result<Resolver, KpsewhichError> -where - R: Fn(&'a Path) -> F, - F: Future<Output = Result<Vec<PathBuf>, KpsewhichError>>, -{ - let mut files_by_name = HashMap::new(); - for directory in root_directories { - for path in reader(directory).await? { - if is_tex_file(&path) { - if let Some(path) = make_absolute(root_directories, &path).await { - if let Some(name) = path - .file_name() - .and_then(OsStr::to_str) - .map(ToString::to_string) - { - files_by_name.insert(name, path); - } - } - } - } - } - Ok(Resolver::new(files_by_name)) -} - -fn is_tex_file(path: &Path) -> bool { - path.extension() - .and_then(OsStr::to_str) - .and_then(Language::by_extension) - .is_some() -} - -async fn make_absolute(root_directories: &[PathBuf], relative_path: &Path) -> Option<PathBuf> { - for dir in root_directories.iter().rev() { - if let Ok(path) = fs::canonicalize(dir.join(&relative_path)).await { - return Some(path); - } - } - None -} - -pub async fn root_directories() -> Result<Vec<PathBuf>, KpsewhichError> { - let texmf = run(&["-var-value", "TEXMF"]).await?; - let expand_arg = format!("--expand-braces={}", texmf); - let expanded = run(&[&expand_arg]).await?; - let directories = env::split_paths(&expanded.replace("!", "")) - .filter(|path| path.exists()) - .collect(); - Ok(directories) -} - -async fn run<I, S>(args: I) -> Result<String, KpsewhichError> -where - I: IntoIterator<Item = S>, - S: AsRef<OsStr>, -{ - let output = Command::new("kpsewhich") - .args(args) - .output() - .await - .map_err(|_| KpsewhichError::NotInstalled)?; - - let result = String::from_utf8(output.stdout)? - .lines() - .next() - .ok_or(KpsewhichError::InvalidOutput)? - .into(); - - Ok(result) -} diff --git a/support/texlab/src/tex/mod.rs b/support/texlab/src/tex/mod.rs deleted file mode 100644 index 59318f9d22..0000000000 --- a/support/texlab/src/tex/mod.rs +++ /dev/null @@ -1,154 +0,0 @@ -mod compile; -mod kpsewhich; -mod miktex; -mod tectonic; -mod texlive; - -pub use self::{ - compile::{Artifacts, CompileError, CompileParams, Format}, - kpsewhich::{KpsewhichError, Resolver}, -}; - -use self::{compile::Compiler, miktex::Miktex, tectonic::Tectonic, texlive::Texlive}; -use async_trait::async_trait; -use std::{fmt, process::Stdio, sync::Arc}; -use tokio::process::Command; - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum DistributionKind { - Texlive, - Miktex, - Tectonic, - Unknown, -} - -impl fmt::Display for DistributionKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match self { - Self::Texlive => "TeXLive", - Self::Miktex => "MikTeX", - Self::Tectonic => "Tectonic", - Self::Unknown => "Unknown", - }; - write!(f, "{}", name) - } -} - -impl DistributionKind { - pub async fn detect() -> Self { - if Command::new("tectonic") - .arg("--version") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .await - .is_ok() - { - return Self::Tectonic; - } - - match Command::new("latex").arg("--version").output().await { - Ok(output) => { - let stdout = String::from_utf8_lossy(&output.stdout); - if stdout.contains("TeX Live") { - Self::Texlive - } else if stdout.contains("MiKTeX") { - Self::Miktex - } else { - Self::Unknown - } - } - Err(_) => Self::Unknown, - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum Language { - Latex, - Bibtex, -} - -impl Language { - pub fn by_extension(extension: &str) -> Option<Self> { - match extension.to_lowercase().as_str() { - "tex" | "sty" | "cls" | "def" | "lco" | "aux" | "rnw" => Some(Language::Latex), - "bib" | "bibtex" => Some(Language::Bibtex), - _ => None, - } - } - - pub fn by_language_id(language_id: &str) -> Option<Self> { - match language_id { - "latex" | "tex" => Some(Language::Latex), - "bibtex" | "bib" => Some(Language::Bibtex), - _ => None, - } - } -} - -#[async_trait] -pub trait Distribution: Send + Sync { - fn kind(&self) -> DistributionKind; - - async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError>; - - async fn load(&self) -> Result<(), KpsewhichError>; - - async fn resolver(&self) -> Arc<Resolver>; -} - -impl dyn Distribution { - pub async fn detect() -> Arc<dyn Distribution> { - let kind = DistributionKind::detect().await; - let distro: Arc<dyn Distribution + Send + Sync> = match kind { - DistributionKind::Texlive => Arc::new(Texlive::default()), - DistributionKind::Miktex => Arc::new(Miktex::default()), - DistributionKind::Tectonic => Arc::new(Tectonic::default()), - DistributionKind::Unknown => Arc::new(UnknownDistribution::default()), - }; - distro - } -} - -impl fmt::Debug for dyn Distribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.kind()) - } -} - -async fn compile(params: CompileParams<'_>) -> Result<Artifacts, CompileError> { - let executable = params.format.executable(); - let args = &["--interaction=batchmode", "-shell-escape", params.file_name]; - let compiler = Compiler { - executable, - args, - file_name: params.file_name, - timeout: params.timeout, - }; - compiler.compile(params.code).await -} - -#[derive(Debug, Default)] -pub struct UnknownDistribution { - resolver: Arc<Resolver>, -} - -#[async_trait] -impl Distribution for UnknownDistribution { - fn kind(&self) -> DistributionKind { - DistributionKind::Unknown - } - - async fn compile<'a>(&'a self, _params: CompileParams<'a>) -> Result<Artifacts, CompileError> { - Err(CompileError::NotInstalled) - } - - async fn load(&self) -> Result<(), KpsewhichError> { - Ok(()) - } - - async fn resolver(&self) -> Arc<Resolver> { - Arc::clone(&self.resolver) - } -} diff --git a/support/texlab/src/tex/tectonic.rs b/support/texlab/src/tex/tectonic.rs deleted file mode 100644 index 1c19466830..0000000000 --- a/support/texlab/src/tex/tectonic.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::{ - compile::{Artifacts, CompileError, CompileParams, Compiler}, - kpsewhich::{KpsewhichError, Resolver}, - Distribution, DistributionKind, -}; -use async_trait::async_trait; -use std::sync::Arc; - -#[derive(Debug, Default)] -pub struct Tectonic; - -#[async_trait] -impl Distribution for Tectonic { - fn kind(&self) -> DistributionKind { - DistributionKind::Tectonic - } - - async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> { - let args = [params.file_name]; - let compiler = Compiler { - executable: "tectonic", - args: &args, - file_name: params.file_name, - timeout: params.timeout, - }; - compiler.compile(params.code).await - } - - async fn load(&self) -> Result<(), KpsewhichError> { - Ok(()) - } - - async fn resolver(&self) -> Arc<Resolver> { - Arc::new(Resolver::default()) - } -} diff --git a/support/texlab/src/tex/texlive.rs b/support/texlab/src/tex/texlive.rs deleted file mode 100644 index 5afb763d9e..0000000000 --- a/support/texlab/src/tex/texlive.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::{ - compile, - kpsewhich::{self, KpsewhichError, Resolver}, - Artifacts, CompileError, CompileParams, Distribution, DistributionKind, -}; -use async_trait::async_trait; -use futures::lock::Mutex; -use std::{ - io, - path::{Path, PathBuf}, - str::Lines, - sync::Arc, -}; -use tokio::fs; - -#[derive(Debug, Default)] -pub struct Texlive { - resolver: Mutex<Arc<Resolver>>, -} - -#[async_trait] -impl Distribution for Texlive { - fn kind(&self) -> DistributionKind { - DistributionKind::Texlive - } - - async fn compile<'a>(&'a self, params: CompileParams<'a>) -> Result<Artifacts, CompileError> { - compile(params).await - } - - async fn load(&self) -> Result<(), KpsewhichError> { - let root_directories = kpsewhich::root_directories().await?; - let resolver = kpsewhich::parse_database(&root_directories, read_database).await?; - *self.resolver.lock().await = Arc::new(resolver); - Ok(()) - } - - async fn resolver(&self) -> Arc<Resolver> { - let resolver = self.resolver.lock().await; - Arc::clone(&resolver) - } -} - -const DATABASE_PATH: &str = "ls-R"; - -async fn read_database(directory: &Path) -> Result<Vec<PathBuf>, KpsewhichError> { - let file = directory.join(DATABASE_PATH); - if !file.is_file() { - return Ok(Vec::new()); - } - - let text = fs::read_to_string(file) - .await - .map_err(|_| KpsewhichError::NoDatabase)?; - parse_database(text.lines()).map_err(|_| KpsewhichError::CorruptDatabase) -} - -fn parse_database(lines: Lines) -> io::Result<Vec<PathBuf>> { - let mut paths = Vec::new(); - let mut directory = ""; - - for line in lines.filter(|x| !x.trim().is_empty() && !x.starts_with('%')) { - if line.ends_with(':') { - directory = &line[..line.len() - 1]; - } else { - let path = PathBuf::from(directory).join(line); - paths.push(path); - } - } - Ok(paths) -} diff --git a/support/texlab/src/protocol/uri.rs b/support/texlab/src/uri.rs index e8dd1ba160..b1ae9073e7 100644 --- a/support/texlab/src/protocol/uri.rs +++ b/support/texlab/src/uri.rs @@ -1,11 +1,11 @@ -use lsp_types::{TextDocumentIdentifier, TextDocumentPositionParams}; -use serde::{Deserialize, Serialize}; use std::{ fmt, hash::{Hash, Hasher}, ops::Deref, path::Path, }; + +use serde::{Deserialize, Serialize}; use url::{ParseError, Url}; #[derive(Eq, Clone, Serialize, Deserialize)] @@ -27,6 +27,10 @@ impl Uri { Url::parse(input).map(|url| url.into()) } + pub fn from_directory_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> { + Url::from_directory_path(path).map(|url| url.into()) + } + pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> { Url::from_file_path(path).map(|url| url.into()) } @@ -79,19 +83,3 @@ impl fmt::Display for Uri { self.0.fmt(f) } } - -pub trait AsUri { - fn as_uri(&self) -> Uri; -} - -impl AsUri for TextDocumentIdentifier { - fn as_uri(&self) -> Uri { - self.uri.clone().into() - } -} - -impl AsUri for TextDocumentPositionParams { - fn as_uri(&self) -> Uri { - self.text_document.as_uri() - } -} diff --git a/support/texlab/src/workspace.rs b/support/texlab/src/workspace.rs index c3c2551325..12924e2cf4 100644 --- a/support/texlab/src/workspace.rs +++ b/support/texlab/src/workspace.rs @@ -1,964 +1,32 @@ -use crate::{ - components::COMPONENT_DATABASE, - protocol::{Options, TextDocumentItem, Uri}, - syntax::{bibtex, latex, LatexIncludeKind}, - tex::{Distribution, Language, Resolver}, -}; -use futures::lock::Mutex; -use log::{debug, error, warn}; -use petgraph::{graph::Graph, visit::Dfs}; -use std::{ - collections::HashMap, - ffi::OsStr, - hash::{Hash, Hasher}, - io, - path::{Path, PathBuf}, - sync::Arc, - time::SystemTime, -}; -use thiserror::Error; -use tokio::fs; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct DocumentParams<'a> { - pub uri: Uri, - pub text: String, - pub language: Language, - pub resolver: &'a Resolver, - pub options: &'a Options, - pub current_dir: &'a Path, -} - -#[derive(Debug, Clone)] -pub enum DocumentContent { - Latex(Box<latex::SymbolTable>), - Bibtex(Box<bibtex::Tree>), -} - -impl DocumentContent { - pub fn as_latex(&self) -> Option<&latex::SymbolTable> { - if let Self::Latex(table) = self { - Some(&table) - } else { - None - } - } - - pub fn as_bibtex(&self) -> Option<&bibtex::Tree> { - if let Self::Bibtex(tree) = self { - Some(&tree) - } else { - None - } - } -} - -#[derive(Debug, Clone)] -pub struct Document { - pub uri: Uri, - pub text: String, - pub content: DocumentContent, - pub modified: SystemTime, -} - -impl Document { - pub fn is_file(&self) -> bool { - self.uri.scheme() == "file" - } - - pub fn open(params: DocumentParams) -> Self { - let DocumentParams { - uri, - text, - language, - resolver, - options, - current_dir, - } = params; - - let content = match language { - Language::Latex => { - let table = latex::open(latex::OpenParams { - uri: &uri, - text: &text, - resolver, - options, - current_dir, - }); - DocumentContent::Latex(Box::new(table)) - } - Language::Bibtex => { - let tree = bibtex::open(&text); - DocumentContent::Bibtex(Box::new(tree)) - } - }; - - Self { - uri, - text, - content, - modified: SystemTime::now(), - } - } -} - -impl PartialEq for Document { - fn eq(&self, other: &Self) -> bool { - self.uri == other.uri - } -} - -impl Eq for Document {} - -impl Hash for Document { - fn hash<H: Hasher>(&self, state: &mut H) { - self.uri.hash(state); - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct Snapshot(pub Vec<Arc<Document>>); - -impl Snapshot { - pub fn new() -> Self { - Self(Vec::new()) - } - - pub fn push(&mut self, doc: Document) { - self.0.push(Arc::new(doc)); - } - - pub fn find(&self, uri: &Uri) -> Option<Arc<Document>> { - self.0.iter().find(|doc| doc.uri == *uri).map(Arc::clone) - } - - pub fn relations( - &self, - uri: &Uri, - options: &Options, - current_dir: &Path, - ) -> Vec<Arc<Document>> { - let mut graph = Graph::new_undirected(); - let mut indices_by_uri = HashMap::new(); - for document in &self.0 { - indices_by_uri.insert(&document.uri, graph.add_node(document)); - } - - for parent in &self.0 { - if let DocumentContent::Latex(table) = &parent.content { - table - .includes - .iter() - .flat_map(|include| include.all_targets.iter()) - .filter_map(|targets| targets.iter().find_map(|target| self.find(target))) - .for_each(|child| { - graph.add_edge(indices_by_uri[&parent.uri], indices_by_uri[&child.uri], ()); - }); - - table - .imports - .iter() - .flat_map(|import| import.targets.iter()) - .find_map(|target| self.find(target)) - .into_iter() - .for_each(|child| { - graph.add_edge(indices_by_uri[&parent.uri], indices_by_uri[&child.uri], ()); - }); - - self.resolve_aux_targets(&parent.uri, options, current_dir, "aux") - .into_iter() - .flatten() - .find_map(|target| self.find(&target)) - .into_iter() - .for_each(|child| { - graph.add_edge(indices_by_uri[&parent.uri], indices_by_uri[&child.uri], ()); - }); - } - } - - let mut documents = Vec::new(); - if self.find(uri).is_some() { - let mut dfs = Dfs::new(&graph, indices_by_uri[uri]); - while let Some(index) = dfs.next(&graph) { - documents.push(Arc::clone(&graph[index])); - } - } - documents - } - - pub fn parent( - &self, - uri: &Uri, - options: &Options, - current_dir: &Path, - ) -> Option<Arc<Document>> { - for doc in self.relations(uri, options, current_dir) { - if let DocumentContent::Latex(table) = &doc.content { - if table.is_standalone { - return Some(doc); - } - } - } - None - } - - pub fn parent_subfile( - &self, - uri: &Uri, - options: &Options, - current_dir: &Path, - ) -> Option<Arc<Document>> { - for doc in self.relations(uri, options, current_dir) { - if let DocumentContent::Latex(table) = &doc.content { - if table.is_standalone - && !table.components.iter().any(|comp| comp == "subfiles.cls") - { - return Some(doc); - } - } - } - None - } - - pub fn expand(&self, options: &Options, current_dir: &Path) -> Vec<Uri> { - let mut unknown_targets = Vec::new(); - for parent in &self.0 { - if let DocumentContent::Latex(table) = &parent.content { - table - .includes - .iter() - .filter(|include| Self::should_expand_include(&table, include)) - .flat_map(|include| include.all_targets.iter()) - .filter(|targets| targets.iter().all(|target| self.find(target).is_none())) - .flatten() - .for_each(|target| unknown_targets.push(target.clone())); - - table - .imports - .iter() - .filter(|import| { - import - .targets - .iter() - .all(|target| self.find(target).is_none()) - }) - .flat_map(|import| import.targets.iter()) - .for_each(|target| unknown_targets.push(target.clone())); - - self.resolve_aux_targets(&parent.uri, options, current_dir, "aux") - .into_iter() - .filter(|targets| targets.iter().all(|target| self.find(target).is_none())) - .flatten() - .for_each(|target| unknown_targets.push(target)); - } - } - unknown_targets - } - - fn should_expand_include(tree: &latex::Tree, include: &latex::Include) -> bool { - match include.kind { - LatexIncludeKind::Bibliography | LatexIncludeKind::Latex => true, - LatexIncludeKind::Everything - | LatexIncludeKind::Image - | LatexIncludeKind::Pdf - | LatexIncludeKind::Svg => false, - LatexIncludeKind::Package | LatexIncludeKind::Class => !include - .paths(tree) - .into_iter() - .all(|name| COMPONENT_DATABASE.contains(name.text())), - } - } - - pub fn resolve_aux_targets( - &self, - tex_uri: &Uri, - options: &Options, - current_dir: &Path, - extension: &str, - ) -> Option<Vec<Uri>> { - let mut targets = Vec::new(); - targets.push(tex_uri.with_extension(extension)?); - if tex_uri.scheme() == "file" { - let tex_path = tex_uri.to_file_path().ok()?; - let file_stem = tex_path.file_stem()?; - let aux_name = format!("{}.{}", file_stem.to_str()?, extension); - - if let Some(root_dir) = options - .latex - .as_ref() - .and_then(|opts| opts.root_directory.as_ref()) - { - let path = current_dir.join(root_dir).join(&aux_name); - targets.push(Uri::from_file_path(path).ok()?); - } - - if let Some(build_dir) = options - .latex - .as_ref() - .and_then(|opts| opts.build.as_ref()) - .and_then(|opts| opts.output_directory.as_ref()) - { - let path = current_dir.join(build_dir).join(&aux_name); - targets.push(Uri::from_file_path(path).ok()?); - } - } - Some(targets) - } -} - -#[derive(Debug, Error)] -pub enum WorkspaceLoadError { - #[error("invalid language id")] - UnknownLanguage, - #[error("invalid file path")] - InvalidPath, - #[error("an I/O error occurred: `{0}`")] - IO(#[from] io::Error), -} - -pub struct Workspace { - distro: Arc<dyn Distribution>, - current_dir: Arc<PathBuf>, - snapshot: Mutex<Arc<Snapshot>>, -} - -impl Workspace { - pub fn new(distro: Arc<dyn Distribution>, current_dir: Arc<PathBuf>) -> Self { - Self { - distro, - current_dir, - snapshot: Mutex::default(), - } - } - - pub async fn get(&self) -> Arc<Snapshot> { - let snapshot = self.snapshot.lock().await; - Arc::clone(&snapshot) - } - - pub async fn add(&self, document: TextDocumentItem, options: &Options) { - let language = match Language::by_language_id(&document.language_id) { - Some(language) => language, - None => { - error!( - "Invalid language id: {} ({})", - &document.language_id, &document.uri, - ); - return; - } - }; - - debug!("Adding document: {}", document.uri); - let mut snapshot = self.snapshot.lock().await; - *snapshot = self - .add_or_update( - &snapshot, - document.uri.into(), - document.text, - language, - options, - ) - .await; - } - - pub async fn load(&self, path: &Path, options: &Options) -> Result<(), WorkspaceLoadError> { - let language = match path - .extension() - .and_then(OsStr::to_str) - .and_then(Language::by_extension) - { - Some(language) => language, - None => { - warn!("Could not determine language: {}", path.to_string_lossy()); - return Err(WorkspaceLoadError::UnknownLanguage); - } - }; - - let uri = match Uri::from_file_path(path) { - Ok(uri) => uri, - Err(_) => { - error!("Invalid path: {}", path.to_string_lossy()); - return Err(WorkspaceLoadError::InvalidPath); - } - }; - - let text = match fs::read_to_string(path).await { - Ok(text) => text, - Err(why) => { - warn!("Could not open file: {}", uri); - return Err(WorkspaceLoadError::IO(why)); - } - }; +mod api; +mod children_expand; +mod document; +mod parent_expand; +mod storage; +mod watch; - debug!("Loading document: {}", uri); - let mut snapshot = self.snapshot.lock().await; - *snapshot = self - .add_or_update(&snapshot, uri, text, language, options) - .await; - Ok(()) - } +use std::sync::Arc; - pub async fn update(&self, uri: Uri, text: String, options: &Options) { - let mut snapshot = self.snapshot.lock().await; +use anyhow::Result; - let old_document = match snapshot.0.iter().find(|x| x.uri == uri) { - Some(document) => document, - None => { - warn!("Document not found: {}", uri); - return; - } - }; +use crate::ServerContext; - let language = match old_document.content { - DocumentContent::Latex(_) => Language::Latex, - DocumentContent::Bibtex(_) => Language::Bibtex, - }; - - *snapshot = self - .add_or_update(&snapshot, uri, text, language, options) - .await; - } - - pub async fn reparse(&self, options: &Options) { - let snapshot = self.get().await; - for doc in &snapshot.0 { - let language = match doc.content { - DocumentContent::Latex(_) => Language::Latex, - DocumentContent::Bibtex(_) => Language::Bibtex, - }; - - let mut snapshot = self.snapshot.lock().await; - debug!("Reparsing document: {}", doc.uri); - *snapshot = self - .add_or_update( - &snapshot, - doc.uri.clone(), - doc.text.clone(), - language, - options, - ) - .await; - } - } - - pub async fn detect_root(&self, uri: &Uri, options: &Options) -> io::Result<()> { - if uri.scheme() != "file" { - return Ok(()); - } - - if let Ok(mut path) = uri.to_file_path() { - while path.pop() { - let snapshot = self.get().await; - if snapshot - .parent_subfile(&uri, &options, &self.current_dir) - .is_some() - { - break; - } - - let mut entries = fs::read_dir(&path).await?; - while let Some(entry) = entries.next_entry().await? { - if entry.file_type().await?.is_file() { - let path = entry.path(); - if path - .extension() - .and_then(OsStr::to_str) - .and_then(Language::by_extension) - .is_some() - { - if let Ok(parent_uri) = Uri::from_file_path(&path) { - if snapshot.find(&parent_uri).is_none() { - let _ = self.load(&path, options).await; - } - } - } - } - } - } - } - Ok(()) - } - - pub async fn detect_children(&self, options: &Options) { - loop { - let mut changed = false; - - let snapshot = self.get().await; - for path in snapshot - .expand(&options, &self.current_dir) - .into_iter() - .filter(|uri| uri.scheme() == "file" && uri.fragment().is_none()) - .filter_map(|uri| uri.to_file_path().ok()) - { - if path.exists() { - changed |= self.load(&path, &options).await.is_ok(); - } - } - - if !changed { - break; - } - } - } - - pub async fn reparse_all_if_newer(&self, options: &Options) { - let snapshot = self.get().await; - for doc in &snapshot.0 { - if let Err(WorkspaceLoadError::IO(why)) = self.reparse_if_newer(doc, options).await { - warn!("Reparsing document {} failed: {}", doc.uri, why); - } - } - } - - async fn reparse_if_newer( - &self, - doc: &Document, - options: &Options, - ) -> Result<(), WorkspaceLoadError> { - if !doc.is_file() { - return Ok(()); - } - - if let Ok(path) = doc.uri.to_file_path() { - let data = fs::metadata(&path).await?; - if data.modified()? > doc.modified { - self.load(&path, options).await?; - } - } - Ok(()) - } - - async fn add_or_update( - &self, - snapshot: &Snapshot, - uri: Uri, - text: String, - language: Language, - options: &Options, - ) -> Arc<Snapshot> { - let resolver = self.distro.resolver().await; - let document = Document::open(DocumentParams { - uri, - text, - language, - resolver: &resolver, - options, - current_dir: &self.current_dir, - }); - - let mut documents: Vec<Arc<Document>> = snapshot - .0 - .iter() - .filter(|x| x.uri != document.uri) - .cloned() - .collect(); +pub use self::{api::*, document::*}; +use self::{ + children_expand::ChildrenExpander, parent_expand::ParentExpander, storage::Storage, + watch::DocumentWatcher, +}; - documents.push(Arc::new(document)); - Arc::new(Snapshot(documents)) - } +pub fn create_workspace_fast(context: Arc<ServerContext>) -> Result<impl Workspace> { + let workspace = Storage::new(context); + let workspace = ChildrenExpander::new(Arc::new(workspace)); + Ok(workspace) } -#[cfg(test)] -mod tests { - use super::*; - use crate::protocol::{LatexBuildOptions, LatexOptions}; - use itertools::Itertools; - use std::env; - - fn create_simple_document(uri: &Uri, language: Language, text: &str) -> Arc<Document> { - Arc::new(Document::open(DocumentParams { - uri: uri.clone(), - text: text.into(), - language, - resolver: &Resolver::default(), - options: &Options::default(), - current_dir: &env::current_dir().unwrap(), - })) - } - - #[test] - fn relations_append_missing_extension() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/bar/baz.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#"\include{bar/baz}"#), - create_simple_document(&uri2, Language::Latex, r#""#), - ]; - let actual_uris: Vec<_> = snapshot - .relations(&uri1, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri1, uri2]); - } - - #[test] - fn relations_parent_directory() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/bar/baz.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#""#), - create_simple_document(&uri2, Language::Latex, r#"\input{../foo.tex}"#), - ]; - let actual_uris: Vec<_> = snapshot - .relations(&uri1, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri1, uri2]); - } - - #[test] - fn relations_invalid_include() { - let uri = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![create_simple_document( - &uri, - Language::Latex, - r#"\include{<foo>?|bar|:}"#, - )]; - let actual_uris: Vec<_> = snapshot - .relations(&uri, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri]); - } - - #[test] - fn relations_bibliography() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/bar.bib").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#"\addbibresource{bar.bib}"#), - create_simple_document(&uri2, Language::Bibtex, r#""#), - ]; - let actual_uris: Vec<_> = snapshot - .relations(&uri2, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri2, uri1]); - } - - #[test] - fn relations_unknown_include() { - let uri = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![create_simple_document( - &uri, - Language::Latex, - r#"\input{bar.tex}"#, - )]; - let actual_uris: Vec<_> = snapshot - .relations(&uri, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri]); - } - - #[test] - fn relations_include_cycle() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/bar.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#"\include{bar}"#), - create_simple_document(&uri2, Language::Latex, r#"\input{foo.tex}"#), - ]; - let actual_uris: Vec<_> = snapshot - .relations(&uri1, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri1, uri2]); - } - - #[test] - fn relations_same_parent() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/bar.tex").unwrap(); - let uri3 = Uri::parse("http://www.example.com/baz.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#"\input{bar.tex}\input{baz.tex}"#), - create_simple_document(&uri2, Language::Latex, r#""#), - create_simple_document(&uri3, Language::Latex, r#""#), - ]; - let actual_uris: Vec<_> = snapshot - .relations(&uri3, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri3, uri1, uri2]); - } - - #[test] - fn relations_aux_default_options() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/foo.aux").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#""#), - create_simple_document(&uri2, Language::Latex, r#""#), - ]; - let actual_uris: Vec<_> = snapshot - .relations(&uri1, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri1, uri2]); - } - - #[test] - fn relations_aux_output_directory() { - let cwd = env::current_dir().unwrap(); - let options = Options { - latex: Some(LatexOptions { - build: Some(LatexBuildOptions { - output_directory: Some(PathBuf::from("build")), - ..LatexBuildOptions::default() - }), - ..LatexOptions::default() - }), - ..Options::default() - }; - - let uri1 = Uri::from_file_path(cwd.join("foo.tex")).unwrap(); - let uri2 = Uri::from_file_path(cwd.join("build/foo.aux")).unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - Arc::new(Document::open(DocumentParams { - uri: uri1.clone(), - text: String::new(), - language: Language::Latex, - resolver: &Resolver::default(), - options: &options, - current_dir: &cwd, - })), - Arc::new(Document::open(DocumentParams { - uri: uri2.clone(), - text: String::new(), - language: Language::Latex, - resolver: &Resolver::default(), - options: &options, - current_dir: &cwd, - })), - ]; - let actual_uris: Vec<_> = snapshot - .relations(&uri1, &options, &cwd) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri1, uri2]); - } - - #[test] - fn relations_aux_root_directory() { - let cwd = env::current_dir().unwrap(); - let options = Options { - latex: Some(LatexOptions { - root_directory: Some(PathBuf::from(".")), - ..LatexOptions::default() - }), - ..Options::default() - }; - - let uri1 = Uri::from_file_path(cwd.join("src/foo.tex")).unwrap(); - let uri2 = Uri::from_file_path(cwd.join("foo.aux")).unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - Arc::new(Document::open(DocumentParams { - uri: uri1.clone(), - text: String::new(), - language: Language::Latex, - resolver: &Resolver::default(), - options: &options, - current_dir: &cwd, - })), - Arc::new(Document::open(DocumentParams { - uri: uri2.clone(), - text: String::new(), - language: Language::Latex, - resolver: &Resolver::default(), - options: &options, - current_dir: &cwd, - })), - ]; - let actual_uris: Vec<_> = snapshot - .relations(&uri1, &options, &cwd) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri1, uri2]); - } - - #[test] - fn relations_import() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/bar/baz.tex").unwrap(); - let uri3 = Uri::parse("http://www.example.com/bar/qux/foo-bar.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#"\import{bar/}{baz.tex}"#), - create_simple_document(&uri2, Language::Latex, r#"\subimport{qux/}{foo-bar}"#), - create_simple_document(&uri3, Language::Latex, r#""#), - ]; - - let actual_uris: Vec<_> = snapshot - .relations(&uri1, &Options::default(), &env::current_dir().unwrap()) - .into_iter() - .map(|doc| doc.uri.clone()) - .collect(); - - assert_eq!(actual_uris, vec![uri1, uri2, uri3]); - } - - #[test] - fn parent() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/bar.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#""#), - create_simple_document( - &uri2, - Language::Latex, - r#"\begin{document}\include{foo}\end{document}"#, - ), - ]; - let doc = snapshot - .parent(&uri1, &Options::default(), &env::current_dir().unwrap()) - .unwrap(); - assert_eq!(doc.uri, uri2); - } - - #[test] - fn parent_nothing_found() { - let uri1 = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/bar.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document(&uri1, Language::Latex, r#""#), - create_simple_document(&uri2, Language::Latex, r#"\begin{document}\end{document}"#), - ]; - let doc = snapshot.parent(&uri1, &Options::default(), &env::current_dir().unwrap()); - assert_eq!(doc, None); - } - - #[test] - fn expand_aux_file() { - let uri = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![create_simple_document(&uri, Language::Latex, r#""#)]; - let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap()); - assert_eq!( - expansion - .iter() - .map(|uri| uri.as_str()) - .filter(|uri| uri.ends_with(".aux")) - .collect_vec(), - vec!["http://www.example.com/foo.aux"] - ); - } - - #[test] - fn expand_local_package() { - let uri = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![create_simple_document( - &uri, - Language::Latex, - r#"\usepackage{foo-bar-baz}"#, - )]; - let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap()); - - assert_eq!( - expansion - .iter() - .map(|uri| uri.as_str()) - .filter(|uri| uri.ends_with(".sty")) - .collect_vec(), - vec!["http://www.example.com/foo-bar-baz.sty"] - ); - } - - #[test] - fn expand_system_package() { - let uri = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![create_simple_document( - &uri, - Language::Latex, - r#"\usepackage{amsmath}"#, - )]; - let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap()); - - assert_eq!( - expansion - .iter() - .map(|uri| uri.as_str()) - .filter(|uri| uri.ends_with(".sty")) - .collect_vec(), - Vec::<&str>::new() - ); - } - - #[test] - fn expand_subdirectory() { - let uri = Uri::parse("http://www.example.com/foo.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![create_simple_document( - &uri, - Language::Latex, - r#"\include{bar/baz}"#, - )]; - let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap()); - assert_eq!( - expansion - .iter() - .map(|uri| uri.as_str()) - .filter(|uri| uri.ends_with(".tex")) - .collect_vec(), - vec!["http://www.example.com/bar/baz.tex"] - ); - } - - #[test] - fn expand_import() { - let uri1 = Uri::parse("http://www.example.com/qux/foo.tex").unwrap(); - let uri2 = Uri::parse("http://www.example.com/qux/baz/bar.tex").unwrap(); - let mut snapshot = Snapshot::new(); - snapshot.0 = vec![ - create_simple_document( - &uri1, - Language::Latex, - r#"\import{.}{foo}\import{baz/}{bar}\import{baz/foo-bar/}{qux}"#, - ), - create_simple_document(&uri2, Language::Latex, r#""#), - ]; - let expansion = snapshot.expand(&Options::default(), &env::current_dir().unwrap()); - assert_eq!( - expansion - .iter() - .map(|uri| uri.as_str()) - .filter(|uri| uri.ends_with(".tex")) - .collect_vec(), - vec!["http://www.example.com/qux/baz/foo-bar/qux.tex"] - ) - } +pub fn create_workspace_full(context: Arc<ServerContext>) -> Result<impl Workspace> { + let workspace = Storage::new(context); + let workspace = DocumentWatcher::new(Arc::new(workspace))?; + let workspace = ParentExpander::new(workspace); + let workspace = ChildrenExpander::new(Arc::new(workspace)); + Ok(workspace) } diff --git a/support/texlab/src/workspace/api.rs b/support/texlab/src/workspace/api.rs new file mode 100644 index 0000000000..38fa0ce530 --- /dev/null +++ b/support/texlab/src/workspace/api.rs @@ -0,0 +1,86 @@ +use std::{fs, path::PathBuf, sync::Arc}; + +use anyhow::Result; + +use crate::{DocumentLanguage, Uri}; + +use super::Document; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum WorkspaceSource { + Client, + Server, +} + +#[derive(Debug, Clone)] +pub struct WorkspaceSubset { + pub documents: Vec<Arc<Document>>, +} + +pub type OpenHandler = Arc<dyn Fn(Arc<dyn Workspace>, Arc<Document>) + Send + Sync + 'static>; + +pub trait Workspace: Send + Sync { + fn open( + &self, + uri: Arc<Uri>, + text: String, + language: DocumentLanguage, + source: WorkspaceSource, + ) -> Arc<Document>; + + fn register_open_handler(&self, handler: OpenHandler); + + fn reload(&self, path: PathBuf) -> Result<Option<Arc<Document>>> { + let uri = Arc::new(Uri::from_file_path(path.clone()).unwrap()); + + if self.is_open(&uri) { + return Ok(self.get(&uri)); + } + + let data = fs::read(&path)?; + let text = String::from_utf8_lossy(&data).into_owned(); + if let Some(language) = DocumentLanguage::by_path(&path) { + Ok(Some(self.open( + uri, + text, + language, + WorkspaceSource::Server, + ))) + } else { + Ok(None) + } + } + + fn load(&self, path: PathBuf) -> Result<Option<Arc<Document>>> { + let uri = Arc::new(Uri::from_file_path(path.clone()).unwrap()); + + if let Some(document) = self.get(&uri) { + return Ok(Some(document)); + } + + let data = fs::read(&path)?; + let text = String::from_utf8_lossy(&data).into_owned(); + if let Some(language) = DocumentLanguage::by_path(&path) { + Ok(Some(self.open( + uri, + text, + language, + WorkspaceSource::Server, + ))) + } else { + Ok(None) + } + } + + fn documents(&self) -> Vec<Arc<Document>>; + + fn has(&self, uri: &Uri) -> bool; + + fn get(&self, uri: &Uri) -> Option<Arc<Document>>; + + fn close(&self, uri: &Uri); + + fn is_open(&self, uri: &Uri) -> bool; + + fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset>; +} diff --git a/support/texlab/src/workspace/children_expand.rs b/support/texlab/src/workspace/children_expand.rs new file mode 100644 index 0000000000..006e7b17e2 --- /dev/null +++ b/support/texlab/src/workspace/children_expand.rs @@ -0,0 +1,92 @@ +use std::sync::Arc; + +use rayon::iter::{IntoParallelIterator, ParallelIterator}; + +use crate::{ + component_db::COMPONENT_DATABASE, Document, DocumentLanguage, OpenHandler, Uri, Workspace, + WorkspaceSource, WorkspaceSubset, +}; + +pub struct ChildrenExpander<W> { + workspace: Arc<W>, +} + +impl<W> ChildrenExpander<W> +where + W: Workspace + Send + Sync + 'static, +{ + pub fn new(workspace: Arc<W>) -> Self { + workspace.register_open_handler(Arc::new(move |workspace, document| { + Self::expand(workspace.as_ref(), document.as_ref()); + })); + Self { workspace } + } + + fn expand(workspace: &dyn Workspace, document: &Document) { + if let Some(data) = document.data.as_latex() { + let extras = &data.extras; + let mut all_targets = vec![&extras.implicit_links.aux, &extras.implicit_links.log]; + for link in &extras.explicit_links { + if link + .as_component_name() + .and_then(|name| COMPONENT_DATABASE.find(&name)) + .is_none() + { + all_targets.push(&link.targets); + } + } + + all_targets.into_par_iter().for_each(|targets| { + for path in targets + .iter() + .filter(|uri| uri.scheme() == "file" && uri.fragment().is_none()) + .filter_map(|uri| uri.to_file_path().ok()) + { + if workspace.load(path).is_ok() { + break; + } + } + }); + } + } +} + +impl<W: Workspace> Workspace for ChildrenExpander<W> { + fn open( + &self, + uri: Arc<Uri>, + text: String, + language: DocumentLanguage, + source: WorkspaceSource, + ) -> Arc<Document> { + self.workspace.open(uri, text, language, source) + } + + fn register_open_handler(&self, handler: OpenHandler) { + self.workspace.register_open_handler(handler) + } + + fn documents(&self) -> Vec<Arc<Document>> { + self.workspace.documents() + } + + fn has(&self, uri: &Uri) -> bool { + self.workspace.has(uri) + } + + fn get(&self, uri: &Uri) -> Option<Arc<Document>> { + self.workspace.get(uri) + } + + fn close(&self, uri: &Uri) { + self.workspace.close(uri) + } + + fn is_open(&self, uri: &Uri) -> bool { + self.workspace.is_open(uri) + } + + fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset> { + self.workspace.subset(uri) + } +} diff --git a/support/texlab/src/workspace/document.rs b/support/texlab/src/workspace/document.rs new file mode 100644 index 0000000000..a98be226c1 --- /dev/null +++ b/support/texlab/src/workspace/document.rs @@ -0,0 +1,127 @@ +use std::{fmt, sync::Arc}; + +use derive_more::From; + +use crate::{ + line_index::LineIndex, + syntax::{ + bibtex, build_log, + latex::{self, LatexAnalyzerContext}, + }, + DocumentLanguage, ServerContext, Uri, +}; + +#[derive(Debug, Clone)] +pub struct LatexDocumentData { + pub root: latex::SyntaxNode, + pub extras: latex::Extras, +} + +#[derive(Debug, Clone)] +pub struct BibtexDocumentData { + pub root: bibtex::SyntaxNode, +} + +#[derive(Debug, Clone, From)] +pub enum DocumentData { + Latex(LatexDocumentData), + Bibtex(BibtexDocumentData), + BuildLog(build_log::Parse), +} + +impl DocumentData { + pub fn language(&self) -> DocumentLanguage { + match self { + Self::Latex(_) => DocumentLanguage::Latex, + Self::Bibtex(_) => DocumentLanguage::Bibtex, + Self::BuildLog(_) => DocumentLanguage::BuildLog, + } + } + + pub fn as_latex(&self) -> Option<&LatexDocumentData> { + if let Self::Latex(data) = self { + Some(data) + } else { + None + } + } + + pub fn as_bibtex(&self) -> Option<&BibtexDocumentData> { + if let Self::Bibtex(data) = self { + Some(data) + } else { + None + } + } + + pub fn as_build_log(&self) -> Option<&build_log::Parse> { + if let Self::BuildLog(v) = self { + Some(v) + } else { + None + } + } +} + +#[derive(Clone)] +pub struct Document { + pub uri: Arc<Uri>, + pub text: String, + pub line_index: LineIndex, + pub data: DocumentData, +} + +impl fmt::Debug for Document { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.uri) + } +} + +impl Document { + pub fn parse( + context: Arc<ServerContext>, + uri: Arc<Uri>, + text: String, + language: DocumentLanguage, + ) -> Self { + let line_index = LineIndex::new(&text); + let data = match language { + DocumentLanguage::Latex => { + let root = latex::parse(&text).root; + + let base_uri = match &context.options.read().unwrap().root_directory { + Some(root_dir) => Uri::from_directory_path(root_dir) + .map(Arc::new) + .unwrap_or_else(|()| Arc::clone(&uri)), + None => Arc::clone(&uri), + }; + let mut context = LatexAnalyzerContext { + inner: context, + extras: latex::Extras::default(), + document_uri: Arc::clone(&uri), + base_uri, + }; + latex::analyze(&mut context, &root); + let extras = context.extras; + + LatexDocumentData { root, extras }.into() + } + DocumentLanguage::Bibtex => { + let root = bibtex::parse(&text).root; + BibtexDocumentData { root }.into() + } + DocumentLanguage::BuildLog => DocumentData::BuildLog(build_log::parse(&text)), + }; + + Self { + uri, + text, + line_index, + data, + } + } + + pub fn language(&self) -> DocumentLanguage { + self.data.language() + } +} diff --git a/support/texlab/src/workspace/parent_expand.rs b/support/texlab/src/workspace/parent_expand.rs new file mode 100644 index 0000000000..33b0ca3c5e --- /dev/null +++ b/support/texlab/src/workspace/parent_expand.rs @@ -0,0 +1,123 @@ +use std::{fs, sync::Arc}; + +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use rustc_hash::FxHashSet; + +use crate::{ + Document, DocumentLanguage, OpenHandler, Uri, Workspace, WorkspaceSource, WorkspaceSubset, +}; + +pub struct ParentExpander<W> { + workspace: W, +} + +impl<W: Workspace> ParentExpander<W> { + pub fn new(workspace: W) -> Self { + Self { workspace } + } +} + +impl<W> Workspace for ParentExpander<W> +where + W: Workspace + Send + Sync + 'static, +{ + fn open( + &self, + uri: Arc<Uri>, + text: String, + language: DocumentLanguage, + source: WorkspaceSource, + ) -> Arc<Document> { + let document = self + .workspace + .open(Arc::clone(&uri), text, language, source); + + let all_current_paths = self + .workspace + .documents() + .into_iter() + .filter_map(|doc| doc.uri.to_file_path().ok()) + .collect::<FxHashSet<_>>(); + + if uri.scheme() == "file" { + if let Ok(mut path) = uri.to_file_path() { + while path.pop() && !self.has_parent(Arc::clone(&uri)).unwrap_or(false) { + let mut files = Vec::new(); + fs::read_dir(&path) + .into_iter() + .flatten() + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().ok().filter(|ty| ty.is_file()).is_some()) + .map(|entry| entry.path()) + .filter(|path| { + matches!( + DocumentLanguage::by_path(&path), + Some(DocumentLanguage::Latex) + ) + }) + .filter(|path| !all_current_paths.contains(path)) + .for_each(|path| { + files.push(path); + }); + files.into_par_iter().for_each(|path| { + let _ = self.workspace.load(path); + }); + } + } + } + + document + } + + fn register_open_handler(&self, handler: OpenHandler) { + self.workspace.register_open_handler(handler) + } + + fn documents(&self) -> Vec<Arc<Document>> { + self.workspace.documents() + } + + fn has(&self, uri: &Uri) -> bool { + self.workspace.has(uri) + } + + fn get(&self, uri: &Uri) -> Option<Arc<Document>> { + self.workspace.get(uri) + } + + fn close(&self, uri: &Uri) { + self.workspace.close(uri) + } + + fn is_open(&self, uri: &Uri) -> bool { + self.workspace.is_open(uri) + } + + fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset> { + self.workspace.subset(uri) + } +} + +impl<W> ParentExpander<W> +where + W: Workspace + Send + Sync + 'static, +{ + fn has_parent(&self, uri: Arc<Uri>) -> Option<bool> { + let subset = self.subset(Arc::clone(&uri))?; + Some(subset.documents.iter().any(|document| { + document + .data + .as_latex() + .map(|data| { + data.extras.has_document_environment + && !data + .extras + .explicit_links + .iter() + .filter_map(|link| link.as_component_name()) + .any(|name| name == "subfiles.cls") + }) + .unwrap_or(false) + })) + } +} diff --git a/support/texlab/src/workspace/storage.rs b/support/texlab/src/workspace/storage.rs new file mode 100644 index 0000000000..964535a718 --- /dev/null +++ b/support/texlab/src/workspace/storage.rs @@ -0,0 +1,136 @@ +use std::sync::{Arc, Mutex}; + +use petgraph::{graphmap::UnGraphMap, visit::Dfs}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + Document, DocumentLanguage, OpenHandler, ServerContext, Uri, Workspace, WorkspaceSource, + WorkspaceSubset, +}; + +#[derive(Clone)] +pub struct Storage { + context: Arc<ServerContext>, + documents_by_uri: Arc<Mutex<FxHashMap<Arc<Uri>, Arc<Document>>>>, + opened_documents: Arc<Mutex<FxHashSet<Arc<Uri>>>>, + open_handlers: Arc<Mutex<Vec<OpenHandler>>>, +} + +impl Storage { + pub fn new(context: Arc<ServerContext>) -> Self { + Self { + context, + documents_by_uri: Arc::default(), + opened_documents: Arc::default(), + open_handlers: Arc::default(), + } + } +} + +impl Workspace for Storage { + fn open( + &self, + uri: Arc<Uri>, + text: String, + language: DocumentLanguage, + source: WorkspaceSource, + ) -> Arc<Document> { + log::debug!("(Re)Loading document: {}", uri); + let document = Arc::new(Document::parse( + Arc::clone(&self.context), + Arc::clone(&uri), + text, + language, + )); + { + self.documents_by_uri + .lock() + .unwrap() + .insert(Arc::clone(&uri), Arc::clone(&document)); + } + + if source == WorkspaceSource::Client { + self.opened_documents.lock().unwrap().insert(uri); + } + + let handlers = { self.open_handlers.lock().unwrap().clone() }; + for handler in handlers { + handler(Arc::new(self.clone()), Arc::clone(&document)); + } + + document + } + + fn register_open_handler(&self, handler: OpenHandler) { + self.open_handlers.lock().unwrap().push(handler); + } + + fn documents(&self) -> Vec<Arc<Document>> { + self.documents_by_uri + .lock() + .unwrap() + .values() + .cloned() + .collect() + } + + fn has(&self, uri: &Uri) -> bool { + self.documents_by_uri.lock().unwrap().contains_key(uri) + } + + fn get(&self, uri: &Uri) -> Option<Arc<Document>> { + self.documents_by_uri.lock().unwrap().get(uri).cloned() + } + + fn close(&self, uri: &Uri) { + self.opened_documents.lock().unwrap().remove(uri); + } + + fn is_open(&self, uri: &Uri) -> bool { + self.opened_documents.lock().unwrap().contains(uri) + } + + fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset> { + let all_current_uris: Vec<Arc<Uri>> = self + .documents_by_uri + .lock() + .unwrap() + .keys() + .cloned() + .collect(); + + let mut edges = Vec::new(); + for (i, uri) in all_current_uris.iter().enumerate() { + let document = self.get(uri); + if let Some(data) = document + .as_ref() + .and_then(|document| document.data.as_latex()) + { + let extras = &data.extras; + let mut all_targets = vec![&extras.implicit_links.aux, &extras.implicit_links.log]; + for link in &extras.explicit_links { + all_targets.push(&link.targets); + } + + for targets in all_targets { + for target in targets { + if let Some(j) = all_current_uris.iter().position(|uri| uri == target) { + edges.push((i, j, ())); + break; + } + } + } + } + } + + let graph = UnGraphMap::from_edges(edges); + let start = all_current_uris.iter().position(|u| *u == uri)?; + let mut dfs = Dfs::new(&graph, start); + let mut documents = Vec::new(); + while let Some(i) = dfs.next(&graph) { + documents.push(self.get(&all_current_uris[i])?); + } + + Some(WorkspaceSubset { documents }) + } +} diff --git a/support/texlab/src/workspace/watch.rs b/support/texlab/src/workspace/watch.rs new file mode 100644 index 0000000000..655872f603 --- /dev/null +++ b/support/texlab/src/workspace/watch.rs @@ -0,0 +1,107 @@ +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use anyhow::Result; +use log::warn; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use rustc_hash::FxHashSet; + +use crate::{ + Document, DocumentLanguage, OpenHandler, Uri, Workspace, WorkspaceSource, WorkspaceSubset, +}; + +pub struct DocumentWatcher<W> { + workspace: Arc<W>, + watcher: Mutex<RecommendedWatcher>, + watched_paths: Mutex<FxHashSet<PathBuf>>, +} + +impl<W> DocumentWatcher<W> +where + W: Workspace + Send + Sync + 'static, +{ + pub fn new(workspace: Arc<W>) -> Result<Self> { + let watcher = Self::create_watcher(Arc::clone(&workspace))?; + Ok(Self { + workspace, + watcher: Mutex::new(watcher), + watched_paths: Mutex::default(), + }) + } + + fn create_watcher(workspace: Arc<W>) -> Result<RecommendedWatcher> { + let watcher = Watcher::new_immediate(move |event: notify::Result<notify::Event>| { + if let Ok(event) = event { + if event.kind.is_modify() { + for path in event.paths { + let _ = workspace.reload(path); + } + } + } + })?; + Ok(watcher) + } +} + +impl<W: Workspace> Workspace for DocumentWatcher<W> { + fn open( + &self, + uri: Arc<Uri>, + text: String, + language: DocumentLanguage, + source: WorkspaceSource, + ) -> Arc<Document> { + let document = self.workspace.open(uri, text, language, source); + if document.uri.scheme() == "file" { + if let Ok(mut path) = document.uri.to_file_path() { + path.pop(); + let mut watched_paths = self.watched_paths.lock().unwrap(); + if !watched_paths.contains(&path) { + if let Err(why) = self + .watcher + .lock() + .unwrap() + .watch(&path, RecursiveMode::NonRecursive) + { + warn!( + "Failed to watch folder of document \"{}\": {}", + document.uri, why + ); + } + watched_paths.insert(path); + } + } + } + document + } + + fn register_open_handler(&self, handler: OpenHandler) { + self.workspace.register_open_handler(handler); + } + + fn documents(&self) -> Vec<Arc<Document>> { + self.workspace.documents() + } + + fn has(&self, uri: &Uri) -> bool { + self.workspace.has(uri) + } + + fn get(&self, uri: &Uri) -> Option<Arc<Document>> { + self.workspace.get(uri) + } + + fn close(&self, uri: &Uri) { + self.workspace.close(uri) + } + + fn is_open(&self, uri: &Uri) -> bool { + self.workspace.is_open(uri) + } + + fn subset(&self, uri: Arc<Uri>) -> Option<WorkspaceSubset> { + self.workspace.subset(uri) + } +} |