diff options
Diffstat (limited to 'support/texlab/src/features/build.rs')
-rw-r--r-- | support/texlab/src/features/build.rs | 335 |
1 files changed, 127 insertions, 208 deletions
diff --git a/support/texlab/src/features/build.rs b/support/texlab/src/features/build.rs index d961052061..645f1e8e0e 100644 --- a/support/texlab/src/features/build.rs +++ b/support/texlab/src/features/build.rs @@ -1,28 +1,18 @@ +mod progress; + use std::{ io::{BufRead, BufReader, Read}, - path::Path, - process::{Command, Stdio}, - sync::{Arc, Mutex}, + path::{Path, PathBuf}, + process::Stdio, thread::{self, JoinHandle}, }; -use anyhow::Result; -use crossbeam_channel::{Receiver, Sender}; -use dashmap::DashMap; use encoding_rs_io::DecodeReaderBytesBuilder; -use lsp_types::{ - notification::{LogMessage, Progress}, - LogMessageParams, NumberOrString, Position, ProgressParams, ProgressParamsValue, - TextDocumentIdentifier, TextDocumentPositionParams, Url, WorkDoneProgress, - WorkDoneProgressBegin, WorkDoneProgressCreateParams, WorkDoneProgressEnd, -}; +use lsp_types::{notification::LogMessage, LogMessageParams, TextDocumentIdentifier, Url}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use uuid::Uuid; -use crate::{client, req_queue::ReqQueue, ClientCapabilitiesExt, DocumentLanguage}; - -use super::{forward_search, FeatureRequest}; +use crate::{client::LspClient, db::Workspace, util::capabilities::ClientCapabilitiesExt, Db}; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -30,6 +20,12 @@ 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 { @@ -39,217 +35,132 @@ pub enum BuildStatus { CANCELLED = 3, } -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildResult { - pub status: BuildStatus, -} - -struct ProgressReporter<'a> { - supports_progress: bool, - req_queue: &'a Mutex<ReqQueue>, - lsp_sender: Sender<lsp_server::Message>, - token: &'a str, +#[derive(Debug)] +pub struct Command { + uri: Url, + progress: bool, + executable: String, + args: Vec<String>, + working_dir: PathBuf, + client: LspClient, } -impl<'a> ProgressReporter<'a> { - pub fn start(&self, uri: &Url) -> Result<()> { - if self.supports_progress { - client::send_request::<lsp_types::request::WorkDoneProgressCreate>( - self.req_queue, - &self.lsp_sender, - WorkDoneProgressCreateParams { - token: NumberOrString::String(self.token.to_string()), - }, - )?; - client::send_notification::<Progress>( - &self.lsp_sender, - ProgressParams { - token: NumberOrString::String(self.token.to_string()), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin( - WorkDoneProgressBegin { - title: "Building".to_string(), - message: Some(uri.as_str().to_string()), - cancellable: Some(false), - percentage: None, - }, - )), - }, - )?; +impl Command { + pub fn new(db: &dyn Db, uri: Url, client: LspClient) -> Option<Self> { + let workspace = Workspace::get(db); + let document = match workspace.lookup_uri(db, &uri) { + Some(child) => workspace + .parents(db, child) + .iter() + .next() + .copied() + .unwrap_or(child), + None => return None, }; - Ok(()) - } -} -impl<'a> Drop for ProgressReporter<'a> { - fn drop(&mut self) { - if self.supports_progress { - drop(client::send_notification::<Progress>( - &self.lsp_sender, - ProgressParams { - token: NumberOrString::String(self.token.to_string()), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::End( - WorkDoneProgressEnd { message: None }, - )), - }, - )); + if document.location(db).path(db).is_none() { + log::warn!("Document {uri} cannot be compiled; skipping..."); + return None; } - } -} - -#[derive(Default)] -pub struct BuildEngine { - lock: Mutex<()>, - pub positions_by_uri: DashMap<Arc<Url>, Position>, -} - -impl BuildEngine { - pub fn build( - &self, - request: FeatureRequest<BuildParams>, - req_queue: &Mutex<ReqQueue>, - lsp_sender: &Sender<lsp_server::Message>, - ) -> Result<BuildResult> { - let lock = self.lock.lock().unwrap(); - - let document = request - .workspace - .documents_by_uri - .values() - .find(|document| { - if let Some(data) = document.data.as_latex() { - data.extras.has_document_environment - } else { - false - } - }) - .unwrap_or_else(|| request.main_document()); - - if document.data.language() != DocumentLanguage::Latex { - return Ok(BuildResult { - status: BuildStatus::SUCCESS, - }); - } - - if document.uri.scheme() != "file" { - return Ok(BuildResult { - status: BuildStatus::FAILURE, - }); - } - let path = document.uri.to_file_path().unwrap(); - - let supports_progress = request - .workspace - .environment - .client_capabilities - .has_work_done_progress_support(); - - let token = format!("texlab-build-{}", Uuid::new_v4()); - let progress_reporter = ProgressReporter { - supports_progress, - req_queue, - lsp_sender: lsp_sender.clone(), - token: &token, - }; - progress_reporter.start(&document.uri)?; - let options = &request.workspace.environment.options; - - let build_dir = options - .root_directory - .as_ref() - .map(AsRef::as_ref) - .or_else(|| path.parent()) - .unwrap(); - - let args: Vec<_> = options - .build + let options = &workspace.options(db).build; + let executable = options.executable.0.clone(); + let path = document.location(db).path(db).as_deref().unwrap(); + let args = options .args + .0 .iter() - .map(|arg| replace_placeholder(arg.clone(), &path)) + .map(|arg| replace_placeholder(arg, path)) .collect(); - let mut process = Command::new(&options.build.executable) - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .current_dir(build_dir) - .spawn()?; - - let (exit_sender, exit_receiver) = crossbeam_channel::bounded(1); - let log_handle = capture_output(&mut process, lsp_sender, exit_receiver); - let success = process.wait().map(|status| status.success())?; - exit_sender.send(())?; - drop(exit_sender); + let working_dir = workspace + .working_dir(db, document.directory(db)) + .path(db) + .clone()?; + + Some(Self { + uri: document.location(db).uri(db).clone(), + progress: workspace + .client_capabilities(db) + .has_work_done_progress_support(), + executable, + args, + working_dir, + client, + }) + } - log_handle.join().unwrap(); - let status = if success { - BuildStatus::SUCCESS + 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 { - BuildStatus::ERROR + None }; - drop(progress_reporter); - drop(lock); - - if options.build.forward_search_after { - let request = FeatureRequest { - params: TextDocumentPositionParams { - position: self - .positions_by_uri - .get(&request.main_document().uri) - .map(|guard| *guard) - .unwrap_or_default(), - text_document: TextDocumentIdentifier::new(request.uri.as_ref().clone()), - }, - uri: request.uri, - workspace: request.workspace, - }; - forward_search::execute_forward_search(request); - } - - Ok(BuildResult { status }) - } -} - -fn capture_output( - process: &mut std::process::Child, - lsp_sender: &Sender<lsp_server::Message>, - exit_receiver: Receiver<()>, -) -> JoinHandle<()> { - let (log_sender, log_receiver) = crossbeam_channel::unbounded(); - track_output(process.stdout.take().unwrap(), log_sender.clone()); - track_output(process.stderr.take().unwrap(), log_sender); - let lsp_sender = lsp_sender.clone(); - thread::spawn(move || loop { - crossbeam_channel::select! { - recv(&log_receiver) -> message => { - if let Ok(message) = message { - client::send_notification::<LogMessage>( - &lsp_sender, - LogMessageParams { - message, - typ: lsp_types::MessageType::LOG, - }, - ) - .unwrap(); - } - }, - recv(&exit_receiver) -> _ => break, + let mut process = match std::process::Command::new(&self.executable) + .args(self.args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(self.working_dir) + .spawn() + { + Ok(process) => process, + Err(_) => { + log::error!("Failed to spawn process: {:?}", self.executable); + return BuildStatus::FAILURE; + } }; - }) -} -fn replace_placeholder(arg: String, file: &Path) -> String { - if arg.starts_with('"') || arg.ends_with('"') { - arg - } else { - arg.replace("%f", &file.to_string_lossy()) + let (line_sender, line_receiver) = flume::unbounded(); + let (exit_sender, exit_receiver) = flume::unbounded(); + track_output(process.stderr.take().unwrap(), line_sender.clone()); + track_output(process.stdout.take().unwrap(), line_sender); + let client = self.client.clone(); + let handle = std::thread::spawn(move || { + let typ = lsp_types::MessageType::LOG; + + loop { + let done = flume::Selector::new() + .recv(&line_receiver, |line| match line { + Ok(message) => { + let params = LogMessageParams { message, typ }; + client.send_notification::<LogMessage>(params).unwrap(); + false + } + Err(_) => true, + }) + .recv(&exit_receiver, |_| true) + .wait(); + + if done { + break; + } + } + }); + + let status = process.wait().map_or(BuildStatus::FAILURE, |result| { + if result.success() { + BuildStatus::SUCCESS + } else { + BuildStatus::ERROR + } + }); + + let _ = exit_sender.send(()); + handle.join().unwrap(); + + drop(reporter); + status } } -fn track_output(output: impl Read + Send + 'static, sender: Sender<String>) -> JoinHandle<()> { +fn track_output( + output: impl Read + Send + 'static, + sender: flume::Sender<String>, +) -> JoinHandle<()> { let reader = BufReader::new( DecodeReaderBytesBuilder::new() .encoding(Some(encoding_rs::UTF_8)) @@ -264,3 +175,11 @@ fn track_output(output: impl Read + Send + 'static, sender: Sender<String>) -> J } }) } + +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()) + } +} |