Skip to main content

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