1use std::{path::Path, sync::atomic::AtomicBool};
2
3use async_lsp::lsp_types::{Position, Range};
4use flams_ontology::uris::{ArchiveURI, DocumentURI, URIRefTrait};
5use flams_stex::quickparse::stex::{STeXParseData, STeXParseDataI};
6use flams_system::backend::{AnyBackend, Backend, GlobalBackend};
7use flams_utils::PathExt;
8
9use crate::{
10 state::{LSPState, UrlOrFile},
11 LSPStore,
12};
13
14#[derive(Debug, PartialEq, Eq)]
15struct DocumentData {
16 path: Option<std::sync::Arc<Path>>,
17 archive: Option<ArchiveURI>,
18 rel_path: Option<Box<str>>,
19 doc_uri: Option<DocumentURI>,
20}
21
22#[derive(Clone, Debug)]
23pub struct LSPDocument {
24 up_to_date: triomphe::Arc<AtomicBool>,
25 text: triomphe::Arc<parking_lot::Mutex<LSPText>>,
26 pub annotations: STeXParseData,
27 data: triomphe::Arc<DocumentData>,
28}
29impl PartialEq for LSPDocument {
30 #[inline]
31 fn eq(&self, other: &Self) -> bool {
32 self.data == other.data
33 }
34}
35
36#[cfg(windows)]
37const PREFIX: &str = "\\source\\";
38#[cfg(not(windows))]
39const PREFIX: &str = "/source/";
40
41impl LSPDocument {
42 #[allow(clippy::cast_possible_truncation)]
43 #[must_use]
44 pub fn new(text: String, lsp_uri: UrlOrFile) -> Self {
45 let path = if let UrlOrFile::File(p) = lsp_uri {
46 Some(p)
47 } else {
48 None
49 }; let default = || {
51 let path = path.as_ref()?.as_slash_str().into();
52 Some((ArchiveURI::no_archive(), Some(path)))
53 };
54 let ap = path
55 .as_ref()
56 .and_then(|path| {
57 GlobalBackend::get().archive_of(path, |a, rp| {
58 let uri = a.uri().owned();
59 let rp = rp.strip_prefix(PREFIX).map(|r| r.into());
60 (uri, rp)
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}");
75 None
76 }
77 }
78 })
79 });
80 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 flams_stex::quickparse::stex::quickparse(
183 uri,&lock.text, path,
184 &AnyBackend::Global(GlobalBackend::get()),
185 &mut store);
186 data.replace(&self.annotations);
188 self.up_to_date
189 .store(true, std::sync::atomic::Ordering::SeqCst);
190 drop(store);
191 drop(docs);
192 drop(lock);
194 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}