flams_lsp/
state.rs

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