strop_engine/editor/directory/
mod.rs1mod filter;
4mod jobs;
5pub(crate) use filter::apply_filter;
6#[cfg(test)]
7mod tests;
8use super::io::{IoEvent, OpenIntent, Opened};
9use super::{Directory, Editor, Key};
10use crate::files::FileTarget;
11use std::collections::HashMap;
12use strop_core::id::{BufferRevision, DocumentId, LineIndex};
13use strop_core::worker::{self, CancelReason, Completion, FailureKind, Outcome, Ticket, WorkerId};
14use strop_workspace::{Filesystem, ResourceLocation};
15
16#[derive(Default)]
17pub(crate) struct DirectoryState {
18 pub filters: HashMap<WorkerId, FilterKey>,
19 pub views: HashMap<ResourceLocation, SavedDirectoryView>,
20 order: std::collections::VecDeque<ResourceLocation>,
21}
22#[derive(Clone)]
23pub(crate) struct SavedDirectoryView {
24 pub record: super::jumps::JumpRecord,
25 pub directory: Directory,
26 row: usize,
27 top: usize,
28 column: usize,
29}
30#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
31pub(crate) enum DirectoryTask {
32 Filter,
33 Reload,
34 EditNames,
35}
36#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
37pub struct FilterKey {
38 pub document: DocumentId,
39 pub revision: BufferRevision,
40 pub location: ResourceLocation,
41 pub query: String,
42 pub focus: u64,
43 pub(crate) task: DirectoryTask,
44 pub(crate) draft: Option<WorkerId>,
45}
46
47impl Editor {
48 pub(crate) fn relocate_directory_history(
49 &mut self,
50 source: Option<&ResourceLocation>,
51 destination: Option<&ResourceLocation>,
52 removed: bool,
53 ) {
54 let map = |location: &ResourceLocation| -> Option<ResourceLocation> {
55 if let Some(source) = source.filter(|source| {
56 source.filesystem == location.filesystem && location.path.starts_with(&source.path)
57 }) {
58 if removed {
59 return None;
60 }
61 if let Some(destination) = destination {
62 return location.relocated(source, destination);
63 }
64 }
65 Some(location.clone())
66 };
67 let previous = std::mem::take(&mut self.directories.views);
68 let mut moved = Vec::new();
69 for (location, mut view) in previous {
70 let Some(target) = map(&location) else {
71 continue;
72 };
73 if removed || destination.is_some() {
74 if let Some(source) = source {
75 view.directory.record_relocation(source, destination);
76 }
77 }
78 view.directory.location = target.clone();
79 if source.into_iter().chain(destination).any(|resource| {
80 resource.filesystem == target.filesystem
81 && (resource.path.parent() == Some(target.path.as_path())
82 || target.path.starts_with(&resource.path))
83 }) {
84 view.directory.stale = Some("filesystem changed; refresh required".into());
85 }
86 if target != location {
87 moved.push((target, view));
88 } else {
89 self.directories.views.insert(target, view);
90 }
91 }
92 self.directories.views.extend(moved);
93 let mut seen = std::collections::HashSet::new();
94 self.directories.order = std::mem::take(&mut self.directories.order)
95 .into_iter()
96 .filter_map(|location| map(&location))
97 .filter(|location| seen.insert(location.clone()))
98 .collect();
99 }
100 pub fn directory(&self) -> Option<&Directory> {
101 if self.docs.is_empty() {
102 return None;
103 }
104 self.docs
105 .get(self.current())
106 .and_then(|doc| doc.directory_metadata_ref())
107 }
108 pub fn directory_visibility_summary(&self) -> Option<&str> {
109 let source = self.directory()?;
110 Some(if source.stale.is_some() {
111 "stale directory — refresh required"
112 } else if !source.state.is_complete() {
113 "incomplete directory listing"
114 } else {
115 &source.summary
116 })
117 }
118 pub(crate) fn remember_directory_view(&mut self) {
119 let Some(source) = self.directory() else {
120 return;
121 };
122 if source.draft.is_some() || source.view_revision != self.buf().revision() {
123 return;
124 }
125 let location = source.location.clone();
126 let saved = SavedDirectoryView {
127 directory: source.clone(),
128 record: self.jump_record(),
129 row: self.buf().line_of(self.head()),
130 top: self.view_top(),
131 column: self.buf().col_of(self.head()),
132 };
133 self.directories.views.insert(location.clone(), saved);
134 self.directories.order.retain(|old| old != &location);
135 self.directories.order.push_back(location);
136 while self.directories.order.len() > 16
137 || self
138 .directories
139 .views
140 .values()
141 .map(|view| view.directory.entries.len())
142 .sum::<usize>()
143 > 200_000
144 {
145 let Some(old) = self.directories.order.pop_front() else {
146 break;
147 };
148 self.directories.views.remove(&old);
149 }
150 }
151 pub(crate) fn restore_directory_view(&mut self, document: DocumentId) {
152 let Some(source) = self
153 .docs
154 .get(document)
155 .and_then(|doc| doc.directory_metadata_ref())
156 else {
157 return;
158 };
159 if let Some(saved) = self.directories.views.get(&source.location).cloned() {
160 let mut view = saved.record;
161 if view.document != document {
162 let locate = |line| {
163 saved
164 .directory
165 .restoration_location(LineIndex::new(line))
166 .and_then(|location| source.line_for(&location))
167 .map(|line| line.get())
168 .unwrap_or(line.min(1))
169 };
170 let row = locate(saved.row);
171 let top = locate(saved.top);
172 let buffer = &self.doc(document).buf;
173 view.document = document;
174 view.offset = buffer.clamp_boundary(
175 (buffer.line_start(row) + saved.column).min(buffer.line_end(row)),
176 );
177 view.anchor = view.offset;
178 view.extras.clear();
179 view.view_top = buffer.line_start(top);
180 }
181 self.jump_to(view);
182 } else if source.entry(LineIndex::new(2)).is_some() {
183 self.set_head(self.buf().line_start(2));
184 } else {
185 self.set_head(self.buf().line_start(1));
186 }
187 }
188 pub(crate) fn open_context(&self) -> ResourceLocation {
191 self.directory().map_or_else(
192 || ResourceLocation::local(self.cwd.clone()),
193 |directory| directory.location.clone(),
194 )
195 }
196 pub(crate) fn directory_context(&self) -> ResourceLocation {
198 if let Some(source) = self.directory() {
199 return source.location.clone();
200 }
201 let source = self
202 .navigation_source()
203 .map_or(self.current(), |(source, _)| source);
204 if let Some(target) = self.doc(source).file_target(&self.cwd) {
205 if let Some(location) = target.resource_location() {
206 if let Some(parent) = strop_fs::parent(&location) {
207 return parent;
208 }
209 }
210 }
211 ResourceLocation::local(self.cwd.clone())
212 }
213 pub(crate) fn directory_operand(&self, value: &str) -> Result<FileTarget, String> {
214 Self::directory_operand_from(self.directory_context(), value)
215 }
216 pub(crate) fn directory_operand_from(
217 mut context: ResourceLocation,
218 value: &str,
219 ) -> Result<FileTarget, String> {
220 if value.starts_with("ssh://")
221 || value.starts_with("file://")
222 || value.starts_with("container:")
223 {
224 return FileTarget::parse(value.into()).map_err(|error| error.to_string());
225 }
226 context.path = context.path.join(value);
227 FileTarget::from_location(&context).map_err(|error| error.to_string())
228 }
229 pub(crate) fn browse_directory(&mut self, operand: &str) -> Result<(), String> {
230 let target = if !operand.is_empty() {
231 Self::directory_operand_from(self.open_context(), operand)?
232 } else if let Some(source) = self.directory() {
233 FileTarget::from_location(&source.location).map_err(|error| error.to_string())?
234 } else {
235 let source = self
236 .navigation_source()
237 .map_or(self.current(), |(source, _)| source);
238 let parent = self
239 .doc(source)
240 .file_target(&self.cwd)
241 .and_then(|target| target.resource_location())
242 .and_then(|location| strop_fs::parent(&location))
243 .unwrap_or_else(|| ResourceLocation::local(self.cwd.clone()));
244 FileTarget::from_location(&parent).map_err(|error| error.to_string())?
245 };
246 self.push_jump();
247 self.request_target(target, OpenIntent::Browse);
248 Ok(())
249 }
250 pub fn reveal_source(&mut self) {
251 if self.directory().is_some() {
252 return;
253 }
254 let source = self
255 .navigation_source()
256 .map_or(self.current(), |(source, _)| source);
257 let child = self
258 .doc(source)
259 .file_target(&self.cwd)
260 .and_then(|target| target.resource_location());
261 let Some(child) = child else {
262 if let Err(error) = self.browse_directory("") {
263 self.message = error;
264 }
265 return;
266 };
267 let Some(parent) = strop_fs::parent(&child) else {
268 self.message = "resource has no parent directory".into();
269 return;
270 };
271 match FileTarget::from_location(&parent) {
272 Ok(target) => {
273 self.push_jump();
274 self.request_target(target, OpenIntent::DirectoryParent { child });
275 }
276 Err(error) => self.message = error.to_string(),
277 }
278 }
279 pub(crate) fn refresh_directory(&mut self) -> bool {
280 let Some(location) = self.directory().map(|source| source.location.clone()) else {
281 return false;
282 };
283 match FileTarget::from_location(&location) {
284 Ok(target) => self.request_target(target, OpenIntent::Refresh),
285 Err(error) => self.message = error.to_string(),
286 }
287 true
288 }
289 pub(crate) fn directory_key(&mut self, key: Key) -> bool {
290 if !matches!(key, Key::Enter | Key::Backspace | Key::Char('-')) {
291 return false;
292 }
293 let Some(source) = self.directory() else {
294 return false;
295 };
296 if source.draft.is_some() || !self.buf().readonly {
297 return false;
298 }
299 if source.view_revision != self.buf().revision() {
300 self.message = "directory presentation changed; refresh before navigating".into();
301 return true;
302 }
303 let line = self.buf().line_of(self.head());
304 let parent = matches!(key, Key::Backspace | Key::Char('-')) || line == 1;
305 let (location, intent) = if parent {
306 (
307 source.parent(),
308 OpenIntent::DirectoryParent {
309 child: source.location.clone(),
310 },
311 )
312 } else {
313 let entry = source.entry(LineIndex::new(line));
314 (
315 entry.map(|entry| source.location_of(entry)),
316 if entry.is_some_and(|entry| {
317 entry.observation.kind == strop_workspace::EntryKind::Directory
318 }) {
319 OpenIntent::Browse
320 } else {
321 OpenIntent::Switch { readonly: false }
322 },
323 )
324 };
325 match location.map(|location| FileTarget::from_location(&location)) {
326 Some(Ok(target)) => {
327 self.push_jump();
328 self.request_target(target, intent);
329 }
330 Some(Err(error)) => self.message = error.to_string(),
331 None => self.message = "no directory entry at this position".into(),
332 }
333 true
334 }
335}