diff options
author | Norbert Preining <norbert@preining.info> | 2023-04-17 03:03:48 +0000 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2023-04-17 03:03:48 +0000 |
commit | 88aa9bb9a3222cf13820ae3b6f64ce48dcd003ea (patch) | |
tree | 9495d0cd2219bb309106f5330e0aeba36a675319 /support/texlab/crates | |
parent | 421b47819f21160c3662c40f7da028f15b726577 (diff) |
CTAN sync 202304170303
Diffstat (limited to 'support/texlab/crates')
37 files changed, 1097 insertions, 949 deletions
diff --git a/support/texlab/crates/base-db/src/config.rs b/support/texlab/crates/base-db/src/config.rs index 56ee5ee237..cf747d4ecd 100644 --- a/support/texlab/crates/base-db/src/config.rs +++ b/support/texlab/crates/base-db/src/config.rs @@ -1,7 +1,7 @@ use std::time::Duration; +use parser::SyntaxConfig; use regex::Regex; -use rustc_hash::FxHashSet; #[derive(Debug)] pub struct Config { @@ -12,6 +12,7 @@ pub struct Config { pub synctex: Option<SynctexConfig>, pub symbols: SymbolConfig, pub syntax: SyntaxConfig, + pub completion: CompletionConfig, } #[derive(Debug)] @@ -72,10 +73,16 @@ pub struct SymbolConfig { } #[derive(Debug)] -pub struct SyntaxConfig { - pub math_environments: FxHashSet<String>, - pub enum_environments: FxHashSet<String>, - pub verbatim_environments: FxHashSet<String>, +pub struct CompletionConfig { + pub matcher: MatchingAlgo, +} + +#[derive(Debug)] +pub enum MatchingAlgo { + Skim, + SkimIgnoreCase, + Prefix, + PrefixIgnoreCase, } impl Default for Config { @@ -88,6 +95,7 @@ impl Default for Config { synctex: None, symbols: SymbolConfig::default(), syntax: SyntaxConfig::default(), + completion: CompletionConfig::default(), } } } @@ -157,77 +165,10 @@ impl Default for SymbolConfig { } } -impl Default for SyntaxConfig { +impl Default for CompletionConfig { fn default() -> Self { - let math_environments = DEFAULT_MATH_ENVIRONMENTS - .iter() - .copied() - .map(String::from) - .collect(); - - let enum_environments = ["enumerate", "itemize", "description"] - .into_iter() - .map(String::from) - .collect(); - - let verbatim_environments = ["pycode", "minted", "asy", "lstlisting", "verbatim"] - .into_iter() - .map(String::from) - .collect(); - Self { - math_environments, - enum_environments, - verbatim_environments, + matcher: MatchingAlgo::SkimIgnoreCase, } } } - -static DEFAULT_MATH_ENVIRONMENTS: &[&str] = &[ - "align", - "align*", - "alignat", - "alignat*", - "aligned", - "aligned*", - "alignedat", - "alignedat*", - "array", - "array*", - "Bmatrix", - "Bmatrix*", - "bmatrix", - "bmatrix*", - "cases", - "cases*", - "CD", - "CD*", - "eqnarray", - "eqnarray*", - "equation", - "equation*", - "IEEEeqnarray", - "IEEEeqnarray*", - "subequations", - "subequations*", - "gather", - "gather*", - "gathered", - "gathered*", - "matrix", - "matrix*", - "multline", - "multline*", - "pmatrix", - "pmatrix*", - "smallmatrix", - "smallmatrix*", - "split", - "split*", - "subarray", - "subarray*", - "Vmatrix", - "Vmatrix*", - "vmatrix", - "vmatrix*", -]; diff --git a/support/texlab/crates/base-db/src/document.rs b/support/texlab/crates/base-db/src/document.rs index 39d9ec3cf7..c6148deaa8 100644 --- a/support/texlab/crates/base-db/src/document.rs +++ b/support/texlab/crates/base-db/src/document.rs @@ -53,7 +53,7 @@ impl Document { let diagnostics = Vec::new(); let data = match language { Language::Tex => { - let green = parser::parse_latex(&text); + let green = parser::parse_latex(&text, &config.syntax); let mut semantics = semantics::tex::Semantics::default(); semantics.process_root(&latex::SyntaxNode::new_root(green.clone())); DocumentData::Tex(TexDocumentData { green, semantics }) @@ -63,7 +63,7 @@ impl Document { DocumentData::Bib(BibDocumentData { green }) } Language::Aux => { - let green = parser::parse_latex(&text); + let green = parser::parse_latex(&text, &config.syntax); let mut semantics = semantics::auxiliary::Semantics::default(); semantics.process_root(&latex::SyntaxNode::new_root(green.clone())); DocumentData::Aux(AuxDocumentData { green, semantics }) diff --git a/support/texlab/crates/base-feature/Cargo.toml b/support/texlab/crates/base-feature/Cargo.toml new file mode 100644 index 0000000000..efd9247f78 --- /dev/null +++ b/support/texlab/crates/base-feature/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "base-feature" +version = "0.0.0" +license.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +rustc-hash = "1.1.0" +url = "2.3.1" + +[lib] +doctest = false diff --git a/support/texlab/crates/base-feature/src/lib.rs b/support/texlab/crates/base-feature/src/lib.rs new file mode 100644 index 0000000000..589ec20de4 --- /dev/null +++ b/support/texlab/crates/base-feature/src/lib.rs @@ -0,0 +1,4 @@ +mod normalize_uri; +mod placeholders; + +pub use self::{normalize_uri::normalize_uri, placeholders::*}; diff --git a/support/texlab/crates/base-feature/src/normalize_uri.rs b/support/texlab/crates/base-feature/src/normalize_uri.rs new file mode 100644 index 0000000000..041258e817 --- /dev/null +++ b/support/texlab/crates/base-feature/src/normalize_uri.rs @@ -0,0 +1,56 @@ +use url::Url; + +pub fn normalize_uri(uri: &mut Url) { + if let Some(mut segments) = uri.path_segments() { + if let Some(mut path) = segments.next().and_then(fix_drive_letter) { + for segment in segments { + path.push('/'); + path.push_str(segment); + } + + uri.set_path(&path); + } + } + + uri.set_fragment(None); +} + +fn fix_drive_letter(text: &str) -> Option<String> { + if !text.is_ascii() { + return None; + } + + match &text[1..] { + ":" => Some(text.to_ascii_uppercase()), + "%3A" | "%3a" => Some(format!("{}:", text[0..1].to_ascii_uppercase())), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use url::Url; + + use super::normalize_uri; + + #[test] + fn test_lowercase_drive_letter() { + let mut uri = Url::parse("file://c:/foo/bar.txt").unwrap(); + normalize_uri(&mut uri); + assert_eq!(uri.as_str(), "file:///C:/foo/bar.txt"); + } + + #[test] + fn test_uppercase_drive_letter() { + let mut uri = Url::parse("file://C:/foo/bar.txt").unwrap(); + normalize_uri(&mut uri); + assert_eq!(uri.as_str(), "file:///C:/foo/bar.txt"); + } + + #[test] + fn test_fragment() { + let mut uri = Url::parse("foo:///bar/baz.txt#qux").unwrap(); + normalize_uri(&mut uri); + assert_eq!(uri.as_str(), "foo:///bar/baz.txt"); + } +} diff --git a/support/texlab/crates/base-feature/src/placeholders.rs b/support/texlab/crates/base-feature/src/placeholders.rs new file mode 100644 index 0000000000..913af33edf --- /dev/null +++ b/support/texlab/crates/base-feature/src/placeholders.rs @@ -0,0 +1,50 @@ +use rustc_hash::FxHashMap; + +pub fn replace_placeholders(args: &[String], pairs: &[(char, &str)]) -> Vec<String> { + let map = FxHashMap::from_iter(pairs.iter().copied()); + args.iter() + .map(|input| { + let quoted = input + .strip_prefix('"') + .and_then(|input| input.strip_suffix('"')); + + match quoted { + Some(output) => String::from(output), + None => { + let mut output = String::new(); + let mut chars = input.chars(); + while let Some(ch) = chars.next() { + if ch == '%' { + match chars.next() { + Some(key) => match map.get(&key) { + Some(value) => output.push_str(&value), + None => output.push(key), + }, + None => output.push('%'), + }; + } else { + output.push(ch); + } + } + + output + } + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::replace_placeholders; + + #[test] + fn test_quoted() { + let output = replace_placeholders( + &["foo".into(), "\"%f\"".into(), "%%f".into(), "%fbar".into()], + &[('f', "foo")], + ); + + assert_eq!(output, vec!["foo".into(), "%f", "%f".into(), "foobar"]); + } +} diff --git a/support/texlab/crates/commands/Cargo.toml b/support/texlab/crates/commands/Cargo.toml new file mode 100644 index 0000000000..e5a6f81a95 --- /dev/null +++ b/support/texlab/crates/commands/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "commands" +version = "0.0.0" +license.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = "1.0.70" +base-db = { path = "../base-db" } +base-feature = { path = "../base-feature" } +bstr = "1.4.0" +crossbeam-channel = "0.5.8" +itertools = "0.10.5" +log = "0.4.17" +rowan = "0.15.11" +rustc-hash = "1.1.0" +syntax = { path = "../syntax" } +thiserror = "1.0.40" +url = "2.3.1" + +[lib] +doctest = false diff --git a/support/texlab/crates/commands/src/build.rs b/support/texlab/crates/commands/src/build.rs new file mode 100644 index 0000000000..9a115db888 --- /dev/null +++ b/support/texlab/crates/commands/src/build.rs @@ -0,0 +1,102 @@ +use std::{ + io::{BufReader, Read}, + path::{Path, PathBuf}, + process::{ExitStatus, Stdio}, + thread::{self, JoinHandle}, +}; + +use anyhow::Result; +use base_db::Workspace; +use base_feature::replace_placeholders; +use bstr::io::BufReadExt; +use crossbeam_channel::Sender; +use thiserror::Error; +use url::Url; + +#[derive(Debug, Error)] +pub enum BuildError { + #[error("Document \"{0}\" was not found")] + NotFound(Url), + + #[error("Document \"{0}\" does not exist on the local file system")] + NotLocal(Url), + + #[error("Unable to run compiler: {0}")] + Compile(#[from] std::io::Error), +} + +#[derive(Debug)] +pub struct BuildCommand { + program: String, + args: Vec<String>, + working_dir: PathBuf, +} + +impl BuildCommand { + pub fn new(workspace: &Workspace, uri: &Url) -> Result<Self, BuildError> { + let Some(document) = workspace.lookup(uri) else { + return Err(BuildError::NotFound(uri.clone())); + }; + + let document = workspace + .parents(document) + .into_iter() + .next() + .unwrap_or(document); + + let Some(path) = document.path.as_deref().and_then(Path::to_str) else { + return Err(BuildError::NotLocal(document.uri.clone())); + }; + + let config = &workspace.config().build; + let program = config.program.clone(); + let args = replace_placeholders(&config.args, &[('f', path)]); + + let Ok(working_dir) = workspace.current_dir(&document.dir).to_file_path() else { + return Err(BuildError::NotLocal(document.uri.clone())); + }; + + Ok(Self { + program, + args, + working_dir, + }) + } + + pub fn run(self, sender: Sender<String>) -> Result<ExitStatus, BuildError> { + log::debug!( + "Spawning compiler {} {:#?} in directory {}", + self.program, + self.args, + self.working_dir.display() + ); + + let mut process = std::process::Command::new(&self.program) + .args(self.args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(&self.working_dir) + .spawn()?; + + track_output(process.stderr.take().unwrap(), sender.clone()); + track_output(process.stdout.take().unwrap(), sender); + + let status = process.wait(); + Ok(status?) + } +} + +fn track_output( + output: impl Read + Send + 'static, + sender: Sender<String>, +) -> JoinHandle<std::io::Result<()>> { + let mut reader = BufReader::new(output); + thread::spawn(move || { + reader.for_byte_line(|line| { + let text = String::from_utf8_lossy(line).into_owned(); + let _ = sender.send(text); + Ok(true) + }) + }) +} diff --git a/support/texlab/crates/commands/src/change_env.rs b/support/texlab/crates/commands/src/change_env.rs new file mode 100644 index 0000000000..2b1c1afc35 --- /dev/null +++ b/support/texlab/crates/commands/src/change_env.rs @@ -0,0 +1,40 @@ +use base_db::Document; +use rowan::{ast::AstNode, TextRange, TextSize}; +use syntax::latex; + +#[derive(Debug)] +pub struct ChangeEnvironmentResult<'a> { + pub begin: TextRange, + pub end: TextRange, + pub old_name: String, + pub new_name: &'a str, +} + +pub fn change_environment<'a>( + document: &'a Document, + position: TextSize, + new_name: &'a str, +) -> Option<ChangeEnvironmentResult<'a>> { + let root = document.data.as_tex()?.root_node(); + + let environment = root + .token_at_offset(position) + .right_biased()? + .parent_ancestors() + .find_map(latex::Environment::cast)?; + + let begin = environment.begin()?.name()?.key()?; + let end = environment.end()?.name()?.key()?; + + let old_name = begin.to_string(); + if old_name != end.to_string() { + return None; + } + + Some(ChangeEnvironmentResult { + begin: latex::small_range(&begin), + end: latex::small_range(&end), + old_name, + new_name, + }) +} diff --git a/support/texlab/crates/commands/src/clean.rs b/support/texlab/crates/commands/src/clean.rs new file mode 100644 index 0000000000..391be79c81 --- /dev/null +++ b/support/texlab/crates/commands/src/clean.rs @@ -0,0 +1,57 @@ +use std::process::Stdio; + +use anyhow::Result; +use base_db::{Document, Workspace}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub enum CleanTarget { + Auxiliary, + Artifacts, +} + +#[derive(Debug)] +pub struct CleanCommand { + executable: String, + args: Vec<String>, +} + +impl CleanCommand { + pub fn new(workspace: &Workspace, document: &Document, target: CleanTarget) -> Result<Self> { + let Some(path) = document.path.as_deref() else { + anyhow::bail!("document '{}' is not a local file", document.uri) + }; + + let dir = workspace.current_dir(&document.dir); + let dir = workspace.output_dir(&dir).to_file_path().unwrap(); + + let flag = match target { + CleanTarget::Auxiliary => "-c", + CleanTarget::Artifacts => "-C", + }; + + let executable = String::from("latexmk"); + let args = vec![ + format!("-outdir={}", dir.display()), + String::from(flag), + path.display().to_string(), + ]; + + Ok(Self { executable, args }) + } + + pub fn run(self) -> Result<()> { + log::debug!("Cleaning output files: {} {:?}", self.executable, self.args); + let result = std::process::Command::new(self.executable) + .args(self.args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + if let Err(why) = result { + anyhow::bail!("failed to spawn process: {why}") + } + + Ok(()) + } +} diff --git a/support/texlab/crates/texlab/src/features/workspace_command/dep_graph.rs b/support/texlab/crates/commands/src/dep_graph.rs index faf20d301e..1388756ddf 100644 --- a/support/texlab/crates/texlab/src/features/workspace_command/dep_graph.rs +++ b/support/texlab/crates/commands/src/dep_graph.rs @@ -1,8 +1,8 @@ +use std::io::Write; + use anyhow::Result; use base_db::{graph, Document, Workspace}; use itertools::Itertools; -use std::io::Write; - use rustc_hash::FxHashMap; pub fn show_dependency_graph(workspace: &Workspace) -> Result<String> { diff --git a/support/texlab/crates/commands/src/fwd_search.rs b/support/texlab/crates/commands/src/fwd_search.rs new file mode 100644 index 0000000000..eaa27e5013 --- /dev/null +++ b/support/texlab/crates/commands/src/fwd_search.rs @@ -0,0 +1,107 @@ +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, + process::Stdio, +}; + +use anyhow::Result; +use base_db::Workspace; +use base_feature::replace_placeholders; +use thiserror::Error; +use url::Url; + +#[derive(Debug, Error)] +pub enum ForwardSearchError { + #[error("Forward search is not configured")] + Unconfigured, + + #[error("Document \"{0}\" does not exist on the local file system")] + NotLocal(Url), + + #[error("Document \"{0}\" has an invalid file path")] + InvalidPath(Url), + + #[error("TeX document \"{0}\" not found")] + TexNotFound(Url), + + #[error("PDF document \"{0}\" not found")] + PdfNotFound(PathBuf), + + #[error("Unable to launch PDF viewer: {0}")] + LaunchViewer(#[from] std::io::Error), +} + +pub struct ForwardSearch { + program: String, + args: Vec<String>, +} + +impl ForwardSearch { + pub fn new( + workspace: &Workspace, + uri: &Url, + line: Option<u32>, + ) -> Result<Self, ForwardSearchError> { + let Some(config) = &workspace.config().synctex else { + return Err(ForwardSearchError::Unconfigured); + }; + + let Some(child) = workspace.lookup(uri) else { + return Err(ForwardSearchError::TexNotFound(uri.clone())); + }; + + let parents = workspace.parents(child); + let parent = parents.into_iter().next().unwrap_or(child); + if parent.uri.scheme() != "file" { + return Err(ForwardSearchError::NotLocal(parent.uri.clone())); + } + + let dir = workspace.current_dir(&parent.dir); + let dir = workspace.output_dir(&dir).to_file_path().unwrap(); + + let Some(tex_path) = &child.path else { + return Err(ForwardSearchError::InvalidPath(child.uri.clone())); + }; + + let Some(pdf_path) = parent.path + .as_deref() + .and_then(Path::file_stem) + .and_then(OsStr::to_str) + .map(|stem| dir.join(format!("{stem}.pdf"))) else + { + return Err(ForwardSearchError::InvalidPath(parent.uri.clone())); + }; + + if !pdf_path.exists() { + return Err(ForwardSearchError::PdfNotFound(pdf_path.clone())); + } + + let tex_path = tex_path.to_string_lossy().into_owned(); + let pdf_path = pdf_path.to_string_lossy().into_owned(); + let line = line.unwrap_or_else(|| child.line_index.line_col(child.cursor).line); + let line = (line + 1).to_string(); + + let program = config.program.clone(); + let args = replace_placeholders( + &config.args, + &[('f', &tex_path), ('p', &pdf_path), ('l', &line)], + ); + + Ok(Self { program, args }) + } +} + +impl ForwardSearch { + pub fn run(self) -> Result<(), ForwardSearchError> { + log::debug!("Executing forward search: {} {:?}", self.program, self.args); + + std::process::Command::new(self.program) + .args(self.args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + + Ok(()) + } +} diff --git a/support/texlab/crates/commands/src/lib.rs b/support/texlab/crates/commands/src/lib.rs new file mode 100644 index 0000000000..6c8f1e8f4f --- /dev/null +++ b/support/texlab/crates/commands/src/lib.rs @@ -0,0 +1,13 @@ +mod build; +mod change_env; +mod clean; +mod dep_graph; +mod fwd_search; + +pub use self::{ + build::{BuildCommand, BuildError}, + change_env::{change_environment, ChangeEnvironmentResult}, + clean::{CleanCommand, CleanTarget}, + dep_graph::show_dependency_graph, + fwd_search::{ForwardSearch, ForwardSearchError}, +}; diff --git a/support/texlab/crates/parser/Cargo.toml b/support/texlab/crates/parser/Cargo.toml index 3f2efc0450..8e8b88841f 100644 --- a/support/texlab/crates/parser/Cargo.toml +++ b/support/texlab/crates/parser/Cargo.toml @@ -11,6 +11,7 @@ logos = "0.13.0" once_cell = "1.17.1" regex = "1.7.3" rowan = "0.15.11" +rustc-hash = "1.1.0" syntax = { path = "../syntax" } [dev-dependencies] diff --git a/support/texlab/crates/parser/src/config.rs b/support/texlab/crates/parser/src/config.rs new file mode 100644 index 0000000000..b6f3e8476c --- /dev/null +++ b/support/texlab/crates/parser/src/config.rs @@ -0,0 +1,153 @@ +use rustc_hash::FxHashSet; + +#[derive(Debug)] +pub struct SyntaxConfig { + pub math_environments: FxHashSet<String>, + pub enum_environments: FxHashSet<String>, + pub verbatim_environments: FxHashSet<String>, + pub citation_commands: FxHashSet<String>, +} + +impl Default for SyntaxConfig { + fn default() -> Self { + let math_environments = DEFAULT_MATH_ENVIRONMENTS + .iter() + .map(ToString::to_string) + .collect(); + + let enum_environments = DEFAULT_ENUM_ENVIRONMENTS + .iter() + .map(ToString::to_string) + .collect(); + + let verbatim_environments = DEFAULT_VERBATIM_ENVIRONMENTS + .iter() + .map(ToString::to_string) + .collect(); + + let citation_commands = DEFAULT_CITATION_COMMANDS + .iter() + .map(ToString::to_string) + .collect(); + + Self { + math_environments, + enum_environments, + verbatim_environments, + citation_commands, + } + } +} + +static DEFAULT_MATH_ENVIRONMENTS: &[&str] = &[ + "align", + "align*", + "alignat", + "alignat*", + "aligned", + "aligned*", + "alignedat", + "alignedat*", + "array", + "array*", + "Bmatrix", + "Bmatrix*", + "bmatrix", + "bmatrix*", + "cases", + "cases*", + "CD", + "CD*", + "eqnarray", + "eqnarray*", + "equation", + "equation*", + "IEEEeqnarray", + "IEEEeqnarray*", + "subequations", + "subequations*", + "gather", + "gather*", + "gathered", + "gathered*", + "matrix", + "matrix*", + "multline", + "multline*", + "pmatrix", + "pmatrix*", + "smallmatrix", + "smallmatrix*", + "split", + "split*", + "subarray", + "subarray*", + "Vmatrix", + "Vmatrix*", + "vmatrix", + "vmatrix*", +]; + +static DEFAULT_ENUM_ENVIRONMENTS: &[&str] = &["enumerate", "itemize", "description"]; + +static DEFAULT_VERBATIM_ENVIRONMENTS: &[&str] = + &["pycode", "minted", "asy", "lstlisting", "verbatim"]; + +static DEFAULT_CITATION_COMMANDS: &[&str] = &[ + "cite", + "cite*", + "Cite", + "nocite", + "citet", + "citet*", + "citep", + "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", + "supercite", + "autocite", + "autocite*", + "Autocite", + "Autocite*", + "volcite", + "Volcite", + "pvolcite", + "Pvolcite", + "fvolcite", + "ftvolcite", + "svolcite", + "Svolcite", + "tvolcite", + "Tvolcite", + "avolcite", + "Avolcite", + "notecite", + "pnotecite", + "Pnotecite", + "fnotecite", + "citeA", + "citeA*", +]; diff --git a/support/texlab/crates/parser/src/latex.rs b/support/texlab/crates/parser/src/latex.rs index 52e164a349..f3e978a93b 100644 --- a/support/texlab/crates/parser/src/latex.rs +++ b/support/texlab/crates/parser/src/latex.rs @@ -3,6 +3,8 @@ mod lexer; use rowan::{GreenNode, GreenNodeBuilder}; use syntax::latex::SyntaxKind::{self, *}; +use crate::SyntaxConfig; + use self::lexer::{ types::{CommandName, SectionLevel, Token}, Lexer, @@ -30,9 +32,9 @@ struct Parser<'a> { } impl<'a> Parser<'a> { - pub fn new(text: &'a str) -> Self { + pub fn new(text: &'a str, config: &SyntaxConfig) -> Self { Self { - lexer: Lexer::new(text), + lexer: Lexer::new(text, config), builder: GreenNodeBuilder::new(), } } @@ -1116,21 +1118,23 @@ impl<'a> Parser<'a> { } } -pub fn parse_latex(text: &str) -> GreenNode { - Parser::new(text).parse() +pub fn parse_latex(text: &str, config: &SyntaxConfig) -> GreenNode { + Parser::new(text, config).parse() } #[cfg(test)] mod tests { use syntax::latex; + use crate::SyntaxConfig; + use super::parse_latex; #[test] fn test_parse() { insta::glob!("test_data/latex/{,**/}*.txt", |path| { let text = std::fs::read_to_string(path).unwrap().replace("\r\n", "\n"); - let root = latex::SyntaxNode::new_root(parse_latex(&text)); + let root = latex::SyntaxNode::new_root(parse_latex(&text, &SyntaxConfig::default())); insta::assert_debug_snapshot!(root); }); } diff --git a/support/texlab/crates/parser/src/latex/lexer.rs b/support/texlab/crates/parser/src/latex/lexer.rs index c0129ed3ba..e53a2ed042 100644 --- a/support/texlab/crates/parser/src/latex/lexer.rs +++ b/support/texlab/crates/parser/src/latex/lexer.rs @@ -4,6 +4,8 @@ pub(super) mod types; use logos::Logos; use syntax::latex::SyntaxKind; +use crate::SyntaxConfig; + use self::types::{CommandName, Token}; #[derive(Debug, PartialEq, Eq, Clone)] @@ -12,8 +14,8 @@ pub struct Lexer<'a> { } impl<'a> Lexer<'a> { - pub fn new(input: &'a str) -> Self { - let mut tokens = tokenize(input); + pub fn new(input: &'a str, config: &SyntaxConfig) -> Self { + let mut tokens = tokenize(input, config); tokens.reverse(); Self { tokens } } @@ -45,7 +47,7 @@ impl<'a> Lexer<'a> { } } -fn tokenize(input: &str) -> Vec<(Token, &str)> { +fn tokenize<'a>(input: &'a str, config: &SyntaxConfig) -> Vec<(Token, &'a str)> { let mut lexer = Token::lexer(input); std::iter::from_fn(move || { let kind = lexer.next()?.unwrap(); @@ -54,7 +56,7 @@ fn tokenize(input: &str) -> Vec<(Token, &str)> { }) .map(|(kind, text)| { if kind == Token::CommandName(CommandName::Generic) { - let name = commands::classify(&text[1..]); + let name = commands::classify(&text[1..], config); (Token::CommandName(name), text) } else { (kind, text) diff --git a/support/texlab/crates/parser/src/latex/lexer/commands.rs b/support/texlab/crates/parser/src/latex/lexer/commands.rs index ba609f5d7f..8354fa1bfc 100644 --- a/support/texlab/crates/parser/src/latex/lexer/commands.rs +++ b/support/texlab/crates/parser/src/latex/lexer/commands.rs @@ -1,6 +1,8 @@ +use crate::SyntaxConfig; + use super::types::{CommandName, SectionLevel}; -pub fn classify(name: &str) -> CommandName { +pub fn classify(name: &str, config: &SyntaxConfig) -> CommandName { match name { "begin" => CommandName::BeginEnvironment, "end" => CommandName::EndEnvironment, @@ -15,16 +17,6 @@ pub fn classify(name: &str) -> CommandName { "subparagraph" | "subparagraph*" => CommandName::Section(SectionLevel::Subparagraph), "item" => CommandName::EnumItem, "caption" => CommandName::Caption, - "cite" | "cite*" | "Cite" | "nocite" | "citet" | "citet*" | "citep" | "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" | "supercite" | "autocite" | "autocite*" - | "Autocite" | "Autocite*" | "volcite" | "Volcite" | "pvolcite" | "Pvolcite" - | "fvolcite" | "ftvolcite" | "svolcite" | "Svolcite" | "tvolcite" | "Tvolcite" - | "avolcite" | "Avolcite" | "notecite" | "pnotecite" | "Pnotecite" | "fnotecite" - | "citeA" | "citeA*" => CommandName::Citation, "usepackage" | "RequirePackage" => CommandName::PackageInclude, "documentclass" => CommandName::ClassInclude, "include" | "subfileinclude" | "input" | "subfile" => CommandName::LatexInclude, @@ -88,6 +80,7 @@ pub fn classify(name: &str) -> CommandName { "graphicspath" => CommandName::GraphicsPath, "iffalse" => CommandName::BeginBlockComment, "fi" => CommandName::EndBlockComment, + _ if config.citation_commands.contains(name) => CommandName::Citation, _ => CommandName::Generic, } } diff --git a/support/texlab/crates/parser/src/lib.rs b/support/texlab/crates/parser/src/lib.rs index 51d56475cf..fab576a041 100644 --- a/support/texlab/crates/parser/src/lib.rs +++ b/support/texlab/crates/parser/src/lib.rs @@ -1,5 +1,6 @@ mod bibtex; mod build_log; +mod config; mod latex; -pub use self::{bibtex::parse_bibtex, build_log::parse_build_log, latex::parse_latex}; +pub use self::{bibtex::parse_bibtex, build_log::parse_build_log, config::*, latex::parse_latex}; diff --git a/support/texlab/crates/texlab/Cargo.toml b/support/texlab/crates/texlab/Cargo.toml index 09fff5f6db..ebc7cd222d 100644 --- a/support/texlab/crates/texlab/Cargo.toml +++ b/support/texlab/crates/texlab/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "texlab" description = "LaTeX Language Server" -version = "5.4.2" +version = "5.5.0" license.workspace = true readme = "README.md" authors.workspace = true @@ -33,10 +33,11 @@ doctest = false [dependencies] anyhow = "1.0.70" base-db = { path = "../base-db" } +base-feature = { path = "../base-feature" } citeproc = { path = "../citeproc" } -clap = { version = "4.2.1", features = ["derive"] } +clap = { version = "4.2.2", features = ["derive"] } +commands = { path = "../commands" } crossbeam-channel = "0.5.8" -dashmap = "5.4.0" dirs = "5.0.0" distro = { path = "../distro" } encoding_rs = "0.8.32" @@ -44,7 +45,6 @@ encoding_rs_io = "0.1.7" fern = "0.6.2" flate2 = "1.0.25" fuzzy-matcher = { version = "0.3.7", features = ["compact"] } -human_name = { version = "2.0.1", default-features = false } itertools = "0.10.5" log = "0.4.17" lsp-server = "0.7.0" @@ -56,14 +56,13 @@ parser = { path = "../parser" } regex = "1.7.3" rowan = "0.15.11" rustc-hash = "1.1.0" -serde = "1.0.159" -serde_json = "1.0.95" +serde = "1.0.160" +serde_json = "1.0.96" serde_regex = "1.1.0" serde_repr = "0.1.12" -smol_str = { version = "0.1.24", features = ["serde"] } +smol_str = { version = "0.2.0", features = ["serde"] } syntax = { path = "../syntax" } tempfile = "3.5.0" -thiserror = "1.0.40" threadpool = "1.8.1" titlecase = "2.2.1" diff --git a/support/texlab/crates/texlab/benches/bench_main.rs b/support/texlab/crates/texlab/benches/bench_main.rs index d9e4937dbd..8e69e1e5a4 100644 --- a/support/texlab/crates/texlab/benches/bench_main.rs +++ b/support/texlab/crates/texlab/benches/bench_main.rs @@ -2,14 +2,15 @@ use base_db::{Owner, Workspace}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use distro::Language; use lsp_types::{ClientCapabilities, Position, Url}; -use parser::parse_latex; +use parser::{parse_latex, SyntaxConfig}; use rowan::TextSize; const CODE: &str = include_str!("../../../texlab.tex"); fn criterion_benchmark(c: &mut Criterion) { + let config = SyntaxConfig::default(); c.bench_function("LaTeX/Parser", |b| { - b.iter(|| parse_latex(black_box(CODE))); + b.iter(|| parse_latex(black_box(CODE), &config)); }); c.bench_function("LaTeX/Completion/Command", |b| { diff --git a/support/texlab/crates/texlab/src/client.rs b/support/texlab/crates/texlab/src/client.rs index 0439445092..9bbace9938 100644 --- a/support/texlab/crates/texlab/src/client.rs +++ b/support/texlab/crates/texlab/src/client.rs @@ -5,9 +5,10 @@ use std::sync::{ use anyhow::{bail, Result}; use crossbeam_channel::Sender; -use dashmap::DashMap; use lsp_server::{ErrorCode, Message, Request, RequestId, Response}; use lsp_types::{notification::ShowMessage, MessageType, ShowMessageParams}; +use parking_lot::Mutex; +use rustc_hash::FxHashMap; use serde::{de::DeserializeOwned, Serialize}; use crate::server::options::Options; @@ -16,7 +17,7 @@ use crate::server::options::Options; struct RawClient { sender: Sender<Message>, next_id: AtomicI32, - pending: DashMap<RequestId, Sender<Response>>, + pending: Mutex<FxHashMap<RequestId, Sender<Response>>>, } #[derive(Debug, Clone)] @@ -29,7 +30,7 @@ impl LspClient { let raw = Arc::new(RawClient { sender, next_id: AtomicI32::new(1), - pending: DashMap::default(), + pending: Default::default(), }); Self { raw } @@ -55,7 +56,7 @@ impl LspClient { let id = RequestId::from(self.raw.next_id.fetch_add(1, Ordering::SeqCst)); let (tx, rx) = crossbeam_channel::bounded(1); - self.raw.pending.insert(id.clone(), tx); + self.raw.pending.lock().insert(id.clone(), tx); self.raw .sender @@ -81,9 +82,10 @@ impl LspClient { } pub fn recv_response(&self, response: lsp_server::Response) -> Result<()> { - let (_, tx) = self + let tx = self .raw .pending + .lock() .remove(&response.id) .expect("response with known request id received"); diff --git a/support/texlab/crates/texlab/src/features.rs b/support/texlab/crates/texlab/src/features.rs index 0d82bcb4f4..9115580ce0 100644 --- a/support/texlab/crates/texlab/src/features.rs +++ b/support/texlab/crates/texlab/src/features.rs @@ -1,9 +1,7 @@ -pub mod build; pub mod completion; pub mod definition; pub mod folding; pub mod formatting; -pub mod forward_search; pub mod highlight; pub mod hover; pub mod inlay_hint; @@ -11,4 +9,3 @@ pub mod link; pub mod reference; pub mod rename; pub mod symbol; -pub mod workspace_command; diff --git a/support/texlab/crates/texlab/src/features/build.rs b/support/texlab/crates/texlab/src/features/build.rs deleted file mode 100644 index a37fe65bf8..0000000000 --- a/support/texlab/crates/texlab/src/features/build.rs +++ /dev/null @@ -1,171 +0,0 @@ -mod progress; - -use std::{ - io::{BufRead, BufReader, Read}, - path::{Path, PathBuf}, - process::Stdio, - thread::{self, JoinHandle}, -}; - -use base_db::Workspace; -use encoding_rs_io::DecodeReaderBytesBuilder; -use lsp_types::{ - notification::LogMessage, ClientCapabilities, LogMessageParams, TextDocumentIdentifier, Url, -}; -use serde::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; - -use crate::{client::LspClient, util::capabilities::ClientCapabilitiesExt}; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildParams { - pub text_document: TextDocumentIdentifier, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildResult { - pub status: BuildStatus, -} - -#[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)] -pub struct Command { - uri: Url, - progress: bool, - program: String, - args: Vec<String>, - working_dir: PathBuf, - client: LspClient, -} - -impl Command { - pub fn new( - workspace: &Workspace, - uri: Url, - client: LspClient, - client_capabilities: &ClientCapabilities, - ) -> Option<Self> { - let Some(document) = workspace - .lookup(&uri) - .map(|child| workspace.parents(child).into_iter().next().unwrap_or(child)) else { return None }; - - let Some(path) = document.path.as_deref() else { - log::warn!("Document {uri} cannot be compiled; skipping..."); - return None; - }; - - let config = &workspace.config().build; - let program = config.program.clone(); - let args = config - .args - .iter() - .map(|arg| replace_placeholder(arg, path)) - .collect(); - - let working_dir = workspace.current_dir(&document.dir).to_file_path().ok()?; - - Some(Self { - uri: document.uri.clone(), - progress: client_capabilities.has_work_done_progress_support(), - program, - args, - working_dir, - client, - }) - } - - pub fn run(self) -> BuildStatus { - let reporter = if self.progress { - let inner = progress::Reporter::new(&self.client); - inner.start(&self.uri).expect("report progress"); - Some(inner) - } else { - None - }; - - let mut process = match std::process::Command::new(&self.program) - .args(self.args) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .current_dir(&self.working_dir) - .spawn() - { - Ok(process) => process, - Err(why) => { - log::error!( - "Failed to spawn process {:?} in directory {}: {}", - self.program, - self.working_dir.display(), - why - ); - return BuildStatus::FAILURE; - } - }; - - let (sender, receiver) = crossbeam_channel::unbounded(); - track_output(process.stderr.take().unwrap(), sender.clone()); - track_output(process.stdout.take().unwrap(), sender.clone()); - let client = self.client.clone(); - let handle = std::thread::spawn(move || { - let typ = lsp_types::MessageType::LOG; - - while let Ok(Some(message)) = receiver.recv() { - let params = LogMessageParams { message, typ }; - let _ = client.send_notification::<LogMessage>(params); - } - }); - - let status = process.wait().map_or(BuildStatus::FAILURE, |result| { - if result.success() { - BuildStatus::SUCCESS - } else { - BuildStatus::ERROR - } - }); - - let _ = sender.send(None); - handle.join().unwrap(); - - drop(reporter); - status - } -} - -fn track_output( - output: impl Read + Send + 'static, - sender: crossbeam_channel::Sender<Option<String>>, -) -> JoinHandle<()> { - let reader = BufReader::new( - DecodeReaderBytesBuilder::new() - .encoding(Some(encoding_rs::UTF_8)) - .utf8_passthru(true) - .strip_bom(true) - .build(output), - ); - - thread::spawn(move || { - let _ = reader - .lines() - .flatten() - .try_for_each(|line| sender.send(Some(line))); - }) -} - -fn replace_placeholder(arg: &str, file: &Path) -> String { - if arg.starts_with('"') || arg.ends_with('"') { - arg.to_string() - } else { - arg.replace("%f", &file.to_string_lossy()) - } -} diff --git a/support/texlab/crates/texlab/src/features/build/progress.rs b/support/texlab/crates/texlab/src/features/build/progress.rs deleted file mode 100644 index 6f235bebd6..0000000000 --- a/support/texlab/crates/texlab/src/features/build/progress.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::sync::atomic::{AtomicI32, Ordering}; - -use anyhow::Result; -use lsp_types::{ - notification::Progress, request::WorkDoneProgressCreate, NumberOrString, ProgressParams, - ProgressParamsValue, Url, WorkDoneProgress, WorkDoneProgressBegin, - WorkDoneProgressCreateParams, WorkDoneProgressEnd, -}; - -use crate::client::LspClient; - -static NEXT_TOKEN: AtomicI32 = AtomicI32::new(1); - -pub struct Reporter<'a> { - client: &'a LspClient, - token: i32, -} - -impl<'a> Reporter<'a> { - pub fn new(client: &'a LspClient) -> Self { - let token = NEXT_TOKEN.fetch_add(1, Ordering::SeqCst); - Self { client, token } - } - - pub fn start(&self, uri: &Url) -> Result<()> { - self.client - .send_request::<WorkDoneProgressCreate>(WorkDoneProgressCreateParams { - token: NumberOrString::Number(self.token), - })?; - - self.client.send_notification::<Progress>(ProgressParams { - token: NumberOrString::Number(self.token), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(WorkDoneProgressBegin { - title: "Building".to_string(), - message: Some(uri.as_str().to_string()), - cancellable: Some(false), - percentage: None, - })), - })?; - - Ok(()) - } -} - -impl<'a> Drop for Reporter<'a> { - fn drop(&mut self) { - let _ = self.client.send_notification::<Progress>(ProgressParams { - token: NumberOrString::Number(self.token), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd { - message: None, - })), - }); - } -} diff --git a/support/texlab/crates/texlab/src/features/completion.rs b/support/texlab/crates/texlab/src/features/completion.rs index 61528057a5..ab7cc4a1b6 100644 --- a/support/texlab/crates/texlab/src/features/completion.rs +++ b/support/texlab/crates/texlab/src/features/completion.rs @@ -13,6 +13,7 @@ mod glossary_ref; mod import; mod include; mod label; +mod matcher; mod theorem; mod tikz_library; mod user_command; diff --git a/support/texlab/crates/texlab/src/features/completion/builder.rs b/support/texlab/crates/texlab/src/features/completion/builder.rs index a503fc6e1c..f37718a4ba 100644 --- a/support/texlab/crates/texlab/src/features/completion/builder.rs +++ b/support/texlab/crates/texlab/src/features/completion/builder.rs @@ -1,5 +1,5 @@ -use base_db::Document; -use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; +use base_db::{Document, MatchingAlgo}; +use fuzzy_matcher::skim::SkimMatcherV2; use itertools::Itertools; use lsp_types::{ ClientCapabilities, ClientInfo, CompletionItem, CompletionItemKind, CompletionList, @@ -23,12 +23,15 @@ use crate::util::{ lsp_enums::Structure, }; -use super::COMPLETION_LIMIT; +use super::{ + matcher::{self, Matcher}, + COMPLETION_LIMIT, +}; pub struct CompletionBuilder<'a> { context: &'a CursorContext<'a>, items: Vec<Item<'a>>, - matcher: SkimMatcherV2, + matcher: Box<dyn Matcher>, text_pattern: String, file_pattern: String, preselect: Option<String>, @@ -45,7 +48,13 @@ impl<'a> CompletionBuilder<'a> { client_info: Option<&'a ClientInfo>, ) -> Self { let items = Vec::new(); - let matcher = SkimMatcherV2::default().ignore_case(); + let matcher: Box<dyn Matcher> = match context.workspace.config().completion.matcher { + MatchingAlgo::Skim => Box::new(SkimMatcherV2::default()), + MatchingAlgo::SkimIgnoreCase => Box::new(SkimMatcherV2::default().ignore_case()), + MatchingAlgo::Prefix => Box::new(matcher::Prefix), + MatchingAlgo::PrefixIgnoreCase => Box::new(matcher::PrefixIgnoreCase), + }; + let text_pattern = match &context.cursor { Cursor::Tex(token) if token.kind() == latex::COMMAND_NAME => { if token.text_range().start() + TextSize::from(1) == context.offset { @@ -124,7 +133,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn glossary_entry(&mut self, range: TextRange, name: String) -> Option<()> { - let score = self.matcher.fuzzy_match(&name, &self.text_pattern)?; + let score = self.matcher.score(&name, &self.text_pattern)?; self.items.push(Item { range, data: Data::GlossaryEntry { name }, @@ -141,7 +150,7 @@ impl<'a> CompletionBuilder<'a> { name: &'a str, image: Option<&'a str>, ) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern)?; + let score = self.matcher.score(name, &self.text_pattern)?; self.items.push(Item { range, data: Data::Argument { name, image }, @@ -154,7 +163,7 @@ impl<'a> CompletionBuilder<'a> { pub fn begin_snippet(&mut self, range: TextRange) -> Option<()> { if self.snippets { - let score = self.matcher.fuzzy_match("begin", &self.text_pattern[1..])?; + let score = self.matcher.score("begin", &self.text_pattern[1..])?; self.items.push(Item { range, data: Data::BeginSnippet, @@ -187,7 +196,7 @@ impl<'a> CompletionBuilder<'a> { .trim(), ); - let score = self.matcher.fuzzy_match(&filter_text, &self.text_pattern)?; + let score = self.matcher.score(&filter_text, &self.text_pattern)?; let data = Data::Citation { document, @@ -207,7 +216,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn color_model(&mut self, range: TextRange, name: &'a str) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern)?; + let score = self.matcher.score(name, &self.text_pattern)?; self.items.push(Item { range, data: Data::ColorModel { name }, @@ -219,7 +228,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn color(&mut self, range: TextRange, name: &'a str) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern)?; + let score = self.matcher.score(name, &self.text_pattern)?; self.items.push(Item { range, data: Data::Color { name }, @@ -238,7 +247,7 @@ impl<'a> CompletionBuilder<'a> { glyph: Option<&'a str>, file_names: &'a [SmolStr], ) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern[1..])?; + let score = self.matcher.score(name, &self.text_pattern[1..])?; let data = Data::ComponentCommand { name, image, @@ -262,7 +271,7 @@ impl<'a> CompletionBuilder<'a> { name: &'a str, file_names: &'a [SmolStr], ) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern)?; + let score = self.matcher.score(name, &self.text_pattern)?; self.items.push(Item { range, data: Data::ComponentEnvironment { name, file_names }, @@ -280,7 +289,7 @@ impl<'a> CompletionBuilder<'a> { ) -> Option<()> { let score = self .matcher - .fuzzy_match(&entry_type.name, &self.text_pattern[1..])?; + .score(&entry_type.name, &self.text_pattern[1..])?; self.items.push(Item { range, @@ -293,7 +302,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn field(&mut self, range: TextRange, field: &'a BibtexFieldDoc) -> Option<()> { - let score = self.matcher.fuzzy_match(&field.name, &self.text_pattern)?; + let score = self.matcher.score(&field.name, &self.text_pattern)?; self.items.push(Item { range, data: Data::Field { field }, @@ -305,7 +314,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn class(&mut self, range: TextRange, name: &'a str) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern)?; + let score = self.matcher.score(name, &self.text_pattern)?; self.items.push(Item { range, data: Data::Class { name }, @@ -317,7 +326,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn package(&mut self, range: TextRange, name: &'a str) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern)?; + let score = self.matcher.score(name, &self.text_pattern)?; self.items.push(Item { range, data: Data::Package { name }, @@ -329,7 +338,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn file(&mut self, range: TextRange, name: String) -> Option<()> { - let score = self.matcher.fuzzy_match(&name, &self.file_pattern)?; + let score = self.matcher.score(&name, &self.file_pattern)?; self.items.push(Item { range, data: Data::File { name }, @@ -341,7 +350,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn directory(&mut self, range: TextRange, name: String) -> Option<()> { - let score = self.matcher.fuzzy_match(&name, &self.file_pattern)?; + let score = self.matcher.score(&name, &self.file_pattern)?; self.items.push(Item { range, data: Data::Directory { name }, @@ -361,7 +370,7 @@ impl<'a> CompletionBuilder<'a> { footer: Option<&'a str>, text: String, ) -> Option<()> { - let score = self.matcher.fuzzy_match(&text, &self.text_pattern)?; + let score = self.matcher.score(&text, &self.text_pattern)?; self.items.push(Item { range, data: Data::Label { @@ -379,7 +388,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn tikz_library(&mut self, range: TextRange, name: &'a str) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern)?; + let score = self.matcher.score(name, &self.text_pattern)?; self.items.push(Item { range, data: Data::TikzLibrary { name }, @@ -391,7 +400,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn user_command(&mut self, range: TextRange, name: &'a str) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern[1..])?; + let score = self.matcher.score(name, &self.text_pattern[1..])?; self.items.push(Item { range, data: Data::UserCommand { name }, @@ -403,7 +412,7 @@ impl<'a> CompletionBuilder<'a> { } pub fn user_environment(&mut self, range: TextRange, name: &'a str) -> Option<()> { - let score = self.matcher.fuzzy_match(name, &self.text_pattern)?; + let score = self.matcher.score(name, &self.text_pattern)?; self.items.push(Item { range, data: Data::UserEnvironment { name }, diff --git a/support/texlab/crates/texlab/src/features/completion/matcher.rs b/support/texlab/crates/texlab/src/features/completion/matcher.rs new file mode 100644 index 0000000000..a2b1a45e6a --- /dev/null +++ b/support/texlab/crates/texlab/src/features/completion/matcher.rs @@ -0,0 +1,42 @@ +pub trait Matcher { + fn score(&mut self, choice: &str, pattern: &str) -> Option<i32>; +} + +impl<T: fuzzy_matcher::FuzzyMatcher> Matcher for T { + fn score(&mut self, choice: &str, pattern: &str) -> Option<i32> { + fuzzy_matcher::FuzzyMatcher::fuzzy_match(self, choice, pattern) + } +} + +#[derive(Debug)] +pub struct Prefix; + +impl Matcher for Prefix { + fn score(&mut self, choice: &str, pattern: &str) -> Option<i32> { + if choice.starts_with(pattern) { + Some(-(choice.len() as i32)) + } else { + None + } + } +} + +#[derive(Debug)] +pub struct PrefixIgnoreCase; + +impl Matcher for PrefixIgnoreCase { + fn score(&mut self, choice: &str, pattern: &str) -> Option<i32> { + if pattern.len() > choice.len() { + return None; + } + + let mut cs = choice.chars(); + for p in pattern.chars() { + if !cs.next().unwrap().eq_ignore_ascii_case(&p) { + return None; + } + } + + return Some(-(choice.len() as i32)); + } +} diff --git a/support/texlab/crates/texlab/src/features/forward_search.rs b/support/texlab/crates/texlab/src/features/forward_search.rs deleted file mode 100644 index 019c08022e..0000000000 --- a/support/texlab/crates/texlab/src/features/forward_search.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::{ - io, - path::{Path, PathBuf}, - process::Stdio, -}; - -use base_db::Workspace; -use log::error; -use lsp_types::{Position, Url}; -use thiserror::Error; - -use crate::util::line_index_ext::LineIndexExt; - -#[derive(Debug, Error)] -pub enum Error { - #[error("TeX document '{0}' not found")] - TexNotFound(Url), - - #[error("TeX document '{0}' is invalid")] - InvalidTexFile(Url), - - #[error("PDF document '{0}' not found")] - PdfNotFound(PathBuf), - - #[error("TeX document '{0}' is not a local file")] - NoLocalFile(Url), - - #[error("PDF viewer is not configured")] - Unconfigured, - - #[error("Failed to spawn process: {0}")] - Spawn(io::Error), -} - -pub struct Command { - program: String, - args: Vec<String>, -} - -impl Command { - pub fn configure( - workspace: &Workspace, - uri: &Url, - position: Option<Position>, - ) -> Result<Self, Error> { - let child = workspace - .lookup(uri) - .ok_or_else(|| Error::TexNotFound(uri.clone()))?; - - let parent = *workspace.parents(child).iter().next().unwrap_or(&child); - if parent.uri.scheme() != "file" { - return Err(Error::NoLocalFile(parent.uri.clone())); - } - - let output_dir = workspace - .output_dir(&workspace.current_dir(&parent.dir)) - .to_file_path() - .unwrap(); - - let tex_path = child - .path - .as_deref() - .ok_or_else(|| Error::NoLocalFile(uri.clone()))?; - - let pdf_path = match parent - .path - .as_deref() - .unwrap() - .file_stem() - .and_then(|stem| stem.to_str()) - { - Some(stem) => output_dir.join(format!("{}.pdf", stem)), - None => return Err(Error::InvalidTexFile(uri.clone())), - }; - - if !pdf_path.exists() { - return Err(Error::PdfNotFound(pdf_path)); - } - - let position = position.unwrap_or_else(|| child.line_index.line_col_lsp(child.cursor)); - - let Some(config) = &workspace.config().synctex else { - return Err(Error::Unconfigured); - }; - - let program = config.program.clone(); - - let args: Vec<_> = config - .args - .iter() - .flat_map(|arg| replace_placeholder(tex_path, &pdf_path, position.line, arg)) - .collect(); - - Ok(Self { program, args }) - } -} - -impl Command { - pub fn run(self) -> Result<(), Error> { - log::debug!("Executing forward search: {} {:?}", self.program, self.args); - - std::process::Command::new(self.program) - .args(self.args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map_err(Error::Spawn)?; - - Ok(()) - } -} - -/// Iterate overs chunks of a string. Either returns a slice of the -/// original string, or the placeholder replacement. -struct PlaceHolderIterator<'a> { - remainder: &'a str, - tex_file: &'a str, - pdf_file: &'a str, - line_number: &'a str, -} - -impl<'a> PlaceHolderIterator<'a> { - pub fn new(s: &'a str, tex_file: &'a str, pdf_file: &'a str, line_number: &'a str) -> Self { - Self { - remainder: s, - tex_file, - pdf_file, - line_number, - } - } - - pub fn yield_remainder(&mut self) -> Option<&'a str> { - let chunk = self.remainder; - self.remainder = ""; - Some(chunk) - } - - pub fn yield_placeholder(&mut self) -> Option<&'a str> { - if self.remainder.len() >= 2 { - let placeholder = self.remainder; - self.remainder = &self.remainder[2..]; - match &placeholder[1..2] { - "f" => Some(self.tex_file), - "p" => Some(self.pdf_file), - "l" => Some(self.line_number), - "%" => Some("%"), // escape % - _ => Some(&placeholder[0..2]), - } - } else { - self.remainder = &self.remainder[1..]; - Some("%") - } - } - - pub fn yield_str(&mut self, end: usize) -> Option<&'a str> { - let chunk = &self.remainder[..end]; - self.remainder = &self.remainder[end..]; - Some(chunk) - } -} - -impl<'a> Iterator for PlaceHolderIterator<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option<Self::Item> { - return if self.remainder.is_empty() { - None - } else if self.remainder.starts_with('%') { - self.yield_placeholder() - } else { - // yield up to the next % or to the end - match self.remainder.find('%') { - None => self.yield_remainder(), - Some(end) => self.yield_str(end), - } - }; - } -} - -fn replace_placeholder( - tex_file: &Path, - pdf_file: &Path, - line_number: u32, - argument: &str, -) -> Option<String> { - let result = if argument.starts_with('"') || argument.ends_with('"') { - argument.to_string() - } else { - let line = &(line_number + 1).to_string(); - let it = PlaceHolderIterator::new(argument, tex_file.to_str()?, pdf_file.to_str()?, line); - it.collect::<Vec<&str>>().join("") - }; - Some(result) -} diff --git a/support/texlab/crates/texlab/src/features/workspace_command.rs b/support/texlab/crates/texlab/src/features/workspace_command.rs deleted file mode 100644 index cac998125e..0000000000 --- a/support/texlab/crates/texlab/src/features/workspace_command.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod change_environment; -pub mod clean; -pub mod dep_graph; diff --git a/support/texlab/crates/texlab/src/features/workspace_command/change_environment.rs b/support/texlab/crates/texlab/src/features/workspace_command/change_environment.rs deleted file mode 100644 index a7d2eba6cf..0000000000 --- a/support/texlab/crates/texlab/src/features/workspace_command/change_environment.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::collections::hash_map::HashMap; - -use anyhow::Result; -use base_db::Workspace; -use lsp_types::{ApplyWorkspaceEditParams, TextDocumentPositionParams, TextEdit, WorkspaceEdit}; -use rowan::ast::AstNode; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use crate::{ - normalize_uri, - util::{cursor::CursorContext, line_index_ext::LineIndexExt}, -}; - -fn change_environment_context( - workspace: &Workspace, - args: Vec<serde_json::Value>, -) -> Result<CursorContext<Params>> { - let params: ChangeEnvironmentParams = serde_json::from_value( - args.into_iter() - .next() - .ok_or(ChangeEnvironmentError::MissingArg)?, - ) - .map_err(ChangeEnvironmentError::InvalidArg)?; - - let mut uri = params.text_document_position.text_document.uri; - normalize_uri(&mut uri); - let position = params.text_document_position.position; - - CursorContext::new( - workspace, - &uri, - position, - Params { - new_name: params.new_name, - }, - ) - .ok_or(ChangeEnvironmentError::FailedCreatingContext.into()) -} - -pub fn change_environment( - workspace: &Workspace, - args: Vec<serde_json::Value>, -) -> Option<((), ApplyWorkspaceEditParams)> { - let context = change_environment_context(workspace, args).ok()?; - let (beg, end) = context.find_environment()?; - - let beg_name = beg.to_string(); - let end_name = end.to_string(); - - if beg_name != end_name { - return None; - } - let new_name = &context.params.new_name; - - let line_index = &context.document.line_index; - let mut changes = HashMap::default(); - changes.insert( - context.document.uri.clone(), - vec![ - TextEdit::new( - line_index.line_col_lsp_range(beg.syntax().text_range()), - new_name.clone(), - ), - TextEdit::new( - line_index.line_col_lsp_range(end.syntax().text_range()), - new_name.clone(), - ), - ], - ); - - Some(( - (), - ApplyWorkspaceEditParams { - label: Some(format!("change environment: {} -> {}", beg_name, new_name)), - edit: WorkspaceEdit::new(changes), - }, - )) -} - -#[derive(Debug, Error)] -pub enum ChangeEnvironmentError { - #[error("rename parameters was not provided as an argument")] - MissingArg, - - #[error("invalid argument: {0}")] - InvalidArg(serde_json::Error), - - #[error("failed creating context")] - FailedCreatingContext, - - #[error("could not create workspaces edit")] - CouldNotCreateWorkspaceEdit, -} - -#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ChangeEnvironmentParams { - #[serde(flatten)] - pub text_document_position: TextDocumentPositionParams, - - pub new_name: String, -} - -#[derive(Debug)] -pub struct Params { - new_name: String, -} diff --git a/support/texlab/crates/texlab/src/features/workspace_command/clean.rs b/support/texlab/crates/texlab/src/features/workspace_command/clean.rs deleted file mode 100644 index d11c278d8c..0000000000 --- a/support/texlab/crates/texlab/src/features/workspace_command/clean.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::process::Stdio; - -use anyhow::Result; -use base_db::Workspace; -use lsp_types::{TextDocumentIdentifier, Url}; -use thiserror::Error; - -use crate::normalize_uri; - -#[derive(Debug, Error)] -pub enum CleanError { - #[error("document '{0}' not found")] - DocumentNotFound(Url), - - #[error("document '{0}' is not a local file")] - NoLocalFile(Url), - - #[error("document was not provided as an argument")] - MissingArg, - - #[error("invalid argument: {0}")] - InvalidArg(serde_json::Error), - - #[error("failed to spawn process: {0}")] - Spawn(std::io::Error), -} - -#[derive(Debug)] -pub struct CleanCommand { - executable: String, - args: Vec<String>, -} - -impl CleanCommand { - pub fn new( - workspace: &Workspace, - options: CleanOptions, - args: Vec<serde_json::Value>, - ) -> Result<Self> { - let params: TextDocumentIdentifier = - serde_json::from_value(args.into_iter().next().ok_or(CleanError::MissingArg)?) - .map_err(CleanError::InvalidArg)?; - - let mut uri = params.uri; - normalize_uri(&mut uri); - - let document = workspace - .lookup(&uri) - .ok_or_else(|| CleanError::DocumentNotFound(uri.clone()))?; - - let path = document - .path - .as_deref() - .ok_or_else(|| CleanError::NoLocalFile(uri.clone()))?; - - let current_dir = workspace.current_dir(&document.dir); - - let output_dir = workspace.output_dir(¤t_dir).to_file_path().unwrap(); - - let flag = match options { - CleanOptions::Auxiliary => "-c", - CleanOptions::Artifacts => "-C", - }; - - let executable = "latexmk".to_string(); - let args = vec![ - format!("-outdir={}", output_dir.display()), - flag.to_string(), - path.display().to_string(), - ]; - - Ok(Self { executable, args }) - } - - pub fn run(self) -> Result<()> { - log::debug!("Cleaning output files: {} {:?}", self.executable, self.args); - std::process::Command::new(self.executable) - .args(self.args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map_err(move |msg| anyhow::Error::new(CleanError::Spawn(msg)))?; - - Ok(()) - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] -pub enum CleanOptions { - Auxiliary, - Artifacts, -} diff --git a/support/texlab/crates/texlab/src/lib.rs b/support/texlab/crates/texlab/src/lib.rs index e17aab362b..19ef5ba8a9 100644 --- a/support/texlab/crates/texlab/src/lib.rs +++ b/support/texlab/crates/texlab/src/lib.rs @@ -4,30 +4,3 @@ mod server; pub mod util; pub use self::{client::LspClient, server::Server}; - -pub(crate) fn normalize_uri(uri: &mut lsp_types::Url) { - fn fix_drive_letter(text: &str) -> Option<String> { - if !text.is_ascii() { - return None; - } - - match &text[1..] { - ":" => Some(text.to_ascii_uppercase()), - "%3A" | "%3a" => Some(format!("{}:", text[0..1].to_ascii_uppercase())), - _ => None, - } - } - - if let Some(mut segments) = uri.path_segments() { - if let Some(mut path) = segments.next().and_then(fix_drive_letter) { - for segment in segments { - path.push('/'); - path.push_str(segment); - } - - uri.set_path(&path); - } - } - - uri.set_fragment(None); -} diff --git a/support/texlab/crates/texlab/src/server.rs b/support/texlab/crates/texlab/src/server.rs index 0d52c41463..9cc0575e28 100644 --- a/support/texlab/crates/texlab/src/server.rs +++ b/support/texlab/crates/texlab/src/server.rs @@ -1,52 +1,59 @@ mod dispatch; +mod extensions; pub mod options; +mod progress; use std::{ + collections::HashMap, path::PathBuf, - sync::{Arc, Mutex}, + sync::{atomic::AtomicI32, Arc}, }; use anyhow::Result; use base_db::{Config, Owner, Workspace}; +use base_feature::normalize_uri; +use commands::{BuildCommand, CleanCommand, CleanTarget, ForwardSearch}; use crossbeam_channel::{Receiver, Sender}; use distro::{Distro, Language}; use lsp_server::{Connection, ErrorCode, Message, RequestId}; use lsp_types::{notification::*, request::*, *}; -use once_cell::sync::Lazy; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use rowan::{ast::AstNode, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; -use serde::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde::{de::DeserializeOwned, Serialize}; use syntax::bibtex; use threadpool::ThreadPool; use crate::{ client::LspClient, features::{ - build::{self, BuildParams, BuildResult, BuildStatus}, completion::{self, builder::CompletionItemData}, - definition, folding, formatting, forward_search, highlight, hover, inlay_hint, link, - reference, rename, symbol, - workspace_command::{change_environment, clean, dep_graph}, + definition, folding, formatting, highlight, hover, inlay_hint, link, reference, rename, + symbol, }, - normalize_uri, util::{ self, capabilities::ClientCapabilitiesExt, components::COMPONENT_DATABASE, line_index_ext::LineIndexExt, }, }; -use self::options::{Options, StartupOptions}; +use self::{ + extensions::{ + BuildParams, BuildRequest, BuildResult, BuildStatus, ForwardSearchRequest, + ForwardSearchResult, ForwardSearchStatus, + }, + options::{Options, StartupOptions}, + progress::ProgressReporter, +}; #[derive(Debug)] enum InternalMessage { SetDistro(Distro), SetOptions(Options), FileEvent(notify::Event), - ForwardSearch(Url), Diagnostics, ChktexResult(Url, Vec<lsp_types::Diagnostic>), + ForwardSearch(Url, Option<Position>), } pub struct Server { @@ -67,6 +74,7 @@ impl Server { let client = LspClient::new(connection.sender.clone()); let (internal_tx, internal_rx) = crossbeam_channel::unbounded(); let watcher = FileWatcher::new(internal_tx.clone()).expect("init file watcher"); + Self { connection: Arc::new(connection), internal_tx, @@ -94,27 +102,6 @@ impl Server { }); } - fn run_query_and_request<R, S, Q>(&self, id: RequestId, query: Q) - where - R: Request, - S: Serialize, - Q: FnOnce(&Workspace) -> Option<(S, R::Params)> + Send + 'static, - { - let client = self.client.clone(); - let workspace = Arc::clone(&self.workspace); - self.pool.execute(move || match query(&workspace.read()) { - Some((result, request_params)) => { - let response = lsp_server::Response::new_ok(id, result); - client.send_response(response).unwrap(); - client.send_request::<R>(request_params).unwrap(); - } - None => { - let response = lsp_server::Response::new_ok(id, Option::<S>::None); - client.send_response(response).unwrap(); - } - }); - } - fn run_fallible<R, Q>(&self, id: RequestId, query: Q) where R: Serialize, @@ -428,7 +415,13 @@ impl Server { normalize_uri(&mut uri); if self.workspace.read().config().build.on_save { - self.build_internal(uri.clone(), |_| ())?; + let text_document = TextDocumentIdentifier::new(uri.clone()); + let params = BuildParams { + text_document, + position: None, + }; + + self.build(None, params)?; } self.publish_diagnostics_with_delay(); @@ -487,7 +480,7 @@ impl Server { Ok(()) } - fn completion(&mut self, id: RequestId, params: CompletionParams) -> Result<()> { + fn completion(&self, id: RequestId, params: CompletionParams) -> Result<()> { let mut uri = params.text_document_position.text_document.uri; normalize_uri(&mut uri); let position = params.text_document_position.position; @@ -633,29 +626,28 @@ impl Server { Ok(()) } - fn execute_command(&mut self, id: RequestId, params: ExecuteCommandParams) -> Result<()> { + fn execute_command(&self, id: RequestId, params: ExecuteCommandParams) -> Result<()> { match params.command.as_str() { "texlab.cleanAuxiliary" => { - let workspace = self.workspace.read(); - let opt = clean::CleanOptions::Auxiliary; - let command = clean::CleanCommand::new(&workspace, opt, params.arguments); + let command = self.prepare_clean_command(params, CleanTarget::Auxiliary); self.run_fallible(id, || command?.run()); } "texlab.cleanArtifacts" => { - let workspace = self.workspace.read(); - let opt = clean::CleanOptions::Auxiliary; - let command = clean::CleanCommand::new(&workspace, opt, params.arguments); + let command = self.prepare_clean_command(params, CleanTarget::Artifacts); self.run_fallible(id, || command?.run()); } "texlab.changeEnvironment" => { - self.run_query_and_request::<ApplyWorkspaceEdit, _, _>(id, move |workspace| { - change_environment::change_environment(workspace, params.arguments) + let client = self.client.clone(); + let params = self.change_environment(params); + self.run_fallible(id, move || { + client.send_request::<ApplyWorkspaceEdit>(params?) }); } "texlab.showDependencyGraph" => { - self.run_query(id, move |workspace| { - dep_graph::show_dependency_graph(workspace).unwrap() - }); + let workspace = self.workspace.read(); + let dot = commands::show_dependency_graph(&workspace).unwrap(); + self.client + .send_response(lsp_server::Response::new_ok(id, dot))?; } _ => { self.client @@ -682,7 +674,7 @@ impl Server { fn inlay_hint_resolve(&self, id: RequestId, hint: InlayHint) -> Result<()> { let response = lsp_server::Response::new_ok(id, hint); - self.connection.sender.send(response.into()).unwrap(); + self.client.send_response(response)?; Ok(()) } @@ -694,99 +686,114 @@ impl Server { Ok(()) } - fn build(&mut self, id: RequestId, params: BuildParams) -> Result<()> { + fn build(&self, id: Option<RequestId>, params: BuildParams) -> Result<()> { + static LOCK: Mutex<()> = Mutex::new(()); + static NEXT_TOKEN: AtomicI32 = AtomicI32::new(1); + let mut uri = params.text_document.uri; normalize_uri(&mut uri); + let workspace = self.workspace.read(); + let client = self.client.clone(); - self.build_internal(uri, move |status| { - let result = BuildResult { status }; - let _ = client.send_response(lsp_server::Response::new_ok(id, result)); - })?; - Ok(()) - } + let fwd_search_after = workspace.config().build.forward_search_after; - fn build_internal( - &mut self, - uri: Url, - callback: impl FnOnce(BuildStatus) + Send + 'static, - ) -> Result<()> { - static LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); + let (sender, receiver) = crossbeam_channel::unbounded(); + self.redirect_build_log(receiver); - let workspace = self.workspace.read(); - let client = self.client.clone(); - let Some(compiler) = build::Command::new(&workspace, uri.clone(), client, &self.client_capabilities) else { - callback(BuildStatus::FAILURE); - return Ok(()); - }; + let command = BuildCommand::new(&workspace, &uri); + let internal = self.internal_tx.clone(); + let progress = self.client_capabilities.has_work_done_progress_support(); + self.pool.execute(move || { + let guard = LOCK.lock(); - let forward_search_after = workspace.config().build.forward_search_after; + let progress_reporter = if progress { + let token = NEXT_TOKEN.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Some(ProgressReporter::new(client.clone(), token, &uri)) + } else { + None + }; - let sender = self.internal_tx.clone(); - self.pool.execute(move || { - let guard = LOCK.lock().unwrap(); + let status = match command.and_then(|command| command.run(sender)) { + Ok(status) if status.success() => BuildStatus::SUCCESS, + Ok(_) => BuildStatus::ERROR, + Err(why) => { + log::error!("Failed to compile document \"{uri}\": {why}"); + BuildStatus::FAILURE + } + }; + + drop(progress_reporter); + drop(guard); - let status = compiler.run(); - if forward_search_after { - let _ = sender.send(InternalMessage::ForwardSearch(uri)); + if let Some(id) = id { + let result = BuildResult { status }; + let _ = client.send_response(lsp_server::Response::new_ok(id, result)); } - drop(guard); - callback(status); + if fwd_search_after { + let _ = internal.send(InternalMessage::ForwardSearch(uri, params.position)); + } }); Ok(()) } - fn forward_search(&mut self, id: RequestId, params: TextDocumentPositionParams) -> Result<()> { - let mut uri = params.text_document.uri; - normalize_uri(&mut uri); - + fn redirect_build_log(&self, receiver: Receiver<String>) { let client = self.client.clone(); - self.forward_search_internal(uri, Some(params.position), move |status| { - let result = ForwardSearchResult { status }; - let _ = client.send_response(lsp_server::Response::new_ok(id, result)); - })?; - - Ok(()) + self.pool.execute(move || { + let typ = MessageType::LOG; + for message in receiver { + client + .send_notification::<LogMessage>(LogMessageParams { message, typ }) + .unwrap(); + } + }); } - fn forward_search_internal( - &mut self, - uri: Url, + fn forward_search( + &self, + id: Option<RequestId>, + mut uri: Url, position: Option<Position>, - callback: impl FnOnce(ForwardSearchStatus) + Send + 'static, ) -> Result<()> { - let workspace = self.workspace.read(); + normalize_uri(&mut uri); - let command = match forward_search::Command::configure(&workspace, &uri, position) { - Ok(command) => command, - Err(why) => { - log::error!("Forward search failed: {}", why); - callback(why.into()); - return Ok(()); - } - }; + let client = self.client.clone(); + let command = ForwardSearch::new( + &self.workspace.read(), + &uri, + position.map(|position| position.line), + ); self.pool.execute(move || { - let status = command - .run() - .map_or_else(ForwardSearchStatus::from, |()| ForwardSearchStatus::SUCCESS); + let status = match command.and_then(ForwardSearch::run) { + Ok(()) => ForwardSearchStatus::SUCCESS, + Err(why) => { + log::error!("Failed to execute forward search: {why}"); + ForwardSearchStatus::from(why) + } + }; - callback(status); + if let Some(id) = id { + let result = ForwardSearchResult { status }; + client + .send_response(lsp_server::Response::new_ok(id, result)) + .unwrap(); + } }); Ok(()) } - fn code_actions(&mut self, id: RequestId, _params: CodeActionParams) -> Result<()> { + fn code_actions(&self, id: RequestId, _params: CodeActionParams) -> Result<()> { self.client .send_response(lsp_server::Response::new_ok(id, Vec::<CodeAction>::new()))?; Ok(()) } - fn code_action_resolve(&mut self, id: RequestId, action: CodeAction) -> Result<()> { + fn code_action_resolve(&self, id: RequestId, action: CodeAction) -> Result<()> { self.client .send_response(lsp_server::Response::new_ok(id, action))?; Ok(()) @@ -830,6 +837,77 @@ impl Server { } } + fn prepare_clean_command( + &self, + params: ExecuteCommandParams, + target: CleanTarget, + ) -> Result<CleanCommand> { + let workspace = self.workspace.read(); + let mut params = self.parse_command_params::<TextDocumentIdentifier>(params.arguments)?; + normalize_uri(&mut params.uri); + let Some(document) = workspace.lookup(¶ms.uri) else { + anyhow::bail!("Document {} is not opened!", params.uri) + }; + + CleanCommand::new(&workspace, document, target) + } + + fn change_environment(&self, params: ExecuteCommandParams) -> Result<ApplyWorkspaceEditParams> { + let workspace = self.workspace.read(); + let params = self.parse_command_params::<RenameParams>(params.arguments)?; + let mut uri = params.text_document_position.text_document.uri; + normalize_uri(&mut uri); + + let Some(document) = workspace.lookup(&uri) else { + anyhow::bail!("Document {} is not opened!", uri) + }; + + let line_index = &document.line_index; + let position = line_index.offset_lsp(params.text_document_position.position); + + let Some(result) = commands::change_environment(document, position, ¶ms.new_name) else { + anyhow::bail!("No environment found at the current position"); + }; + + let range1 = line_index.line_col_lsp_range(result.begin); + let range2 = line_index.line_col_lsp_range(result.end); + + let mut changes = HashMap::new(); + changes.insert( + document.uri.clone(), + vec![ + TextEdit::new(range1, params.new_name.clone()), + TextEdit::new(range2, params.new_name.clone()), + ], + ); + + let label = Some(format!( + "change environment: {} -> {}", + result.old_name, result.new_name + )); + + let edit = WorkspaceEdit { + changes: Some(changes), + document_changes: None, + change_annotations: None, + }; + + Ok(ApplyWorkspaceEditParams { label, edit }) + } + + fn parse_command_params<T: DeserializeOwned>( + &self, + params: Vec<serde_json::Value>, + ) -> Result<T> { + if params.is_empty() { + anyhow::bail!("No argument provided!"); + } + + let value = params.into_iter().next().unwrap(); + let value = serde_json::from_value(value)?; + Ok(value) + } + fn process_messages(&mut self) -> Result<()> { loop { crossbeam_channel::select! { @@ -866,9 +944,9 @@ impl Server { self.document_highlight(id, params) })? .on::<Formatting, _>(|id, params| self.formatting(id, params))? - .on::<BuildRequest, _>(|id, params| self.build(id, params))? + .on::<BuildRequest, _>(|id, params| self.build(Some(id), params))? .on::<ForwardSearchRequest, _>(|id, params| { - self.forward_search(id, params) + self.forward_search(Some(id), params.text_document.uri, Some(params.position)) })? .on::<ExecuteCommand,_>(|id, params| self.execute_command(id, params))? .on::<SemanticTokensRangeRequest, _>(|id, params| { @@ -888,7 +966,7 @@ impl Server { })? .default() { - self.connection.sender.send(response.into())?; + self.client.send_response(response)?; } } Message::Notification(notification) => { @@ -922,9 +1000,6 @@ impl Server { InternalMessage::FileEvent(event) => { self.handle_file_event(event); } - InternalMessage::ForwardSearch(uri) => { - self.forward_search_internal(uri, None, |_| ())?; - } InternalMessage::Diagnostics => { self.publish_diagnostics()?; } @@ -932,6 +1007,9 @@ impl Server { self.chktex_diagnostics.insert(uri, diagnostics); self.publish_diagnostics()?; } + InternalMessage::ForwardSearch(uri, position) => { + self.forward_search(None, uri, position)?; + } }; } }; @@ -969,50 +1047,3 @@ impl FileWatcher { workspace.watch(&mut self.watcher, &mut self.watched_dirs); } } - -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"; -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] -#[repr(i32)] -pub enum ForwardSearchStatus { - SUCCESS = 0, - ERROR = 1, - FAILURE = 2, - UNCONFIGURED = 3, -} - -impl From<forward_search::Error> for ForwardSearchStatus { - fn from(err: forward_search::Error) -> Self { - match err { - forward_search::Error::TexNotFound(_) => ForwardSearchStatus::FAILURE, - forward_search::Error::InvalidTexFile(_) => ForwardSearchStatus::ERROR, - forward_search::Error::PdfNotFound(_) => ForwardSearchStatus::ERROR, - forward_search::Error::NoLocalFile(_) => ForwardSearchStatus::FAILURE, - forward_search::Error::Unconfigured => ForwardSearchStatus::UNCONFIGURED, - forward_search::Error::Spawn(_) => ForwardSearchStatus::ERROR, - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct ForwardSearchResult { - pub status: ForwardSearchStatus, -} diff --git a/support/texlab/crates/texlab/src/server/extensions.rs b/support/texlab/crates/texlab/src/server/extensions.rs new file mode 100644 index 0000000000..e8a2050cff --- /dev/null +++ b/support/texlab/crates/texlab/src/server/extensions.rs @@ -0,0 +1,75 @@ +use commands::ForwardSearchError; +use lsp_types::{Position, TextDocumentIdentifier, TextDocumentPositionParams}; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +pub struct BuildRequest; + +impl lsp_types::request::Request for BuildRequest { + type Params = BuildParams; + + type Result = BuildResult; + + const METHOD: &'static str = "textDocument/build"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildParams { + pub text_document: TextDocumentIdentifier, + + #[serde(default)] + pub position: Option<Position>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildResult { + pub status: BuildStatus, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] +#[repr(i32)] +pub enum BuildStatus { + SUCCESS = 0, + ERROR = 1, + FAILURE = 2, + CANCELLED = 3, +} + +pub struct ForwardSearchRequest; + +impl lsp_types::request::Request for ForwardSearchRequest { + type Params = TextDocumentPositionParams; + + type Result = ForwardSearchResult; + + const METHOD: &'static str = "textDocument/forwardSearch"; +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] +#[repr(i32)] +pub enum ForwardSearchStatus { + SUCCESS = 0, + ERROR = 1, + FAILURE = 2, + UNCONFIGURED = 3, +} + +impl From<ForwardSearchError> for ForwardSearchStatus { + fn from(why: ForwardSearchError) -> Self { + match why { + ForwardSearchError::Unconfigured => ForwardSearchStatus::UNCONFIGURED, + ForwardSearchError::NotLocal(_) => ForwardSearchStatus::FAILURE, + ForwardSearchError::InvalidPath(_) => ForwardSearchStatus::ERROR, + ForwardSearchError::TexNotFound(_) => ForwardSearchStatus::FAILURE, + ForwardSearchError::PdfNotFound(_) => ForwardSearchStatus::ERROR, + ForwardSearchError::LaunchViewer(_) => ForwardSearchStatus::ERROR, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct ForwardSearchResult { + pub status: ForwardSearchStatus, +} diff --git a/support/texlab/crates/texlab/src/server/options.rs b/support/texlab/crates/texlab/src/server/options.rs index a955905d3c..ba5b645402 100644 --- a/support/texlab/crates/texlab/src/server/options.rs +++ b/support/texlab/crates/texlab/src/server/options.rs @@ -20,6 +20,7 @@ pub struct Options { pub symbols: SymbolOptions, pub latexindent: LatexindentOptions, pub forward_search: ForwardSearchOptions, + pub completion: CompletionOptions, pub experimental: ExperimentalOptions, } @@ -111,6 +112,7 @@ pub struct ExperimentalOptions { pub math_environments: Vec<String>, pub enum_environments: Vec<String>, pub verbatim_environments: Vec<String>, + pub citation_commands: Vec<String>, } #[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] @@ -120,6 +122,28 @@ pub struct StartupOptions { pub skip_distro: bool, } +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +pub struct CompletionOptions { + pub matcher: CompletionMatcher, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum CompletionMatcher { + Fuzzy, + FuzzyIgnoreCase, + Prefix, + PrefixIgnoreCase, +} + +impl Default for CompletionMatcher { + fn default() -> Self { + Self::FuzzyIgnoreCase + } +} + impl From<Options> for Config { fn from(value: Options) -> Self { let mut config = Config::default(); @@ -193,6 +217,13 @@ impl From<Options> for Config { .map(|pattern| pattern.0) .collect(); + config.completion.matcher = match value.completion.matcher { + CompletionMatcher::Fuzzy => base_db::MatchingAlgo::Skim, + CompletionMatcher::FuzzyIgnoreCase => base_db::MatchingAlgo::SkimIgnoreCase, + CompletionMatcher::Prefix => base_db::MatchingAlgo::Prefix, + CompletionMatcher::PrefixIgnoreCase => base_db::MatchingAlgo::PrefixIgnoreCase, + }; + config .syntax .math_environments @@ -209,5 +240,10 @@ impl From<Options> for Config { .extend(value.experimental.verbatim_environments); config + .syntax + .citation_commands + .extend(value.experimental.citation_commands); + + config } } diff --git a/support/texlab/crates/texlab/src/server/progress.rs b/support/texlab/crates/texlab/src/server/progress.rs new file mode 100644 index 0000000000..9a5dca9ffe --- /dev/null +++ b/support/texlab/crates/texlab/src/server/progress.rs @@ -0,0 +1,44 @@ +use lsp_types::{ + notification::Progress, request::WorkDoneProgressCreate, NumberOrString, ProgressParams, + ProgressParamsValue, Url, WorkDoneProgress, WorkDoneProgressBegin, + WorkDoneProgressCreateParams, WorkDoneProgressEnd, +}; + +use crate::LspClient; + +#[derive(Debug)] +pub struct ProgressReporter { + client: LspClient, + token: i32, +} + +impl Drop for ProgressReporter { + fn drop(&mut self) { + let _ = self.client.send_notification::<Progress>(ProgressParams { + token: NumberOrString::Number(self.token), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd { + message: None, + })), + }); + } +} + +impl ProgressReporter { + pub fn new(client: LspClient, token: i32, uri: &Url) -> Self { + let _ = client.send_request::<WorkDoneProgressCreate>(WorkDoneProgressCreateParams { + token: NumberOrString::Number(token), + }); + + let _ = client.send_notification::<Progress>(ProgressParams { + token: NumberOrString::Number(token), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(WorkDoneProgressBegin { + title: "Building".into(), + message: Some(String::from(uri.as_str())), + cancellable: Some(false), + percentage: None, + })), + }); + + Self { client, token } + } +} |