Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/helm-editor/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ impl CommandRegistry {
registry
}

/// A registry holding the commands of
/// [`with_editor_commands`](Self::with_editor_commands) and the cursor
/// commands under the ids in
/// [`cursor_commands`](crate::cursor_commands): every command the
/// [`vscode`](crate::vscode) keymap binds. This is the registry
/// `helm-ui` builds its dispatcher with.
pub fn with_all_commands() -> Self {
todo!()
}

/// Adds `command`. Errors, and adds nothing, if its id isn't in the
/// form [`CommandId`] describes or is already registered.
pub fn register(&mut self, command: Command) -> Result<(), RegisterError> {
Expand Down
115 changes: 115 additions & 0 deletions crates/helm-editor/src/cursor_commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! The ids of the cursor commands, which
//! [`CommandRegistry::with_all_commands`] registers next to the
//! [`editor_commands`](crate::editor_commands). Keymaps (T-107, T-108)
//! bind these.
//!
//! Each move and select command runs [`Editor::move_cursors`] or
//! [`Editor::extend_selections`] with the [`Motion`] its name says, and
//! each delete command runs [`Editor::delete_to`]. They live apart from
//! `editor_commands` because T-106's tests pin that module's list.
//!
//! [`CommandRegistry::with_all_commands`]: crate::CommandRegistry::with_all_commands
//! [`Editor::move_cursors`]: crate::Editor::move_cursors
//! [`Editor::extend_selections`]: crate::Editor::extend_selections
//! [`Editor::delete_to`]: crate::Editor::delete_to
//! [`Motion`]: crate::Motion

#![expect(
dead_code,
reason = "T-107 interface stubs; the implementation deletes this"
)]

use crate::CommandRegistry;

/// `move_cursors(Motion::Left)`.
pub const LEFT: &str = "cursor.left";
/// `move_cursors(Motion::Right)`.
pub const RIGHT: &str = "cursor.right";
/// `move_cursors(Motion::Up)`.
pub const UP: &str = "cursor.up";
/// `move_cursors(Motion::Down)`.
pub const DOWN: &str = "cursor.down";
/// `move_cursors(Motion::WordLeft)`.
pub const WORD_LEFT: &str = "cursor.word_left";
/// `move_cursors(Motion::WordRight)`.
pub const WORD_RIGHT: &str = "cursor.word_right";
/// `move_cursors(Motion::LineStart)`.
pub const LINE_START: &str = "cursor.line_start";
/// `move_cursors(Motion::LineEnd)`.
pub const LINE_END: &str = "cursor.line_end";
/// `move_cursors(Motion::TextStart)`.
pub const TEXT_START: &str = "cursor.text_start";
/// `move_cursors(Motion::TextEnd)`.
pub const TEXT_END: &str = "cursor.text_end";

/// `extend_selections(Motion::Left)`.
pub const SELECT_LEFT: &str = "cursor.select_left";
/// `extend_selections(Motion::Right)`.
pub const SELECT_RIGHT: &str = "cursor.select_right";
/// `extend_selections(Motion::Up)`.
pub const SELECT_UP: &str = "cursor.select_up";
/// `extend_selections(Motion::Down)`.
pub const SELECT_DOWN: &str = "cursor.select_down";
/// `extend_selections(Motion::WordLeft)`.
pub const SELECT_WORD_LEFT: &str = "cursor.select_word_left";
/// `extend_selections(Motion::WordRight)`.
pub const SELECT_WORD_RIGHT: &str = "cursor.select_word_right";
/// `extend_selections(Motion::LineStart)`.
pub const SELECT_LINE_START: &str = "cursor.select_line_start";
/// `extend_selections(Motion::LineEnd)`.
pub const SELECT_LINE_END: &str = "cursor.select_line_end";
/// `extend_selections(Motion::TextStart)`.
pub const SELECT_TEXT_START: &str = "cursor.select_text_start";
/// `extend_selections(Motion::TextEnd)`.
pub const SELECT_TEXT_END: &str = "cursor.select_text_end";

/// [`Editor::select_all`](crate::Editor::select_all).
pub const SELECT_ALL: &str = "cursor.select_all";
/// [`Editor::collapse_selections`](crate::Editor::collapse_selections).
pub const COLLAPSE: &str = "cursor.collapse";

