From 428c5c74fdf22c28b07a08062b44df5a859b005b Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Sat, 18 Feb 2023 19:18:05 +0100 Subject: [PATCH 1/3] Expand selection to whole word when pressing `*` --- helix-term/src/commands.rs | 35 +++++++++++++++++----- helix-term/tests/test/commands.rs | 50 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fb55ca2a8e72..41284aab1d6a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1906,18 +1906,37 @@ fn extend_search_prev(cx: &mut Context) { } fn search_selection(cx: &mut Context) { + let count = cx.count(); let (view, doc) = current!(cx.editor); let contents = doc.text().slice(..); - let regex = doc - .selection(view.id) - .iter() - .map(|selection| regex::escape(&selection.fragment(contents))) - .collect::>() // Collect into hashset to deduplicate identical regexes - .into_iter() - .collect::>() - .join("|"); + let regex; // The fragment to set in the search register + // Checks whether there is only one selection with a width of 1 + let selections = doc.selection(view.id); + let primary = selections.primary(); + if selections.len() == 1 && primary.len() == 1 { + let text = doc.text(); + let text_slice = text.slice(..); + // In this case select the WORD under the cursor + let current_word = textobject::textobject_word( + text_slice, + primary, + textobject::TextObject::Inside, + count, + false, + ); + let text_to_search = current_word.fragment(text_slice).to_string(); + regex = regex::escape(&text_to_search); + } else { + regex = selections + .iter() + .map(|selection| regex::escape(&selection.fragment(contents))) + .collect::>() // Collect into hashset to deduplicate identical regexes + .into_iter() + .collect::>() + .join("|"); + } let msg = format!("register '{}' set to '{}'", '/', ®ex); cx.editor.registers.push('/', regex); cx.editor.set_status(msg); diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index e8d16bfaf2be..c0276cd6f284 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -258,6 +258,56 @@ async fn test_goto_file_impl() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_search_selection() -> anyhow::Result<()> { + // Single selection with a length of 1: search for the whole word + test_key_sequence( + &mut helpers::AppBuilder::new().build()?, + Some("ifoobar::baz3bl*"), // 3b places the cursor on the first letter of 'foobar', then move one to the right for good measure + Some(&|app| { + assert!( + r#"register '/' set to 'foobar'"# == app.editor.get_status().unwrap().0 + && Some(&"foobar".to_string()) == app.editor.registers.first('/') + ); + }), + false, + ) + .await?; + + // Single selection with a length greather than 1: only search for the selection + test_key_sequence( + &mut helpers::AppBuilder::new().build()?, + Some("ifoobar::baz3blvll*"), // 3b places the cursor on the first letter of 'foobar', then move one to the right for good measure, then select two more chars for a total of three + Some(&|app| { + assert!( + r#"register '/' set to 'oob'"# == app.editor.get_status().unwrap().0 + && Some(&"oob".to_string()) == app.editor.registers.first('/') + ); + }), + false, + ) + .await?; + + // Multiple selection of length 1 each : should still only search for the selection + test_key_sequence( + &mut helpers::AppBuilder::new().build()?, + Some("ifoobar::bazbar::cruxk3blC*"), // k3b places the cursor on the first letter of 'foobar', then move one to the right for good measure, then adds a cursor on the line below + Some(&|app| { + assert!( + // The selections don't seem to be ordered, so we have to test for the two possible orders. + (r#"register '/' set to 'o|a'"# == app.editor.get_status().unwrap().0 + || r#"register '/' set to 'a|o'"# == app.editor.get_status().unwrap().0) + && (Some(&"o|a".to_string()) == app.editor.registers.first('/') + || Some(&"a|o".to_string()) == app.editor.registers.first('/')) + ); + }), + false, + ) + .await?; + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_multi_selection_paste() -> anyhow::Result<()> { test(( From b917595e9b6d50157acf2209d8ec687578973d06 Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Sat, 18 Feb 2023 19:41:06 +0100 Subject: [PATCH 2/3] Document the new behavior in keymap.md --- book/src/keymap.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 8864ab69617d..93c8ac78c603 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -143,13 +143,13 @@ Search commands all operate on the `/` register by default. Use `"` to operate on a different one. -| Key | Description | Command | -| ----- | ----------- | ------- | -| `/` | Search for regex pattern | `search` | -| `?` | Search for previous pattern | `rsearch` | -| `n` | Select next search match | `search_next` | -| `N` | Select previous search match | `search_prev` | -| `*` | Use current selection as the search pattern | `search_selection` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `/` | Search for regex pattern | `search` | +| `?` | Search for previous pattern | `rsearch` | +| `n` | Select next search match | `search_next` | +| `N` | Select previous search match | `search_prev` | +| `*` | Use current selection as the search pattern, if only a single char is selected the current word (miW) is used instead | `search_selection` | ### Minor modes From fa552f1c476913fb08fd48f9e2bbe2d327551fcc Mon Sep 17 00:00:00 2001 From: Mathieu Agopian Date: Thu, 9 Mar 2023 11:07:45 +0100 Subject: [PATCH 3/3] Implement feedback from cargo clippy --- helix-term/src/commands.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 41284aab1d6a..beeb7ea71344 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1910,12 +1910,10 @@ fn search_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); let contents = doc.text().slice(..); - let regex; // The fragment to set in the search register - // Checks whether there is only one selection with a width of 1 let selections = doc.selection(view.id); let primary = selections.primary(); - if selections.len() == 1 && primary.len() == 1 { + let regex = if selections.len() == 1 && primary.len() == 1 { let text = doc.text(); let text_slice = text.slice(..); // In this case select the WORD under the cursor @@ -1927,16 +1925,16 @@ fn search_selection(cx: &mut Context) { false, ); let text_to_search = current_word.fragment(text_slice).to_string(); - regex = regex::escape(&text_to_search); + regex::escape(&text_to_search) } else { - regex = selections + selections .iter() .map(|selection| regex::escape(&selection.fragment(contents))) .collect::>() // Collect into hashset to deduplicate identical regexes .into_iter() .collect::>() - .join("|"); - } + .join("|") + }; let msg = format!("register '{}' set to '{}'", '/', ®ex); cx.editor.registers.push('/', regex); cx.editor.set_status(msg);