Skip to main content

strop_engine/editor/document/
remote.rs

1//! Remote provenance stays with its real buffer. Live leases never cross replay.
2use super::{Document, DocumentSource, JumpRecord};
3use std::fmt::Write;
4use strop_core::Buffer;
5use strop_remote::{
6    ConnectionLease, ReadSelection, RemoteDirectorySnapshot, RemoteEntry, RemoteEntryKind,
7    RemoteSnapshot, RemoteWindow,
8};
9use strop_workspace::RemoteFile;
10
11#[derive(Clone)]
12pub struct RemoteDocument {
13    pub file: RemoteFile,
14    pub window: RemoteWindow,
15    pub selection: ReadSelection,
16    pub connection: Option<ConnectionLease>,
17    pub return_to: Option<JumpRecord>,
18    pub(crate) write: Option<crate::editor::remote::save::WritePermit>,
19}
20impl std::fmt::Debug for RemoteDocument {
21    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        formatter
23            .debug_struct("RemoteDocument")
24            .field("file", &self.file)
25            .field("window", &self.window)
26            .field("selection", &self.selection)
27            .field("connected", &self.connection.is_some())
28            .field("write_authorized", &self.write.is_some())
29            .finish()
30    }
31}
32
33#[derive(Clone)]
34pub struct RemoteDirectory {
35    pub directory: RemoteFile,
36    pub entries: std::sync::Arc<[RemoteEntry]>,
37    pub visible: Vec<usize>,
38    pub filter: String,
39    pub connection: Option<ConnectionLease>,
40    pub return_to: Option<JumpRecord>,
41}
42impl std::fmt::Debug for RemoteDirectory {
43    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        formatter
45            .debug_struct("RemoteDirectory")
46            .field("directory", &self.directory)
47            .field("entries", &self.entries.len())
48            .field("filter", &self.filter)
49            .field("connected", &self.connection.is_some())
50            .finish()
51    }
52}
53impl RemoteDirectory {
54    pub fn text(&self) -> String {
55        let size_width = self
56            .visible
57            .iter()
58            .filter_map(|&index| self.entries[index].size)
59            .map(|size| size.get().checked_ilog10().unwrap_or(0) as usize + 1)
60            .max()
61            .unwrap_or(1)
62            .max(5);
63        let mut text = String::new();
64        // fmt::Write on String is infallible; parent attributes were not fetched.
65        let _ = writeln!(text, "d????????? {:>size_width$} ../", "?");
66        for &index in &self.visible {
67            let entry = &self.entries[index];
68            text.push(entry.kind.marker());
69            match entry.permissions {
70                Some(permissions) => {
71                    let _ = write!(text, "{permissions}");
72                }
73                None => text.push_str("?????????"),
74            }
75            match entry.size {
76                Some(size) => {
77                    let _ = write!(text, " {:>size_width$} ", size.get());
78                }
79                None => {
80                    let _ = write!(text, " {:>size_width$} ", "?");
81                }
82            }
83            let name = entry
84                .file
85                .path()
86                .file_name()
87                .unwrap_or_default()
88                .to_string_lossy();
89            text.push_str(&strop_core::layout::printable_text(name));
90            if entry.kind == RemoteEntryKind::Directory {
91                text.push('/');
92            }
93            if entry.kind == RemoteEntryKind::SymbolicLink {
94                text.push('@');
95            }
96            text.push('\n');
97        }
98        text
99    }
100    pub fn parent(&self) -> Result<Option<RemoteFile>, strop_workspace::AddressError> {
101        self.directory
102            .path()
103            .parent()
104            .map(|parent| self.directory.with_path(parent.to_path_buf()))
105            .transpose()
106    }
107    pub fn entry(&self, line: strop_core::id::LineIndex) -> Option<&RemoteEntry> {
108        line.get()
109            .checked_sub(1)
110            .and_then(|line| self.visible.get(line))
111            .and_then(|&index| self.entries.get(index))
112    }
113    /// Entries are grouped directory-first then native-path sorted. Search
114    /// both groups and the sorted visible mapping without scanning the listing.
115    pub(crate) fn line_for(&self, file: &RemoteFile) -> Option<strop_core::id::LineIndex> {
116        if file.endpoint() != self.directory.endpoint() {
117            return None;
118        }
119        let split = self
120            .entries
121            .partition_point(|entry| entry.kind == RemoteEntryKind::Directory);
122        for (offset, entries) in [(0, &self.entries[..split]), (split, &self.entries[split..])] {
123            if let Ok(index) = entries.binary_search_by(|entry| {
124                entry.file.path().as_os_str().cmp(file.path().as_os_str())
125            }) {
126                return self
127                    .visible
128                    .binary_search(&(offset + index))
129                    .ok()
130                    .map(|row| strop_core::id::LineIndex::new(row + 1));
131            }
132        }
133        None
134    }
135}
136
137impl Document {
138    pub fn remote(mut buffer: Buffer, source: RemoteDocument) -> Self {
139        debug_assert!(
140            buffer.path.is_none(),
141            "remote identity cannot become a local file"
142        );
143        buffer.name = Some(source.file.to_string());
144        buffer.readonly = true;
145        let detection = Some(super::detect_indent(buffer.text()));
146        Self {
147            buf: buffer,
148            syntax_hint: None,
149            indent: super::Indent::default(),
150            indent_override: super::IndentOverride::default(),
151            detection,
152            source: DocumentSource::Remote(Box::new(source)),
153        }
154    }
155    pub(crate) fn remote_snapshot(snapshot: RemoteSnapshot, selection: ReadSelection) -> Self {
156        Self::remote(
157            snapshot.buffer,
158            RemoteDocument {
159                file: snapshot.file,
160                window: snapshot.window,
161                selection,
162                connection: Some(snapshot.connection),
163                return_to: None,
164                write: None,
165            },
166        )
167    }
168    pub(crate) fn remote_directory(snapshot: RemoteDirectorySnapshot) -> Self {
169        let mut entries = snapshot.entries;
170        entries.sort_by(|left, right| {
171            let left_directory = left.kind == RemoteEntryKind::Directory;
172            let right_directory = right.kind == RemoteEntryKind::Directory;
173            right_directory.cmp(&left_directory).then_with(|| {
174                left.file
175                    .path()
176                    .as_os_str()
177                    .cmp(right.file.path().as_os_str())
178            })
179        });
180        let directory = RemoteDirectory {
181            directory: snapshot.directory,
182            visible: (0..entries.len()).collect(),
183            entries: entries.into(),
184            filter: String::new(),
185            connection: Some(snapshot.connection),
186            return_to: None,
187        };
188        let buffer = Buffer::from_text(&directory.text());
189        Self::directory(buffer, directory)
190    }
191    pub fn directory(mut buffer: Buffer, source: RemoteDirectory) -> Self {
192        buffer.name = Some(source.directory.to_string());
193        buffer.readonly = true;
194        Self {
195            buf: buffer,
196            syntax_hint: None,
197            indent: super::Indent::default(),
198            indent_override: super::IndentOverride::default(),
199            detection: None,
200            source: DocumentSource::RemoteDirectory(Box::new(source)),
201        }
202    }
203    pub fn remote_metadata(&self) -> Option<&RemoteDocument> {
204        match &self.source {
205            DocumentSource::Remote(source) => Some(source),
206            _ => None,
207        }
208    }
209    pub fn directory_metadata_ref(&self) -> Option<&RemoteDirectory> {
210        match &self.source {
211            DocumentSource::RemoteDirectory(source) => Some(source),
212            _ => None,
213        }
214    }
215}
216
217impl Document {
218    pub(crate) fn return_point(&self) -> Option<&JumpRecord> {
219        match &self.source {
220            DocumentSource::Surface(surface) => surface.content.return_point(),
221            DocumentSource::Remote(source) => source.return_to.as_ref(),
222            DocumentSource::RemoteDirectory(source) => source.return_to.as_ref(),
223            DocumentSource::Output { return_to } => return_to.as_ref(),
224            _ => None,
225        }
226    }
227    pub(crate) fn return_point_mut(&mut self) -> Option<&mut JumpRecord> {
228        match &mut self.source {
229            DocumentSource::Surface(surface) => surface.content.return_slot().as_mut(),
230            DocumentSource::Remote(source) => source.return_to.as_mut(),
231            DocumentSource::RemoteDirectory(source) => source.return_to.as_mut(),
232            DocumentSource::Output { return_to } => return_to.as_mut(),
233            _ => None,
234        }
235    }
236    pub(crate) fn set_return_point(&mut self, point: JumpRecord) {
237        match &mut self.source {
238            DocumentSource::Surface(surface) => surface.content.set_return_point(point),
239            DocumentSource::Remote(source) => source.return_to = Some(point),
240            DocumentSource::RemoteDirectory(source) => source.return_to = Some(point),
241            DocumentSource::Output { return_to } => *return_to = Some(point),
242            _ => {}
243        }
244    }
245}