summaryrefslogtreecommitdiff
path: root/support/texlab/crates
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2023-04-17 03:03:48 +0000
committerNorbert Preining <norbert@preining.info>2023-04-17 03:03:48 +0000
commit88aa9bb9a3222cf13820ae3b6f64ce48dcd003ea (patch)
tree9495d0cd2219bb309106f5330e0aeba36a675319 /support/texlab/crates
parent421b47819f21160c3662c40f7da028f15b726577 (diff)
CTAN sync 202304170303
Diffstat (limited to 'support/texlab/crates')
-rw-r--r--support/texlab/crates/base-db/src/config.rs89
-rw-r--r--support/texlab/crates/base-db/src/document.rs4
-rw-r--r--support/texlab/crates/base-feature/Cargo.toml14
-rw-r--r--support/texlab/crates/base-feature/src/lib.rs4
-rw-r--r--support/texlab/crates/base-feature/src/normalize_uri.rs56
-rw-r--r--support/texlab/crates/base-feature/src/placeholders.rs50
-rw-r--r--support/texlab/crates/commands/Cargo.toml24
-rw-r--r--support/texlab/crates/commands/src/build.rs102
-rw-r--r--support/texlab/crates/commands/src/change_env.rs40
-rw-r--r--support/texlab/crates/commands/src/clean.rs57
-rw-r--r--support/texlab/crates/commands/src/dep_graph.rs (renamed from support/texlab/crates/texlab/src/features/workspace_command/dep_graph.rs)4
-rw-r--r--support/texlab/crates/commands/src/fwd_search.rs107
-rw-r--r--support/texlab/crates/commands/src/lib.rs13
-rw-r--r--support/texlab/crates/parser/Cargo.toml1
-rw-r--r--support/texlab/crates/parser/src/config.rs153
-rw-r--r--support/texlab/crates/parser/src/latex.rs14
-rw-r--r--support/texlab/crates/parser/src/latex/lexer.rs10
-rw-r--r--support/texlab/crates/parser/src/latex/lexer/commands.rs15
-rw-r--r--support/texlab/crates/parser/src/lib.rs3
-rw-r--r--support/texlab/crates/texlab/Cargo.toml15
-rw-r--r--support/texlab/crates/texlab/benches/bench_main.rs5
-rw-r--r--support/texlab/crates/texlab/src/client.rs12
-rw-r--r--support/texlab/crates/texlab/src/features.rs3
-rw-r--r--support/texlab/crates/texlab/src/features/build.rs171
-rw-r--r--support/texlab/crates/texlab/src/features/build/progress.rs54
-rw-r--r--support/texlab/crates/texlab/src/features/completion.rs1
-rw-r--r--support/texlab/crates/texlab/src/features/completion/builder.rs55
-rw-r--r--support/texlab/crates/texlab/src/features/completion/matcher.rs42
-rw-r--r--support/texlab/crates/texlab/src/features/forward_search.rs195
-rw-r--r--support/texlab/crates/texlab/src/features/workspace_command.rs3
-rw-r--r--support/texlab/crates/texlab/src/features/workspace_command/change_environment.rs108
-rw-r--r--support/texlab/crates/texlab/src/features/workspace_command/clean.rs93
-rw-r--r--support/texlab/crates/texlab/src/lib.rs27
-rw-r--r--support/texlab/crates/texlab/src/server.rs347
-rw-r--r--support/texlab/crates/texlab/src/server/extensions.rs75
-rw-r--r--support/texlab/crates/texlab/src/server/options.rs36
-rw-r--r--support/texlab/crates/texlab/src/server/progress.rs44
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(&current_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(&params.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, &params.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 }
+ }
+}