mod ex;
mod execute;
mod motions;
mod pending;
mod preview;
mod search;
use strop_grammar::{self as grammar, Parse};
use super::Editor;
use strop_core::frontend_input::Key;
pub(crate) const EX_COMMANDS: &[(&str, &str)] = &[
("theme", "choose a built-in interface palette"),
("terminal", "local interactive shell or explicit command"),
(
"terminal-local",
"explicit local shell even from an SSH/container context",
),
(
"terminal-stop",
"stop the current terminal and retain its output",
),
(
"terminal-paste",
"confirm the current terminal's held multiline/control paste",
),
(
"terminal-paste-cancel",
"discard the current terminal's held paste",
),
(
"terminal-refresh",
"reinstall the latest output into an inspected terminal snapshot",
),
("w", "write"),
("w!", "write, force (file changed on disk)"),
("wq!", "write forced + quit"),
("q", "quit"),
("q!", "quit, force"),
("wq", "write + quit"),
("e", "edit file"),
("e!", "edit file, force"),
("vs", "split vertical"),
("vsplit", "split vertical"),
("sp", "split horizontal"),
("split", "split horizontal"),
("help", "help buffer"),
("h", "help buffer"),
("jumps", "open the jumplist picker"),
("!", "run shell command"),
("view", "open readonly"),
("set", "readonly option: ro/noro"),
("noh", "clear the persistent search highlight"),
("tail", "remote tail: :tail [BYTES] URI"),
("range", "remote range: :range START BYTES URI"),
("follow", "follow remote EOF: :follow [URI]"),
("unfollow", "stop remote following"),
("browse", "browse remote directory: :browse [URI]"),
("filter", "filter remote directory by literal filename"),
(
"remote",
"choose remote destination; edit/verify/root/home/connect/disconnect/clear/list",
),
("trust", "authorize the pending endpoint/project command"),
(
"explain",
"why: workspaces, LSP readiness, effective config",
),
("containers", "attach to a running container (read-only)"),
(
"container-worker",
"authorize a verified worker for the attached container",
),
("format", "format the buffer through the language server"),
("rename", "rename the symbol under the cursor: :rename NEW"),
("symbols", "open the document symbols picker"),
("undo-change", "undo the last applied change plan"),
(
"save-change",
"save files changed by the latest reviewed operation",
),
("apply-change", "apply the prepared proposal"),
("cancel-change", "cancel the prepared proposal"),
(
"select-next",
"select the next occurrence of the word or selection",
),
("select-all", "select all occurrences in buffer"),
("select-skip", "skip the next occurrence candidate"),
("select-pop", "drop the last-added occurrence"),
("search-options", "choose hidden and ignored search policy"),
("tab-size", "choose indentation width: N or auto"),
(
"indent-style",
"choose indentation style: spaces, tabs or auto",
),
(
"collection",
"source, expand or contract the excerpt at the caret",
),
(
"recover",
"draft recovery: list, restore N, discard N, consent remote|off",
),
("qa", "quit all (fails on unsaved; :qa! discards)"),
("qall", "quit all (fails on unsaved; :qall! discards)"),
];
impl Editor {
pub(crate) fn feed_normal(&mut self, key: Key) {
if self.buf().readonly {
return self.feed_readonly(key);
}
if self.walker.is_ground() && self.collections.contains_key(&self.current()) {
match key {
Key::Char('+') => {
self.collection_context_step(true);
return;
}
Key::Char('-') => {
self.collection_context_step(false);
return;
}
_ => {}
}
}
if key == Key::Esc {
self.cancel_remote_write(self.current());
self.collapse_cursors();
self.walker.clear();
return;
}
self.feed_command(key);
}
pub(crate) fn feed_command(&mut self, key: Key) {
let action = self.walker.feed(key);
match action {
super::input::Action::Pending => {}
super::input::Action::Invalid(keys) => {
self.message = format!("not an editor command: {keys}")
}
super::input::Action::QueryError(error) => self.message = error.to_string(),
super::input::Action::EnterText { sigil, state } => self.begin_text_line(sigil, state),
super::input::Action::Grammar(command) => self.dispatch_grammar(&command),
super::input::Action::Row {
row,
count,
register,
arg,
key,
} => self.dispatch_row(row, count, register, arg, key),
super::input::Action::VisualSurround(_) => {
unreachable!("normal Walker cannot emit visual actions")
}
}
}
pub(crate) fn dispatch_grammar(&mut self, command: &grammar::Command) {
match command.op {
None => self.move_cursor(command),
Some(grammar::Op::Yank) if self.buf().readonly => self.yank_only(command),
Some(_) if self.buf().readonly => self.message = "readonly buffer".into(),
Some(_) => self.execute(command),
}
}
fn dispatch_row(
&mut self,
row: &'static crate::keymap::Binding,
count: Option<usize>,
register: Option<char>,
arg: Option<char>,
key: char,
) {
use crate::keymap::Handler;
if self.buf().readonly {
let allowed = match row.handler {
Handler::Alias(_) => true, Handler::Leaf(_) => {
row.sections.contains(&"git")
|| matches!(
row.id,
"search-next"
| "search-prev"
| "visual-enter"
| "visual-block"
| "pane-nav"
| "pane-split"
| "pane-close"
| "hunk-nav"
| "clip-yank"
| "redraw"
| "view-place"
| "visible-jumps"
| "scroll-pages"
| "enter"
| "jumplist"
| "jump-forward"
| "goto-definition"
| "switch-source-header"
| "references"
| "implementation"
| "type-definition"
| "declaration"
| "diagnostic-jumps"
| "hover"
| "remote-open"
| "files"
| "directory-reveal"
| "buffers"
| "search"
| "replace-global"
| "help"
| "alternate-buffer"
| "word-search"
| "document-symbols"
| "diagnostics"
| "code-actions"
| "jumplist-picker"
| "undo-tree"
)
}
Handler::AbsorbChar(
crate::keymap::AbsorbKind::Find
| crate::keymap::AbsorbKind::MarkSet
| crate::keymap::AbsorbKind::MarkJump,
) => true,
_ => false,
};
if !allowed {
self.message = "readonly buffer".into();
return;
}
}
let n = count.unwrap_or(1);
match row.handler {
Handler::Leaf(f) => {
let last = key;
match row.id {
"visible-jumps" => self.jump_visible(last, n),
"insert-entries" => {
self.insert_count = n;
f(self, last);
}
"paste" => self.paste_named(register, n, last == 'P'),
"scroll-pages" => self.scroll_counted(last, n),
"clip-yank" => self
.walker
.begin_operator(grammar::Op::Yank, Some('+'), count),
_ => self.run_counted_leaf(f, last, n),
}
}
Handler::Alias(expansion) => match grammar::parse(expansion) {
Parse::Complete(mut cmd) => {
cmd.count = Some(n.saturating_mul(cmd.count.unwrap_or(1)));
if register.is_some() {
cmd.register = register;
}
self.dispatch_grammar(&cmd);
}
Parse::QueryError(error) => self.message = error.to_string(),
Parse::Incomplete | Parse::Invalid => {
self.message = format!("not an editor command: {key}");
}
},
Handler::AbsorbChar(kind) => {
let c = arg.unwrap_or('\0');
use crate::keymap::AbsorbKind;
match kind {
AbsorbKind::Replace => self.replace_char_n(c, n),
AbsorbKind::MarkSet => self.set_mark(c),
AbsorbKind::MarkJump => self.jump_mark(c),
AbsorbKind::Find => {} AbsorbKind::MacroRecord => self.macro_toggle(c),
AbsorbKind::MacroPlay if c == '@' => self.macro_again(n),
AbsorbKind::MacroPlay => self.macro_play(c, n),
}
}
Handler::Prefix
| Handler::Motion
| Handler::Operator
| Handler::ObjectPrefix
| Handler::TextLine
| Handler::AbsorbRegister
| Handler::Soon
| Handler::Contextual => {}
}
}
pub(crate) fn repeat_search_pub(&mut self, invert: bool) {
self.repeat_search(invert);
}
pub(crate) fn jump_hunk_pub(&mut self, forward: bool) {
self.jump_hunk(forward);
}
pub(crate) fn search_word_under_cursor_pub(&mut self, backward: bool) {
self.search_word_under_cursor(backward);
}
pub(crate) fn repeat_find_pub(&mut self, reverse: bool) {
self.repeat_find(reverse);
}
pub(crate) fn paste_named_pub(&mut self, name: Option<char>, count: usize, before: bool) {
self.paste_named(name, count, before);
}
pub(crate) fn join_or_repeat(&mut self, key: char) {
if key == 'J' {
self.join_lines_pub();
} else {
self.dot_repeat_pub();
}
}
pub(crate) fn insert_entry_pub(&mut self, key: char) {
match key {
'i' => self.enter_insert_from("i"),
'a' => self.append(),
'A' => self.append_eol(),
'o' => self.open_below(),
'O' => self.open_above(),
'I' => {
self.run_motion("^");
self.enter_insert_from("I");
}
_ => {}
}
}
pub(crate) fn enter_visual_pub(&mut self, key: char) {
self.enter_visual(key == 'V');
}
}