Skip to content

Commit 0443b11

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 0443b11

11 files changed

Lines changed: 699 additions & 19 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: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ View loaded plugins with the `dp` keybinding or `ListPlugins` command.
6262
1. Create a JavaScript or TypeScript file that exports an `activate` function:
6363

6464
**Plugin Lifecycle:**
65-
- `activate(red)` - Called when the plugin is loaded
65+
- `activate(red)` - Called when the plugin is loaded. Async activation is
66+
supported, but startup does not wait for it to finish.
6667
- `deactivate(red)` - Optional, called when the plugin is unloaded
68+
- `beforeExit(red, state)` - Optional, awaited after quit succeeds and before
69+
plugin deactivation. `state` is the current editor session snapshot.
6770

6871
```javascript
6972
export async function activate(red) {
@@ -128,6 +131,7 @@ Subscribes to editor events. Available events include:
128131
- `cursor:moved` - Cursor position changes (may fire frequently)
129132
- `file:opened` - File opened in a buffer
130133
- `file:saved` - File saved from a buffer
134+
- `editor:ready` - Plugins have loaded and startup work can begin
131135
132136
#### Editor Information
133137
```javascript
@@ -139,6 +143,26 @@ Returns an object containing:
139143
- `size` - Editor dimensions (rows, cols)
140144
- `theme` - Current theme information
141145
146+
#### Session State
147+
```javascript
148+
const state = await red.getEditorState()
149+
const result = await red.restoreEditorState(state)
150+
```
151+
152+
The snapshot includes file-backed buffers, cursor and viewport positions, the
153+
active buffer, cwd, and window split layout. Restore skips missing files and
154+
returns `{ restored, openedFiles, skippedFiles, warnings }`.
155+
156+
#### Plugin Storage
157+
```javascript
158+
await red.storage.set("latest", state)
159+
const state = await red.storage.get("latest")
160+
await red.storage.delete("latest")
161+
```
162+
163+
Storage is JSON, namespaced by plugin, and written under Red's config state
164+
directory.
165+
142166
#### UI Interaction
143167
```javascript
144168
// 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)