flams_lsp/
documents.rs

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        }; //lsp_uri.to_file_path().ok().map(Into::into);
48        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        //tracing::info!("Document: {lsp_uri}\n - {doc_uri:?}\n - [{archive:?}]{{{rel_path:?}}}");
78        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    //let (data,t) = measure(||
179      flams_stex::quickparse::stex::quickparse(
180      uri,&lock.text, path,
181      &AnyBackend::Global,
182      &mut store);
183        //);
184        data.replace(&self.annotations);
185        self.up_to_date
186            .store(true, std::sync::atomic::Ordering::SeqCst);
187        drop(store);
188        drop(docs);
189        //tracing::info!("quickparse took {t}");
190        drop(lock);
191        /*let path = path.clone();
192        let _ = tokio::task::spawn_blocking(move || {
193          state.relint_dependents(path);
194        });*/
195        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}