Skip to main content

strop_engine/editor/document/
directory.rs

1//! One real Directory buffer for every supported filesystem namespace.
2use super::{Document, DocumentSource, JumpRecord};
3use imbl::OrdMap;
4use std::fmt::Write;
5use std::sync::Arc;
6use strop_core::{id::LineIndex, Buffer};
7use strop_workspace::{
8    DirectoryEntry, EntryKind, EntryName, ListingState, Observation, ResourceLocation,
9};
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct Directory {
13    pub location: ResourceLocation,
14    pub entries: Arc<[DirectoryEntry]>,
15    pub visible: Arc<[usize]>,
16    pub filter: String,
17    pub summary: String,
18    pub state: ListingState,
19    pub stale: Option<String>,
20    pub view_revision: strop_core::id::BufferRevision,
21    #[serde(with = "entry_map")]
22    pub marked: OrdMap<EntryName, Observation>,
23    /// Verified name changes used only to restore positions on the next snapshot.
24    /// Navigation continues to describe the displayed (possibly stale) listing.
25    #[serde(with = "entry_map")]
26    restoration: OrdMap<EntryName, Option<ResourceLocation>>,
27    #[serde(skip)]
28    pub connection: Option<strop_remote::ConnectionLease>,
29    pub return_to: Option<JumpRecord>,
30    pub draft: Option<crate::editor::filesystem::draft::Draft>,
31}
32
33impl Directory {
34    pub fn from_listing(listed: strop_fs::ListedDirectory) -> Self {
35        let mut source = Self::new(listed.snapshot);
36        source.connection = listed.connection;
37        source
38    }
39    pub fn new(snapshot: strop_workspace::DirectorySnapshot) -> Self {
40        Self {
41            location: snapshot.location,
42            visible: (0..snapshot.entries.len()).collect(),
43            entries: snapshot.entries,
44            state: snapshot.state,
45            filter: String::new(),
46            stale: None,
47            marked: OrdMap::new(),
48            restoration: OrdMap::new(),
49            connection: None,
50            return_to: None,
51            draft: None,
52            view_revision: strop_core::id::BufferRevision::new(0),
53            summary: "folder · hidden on · no ignores".into(),
54        }
55    }
56
57    pub(crate) fn record_relocation(
58        &mut self,
59        source: &ResourceLocation,
60        destination: Option<&ResourceLocation>,
61    ) {
62        let updates: Vec<_> = self
63            .restoration
64            .iter()
65            .filter_map(|(name, location)| {
66                let location = location.as_ref()?;
67                (location.filesystem == source.filesystem
68                    && location.path.starts_with(&source.path))
69                .then(|| {
70                    (
71                        name.clone(),
72                        destination.and_then(|target| location.relocated(source, target)),
73                    )
74                })
75            })
76            .collect();
77        for (name, location) in updates {
78            self.restoration.insert(name, location);
79        }
80        if let Some(index) = self.entry_index(source) {
81            let name = self.entries[index].name.clone();
82            if !self.restoration.contains_key(&name) {
83                self.restoration.insert(name, destination.cloned());
84            }
85        }
86        debug_assert!(self.restoration.len() <= self.entries.len());
87    }
88
89    pub(crate) fn restoration_location(&self, line: LineIndex) -> Option<ResourceLocation> {
90        if self.draft.is_some() {
91            return self.entry_location(line);
92        }
93        let entry = self.entry(line)?;
94        self.restoration
95            .get(&entry.name)
96            .cloned()
97            .unwrap_or_else(|| Some(self.location_of(entry)))
98    }
99    /// Worker-side projection. Filenames are escaped for display, never decoded
100    /// back into resources by navigation, marks or operation admission.
101    pub fn text(&self) -> String {
102        let mut text = String::new();
103        let _ = writeln!(
104            text,
105            "{} — {} / {} entries{}",
106            self.location.label(),
107            self.visible.len(),
108            self.entries.len(),
109            match self.state {
110                ListingState::Complete => "",
111                ListingState::Limited { .. } => " — LIMITED",
112                ListingState::Failed { .. } => " — INCOMPLETE",
113            }
114        );
115        text.push_str("../\n");
116        for &index in self.visible.iter() {
117            let entry = &self.entries[index];
118            text.push_str(&entry.name.display());
119            match entry.observation.kind {
120                EntryKind::Directory => text.push('/'),
121                EntryKind::SymbolicLink => text.push('@'),
122                _ => {}
123            }
124            text.push_str("  ");
125            text.push(entry.observation.kind.marker());
126            match entry.observation.permissions {
127                Some(permissions) => {
128                    let _ = write!(text, "{permissions}");
129                }
130                None => text.push_str("?????????"),
131            }
132            match entry.observation.size {
133                Some(size) => {
134                    let _ = write!(text, "  {size}");
135                }
136                None => text.push_str("  ?"),
137            }
138            if entry.error.is_some() {
139                text.push_str("  unavailable");
140            }
141            text.push('\n');
142        }
143        text
144    }
145    pub fn parent(&self) -> Option<ResourceLocation> {
146        strop_fs::parent(&self.location)
147    }
148    pub fn entry(&self, line: LineIndex) -> Option<&DirectoryEntry> {
149        line.get()
150            .checked_sub(2)
151            .and_then(|row| self.visible.get(row))
152            .and_then(|&index| self.entries.get(index))
153    }
154    pub fn entry_location(&self, line: LineIndex) -> Option<ResourceLocation> {
155        if let Some(draft) = &self.draft {
156            return draft.location(line.get());
157        }
158        self.entry(line).map(|entry| self.location_of(entry))
159    }
160    pub fn location_of(&self, entry: &DirectoryEntry) -> ResourceLocation {
161        ResourceLocation {
162            filesystem: self.location.filesystem.clone(),
163            path: self.location.path.join(entry.name.as_path()),
164        }
165    }
166    pub fn line_for(&self, location: &ResourceLocation) -> Option<LineIndex> {
167        if let Some(draft) = &self.draft {
168            return (0..=super::super::filesystem::draft::ROW_LIMIT)
169                .find(|row| draft.location(*row).as_ref() == Some(location))
170                .map(LineIndex::new);
171        }
172        self.visible
173            .binary_search(&self.entry_index(location)?)
174            .ok()
175            .map(|row| LineIndex::new(row + 2))
176    }
177    pub(crate) fn entry_index(&self, location: &ResourceLocation) -> Option<usize> {
178        if location.filesystem != self.location.filesystem
179            || location.path.parent() != Some(self.location.path.as_path())
180        {
181            return None;
182        }
183        let name = location.path.file_name()?;
184        let split = self
185            .entries
186            .partition_point(|entry| entry.observation.kind == EntryKind::Directory);
187        for (offset, entries) in [(0, &self.entries[..split]), (split, &self.entries[split..])] {
188            if let Ok(index) =
189                entries.binary_search_by(|entry| entry.name.as_path().as_os_str().cmp(name))
190            {
191                return Some(offset + index);
192            }
193        }
194        None
195    }
196    pub fn toggle_mark(&mut self, line: LineIndex) -> bool {
197        let Some(entry) = self.entry(line) else {
198            return false;
199        };
200        let name = entry.name.clone();
201        let observation = entry.observation.clone();
202        if self.marked.remove(&name).is_none() {
203            self.marked.insert(name, observation);
204        }
205        true
206    }
207    pub fn validate(&self) -> bool {
208        self.location.path.is_absolute()
209            && self
210                .draft
211                .as_ref()
212                .is_none_or(|draft| draft.root == self.location && draft.valid())
213            && !self
214                .location
215                .path
216                .as_os_str()
217                .as_encoded_bytes()
218                .contains(&0)
219            && self.visible.iter().all(|&index| index < self.entries.len())
220            && self.visible.windows(2).all(|pair| pair[0] < pair[1])
221            && self
222                .entries
223                .iter()
224                .map(|entry| &entry.name)
225                .collect::<std::collections::HashSet<_>>()
226                .len()
227                == self.entries.len()
228            && self.entries.windows(2).all(|pair| {
229                let group = |entry: &DirectoryEntry| {
230                    usize::from(entry.observation.kind != EntryKind::Directory)
231                };
232                (group(&pair[0]), &pair[0].name) < (group(&pair[1]), &pair[1].name)
233            })
234    }
235}
236
237impl Document {
238    pub fn directory(mut buffer: Buffer, mut source: Directory) -> Self {
239        debug_assert!(
240            source.validate(),
241            "directory rows have one native identity each"
242        );
243        source.view_revision = buffer.revision();
244        buffer.path = None;
245        buffer.name = Some(format!("directory {}", source.location.label()));
246        buffer.readonly = source.draft.as_ref().is_none_or(|draft| !draft.editable());
247        Self {
248            buf: buffer,
249            syntax_hint: None,
250            indent: super::Indent::default(),
251            indent_override: super::IndentOverride::default(),
252            detection: None,
253            source: DocumentSource::Directory(Box::new(source)),
254        }
255    }
256    pub fn directory_metadata_ref(&self) -> Option<&Directory> {
257        match &self.source {
258            DocumentSource::Directory(source) => Some(source),
259            _ => None,
260        }
261    }
262    pub(crate) fn directory_metadata_mut(&mut self) -> Option<&mut Directory> {
263        match &mut self.source {
264            DocumentSource::Directory(source) => Some(source),
265            _ => None,
266        }
267    }
268}
269
270mod entry_map {
271    use super::*;
272    use serde::Deserialize;
273    pub fn serialize<S: serde::Serializer, T: serde::Serialize + Clone>(
274        value: &OrdMap<EntryName, T>,
275        serializer: S,
276    ) -> Result<S::Ok, S::Error> {
277        serializer.collect_seq(value.iter())
278    }
279    pub fn deserialize<'de, D: serde::Deserializer<'de>, T: serde::Deserialize<'de> + Clone>(
280        deserializer: D,
281    ) -> Result<OrdMap<EntryName, T>, D::Error> {
282        let entries = Vec::<(EntryName, T)>::deserialize(deserializer)?;
283        let count = entries.len();
284        let result: OrdMap<_, _> = entries.into_iter().collect();
285        if result.len() != count {
286            return Err(serde::de::Error::custom(
287                "duplicate native directory entry key",
288            ));
289        }
290        Ok(result)
291    }
292}