texlab 4.3.2

LaTeX Language Server
Documentation
use std::{
    io::{BufRead, BufReader, Read},
    path::Path,
    process::{Command, Stdio},
    sync::{Arc, Mutex},
    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, Url, WorkDoneProgress, WorkDoneProgressBegin,
    WorkDoneProgressCreateParams, WorkDoneProgressEnd,
};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use uuid::Uuid;

use crate::{client::LspClient, ClientCapabilitiesExt};

use super::{FeatureRequest, ForwardSearch};

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildParams {
    pub text_document: TextDocumentIdentifier,
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)]
#[repr(i32)]
pub enum BuildStatus {
    SUCCESS = 0,
    ERROR = 1,
    FAILURE = 2,
    CANCELLED = 3,
}

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildResult {
    pub status: BuildStatus,
}

struct ProgressReporter<'a> {
    supports_progress: bool,
    client: LspClient,
    token: &'a str,
}

impl<'a> ProgressReporter<'a> {
    pub fn start(&self, uri: &Url) -> Result<()> {
        if self.supports_progress {
            self.client
                .send_request::<lsp_types::request::WorkDoneProgressCreate>(
                    WorkDoneProgressCreateParams {
                        token: NumberOrString::String(self.token.to_string()),
                    },
                )?;

            self.client.send_notification::<Progress>(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,
                    },
                )),
            })?;
        };
        Ok(())
    }
}

impl<'a> Drop for ProgressReporter<'a> {
    fn drop(&mut self) {
        if self.supports_progress {
            drop(self.client.send_notification::<Progress>(ProgressParams {
                token: NumberOrString::String(self.token.to_string()),
                value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd {
                    message: 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>,
        client: LspClient,
    ) -> Result<BuildResult> {
        let lock = self.lock.lock().unwrap();

        let document = request
            .workspace
            .iter()
            .find(|document| {
                document
                    .data()
                    .as_latex()
                    .map_or(false, |data| data.extras.has_document_environment)
            })
            .unwrap_or_else(|| request.main_document());

        if !document.can_be_compiled() {
            log::info!(
                "Document {} cannot be compiled; skipping...",
                document.uri()
            );

            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,
            client: client.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
            .args
            .0
            .iter()
            .map(|arg| replace_placeholder(arg.clone(), &path))
            .collect();

        let mut process = Command::new(&options.build.executable.0)
            .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, client, exit_receiver);
        let success = process.wait().map(|status| status.success())?;
        exit_sender.send(())?;
        drop(exit_sender);

        log_handle.join().unwrap();
        let status = if success {
            BuildStatus::SUCCESS
        } else {
            BuildStatus::ERROR
        };

        drop(progress_reporter);
        drop(lock);

        if let Some((executable, args)) = options
            .forward_search
            .executable
            .as_deref()
            .zip(options.forward_search.args.as_deref())
            .filter(|_| options.build.forward_search_after)
        {
            let position = self
                .positions_by_uri
                .get(&request.uri)
                .map(|entry| *entry.value())
                .unwrap_or_default();

            ForwardSearch::builder()
                .executable(executable)
                .args(args)
                .line(position.line)
                .workspace(&request.workspace)
                .tex_uri(&request.uri)
                .build()
                .execute();
        }

        Ok(BuildResult { status })
    }
}

fn capture_output(
    process: &mut std::process::Child,
    client: LspClient,
    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);
    thread::spawn(move || loop {
        crossbeam_channel::select! {
            recv(&log_receiver) -> message => {
                if let Ok(message) = message {
                    client.send_notification::<LogMessage>(
                        LogMessageParams {
                            message,
                            typ: lsp_types::MessageType::LOG,
                        },
                    )
                    .unwrap();
                }
            },
            recv(&exit_receiver) -> _ => break,
        };
    })
}

fn replace_placeholder(arg: String, file: &Path) -> String {
    if arg.starts_with('"') || arg.ends_with('"') {
        arg
    } else {
        arg.replace("%f", &file.to_string_lossy())
    }
}

fn track_output(output: impl Read + Send + 'static, sender: Sender<String>) -> JoinHandle<()> {
    let reader = BufReader::new(
        DecodeReaderBytesBuilder::new()
            .encoding(Some(encoding_rs::UTF_8))
            .utf8_passthru(true)
            .strip_bom(true)
            .build(output),
    );

    thread::spawn(move || {
        for line in reader.lines() {
            sender.send(line.unwrap()).unwrap();
        }
    })
}