Skip to main content

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::{
20    domain::{declarations::symbols::Symbol, modules::Module},
21    narrative::{
22        SharedDocumentElement,
23        documents::Document,
24        elements::{DocumentTerm, LogicalParagraph, VariableDeclaration},
25    },
26    terms::TermContainer,
27    utils::RefTree,
28};
29use ftml_solver::results::{
30    CheckResult, ContentCheckResult, DocumentCheckResult, SymbolCheckResult, TypeCheckResult,
31};
32use ftml_uris::{DocumentElementUri, DocumentUri};
33use smallvec::SmallVec;
34
35use crate::{
36    ClientExt, LSPStore, ProgressCallbackServer, annotations::to_diagnostic, documents::LSPDocument,
37};
38
39#[derive(Clone)]
40pub enum DocData {
41    Doc(LSPDocument),
42    Data(STeXParseData, bool),
43}
44impl DocData {
45    pub fn merge(&mut self, other: Self) {
46        fn merge_a(from: &mut STeXParseDataI, to: &mut STeXParseDataI) {
47            to.dependencies = std::mem::take(&mut from.dependencies);
48            /*for dep in std::mem::take(&mut from.dependents) {
49              to.dependents.insert(dep);
50            }*/
51            for d in std::mem::take(&mut from.diagnostics) {
52                to.diagnostics.insert(d);
53            }
54        }
55        match (self, other) {
56            (Self::Doc(d1), Self::Doc(d2)) => {
57                //merge_a(&mut d1.annotations.lock(),&mut d2.annotations.lock());
58                *d1 = d2;
59            }
60            (Self::Doc(d1), Self::Data(d2, _)) => {
61                merge_a(&mut d2.lock(), &mut d1.annotations.lock());
62            }
63            (d2 @ Self::Data(_, _), Self::Doc(d1)) => {
64                {
65                    let Self::Data(d2, _) = d2 else { impossible!() };
66                    merge_a(&mut d2.lock(), &mut d1.annotations.lock());
67                }
68                *d2 = Self::Doc(d1);
69            }
70            (d1 @ Self::Data(_, false), Self::Data(d2, true)) => {
71                {
72                    let Self::Data(_, _) = d1 else { impossible!() };
73                    //merge_a(&mut d1.lock(),&mut d2.lock());
74                }
75                *d1 = Self::Data(d2, true);
76            }
77            (Self::Data(d1, _), Self::Data(d2, _)) => {
78                merge_a(&mut d2.lock(), &mut d1.lock());
79            }
80        }
81    }
82}
83
84#[derive(Clone, Debug, Hash, PartialEq, Eq)]
85pub enum UrlOrFile {
86    Url(lsp::Url),
87    File(std::sync::Arc<Path>),
88}
89impl UrlOrFile {
90    pub fn name(&self) -> &str {
91        match self {
92            Self::Url(u) => u.path().split('/').next_back().unwrap_or(""),
93            Self::File(p) => p.file_name().and_then(|s| s.to_str()).unwrap_or(""),
94        }
95    }
96}
97impl From<lsp::Url> for UrlOrFile {
98    fn from(value: lsp::Url) -> Self {
99        match value.to_file_path() {
100            Ok(p) => Self::File(p.into()),
101            Err(()) => {
102                tracing::error!("Not a file uri: {value}");
103                Self::Url(value)
104            }
105        }
106    }
107}
108impl Into<lsp::Url> for UrlOrFile {
109    fn into(self) -> lsp::Url {
110        match self {
111            Self::Url(u) => u,
112            Self::File(p) => lsp::Url::from_file_path(p).expect("this shouldn't happen"),
113        }
114    }
115}
116impl std::fmt::Display for UrlOrFile {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        match self {
119            Self::Url(u) => u.fmt(f),
120            Self::File(p) => p.display().fmt(f),
121        }
122    }
123}
124
125#[derive(Default, Clone)]
126pub struct LSPState {
127    pub documents: triomphe::Arc<parking_lot::RwLock<HMap<UrlOrFile, DocData>>>,
128    rustex: triomphe::Arc<std::sync::OnceLock<RusTeX>>,
129    //backend: TemporaryBackend,
130}
131impl LSPState {
132    #[inline]
133    #[must_use]
134    pub fn backend(&self) -> &TemporaryBackend {
135        let AnyBackend::Temp(t) = flams_system::backend::backend() else {
136            panic!("this is a bug")
137        };
138        t
139    }
140
141    #[must_use]
142    #[inline]
143    pub fn rustex(&self) -> &RusTeX {
144        self.rustex.get_or_init(|| {
145            RusTeX::get().unwrap_or_else(|()| {
146                tracing::error!("Could not initialize RusTeX");
147                panic!("Could not initialize RusTeX")
148            })
149        })
150    }
151
152    pub fn build_html(&self, uri: &UrlOrFile, client: &mut ClientSocket) -> Option<DocumentUri> {
153        let Some(DocData::Doc(doc)) = self.documents.read().get(uri).cloned() else {
154            return None;
155        };
156        let UrlOrFile::File(path) = uri else {
157            return None;
158        }; //.to_file_path().ok()?;
159        let doc_uri = doc.document_uri().cloned()?;
160        if doc.html_up_to_date() {
161            return Some(doc_uri);
162        };
163        doc.relative_path()?;
164        let engine = self
165            .rustex()
166            .builder()
167            .set_sourcerefs(true)
168            .set_font_debug_info(true);
169        let engine = doc.with_text(|text| engine.set_string(path, text))?;
170        ProgressCallbackServer::with(
171            client.clone(),
172            format!("Building {}", uri.name()),
173            None,
174            move |progress| {
175                let out = ClientOutput(std::cell::RefCell::new(progress));
176                let (mut res, old) = engine.set_output(out).run();
177                doc.set_html_up_to_date();
178                {
179                    let mut lock = doc.annotations.lock();
180                    for (fnt, dt) in &res.font_data {
181                        if dt.web.is_none() {
182                            lock.diagnostics.insert(STeXDiagnostic {
183                                level: DiagnosticLevel::Warning,
184                                message: format!("Unknown web font for {fnt}"),
185                                range: SourceRange::default(),
186                            });
187                            for (glyph, char) in &dt.missing.inner {
188                                lock.diagnostics.insert(STeXDiagnostic {
189                                    level: DiagnosticLevel::Warning,
190                                    message: format!("unknown unicode character for glyph {char} ({glyph}) in font {fnt}"),
191                                    range: SourceRange::default()
192                                });
193                            }
194                        }
195                    }
196                }
197                //let progress: ClientOutput = old.take_output().unwrap_or_else(|| unreachable!());
198                if let Some((e, ft)) = &mut res.error {
199                    let mut done = None;
200                    for ft in std::mem::take(ft) {
201                        let url = UrlOrFile::File(ft.file.into());
202                        if url == *uri {
203                            done = Some(SourceRange {
204                                start: LSPLineCol {
205                                    line: ft.line,
206                                    col: 0,
207                                },
208                                end: LSPLineCol {
209                                    line: ft.line,
210                                    col: ft.col,
211                                },
212                            });
213                        } else if let Some(dc) = self.documents.read().get(&url) {
214                            let data = match dc {
215                                DocData::Data(d, _) => d,
216                                DocData::Doc(d) => &d.annotations,
217                            };
218                            let mut lock = data.lock();
219                            lock.diagnostics.insert(STeXDiagnostic {
220                                level: DiagnosticLevel::Error,
221                                message: format!("RusTeX Error: {e}"),
222                                range: SourceRange {
223                                    start: LSPLineCol {
224                                        line: ft.line,
225                                        col: 0,
226                                    },
227                                    end: LSPLineCol {
228                                        line: ft.line,
229                                        col: ft.col,
230                                    },
231                                },
232                            });
233                            let _ = client.publish_diagnostics(lsp::PublishDiagnosticsParams {
234                                uri: url.clone().into(),
235                                version: None,
236                                diagnostics: lock.diagnostics.iter().map(to_diagnostic).collect(),
237                            });
238                        }
239                    }
240                    let mut lock = doc.annotations.lock();
241                    lock.check = None;
242                    lock.diagnostics.insert(STeXDiagnostic {
243                        level: DiagnosticLevel::Error,
244                        message: format!("RusTeX Error: {e}"),
245                        range: done.unwrap_or_default(),
246                    });
247                    let _ = client.publish_diagnostics(lsp::PublishDiagnosticsParams {
248                        uri: uri.clone().into(),
249                        version: None,
250                        diagnostics: lock.diagnostics.iter().map(to_diagnostic).collect(),
251                    });
252                    drop(lock);
253                    None
254                } else {
255                    let html = res.to_string();
256                    //let rel_path = doc.relative_path().unwrap_or_else(|| unreachable!());
257                    match flams_system::logging::ignore_traces(|| {
258                        flams_ftml::build_ftml(
259                            &AnyBackend::Temp(self.backend().clone()),
260                            &html,
261                            doc_uri.clone(),
262                        )
263                    }) {
264                        Ok(FtmlResult {
265                            doc: docresult,
266                            ftml,
267                            css,
268                            body,
269                            inner_offset,
270                            ..
271                        }) => {
272                            /*tracing::warn!(
273                                "Adding HTML for {}\nSanity check: {:?}\n{:#?}",
274                                docresult.document.uri,
275                                &docresult.data[0..140],
276                                docresult.document
277                            );*/
278                            self.backend().add_html(
279                                docresult.document.uri.clone(),
280                                HTMLData {
281                                    html: ftml,
282                                    css,
283                                    body,
284                                    inner_offset: inner_offset as _,
285                                    refs: docresult.data,
286                                },
287                            );
288                            self.backend()
289                                .add_triples(&docresult.document.uri, docresult.triples);
290                            self.backend().add_document(docresult.document.clone());
291                            for m in &docresult.modules {
292                                self.backend().add_module(m.clone());
293                            }
294                            old.memorize(self.rustex());
295                            let mut checker = ftml_solver::Checker::<
296                                ftml_solver::split::SingleThreadedSplit,
297                            >::new(AnyBackend::Temp(
298                                self.backend().clone(),
299                            ));
300                            let _ = checker.add_modules(docresult.modules);
301                            let Ok((logs, mods)) = checker
302                                .check_document(&docresult.document)
303                                .inspect_err(|m| {
304                                    let _ =
305                                        client.publish_diagnostics(lsp::PublishDiagnosticsParams {
306                                            uri: uri.clone().into(),
307                                            version: None,
308                                            diagnostics: vec![to_diagnostic(&STeXDiagnostic {
309                                                level: DiagnosticLevel::Error,
310                                                message: format!("Module {m} not found"),
311                                                range: SourceRange::default(),
312                                            })],
313                                        });
314                                })
315                            else {
316                                return Some(doc_uri);
317                            };
318                            let mut lock = doc.annotations.lock();
319                            lock.diagnostics
320                                .0
321                                .extend(check_diagnostics(&logs, (&docresult.document, &mods)));
322                            lock.check = Some(logs);
323                            if !lock.diagnostics.is_empty() {
324                                let _ = client.publish_diagnostics(lsp::PublishDiagnosticsParams {
325                                    uri: uri.clone().into(),
326                                    version: None,
327                                    diagnostics: lock
328                                        .diagnostics
329                                        .iter()
330                                        .map(to_diagnostic)
331                                        .collect(),
332                                });
333                            }
334                            drop(lock);
335                            for m in mods {
336                                self.backend().add_module(m.clone());
337                            }
338                            Some(doc_uri)
339                        }
340                        Err(e) => {
341                            let mut lock = doc.annotations.lock();
342                            lock.check = None;
343                            lock.diagnostics.insert(STeXDiagnostic {
344                                level: DiagnosticLevel::Error,
345                                message: format!("FTML Error: {e}"),
346                                range: SourceRange::default(),
347                            });
348                            let _ = client.publish_diagnostics(lsp::PublishDiagnosticsParams {
349                                uri: uri.clone().into(),
350                                version: None,
351                                diagnostics: lock.diagnostics.iter().map(to_diagnostic).collect(),
352                            });
353                            drop(lock);
354                            None
355                        }
356                    }
357                }
358            },
359        )
360    }
361
362    #[inline]
363    pub fn build_html_and_notify(&self, uri: &UrlOrFile, mut client: ClientSocket) {
364        if let Some(uri) = self.build_html(uri, &mut client) {
365            client.html_result(&uri);
366        }
367    }
368
369    pub fn relint_dependents(self, path: std::sync::Arc<Path>) { /*
370        let docs = self.documents.read();
371        let mut deps = vec![UrlOrFile::File(path.clone())];
372        for (k,v) in docs.iter() {
373        if matches!(k,UrlOrFile::File(p) if p == path) { continue }
374        let d = match v {
375        DocData::Doc(d) => &d.annotations,
376        DocData::Data(d,_) => d
377        };
378        let lock = d.lock();
379        if lock.dependencies.contains(&path) {
380        deps.push(k.clone());
381        }
382        } */
383    }
384    /*
385    fn relint_dependents_i(&mut self,path:std::sync::Arc<Path>,&mut v:Vec<UrlOrFile>) {
386      let docs = self.documents.read();
387      let mut deps = vec![UrlOrFile::File(path.clone())];
388      for (k,v) in docs.iter() {
389        if matches!(k,UrlOrFile::File(p) if p == path) { continue }
390        let d = match v {
391          DocData::Doc(d) => &d.annotations,
392          DocData::Data(d,_) => d
393        };
394        let lock = d.lock();
395        if lock.dependencies.contains(&path) {
396          deps.push(k.clone());
397        }
398      }
399    } */
400
401    pub fn load_mathhubs(&self, client: ClientSocket) {
402        let (_, t) = ftml_ontology::utils::time::measure(move || {
403            let mut files = Vec::new();
404
405            for a in GlobalBackend.all_archives().iter() {
406                if let Archive::Local(a) = a {
407                    let mut v = Vec::new();
408                    a.with_sources(|d| {
409                        for e in d.dfs() {
410                            match e {
411                                SourceEntry::File(f) => {
412                                    let uri = match DocumentUri::from_archive_relpath(
413                                        a.uri().clone(),
414                                        f.relative_path.as_ref(),
415                                    ) {
416                                        Ok(u) => u,
417                                        Err(e) => {
418                                            tracing::error!("{e}");
419                                            continue;
420                                        }
421                                    };
422                                    v.push((
423                                        f.relative_path
424                                            .steps()
425                                            .fold(a.source_dir(), |p, s| p.join(s))
426                                            .into(),
427                                        uri,
428                                    ))
429                                }
430                                _ => {}
431                            }
432                        }
433                    });
434                    files.push((a.id().clone(), v))
435                }
436            }
437
438            ProgressCallbackServer::with(
439                client,
440                "Linting MathHub".to_string(),
441                Some(files.len() as _),
442                move |progress| {
443                    self.load_all(
444                        files
445                            .into_iter()
446                            .map(|(id, v)| {
447                                progress.update(id.to_string(), Some(1));
448                                v
449                            })
450                            .flatten(),
451                        |file, data| {
452                            let lock = data.lock();
453                            if !lock.diagnostics.is_empty() {
454                                if let Ok(uri) = lsp::Url::from_file_path(&file) {
455                                    let _ = progress.client().clone().publish_diagnostics(
456                                        lsp::PublishDiagnosticsParams {
457                                            uri,
458                                            version: None,
459                                            diagnostics: lock
460                                                .diagnostics
461                                                .iter()
462                                                .map(to_diagnostic)
463                                                .collect(),
464                                        },
465                                    );
466                                }
467                            }
468                        },
469                    );
470                },
471            );
472        });
473        tracing::info!("Linting mathhubs finished after {t}");
474    }
475
476    pub fn load_all<I: IntoIterator<Item = (std::sync::Arc<Path>, DocumentUri)>>(
477        &self,
478        iter: I,
479        mut and_then: impl FnMut(&std::sync::Arc<Path>, &STeXParseData),
480    ) {
481        let mut ndocs = HMap::default();
482        let mut state = LSPStore::<true>::new(&mut ndocs);
483        for (p, uri) in iter {
484            if let Some(ret) = state.load(p.as_ref(), &uri) {
485                and_then(&p, &ret);
486                let p = UrlOrFile::File(p);
487                match state.map.entry(p) {
488                    Entry::Vacant(e) => {
489                        e.insert(DocData::Data(ret, true));
490                    }
491                    Entry::Occupied(mut e) => {
492                        e.get_mut().merge(DocData::Data(ret, true));
493                    }
494                }
495            }
496        }
497        let mut docs = self.documents.write();
498        for (k, v) in ndocs {
499            match docs.entry(k) {
500                Entry::Vacant(e) => {
501                    e.insert(v);
502                }
503                Entry::Occupied(mut e) => {
504                    e.get_mut().merge(v);
505                }
506            }
507        }
508    }
509
510    pub fn load<const FULL: bool>(
511        &self,
512        p: std::sync::Arc<Path>,
513        uri: &DocumentUri,
514        and_then: impl FnOnce(&STeXParseData),
515    ) {
516        //let Some(lsp_uri) = lsp::Url::from_file_path(p).ok() else {return};
517        let lsp_uri = UrlOrFile::File(p);
518        let UrlOrFile::File(path) = &lsp_uri else {
519            unreachable!()
520        };
521        if self.documents.read().get(&lsp_uri).is_some() {
522            return;
523        }
524        let mut docs = self.documents.write();
525        let mut state = LSPStore::<'_, FULL>::new(&mut *docs);
526        if let Some(ret) = state.load(path, uri) {
527            and_then(&ret);
528            drop(state);
529            match docs.entry(lsp_uri) {
530                Entry::Vacant(e) => {
531                    e.insert(DocData::Data(ret, FULL));
532                }
533                Entry::Occupied(mut e) => {
534                    e.get_mut().merge(DocData::Data(ret, FULL));
535                }
536            }
537        }
538    }
539
540    #[allow(clippy::let_underscore_future)]
541    pub fn insert(&self, uri: UrlOrFile, doctext: String) {
542        let doc = self.documents.read().get(&uri).cloned();
543        match doc {
544            Some(DocData::Doc(doc)) => {
545                if doc.set_text(doctext) {
546                    doc.compute_annots(self.clone());
547                }
548            }
549            _ => {
550                let doc = LSPDocument::new(doctext, uri.clone());
551                if doc.has_annots() {
552                    doc.compute_annots(self.clone());
553                }
554                match self.documents.write().entry(uri) {
555                    Entry::Vacant(e) => {
556                        e.insert(DocData::Doc(doc));
557                    }
558                    Entry::Occupied(mut e) => {
559                        e.get_mut().merge(DocData::Doc(doc));
560                    }
561                }
562            }
563        }
564    }
565
566    #[must_use]
567    pub fn get(&self, uri: &UrlOrFile) -> Option<LSPDocument> {
568        if let Some(DocData::Doc(doc)) = self.documents.read().get(uri) {
569            Some(doc.clone())
570        } else {
571            None
572        }
573    }
574
575    pub fn force_get(&self, uri: &UrlOrFile) -> Option<LSPDocument> {
576        if let Some(DocData::Doc(doc)) = self.documents.read().get(uri) {
577            return Some(doc.clone());
578        }
579        let UrlOrFile::File(f) = uri else { return None };
580        let Some(s) = std::fs::read_to_string(f).ok() else {
581            return None;
582        };
583        self.insert(uri.clone(), s);
584        self.get(uri)
585    }
586}
587
588struct ClientOutput(std::cell::RefCell<ProgressCallbackServer>);
589impl OutputCont for ClientOutput {
590    fn message(&self, _: String) {}
591    fn errmessage(&self, text: String) {
592        let _ = self
593            .0
594            .borrow_mut()
595            .client_mut()
596            .show_message(lsp::ShowMessageParams {
597                typ: lsp::MessageType::ERROR,
598                message: text,
599            });
600    }
601    fn file_open(&self, text: String) {
602        self.0.borrow().update(text, None);
603    }
604    fn file_close(&self, _text: String) {}
605    fn write_16(&self, _text: String) {}
606    fn write_17(&self, _text: String) {}
607    fn write_18(&self, _text: String) {}
608    fn write_neg1(&self, _text: String) {}
609    fn write_other(&self, _text: String) {}
610
611    #[inline]
612    fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
613        self
614    }
615}
616
617fn check_diagnostics(
618    res: &DocumentCheckResult,
619    src: (&Document, &[Module]),
620) -> impl Iterator<Item = STeXDiagnostic> {
621    use either_of::EitherOf5 as E;
622    res.checks.iter().flat_map(|cr| match cr {
623        CheckResult::Missing(u) => E::A(std::iter::once(STeXDiagnostic {
624            level: DiagnosticLevel::Error,
625            message: format!("Module {u} not found"),
626            range: SourceRange::default(),
627        })),
628        CheckResult::Variable(e, r) => {
629            let vd = src.0.get_as::<VariableDeclaration>(e.name());
630            if vd.is_none() {
631                tracing::error!("Variable {e} not found!");
632            }
633            E::B(symbol_check_result(
634                r,
635                e.name().as_ref(),
636                vd.as_ref().map(|v| &v.data.tp),
637                vd.as_ref().map(|v| &v.data.df),
638            ))
639        }
640        CheckResult::Proof(uri, res) if res.iter().any(|r| !r.success()) => {
641            let prf = src.0.get_as::<LogicalParagraph>(uri.name());
642            E::A(std::iter::once(STeXDiagnostic {
643                level: DiagnosticLevel::Error,
644                message: format!("Checking proof {uri} failed"),
645                range: if let Some(prf) = prf {
646                    conv_range(prf.source)
647                } else {
648                    SourceRange::default()
649                },
650            }))
651        }
652        CheckResult::Proof(..) => E::C(std::iter::empty()),
653        CheckResult::Term { uri, inferred, .. } => {
654            if inferred.is_some() {
655                E::C(std::iter::empty())
656            } else {
657                let term = src.0.get_as::<DocumentTerm>(uri.name());
658                if term.is_none() {
659                    tracing::error!("Term {uri} not found!");
660                }
661                let range = term.map(|t| conv_range(t.term.source)).unwrap_or_default();
662                E::A(std::iter::once(STeXDiagnostic {
663                    level: DiagnosticLevel::Info,
664                    message: format!("checking term {} failed", uri.name()),
665                    range,
666                }))
667            }
668        }
669        CheckResult::Content(ccr) => match ccr {
670            ContentCheckResult::Symbol(uri, res) => {
671                let uri = uri.as_simple_module();
672                let sym = src
673                    .1
674                    .iter()
675                    .find(|m| m.uri == uri.module)
676                    .and_then(|m| m.get_as::<Symbol>(uri.name()));
677                if sym.is_none() {
678                    tracing::error!("Symbol {uri} not found!");
679                }
680                E::D(
681                    symbol_check_result(
682                        res,
683                        uri.name().as_ref(),
684                        sym.as_ref().map(|v| &v.data.tp),
685                        sym.as_ref().map(|v| &v.data.df),
686                    )
687                    .collect::<SmallVec<_, 1>>()
688                    .into_iter(),
689                )
690            }
691        },
692        CheckResult::Module { checks, .. } => E::E(checks.iter().flat_map(|ccr| match ccr {
693            ContentCheckResult::Symbol(uri, res) => {
694                let uri = uri.as_simple_module();
695                let sym = src
696                    .1
697                    .iter()
698                    .find(|m| m.uri == uri.module)
699                    .and_then(|m| m.get_as::<Symbol>(uri.name()));
700                if sym.is_none() {
701                    tracing::error!("Symbol {uri} not found!");
702                }
703                symbol_check_result(
704                    res,
705                    uri.name().as_ref(),
706                    sym.as_ref().map(|v| &v.data.tp),
707                    sym.as_ref().map(|v| &v.data.df),
708                )
709                .collect::<SmallVec<_, 1>>()
710                .into_iter()
711            }
712        })),
713    })
714}
715
716fn symbol_check_result(
717    result: &SymbolCheckResult,
718    name: &str,
719    tp: Option<&TermContainer>,
720    df: Option<&TermContainer>,
721) -> impl Iterator<Item = STeXDiagnostic> + use<> {
722    use either_of::Either::{Left as A, Right as B};
723    match result {
724        SymbolCheckResult::TypeOnly { result } => {
725            if result.success {
726                A(std::iter::empty())
727            } else {
728                let range = tp.map(|v| conv_range(v.source)).unwrap_or_default();
729                B(std::iter::once(STeXDiagnostic {
730                    level: DiagnosticLevel::Info,
731                    message: format!("checking type of {name} failed"),
732                    range,
733                }))
734            }
735        }
736        SymbolCheckResult::DefiniensOnly { inferred, .. } => {
737            if inferred.is_some() {
738                A(std::iter::empty())
739            } else {
740                let range = df.map(|v| conv_range(v.source)).unwrap_or_default();
741                B(std::iter::once(STeXDiagnostic {
742                    level: DiagnosticLevel::Info,
743                    message: format!("checking definiens of {name} failed ({range:?})"),
744                    range,
745                }))
746            }
747        }
748        SymbolCheckResult::Both { matches: None, .. } => {
749            let range = tp.map(|v| conv_range(v.source)).unwrap_or_default();
750            B(std::iter::once(STeXDiagnostic {
751                level: DiagnosticLevel::Info,
752                message: format!("checking type of {name} failed ({range:?})"),
753                range,
754            }))
755        }
756        SymbolCheckResult::Both {
757            matches: Some(matches),
758            ..
759        } => {
760            if matches.success {
761                return A(std::iter::empty());
762            }
763            let df_range = df.map(|v| conv_range(v.source)).unwrap_or_default();
764            B(std::iter::once(STeXDiagnostic {
765                level: DiagnosticLevel::Info,
766                message: format!("checking definiens of {name} failed"),
767                range: df_range,
768            }))
769        }
770    }
771}
772
773const fn conv_range(
774    ftml_ontology::utils::SourceRange { start, end }: ftml_ontology::utils::SourceRange,
775) -> SourceRange<LSPLineCol> {
776    SourceRange {
777        start: LSPLineCol {
778            line: start.line,
779            col: start.col,
780        },
781        end: LSPLineCol {
782            line: end.line,
783            col: end.col,
784        },
785    }
786}