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