diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index a0357b9ded..8a48700adc 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -215,6 +215,8 @@ impl EditorLspTestContext { ("[" @open "]" @close) ("{" @open "}" @close) ("<" @open ">" @close) + ("'" @open "'" @close) + ("`" @open "`" @close) ("\"" @open "\"" @close)"#})), indents: Some(Cow::from(indoc! {r#" [ diff --git a/crates/languages/src/bash/brackets.scm b/crates/languages/src/bash/brackets.scm index 191fd9c084..c61aaa1037 100644 --- a/crates/languages/src/bash/brackets.scm +++ b/crates/languages/src/bash/brackets.scm @@ -1,3 +1,6 @@ ("(" @open ")" @close) ("[" @open "]" @close) ("{" @open "}" @close) +("\"" @open "\"" @close) +("`" @open "`" @close) +((raw_string) @open @close) diff --git a/crates/languages/src/c/brackets.scm b/crates/languages/src/c/brackets.scm index 2fbfd44362..2f886c4240 100644 --- a/crates/languages/src/c/brackets.scm +++ b/crates/languages/src/c/brackets.scm @@ -2,3 +2,4 @@ ("[" @open "]" @close) ("{" @open "}" @close) ("\"" @open "\"" @close) +("'" @open "'" @close) diff --git a/crates/languages/src/cpp/brackets.scm b/crates/languages/src/cpp/brackets.scm index 2fbfd44362..2f886c4240 100644 --- a/crates/languages/src/cpp/brackets.scm +++ b/crates/languages/src/cpp/brackets.scm @@ -2,3 +2,4 @@ ("[" @open "]" @close) ("{" @open "}" @close) ("\"" @open "\"" @close) +("'" @open "'" @close) diff --git a/crates/languages/src/css/brackets.scm b/crates/languages/src/css/brackets.scm index 191fd9c084..2f886c4240 100644 --- a/crates/languages/src/css/brackets.scm +++ b/crates/languages/src/css/brackets.scm @@ -1,3 +1,5 @@ ("(" @open ")" @close) ("[" @open "]" @close) ("{" @open "}" @close) +("\"" @open "\"" @close) +("'" @open "'" @close) diff --git a/crates/languages/src/go/brackets.scm b/crates/languages/src/go/brackets.scm index 2fbfd44362..0ced37682d 100644 --- a/crates/languages/src/go/brackets.scm +++ b/crates/languages/src/go/brackets.scm @@ -2,3 +2,5 @@ ("[" @open "]" @close) ("{" @open "}" @close) ("\"" @open "\"" @close) +("`" @open "`" @close) +((rune_literal) @open @close) diff --git a/crates/languages/src/javascript/brackets.scm b/crates/languages/src/javascript/brackets.scm index 63395f81d8..48afefeef0 100644 --- a/crates/languages/src/javascript/brackets.scm +++ b/crates/languages/src/javascript/brackets.scm @@ -3,3 +3,5 @@ ("{" @open "}" @close) ("<" @open ">" @close) ("\"" @open "\"" @close) +("'" @open "'" @close) +("`" @open "`" @close) diff --git a/crates/languages/src/markdown/brackets.scm b/crates/languages/src/markdown/brackets.scm new file mode 100644 index 0000000000..23f3e4d3d0 --- /dev/null +++ b/crates/languages/src/markdown/brackets.scm @@ -0,0 +1,7 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) +("`" @open "`" @close) +("'" @open "'" @close) +((fenced_code_block_delimiter) @open (fenced_code_block_delimiter) @close) diff --git a/crates/languages/src/python/brackets.scm b/crates/languages/src/python/brackets.scm index 191fd9c084..be68033587 100644 --- a/crates/languages/src/python/brackets.scm +++ b/crates/languages/src/python/brackets.scm @@ -1,3 +1,4 @@ ("(" @open ")" @close) ("[" @open "]" @close) ("{" @open "}" @close) +((string_start) @open (string_end) @close) diff --git a/crates/languages/src/rust/brackets.scm b/crates/languages/src/rust/brackets.scm index eeee5f0e26..0bf19b8085 100644 --- a/crates/languages/src/rust/brackets.scm +++ b/crates/languages/src/rust/brackets.scm @@ -4,3 +4,4 @@ ("<" @open ">" @close) ("\"" @open "\"" @close) (closure_parameters "|" @open "|" @close) +("'" @open "'" @close) diff --git a/crates/languages/src/tsx/brackets.scm b/crates/languages/src/tsx/brackets.scm index 7a02f815c0..66bf14f137 100644 --- a/crates/languages/src/tsx/brackets.scm +++ b/crates/languages/src/tsx/brackets.scm @@ -5,3 +5,5 @@ ("<" @open "/>" @close) ("" @close) ("\"" @open "\"" @close) +("'" @open "'" @close) +("`" @open "`" @close) diff --git a/crates/languages/src/typescript/brackets.scm b/crates/languages/src/typescript/brackets.scm index 63395f81d8..48afefeef0 100644 --- a/crates/languages/src/typescript/brackets.scm +++ b/crates/languages/src/typescript/brackets.scm @@ -3,3 +3,5 @@ ("{" @open "}" @close) ("<" @open ">" @close) ("\"" @open "\"" @close) +("'" @open "'" @close) +("`" @open "`" @close) diff --git a/crates/languages/src/yaml/brackets.scm b/crates/languages/src/yaml/brackets.scm index 9e8c9cd93c..59cf45205f 100644 --- a/crates/languages/src/yaml/brackets.scm +++ b/crates/languages/src/yaml/brackets.scm @@ -1,3 +1,4 @@ ("[" @open "]" @close) ("{" @open "}" @close) ("\"" @open "\"" @close) +("'" @open "'" @close) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 06b40587c4..9218c6dff6 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -8,7 +8,7 @@ use crate::{ use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, - Bias, DisplayPoint, Editor, + Bias, DisplayPoint, Editor, ToOffset, }; use gpui::{actions, impl_actions, Window}; use itertools::Itertools; @@ -64,6 +64,192 @@ struct IndentObj { include_below: bool, } +#[derive(Debug, Clone)] +pub struct CandidateRange { + pub start: DisplayPoint, + pub end: DisplayPoint, +} + +#[derive(Debug, Clone)] +pub struct CandidateWithRanges { + candidate: CandidateRange, + open_range: Range, + close_range: Range, +} + +fn cover_or_next, Range)>>( + candidates: Option, + caret: DisplayPoint, + map: &DisplaySnapshot, + range_filter: Option<&dyn Fn(Range, Range) -> bool>, +) -> Option { + let caret_offset = caret.to_offset(map, Bias::Left); + let mut covering = vec![]; + let mut next_ones = vec![]; + let snapshot = &map.buffer_snapshot; + + if let Some(ranges) = candidates { + for (open_range, close_range) in ranges { + let start_off = open_range.start; + let end_off = close_range.end; + if let Some(range_filter) = range_filter { + if !range_filter(open_range.clone(), close_range.clone()) { + continue; + } + } + let candidate = CandidateWithRanges { + candidate: CandidateRange { + start: start_off.to_display_point(map), + end: end_off.to_display_point(map), + }, + open_range: open_range.clone(), + close_range: close_range.clone(), + }; + + if open_range + .start + .to_offset(snapshot) + .to_display_point(map) + .row() + == caret_offset.to_display_point(map).row() + { + if start_off <= caret_offset && caret_offset < end_off { + covering.push(candidate); + } else if start_off >= caret_offset { + next_ones.push(candidate); + } + } + } + } + + // 1) covering -> smallest width + if !covering.is_empty() { + return covering.into_iter().min_by_key(|r| { + r.candidate.end.to_offset(map, Bias::Right) + - r.candidate.start.to_offset(map, Bias::Left) + }); + } + + // 2) next -> closest by start + if !next_ones.is_empty() { + return next_ones.into_iter().min_by_key(|r| { + let start = r.candidate.start.to_offset(map, Bias::Left); + (start as isize - caret_offset as isize).abs() + }); + } + + None +} + +type DelimiterPredicate = dyn Fn(&BufferSnapshot, usize, usize) -> bool; + +struct DelimiterRange { + open: Range, + close: Range, +} + +impl DelimiterRange { + fn to_display_range(&self, map: &DisplaySnapshot, around: bool) -> Range { + if around { + self.open.start.to_display_point(map)..self.close.end.to_display_point(map) + } else { + self.open.end.to_display_point(map)..self.close.start.to_display_point(map) + } + } +} + +fn find_any_delimiters( + map: &DisplaySnapshot, + display_point: DisplayPoint, + around: bool, + is_valid_delimiter: &DelimiterPredicate, +) -> Option> { + let point = map.clip_at_line_end(display_point).to_point(map); + let offset = point.to_offset(&map.buffer_snapshot); + + let line_range = get_line_range(map, point); + let visible_line_range = get_visible_line_range(&line_range); + + let snapshot = &map.buffer_snapshot; + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer = excerpt.buffer(); + + let bracket_filter = |open: Range, close: Range| { + is_valid_delimiter(buffer, open.start, close.start) + }; + + // Try to find delimiters in visible range first + let ranges = map + .buffer_snapshot + .bracket_ranges(visible_line_range.clone()); + if let Some(candidate) = cover_or_next(ranges, display_point, map, Some(&bracket_filter)) { + return Some( + DelimiterRange { + open: candidate.open_range, + close: candidate.close_range, + } + .to_display_range(map, around), + ); + } + + // Fall back to innermost enclosing brackets + let (open_bracket, close_bracket) = + buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?; + + Some( + DelimiterRange { + open: open_bracket, + close: close_bracket, + } + .to_display_range(map, around), + ) +} + +fn get_line_range(map: &DisplaySnapshot, point: Point) -> Range { + let (start, mut end) = ( + map.prev_line_boundary(point).0, + map.next_line_boundary(point).0, + ); + + if end == point { + end = map.max_point().to_point(map); + } + + start..end +} + +fn get_visible_line_range(line_range: &Range) -> Range { + let end_column = line_range.end.column.saturating_sub(1); + line_range.start..Point::new(line_range.end.row, end_column) +} + +fn is_quote_delimiter(buffer: &BufferSnapshot, _start: usize, end: usize) -> bool { + matches!(buffer.chars_at(end).next(), Some('\'' | '"' | '`')) +} + +fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> bool { + matches!( + buffer.chars_at(start).next(), + Some('(' | '[' | '{' | '<' | '|') + ) +} + +fn find_any_quotes( + map: &DisplaySnapshot, + display_point: DisplayPoint, + around: bool, +) -> Option> { + find_any_delimiters(map, display_point, around, &is_quote_delimiter) +} + +fn find_any_brackets( + map: &DisplaySnapshot, + display_point: DisplayPoint, + around: bool, +) -> Option> { + find_any_delimiters(map, display_point, around, &is_bracket_delimiter) +} + impl_actions!(vim, [Word, Subword, IndentObj]); actions!( @@ -304,26 +490,7 @@ impl Object { Object::BackQuotes => { surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`') } - Object::AnyQuotes => { - let quote_types = ['\'', '"', '`']; // Types of quotes to handle - let relative_offset = relative_to.to_offset(map, Bias::Left) as isize; - - // Find the closest matching quote range - quote_types - .iter() - .flat_map(|"e| { - // Get ranges for each quote type - surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - quote, - quote, - ) - }) - .min_by_key(|range| calculate_range_distance(range, relative_offset, map)) - } + Object::AnyQuotes => find_any_quotes(map, relative_to, around), Object::DoubleQuotes => { surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') } @@ -338,24 +505,7 @@ impl Object { let range = selection.range(); surrounding_html_tag(map, head, range, around) } - Object::AnyBrackets => { - let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; - let relative_offset = relative_to.to_offset(map, Bias::Left) as isize; - - bracket_pairs - .iter() - .flat_map(|&(open_bracket, close_bracket)| { - surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - open_bracket, - close_bracket, - ) - }) - .min_by_key(|range| calculate_range_distance(range, relative_offset, map)) - } + Object::AnyBrackets => find_any_brackets(map, relative_to, around), Object::SquareBrackets => { surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') } @@ -657,37 +807,6 @@ fn around_word( } } -/// Calculate distance between a range and a cursor position -/// -/// Returns a score where: -/// - Lower values indicate better matches -/// - Range containing cursor gets priority (returns range length) -/// - For non-containing ranges, uses minimum distance to boundaries as primary factor -/// - Range length is used as secondary factor for tiebreaking -fn calculate_range_distance( - range: &Range, - cursor_offset: isize, - map: &DisplaySnapshot, -) -> isize { - let start_offset = range.start.to_offset(map, Bias::Left) as isize; - let end_offset = range.end.to_offset(map, Bias::Right) as isize; - let range_length = end_offset - start_offset; - - // If cursor is inside the range, return range length - if cursor_offset >= start_offset && cursor_offset <= end_offset { - return range_length; - } - - // Calculate minimum distance to range boundaries - let start_distance = (cursor_offset - start_offset).abs(); - let end_distance = (cursor_offset - end_offset).abs(); - let min_distance = start_distance.min(end_distance); - - // Use min_distance as primary factor, range_length as secondary - // Multiply by large number to ensure distance is primary factor - min_distance * 10000 + range_length -} - fn around_subword( map: &DisplaySnapshot, relative_to: DisplayPoint, @@ -2098,9 +2217,36 @@ mod test { #[gpui::test] async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; + let mut cx = VimTestContext::new_typescript(cx).await; const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ + // Special cases from mini.ai plugin + // the false string in the middle should not be considered + ( + "c i q", + "'first' false ˇstring 'second'", + "'first' false string 'ˇ'", + Mode::Insert, + ), + // Multiline support :)! Same behavior as mini.ai plugin + ( + "c i q", + indoc! {" + ` + first + middle ˇstring + second + ` + "}, + indoc! {" + `ˇ` + "}, + Mode::Insert, + ), + // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote + // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in AnyQuotes :) + // Bug reference: https://github.com/zed-industries/zed/issues/23889 + ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert), // Single quotes ( "c i q", @@ -2111,19 +2257,19 @@ mod test { ( "c a q", "Thisˇ is a 'quote' example.", - "This is a ˇexample.", + "This is a ˇ example.", // same mini.ai plugin behavior Mode::Insert, ), ( "c i q", "This is a \"simple 'qˇuote'\" example.", - "This is a \"simple 'ˇ'\" example.", + "This is a \"ˇ\" example.", // Not supported by tree sitter queries for now Mode::Insert, ), ( "c a q", "This is a \"simple 'qˇuote'\" example.", - "This is a \"simpleˇ\" example.", + "This is a ˇ example.", // Not supported by tree sitter queries for now Mode::Insert, ), ( @@ -2135,7 +2281,7 @@ mod test { ( "c a q", "This is a 'qˇuote' example.", - "This is a ˇexample.", + "This is a ˇ example.", // same mini.ai plugin behavior Mode::Insert, ), ( @@ -2147,7 +2293,7 @@ mod test { ( "d a q", "This is a 'qˇuote' example.", - "This is a ˇexample.", + "This is a ˇ example.", // same mini.ai plugin behavior Mode::Normal, ), // Double quotes @@ -2160,7 +2306,7 @@ mod test { ( "c a q", "This is a \"qˇuote\" example.", - "This is a ˇexample.", + "This is a ˇ example.", // same mini.ai plugin behavior Mode::Insert, ), ( @@ -2172,7 +2318,7 @@ mod test { ( "d a q", "This is a \"qˇuote\" example.", - "This is a ˇexample.", + "This is a ˇ example.", // same mini.ai plugin behavior Mode::Normal, ), // Back quotes @@ -2185,7 +2331,7 @@ mod test { ( "c a q", "This is a `qˇuote` example.", - "This is a ˇexample.", + "This is a ˇ example.", // same mini.ai plugin behavior Mode::Insert, ), ( @@ -2197,7 +2343,7 @@ mod test { ( "d a q", "This is a `qˇuote` example.", - "This is a ˇexample.", + "This is a ˇ example.", // same mini.ai plugin behavior Mode::Normal, ), ]; @@ -2246,6 +2392,76 @@ mod test { }); const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ + // Special cases from mini.ai plugin + // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying + // Same behavior as mini.ai plugin + ( + "c i b", + indoc! {" + { + { + ˇprint('hello') + } + } + "}, + indoc! {" + { + { + print(ˇ) + } + } + "}, + Mode::Insert, + ), + // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket + // Same behavior as mini.ai plugin + ( + "c i b", + indoc! {" + { + { + ˇ + print('hello') + } + } + "}, + indoc! {" + { + {ˇ} + } + "}, + Mode::Insert, + ), + // If you are in the open bracket then it has higher priority + ( + "c i b", + indoc! {" + «{ˇ» + { + print('hello') + } + } + "}, + indoc! {" + {ˇ} + "}, + Mode::Insert, + ), + // If you are in the close bracket then it has higher priority + ( + "c i b", + indoc! {" + { + { + print('hello') + } + «}ˇ» + "}, + indoc! {" + {ˇ} + "}, + Mode::Insert, + ), // Bracket (Parentheses) ( "c i b",