flams_lsp/
state.rs

1use std::{collections::hash_map::Entry, path::Path};
2
3use async_lsp::{lsp_types as lsp, ClientSocket, LanguageClient};
4use flams_ontology::uris::{DocumentURI, URIRefTrait};
5use flams_stex::{
6    quickparse::stex::{DiagnosticLevel, STeXDiagnostic, STeXParseData, STeXParseDataI},
7    OutputCont, RusTeX,
8};
9use flams_system::{
10    backend::{
11        archives::{
12            source_files::{SourceDir, SourceEntry},
13            Archive,
14        },
15        AnyBackend, Backend, GlobalBackend, TemporaryBackend,
16    },
17    formats::OMDocResult,
18};
19use flams_utils::{
20    impossible,
21    prelude::{HMap, TreeChildIter},
22    sourcerefs::{LSPLineCol, SourceRange},
23};
24
25use crate::{
26    annotations::to_diagnostic, documents::LSPDocument, ClientExt, LSPStore, ProgressCallbackServer,
27};
28
29#[derive(Clone)]
30pub enum DocData {
31    Doc(LSPDocument),
32    Data(STeXParseData, bool),
33}
34impl DocData {
35    pub fn merge(&mut self, other: Self) {
36        fn merge_a(from: &mut STeXParseDataI, to: &mut STeXParseDataI) {
37            to.dependencies = std::mem::take(&mut from.dependencies);
38            /*for dep in std::mem::take(&mut from.dependents) {
39              to.dependents.insert(dep);
40            }*/
41            for d in std::mem::take(&mut from.diagnostics) {
42                to.diagnostics.insert(d);
43            }
44        }
45        match (self, other) {
46            (Self::Doc(d1), Self::Doc(d2)) => {
47                //merge_a(&mut d1.annotations.lock(),&mut d2.annotations.lock());
48                *d1 = d2;
49            }
50            (Self::Doc(d1), Self::Data(d2, _)) => {
51                merge_a(&mut d2.lock(), &mut d1.annotations.lock());
52            }
53            (d2 @ Self::Data(_, _), Self::Doc(d1)) => {
54                {
55                    let Self::Data(ref mut d2, _) = d2 else {
56                        impossible!()
57                    };
58                    merge_a(&mut d2.lock(), &mut d1.annotations.lock());
59                }
60                *d2 = Self::Doc(d1)
61            }
62            (d1 @ Self::Data(_, false), Self::Data(d2, true)) => {
63                {
64                    let Self::Data(_, _) = d1 else { impossible!() };
65                    //merge_a(&mut d1.lock(),&mut d2.lock());
66                }
67                *d1 = Self::Data(d2, true)
68            }
69            (Self::Data(d1, _), Self::Data(d2, _)) => {
70                merge_a(&mut d2.lock(), &mut d1.lock());
71            }
72        }
73    }
74}
75
76#[derive(Clone, Debug, Hash, PartialEq, Eq)]
77pub enum UrlOrFile {
78    Url(lsp::Url),
79    File(std::sync::Arc<Path>),
80}
81impl UrlOrFile {
82    pub fn name(&self) -> &str {
83        match self {
84            Self::Url(u) => u.path().split('/').last().unwrap_or(""),
85            Self::File(p) => p.file_name().and_then(|s| s.to_str()).unwrap_or(""),
86        }
87    }
88}
89impl From<lsp::Url> for UrlOrFile {
90    fn from(value: lsp::Url) -> Self {
91        match value.to_file_path() {
92            Ok(p) => Self::File(p.into()),
93            Err(_) => Self::Url(value),
94        }
95    }
96}
97impl Into<lsp::Url> for UrlOrFile {
98    fn into(self) -> lsp::Url {
99        match self {
100            Self::Url(u) => u,
101            Self::File(p) => lsp::Url::from_file_path(p).unwrap(),
102        }
103    }
104}
105impl std::fmt::Display for UrlOrFile {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self {
108            Self::Url(u) => u.fmt(f),
109            Self::File(p) => p.display().fmt(f),
110        }
111    }
112}
113
114#[derive(Default, Clone)]
115pub struct LSPState {
116    pub documents: triomphe::Arc<parking_lot::RwLock<HMap<UrlOrFile, DocData>>>,
117    rustex: triomphe::Arc<std::sync::OnceLock<RusTeX>>,
118    backend: TemporaryBackend,
119}
120impl LSPState {
121    #[inline]
122    #[must_use]
123    pub const fn backend(&self) -> &TemporaryBackend {
124        &self.backend
125    }
126
127    #[must_use]
128    #[inline]
129    pub fn rustex(&self) -> &RusTeX {
130        self.rustex.get_or_init(|| {
131            RusTeX::get().unwrap_or_else(|()| {
132                tracing::error!("Could not initialize RusTeX");
133                panic!("Could not initialize RusTeX")
134            })
135        })
136    }
137
138    pub fn build_html(&self, uri: &UrlOrFile, client: &mut ClientSocket) -> Option<DocumentURI> {
139        let Some(DocData::Doc(doc)) = self.documents.read().get(uri).cloned() else {
140            return None;
141        };
142        let UrlOrFile::File(path) = uri else {
143            return None;
144        }; //.to_file_path().ok()?;
145        let doc_uri = doc.document_uri().cloned()?;
146        if doc.html_up_to_date() {
147            return Some(doc_uri);
148        };
149        if doc.relative_path().is_none() {
150            return None;
151        };
152        let engine = self
153            .rustex()
154            .builder()
155            .set_sourcerefs(true)
156            .set_font_debug_info(true);
157        let engine = doc.with_text(|text| engine.set_string(path, text))?;
158        ProgressCallbackServer::with(
159            client.clone(),
160            format!("Building {}", uri.name()),
161            None,
162            move |progress| {
163                let out = ClientOutput(std::cell::RefCell::new(progress));
164                let (mut res, old) = engine.set_output(out).run();
165                doc.set_html_up_to_date();
166                {
167                    let mut lock = doc.annotations.lock();
168                    for (fnt, dt) in &res.font_data {
169                        if dt.web.is_none() {
170                            lock.diagnostics.insert(STeXDiagnostic {
171                                level: DiagnosticLevel::Warning,
172                                message: format!("Unknown web font for {fnt}"),
173                                range: SourceRange::default(),
174                            });
175                            for (glyph, char) in &dt.missing.inner {
176                                lock.diagnostics.insert(STeXDiagnostic {
177                level: DiagnosticLevel::Warning,
178                message: format!("unknown unicode character for glyph {char} ({glyph}) in font {fnt}"),
179                range: SourceRange::default()
180              });
181                            }
182                        }
183                    }
184                }
185                //let progress: ClientOutput = old.take_output().unwrap_or_else(|| unreachable!());
186                if let Some((ref e, ft)) = &mut res.error {
187                    let mut done = None;
188                    for ft in std::mem::take(ft) {
189                        let url = UrlOrFile::File(ft.file.into());
190                        if url == *uri {
191                            done = Some(SourceRange {
192                                start: LSPLineCol {
193                                    line: ft.line,
194                                    col: 0,
195                                },
196                                end: LSPLineCol {
197                                    line: ft.line,
198                                    col: ft.col,
199                                },
200                            });
201                        } else if let Some(dc) = self.documents.read().get(&url) {
202                            let data = match dc {
203                                DocData::Data(d, _) => d,
204                                DocData::Doc(d) => &d.annotations,
205                            };
206                            let mut lock = data.lock();
207                            lock.diagnostics.insert(STeXDiagnostic {
208                                level: DiagnosticLevel::Error,
209                                message: format!("RusTeX Error: {e}"),
210                                range: SourceRange {
211                                    start: LSPLineCol {
212                                        line: ft.line,
213                                        col: 0,
214                                    },
215                                    end: LSPLineCol {
216                                        line: ft.line,
217                                        col: ft.col,
218                                    },
219                                },
220                            });
221                            let _ = client.publish_diagnostics(lsp::PublishDiagnosticsParams {
222                                uri: url.clone().into(),
223                                version: None,
224                                diagnostics: lock.diagnostics.iter().map(to_diagnostic).collect(),
225                            });
226                        }
227                    }
228                    let mut lock = doc.annotations.lock();
229                    lock.diagnostics.insert(STeXDiagnostic {
230                        level: DiagnosticLevel::Error,
231                        message: format!("RusTeX Error: {e}"),
232                        range: done.unwrap_or_default(),
233                    });
234                    let _ = client.publish_diagnostics(lsp::PublishDiagnosticsParams {
235                        uri: uri.clone().into(),
236                        version: None,
237                        diagnostics: lock.diagnostics.iter().map(to_diagnostic).collect(),
238                    });
239                    drop(lock);
240                    None
241                } else {
242                    let html = res.to_string();
243                    let rel_path = doc.relative_path().unwrap_or_else(|| unreachable!());
244                    match flams_ftml::build_ftml(
245                        &AnyBackend::Temp(self.backend.clone()),
246                        &html,
247                        doc_uri.clone(),
248                        rel_path,
249                    ) {
250                        Ok((
251                            OMDocResult {
252                                document,
253                                html,
254                                modules,
255                            },
256                            _,
257                        )) => {
258                            self.backend.add_html(document.uri.clone(), html);
259                            for m in modules {
260                                let m = m.check(&mut self.backend.as_checker());
261                                self.backend.add_module(m);
262                            }
263                            let document = document.check(&mut self.backend.as_checker());
264                            self.backend.add_document(document);
265                            old.memorize(self.rustex());
266                            Some(doc_uri)
267                        }
268                        Err(e) => {
269                            let mut lock = doc.annotations.lock();
270                            lock.diagnostics.insert(STeXDiagnostic {
271                                level: DiagnosticLevel::Error,
272                                message: format!("FTML Error: {e}"),
273                                range: SourceRange::default(),
274                            });
275                            let _ = client.publish_diagnostics(lsp::PublishDiagnosticsParams {
276                                uri: uri.clone().into(),
277                                version: None,
278                                diagnostics: lock.diagnostics.iter().map(to_diagnostic).collect(),
279                            });
280                            drop(lock);
281                            None
282                        }
283                    }
284                }
285            },
286        )
287    }
288
289    #[inline]
290    pub fn build_html_and_notify(&self, uri: &UrlOrFile, mut client: ClientSocket) {
291        if let Some(uri) = self.build_html(uri, &mut client) {
292            client.html_result(&uri)
293        }
294    }
295
296    pub fn relint_dependents(self, path: std::sync::Arc<Path>) { /*
297                                                                 let docs = self.documents.read();
298                                                                 let mut deps = vec![UrlOrFile::File(path.clone())];
299                                                                 for (k,v) in docs.iter() {
300                                                                   if matches!(k,UrlOrFile::File(p) if p == path) { continue }
301                                                                   let d = match v {
302                                                                     DocData::Doc(d) => &d.annotations,
303                                                                     DocData::Data(d,_) => d
304                                                                   };
305                                                                   let lock = d.lock();
306                                                                   if lock.dependencies.contains(&path) {
307                                                                     deps.push(k.clone());
308                                                                   }
309                                                                 } */
310    }
311    /*
312    fn relint_dependents_i(&mut self,path:std::sync::Arc<Path>,&mut v:Vec<UrlOrFile>) {
313      let docs = self.documents.read();
314      let mut deps = vec![UrlOrFile::File(path.clone())];
315      for (k,v) in docs.iter() {
316        if matches!(k,UrlOrFile::File(p) if p == path) { continue }
317        let d = match v {
318          DocData::Doc(d) => &d.annotations,
319          DocData::Data(d,_) => d
320        };
321        let lock = d.lock();
322        if lock.dependencies.contains(&path) {
323          deps.push(k.clone());
324        }
325      }
326    } */
327
328    pub fn load_mathhubs(&self, client: ClientSocket) {
329        let (_, t) = flams_utils::time::measure(move || {
330            let mut files = Vec::new();
331
332            for a in GlobalBackend::get().all_archives().iter() {
333                if let Archive::Local(a) = a {
334                    let mut v = Vec::new();
335                    a.with_sources(|d| {
336                        for e in <_ as TreeChildIter<SourceDir>>::dfs(d.children.iter()) {
337                            match e {
338                                SourceEntry::File(f) => {
339                                    let uri = match DocumentURI::from_archive_relpath(
340                                        a.uri().owned(),
341                                        &f.relative_path,
342                                    ) {
343                                        Ok(u) => u,
344                                        Err(e) => {
345                                            tracing::error!("{e}");
346                                            continue;
347                                        }
348                                    };
349                                    v.push((
350                                        f.relative_path
351                                            .split('/')
352                                            .fold(a.source_dir(), |p, s| p.join(s))
353                                            .into(),
354                                        uri,
355                                    ))
356                                }
357                                _ => {}
358                            }
359                        }
360                    });
361                    files.push((a.id().clone(), v))
362                }
363            }
364
365            ProgressCallbackServer::with(
366                client,
367                "Linting MathHub".to_string(),
368                Some(files.len() as _),
369                move |progress| {
370                    self.load_all(
371                        files
372                            .into_iter()
373                            .map(|(id, v)| {
374                                progress.update(id.to_string(), Some(1));
375                                v
376                            })
377                            .flatten(),
378                        |file, data| {
379                            let lock = data.lock();
380                            if !lock.diagnostics.is_empty() {
381                                if let Ok(uri) = lsp::Url::from_file_path(&file) {
382                                    let _ = progress.client().clone().publish_diagnostics(
383                                        lsp::PublishDiagnosticsParams {
384                                            uri,
385                                            version: None,
386                                            diagnostics: lock
387                                                .diagnostics
388                                                .iter()
389                                                .map(to_diagnostic)
390                                                .collect(),
391                                        },
392                                    );
393                                }
394                            }
395                        },
396                    );
397                },
398            );
399        });
400        tracing::info!("Linting mathhubs finished after {t}");
401    }
402
403    pub fn load_all<I: IntoIterator<Item = (std::sync::Arc<Path>, DocumentURI)>>(
404        &self,
405        iter: I,
406        mut and_then: impl FnMut(&std::sync::Arc<Path>, &STeXParseData),
407    ) {
408        let mut ndocs = HMap::default();
409        let mut state = LSPStore::<true>::new(&mut ndocs);
410        for (p, uri) in iter {
411            if let Some(ret) = state.load(p.as_ref(), &uri) {
412                and_then(&p, &ret);
413                let p = UrlOrFile::File(p);
414                match state.map.entry(p) {
415                    Entry::Vacant(e) => {
416                        e.insert(DocData::Data(ret, true));
417                    }
418                    Entry::Occupied(mut e) => {
419                        e.get_mut().merge(DocData::Data(ret, true));
420                    }
421                }
422            }
423        }
424        let mut docs = self.documents.write();
425        for (k, v) in ndocs {
426            match docs.entry(k) {
427                Entry::Vacant(e) => {
428                    e.insert(v);
429                }
430                Entry::Occupied(mut e) => {
431                    e.get_mut().merge(v);
432                }
433            }
434        }
435    }
436
437    pub fn load<const FULL: bool>(
438        &self,
439        p: std::sync::Arc<Path>,
440        uri: &DocumentURI,
441        and_then: impl FnOnce(&STeXParseData),
442    ) {
443        //let Some(lsp_uri) = lsp::Url::from_file_path(p).ok() else {return};
444        let lsp_uri = UrlOrFile::File(p);
445        let UrlOrFile::File(path) = &lsp_uri else {
446            unreachable!()
447        };
448        if self.documents.read().get(&lsp_uri).is_some() {
449            return;
450        }
451        let mut docs = self.documents.write();
452        let mut state = LSPStore::<'_, FULL>::new(&mut *docs);
453        if let Some(ret) = state.load(path, uri) {
454            and_then(&ret);
455            drop(state);
456            match docs.entry(lsp_uri) {
457                Entry::Vacant(e) => {
458                    e.insert(DocData::Data(ret, FULL));
459                }
460                Entry::Occupied(mut e) => {
461                    e.get_mut().merge(DocData::Data(ret, FULL));
462                }
463            }
464        }
465    }
466
467    #[allow(clippy::let_underscore_future)]
468    pub fn insert(&self, uri: UrlOrFile, doctext: String) {
469        let doc = self.documents.read().get(&uri).cloned();
470        match doc {
471            Some(DocData::Doc(doc)) => {
472                if doc.set_text(doctext) {
473                    doc.compute_annots(self.clone());
474                }
475            }
476            _ => {
477                let doc = LSPDocument::new(doctext, uri.clone());
478                if doc.has_annots() {
479                    doc.compute_annots(self.clone());
480                }
481                match self.documents.write().entry(uri) {
482                    Entry::Vacant(e) => {
483                        e.insert(DocData::Doc(doc));
484                    }
485                    Entry::Occupied(mut e) => {
486                        e.get_mut().merge(DocData::Doc(doc));
487                    }
488                }
489            }
490        }
491    }
492
493    #[must_use]
494    pub fn get(&self, uri: &UrlOrFile) -> Option<LSPDocument> {
495        if let Some(DocData::Doc(doc)) = self.documents.read().get(uri) {
496            Some(doc.clone())
497        } else {
498            None
499        }
500    }
501
502    pub fn force_get(&self, uri: &UrlOrFile) -> Option<LSPDocument> {
503        if let Some(DocData::Doc(doc)) = self.documents.read().get(uri) {
504            return Some(doc.clone());
505        }
506        let UrlOrFile::File(f) = uri else { return None };
507        let Some(s) = std::fs::read_to_string(f).ok() else {
508            return None;
509        };
510        self.insert(uri.clone(), s);
511        self.get(uri)
512    }
513}
514
515struct ClientOutput(std::cell::RefCell<ProgressCallbackServer>);
516impl OutputCont for ClientOutput {
517    fn message(&self, _: String) {}
518    fn errmessage(&self, text: String) {
519        let _ = self
520            .0
521            .borrow_mut()
522            .client_mut()
523            .show_message(lsp::ShowMessageParams {
524                typ: lsp::MessageType::ERROR,
525                message: text,
526            });
527    }
528    fn file_open(&self, text: String) {
529        self.0.borrow().update(text, None);
530    }
531    fn file_close(&self, _text: String) {}
532    fn write_16(&self, _text: String) {}
533    fn write_17(&self, _text: String) {}
534    fn write_18(&self, _text: String) {}
535    fn write_neg1(&self, _text: String) {}
536    fn write_other(&self, _text: String) {}
537
538    #[inline]
539    fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
540        self
541    }
542}