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: &[],