1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! File-open orchestration: load the buffer synchronously and stash
//! the previous one. The expensive follow-up work (tree-sitter
//! highlighter build, LSP server spawn) is fanned out via
//! [`super::workers`]; multi-buffer cycling and deletion live in
//! [`super::buffer_list`].
use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::editor::Buffer;
use crate::buffer_ref::BufferRef;
use super::{App, SleepingBuffer, Toast};
impl App {
/// Dispatch a buffer-picker selection. Scratch and File both go
/// through the same stash-and-restore flow as
/// [`Self::open_path`] — if the target has a sleeping snapshot
/// (with preserved unsaved edits, cursor, undo history), we
/// restore it; otherwise we fall through to a fresh load.
pub fn switch_to_buffer(&mut self, r: BufferRef) -> Result<()> {
match r {
BufferRef::Scratch => {
if self.buffer.path.is_none() {
return Ok(());
}
self.lsp.detach_current();
// `Buffer::new` (one empty line) ≠ `Buffer::default`
// (zero lines), so we can't use `unwrap_or_default`
// here — the wrong default would leave the buffer
// with an empty `lines` Vec and crash motions.
let next = match self.sleeping.remove(&BufferRef::Scratch) {
Some(b) => b.thaw(),
None => Buffer::new(),
};
self.stash_and_install(next);
self.open_gen = self.open_gen.wrapping_add(1);
self.lsp.set_last_synced_version(self.buffer.version);
self.record_opened(BufferRef::Scratch);
self.toast = Toast::info("scratch");
Ok(())
}
BufferRef::File(path) => {
// Already on this file? Leave cursor/unsaved state alone.
let current = self
.buffer
.path
.as_ref()
.and_then(|p| p.canonicalize().ok());
if current.as_ref() == Some(&path) {
return Ok(());
}
self.open_path(&path)
}
}
}
/// Move the currently-active buffer into the sleeping map
/// (keyed by its [`BufferRef`]) and install `next` as the new
/// active buffer. The outgoing buffer is freeze-compressed; its
/// highlighter is dropped (rebuilt on restore). The version
/// counter is preserved so LSP `didChange` sequencing re-anchors
/// cleanly when the buffer wakes up again.
pub(super) fn stash_and_install(&mut self, next: Buffer) {
let key = self.active_ref();
let mut prev = std::mem::replace(&mut self.buffer, next);
prev.highlighter = None;
self.sleeping.insert(key, SleepingBuffer::freeze(prev));
}
/// Install `next` as the active buffer without stashing the
/// previous one. Used by `:bd` where the deleted buffer is
/// supposed to vanish entirely. Callers must have already cleaned
/// up any MRU / sleeping entries that refer to the outgoing
/// buffer.
pub(super) fn install_buffer(&mut self, next: Buffer) {
let _ = std::mem::replace(&mut self.buffer, next);
}
/// [`BufferRef`] for the currently-active buffer.
pub(super) fn active_ref(&self) -> BufferRef {
match &self.buffer.path {
Some(p) => BufferRef::File(p.canonicalize().unwrap_or_else(|_| p.clone())),
None => BufferRef::Scratch,
}
}
/// Open `path`. If the buffer for this path is sleeping (i.e. the
/// user previously visited it and switched away), wake it up
/// instead of re-reading from disk — that's what preserves the
/// unsaved edits, undo stack, and cursor position across a
/// `<space>b` round-trip. Otherwise load fresh from disk.
pub fn open_path(&mut self, path: &Path) -> Result<()> {
let path = self.absolutize(path);
let canon = path.canonicalize().unwrap_or_else(|_| path.clone());
let key = BufferRef::File(canon);
if let Some(restored) = self.sleeping.remove(&key) {
self.lsp.detach_current();
self.stash_and_install(restored.thaw());
self.record_opened(key);
self.open_gen = self.open_gen.wrapping_add(1);
self.lsp.set_last_synced_version(self.buffer.version);
self.toast = Toast::info(format!("restored {}", path.display()));
self.spawn_highlighter_worker(&path);
self.spawn_lsp_worker(&path);
return Ok(());
}
self.open_path_force(&path)
}
/// Resolve a user-supplied path to an absolute path against
/// `startup_cwd`. Doesn't touch the filesystem — works for files
/// that don't exist yet, which `canonicalize()` rejects. Critical
/// for `:e new_file.rs`: without absolutizing, the relative path
/// flows into [`crate::lsp::path_to_uri`] which produces a broken
/// `file:///new_file.rs` URI (no directory), and the LSP server
/// silently ignores the document.
fn absolutize(&self, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
self.startup_cwd.join(path)
}
}
/// Open `path` from disk, discarding any sleeping copy. Used on
/// the initial command-line load and as the fall-through for
/// `open_path` when there's no sleeping snapshot to restore.
pub fn open_path_force(&mut self, path: &Path) -> Result<()> {
// Load up front — if this fails we want to leave the active
// buffer alone. Missing files are treated as a new, unsaved
// buffer attached to `path` so `:w` materializes the file.
let (loaded, is_new) = match Buffer::load(path) {
Ok(b) => (b, false),
Err(e)
if e.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound) =>
{
let mut b = Buffer::new();
b.path = Some(path.to_path_buf());
(b, true)
}
Err(e) => return Err(e),
};
let canon = path
.canonicalize()
.unwrap_or_else(|_| path.to_path_buf());
// Tell the previous LSP client we're done with that document so
// it can drop diagnostics and stop watching it.
self.lsp.detach_current();
self.stash_and_install(loaded);
// Re-loading a path drops any previously-sleeping copy of it
// — the user explicitly asked for the disk version.
self.sleeping.remove(&BufferRef::File(canon.clone()));
self.record_opened(BufferRef::File(canon));
// Bump the generation: any in-flight worker thread from a
// previous `open_path` is now stale. Its result will be dropped
// when it lands instead of clobbering this buffer.
self.open_gen = self.open_gen.wrapping_add(1);
// Pre-seed the LSP sync version so the first `didChange` after
// open is a no-op when nothing has changed since load.
self.lsp.set_last_synced_version(self.buffer.version);
self.toast = if is_new {
Toast::info(format!("{} [new file]", path.display()))
} else {
Toast::info(format!("opened {}", path.display()))
};
// If the fuzzy preview worker already built a highlighter for
// this path, steal it: we're about to render the buffer and the
// tree is ready right now. Saves a worker round-trip and the
// "plain text → highlighted" flash. Re-`refresh` against the
// buffer's source/version so the cached tree's incremental diff
// re-anchors on whatever `Buffer::load` just read (usually a
// no-op because the file hasn't changed since the preview ran).
if let Some(entry) = self.preview_lru.borrow_mut().take(path) {
self.buffer.highlighter = None;
let mut h = entry.highlighter;
let source = self.buffer.lines.join("\n");
h.refresh(&source, self.buffer.version);
self.buffer.highlighter = Some(h);
} else {
self.spawn_highlighter_worker(path);
}
self.spawn_lsp_worker(path);
Ok(())
}
}