diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 8c8dde521a34ac..538ec3d3b7a829 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1369,12 +1369,6 @@ impl Sub for DisplayRow { } impl DisplayPoint { - pub fn offset_plus(&self, map: &DisplaySnapshot, count: usize) -> DisplayPoint { - let line_len = map.line_len(self.row()) as usize; - let new_col = (self.column() as usize + count).min(line_len); - DisplayPoint::new(self.row(), new_col as u32) - } - pub fn new(row: DisplayRow, column: u32) -> Self { Self(BlockPoint(Point::new(row.0, column))) } diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index a0357b9dedb09f..8a48700adc354a 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/typescript/brackets.scm b/crates/languages/src/typescript/brackets.scm index 63395f81d84e64..48afefeef07e99 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/vim/src/object.rs b/crates/vim/src/object.rs index 0bc384c3f251a4..eaf3dffd9402d1 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -6,9 +6,9 @@ use crate::{ Vim, }; use editor::{ - display_map::{DisplayRow, DisplaySnapshot, ToDisplayPoint}, + display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, - Bias, DisplayPoint, Editor, + Bias, DisplayPoint, Editor, ToOffset, }; use gpui::{actions, impl_actions, Window}; use itertools::Itertools; @@ -63,288 +63,49 @@ struct IndentObj { include_below: bool, } -/// Minimal struct to hold the start/end as display points. #[derive(Debug, Clone)] pub struct CandidateRange { pub start: DisplayPoint, pub end: DisplayPoint, } -fn gather_line_quotes(map: &DisplaySnapshot, line: DisplayRow) -> Vec { - // 1. figure out line length in display columns - let line_len = map.line_len(line); - - // 2. Convert (line, col=0) to a global text offset - // so we can collect line text out of the buffer - let line_start_dp = DisplayPoint::new(line, 0); - // `to_offset(map, Bias::Left)` returns a `usize` offset in the underlying buffer - let start_offset = line_start_dp.to_offset(map, Bias::Left); - // Similarly for col=line_len, bias=Right if you want the end - let line_end_dp = DisplayPoint::new(line, line_len); - let end_offset = line_end_dp.to_offset(map, Bias::Right); - - // 3. Actually build the *raw text* for that line by collecting chars - // from `start_offset` up to `end_offset`. - let count = end_offset.saturating_sub(start_offset); - let line_chars: String = map - .buffer_chars_at(start_offset) - .take(count) // only up to line_end - .map(|(ch, _off)| ch) - .collect(); - - // 4. Regex for quotes. You can also do `'([^']*)'` etc. - let mut ranges = Vec::new(); - let patterns = &["\"([^\"]*)\"", "'([^']*)'", "`([^`]*)`"]; - for pat in patterns { - let re = regex::Regex::new(pat).unwrap(); - for mat in re.find_iter(&line_chars) { - let local_start = mat.start(); - let local_end = mat.end(); - - // Convert these back to global offsets - let global_start = start_offset + local_start; - let global_end = start_offset + local_end; - - // Convert offsets → display points again - let start_dp = DisplayPoint::new(line, 0).offset_plus(map, global_start - start_offset); - let end_dp = DisplayPoint::new(line, 0).offset_plus(map, global_end - start_offset); - - ranges.push(CandidateRange { - start: start_dp, - end: end_dp, - }); - } - } - - ranges -} - -/// Gather `"..."`, `'...'`, and `` `...` `` pairs across the entire buffer. -/// Uses a single-pass approach over the combined text, storing offsets -> DisplayPoint, -/// then runs a naive multiline regex. -fn gather_quotes_multiline(map: &DisplaySnapshot) -> Vec { - // 1) Build entire buffer text + a mapping from “text index” -> DisplayPoint - let mut text = String::new(); - let mut offsets_to_dp = Vec::new(); - - let max_row = map.max_point().row().0; - let mut _global_offset = 0; - - for row_u32 in 0..=max_row { - let line = DisplayRow(row_u32); - - let line_len_u32 = map.line_len(line); - let line_start_dp = DisplayPoint::new(line, 0); - let start_offset = line_start_dp.to_offset(map, Bias::Left); - - let line_end_dp = DisplayPoint::new(line, line_len_u32); - let end_offset = line_end_dp.to_offset(map, Bias::Right); - - let count = end_offset.saturating_sub(start_offset); - let line_string: String = map - .buffer_chars_at(start_offset) - .take(count) - .map(|(ch, _off)| ch) - .collect(); - - // Store these characters in `text`, track each char’s DisplayPoint - for (i, ch) in line_string.chars().enumerate() { - text.push(ch); - offsets_to_dp.push(DisplayPoint::new(line, i as u32)); - _global_offset += 1; - } - } - - // 2) We run three naive multiline regexes: - // (?s)"[^"]*" or (?s)'[^']*' or (?s)`[^`]*` - // “(?s)” = “dot matches newline” - // disclaim: no escaping logic, just naive - let patterns = &[ - r#"(?s)"[^"]*""#, // double quotes - r#"(?s)'[^']*'"#, // single quotes - r#"(?s)`[^`]*`"#, // backtick - ]; - - let mut candidates = Vec::new(); - let combined_text_len = offsets_to_dp.len(); - - for pat in patterns { - let re = regex::Regex::new(pat).unwrap(); - // For each match, convert the match’s [start..end) indices -> display points - for mat in re.find_iter(&text) { - let start_idx = mat.start(); - let end_idx = mat.end().saturating_sub(1); // inclusive end - if end_idx >= combined_text_len { - continue; - } - - // The DP for the opening character - let dp_start = offsets_to_dp[start_idx]; - // The DP for the last char. We'll make it half‐open by +1 column - let dp_end_char = offsets_to_dp[end_idx]; - - // Make final end = last char’s column + 1 - let final_end = - DisplayPoint::new(dp_end_char.row(), dp_end_char.column().saturating_add(1)); - - candidates.push(CandidateRange { - start: dp_start, - end: final_end, - }); - } - } - - candidates -} - -/// Gather bracket pairs ((), [], {}, <>) across the entire buffer, not just one line. -/// This fixes the multiline `{ ... }` issue. -fn gather_brackets_multiline(map: &DisplaySnapshot) -> Vec { - // 1) Build the entire buffer as a single string. We also store the offset - // => (display row, column) mapping so we can convert back to DisplayPoints. - let mut text = String::new(); - let mut offsets_to_dp = Vec::new(); // for each character in `text`, store its DisplayPoint - - // We'll iterate line by line, but keep a running `global_offset` for the final big string - let max_row = map.max_point().row().0; - let mut _global_offset = 0; - - for row_u32 in 0..=max_row { - let line = DisplayRow(row_u32); - - let line_len_u32 = map.line_len(line); - let line_start_dp = DisplayPoint::new(line, 0); - let start_offset = line_start_dp.to_offset(map, Bias::Left); - - let line_end_dp = DisplayPoint::new(line, line_len_u32); - let end_offset = line_end_dp.to_offset(map, Bias::Right); - - // For each line, gather its characters - let count = end_offset.saturating_sub(start_offset); - let line_string: String = map - .buffer_chars_at(start_offset) - .take(count) - .map(|(ch, _off)| ch) - .collect(); - - // Store them in `text`, but also track each char's "DisplayPoint" - for (i, ch) in line_string.chars().enumerate() { - text.push(ch); - let dp = DisplayPoint::new(line, i as u32); - offsets_to_dp.push(dp); - _global_offset += 1; - } - } - - // 2) Single pass stack approach for each bracket type - let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; - let mut candidates = Vec::new(); - - for (open, close) in bracket_pairs { - let mut stack = Vec::new(); // store the "global index in text" - for (i, ch) in text.chars().enumerate() { - if ch == open { - stack.push(i); - } else if ch == close { - if let Some(open_i) = stack.pop() { - // We have a bracket pair from `open_i .. i` - // Convert each to the corresponding DisplayPoint - let start_dp = offsets_to_dp[open_i]; - // We might need +1 if `close` is multiple bytes, so do `i + ch.len_utf8()` if we want a *half‐open* range - let end_idx = i + ch.len_utf8().saturating_sub(1); - // But we also need to be sure we don't overflow the `offsets_to_dp` array - let end_idx_clamped = end_idx.min(offsets_to_dp.len().saturating_sub(1)); - let end_dp = offsets_to_dp[end_idx_clamped]; - - candidates.push(CandidateRange { - start: start_dp, - end: DisplayPoint::new( - end_dp.row(), - end_dp.column() + 1, // convert inclusive -> exclusive if you want - ), - }); - } - } - } - } - - candidates -} - -/// Gather bracket pairs on a single line: (), [], {}, <>. -/// Uses a simple stack approach for each bracket type. -fn gather_line_brackets(map: &DisplaySnapshot, line: DisplayRow) -> Vec { - // 1) line length - let line_len_u32 = map.line_len(line); - - // 2) Convert (line, col=0) -> offset in buffer - let line_start_dp = DisplayPoint::new(line, 0); - let start_offset = line_start_dp.to_offset(map, Bias::Left); - - // 3) Similarly for col=line_len - let line_end_dp = DisplayPoint::new(line, line_len_u32); - let end_offset = line_end_dp.to_offset(map, Bias::Right); - - // 4) Build the text for that line - let count = end_offset.saturating_sub(start_offset); - let line_text: String = map - .buffer_chars_at(start_offset) - .take(count) - .map(|(ch, _off)| ch) - .collect(); - - // 5) We'll do a single pass stack for all bracket types. One approach: - // Collect them all in one pass or do multiple passes. Here we do a single pass *per bracket type* for clarity. - let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; - - let mut candidates = Vec::new(); - - for (open, close) in bracket_pairs { - let mut stack = Vec::new(); - for (i, ch) in line_text.chars().enumerate() { - if ch == open { - stack.push(i); - } else if ch == close { - if let Some(open_i) = stack.pop() { - // Convert offsets -> display points - let dp_start = DisplayPoint::new(line, 0).offset_plus(map, open_i); - let dp_end = DisplayPoint::new(line, 0).offset_plus(map, i + ch.len_utf8()); - - candidates.push(CandidateRange { - start: dp_start, - end: dp_end, - }); - } - } - } - } - - candidates -} - -// -// 3) COVER OR NEXT" PICKING -// -fn pick_best_range<'a>( - candidates: &'a [CandidateRange], +fn cover_or_next, Range)>>( + candidates: Option, caret: DisplayPoint, map: &DisplaySnapshot, -) -> Option<&'a CandidateRange> { + 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 mut prev_ones = vec![]; - - for c in candidates { - let start_off = c.start.to_offset(map, Bias::Left); - let end_off = c.end.to_offset(map, Bias::Right); - - if start_off <= caret_offset && caret_offset < end_off { - covering.push(c); - } else if start_off >= caret_offset { - next_ones.push(c); - } else if end_off <= caret_offset { - prev_ones.push(c); + 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 c = CandidateRange { + start: start_off.to_display_point(map), + end: end_off.to_display_point(map), + }; + 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(c); + } else if start_off >= caret_offset { + next_ones.push(c); + } + } } } @@ -363,112 +124,94 @@ fn pick_best_range<'a>( }); } - // 3) prev -> closest by end - if !prev_ones.is_empty() { - return prev_ones.into_iter().min_by_key(|r| { - let end = r.end.to_offset(map, Bias::Right); - (end as isize - caret_offset as isize).abs() - }); - } - None } -fn find_any_quotes( +fn find_any_delimiters( map: &DisplaySnapshot, - caret: DisplayPoint, + display_point: DisplayPoint, around: bool, + is_valid_delimiter: impl Fn(&BufferSnapshot, usize) -> bool, ) -> Option> { - // 1) gather quotes on caret’s line - let line_candidates = gather_line_quotes(map, caret.row()); - if let Some(best_line) = pick_best_range(&line_candidates, caret, map) { - // Found a line-based quote pair => done - return finalize_quote_range(best_line.clone(), map, around); + let display_point = map.clip_at_line_end(display_point); + let point = display_point.to_point(map); + let offset = point.to_offset(&map.buffer_snapshot); + + // Ensure the range is contained by the current line. + let mut line_end = map.next_line_boundary(point).0; + if line_end == point { + line_end = map.max_point().to_point(map); } - // 2) fallback: gather from entire file (multiline) - let all_candidates = gather_quotes_multiline(map); - let best = pick_best_range(&all_candidates, caret, map)?; + let line_range = map.prev_line_boundary(point).0..line_end; + let visible_line_range = + line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1)); + let ranges = map + .buffer_snapshot + .bracket_ranges(visible_line_range.clone()); - // 3) Return final range, skipping bounding quote chars if “inner” - finalize_quote_range(best.clone(), map, around) -} + let snapshot = &map.buffer_snapshot; + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer = excerpt.buffer(); -/// A tiny helper to do “outer vs. inner” logic for quotes -fn finalize_quote_range( - pair: CandidateRange, - map: &DisplaySnapshot, - around: bool, -) -> Option> { - if around { - return Some(pair.start..pair.end); - } + let bracket_filter = |open: Range, close: Range| { + if open.end == close.start { + return false; + } - // “inner”: skip bounding quotes if possible - let start_off = pair.start.to_offset(map, Bias::Left); - let end_off = pair.end.to_offset(map, Bias::Right); - if end_off.saturating_sub(start_off) < 2 { - // not enough room to skip - return None; + is_valid_delimiter(buffer, open.start) + }; + + if let Some(best_line) = cover_or_next(ranges, display_point, map, Some(&bracket_filter)) { + return select_inside_or_around_delimiter_range(best_line.clone(), around); } - let new_start = DisplayPoint::new(pair.start.row(), pair.start.column() + 1); - let new_end = DisplayPoint::new(pair.end.row(), pair.end.column().saturating_sub(1)); + let (open_bracket, close_bracket) = + buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?; + + let new_start = open_bracket.end.to_display_point(map); + let new_end = close_bracket.start.to_display_point(map); + Some(new_start..new_end) } -/// Return the final bracket pair as a Range with line-first priority. -/// - If any bracket pair is found covering or next on the caret’s line, pick that. -/// - Otherwise, gather from the entire file (multiline) and pick again. -/// - `around` == true => return the full bracket pair. -/// - `around` == false => skip bounding chars. -fn find_any_brackets( +fn find_any_quotes( map: &DisplaySnapshot, - caret: DisplayPoint, + display_point: DisplayPoint, around: bool, -) -> Option> { - // 1) Gather bracket pairs on the caret’s line - let line_candidates = gather_line_brackets(map, caret.row()); - // “cover-or-next” logic in just those - if let Some(best_line) = pick_best_range(&line_candidates, caret, map) { - // We found a match on the same line => done - return finalize_bracket_range(best_line.clone(), map, around); - } - - // 2) If none on the same line, gather from entire buffer (multi-line) - let all_candidates = gather_brackets_multiline(map); - let best = pick_best_range(&all_candidates, caret, map)?; +) -> Option> { + find_any_delimiters(map, display_point, around, |buffer, start| { + matches!(buffer.chars_at(start).next(), Some('\'' | '"' | '`')) + }) +} - // 3) Return the final range, skipping bounding chars if `around == false` - finalize_bracket_range(best.clone(), map, around) +fn find_any_brackets( + map: &DisplaySnapshot, + display_point: DisplayPoint, + around: bool, +) -> Option> { + find_any_delimiters(map, display_point, around, |buffer, start| { + matches!( + buffer.chars_at(start).next(), + Some('(' | '[' | '{' | '<' | '|') + ) + }) } -/// A small helper to handle the “inner vs. outer” logic for bracket textobjects. -/// - If `around == false`, we skip the bounding chars, but only if at least 2 wide. -fn finalize_bracket_range( +fn select_inside_or_around_delimiter_range( pair: CandidateRange, - map: &DisplaySnapshot, around: bool, ) -> Option> { if around { - // Full bracket pair return Some(pair.start..pair.end); + } else { + let new_start = pair.start.column() + 1; + let new_end = pair.end.column() - 1; + return Some( + DisplayPoint::new(pair.start.row(), new_start) + ..DisplayPoint::new(pair.end.row(), new_end), + ); } - - // “inner”: skip the bounding chars if possible - let start_off = pair.start.to_offset(map, Bias::Left); - let end_off = pair.end.to_offset(map, Bias::Right); - - if end_off.saturating_sub(start_off) < 2 { - // Not enough room to skip - return None; - } - - // Shift start +1, end -1 - let new_start = DisplayPoint::new(pair.start.row(), pair.start.column() + 1); - let new_end = DisplayPoint::new(pair.end.row(), pair.end.column().saturating_sub(1)); - - Some(new_start..new_end) } impl_actions!(vim, [Word, Subword, IndentObj]); @@ -2313,7 +2056,7 @@ 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 @@ -2328,14 +2071,14 @@ mod test { ( "c i q", indoc! {" - ' + ` first middle ˇstring second - ' + ` "}, indoc! {" - 'ˇ' + `ˇ` "}, Mode::Insert, ), @@ -2359,13 +2102,13 @@ mod test { ( "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.", // same mini.ai plugin behavior + "This is a ˇ example.", // Not supported by tree sitter queries for now Mode::Insert, ), (