Skip to content

Commit

Permalink
Fix vim code working on display map chars (#10103)
Browse files Browse the repository at this point in the history
Release Notes:

- vim: Fixed motion bugs when softwrap, folds or inlay hints were used.
  • Loading branch information
ConradIrwin authored Apr 3, 2024
1 parent 754547f commit 5a2a85a
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 140 deletions.
82 changes: 16 additions & 66 deletions crates/editor/src/display_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ impl DisplaySnapshot {
layout_line.closest_index_for_x(x) as u32
}

pub fn chars_at(
pub fn display_chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
Expand All @@ -684,62 +684,26 @@ impl DisplaySnapshot {
})
}

pub fn reverse_chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
self.reverse_text_chunks(point.row())
.flat_map(|chunk| chunk.chars().rev())
.skip_while({
let mut column = self.line_len(point.row());
if self.max_point().row() > point.row() {
column += 1;
}
pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator<Item = (char, usize)> + '_ {
self.buffer_snapshot.chars_at(offset).map(move |ch| {
let ret = (ch, offset);
offset += ch.len_utf8();
ret
})
}

move |char| {
let at_point = column <= point.column();
column = column.saturating_sub(char.len_utf8() as u32);
!at_point
}
})
pub fn reverse_buffer_chars_at(
&self,
mut offset: usize,
) -> impl Iterator<Item = (char, usize)> + '_ {
self.buffer_snapshot
.reversed_chars_at(offset)
.map(move |ch| {
if ch == '\n' {
*point.row_mut() -= 1;
*point.column_mut() = self.line_len(point.row());
} else {
*point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
}
(ch, point)
offset -= ch.len_utf8();
(ch, offset)
})
}

pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0;
let mut column = 0;
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
if column >= target {
break;
}
count += 1;
column += c.len_utf8() as u32;
}
count
}

pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
let mut column = 0;

for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
if c == '\n' || count >= char_count as usize {
break;
}
column += c.len_utf8() as u32;
}

column
}

pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
let mut clipped = self.block_snapshot.clip_point(point.0, bias);
if self.clip_at_line_ends {
Expand Down Expand Up @@ -808,20 +772,6 @@ impl DisplaySnapshot {
result
}

pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
let mut indent = 0;
let mut is_blank = true;
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
if c == ' ' {
indent += 1;
} else {
is_blank = c == '\n';
break;
}
}
(indent, is_blank)
}

pub fn line_indent_for_buffer_row(&self, buffer_row: u32) -> (u32, bool) {
let (buffer, range) = self
.buffer_snapshot
Expand Down
9 changes: 4 additions & 5 deletions crates/editor/src/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -876,10 +876,8 @@ impl EditorElement {
block_width = em_width;
}
let block_text = if let CursorShape::Block = selection.cursor_shape {
snapshot
.chars_at(cursor_position)
.next()
.and_then(|(character, _)| {
snapshot.display_chars_at(cursor_position).next().and_then(
|(character, _)| {
let text = if character == '\n' {
SharedString::from(" ")
} else {
Expand All @@ -900,7 +898,8 @@ impl EditorElement {
}],
)
.log_err()
})
},
)
} else {
None
};
Expand Down
3 changes: 3 additions & 0 deletions crates/multi_buffer/src/anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@ impl AnchorRangeExt for Range<Anchor> {
self.start.to_point(content)..self.end.to_point(content)
}
}

#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Ord, PartialOrd)]
pub struct Offset(pub usize);
2 changes: 1 addition & 1 deletion crates/multi_buffer/src/multi_buffer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mod anchor;

