From fdce4d87c93b2af7962cafe1942f5c2183258f55 Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Sun, 14 Nov 2021 21:56:07 +0800 Subject: [PATCH 1/8] align lines --- helix-term/src/commands.rs | 80 ++++++++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 1 + 2 files changed, 81 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d5a48c5f74f7..2bf2fec10db1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -301,6 +301,7 @@ impl Command { join_selections, "Join lines inside selection", keep_selections, "Keep selections matching regex", remove_selections, "Remove selections matching regex", + align_lines, "Align lines by matching regex", keep_primary_selection, "Keep primary selection", remove_primary_selection, "Remove primary selection", completion, "Invoke completion popup", @@ -3865,6 +3866,85 @@ fn redo(cx: &mut Context) { } } +// align text in selection +fn align_lines(cx: &mut Context) { + let align_style = cx.count(); + let reg = cx.register.unwrap_or('/'); + let prompt = ui::regex_prompt( + cx, + "align:".into(), + Some(reg), + |_input: &str| Vec::new(), + move |view, doc, regex, event| { + if event != PromptEvent::Update { + return; + } + let text = doc.text().slice(..); + let selection = doc.selection(view.id); + let start_line = text.char_to_line(selection.ranges()[0].from()); + let end_line = text.char_to_line(selection.ranges()[selection.len() - 1].to()); + if start_line == end_line { + return; + } + let mut column_widths = vec![]; + let mut fields = vec![vec![]; end_line - start_line + 1]; + for l in start_line..end_line + 1 { + let line_text = text.line(l).to_string(); + let line_start = text.line_to_byte(l); + let line_fields = &mut fields[l - start_line]; + let mut start = line_start; + for mat in regex.find_iter(line_text.as_str()) { + line_fields.push((start, line_start + mat.start())); + let w = line_start + mat.start() - start; + start = line_start + mat.end() + 1; + + if column_widths.len() < line_fields.len() { + column_widths.push(w); + } else if w > column_widths[line_fields.len() - 1] { + column_widths[line_fields.len() - 1] = w; + } + } + } + log::error!("align contents\n {:?}\n {:?}", &column_widths, &fields); + let transaction = Transaction::change( + doc.text(), + fields.iter().flat_map(|line| { + line.into_iter().enumerate().map(|(i, &(from, to))| { + ( + from, + to, + Some( + align_fragment_to_width( + text.slice(from..to).to_string().as_str(), + column_widths[i], + align_style, + ) + .into(), + ), + ) + }) + }), + ); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); + }, + ); + + cx.push_layer(Box::new(prompt)); +} + +fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String { + let trimed = fragment.trim(); + let mut s = " ".repeat(width - trimed.chars().count()); + match align_style { + 2 => s.insert_str(s.len() / 2, trimed), // center align + 3 => s.push_str(trimed), // right align + _ => s.insert_str(0, trimed), // left align + } + s +} + // Yank / Paste fn yank(cx: &mut Context) { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index e3e019957144..d3161feb17bd 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -601,6 +601,7 @@ impl Default for Keymaps { // "q" => record_macro, // "Q" => replay_macro, + "&" => align_lines, // & align selections // _ trim selections From 00f611fe81b965638cc8c81f12b5a59d3b0ad62d Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Sun, 14 Nov 2021 22:22:32 +0800 Subject: [PATCH 2/8] remove log statement --- helix-term/src/commands.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2bf2fec10db1..814a366ef834 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3905,7 +3905,6 @@ fn align_lines(cx: &mut Context) { } } } - log::error!("align contents\n {:?}\n {:?}", &column_widths, &fields); let transaction = Transaction::change( doc.text(), fields.iter().flat_map(|line| { From bb592bd2ebab324960a801ed474d8c07669e629f Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Mon, 15 Nov 2021 11:07:10 +0800 Subject: [PATCH 3/8] use selections to align --- book/src/keymap.md | 1 + helix-term/src/commands.rs | 138 ++++++++++++++++--------------------- helix-term/src/keymap.rs | 3 +- 3 files changed, 61 insertions(+), 81 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 88610a77947a..969ff59b898e 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -90,6 +90,7 @@ | `s` | Select all regex matches inside selections | `select_regex` | | `S` | Split selection into subselections on regex matches | `split_selection` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` | +| `&` | Align selection in columns | `align_selections` | | `_` | Trim whitespace from the selection | `trim_selections` | | `;` | Collapse selection onto a single cursor | `collapse_selection` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fa6d4c829ff8..994b6c8c07da 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -308,7 +308,7 @@ impl Command { join_selections, "Join lines inside selection", keep_selections, "Keep selections matching regex", remove_selections, "Remove selections matching regex", - align_lines, "Align lines by matching regex", + align_selections, "Align selections in column", keep_primary_selection, "Keep primary selection", remove_primary_selection, "Remove primary selection", completion, "Invoke completion popup", @@ -626,6 +626,64 @@ fn trim_selections(cx: &mut Context) { }; } +// align text in selection +fn align_selections(cx: &mut Context) { + let align_style = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let selection = doc.selection(view.id); + let mut column_widths = vec![]; + let mut last_line = text.len_lines(); + let mut column = 0; + for sel in selection { + let (l1, l2) = sel.line_range(text); + if l1 != l2 { + cx.editor + .set_error(format!("align cannot work with multi line selections")); + return; + } + column = if l1 != last_line { 0 } else { column + 1 }; + last_line = l1; + + if column < column_widths.len() { + if sel.to() - sel.from() > column_widths[column] { + column_widths[column] = sel.to() - sel.from(); + } + } else { + column_widths.push(sel.to() - sel.from()); + } + } + last_line = text.len_lines(); + let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { + let l = range.cursor_line(text); + column = if l != last_line { 0 } else { column + 1 }; + last_line = l; + + ( + range.from(), + range.to(), + Some( + align_fragment_to_width(&range.fragment(text), column_widths[column], align_style) + .into(), + ), + ) + }); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); +} + +fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String { + let trimed = fragment.trim_matches(|c| c == ' '); + let mut s = " ".repeat(width - trimed.chars().count()); + match align_style { + 2 => s.insert_str(s.len() / 2, trimed), // center align + 3 => s.push_str(trimed), // right align + _ => s.insert_str(0, trimed), // left align + } + s +} + fn goto_window(cx: &mut Context, align: Align) { let (view, doc) = current!(cx.editor); @@ -4007,84 +4065,6 @@ fn later(cx: &mut Context) { } } -// align text in selection -fn align_lines(cx: &mut Context) { - let align_style = cx.count(); - let reg = cx.register.unwrap_or('/'); - let prompt = ui::regex_prompt( - cx, - "align:".into(), - Some(reg), - |_input: &str| Vec::new(), - move |view, doc, regex, event| { - if event != PromptEvent::Update { - return; - } - let text = doc.text().slice(..); - let selection = doc.selection(view.id); - let start_line = text.char_to_line(selection.ranges()[0].from()); - let end_line = text.char_to_line(selection.ranges()[selection.len() - 1].to()); - if start_line == end_line { - return; - } - let mut column_widths = vec![]; - let mut fields = vec![vec![]; end_line - start_line + 1]; - for l in start_line..end_line + 1 { - let line_text = text.line(l).to_string(); - let line_start = text.line_to_byte(l); - let line_fields = &mut fields[l - start_line]; - let mut start = line_start; - for mat in regex.find_iter(line_text.as_str()) { - line_fields.push((start, line_start + mat.start())); - let w = line_start + mat.start() - start; - start = line_start + mat.end() + 1; - - if column_widths.len() < line_fields.len() { - column_widths.push(w); - } else if w > column_widths[line_fields.len() - 1] { - column_widths[line_fields.len() - 1] = w; - } - } - } - let transaction = Transaction::change( - doc.text(), - fields.iter().flat_map(|line| { - line.into_iter().enumerate().map(|(i, &(from, to))| { - ( - from, - to, - Some( - align_fragment_to_width( - text.slice(from..to).to_string().as_str(), - column_widths[i], - align_style, - ) - .into(), - ), - ) - }) - }), - ); - - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); - }, - ); - - cx.push_layer(Box::new(prompt)); -} - -fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String { - let trimed = fragment.trim(); - let mut s = " ".repeat(width - trimed.chars().count()); - match align_style { - 2 => s.insert_str(s.len() / 2, trimed), // center align - 3 => s.push_str(trimed), // right align - _ => s.insert_str(0, trimed), // left align - } - s -} - // Yank / Paste fn yank(cx: &mut Context) { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 25f767c8e3c6..fa7210d224bd 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -604,8 +604,7 @@ impl Default for Keymaps { // "q" => record_macro, // "Q" => replay_macro, - "&" => align_lines, - // & align selections + "&" => align_selections, "_" => trim_selections, "(" => rotate_selections_backward, From ca2bee84b062d43958a418771ceaf240dcf24872 Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Mon, 15 Nov 2021 11:53:41 +0800 Subject: [PATCH 4/8] fix a clippy issue --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 994b6c8c07da..200a3c816d15 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -639,7 +639,7 @@ fn align_selections(cx: &mut Context) { let (l1, l2) = sel.line_range(text); if l1 != l2 { cx.editor - .set_error(format!("align cannot work with multi line selections")); + .set_error("align cannot work with multi line selections".to_string()); return; } column = if l1 != last_line { 0 } else { column + 1 }; From 1efa21099049f1548ddc5d585826ecb33e2ac8e2 Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Fri, 19 Nov 2021 13:28:04 +0800 Subject: [PATCH 5/8] only accept 1,2,3 as user count --- helix-term/src/commands.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 200a3c816d15..fea6c9cfcba8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -629,6 +629,12 @@ fn trim_selections(cx: &mut Context) { // align text in selection fn align_selections(cx: &mut Context) { let align_style = cx.count(); + if align_style > 3 { + cx.editor.set_error( + "align only accept 1,2,3 as count to set left/center/right align".to_string(), + ); + } + let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); let selection = doc.selection(view.id); From bac093c8135f4cf474c5261b1622d6693494ed84 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 19 Nov 2021 16:15:00 +0800 Subject: [PATCH 6/8] Update helix-term/src/commands.rs Co-authored-by: Ivan Tham --- helix-term/src/commands.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fea6c9cfcba8..20913319f7f6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -685,7 +685,8 @@ fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> match align_style { 2 => s.insert_str(s.len() / 2, trimed), // center align 3 => s.push_str(trimed), // right align - _ => s.insert_str(0, trimed), // left align + 1 => s.insert_str(0, trimed), // left align + n => unimplemented!(n), } s } From 09c581595d1d4a9e54cd2b0fb179a6797997d590 Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Sat, 20 Nov 2021 09:14:41 +0800 Subject: [PATCH 7/8] return if user count is not correct --- helix-term/src/commands.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 20913319f7f6..d62df524fff2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -633,6 +633,7 @@ fn align_selections(cx: &mut Context) { cx.editor.set_error( "align only accept 1,2,3 as count to set left/center/right align".to_string(), ); + return; } let (view, doc) = current!(cx.editor); @@ -686,7 +687,7 @@ fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> 2 => s.insert_str(s.len() / 2, trimed), // center align 3 => s.push_str(trimed), // right align 1 => s.insert_str(0, trimed), // left align - n => unimplemented!(n), + n => unimplemented!("{}", n), } s } From 564a49bc3372d826d5620924871e77068cc8ed08 Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Sat, 20 Nov 2021 15:45:00 +0800 Subject: [PATCH 8/8] add doc --- helix-term/src/commands.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d62df524fff2..a19b739e4fb2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -642,6 +642,7 @@ fn align_selections(cx: &mut Context) { let mut column_widths = vec![]; let mut last_line = text.len_lines(); let mut column = 0; + // first of all, we need compute all column's width, let use max width of the selections in a column for sel in selection { let (l1, l2) = sel.line_range(text); if l1 != l2 { @@ -649,6 +650,7 @@ fn align_selections(cx: &mut Context) { .set_error("align cannot work with multi line selections".to_string()); return; } + // if the selection is not in the same line with last selection, we set the column to 0 column = if l1 != last_line { 0 } else { column + 1 }; last_line = l1; @@ -657,10 +659,12 @@ fn align_selections(cx: &mut Context) { column_widths[column] = sel.to() - sel.from(); } } else { + // a new column, current selection width is the temp width of the column column_widths.push(sel.to() - sel.from()); } } last_line = text.len_lines(); + // once we get the with of each column, we transform each selection with to it's column width based on the align style let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { let l = range.cursor_line(text); column = if l != last_line { 0 } else { column + 1 }; @@ -684,9 +688,9 @@ fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> let trimed = fragment.trim_matches(|c| c == ' '); let mut s = " ".repeat(width - trimed.chars().count()); match align_style { + 1 => s.insert_str(0, trimed), // left align 2 => s.insert_str(s.len() / 2, trimed), // center align 3 => s.push_str(trimed), // right align - 1 => s.insert_str(0, trimed), // left align n => unimplemented!("{}", n), } s