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 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(crate) ignore_verbalizations:triomphe::Arc<parking_lot::Mutex<Vec<(String, Option<StringRange<LSPLineCol>>)>>>,
33    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        }; //lsp_uri.to_file_path().ok().map(Into::into);
53        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        //tracing::info!("Document: {lsp_uri}\n - {doc_uri:?}\n - [{archive:?}]{{{rel_path:?}}}");
86        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            //ignore_verbalizations: triomphe::Arc::new(parking_lot::Mutex::new(Vec::new())),
97            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]#[must_use]
160    pub fn get_range(&self, range: Range) -> (usize, usize) {
161        self.text.lock().get_range(range)
162    }*/
163    #[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 = self.ignore_verbalizations.lock();
189        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        //tracing::info!("quickparse took {t}");
225        drop(text_lock);
226        /*let path = path.clone();
227        let _ = tokio::task::spawn_blocking(move || {
228          state.relint_dependents(path);
229        });*/
230        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}