pub use anchor::{Anchor, AnchorRangeExt};
pub use anchor::{Anchor, AnchorRangeExt, Offset};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
Expand Down
8 changes: 4 additions & 4 deletions crates/vim/src/motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,21 +1283,21 @@ pub(crate) fn first_non_whitespace(
display_lines: bool,
from: DisplayPoint,
) -> DisplayPoint {
let mut last_point = start_of_line(map, display_lines, from);
let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
for (ch, point) in map.chars_at(last_point) {
for (ch, offset) in map.buffer_chars_at(start_offset) {
if ch == '\n' {
return from;
}

last_point = point;
start_offset = offset;

if char_kind(&scope, ch) != CharKind::Whitespace {
break;
}
}

map.clip_point(last_point, Bias::Left)
start_offset.to_display_point(map)
}

pub(crate) fn start_of_line(
Expand Down
47 changes: 24 additions & 23 deletions crates/vim/src/normal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ use crate::{
};
use collections::BTreeSet;
use editor::scroll::Autoscroll;
use editor::{Bias, DisplayPoint};
use editor::Bias;
use gpui::{actions, ViewContext, WindowContext};
use language::SelectionGoal;
use language::{Point, SelectionGoal};
use log::error;
use workspace::Workspace;

Expand Down Expand Up @@ -283,19 +283,20 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(cx);
let selection_start_rows: BTreeSet<u32> = old_selections
let selections = editor.selections.all::<Point>(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);

let selection_start_rows: BTreeSet<u32> = selections
.into_iter()
.map(|selection| selection.start.row())
.map(|selection| selection.start.row)
.collect();
let edits = selection_start_rows.into_iter().map(|row| {
let (indent, _) = map.line_indent(row);
let start_of_line =
motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
.to_point(&map);
let mut new_text = " ".repeat(indent as usize);
new_text.push('\n');
(start_of_line..start_of_line, new_text)
let indent = snapshot
.indent_size_for_line(row)
.chars()
.collect::<String>();
let start_of_line = Point::new(row, 0);
(start_of_line..start_of_line, indent + "\n")
});
editor.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
Expand All @@ -317,20 +318,20 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
vim.update_active_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(cx);
let selection_end_rows: BTreeSet<u32> = old_selections
let selections = editor.selections.all::<Point>(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);

let selection_end_rows: BTreeSet<u32> = selections
.into_iter()
.map(|selection| selection.end.row())
.map(|selection| selection.end.row)
.collect();
let edits = selection_end_rows.into_iter().map(|row| {
let (indent, _) = map.line_indent(row);
let end_of_line =
motion::end_of_line(&map, false, DisplayPoint::new(row, 0), 1)
.to_point(&map);

let mut new_text = "\n".to_string();
new_text.push_str(&" ".repeat(indent as usize));
(end_of_line..end_of_line, new_text)
let indent = snapshot
.indent_size_for_line(row)
.chars()
.collect::<String>();
let end_of_line = Point::new(row, snapshot.line_len(row));
(end_of_line..end_of_line, "\n".to_string() + &indent)
});
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Expand Down
13 changes: 9 additions & 4 deletions crates/vim/src/normal/change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use crate::{
Vim,
};
use editor::{
display_map::DisplaySnapshot, movement::TextLayoutDetails, scroll::Autoscroll, DisplayPoint,
display_map::{DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
scroll::Autoscroll,
Bias, DisplayPoint,
};
use gpui::WindowContext;
use language::{char_kind, CharKind, Selection};
Expand Down Expand Up @@ -56,15 +59,17 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
&text_layout_details,
);
if let Motion::CurrentLine = motion {
let mut start_offset = selection.start.to_offset(map, Bias::Left);
let scope = map
.buffer_snapshot
.language_scope_at(selection.start.to_point(&map));
for (ch, _) in map.chars_at(selection.start) {
for (ch, offset) in map.buffer_chars_at(start_offset) {
if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
break;
}
*selection.start.column_mut() += 1;
start_offset = offset + ch.len_utf8();
}
selection.start = start_offset.to_display_point(map);
}
result
};
Expand Down Expand Up @@ -126,7 +131,7 @@ fn expand_changed_word_selection(
.buffer_snapshot
.language_scope_at(selection.start.to_point(map));
let in_word = map
.chars_at(selection.head())
.buffer_chars_at(selection.head().to_offset(map, Bias::Left))
.next()
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
.unwrap_or_default();
Expand Down
8 changes: 5 additions & 3 deletions crates/vim/src/normal/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,15 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
selection.start = (start - '\n'.len_utf8()).to_display_point(map);
}
};
let range = selection.start.to_offset(map, Bias::Left)
..selection.end.to_offset(map, Bias::Right);
let contains_only_newlines = map
.chars_at(selection.start)
.take_while(|(_, p)| p < &selection.end)
.buffer_chars_at(range.start)
.take_while(|(_, p)| p < &range.end)
.all(|(char, _)| char == '\n')
&& !offset_range.is_empty();
let end_at_newline = map
.chars_at(selection.end)
.buffer_chars_at(range.end)
.next()
.map(|(c, _)| c == '\n')
.unwrap_or(false);
Expand Down
Loading

0 comments on commit 5a2a85a

Please sign in to comment.