ad_editor/buffer/
buffers.rs

1use crate::{
2    buffer::{Buffer, BufferKind, Cur, WELCOME_SQUIRREL},
3    config::Config,
4    dot::TextObject,
5    lsp::LspManagerHandle,
6    ziplist,
7    ziplist::{Position, ZipList},
8};
9use ad_event::Source;
10use std::{
11    collections::VecDeque,
12    io::{self, ErrorKind},
13    mem,
14    path::Path,
15    sync::{Arc, RwLock},
16};
17
18#[cfg(test)]
19use crate::lsp::Req;
20#[cfg(test)]
21use std::sync::mpsc::Sender;
22
23const MAX_JUMPS: usize = 100;
24
25/// An ID for a known Buffer
26pub type BufferId = usize;
27
28/// A non-empty vec of buffers where the active buffer is accessible and default
29/// buffers are inserted where needed to maintain invariants
30#[derive(Debug)]
31pub struct Buffers {
32    next_id: BufferId,
33    inner: ZipList<Buffer>,
34    jump_list: JumpList,
35    lsp_handle: Arc<LspManagerHandle>,
36    config: Arc<RwLock<Config>>,
37}
38
39impl Buffers {
40    pub fn new(lsp_handle: Arc<LspManagerHandle>, config: Arc<RwLock<Config>>) -> Self {
41        Self {
42            next_id: 1,
43            inner: ziplist![Buffer::new_unnamed(0, "", config.clone())],
44            jump_list: JumpList::default(),
45            lsp_handle,
46            config,
47        }
48    }
49
50    #[cfg(test)]
51    pub(crate) fn new_with_raw_sender(tx_req: Sender<Req>, config: Arc<RwLock<Config>>) -> Self {
52        Self {
53            next_id: 1,
54            inner: ziplist![Buffer::new_unnamed(0, "", config.clone())],
55            jump_list: JumpList::default(),
56            lsp_handle: Arc::new(LspManagerHandle::new_stubbed(tx_req)),
57            config,
58        }
59    }
60
61    #[cfg(test)]
62    pub(crate) fn new_stubbed(
63        ids: &[usize],
64        tx_req: Sender<Req>,
65        config: Arc<RwLock<Config>>,
66    ) -> Self {
67        Self {
68            next_id: ids.last().unwrap() + 1,
69            inner: ZipList::try_from_iter(
70                ids.iter()
71                    .map(|i| Buffer::new_virtual(*i, "", "", config.clone())),
72            )
73            .unwrap(),
74            jump_list: JumpList::default(),
75            lsp_handle: Arc::new(LspManagerHandle::new_stubbed(tx_req)),
76            config,
77        }
78    }
79
80    /// Returns the id of a newly created buffer, None if the buffer already existed
81    pub fn open_or_focus<P: AsRef<Path>>(
82        &mut self,
83        path: P,
84        retain_empty_unnamed: bool,
85    ) -> io::Result<Option<BufferId>> {
86        let path = match path.as_ref().canonicalize() {
87            Ok(p) => p,
88            Err(e) if e.kind() == ErrorKind::NotFound => path.as_ref().to_path_buf(),
89            Err(e) => return Err(e),
90        };
91
92        // Opening a directory from an existing directory buffer replaces the existing content
93        // rather than opening a new buffer in order to prevent the issue in Acme where drilling
94        // down into subdirectories results in having multiple.
95        if self.active().kind.is_dir() && path.metadata().map(|m| m.is_dir()).unwrap_or_default() {
96            let b = self.active_mut();
97            b.kind = BufferKind::Directory(path);
98            b.reload_from_disk();
99            b.set_dot(TextObject::BufferStart, 1);
100            return Ok(None);
101        }
102
103        // If we already have the requested path open then we focus that buffer instead of opening
104        // a duplicate.
105        let existing_id = self.with_path(&path).map(|b| b.id);
106        if let Some(existing_id) = existing_id {
107            self.notify_lsp_changes_if_dirty();
108            self.record_jump_position();
109            self.inner.focus_element_by(|b| b.id == existing_id);
110            return Ok(None);
111        }
112
113        // Otherwise we load the file and add it to the buffer list.
114        let id = self.next_id;
115        self.next_id += 1;
116        let mut b = Buffer::new_from_canonical_file_path(id, path, self.config.clone())?;
117
118        // Remove an empty unnamed buffer if the user has now opened a file and we have not
119        // been told to retain it (typically because we have an open window showing it)
120        if !retain_empty_unnamed && self.is_empty_squirrel() {
121            mem::swap(&mut self.inner.focus, &mut b);
122        } else {
123            self.record_jump_position();
124            self.push_buffer(b);
125        }
126
127        self.lsp_handle.document_opened(self);
128
129        Ok(Some(id))
130    }
131
132    pub fn ensure_file_is_open<P: AsRef<Path>>(&mut self, path: P) {
133        let p = match path.as_ref().canonicalize() {
134            Ok(p) => p,
135            Err(_) => return,
136        };
137
138        if self.with_path(&p).is_some() {
139            return;
140        }
141
142        let id = self.next_id;
143        self.next_id += 1;
144
145        if let Ok(b) = Buffer::new_from_canonical_file_path(id, p, self.config.clone()) {
146            self.lsp_handle.document_opened(self);
147            self.inner.insert_at(Position::Tail, b);
148        }
149    }
150
151    /// This is a pretty hacky way to handle document sync but given that we are not wanting
152    /// the standard IDE behaviour of everything updating as you type, notifying changes when
153    /// we switch focus to another buffer (or within ../lsp/mod.rs before sending requests)
154    /// is sufficient for maintaining document sync.
155    #[inline]
156    fn notify_lsp_changes_if_dirty(&self) {
157        if self.inner.focus.dirty {
158            self.lsp_handle.document_changed(&self.inner.focus);
159        }
160    }
161
162    pub fn next(&mut self) {
163        self.notify_lsp_changes_if_dirty();
164        self.inner.focus_down();
165    }
166
167    pub fn previous(&mut self) {
168        self.notify_lsp_changes_if_dirty();
169        self.inner.focus_up();
170    }
171
172    pub fn iter(&self) -> impl Iterator<Item = &Buffer> {
173        self.inner.iter().map(|(_, b)| b)
174    }
175
176    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Buffer> {
177        self.inner.iter_mut().map(|(_, b)| b)
178    }
179
180    pub fn close_buffer(&mut self, id: BufferId) {
181        let removed = self.inner.remove_where_with_default(
182            |b| b.id == id,
183            || Buffer::new_unnamed(self.next_id, "", self.config.clone()),
184        );
185        self.jump_list.clear_for_buffer(id);
186
187        if let Some(b) = removed {
188            self.lsp_handle.document_closed(&b);
189            self.next_id += 1; // for the newly added unnamed buffer
190        }
191    }
192
193    fn push_buffer(&mut self, buf: Buffer) {
194        self.inner.insert(buf);
195    }
196
197    pub(crate) fn open_virtual(&mut self, name: String, content: String) -> BufferId {
198        let existing_id = self
199            .inner
200            .iter()
201            .find(|(_, b)| match &b.kind {
202                BufferKind::Virtual(s) => s == &name,
203                _ => false,
204            })
205            .map(|(_, b)| b.id);
206
207        if let Some(id) = existing_id {
208            self.focus_id(id);
209            self.inner.focus.txt = content.into();
210            let n = self.inner.focus.txt.len_chars();
211            self.inner.focus.dot.clamp_idx(n);
212            self.inner.focus.xdot.clamp_idx(n);
213            return id;
214        }
215
216        let id = self.next_id;
217        let buf = Buffer::new_virtual(id, name, content, self.config.clone());
218        self.record_jump_position();
219        self.push_buffer(buf);
220        self.next_id += 1;
221
222        id
223    }
224
225    /// Used to seed the buffer selection mini-buffer
226    pub(crate) fn as_buffer_list(&self) -> Vec<String> {
227        let mut entries: Vec<(usize, String)> = self
228            .inner
229            .iter()
230            .map(|(focused, b)| {
231                (
232                    b.id,
233                    format!(
234                        "{:<4} {} {}",
235                        b.id,
236                        if focused { '*' } else { ' ' },
237                        b.full_name()
238                    ),
239                )
240            })
241            .collect();
242        entries.sort_by_key(|e| e.0);
243
244        entries.into_iter().map(|(_, s)| s).collect()
245    }
246
247    pub(crate) fn contains_bufid(&self, id: BufferId) -> bool {
248        self.inner.iter().any(|(_, b)| b.id == id)
249    }
250
251    pub(crate) fn focus_id(&mut self, id: BufferId) -> Option<BufferId> {
252        if !self.contains_bufid(id) || self.active().id == id {
253            return None;
254        }
255        self.notify_lsp_changes_if_dirty();
256        self.record_jump_position();
257        self.inner.focus_element_by(|b| b.id == id);
258
259        Some(id)
260    }
261
262    /// Focus the given buffer ID without touching the jump list
263    pub(crate) fn focus_id_silent(&mut self, id: BufferId) {
264        self.notify_lsp_changes_if_dirty();
265        self.inner.focus_element_by(|b| b.id == id);
266    }
267
268    pub(crate) fn with_id(&self, id: BufferId) -> Option<&Buffer> {
269        self.inner.iter().find(|(_, b)| b.id == id).map(|(_, b)| b)
270    }
271
272    pub(crate) fn with_id_mut(&mut self, id: BufferId) -> Option<&mut Buffer> {
273        self.inner
274            .iter_mut()
275            .find(|(_, b)| b.id == id)
276            .map(|(_, b)| b)
277    }
278
279    pub(crate) fn with_path<P: AsRef<Path>>(&self, path: P) -> Option<&Buffer> {
280        let path = path.as_ref();
281        self.inner
282            .iter()
283            .find(|(_, b)| match &b.kind {
284                BufferKind::File(p) | BufferKind::Directory(p) => p == path,
285                _ => false,
286            })
287            .map(|(_, b)| b)
288    }
289
290    pub fn dirty_buffers(&self) -> Vec<String> {
291        self.inner
292            .iter()
293            .filter(|(_, b)| b.dirty && b.kind.is_file())
294            .map(|(_, b)| b.full_name().to_string())
295            .collect()
296    }
297
298    #[inline]
299    pub fn active(&self) -> &Buffer {
300        &self.inner.focus
301    }
302
303    #[inline]
304    pub fn active_mut(&mut self) -> &mut Buffer {
305        &mut self.inner.focus
306    }
307
308    pub fn record_jump_position(&mut self) {
309        self.jump_list
310            .push(self.inner.focus.id, self.inner.focus.dot.active_cur());
311    }
312
313    fn jump(&mut self, bufid: BufferId, mut cur: Cur) -> (BufferId, BufferId) {
314        self.notify_lsp_changes_if_dirty();
315        let prev_id = self.inner.focus.id;
316        self.inner.focus_element_by(|b| b.id == bufid);
317        let new_id = self.inner.focus.id;
318        if new_id == bufid {
319            // Clamp the cursor to the buffer's valid range before setting it
320            // in case the buffer was edited since this position was recorded
321            cur.clamp_idx(self.inner.focus.txt.len_chars());
322            self.inner.focus.dot = cur.into();
323        }
324
325        (prev_id, new_id)
326    }
327
328    pub fn jump_list_forward(&mut self) -> Option<(BufferId, BufferId)> {
329        if let Some((bufid, cur)) = self.jump_list.forward() {
330            Some(self.jump(bufid, cur))
331        } else {
332            None
333        }
334    }
335
336    pub fn jump_list_backward(&mut self) -> Option<(BufferId, BufferId)> {
337        let (bufid, cur) = (self.inner.focus.id, self.inner.focus.dot.active_cur());
338        if let Some((bufid, cur)) = self.jump_list.backward(bufid, cur) {
339            Some(self.jump(bufid, cur))
340        } else {
341            None
342        }
343    }
344
345    #[inline]
346    pub fn len(&self) -> usize {
347        self.inner.len()
348    }
349
350    /// Whether or not the only buffer we currently have open is the default squirrel buffer rather
351    /// than a "real" buffer that has been opened by the user.
352    #[inline]
353    pub fn is_empty_squirrel(&self) -> bool {
354        self.inner.len() == 1
355            && self.inner.focus.is_unnamed()
356            && (self.inner.focus.txt.is_empty() || self.inner.focus.txt == WELCOME_SQUIRREL)
357    }
358
359    /// Append to the +output buffer assigned to the buffer with provided id.
360    pub(crate) fn write_output_for_buffer(&mut self, id: usize, s: String, cwd: &Path) -> BufferId {
361        let key = match self.with_id(id) {
362            Some(b) => b.output_file_key(cwd),
363            None => format!("{}/DEFAULT_OUTPUT_BUFFER", cwd.display()),
364        };
365
366        let k = BufferKind::Output(key.clone());
367        match self.inner.iter_mut().find(|(_, b)| b.kind == k) {
368            Some((_, b)) => {
369                b.append(s, Source::Fsys);
370
371                b.id
372            }
373
374            None => {
375                let id = self.next_id;
376                self.next_id += 1;
377                let b = Buffer::new_output(id, key, s, self.config.clone());
378                self.record_jump_position();
379                self.inner.insert(b);
380
381                id
382            }
383        }
384    }
385}
386
387#[derive(Debug, Clone, PartialEq, Eq)]
388struct JumpList {
389    idx: usize,
390    jumps: VecDeque<(BufferId, Cur)>,
391}
392
393impl Default for JumpList {
394    fn default() -> Self {
395        Self {
396            idx: 0,
397            jumps: VecDeque::with_capacity(MAX_JUMPS),
398        }
399    }
400}
401
402impl JumpList {
403    fn push(&mut self, id: BufferId, cur: Cur) {
404        self.jumps.truncate(self.idx);
405        let jump = (id, cur);
406
407        if self.jumps.back() == Some(&jump) {
408            return;
409        }
410        if self.jumps.len() == MAX_JUMPS {
411            self.jumps.pop_front();
412        }
413
414        self.jumps.push_back(jump);
415        self.idx = self.jumps.len();
416    }
417
418    fn forward(&mut self) -> Option<(BufferId, Cur)> {
419        if self.idx + 1 >= self.jumps.len() {
420            return None;
421        }
422
423        self.idx += 1;
424        self.jumps.get(self.idx).copied()
425    }
426
427    fn backward(&mut self, id: BufferId, cur: Cur) -> Option<(BufferId, Cur)> {
428        if self.idx == 0 {
429            return None;
430        }
431
432        // Mark our current position so we can jump forward to it later.
433        // We need to move self.idx back after the push so we don't get stuck
434        // and no-op jump immediately after
435        if self.idx == self.jumps.len() {
436            self.push(id, cur);
437            self.idx -= 1;
438        }
439
440        self.idx -= 1;
441        self.jumps.get(self.idx).copied()
442    }
443
444    // FIXME: this needs to be smarter about modifying self.idx based on which entries are removed
445    fn clear_for_buffer(&mut self, id: BufferId) {
446        self.jumps.retain(|j| j.0 != id);
447        self.idx = self.jumps.len();
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn jump_backward_clamps_stale_cursor_positions() {
457        let (tx, _rx) = std::sync::mpsc::channel();
458        let config = Arc::new(RwLock::new(Config::default()));
459        let mut buffers = Buffers::new_with_raw_sender(tx, config.clone());
460
461        let initial_content = "This is a test buffer with enough content to demonstrate the bug.";
462        buffers.inner.focus.txt = initial_content.into();
463
464        buffers.inner.focus.set_dot_from_cursor(60);
465        assert_eq!(buffers.inner.focus.dot.active_cur().idx, 60);
466
467        buffers.record_jump_position();
468
469        assert_eq!(buffers.inner.focus.txt.len_chars(), 65);
470        let new_len = 30;
471        buffers
472            .inner
473            .focus
474            .txt
475            .remove_range(new_len, buffers.inner.focus.txt.len_chars());
476        assert_eq!(buffers.inner.focus.txt.len_chars(), new_len);
477
478        buffers.inner.focus.set_dot_from_cursor(0);
479        let result = buffers.jump_list_backward();
480
481        assert!(result.is_some());
482
483        let cur = buffers.inner.focus.dot.active_cur();
484        let (_y, _x) = cur.as_yx(&buffers.inner.focus);
485
486        assert!(
487            cur.idx <= buffers.inner.focus.txt.len_chars(),
488            "cursor index {} exceeds buffer length {}",
489            cur.idx,
490            buffers.inner.focus.txt.len_chars()
491        );
492    }
493}