on master help sign in
feat(editor): add config for search wrap_around (#1516)
fdb9a16Matouš Dzivjak
helix-term/src/commands.rs+35−13
@@ -1459,6 +1459,7 @@ fn split_selection_on_newline(cx: &mut Context) {
1459 doc.set_selection(view.id, selection);
1459 doc.set_selection(view.id, selection);
1460}
1460}
1461
1461
1462+#[allow(clippy::too_many_arguments)]
1462fn search_impl(
1463fn search_impl(
1463 doc: &mut Document,
1464 doc: &mut Document,
1464 view: &mut View,
1465 view: &mut View,
@@ -1467,6 +1468,7 @@ fn search_impl(
1467 movement: Movement,
1468 movement: Movement,
1468 direction: Direction,
1469 direction: Direction,
1469 scrolloff: usize,
1470 scrolloff: usize,
1471+ wrap_around: bool,
1470) {
1472) {
1471 let text = doc.text().slice(..);
1473 let text = doc.text().slice(..);
1472 let selection = doc.selection(view.id);
1474 let selection = doc.selection(view.id);
@@ -1492,16 +1494,22 @@ fn search_impl(
1492
1494
1493 // use find_at to find the next match after the cursor, loop around the end
1495 // use find_at to find the next match after the cursor, loop around the end
1494 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1496 // Careful, `Regex` uses `bytes` as offsets, not character indices!
1495− let mat = match direction {
1497+ let mut mat = match direction {
1496− Direction::Forward => regex
1498+ Direction::Forward => regex.find_at(contents, start),
1497− .find_at(contents, start)
1498− .or_else(|| regex.find(contents)),
1499− Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| {
1499+ Direction::Backward => regex.find_iter(&contents[..start]).last(),
1500− offset = start;
1501− regex.find_iter(&contents[start..]).last()
1502− }),
1503 };
1500 };
1501+
1502+ if wrap_around && mat.is_none() {
1503+ mat = match direction {
1504+ Direction::Forward => regex.find(contents),
1505+ Direction::Backward => {
1506+ offset = start;
1507+ regex.find_iter(&contents[start..]).last()
1508+ }
1509+ }
1504− // TODO: message on wraparound
1510+ // TODO: message on wraparound
1511+ }
1512+
1505 if let Some(mat) = mat {
1513 if let Some(mat) = mat {
1506 let start = text.byte_to_char(mat.start() + offset);
1514 let start = text.byte_to_char(mat.start() + offset);
1507 let end = text.byte_to_char(mat.end() + offset);
1515 let end = text.byte_to_char(mat.end() + offset);
@@ -1554,6 +1562,7 @@ fn rsearch(cx: &mut Context) {
1554fn searcher(cx: &mut Context, direction: Direction) {
1562fn searcher(cx: &mut Context, direction: Direction) {
1555 let reg = cx.register.unwrap_or('/');
1563 let reg = cx.register.unwrap_or('/');
1556 let scrolloff = cx.editor.config.scrolloff;
1564 let scrolloff = cx.editor.config.scrolloff;
1565+ let wrap_around = cx.editor.config.search.wrap_around;
1557
1566
1558 let doc = doc!(cx.editor);
1567 let doc = doc!(cx.editor);
1559
1568
@@ -1587,6 +1596,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
1587 Movement::Move,
1596 Movement::Move,
1588 direction,
1597 direction,
1589 scrolloff,
1598 scrolloff,
1599+ wrap_around,
1590 );
1600 );
1591 },
1601 },
1592 );
1602 );
@@ -1601,16 +1611,27 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
1601 if let Some(query) = registers.read('/') {
1611 if let Some(query) = registers.read('/') {
1602 let query = query.last().unwrap();
1612 let query = query.last().unwrap();
1603 let contents = doc.text().slice(..).to_string();
1613 let contents = doc.text().slice(..).to_string();
1604− let case_insensitive = if cx.editor.config.smart_case {
1614+ let search_config = &cx.editor.config.search;
1615+ let case_insensitive = if search_config.smart_case {
1605 !query.chars().any(char::is_uppercase)
1616 !query.chars().any(char::is_uppercase)
1606 } else {
1617 } else {
1607 false
1618 false
1608 };
1619 };
1620+ let wrap_around = search_config.wrap_around;
1609 if let Ok(regex) = RegexBuilder::new(query)
1621 if let Ok(regex) = RegexBuilder::new(query)
1610 .case_insensitive(case_insensitive)
1622 .case_insensitive(case_insensitive)
1611 .build()
1623 .build()
1612 {
1624 {
1613− search_impl(doc, view, &contents, &regex, movement, direction, scrolloff);
1625+ search_impl(
1626+ doc,
1627+ view,
1628+ &contents,
1629+ &regex,
1630+ movement,
1631+ direction,
1632+ scrolloff,
1633+ wrap_around,
1634+ );
1614 } else {
1635 } else {
1615 // get around warning `mutable_borrow_reservation_conflict`
1636 // get around warning `mutable_borrow_reservation_conflict`
1616 // which will be a hard error in the future
1637 // which will be a hard error in the future
@@ -1649,7 +1670,7 @@ fn search_selection(cx: &mut Context) {
1649fn global_search(cx: &mut Context) {
1670fn global_search(cx: &mut Context) {
1650 let (all_matches_sx, all_matches_rx) =
1671 let (all_matches_sx, all_matches_rx) =
1651 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1672 tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
1652− let smart_case = cx.editor.config.smart_case;
1673+ let smart_case = cx.editor.config.search.smart_case;
1653 let file_picker_config = cx.editor.config.file_picker.clone();
1674 let file_picker_config = cx.editor.config.file_picker.clone();
1654
1675
1655 let completions = search_completions(cx, None);
1676 let completions = search_completions(cx, None);
@@ -2707,12 +2728,13 @@ pub mod cmd {
2707 "mouse" => runtime_config.mouse = arg.parse()?,
2728 "mouse" => runtime_config.mouse = arg.parse()?,
2708 "line-number" => runtime_config.line_number = arg.parse()?,
2729 "line-number" => runtime_config.line_number = arg.parse()?,
2709 "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
2730 "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
2710− "smart-case" => runtime_config.smart_case = arg.parse()?,
2711 "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
2731 "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
2712 "auto-completion" => runtime_config.auto_completion = arg.parse()?,
2732 "auto-completion" => runtime_config.auto_completion = arg.parse()?,
2713 "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
2733 "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
2714 "auto-info" => runtime_config.auto_info = arg.parse()?,
2734 "auto-info" => runtime_config.auto_info = arg.parse()?,
2715 "true-color" => runtime_config.true_color = arg.parse()?,
2735 "true-color" => runtime_config.true_color = arg.parse()?,
2736+ "search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
2737+ "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
2716 _ => anyhow::bail!("Unknown key `{}`.", args[0]),
2738 _ => anyhow::bail!("Unknown key `{}`.", args[0]),
2717 }
2739 }
2718
2740
helix-term/src/ui/mod.rs+1−1
@@ -65,7 +65,7 @@ pub fn regex_prompt(
65 return;
65 return;
66 }
66 }
67
67
68− let case_insensitive = if cx.editor.config.smart_case {
68+ let case_insensitive = if cx.editor.config.search.smart_case {
69 !input.chars().any(char::is_uppercase)
69 !input.chars().any(char::is_uppercase)
70 } else {
70 } else {
71 false
71 false
helix-view/src/editor.rs+22−3
@@ -93,8 +93,6 @@ pub struct Config {
93 pub line_number: LineNumber,
93 pub line_number: LineNumber,
94 /// Middle click paste support. Defaults to true.
94 /// Middle click paste support. Defaults to true.
95 pub middle_click_paste: bool,
95 pub middle_click_paste: bool,
96− /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
97− pub smart_case: bool,
98 /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
96 /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
99 pub auto_pairs: bool,
97 pub auto_pairs: bool,
100 /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
98 /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
@@ -110,6 +108,18 @@ pub struct Config {
110 pub cursor_shape: CursorShapeConfig,
108 pub cursor_shape: CursorShapeConfig,
111 /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
109 /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
112 pub true_color: bool,
110 pub true_color: bool,
111+ /// Search configuration.
112+ #[serde(default)]
113+ pub search: SearchConfig,
114+}
115+
116+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
118+pub struct SearchConfig {
119+ /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
120+ pub smart_case: bool,
121+ /// Whether the search should wrap after depleting the matches. Default to true.
122+ pub wrap_around: bool,
113}
123}
114
124
115// Cursor shape is read and used on every rendered frame and so needs
125// Cursor shape is read and used on every rendered frame and so needs
@@ -202,7 +212,6 @@ impl Default for Config {
202 },
212 },
203 line_number: LineNumber::Absolute,
213 line_number: LineNumber::Absolute,
204 middle_click_paste: true,
214 middle_click_paste: true,
205− smart_case: true,
206 auto_pairs: true,
215 auto_pairs: true,
207 auto_completion: true,
216 auto_completion: true,
208 idle_timeout: Duration::from_millis(400),
217 idle_timeout: Duration::from_millis(400),
@@ -211,6 +220,16 @@ impl Default for Config {
211 file_picker: FilePickerConfig::default(),
220 file_picker: FilePickerConfig::default(),
212 cursor_shape: CursorShapeConfig::default(),
221 cursor_shape: CursorShapeConfig::default(),
213 true_color: false,
222 true_color: false,
223+ search: SearchConfig::default(),
224+ }
225+ }
226+}
227+
228+impl Default for SearchConfig {
229+ fn default() -> Self {
230+ Self {
231+ wrap_around: true,
232+ smart_case: true,
214 }
233 }
215 }
234 }
216}
235}