More buffer design changes
This commit is contained in:
@@ -92,94 +92,209 @@ Special buffers in Emacs are distinguished by convention and properties, not by
|
||||
|
||||
### Kakoune
|
||||
|
||||
The prompt line is a regular buffer. Kakoune treats everything uniformly -- buffers are buffers, and their role comes from context/properties rather than identity.
|
||||
The prompt line is a regular buffer that supports normal editing commands. You can use movement, selection, and editing primitives to compose your command — the same muscle memory works everywhere. This is a notable improvement over Vim, where the command line is a separate input context with its own limited editing model. (Vim's `q:` cmdwin partially addresses this but is clunky.)
|
||||
|
||||
### Xi Editor
|
||||
|
||||
Every view is just a buffer with metadata attached. No special-cased IDs.
|
||||
|
||||
## Design Space for pane-editor
|
||||
### Command Palettes (VS Code, Sublime, etc.)
|
||||
|
||||
There's a spectrum between Neovim's explicit enum and Emacs's emergent properties:
|
||||
The command palette pattern treats command input as a prominent UI element — typically a floating centered or top-anchored panel with:
|
||||
- Real-time filtering/fuzzy matching
|
||||
- Hints and documentation alongside candidates
|
||||
- Categorized results (commands, files, symbols, etc.)
|
||||
|
||||
### Option A: `BufferRole` enum (Neovim-like)
|
||||
The key insight is that this is a *presentation* concern. The underlying buffer holding the user's input text is the same regardless of whether it renders as a vim-style bottom line or a centered floating palette. The pane layout / rendering layer decides how and where to display it.
|
||||
|
||||
```rust
|
||||
enum BufferRole {
|
||||
Document, // normal file editing
|
||||
Command, // command-line input (single-line, always insert mode)
|
||||
Prompt, // general single-line input (save confirmation, etc.)
|
||||
Finder, // search/picker
|
||||
Help, // read-only informational
|
||||
Scratch, // no file, writable
|
||||
}
|
||||
```
|
||||
## Decomposing the Command Buffer
|
||||
|
||||
`TextBuffer` gets a `role: BufferRole` field. Core logic checks `buffer.role()` instead of `buffer_id == COMMAND_BUFFER_ID`.
|
||||
Before choosing an approach, it's worth asking: what actually makes the command buffer special? Here's every special behavior in the current code, and what property would express it:
|
||||
|
||||
**Pros:** Simple, explicit, easy to reason about. The core knows all possible roles at compile time.
|
||||
**Cons:** Every new kind of buffer requires a new enum variant. Roles are mutually exclusive.
|
||||
| Current behavior | Where | Property |
|
||||
|---|---|---|
|
||||
| Always in Insert mode | `core.rs:69` — `set_mode(Mode::Insert)` at creation | **Remove this.** Command buffer should support Normal mode like any other buffer (Kakoune-style). You should be able to hjkl around and edit your command. |
|
||||
| Enter submits instead of inserting `\n` | `configuration.rs:49` — `(Mode::Insert, true) => Command(Execute)` | Submit behavior lives in the input mapping layer (Configuration / future DSL), keyed off buffer properties. Not inherently tied to single-line — a multi-line command buffer could submit on e.g. Ctrl+Enter or a keybinding. |
|
||||
| Escape returns to previous buffer | `configuration.rs:58` — `(_, true) => FocusPreviousBuffer` | `ephemeral: true` (Escape on ephemeral buffer = defocus) |
|
||||
| Tab triggers completion | `configuration.rs:62` — `(Mode::Insert, true) => Command(Tab)` | Tab behavior is context-dependent, lives in input mapping |
|
||||
| Cleared on focus | `core.rs:254` — clears when `target == COMMAND_BUFFER_ID` | `ephemeral: true` |
|
||||
| Cleared on defocus | `core.rs:265` — clears when leaving command buffer | `ephemeral: true` |
|
||||
| Doesn't replace main display | `core.rs:115` — `main_display_buffer()` skips it | `listed: false` (unlisted buffers don't claim the main editing area) |
|
||||
| Not in tab bar | implicit | `listed: false` |
|
||||
|
||||
### Option B: Capability flags / properties (Emacs-like)
|
||||
Almost every behavior decomposes into generic properties (`ephemeral`, `listed`). But there is one irreducible property: *this buffer's contents are commands to be interpreted by the editor's command system*. That's not the same as "ephemeral" — a finder buffer is ephemeral too, but submitting it opens a file rather than executing a command. The core needs to know which buffer to read command text from when `CommandAction::Execute` fires, and that's a property of the buffer itself: `command: bool`.
|
||||
|
||||
### What Vim Gets Wrong Here
|
||||
|
||||
In Vim, the command line (`:` prompt) is a separate input context with its own limited editing model. You can't use Normal mode motions to edit your command — only basic readline-style bindings. Vim's `q:` cmdwin partially addresses this by opening an actual buffer, but it's a separate workflow. This is an unnecessary inconsistency.
|
||||
|
||||
In pane-editor, the command buffer is just a buffer. It starts in Insert mode (because you're typing a command), but you can Escape to Normal mode and use the full editing vocabulary — hjkl, word motions, etc. — then `i` back to Insert to keep typing. The same keybindings, the same muscle memory, everywhere.
|
||||
|
||||
## Design: Properties-Only (Emacs-like)
|
||||
|
||||
Drop `BufferKind` entirely. Buffer behavior is determined by composable properties on `TextBuffer`. The properties are minimal — they capture what the *core* needs to know to handle buffer lifecycle. Everything else (keybindings, submit behavior, rendering position) lives in other layers.
|
||||
|
||||
### Three Layers of "Buffer Behavior"
|
||||
|
||||
It's important to separate what lives where:
|
||||
|
||||
1. **Buffer properties** (on `TextBuffer`) — intrinsic buffer state that the core needs for lifecycle management: Is it listed? Is it ephemeral? Is it read-only? Does it have a backing file?
|
||||
|
||||
2. **Input mapping** (in `Configuration` / future DSL) — what keystrokes do in a given buffer context. Enter submits vs inserts newline. Tab completes vs inserts tab. Escape defocuses vs transitions to Normal mode. This is user-configurable and can vary per buffer properties, per mode, per anything.
|
||||
|
||||
3. **Presentation** (in pane layout / rendering) — where and how a buffer is displayed. Bottom command line vs centered floating palette vs side panel. This is orthogonal to both buffer properties and input mapping.
|
||||
|
||||
This separation is what makes it possible to have the same command buffer render as a vim-style bottom line OR a VS Code-style command palette — the buffer doesn't change, only the pane layout does.
|
||||
|
||||
### Buffer Properties
|
||||
|
||||
```rust
|
||||
struct BufferProperties {
|
||||
file_backed: bool, // has a backing file
|
||||
writable: bool, // can be edited
|
||||
single_line: bool, // constrained to one line
|
||||
ephemeral: bool, // cleared on defocus, no save semantics
|
||||
listed: bool, // appears in buffer list / tab bar
|
||||
on_submit: Option<...>, // callback when Enter is pressed
|
||||
/// Buffer appears in tab bar / buffer list and claims the main editing area.
|
||||
listed: bool,
|
||||
|
||||
/// Buffer is cleared on focus and defocus. No unsaved-changes warning.
|
||||
ephemeral: bool,
|
||||
|
||||
/// Buffer contents cannot be modified. Mode::Insert doesn't apply —
|
||||
/// `TransitionToMode(Mode::Insert)` is a no-op on a read-only buffer.
|
||||
read_only: bool,
|
||||
|
||||
/// Buffer contents are interpreted as editor commands on submit.
|
||||
/// The core reads this buffer's text when CommandAction::Execute fires.
|
||||
command: bool,
|
||||
}
|
||||
```
|
||||
|
||||
Buffer "type" is emergent from the combination of properties. A command buffer is `{ single_line: true, ephemeral: true, on_submit: Some(execute_command), listed: false }`.
|
||||
Four properties. Note what's *not* here:
|
||||
|
||||
**Pros:** Composable, open-ended. New buffer behaviors don't require new enum variants. Properties can be changed at runtime.
|
||||
**Cons:** Harder to reason about -- you need to check multiple flags. Possible to create nonsensical combinations. More complex.
|
||||
- **No `single_line`.** Whether a buffer is constrained to one line is a rendering/UI concern, not an intrinsic buffer property. A command buffer could be single-line in a vim-style bottom bar and multi-line in a command palette. The buffer itself doesn't care.
|
||||
- **No `on_submit` callback.** The `command` flag tells the core *where submission goes* (the command system). The input mapping layer decides *when* submission happens (Enter, Ctrl+Enter, whatever). Other buffer types (finder, prompt) would have their own semantic flags and their own submit routing.
|
||||
- **No mode override.** The command buffer supports Normal mode just like any other buffer. It starts in Insert mode because that's natural, but you can Escape to Normal, edit with motions, and `i` back.
|
||||
|
||||
### Option C: Hybrid (recommended starting point)
|
||||
### Constructing Buffers by Role
|
||||
|
||||
A small `BufferKind` enum for the core categories the editor fundamentally understands, plus property flags for the orthogonal behavioral dimensions:
|
||||
Convenience constructors express common configurations:
|
||||
|
||||
```rust
|
||||
/// What kind of content this buffer holds. Determines core editor behavior.
|
||||
enum BufferKind {
|
||||
Document, // file or scratch text editing
|
||||
Command, // command-line input
|
||||
Info, // read-only informational (help, about, etc.)
|
||||
}
|
||||
impl BufferProperties {
|
||||
/// A normal file-editing buffer.
|
||||
fn document() -> Self {
|
||||
BufferProperties { listed: true, ephemeral: false, read_only: false, command: false }
|
||||
}
|
||||
|
||||
/// Behavioral properties orthogonal to kind.
|
||||
struct BufferProperties {
|
||||
listed: bool, // appears in tab bar / buffer list
|
||||
ephemeral: bool, // no unsaved-changes warning
|
||||
single_line: bool, // constrain to one line of input
|
||||
// ... extend as needed
|
||||
/// A command-line input buffer (the `:` prompt).
|
||||
fn command() -> Self {
|
||||
BufferProperties { listed: false, ephemeral: true, read_only: false, command: true }
|
||||
}
|
||||
|
||||
/// A read-only informational buffer (help text, about, etc.)
|
||||
fn info() -> Self {
|
||||
BufferProperties { listed: false, ephemeral: false, read_only: true, command: false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This gives you the benefits of explicit categorization where it matters (the core needs to know "is this a command buffer?" to handle Enter/Escape/focus semantics) while keeping orthogonal properties composable.
|
||||
A "finder" buffer is `{ listed: false, ephemeral: true, read_only: false, command: false }` — ephemeral like a command buffer, but its submit action is "open the selected file" (handled by finder-specific logic, not the command system). A "scratch" buffer is `document()` with no backing file.
|
||||
|
||||
### How the Core Uses Properties
|
||||
|
||||
The core never asks "is this the command buffer?" — it asks about capabilities:
|
||||
|
||||
```rust
|
||||
// Instead of: if target == COMMAND_BUFFER_ID { self.clear_command_buffer(); }
|
||||
if buffer.properties.ephemeral {
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
// Instead of: if self.active_buffer == COMMAND_BUFFER_ID { return previous }
|
||||
if !active_buffer.properties.listed {
|
||||
// unlisted buffer doesn't claim the main display area
|
||||
return self.previous_buffer();
|
||||
}
|
||||
|
||||
// Instead of: force Insert mode on command buffer
|
||||
// Nothing. The buffer starts in whatever mode it starts in. Mode transitions
|
||||
// are handled normally. read_only buffers reject TransitionToMode(Insert).
|
||||
```
|
||||
|
||||
### How Configuration Uses Properties
|
||||
|
||||
`Configuration::get_editor_action` receives `&BufferProperties` instead of a `BufferId`:
|
||||
|
||||
```rust
|
||||
fn get_editor_action(&self, input: Input, props: &BufferProperties, mode: Mode, ...) {
|
||||
match input {
|
||||
// Escape in an ephemeral buffer defocuses. In a non-ephemeral buffer,
|
||||
// transitions to Normal (from Insert) or is a no-op (already Normal).
|
||||
Input::Escape if props.ephemeral => FocusPreviousBuffer,
|
||||
Input::Escape => match mode {
|
||||
Mode::Insert => TransitionToMode(Mode::Normal),
|
||||
Mode::Normal => None,
|
||||
},
|
||||
|
||||
// Enter in Insert mode in a command buffer submits to the command system.
|
||||
// In a non-command buffer, inserts a newline. The future DSL will allow
|
||||
// full customization of this per buffer property.
|
||||
Input::Newline => match (mode, props.command) {
|
||||
(Mode::Insert, true) => Command(CommandAction::Execute),
|
||||
(Mode::Insert, false) => InsertCharacter('\n'),
|
||||
_ => None,
|
||||
},
|
||||
|
||||
// Normal mode works everywhere — hjkl, i, etc.
|
||||
// The same bindings apply in a command buffer as in a document buffer.
|
||||
Input::Character(ch) => match mode {
|
||||
Mode::Insert if !props.read_only => InsertCharacter(ch),
|
||||
Mode::Normal if ch == 'i' && !props.read_only => TransitionToMode(Mode::Insert),
|
||||
Mode::Normal if ch == ':' => FocusCommandBuffer,
|
||||
// hjkl etc. — same in all buffers
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice: `is_command` (the identity check) disappears. The configuration checks `props.command`, `props.ephemeral`, and `props.read_only` — meaningful properties, not magic IDs. The same Normal mode keybindings work in every buffer.
|
||||
|
||||
### Mode and read_only Interaction
|
||||
|
||||
`Mode::Insert` simply doesn't apply to a read-only buffer:
|
||||
- `TransitionToMode(Mode::Insert)` is a no-op (or never generated — Configuration checks `read_only` before emitting it)
|
||||
- The buffer is always effectively in Normal mode
|
||||
- This covers help buffers, about screens, etc. naturally
|
||||
|
||||
### What About the Future DSL?
|
||||
|
||||
All of the hardcoded `match` arms in Configuration are temporary. The future scripting/DSL layer will allow users to define arbitrary input mappings per buffer property, per mode, per any predicate. The point of `BufferProperties` is that it provides stable, meaningful predicates for those mappings to key off of — not to encode behavior directly.
|
||||
|
||||
## What Changes
|
||||
|
||||
Regardless of which option, the refactor involves:
|
||||
1. **Add `BufferProperties` to `TextBuffer`.** Four fields: `listed`, `ephemeral`, `read_only`, `command`. Plus convenience constructors `document()`, `command()`, `info()`.
|
||||
|
||||
1. **Remove `COMMAND_BUFFER_ID` constant.** Buffer identity no longer implies role.
|
||||
2. **Remove `COMMAND_BUFFER_ID` constant.** Buffer identity no longer implies role.
|
||||
|
||||
2. **Add role/kind/properties to `TextBuffer`.** The buffer itself knows what it is.
|
||||
3. **Remove forced `set_mode(Mode::Insert)` on command buffer.** The command buffer starts in Insert mode naturally (the `FocusBuffer` action could transition to Insert, or it just stays in whatever mode — the user can switch freely).
|
||||
|
||||
3. **Replace identity checks with property checks.** `id == COMMAND_BUFFER_ID` becomes `buffer.is_command()` or `buffer.kind() == BufferKind::Command`.
|
||||
4. **Replace identity checks with property checks in `EditorCore`:**
|
||||
- `main_display_buffer()` checks `!listed` instead of `== COMMAND_BUFFER_ID`
|
||||
- `FocusBuffer` clears buffer if `ephemeral` instead of `== COMMAND_BUFFER_ID`
|
||||
- `FocusPreviousBuffer` clears buffer if `ephemeral`
|
||||
- `command_buffer()` / `command_buffer_is_active()` become property-based queries or go away entirely
|
||||
|
||||
4. **Find command buffer(s) by query, not by ID.** `EditorCore` could cache the active command buffer ID, or search: `self.buffers.values().find(|b| b.is_command())`.
|
||||
5. **Update `Configuration`:**
|
||||
- Receives `&BufferProperties` instead of `BufferId`
|
||||
- Checks `props.command`, `props.ephemeral`, `props.read_only` instead of `is_command`
|
||||
- No longer imports `COMMAND_BUFFER_ID`
|
||||
- Normal mode keybindings work uniformly across all buffers
|
||||
|
||||
5. **Generalize focus semantics.** The "clear on focus, restore previous buffer on defocus" behavior currently hardcoded for the command buffer becomes driven by buffer properties (e.g., `ephemeral` buffers always clear and restore).
|
||||
|
||||
6. **`Configuration` stops importing `COMMAND_BUFFER_ID`.** Input mapping checks buffer properties instead: `if buffer.is_command()` rather than `if active_buffer_id == COMMAND_BUFFER_ID`.
|
||||
6. **Core tracks command buffer by `Option<BufferId>`** rather than a constant. Set when a command buffer is created/focused, cleared when it's destroyed.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `BufferKind` be extensible (e.g., plugins can register new kinds)? Probably not yet -- start simple.
|
||||
- Should the command buffer be created lazily (only when `:` is pressed) or eagerly at startup? Currently eager. Lazy creation would mean the command buffer truly is "just another buffer" that gets created and destroyed on demand.
|
||||
- How does this interact with the `Mode` system? Currently the command buffer is always in Insert mode. Should `BufferKind::Command` imply Insert mode, or should that remain a separate property?
|
||||
- Should `BufferKind` affect rendering, or should rendering be driven entirely by pane layout position? (Currently the command pane is rendered based on layout position, which is good separation.)
|
||||
- **Lazy vs eager creation.** Should the command buffer be created lazily (only when `:` is pressed) or eagerly at startup? Currently eager. Lazy creation means the command buffer is truly "just another buffer" that gets created and destroyed on demand, but adds complexity to the `:` keybinding path.
|
||||
- **Presentation layer.** Rendering is currently driven by pane layout position (the command pane is always at the bottom). `BufferProperties` should not dictate rendering — the same `ephemeral` buffer could render as a bottom bar or a floating palette depending on pane layout configuration. Is the current pane layout system flexible enough for this, or does it need extension?
|
||||
- **Label / prompt prefix.** The `:` prefix in the command line is a rendering concern. Should there be a `label: Option<String>` on the buffer or on the pane layout entry? Probably the latter (presentation, not buffer state).
|
||||
- **Submit semantics.** `command: true` tells the core where submission goes (the command system). But what about non-command buffers that also have submit behavior (finder, prompt)? Each would need its own flag, or there could be a more general `submit_target: Option<SubmitTarget>` enum. For now `command: bool` is sufficient — generalize when a second submit target actually appears.
|
||||
- **History.** Vim's command line has history (up/down to cycle previous commands). If the command buffer is ephemeral (cleared on focus), history would need to live outside the buffer — in the command system. This is probably the right place for it anyway.
|
||||
|
||||
Reference in New Issue
Block a user