texlab/
workspace.rs

1use std::{
2    fs::{self, FileType},
3    path::{Path, PathBuf},
4    sync::{Arc, Mutex},
5};
6
7use anyhow::Result;
8use crossbeam_channel::Sender;
9use lsp_types::Url;
10use notify::Watcher;
11use once_cell::sync::Lazy;
12use petgraph::{graphmap::DiGraphMap, visit::Dfs};
13use rustc_hash::{FxHashMap, FxHashSet};
14
15use crate::{
16    component_db::COMPONENT_DATABASE, distro::Resolver, syntax::latex::ExplicitLink, Document,
17    DocumentLanguage, Environment,
18};
19
20#[derive(Debug, Clone)]
21pub enum WorkspaceEvent {
22    Changed(Workspace, Document),
23}
24
25#[derive(Debug, Clone, Default)]
26pub struct Workspace {
27    documents_by_uri: FxHashMap<Arc<Url>, Document>,
28    pub viewport: FxHashSet<Arc<Url>>,
29    pub listeners: Vec<Sender<WorkspaceEvent>>,
30    pub environment: Environment,
31    watcher: Option<Arc<Mutex<notify::RecommendedWatcher>>>,
32    watched_dirs: Arc<Mutex<FxHashSet<PathBuf>>>,
33}
34
35impl Workspace {
36    pub fn new(environment: Environment) -> Self {
37        Self {
38            environment,
39            ..Self::default()
40        }
41    }
42
43    pub fn get(&self, uri: &Url) -> Option<Document> {
44        self.documents_by_uri.get(uri).cloned()
45    }
46
47    pub fn remove(&mut self, uri: &Url) {
48        self.documents_by_uri.remove(uri);
49    }
50
51    pub fn iter<'a>(&'a self) -> impl Iterator<Item = Document> + 'a {
52        self.documents_by_uri.values().cloned()
53    }
54
55    pub fn register_watcher(&mut self, watcher: notify::RecommendedWatcher) {
56        self.watcher = Some(Arc::new(Mutex::new(watcher)));
57    }
58
59    pub fn watch_dir(&self, path: &Path) {
60        if let Some(watcher) = &self.watcher {
61            if self.watched_dirs.lock().unwrap().insert(path.to_owned()) {
62                let _ = watcher
63                    .lock()
64                    .unwrap()
65                    .watch(path, notify::RecursiveMode::NonRecursive);
66            }
67        }
68    }
69
70    pub fn open(
71        &mut self,
72        uri: Arc<Url>,
73        text: Arc<String>,
74        language: DocumentLanguage,
75    ) -> Result<Document> {
76        if uri.scheme() == "file" {
77            if let Ok(mut path) = uri.to_file_path() {
78                path.pop();
79                self.watch_dir(&path);
80            }
81        }
82
83        log::debug!("(Re)Loading document: {}", uri);
84        let document = Document::parse(&self.environment, Arc::clone(&uri), text, language);
85
86        self.documents_by_uri
87            .insert(Arc::clone(&uri), document.clone());
88
89        for listener in &self.listeners {
90            listener.send(WorkspaceEvent::Changed(self.clone(), document.clone()))?;
91        }
92
93        self.expand_parent(&document);
94        self.expand_children(&document);
95        Ok(document)
96    }
97
98    pub fn reload(&mut self, path: PathBuf) -> Result<Option<Document>> {
99        let uri = Arc::new(Url::from_file_path(path.clone()).unwrap());
100        if self.is_open(&uri) || !(uri.as_str().ends_with(".log") || uri.as_str().ends_with(".aux"))
101        {
102            return Ok(self.documents_by_uri.get(&uri).cloned());
103        }
104
105        if let Some(language) = DocumentLanguage::by_path(&path) {
106            let data = fs::read(&path)?;
107            let text = Arc::new(String::from_utf8_lossy(&data).into_owned());
108            Ok(Some(self.open(uri, text, language)?))
109        } else {
110            Ok(None)
111        }
112    }
113
114    pub fn load(&mut self, path: PathBuf) -> Result<Option<Document>> {
115        let uri = Arc::new(Url::from_file_path(path.clone()).unwrap());
116
117        if let Some(document) = self.documents_by_uri.get(&uri).cloned() {
118            return Ok(Some(document));
119        }
120
121        let data = fs::read(&path)?;
122        let text = Arc::new(String::from_utf8_lossy(&data).into_owned());
123        if let Some(language) = DocumentLanguage::by_path(&path) {
124            Ok(Some(self.open(uri, text, language)?))
125        } else {
126            Ok(None)
127        }
128    }
129
130    pub fn close(&mut self, uri: &Url) {
131        self.viewport.remove(uri);
132    }
133
134    pub fn is_open(&self, uri: &Url) -> bool {
135        self.viewport.contains(uri)
136    }
137
138    pub fn slice(&self, uri: &Url) -> Self {
139        let all_uris: Vec<_> = self.documents_by_uri.keys().cloned().collect();
140
141        all_uris
142            .iter()
143            .position(|u| u.as_ref() == uri)
144            .map(|start| {
145                let mut edges = Vec::new();
146                for (i, uri) in all_uris.iter().enumerate() {
147                    let document = self.documents_by_uri.get(uri);
148                    if let Some(data) = document
149                        .as_ref()
150                        .and_then(|document| document.data().as_latex())
151                    {
152                        let extras = &data.extras;
153                        let mut all_targets =
154                            vec![&extras.implicit_links.aux, &extras.implicit_links.log];
155                        for link in &extras.explicit_links {
156                            all_targets.push(&link.targets);
157                        }
158
159                        for targets in all_targets {
160                            for target in targets {
161                                if let Some(j) = all_uris.iter().position(|uri| uri == target) {
162                                    edges.push((i, j, ()));
163
164                                    if target.as_str().ends_with(".tex")
165                                        || target.as_str().ends_with(".bib")
166                                        || target.as_str().ends_with(".rnw")
167                                    {
168                                        edges.push((j, i, ()));
169                                    }
170
171                                    break;
172                                }
173                            }
174                        }
175                    }
176                }
177
178                let mut slice = self.clone();
179                slice.documents_by_uri = FxHashMap::default();
180                let graph = DiGraphMap::from_edges(edges);
181                let mut dfs = Dfs::new(&graph, start);
182                while let Some(i) = dfs.next(&graph) {
183                    let uri = &all_uris[i];
184                    let doc = self.documents_by_uri[uri].clone();
185                    slice.documents_by_uri.insert(Arc::clone(uri), doc);
186                }
187
188                slice
189            })
190            .unwrap_or_default()
191    }
192
193    pub fn find_parent(&self, uri: &Url) -> Option<Document> {
194        self.slice(uri)
195            .documents_by_uri
196            .values()
197            .find(|document| {
198                document.data().as_latex().map_or(false, |data| {
199                    data.extras.has_document_environment
200                        && !data
201                            .extras
202                            .explicit_links
203                            .iter()
204                            .filter_map(ExplicitLink::as_component_name)
205                            .any(|name| name == "subfiles.cls")
206                })
207            })
208            .cloned()
209    }
210
211    fn expand_parent(&mut self, document: &Document) {
212        let all_current_paths = self
213            .documents_by_uri
214            .values()
215            .filter_map(|doc| doc.uri().to_file_path().ok())
216            .collect::<FxHashSet<_>>();
217
218        if document.uri().scheme() == "file" {
219            if let Ok(mut path) = document.uri().to_file_path() {
220                while path.pop() && self.find_parent(document.uri()).is_none() {
221                    std::fs::read_dir(&path)
222                        .into_iter()
223                        .flatten()
224                        .filter_map(Result::ok)
225                        .filter(|entry| entry.file_type().ok().filter(FileType::is_file).is_some())
226                        .map(|entry| entry.path())
227                        .filter(|path| {
228                            matches!(
229                                DocumentLanguage::by_path(path),
230                                Some(DocumentLanguage::Latex)
231                            )
232                        })
233                        .filter(|path| !all_current_paths.contains(path))
234                        .for_each(|path| {
235                            let _ = self.load(path);
236                        });
237                }
238            }
239        }
240    }
241
242    fn expand_children(&mut self, document: &Document) {
243        if let Some(data) = document.data().as_latex() {
244            let extras = &data.extras;
245            let mut all_targets = vec![&extras.implicit_links.aux, &extras.implicit_links.log];
246            for link in &extras.explicit_links {
247                if should_follow_link(link, &self.environment.resolver) {
248                    all_targets.push(&link.targets);
249                }
250            }
251
252            for targets in all_targets {
253                for path in targets
254                    .iter()
255                    .filter(|uri| uri.scheme() == "file" && uri.fragment().is_none())
256                    .filter_map(|uri| uri.to_file_path().ok())
257                {
258                    if self.load(path).is_ok() {
259                        break;
260                    }
261                }
262            }
263        }
264    }
265}
266
267static HOME_DIR: Lazy<Option<PathBuf>> = Lazy::new(|| dirs::home_dir());
268
269fn should_follow_link(link: &ExplicitLink, resolver: &Resolver) -> bool {
270    match link.as_component_name() {
271        Some(name) if COMPONENT_DATABASE.find(&name).is_some() => false,
272        Some(name) => {
273            let file = resolver.files_by_name.get(name.as_str());
274            let home = HOME_DIR.as_deref();
275            match (file, home) {
276                (Some(file), Some(home)) => file.starts_with(home),
277                (Some(_), None) => false,
278                (None, Some(_)) => true,
279                (None, None) => true,
280            }
281        }
282        None => true,
283    }
284}