diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 219f6b95f..52f7de3f6 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -70,6 +70,7 @@ | `:toggle-option`, `:toggle` | Toggle a config option at runtime.
For example to toggle smart case search, use `:toggle search.smart-case`. | | `:get-option`, `:get` | Get the current value of a config option. | | `:sort` | Sort ranges in selection. | +| `:index` | Inserts indexes into selections. | | `:reflow` | Hard-wrap the current selection of lines to a given width. | | `:tree-sitter-subtree`, `:ts-subtree` | Display the smallest tree-sitter subtree that spans the primary selection, primarily for debugging queries. | | `:config-reload` | Refresh user config. | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 2013a9d81..2421652cf 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -9,7 +9,7 @@ use super::*; use helix_core::command_line::{Args, Flag, Signature, Token, TokenKind}; use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; -use helix_core::line_ending; +use helix_core::{line_ending, SmartString}; use helix_stdx::path::home_dir; use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; use helix_view::editor::{CloseError, ConfigEvent}; @@ -2146,6 +2146,115 @@ fn sort(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow: Ok(()) } +fn index(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let step = if let Some(arg) = args.first() { + arg.parse() + .ok() + .filter(|&step| step != 0) + .context("Step must be a positive integer greater than zero")? + } else { + 1 + }; + + let start = if let Some(arg) = args.get_flag("start") { + arg.parse().context("Argument to --start must be an integer")? + } else { + 1 + }; + + index_impl( + cx, + args.has_flag("reverse"), + args.has_flag("desc"), + args.has_flag("pad"), + start, + step, + ) +} + +fn index_impl( + cx: &mut compositor::Context, + reverse: bool, + desc: bool, + pad: bool, + start: isize, + step: usize, +) -> anyhow::Result<()> { + let scrolloff = cx.editor.config().scrolloff; + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id); + + if selection.len() == 1 { + bail!("Sorting requires multiple selections. Hint: split selection first"); + } + + let mut fragments: Vec<_> = selection + .slices(text) + .map(|fragment| fragment.chunks().collect()) + .collect(); + + let count_selections = fragments.len(); + + let mut iter: Vec = if desc { + let start_from = start - ((count_selections - 1) * step) as isize; + (start_from..=start) + .rev() + .step_by(step) + .take(count_selections) + .collect() + } else { + (start..).step_by(step).take(count_selections).collect() + }; + + if reverse { + iter.reverse(); + } + + fragments.iter_mut().zip(&iter).for_each(|(frag, index)| { + let index_str = if pad { + let width = iter + .iter() + .map(|&num| { + if num == 0 { + return 1; + } + let width = num.abs().ilog10() as usize + 1; + if num > 0 { + width + } else { + width + 1 + } + }) + .max() + .unwrap(); // we already checked that we have multiple selections + format!("{:0width$}", index, width = width) + } else { + index.to_string() + }; + *frag = SmartString::from(index_str); + }); + + let transaction = Transaction::change( + doc.text(), + selection + .into_iter() + .zip(fragments) + .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))), + ); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view); + view.ensure_cursor_in_view(doc, scrolloff); + + Ok(()) +} + fn reflow(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); @@ -3373,6 +3482,43 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ ..Signature::DEFAULT }, }, + TypableCommand { + name: "index", + aliases: &["i"], + doc: "Inserts indexes into selections.", + fun: index, + completer: CommandCompleter::none(), + signature: Signature { + positionals: (0, Some(1)), + flags: &[ + Flag { + name: "start", + alias: Some('x'), + doc: "Set the starting number to count from", + completions: Some(&[]) + }, + Flag { + name: "reverse", + alias: Some('r'), + doc: "Index in reverse order", + ..Flag::DEFAULT + }, + Flag { + name: "desc", + alias: Some('d'), + doc: "Index in descending order", + ..Flag::DEFAULT + }, + Flag { + name: "pad", + alias: Some('p'), + doc: "Add leading zeros to start", + ..Flag::DEFAULT + }, + ], + ..Signature::DEFAULT + }, + }, TypableCommand { name: "reflow", aliases: &[],