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