Skip to content

Commit 7f7e0aa

Browse files
committed
feat(plugin): add session restore lifecycle support
Add editor session snapshot APIs, plugin storage, and lifecycle hooks so plugins can persist and restore workspace state across editor runs. Include a built-in `session_restore` plugin that saves file-backed buffers, cursor positions, viewport state, and window splits on exit.
1 parent 650de27 commit 7f7e0aa

11 files changed

Lines changed: 696 additions & 18 deletions

File tree

default_config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,4 @@ Esc = { EnterMode = "Normal" }
168168
[plugins]
169169
buffer_picker = "buffer_picker.js"
170170
neotree = "neotree.js"
171+
session_restore = "session_restore.js"

docs/PLUGIN_SYSTEM.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ View loaded plugins with the `dp` keybinding or `ListPlugins` command.
6464
**Plugin Lifecycle:**
6565
- `activate(red)` - Called when the plugin is loaded
6666
- `deactivate(red)` - Optional, called when the plugin is unloaded
67+
- `beforeExit(red, state)` - Optional, awaited after quit succeeds and before
68+
plugin deactivation. `state` is the current editor session snapshot.
6769

6870
```javascript
6971
export async function activate(red) {
@@ -128,6 +130,7 @@ Subscribes to editor events. Available events include:
128130
- `cursor:moved` - Cursor position changes (may fire frequently)
129131
- `file:opened` - File opened in a buffer
130132
- `file:saved` - File saved from a buffer
133+
- `editor:ready` - Plugins have loaded and startup work can begin
131134
132135
#### Editor Information
133136
```javascript
@@ -139,6 +142,26 @@ Returns an object containing:
139142
- `size` - Editor dimensions (rows, cols)
140143
- `theme` - Current theme information
141144
145+
#### Session State
146+
```javascript
147+
const state = await red.getEditorState()
148+
const result = await red.restoreEditorState(state)
149+
```
150+
151+
The snapshot includes file-backed buffers, cursor and viewport positions, the
152+
active buffer, cwd, and window split layout. Restore skips missing files and
153+
returns `{ restored, openedFiles, skippedFiles, warnings }`.
154+
155+
#### Plugin Storage
156+
```javascript
157+
await red.storage.set("latest", state)
158+
const state = await red.storage.get("latest")
159+
await red.storage.delete("latest")
160+
```
161+
162+
Storage is JSON, namespaced by plugin, and written under Red's config state
163+
directory.
164+
142165
#### UI Interaction
143166
```javascript
144167
// Show a picker dialog

plugins/session_restore.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const STORAGE_KEY = "latest";
2+
3+
let redApi = null;
4+
5+
export async function activate(red) {
6+
redApi = red;
7+
8+
red.on("editor:ready", async () => {
9+
try {
10+
const startupFileCount = await red.getConfig("startup_file_count");
11+
if (startupFileCount > 0) {
12+
return;
13+
}
14+
15+
const snapshot = await red.storage.get(STORAGE_KEY);
16+
if (!snapshot || snapshot.version !== 1) {
17+
return;
18+
}
19+
20+
const cwd = await red.getConfig("cwd");
21+
if (snapshot.cwd && cwd && snapshot.cwd !== cwd) {
22+
red.logInfo("Session restore skipped: saved cwd differs from current cwd");
23+
return;
24+
}
25+
26+
const result = await red.restoreEditorState(snapshot);
27+
if (!result.restored) {
28+
red.logWarn("Session restore did not restore files", result.warnings);
29+
}
30+
for (const skipped of result.skippedFiles || []) {
31+
red.logWarn("Session restore skipped file", skipped.path, skipped.reason);
32+
}
33+
} catch (error) {
34+
red.logError("Session restore failed", error?.message || error);
35+
}
36+
});
37+
}
38+
39+
export async function beforeExit(red, state) {
40+
const api = red || redApi;
41+
if (!api || !state) return;
42+
43+
const cleanState = {
44+
...state,
45+
buffers: (state.buffers || []).filter((buffer) => buffer.path && !buffer.dirty),
46+
};
47+
48+
await api.storage.set(STORAGE_KEY, cleanState);
49+
}
50+
51+
export function deactivate() {
52+
redApi = null;
53+
}

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ pub struct Config {
1919
pub show_diagnostics: bool,
2020
#[serde(default = "default_false")]
2121
pub window_borders_ascii: bool,
22+
#[serde(default, skip_serializing)]
23+
pub startup_file_count: usize,
2224
}
2325

2426
#[derive(Debug, Serialize, Deserialize, Clone)]

0 commit comments

Comments
 (0)