/// `delete_to(Motion::WordLeft)`.
pub const DELETE_WORD_LEFT: &str = "cursor.delete_word_left";
/// `delete_to(Motion::WordRight)`.
pub const DELETE_WORD_RIGHT: &str = "cursor.delete_word_right";
/// `delete_to(Motion::LineStart)`.
pub const DELETE_LINE_START: &str = "cursor.delete_line_start";
/// `delete_to(Motion::LineEnd)`.
pub const DELETE_LINE_END: &str = "cursor.delete_line_end";

/// Every id above, sorted.
pub const ALL: [&str; 26] = [
COLLAPSE,
DELETE_LINE_END,
DELETE_LINE_START,
DELETE_WORD_LEFT,
DELETE_WORD_RIGHT,
DOWN,
LEFT,
LINE_END,
LINE_START,
RIGHT,
SELECT_ALL,
SELECT_DOWN,
SELECT_LEFT,
SELECT_LINE_END,
SELECT_LINE_START,
SELECT_RIGHT,
SELECT_TEXT_END,
SELECT_TEXT_START,
SELECT_UP,
SELECT_WORD_LEFT,
SELECT_WORD_RIGHT,
TEXT_END,
TEXT_START,
UP,
WORD_LEFT,
WORD_RIGHT,
];

/// Registers one command for each id in [`ALL`], each with a non-empty
/// title.
pub(crate) fn register_all(registry: &mut CommandRegistry) {
let _ = registry;
todo!()
}
115 changes: 114 additions & 1 deletion crates/helm-editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use helm_text::{
Buffer, BufferId, BufferSet, Edit, FileFormat, TextBuffer, Transaction, TransactionId,
};

use crate::{EditorError, Selection, Selections};
use crate::{EditorError, Motion, Selection, Selections};

/// The selections on one buffer of a [`BufferSet`], and the commands that
/// edit at every selection or add new ones.
Expand Down Expand Up @@ -70,6 +70,30 @@ use crate::{EditorError, Selection, Selections};
/// around a cursor extends left from it while the character before is a
/// word character, and right while the character after is one. If both
/// neighbours aren't word characters, there is no word.
///
/// # Motions (T-107)
///
/// [`move_cursors`](Self::move_cursors),
/// [`extend_selections`](Self::extend_selections) and
/// [`delete_to`](Self::delete_to) move each selection's head by a
/// [`Motion`], which documents where each one goes. Moving reads only the
/// text near each head, and the lines it crosses for
/// [`Motion::Up`] and [`Motion::Down`]; it never copies the whole text.
///
/// # Goal columns
///
/// Moving up and down through a short line and back keeps the column, as
/// in every editor. Each selection has a goal column, in chars from the
/// start of its head's line. The first [`Motion::Up`] or [`Motion::Down`],
/// through `move_cursors` or `extend_selections`, sets it to the head's
/// column, capped at the line's length without its line break. Later Up
/// and Down moves aim for the goal, not the current column, and keep it.
///
/// Any other `&mut self` method drops every goal, including other motions,
/// editing commands, undo, redo and
/// [`set_selections`](Self::set_selections). If an Up or Down move merges
/// selections, the merged one keeps the goal of one of them; which one is
/// not specified.
#[derive(Clone, Debug)]
pub struct Editor {
buffer: BufferId,
Expand Down Expand Up @@ -326,6 +350,95 @@ impl Editor {
}
Ok(id)
}

/// Moves every selection by `motion` (T-107): each becomes a cursor
/// where `motion` takes its head. Returns whether the selections
/// changed.
///
/// [`Motion::Left`] and [`Motion::Right`] are the exception: a
/// selection that isn't empty becomes a cursor at its start or its end,
/// without moving further. [`Motion::Up`] and [`Motion::Down`] use and
/// keep goal columns, as the type docs describe.
///
/// The selections are clamped first and put back in normal form after,
/// so cursors that land on one offset merge. The primary is the
/// selection the old primary became.
#[expect(
unused_variables,
reason = "T-107 interface stub; the implementation deletes this"
)]
pub fn move_cursors(&mut self, set: &BufferSet, motion: Motion) -> Result<bool, EditorError> {
todo!()
}

/// Extends every selection by `motion` (T-107): each head moves where
/// `motion` takes it, and each anchor stays. Returns whether the
/// selections changed.
///
/// Unlike [`move_cursors`](Self::move_cursors), [`Motion::Left`] and
/// [`Motion::Right`] move the head of a selection that isn't empty like
/// any other. A head that moves back onto its anchor leaves a cursor.
/// Clamping, normal form, the primary and goal columns work as in
/// `move_cursors`.
#[expect(
unused_variables,
reason = "T-107 interface stub; the implementation deletes this"
)]
pub fn extend_selections(
&mut self,
set: &BufferSet,
motion: Motion,
) -> Result<bool, EditorError> {
todo!()
}

