1use std::{path::Path, sync::atomic::AtomicBool};
2
3use async_lsp::lsp_types::{Position, Range};
4use flams_math_archives::{
5 MathArchive,
6 backend::{AnyBackend, GlobalBackend, LocalBackend},
7 utils::path_ext::PathExt,
8};
9use flams_stex::quickparse::stex::{STeXParseData, STeXParseDataI};
10use flams_utils::sourcerefs::{
11 ByteOffset, LSPLineCol, PositionConverter, StringPosition, StringRange,
12};
13use ftml_uris::{ArchiveUri, DocumentUri};
14
15use crate::{
16 LSPStore,
17 state::{LSPState, UrlOrFile},
18};
19
20#[derive(Debug, PartialEq, Eq)]
21struct DocumentData {
22 path: Option<std::sync::Arc<Path>>,
23 archive: Option<ArchiveUri>,
24 rel_path: Option<Box<str>>,
25 doc_uri: Option<DocumentUri>,
26}
27
28#[derive(Clone, Debug)]
29pub struct LSPDocument {
30 pub(crate) up_to_date: triomphe::Arc<AtomicBool>,
31 text: triomphe::Arc<parking_lot::Mutex<LSPText>>,
32 pub annotations: STeXParseData,
34 data: triomphe::Arc<DocumentData>,
35 pub(crate) force_snify: triomphe::Arc<AtomicBool>,
36}
37impl PartialEq for LSPDocument {
38 #[inline]
39 fn eq(&self, other: &Self) -> bool {
40 self.data == other.data
41 }
42}
43
44impl LSPDocument {
45 #[allow(clippy::cast_possible_truncation)]
46 #[must_use]
47 pub fn new(text: String, lsp_uri: UrlOrFile) -> Self {
48 let path = if let UrlOrFile::File(p) = lsp_uri {
49 Some(p)
50 } else {
51 None
52 }; let default = || {
54 let path = path.as_ref()?.as_slash_str().into_owned();
55 Some((
56 ArchiveUri::no_archive().clone(),
57 Some(path.into_boxed_str()),
58 ))
59 };
60 let ap = path
61 .as_ref()
62 .and_then(|path| {
63 GlobalBackend.archive_of_source(path, |a, rp| {
64 let uri = a.uri().clone();
65 (uri, Some(rp.to_string().into_boxed_str()))
66 })
67 })
68 .or_else(default);
69 let (archive, rel_path) = ap.map_or((None, None), |(a, p)| (Some(a), p));
70 let r = LSPText {
71 text,
72 html_up_to_date: false,
73 };
74 let doc_uri = archive.as_ref().and_then(|a| {
75 rel_path.as_deref().and_then(|rp: &str| {
76 match DocumentUri::from_archive_relpath(a.clone(), rp) {
77 Ok(u) => Some(u),
78 Err(e) => {
79 tracing::error!("Error in URI {rp} in {a}: {e} ({path:?})");
80 None
81 }
82 }
83 })
84 });
85 let data = DocumentData {
87 path,
88 archive,
89 rel_path,
90 doc_uri,
91 };
92 Self {
93 up_to_date: triomphe::Arc::new(AtomicBool::new(false)),
94 text: triomphe::Arc::new(parking_lot::Mutex::new(r)),
95 data: triomphe::Arc::new(data),
96 annotations: STeXParseData::default(),
98 force_snify: triomphe::Arc::new(AtomicBool::new(false)),
99 }
100 }
101
102 #[inline]
103 #[must_use]
104 pub fn path(&self) -> Option<&Path> {
105 self.data.path.as_deref()
106 }
107
108 #[inline]
109 #[must_use]
110 pub fn archive(&self) -> Option<&ArchiveUri> {
111 self.data.archive.as_ref()
112 }
113
114 #[inline]
115 #[must_use]
116 pub fn relative_path(&self) -> Option<&str> {
117 self.data.rel_path.as_deref()
118 }
119
120 #[inline]
121 #[must_use]
122 pub fn document_uri(&self) -> Option<&DocumentUri> {
123 self.data.doc_uri.as_ref()
124 }
125
126 #[inline]
127 pub fn set_text(&self, s: String) -> bool {
128 let mut txt = self.text.lock();
129 if txt.text == s {
130 return false;
131 }
132 txt.text = s;
133 self.up_to_date
134 .store(false, std::sync::atomic::Ordering::SeqCst);
135 true
136 }
137
138 #[inline]
139 pub fn with_text<R>(&self, f: impl FnOnce(&str) -> R) -> R {
140 f(&self.text.lock().text)
141 }
142
143 #[inline]
144 pub fn html_up_to_date(&self) -> bool {
145 self.text.lock().html_up_to_date
146 }
147
148 pub fn set_html_up_to_date(&self) {
149 self.text.lock().html_up_to_date = true
150 }
151
152 #[inline]
153 pub fn delta(&self, text: String, range: Option<Range>) {
154 self.up_to_date
155 .store(false, std::sync::atomic::Ordering::SeqCst);
156 self.text.lock().delta(text, range);
157 }
158
159 #[inline]
164 #[must_use]
165 pub fn get_position(&self, pos: Position) -> usize {
166 self.text.lock().get_position(pos)
167 }
168
169 #[inline]
170 #[must_use]
171 pub fn has_annots(&self) -> bool {
172 self.data.doc_uri.is_some() && self.data.path.is_some()
173 }
174
175 #[allow(clippy::significant_drop_tightening)]
176 pub(crate) fn load_annotations_and<R>(
177 &self,
178 state: LSPState,
179 snify: bool,
180 f: impl FnOnce(&STeXParseDataI) -> R,
181 ) -> Option<R> {
182 let text_lock = self.text.lock();
183 let uri = self.data.doc_uri.as_ref()?;
184 let path = self.data.path.as_ref()?;
185
186 let mut docs = state.documents.write();
187 let mut vlock = state.verbalizations.lock();
188 let ignores = if snify {
190 let mut ret = Vec::new();
191 for line in text_lock.text.lines().rev() {
192 let trimmed = line.trim();
193 if !trimmed.is_empty() && !trimmed.starts_with('%') {
194 break;
195 }
196 let Some(line) = trimmed.strip_prefix("% srskip ") else {
197 continue;
198 };
199 for v in line.split(',') {
200 let trimmed = v.trim();
201 if let Some(name) = trimmed.strip_prefix("l:") {
202 ret.push(name);
203 }
204 }
205 }
206 ret
207 } else {
208 Vec::new()
209 };
210 let mut store = LSPStore::<true>::new(&mut docs, Some(&mut vlock), &ignores, snify);
211 let data = flams_stex::quickparse::stex::quickparse(
212 uri,
213 &text_lock.text,
214 path,
215 &AnyBackend::Global,
216 &mut store,
217 );
218 data.replace(&self.annotations);
219 self.up_to_date
220 .store(true, std::sync::atomic::Ordering::SeqCst);
221 drop(store);
222 drop(vlock);
223 drop(docs);
224 drop(text_lock);
226 let lock = self.annotations.lock();
231 Some(f(&lock))
232 }
233
234 pub fn is_up_to_date(&self) -> bool {
235 self.up_to_date.load(std::sync::atomic::Ordering::SeqCst)
236 }
237
238 #[inline]
239 #[must_use]
240 #[allow(clippy::significant_drop_tightening)]
241 pub async fn with_annots<R: Send + 'static>(
242 self,
243 state: LSPState,
244 snify: bool,
245 f: impl FnOnce(&STeXParseDataI) -> R + Send + 'static,
246 ) -> Option<R> {
247 if !self.has_annots() {
248 return None;
249 }
250 if self.is_up_to_date() {
251 let lock = self.annotations.lock();
252 if lock.is_empty() {
253 return None;
254 }
255 return Some(f(&lock));
256 }
257 let forced = self
258 .force_snify
259 .swap(false, std::sync::atomic::Ordering::AcqRel);
260 let snify = snify || forced;
261 match tokio::task::spawn_blocking(move || self.load_annotations_and(state, snify, f)).await
262 {
263 Ok(r) => r,
264 Err(e) => {
265 tracing::error!("Error computing annots: {}", e);
266 None
267 }
268 }
269 }
270
271 #[must_use]
272 #[allow(clippy::significant_drop_tightening)]
273 pub async fn with_annots_block<R: Send + 'static>(
274 self,
275 state: LSPState,
276 snify: bool,
277 f: impl FnOnce(&STeXParseDataI) -> R + Send + 'static,
278 ) -> Option<R> {
279 if !self.has_annots() {
280 return None;
281 }
282 if self.is_up_to_date() {
283 if self.annotations.lock().is_empty() {
284 return None;
285 }
286 let annot = self.annotations.clone();
287 return match tokio::task::spawn_blocking(move || f(&annot.lock())).await {
288 Ok(r) => Some(r),
289 Err(e) => {
290 tracing::error!("Error computing annots: {}", e);
291 None
292 }
293 };
294 }
295 match tokio::task::spawn_blocking(move || self.load_annotations_and(state, snify, f)).await
296 {
297 Ok(r) => r,
298 Err(e) => {
299 tracing::error!("Error computing annots: {}", e);
300 None
301 }
302 }
303 }
304
305 #[inline]
306 pub fn compute_annots(&self, state: LSPState, snify: bool) {
307 self.load_annotations_and(state, snify, |_| ());
308 }
309}
310
311#[derive(Debug)]
312struct LSPText {
313 text: String,
314 html_up_to_date: bool,
315}
316
317impl LSPText {
318 fn get_position(
319 &self,
320 Position {
321 mut line,
322 character,
323 }: Position,
324 ) -> usize {
325 let mut rest = self.text.as_str();
326 let mut off = 0;
327 while line > 0 {
328 if let Some(i) = rest.find(['\n', '\r']) {
329 off += i + 1;
330 if rest.as_bytes()[i] == b'\r' && rest.as_bytes().get(i + 1) == Some(&b'\n') {
331 off += 1;
332 rest = &rest[i + 2..];
333 } else {
334 rest = &rest[i + 1..];
335 }
336 line -= 1;
337 } else {
338 off = self.text.len();
339 rest = "";
340 break;
341 }
342 }
343 let next = rest
344 .chars()
345 .take(character as usize)
346 .map(char::len_utf8)
347 .sum::<usize>();
348 off += next;
349 off
350 }
351
352 fn get_range(&self, range: Range) -> (usize, usize) {
353 let Range { start, end } = range;
354 let off = PositionConverter::<LSPLineCol, ByteOffset>::new(self.text.as_str()).next_range(
355 StringRange {
356 start: LSPLineCol::new(start.line, start.character),
357 end: LSPLineCol::new(end.line, end.character),
358 },
359 );
360 (off.start.0, off.end.0)
361 }
362
363 #[allow(clippy::cast_possible_truncation)]
364 fn delta(&mut self, text: String, range: Option<Range>) {
365 let Some(range) = range else {
366 self.text = text;
367 return;
368 };
369 let (start, end) = self.get_range(range);
370 self.text.replace_range(start..end, &text);
371 self.html_up_to_date = false;
372 }
373}