152 lines
7.0 KiB
Markdown
152 lines
7.0 KiB
Markdown
# TECH.md — Restore Open Files and Markdown Editors
|
|
|
|
**GitHub Issue:** [warpdotdev/warp-external#371](https://github.com/warpdotdev/warp-external/issues/371)
|
|
**Product Spec:** `specs/GH371/product.md`
|
|
|
|
## Problem
|
|
|
|
Code editor panes were persisted to SQLite on quit but restoration was disabled — the `restore_pane_leaf` function returned `Err("Can't restore code panes")` for `LeafContents::Code`, causing the code pane to be silently dropped from the restored pane tree.
|
|
|
|
Additionally, the original snapshot only captured a single file path (the active tab's path), but `CodeView` supports multiple open tabs (`tab_group: Vec<TabData>`). Multi-tab state was lost.
|
|
|
|
Markdown file panes (`FileNotebookView` via `NotebookPaneSnapshot::LocalFileNotebook`) are already correctly persisted and restored — no changes were needed for basic markdown restoration.
|
|
|
|
## Relevant Code
|
|
|
|
- `app/src/pane_group/mod.rs (1676-1679)` — `restore_pane_leaf`: the `LeafContents::Code` match arm that returns an error instead of restoring
|
|
- `app/src/pane_group/pane/code_pane.rs:210-212` — `CodePane::snapshot()`: serializes only the active tab's path
|
|
- `app/src/pane_group/pane/code_pane.rs:38-45` — `CodePane::new()`: constructor that takes a `CodeSource` and optional line/column
|
|
- `app/src/app_state.rs:197-210` — `CodePaneTabSnapshot` struct and `CodePaneSnapShot::Local` enum with `tabs`, `active_tab_index`, and `source`
|
|
- `app/src/persistence/sqlite.rs (1124-1133)` — `save_pane_state`: writes the code pane to SQLite
|
|
- `app/src/persistence/sqlite.rs (2423-2431)` — reads code pane from SQLite on startup
|
|
- `crates/persistence/src/schema.rs (115-121)` — `code_panes` table schema
|
|
- `crates/persistence/src/model.rs (601-606)` — `NewCodePane` insert model
|
|
- `crates/persistence/src/model.rs (430-437)` — `CodePane` queryable model
|
|
- `app/src/code/view.rs:230-239` — `CodeView` struct with `tab_group` and `active_tab_index`
|
|
- `app/src/code/editor_management.rs:103-131` — `CodeSource` enum
|
|
- `app/src/pane_group/pane/file_pane.rs:152-155` — `FilePane::snapshot()`: already works
|
|
|
|
## Current State
|
|
|
|
Persistence and restoration of code panes are both implemented. `CodePaneSnapShot::Local` stores all open tabs, the active tab index, and the `CodeSource`:
|
|
|
|
```rust
|
|
pub struct CodePaneTabSnapshot {
|
|
pub path: Option<PathBuf>,
|
|
}
|
|
|
|
pub enum CodePaneSnapShot {
|
|
Local {
|
|
tabs: Vec<CodePaneTabSnapshot>,
|
|
active_tab_index: usize,
|
|
source: Option<CodeSource>,
|
|
},
|
|
}
|
|
```
|
|
|
|
`restore_pane_leaf` destructures this single variant to reconstruct the `CodePane` via `CodeView::restore()`, which reopens each tab and selects the active one.
|
|
|
|
Markdown file panes (`FilePane`/`FileNotebookView`) continue to work via `NotebookPaneSnapshot::LocalFileNotebook`.
|
|
|
|
## Proposed Changes
|
|
|
|
### Implementation
|
|
|
|
Code pane restoration and multi-tab persistence were implemented together.
|
|
|
|
#### Snapshot model
|
|
|
|
`CodePaneSnapShot` has a single `Local` variant that holds all tabs, the active tab index, and the full `CodeSource`:
|
|
|
|
```rust
|
|
pub struct CodePaneTabSnapshot {
|
|
pub path: Option<PathBuf>,
|
|
}
|
|
|
|
pub enum CodePaneSnapShot {
|
|
Local {
|
|
tabs: Vec<CodePaneTabSnapshot>,
|
|
active_tab_index: usize,
|
|
source: Option<CodeSource>,
|
|
},
|
|
}
|
|
```
|
|
|
|
The `source` field stores the full `CodeSource` enum (serialized as JSON in SQLite) so that restored panes retain the correct source semantics. Variants whose extra data cannot be reconstructed at restore time fall back to `CodeSource::FileTree` if a path is available.
|
|
|
|
#### Snapshot creation
|
|
|
|
`CodePane::snapshot()` iterates the tab group, skipping preview tabs, and stores the source:
|
|
|
|
```rust
|
|
LeafContents::Code(CodePaneSnapShot::Local {
|
|
tabs,
|
|
active_tab_index,
|
|
source,
|
|
})
|
|
```
|
|
|
|
#### SQLite persistence
|
|
|
|
Tabs are stored in a dedicated `code_pane_tabs` table (one row per tab, ordered by `tab_index`). The legacy `local_path` column on `code_panes` is still written for backward compatibility. The `CodeSource` is serialized as JSON into a `source` column on `code_panes`.
|
|
|
|
#### Restoration
|
|
|
|
In `restore_pane_leaf`, the `LeafContents::Code` arm (gated behind `#[cfg(feature = "local_fs")]`) destructures the snapshot and calls `CodeView::restore(&tabs, active_tab_index, source, ctx)` to rebuild the pane with all tabs.
|
|
|
|
### Phase 3: Markdown display mode preservation (optional enhancement)
|
|
|
|
The `FileNotebookView` display mode (rendered vs. raw) is not currently persisted. To support this:
|
|
|
|
1. Add a `display_mode` field to `NotebookPaneSnapshot::LocalFileNotebook`.
|
|
2. Persist via an optional column in `notebook_panes` (or encoded in `local_path` metadata).
|
|
3. On restore, pass the display mode to `FileNotebookView`.
|
|
|
|
This is lower priority since the rendered mode is the default and switching modes on restore is a single click.
|
|
|
|
## End-to-End Flow
|
|
|
|
### Quit (persist)
|
|
|
|
```
|
|
User quits → Workspace::snapshot_app_state()
|
|
→ PaneGroup::snapshot() walks the pane tree
|
|
→ CodePane::snapshot() → CodePaneSnapShot::Local { tabs, active_tab_index, source }
|
|
→ save_app_state() writes to SQLite
|
|
→ code_panes row + code_pane_tabs rows
|
|
```
|
|
|
|
### Launch (restore)
|
|
|
|
```
|
|
App starts → read_sqlite_data()
|
|
→ reads code_panes + code_pane_tabs → CodePaneSnapShot::Local
|
|
→ restore_pane_leaf()
|
|
→ CodeView::restore(&tabs, active_tab_index, source, ctx)
|
|
→ pane inserted into pane tree at correct position
|
|
```
|
|
|
|
## Risks and Mitigations
|
|
|
|
1. **Stale file paths.** A persisted path may no longer exist. The `CodeView`/`LocalCodeEditorView` already handles this gracefully (shows file-not-found state). The pane is created regardless. No extra handling is needed.
|
|
|
|
2. **CodeManager deduplication.** `CodePane::pre_attach()` checks `CodeManager` for duplicate paths in the same tab. During restoration, panes are created and attached in tree order, so the first pane to attach with a given path "wins". Subsequent panes with the same path would be deduplicated to the first. This matches existing runtime behavior and is acceptable.
|
|
|
|
3. **Migration safety.** The new DB columns are nullable, so the migration is backward-compatible. Existing databases with only `local_path` continue to work.
|
|
|
|
4. **Preview tabs and `pre_attach`.** Restored preview tabs may trigger `pre_attach` deduplication if the file is already open elsewhere. This is the same behavior as at runtime and acceptable.
|
|
|
|
## Testing and Validation
|
|
|
|
1. **Unit test for snapshot round-trip.** Test that `CodePane::snapshot()` produces a `CodePaneSnapShot` that, when written to and read from SQLite, matches the original data (single tab and multi-tab).
|
|
|
|
2. **Pane tree integrity test.** Verify that restoring a pane tree containing a mix of terminal, code, and notebook panes produces the correct tree structure with all panes present.
|
|
|
|
3. **Manual validation.** Follow the manual test steps in the product spec.
|
|
|
|
## Follow-ups
|
|
|
|
- **Scroll position and cursor restoration.** Could be added to `CodePaneTabSnapshot` in the future.
|
|
- **Unsaved buffer content.** Persisting dirty buffers is significantly more complex and out of scope.
|
|
- **Markdown display mode.** Phase 3 is optional and can be a separate PR.
|