use std::path::PathBuf;
use super::push_word_search;
use crate::action::{Ctx, DirectKind, PromptKind};
use crate::app::App;
use crate::app::eval::format_dirty_list;
use crate::buffer_ref::BufferRef;
use crate::editor::Cursor;
use crate::effect::{Cmd, ScrollAnchor};
use crate::mode::Mode;
pub(super) fn handle_direct(app: &mut App, kind: DirectKind, count: u32, ctx: Ctx) -> Vec<Cmd> {
use DirectKind as D;
let mut cmds = Vec::new();
match kind {
D::EnterMode(m) => cmds.push(Cmd::EnterMode(m)),
D::OpenPrompt(k) => cmds.push(Cmd::OpenPrompt(k)),
D::OpenLineBelow => {
let indent = app.indent_settings();
app.buffer.insert_line_below(indent);
cmds.push(Cmd::EnterMode(Mode::Insert));
}
D::OpenLineAbove => {
let indent = app.indent_settings();
app.buffer.insert_line_above(indent);
cmds.push(Cmd::EnterMode(Mode::Insert));
}
D::AppendAfterCursor => {
app.buffer.move_right(true);
cmds.push(Cmd::EnterMode(Mode::Insert));
}
D::AppendAtLineEnd => {
app.buffer.cursor.col = app.buffer.current_line_len();
cmds.push(Cmd::EnterMode(Mode::Insert));
}
D::InsertAtLineStart => {
let line = app.buffer.current_line();
let col = line.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
app.buffer.cursor.col = col;
cmds.push(Cmd::EnterMode(Mode::Insert));
}
D::ChangeToEol => {
app.buffer.delete_to_eol();
cmds.push(Cmd::EnterMode(Mode::Insert));
}
D::DeleteToEol => app.buffer.delete_to_eol(),
D::YankLine => {
for _ in 0..count {
app.buffer.yank_line();
}
cmds.push(Cmd::SyncYank);
cmds.push(Cmd::ToastInfo("yanked".into()));
}
D::JoinLines => {
for _ in 0..count {
app.buffer.join_next_line();
}
}
D::ToggleCase => {
for _ in 0..count {
app.buffer.toggle_case_under_cursor();
}
}
D::SubstituteChar => {
for _ in 0..count {
app.buffer.delete_char_under_cursor();
}
cmds.push(Cmd::EnterMode(Mode::Insert));
}
D::SubstituteLine => {
app.buffer.clear_current_line();
cmds.push(Cmd::EnterMode(Mode::Insert));
}
D::ReplaceChar { ch } => {
for _ in 0..count {
app.buffer.replace_char(ch);
app.buffer.move_right(false);
}
app.buffer.move_left();
}
D::ViewportCenter => cmds.push(Cmd::Scroll(ScrollAnchor::Center)),
D::ViewportTopAtCursor => cmds.push(Cmd::Scroll(ScrollAnchor::Top)),
D::ViewportBottomAtCursor => cmds.push(Cmd::Scroll(ScrollAnchor::Bottom)),
D::Paste => {
for _ in 0..count {
app.buffer.paste_after();
}
}
D::Undo => {
if !app.buffer.undo() {
cmds.push(Cmd::ToastError("already at oldest change".into()));
}
}
D::Redo => {
if !app.buffer.redo() {
cmds.push(Cmd::ToastError("already at newest change".into()));
}
}
D::DeleteCharUnderCursor => {
for _ in 0..count {
app.buffer.delete_char_under_cursor();
}
}
D::Quit => cmds.push(plan_quit(app)),
D::QuitForce => cmds.push(Cmd::Quit),
D::BufferNext => cmds.push(Cmd::BufferCycle { forward: true }),
D::BufferPrev => cmds.push(Cmd::BufferCycle { forward: false }),
D::BufferDelete => cmds.push(Cmd::BufferDelete { force: false }),
D::BufferDeleteForce => cmds.push(Cmd::BufferDelete { force: true }),
D::BufferDeleteAll => cmds.push(Cmd::BufferDeleteAll),
D::BufferList => {
cmds.push(Cmd::OpenPrompt(PromptKind::Fuzzy(
crate::finder::FuzzyKind::Buffers,
)));
}
D::NewScratchBuffer => cmds.push(Cmd::NewScratchBuffer),
D::SaveAndQuit => cmds.push(Cmd::Save {
path: parse_save_path(ctx.rest),
then_quit: true,
force: false,
}),
D::Save => cmds.push(Cmd::Save {
path: parse_save_path(ctx.rest),
then_quit: false,
force: false,
}),
D::SaveForce => cmds.push(Cmd::Save {
path: parse_save_path(ctx.rest),
then_quit: false,
force: true,
}),
D::Open => {
if ctx.rest.is_empty() {
cmds.push(Cmd::ToastError("missing path".into()));
} else {
cmds.push(Cmd::OpenPath(PathBuf::from(ctx.rest)));
}
}
D::OpenLog => match crate::log::default_path() {
Some(p) => cmds.push(Cmd::OpenPath(p)),
None => cmds.push(Cmd::ToastError("log path unresolved".into())),
},
D::Reload => cmds.push(Cmd::Reload),
D::ReloadAll => cmds.push(Cmd::ReloadAll),
D::GotoLine => match ctx.rest.parse::<usize>() {
Ok(n) if n >= 1 => app.goto_line_n_pure(n),
_ => cmds.push(Cmd::ToastError("usage: :goto <line>".into())),
},
D::GotoDefinition => cmds.push(Cmd::LspJump {
method: "textDocument/definition",
label: "definition",
}),
D::GotoDeclaration => cmds.push(Cmd::LspJump {
method: "textDocument/declaration",
label: "declaration",
}),
D::GotoImplementation => cmds.push(Cmd::LspJump {
method: "textDocument/implementation",
label: "implementation",
}),
D::FindReferences => cmds.push(Cmd::LspFindReferences),
D::Rename => cmds.push(Cmd::OpenRenamePrompt),
D::CodeAction => cmds.push(Cmd::LspCodeAction),
D::Hover => cmds.push(Cmd::LspHover),
D::LspStatus => {
let all = matches!(ctx.rest.trim(), "all" | "a");
cmds.push(Cmd::OpenLspStatus { all });
}
D::GotoDiagnostic { forward } => {
cmds.push(Cmd::GotoDiagnostic { forward, count });
}
D::RepeatLast => unreachable!("RepeatLast handled in App::evaluate"),
D::SearchSelectNext { reverse } => {
cmds.push(Cmd::SearchSelectMatch { reverse });
}
D::SearchWordKeep { forward } => {
push_word_search(app, &mut cmds, forward, false);
}
D::ClearSearch => {
cmds.push(Cmd::SetSearch {
pattern: String::new(),
forward: true,
});
}
D::Substitute => run_substitute(app, ctx.rest, &mut cmds),
D::MultiCursorAddNext => add_next_cursor(app, &mut cmds),
D::MultiCursorAddBelow => add_cursor_below(app, &mut cmds),
D::MultiCursorPop => {
if let Some(c) = app.buffer.extra_cursors.pop() {
app.buffer.cursor = c;
} else {
cmds.push(Cmd::ToastInfo("no extra cursor to remove".into()));
}
}
D::MultiCursorClear => {
if app.buffer.extra_cursors.is_empty() {
cmds.push(Cmd::ToastInfo("no extra cursors".into()));
} else {
let n = app.buffer.extra_cursors.len();
app.buffer.extra_cursors.clear();
cmds.push(Cmd::ToastInfo(format!("cleared {n} extra cursors")));
}
}
D::JumpLabel => cmds.push(Cmd::StartJumpLabel),
D::SelectWholeBuffer => cmds.push(Cmd::SelectWholeBuffer),
D::ToggleComment => match buffer_comment_token(app) {
Some(token) => {
let start_row = app.buffer.cursor.row;
let start_col = app.buffer.cursor.col;
let max = app.buffer.lines.len();
for i in 0..count {
app.buffer.toggle_line_comment(&token);
if i + 1 < count && app.buffer.cursor.row + 1 < max {
app.buffer.cursor.row += 1;
}
}
app.buffer.cursor.row = start_row;
app.buffer.cursor.col = start_col;
app.buffer.clamp_col(false);
}
None => cmds.push(Cmd::ToastError("no comment token for this buffer".into())),
},
D::SplitWindowHorizontal => cmds.push(Cmd::SplitWindow {
dir: crate::app::SplitDir::Horizontal,
}),
D::SplitWindowVertical => cmds.push(Cmd::SplitWindow {
dir: crate::app::SplitDir::Vertical,
}),
D::CloseWindow => cmds.push(Cmd::CloseWindow),
D::FocusWindow { dir } => cmds.push(Cmd::FocusWindow { dir }),
D::CycleWindow => cmds.push(Cmd::CycleWindow),
}
cmds
}
fn plan_quit(app: &App) -> Cmd {
if app.buffer.dirty {
return Cmd::ToastError("unsaved changes (use :q!)".into());
}
let sleeping_dirty: Vec<&BufferRef> = app
.sleeping
.iter()
.filter(|(_, b)| b.dirty)
.map(|(r, _)| r)
.collect();
if !sleeping_dirty.is_empty() {
return Cmd::ToastError(format!(
"unsaved changes in {} (use :q!)",
format_dirty_list(&sleeping_dirty)
));
}
Cmd::Quit
}
fn add_next_cursor(app: &mut App, cmds: &mut Vec<Cmd>) {
let Some(word) = crate::app::eval::word_under_cursor(&app.buffer) else {
app.enter_mode(Mode::Visual);
return;
};
let mut tmp = crate::editor::SearchState::default();
tmp.set(word.clone(), true);
let Some(next) = tmp.find_next(&app.buffer, true) else {
cmds.push(Cmd::ToastError("no further match".into()));
return;
};
let primary = app.buffer.cursor;
if next == primary || app.buffer.extra_cursors.contains(&next) {
cmds.push(Cmd::ToastInfo("no further match".into()));
return;
}
app.buffer.extra_cursors.push(primary);
app.buffer.cursor = next;
cmds.push(Cmd::SetSearch {
pattern: word,
forward: true,
});
let n = app.buffer.extra_cursors.len() + 1;
cmds.push(Cmd::ToastInfo(format!("{n} cursors")));
}
fn add_cursor_below(app: &mut App, cmds: &mut Vec<Cmd>) {
let primary = app.buffer.cursor;
if primary.row + 1 >= app.buffer.lines.len() {
cmds.push(Cmd::ToastError("no line below".into()));
return;
}
let next_row = primary.row + 1;
let next_line_len = app.buffer.lines[next_row].chars().count();
let next_col = primary.col.min(next_line_len.saturating_sub(1));
let next = Cursor {
row: next_row,
col: next_col,
};
if next == primary || app.buffer.extra_cursors.contains(&next) {
cmds.push(Cmd::ToastInfo("cursor already there".into()));
return;
}
app.buffer.extra_cursors.push(primary);
app.buffer.cursor = next;
let n = app.buffer.extra_cursors.len() + 1;
cmds.push(Cmd::ToastInfo(format!("{n} cursors")));
}
fn parse_save_path(rest: &str) -> Option<PathBuf> {
if rest.is_empty() {
None
} else {
Some(PathBuf::from(rest))
}
}
fn run_substitute(app: &mut App, raw: &str, cmds: &mut Vec<Cmd>) {
let Some(parsed) = crate::editor::parse_substitute(raw) else {
cmds.push(Cmd::ToastError("usage: :s/pat/repl/[g]".into()));
return;
};
let args = match parsed {
Ok(a) => a,
Err(msg) => {
cmds.push(Cmd::ToastError(msg.into()));
return;
}
};
let fallback;
let pattern = if args.pattern.is_empty() {
if app.search.query.is_empty() {
cmds.push(Cmd::ToastError("no previous search pattern".into()));
return;
}
fallback = app.search.query.clone();
fallback.as_str()
} else {
args.pattern
};
let resolved = crate::editor::SubsArgs {
range: args.range,
pattern,
replacement: args.replacement,
global: args.global,
};
let outcome = app.buffer.substitute(&resolved);
if outcome.matches == 0 {
cmds.push(Cmd::ToastError(format!("pattern not found: {}", pattern)));
return;
}
cmds.push(Cmd::SetSearch {
pattern: pattern.to_string(),
forward: true,
});
cmds.push(Cmd::ToastInfo(format!(
"{} substitution{} on {} line{}",
outcome.matches,
if outcome.matches == 1 { "" } else { "s" },
outcome.lines_changed,
if outcome.lines_changed == 1 { "" } else { "s" },
)));
}
fn buffer_comment_token(app: &App) -> Option<String> {
let path = app.buffer.path.as_deref()?;
let lang = app.config.languages.by_path(path)?;
lang.comment_token.clone()
}