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 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 *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 }
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 }
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 }; 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 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 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 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>) { }
384 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 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}