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