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}