@@ -314,6 +314,7 @@ impl Command {
314314 join_selections, "Join lines inside selection",
315315 keep_selections, "Keep selections matching regex",
316316 remove_selections, "Remove selections matching regex",
317+ align_selections, "Align selections in column",
317318 keep_primary_selection, "Keep primary selection",
318319 remove_primary_selection, "Remove primary selection",
319320 completion, "Invoke completion popup",
@@ -657,6 +658,76 @@ fn trim_selections(cx: &mut Context) {
657658 };
658659}
659660
661+// align text in selection
662+fn align_selections(cx: &mut Context) {
663+ let align_style = cx.count();
664+ if align_style > 3 {
665+ cx.editor.set_error(
666+ "align only accept 1,2,3 as count to set left/center/right align".to_string(),
667+ );
668+ return;
669+ }
670+
671+ let (view, doc) = current!(cx.editor);
672+ let text = doc.text().slice(..);
673+ let selection = doc.selection(view.id);
674+ let mut column_widths = vec![];
675+ let mut last_line = text.len_lines();
676+ let mut column = 0;
677+ // first of all, we need compute all column's width, let use max width of the selections in a column
678+ for sel in selection {
679+ let (l1, l2) = sel.line_range(text);
680+ if l1 != l2 {
681+ cx.editor
682+ .set_error("align cannot work with multi line selections".to_string());
683+ return;
684+ }
685+ // if the selection is not in the same line with last selection, we set the column to 0
686+ column = if l1 != last_line { 0 } else { column + 1 };
687+ last_line = l1;
688+
689+ if column < column_widths.len() {
690+ if sel.to() - sel.from() > column_widths[column] {
691+ column_widths[column] = sel.to() - sel.from();
692+ }
693+ } else {
694+ // a new column, current selection width is the temp width of the column
695+ column_widths.push(sel.to() - sel.from());
696+ }
697+ }
698+ last_line = text.len_lines();
699+ // once we get the with of each column, we transform each selection with to it's column width based on the align style
700+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
701+ let l = range.cursor_line(text);
702+ column = if l != last_line { 0 } else { column + 1 };
703+ last_line = l;
704+
705+ (
706+ range.from(),
707+ range.to(),
708+ Some(
709+ align_fragment_to_width(&range.fragment(text), column_widths[column], align_style)
710+ .into(),
711+ ),
712+ )
713+ });
714+
715+ doc.apply(&transaction, view.id);
716+ doc.append_changes_to_history(view.id);
717+}
718+
719+fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String {
720+ let trimed = fragment.trim_matches(|c| c == ' ');
721+ let mut s = " ".repeat(width - trimed.chars().count());
722+ match align_style {
723+ 1 => s.insert_str(0, trimed), // left align
724+ 2 => s.insert_str(s.len() / 2, trimed), // center align
725+ 3 => s.push_str(trimed), // right align
726+ n => unimplemented!("{}", n),
727+ }
728+ s
729+}
730+
660731fn goto_window(cx: &mut Context, align: Align) {
661732 let (view, doc) = current!(cx.editor);
662733