/// Deletes from every cursor to where `motion` takes it (T-107), in one
/// transaction, such as Option+Backspace for [`Motion::WordLeft`].
/// A selection that isn't empty deletes its range instead, as
/// [`delete_backward`](Self::delete_backward) does.
///
/// Each cursor's range runs between its head and where `motion` takes
/// it, whichever comes first. Otherwise it works as `delete_backward`:
/// overlapping ranges are joined, each selection that deleted something
/// becomes a cursor where its range started, the others stay where they
/// were, moved by the edits before them, and with nothing to delete it
/// applies no transaction and returns `Ok(None)`. Undo and redo put
/// the selections back as for any editing command.
#[expect(
unused_variables,
reason = "T-107 interface stub; the implementation deletes this"
)]
pub fn delete_to(
&mut self,
set: &mut BufferSet,
motion: Motion,
) -> Result<Option<TransactionId>, EditorError> {
todo!()
}

/// Replaces the selections with one selection of the whole text,
/// pointing forward from offset 0, as the primary (T-107). Returns
/// whether the selections changed. In an empty text that is a cursor
/// at 0.
#[expect(
unused_variables,
reason = "T-107 interface stub; the implementation deletes this"
)]
pub fn select_all(&mut self, set: &BufferSet) -> Result<bool, EditorError> {
todo!()
}

/// VS Code's Escape (T-107). With more than one selection, keeps only
/// the primary, as it is. With one selection, makes it a cursor at its
/// head. Returns whether the selections changed. The selections are
/// clamped first.
#[expect(
unused_variables,
reason = "T-107 interface stub; the implementation deletes this"
)]
pub fn collapse_selections(&mut self, set: &BufferSet) -> Result<bool, EditorError> {
todo!()
}
}

impl Editor {
Expand Down
14 changes: 10 additions & 4 deletions crates/helm-editor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@
//! `"editor.delete_backward"`, and runs against one [`Editor`] and the
//! [`BufferSet`](helm_text::BufferSet) that owns its buffer. A
//! [`CommandRegistry`] maps ids to commands.
//! [`CommandRegistry::with_editor_commands`] registers every `Editor`
//! command under the ids in [`editor_commands`].
//! [`CommandRegistry::with_editor_commands`] registers the T-105 `Editor`
//! commands under the ids in [`editor_commands`], and
//! [`CommandRegistry::with_all_commands`] adds the cursor commands in
//! [`cursor_commands`]: moving, selecting and deleting by a [`Motion`].
//!
//! # Keys, keymaps and modes
//!
Expand All @@ -58,8 +60,8 @@
//! A [`Keymap`] is data: a list of [`Mode`]s and a list of [`Binding`]s
//! from a [`KeySequence`] (one keystroke, or a chord such as
//! `cmd-k cmd-s`) to a command id, each scoped to a mode and an optional
//! context. VS Code's keymap (T-107) has one mode; Vim's (T-108) has
//! several. A mode says whether a key no binding matches types its text,
//! context. VS Code's keymap ([`vscode::keymap`], T-107) has one mode;
//! Vim's (T-108) has several. A mode says whether a key no binding matches types its text,
//! so typing takes the same path as every other command.
//!
//! A [`Dispatcher`] holds the active keymap and the registry, and each
Expand All @@ -70,13 +72,16 @@
//! editor uses the new keymap from its next key (ED-6).

mod command;
pub mod cursor_commands;
mod dispatch;
mod editor;
pub mod editor_commands;
mod error;
mod key;
mod keymap;
mod motion;
mod selection;
pub mod vscode;

pub use command::{
Command, CommandArgs, CommandContext, CommandError, CommandFn, CommandId, CommandRegistry,
Expand All @@ -87,4 +92,5 @@ pub use editor::Editor;
pub use error::EditorError;
pub use key::{Key, KeyEvent, KeyParseError, KeySequence, Keystroke, Modifiers};
pub use keymap::{Action, Binding, Keymap, KeymapError, Mode, ModeId, Typing};
pub use motion::Motion;
pub use selection::{Selection, Selections};
Loading
Loading