mirror of
https://github.com/latex-lsp/texlab.git
synced 2025-08-04 10:49:55 +00:00
Refactor textDocument/build request
This commit is contained in:
parent
27aba312a2
commit
669e7629d6
18 changed files with 454 additions and 438 deletions
|
@ -7,10 +7,7 @@ edition.workspace = true
|
|||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base-db = { path = "../base-db" }
|
||||
rowan = "0.15.11"
|
||||
rustc-hash = "1.1.0"
|
||||
syntax = { path = "../syntax" }
|
||||
url = "2.3.1"
|
||||
|
||||
[lib]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mod normalize;
|
||||
mod normalize_uri;
|
||||
mod placeholders;
|
||||
|
||||
pub use self::{normalize::normalize_uri, placeholders::*};
|
||||
pub use self::{normalize_uri::normalize_uri, placeholders::*};
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
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,
|
||||
}
|
||||
}
|
56
crates/base-feature/src/normalize_uri.rs
Normal file
56
crates/base-feature/src/normalize_uri.rs
Normal file
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -1,34 +1,50 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub fn replace_placeholders<'a>(
|
||||
input: &'a str,
|
||||
placeholders: &FxHashMap<char, &str>,
|
||||
) -> Cow<'a, str> {
|
||||
match input
|
||||
.strip_prefix('"')
|
||||
.and_then(|input| input.strip_suffix('"'))
|
||||
{
|
||||
Some(input) => Cow::Borrowed(input),
|
||||
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 placeholders.get(&key) {
|
||||
Some(value) => output.push_str(&value),
|
||||
None => output.push(key),
|
||||
},
|
||||
None => output.push('%'),
|
||||
};
|
||||
} else {
|
||||
output.push(ch);
|
||||
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()
|
||||
}
|
||||
|
||||
Cow::Owned(output)
|
||||
}
|
||||
#[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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ rust-version.workspace = true
|
|||
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"
|
||||
|
|
102
crates/commands/src/build.rs
Normal file
102
crates/commands/src/build.rs
Normal file
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use base_db::{Document, LineCol, Workspace};
|
||||
use base_db::Workspace;
|
||||
use base_feature::replace_placeholders;
|
||||
use rustc_hash::FxHashMap;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
|
@ -18,7 +16,7 @@ pub enum ForwardSearchError {
|
|||
Unconfigured,
|
||||
|
||||
#[error("Document \"{0}\" does not exist on the local file system")]
|
||||
NonLocalFile(Url),
|
||||
NotLocal(Url),
|
||||
|
||||
#[error("Document \"{0}\" has an invalid file path")]
|
||||
InvalidPath(Url),
|
||||
|
@ -41,17 +39,21 @@ pub struct ForwardSearch {
|
|||
impl ForwardSearch {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
child: &Document,
|
||||
position: LineCol,
|
||||
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::NonLocalFile(parent.uri.clone()));
|
||||
return Err(ForwardSearchError::NotLocal(parent.uri.clone()));
|
||||
}
|
||||
|
||||
let dir = workspace.current_dir(&parent.dir);
|
||||
|
@ -76,20 +78,14 @@ impl ForwardSearch {
|
|||
|
||||
let tex_path = tex_path.to_string_lossy().into_owned();
|
||||
let pdf_path = pdf_path.to_string_lossy().into_owned();
|
||||
let line = (position.line + 1).to_string();
|
||||
|
||||
let mut placeholders = FxHashMap::default();
|
||||
placeholders.insert('f', tex_path.as_str());
|
||||
placeholders.insert('p', pdf_path.as_str());
|
||||
placeholders.insert('l', line.as_str());
|
||||
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 = config
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| replace_placeholders(arg, &placeholders))
|
||||
.map(Cow::into_owned)
|
||||
.collect::<Vec<String>>();
|
||||
let args = replace_placeholders(
|
||||
&config.args,
|
||||
&[('f', &tex_path), ('p', &pdf_path), ('l', &line)],
|
||||
);
|
||||
|
||||
Ok(Self { program, args })
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
mod build;
|
||||
mod change_env;
|
||||
mod clean;
|
||||
mod dep_graph;
|
||||
mod fwd_search;
|
||||
|
||||
pub use self::{change_env::*, clean::*, dep_graph::*, 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},
|
||||
};
|
||||
|
|
|
@ -46,7 +46,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"
|
||||
|
@ -65,7 +64,6 @@ serde_repr = "0.1.12"
|
|||
smol_str = { version = "0.1.24", features = ["serde"] }
|
||||
syntax = { path = "../syntax" }
|
||||
tempfile = "3.5.0"
|
||||
thiserror = "1.0.40"
|
||||
threadpool = "1.8.1"
|
||||
titlecase = "2.2.1"
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
pub mod build;
|
||||
pub mod completion;
|
||||
pub mod definition;
|
||||
pub mod folding;
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,33 +1,32 @@
|
|||
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, LineColUtf16, Owner, Workspace};
|
||||
use base_db::{Config, Owner, Workspace};
|
||||
use base_feature::normalize_uri;
|
||||
use commands::{CleanCommand, CleanTarget, ForwardSearch, ForwardSearchError};
|
||||
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::{de::DeserializeOwned, 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, highlight, hover, inlay_hint, link, reference, rename,
|
||||
symbol,
|
||||
|
@ -38,7 +37,14 @@ use crate::{
|
|||
},
|
||||
};
|
||||
|
||||
use self::options::{Options, StartupOptions};
|
||||
use self::{
|
||||
extensions::{
|
||||
BuildParams, BuildRequest, BuildResult, BuildStatus, ForwardSearchRequest,
|
||||
ForwardSearchResult, ForwardSearchStatus,
|
||||
},
|
||||
options::{Options, StartupOptions},
|
||||
progress::ProgressReporter,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InternalMessage {
|
||||
|
@ -47,6 +53,7 @@ enum InternalMessage {
|
|||
FileEvent(notify::Event),
|
||||
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,
|
||||
|
@ -407,7 +415,9 @@ 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 };
|
||||
self.build(None, params)?;
|
||||
}
|
||||
|
||||
self.publish_diagnostics_with_delay();
|
||||
|
@ -466,7 +476,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;
|
||||
|
@ -612,7 +622,7 @@ 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 command = self.prepare_clean_command(params, CleanTarget::Auxiliary);
|
||||
|
@ -672,62 +682,87 @@ 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 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(())
|
||||
}
|
||||
|
||||
fn build_internal(
|
||||
&mut self,
|
||||
uri: Url,
|
||||
callback: impl FnOnce(BuildStatus) + Send + 'static,
|
||||
) -> Result<()> {
|
||||
static LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||
|
||||
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 forward_search_after = workspace.config().build.forward_search_after;
|
||||
let forward_search = if forward_search_after {
|
||||
self.prepare_forward_search(&uri, None).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let fwd_search_after = workspace.config().build.forward_search_after;
|
||||
|
||||
let (sender, receiver) = crossbeam_channel::unbounded();
|
||||
self.redirect_build_log(receiver);
|
||||
|
||||
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().unwrap();
|
||||
let guard = LOCK.lock();
|
||||
|
||||
let status = compiler.run();
|
||||
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
|
||||
};
|
||||
|
||||
if let Some(forward_search) = forward_search {
|
||||
std::thread::spawn(move || forward_search.run().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);
|
||||
|
||||
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, None));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn forward_search(&mut self, id: RequestId, params: TextDocumentPositionParams) -> Result<()> {
|
||||
let mut uri = params.text_document.uri;
|
||||
fn redirect_build_log(&self, receiver: Receiver<String>) {
|
||||
let client = self.client.clone();
|
||||
self.pool.execute(move || {
|
||||
let typ = MessageType::LOG;
|
||||
for message in receiver {
|
||||
client
|
||||
.send_notification::<LogMessage>(LogMessageParams { message, typ })
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn forward_search(
|
||||
&self,
|
||||
id: Option<RequestId>,
|
||||
mut uri: Url,
|
||||
position: Option<Position>,
|
||||
) -> Result<()> {
|
||||
normalize_uri(&mut uri);
|
||||
|
||||
let client = self.client.clone();
|
||||
let command = self.prepare_forward_search(&uri, Some(params.position));
|
||||
let command = ForwardSearch::new(
|
||||
&self.workspace.read(),
|
||||
&uri,
|
||||
position.map(|position| position.line),
|
||||
);
|
||||
|
||||
self.pool.execute(move || {
|
||||
let status = match command.and_then(ForwardSearch::run) {
|
||||
Ok(()) => ForwardSearchStatus::SUCCESS,
|
||||
|
@ -737,22 +772,24 @@ impl Server {
|
|||
}
|
||||
};
|
||||
|
||||
let result = ForwardSearchResult { status };
|
||||
client
|
||||
.send_response(lsp_server::Response::new_ok(id, result))
|
||||
.unwrap();
|
||||
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(())
|
||||
|
@ -796,28 +833,6 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
fn prepare_forward_search(
|
||||
&self,
|
||||
uri: &Url,
|
||||
position: Option<Position>,
|
||||
) -> Result<ForwardSearch, ForwardSearchError> {
|
||||
let workspace = self.workspace.read();
|
||||
let Some(document) = workspace.lookup(uri) else {
|
||||
return Err(ForwardSearchError::TexNotFound(uri.clone()));
|
||||
};
|
||||
|
||||
let position = match position {
|
||||
Some(position) => {
|
||||
let line = position.line;
|
||||
let col = position.character;
|
||||
document.line_index.to_utf8(LineColUtf16 { line, col })
|
||||
}
|
||||
None => document.line_index.line_col(document.cursor),
|
||||
};
|
||||
|
||||
ForwardSearch::new(&workspace, document, position)
|
||||
}
|
||||
|
||||
fn prepare_clean_command(
|
||||
&self,
|
||||
params: ExecuteCommandParams,
|
||||
|
@ -925,9 +940,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| {
|
||||
|
@ -988,6 +1003,9 @@ impl Server {
|
|||
self.chktex_diagnostics.insert(uri, diagnostics);
|
||||
self.publish_diagnostics()?;
|
||||
}
|
||||
InternalMessage::ForwardSearch(uri, position) => {
|
||||
self.forward_search(None, uri, position)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -1025,50 +1043,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<ForwardSearchError> for ForwardSearchStatus {
|
||||
fn from(why: ForwardSearchError) -> Self {
|
||||
match why {
|
||||
ForwardSearchError::Unconfigured => ForwardSearchStatus::UNCONFIGURED,
|
||||
ForwardSearchError::NonLocalFile(_) => 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,
|
||||
}
|
||||
|
|
72
crates/texlab/src/server/extensions.rs
Normal file
72
crates/texlab/src/server/extensions.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use commands::ForwardSearchError;
|
||||
use lsp_types::{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,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
44
crates/texlab/src/server/progress.rs
Normal file
44
crates/texlab/src/server/progress.rs
Normal file
|
@ -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 }
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue