summaryrefslogtreecommitdiff
path: root/support/texlab/src/diagnostics/latex.rs
blob: 95fcaeeacff6c745669b70cb919e652169b20db5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use crate::{
    protocol::{Diagnostic, DiagnosticSeverity, NumberOrString, Range, RangeExt, Uri},
    workspace::Document,
};
use chashmap::CHashMap;
use futures::{
    future::{AbortHandle, Abortable, Aborted},
    lock::Mutex,
};
use log::trace;
use once_cell::sync::Lazy;
use regex::Regex;
use std::process::Stdio;
use tokio::{prelude::*, process::Command};

#[derive(Debug, Default)]
pub struct LatexDiagnosticsProvider {
    diagnostics_by_uri: CHashMap<Uri, Vec<Diagnostic>>,
    handle: Mutex<Option<AbortHandle>>,
}

impl LatexDiagnosticsProvider {
    pub fn get(&self, document: &Document) -> Vec<Diagnostic> {
        match self.diagnostics_by_uri.get(&document.uri) {
            Some(diagnostics) => diagnostics.to_owned(),
            None => Vec::new(),
        }
    }

    pub async fn update(&self, uri: &Uri, text: &str) {
        if uri.scheme() != "file" {
            return;
        }

        let mut handle_guard = self.handle.lock().await;
        if let Some(handle) = &*handle_guard {
            handle.abort();
        }

        let (handle, registration) = AbortHandle::new_pair();
        *handle_guard = Some(handle);
        drop(handle_guard);

        let future = Abortable::new(
            async move {
                self.diagnostics_by_uri
                    .insert(uri.clone(), lint(text.into()).await.unwrap_or_default());
            },
            registration,
        );

        if let Err(Aborted) = future.await {
            trace!("Killed ChkTeX because it took too long to execute")
        }
    }
}

pub static LINE_REGEX: Lazy<Regex> =
    Lazy::new(|| Regex::new("(\\d+):(\\d+):(\\d+):(\\w+):(\\w+):(.*)").unwrap());

async fn lint(text: String) -> io::Result<Vec<Diagnostic>> {
    let mut process: tokio::process::Child = Command::new("chktex")
        .args(&["-I0", "-f%l:%c:%d:%k:%n:%m\n"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .kill_on_drop(true)
        .spawn()?;

    process
        .stdin
        .take()
        .unwrap()
        .write_all(text.as_bytes())
        .await?;

    let mut stdout = String::new();
    process
        .stdout
        .take()
        .unwrap()
        .read_to_string(&mut stdout)
        .await?;

    let mut diagnostics = Vec::new();
    for line in stdout.lines() {
        if let Some(captures) = LINE_REGEX.captures(line) {
            let line = captures[1].parse::<u64>().unwrap() - 1;
            let character = captures[2].parse::<u64>().unwrap() - 1;
            let digit = captures[3].parse::<u64>().unwrap();
            let kind = &captures[4];
            let code = &captures[5];
            let message = captures[6].into();
            let range = Range::new_simple(line, character, line, character + digit);
            let severity = match kind {
                "Message" => DiagnosticSeverity::Information,
                "Warning" => DiagnosticSeverity::Warning,
                _ => DiagnosticSeverity::Error,
            };

            diagnostics.push(Diagnostic {
                source: Some("chktex".into()),
                code: Some(NumberOrString::String(code.into())),
                message,
                severity: Some(severity),
                range,
                related_information: None,
                tags: None,
            })
        }
    }
    Ok(diagnostics)
}