strop_engine/editor/document/
directory.rs1use 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 #[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 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}