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