Use Horizontal ranges everywhere

Conrad Irwin created

Change summary

crates/editor/src/display_map.rs           |  49 ---------
crates/editor/src/editor.rs                |  28 +++--
crates/editor/src/element.rs               |   5 
crates/editor/src/movement.rs              |   6 +
crates/editor/src/selections_collection.rs |  23 ++-
crates/text/src/selection.rs               |   4 
crates/vim/src/motion.rs                   | 120 ++++++++++++++---------
crates/vim/src/normal.rs                   |  32 ++++--
crates/vim/src/normal/substitute.rs        |   6 -
crates/vim/src/test.rs                     |  56 +++++++++++
crates/vim/src/vim.rs                      |   2 
crates/vim/src/visual.rs                   |  44 ++++++--
crates/vim/test_data/test_j.json           |   3 
13 files changed, 229 insertions(+), 149 deletions(-)

Detailed changes

crates/editor/src/display_map.rs πŸ”—

@@ -15,7 +15,7 @@ use gpui::{
     color::Color,
     fonts::{FontId, HighlightStyle, Underline},
     text_layout::{Line, RunStyle},
-    AppContext, Entity, FontCache, ModelContext, ModelHandle, TextLayoutCache,
+    Entity, ModelContext, ModelHandle,
 };
 use inlay_map::InlayMap;
 use language::{
@@ -576,7 +576,6 @@ impl DisplaySnapshot {
 
         let range = display_row..display_row + 1;
         for chunk in self.highlighted_chunks(range, editor_style) {
-            dbg!(chunk.chunk);
             line.push_str(chunk.chunk);
 
             let text_style = if let Some(style) = chunk.style {
@@ -600,7 +599,6 @@ impl DisplaySnapshot {
             ));
         }
 
-        dbg!(&line, &editor_style.text.font_size, &styles);
         text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles)
     }
 
@@ -623,49 +621,6 @@ impl DisplaySnapshot {
         layout_line.closest_index_for_x(x_coordinate) as u32
     }
 
-    // column_for_x(row, x)
-
-    fn point(
-        &self,
-        display_point: DisplayPoint,
-        text_layout_cache: &TextLayoutCache,
-        editor_style: &EditorStyle,
-        cx: &AppContext,
-    ) -> f32 {
-        let mut styles = Vec::new();
-        let mut line = String::new();
-
-        let range = display_point.row()..display_point.row() + 1;
-        for chunk in self.highlighted_chunks(range, editor_style) {
-            dbg!(chunk.chunk);
-            line.push_str(chunk.chunk);
-
-            let text_style = if let Some(style) = chunk.style {
-                editor_style
-                    .text
-                    .clone()
-                    .highlight(style, cx.font_cache())
-                    .map(Cow::Owned)
-                    .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
-            } else {
-                Cow::Borrowed(&editor_style.text)
-            };
-
-            styles.push((
-                chunk.chunk.len(),
-                RunStyle {
-                    font_id: text_style.font_id,
-                    color: text_style.color,
-                    underline: text_style.underline,
-                },
-            ));
-        }
-
-        dbg!(&line, &editor_style.text.font_size, &styles);
-        let layout_line = text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles);
-        layout_line.x_for_index(display_point.column() as usize)
-    }
-
     pub fn chars_at(
         &self,
         mut point: DisplayPoint,
@@ -1374,7 +1329,6 @@ pub mod tests {
             );
 
             let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
-            dbg!(x);
             assert_eq!(
                 movement::up(
                     &snapshot,
@@ -1401,7 +1355,6 @@ pub mod tests {
                     SelectionGoal::HorizontalPosition(x)
                 )
             );
-            dbg!("starting down...");
             assert_eq!(
                 movement::down(
                     &snapshot,

crates/editor/src/editor.rs πŸ”—

@@ -48,9 +48,9 @@ use gpui::{
     impl_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton},
-    serde_json, text_layout, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem,
-    Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle, WindowContext,
+    serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element,
+    Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -5953,11 +5953,14 @@ impl Editor {
     fn add_selection(&mut self, above: bool, cx: &mut ViewContext<Self>) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut selections = self.selections.all::<Point>(cx);
+        let text_layout_details = TextLayoutDetails::new(self, cx);
         let mut state = self.add_selections_state.take().unwrap_or_else(|| {
             let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
             let range = oldest_selection.display_range(&display_map).sorted();
-            let columns = cmp::min(range.start.column(), range.end.column())
-                ..cmp::max(range.start.column(), range.end.column());
+
+            let start_x = display_map.x_for_point(range.start, &text_layout_details);
+            let end_x = display_map.x_for_point(range.end, &text_layout_details);
+            let positions = start_x.min(end_x)..start_x.max(end_x);
 
             selections.clear();
             let mut stack = Vec::new();
@@ -5965,8 +5968,9 @@ impl Editor {
                 if let Some(selection) = self.selections.build_columnar_selection(
                     &display_map,
                     row,
-                    &columns,
+                    &positions,
                     oldest_selection.reversed,
+                    &text_layout_details,
                 ) {
                     stack.push(selection.id);
                     selections.push(selection);
@@ -5994,12 +5998,15 @@ impl Editor {
                     let range = selection.display_range(&display_map).sorted();
                     debug_assert_eq!(range.start.row(), range.end.row());
                     let mut row = range.start.row();
-                    let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal
+                    let positions = if let SelectionGoal::HorizontalRange { start, end } =
+                        selection.goal
                     {
                         start..end
                     } else {
-                        cmp::min(range.start.column(), range.end.column())
-                            ..cmp::max(range.start.column(), range.end.column())
+                        let start_x = display_map.x_for_point(range.start, &text_layout_details);
+                        let end_x = display_map.x_for_point(range.end, &text_layout_details);
+
+                        start_x.min(end_x)..start_x.max(end_x)
                     };
 
                     while row != end_row {
@@ -6012,8 +6019,9 @@ impl Editor {
                         if let Some(new_selection) = self.selections.build_columnar_selection(
                             &display_map,
                             row,
-                            &columns,
+                            &positions,
                             selection.reversed,
+                            &text_layout_details,
                         ) {
                             state.stack.push(new_selection.id);
                             if above {

crates/editor/src/element.rs πŸ”—

@@ -22,7 +22,7 @@ use git::diff::DiffHunkStatus;
 use gpui::{
     color::Color,
     elements::*,
-    fonts::{HighlightStyle, TextStyle, Underline},
+    fonts::TextStyle,
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
@@ -37,8 +37,7 @@ use gpui::{
 use itertools::Itertools;
 use json::json;
 use language::{
-    language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
-    Selection,
+    language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
 };
 use project::{
     project_settings::{GitGutterSetting, ProjectSettings},

crates/editor/src/movement.rs πŸ”—

@@ -1,6 +1,6 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
 use crate::{char_kind, CharKind, Editor, EditorStyle, ToOffset, ToPoint};
-use gpui::{text_layout, FontCache, TextLayoutCache, WindowContext};
+use gpui::{FontCache, TextLayoutCache, WindowContext};
 use language::Point;
 use std::{ops::Range, sync::Arc};
 
@@ -105,7 +105,9 @@ pub fn up_by_rows(
 ) -> (DisplayPoint, SelectionGoal) {
     let mut goal_x = match goal {
         SelectionGoal::HorizontalPosition(x) => x,
+        SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
         SelectionGoal::HorizontalRange { end, .. } => end,
+        SelectionGoal::WrappedHorizontalRange { end: (_, end), .. } => end,
         _ => map.x_for_point(start, text_layout_details),
     };
 
@@ -140,7 +142,9 @@ pub fn down_by_rows(
 ) -> (DisplayPoint, SelectionGoal) {
     let mut goal_x = match goal {
         SelectionGoal::HorizontalPosition(x) => x,
+        SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
         SelectionGoal::HorizontalRange { end, .. } => end,
+        SelectionGoal::WrappedHorizontalRange { end: (_, end), .. } => end,
         _ => map.x_for_point(start, text_layout_details),
     };
 

crates/editor/src/selections_collection.rs πŸ”—

@@ -1,6 +1,6 @@
 use std::{
     cell::Ref,
-    cmp, iter, mem,
+    iter, mem,
     ops::{Deref, DerefMut, Range, Sub},
     sync::Arc,
 };
@@ -13,6 +13,7 @@ use util::post_inc;
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    movement::TextLayoutDetails,
     Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
 };
 
@@ -305,23 +306,27 @@ impl SelectionsCollection {
         &mut self,
         display_map: &DisplaySnapshot,
         row: u32,
-        columns: &Range<u32>,
+        positions: &Range<f32>,
         reversed: bool,
+        text_layout_details: &TextLayoutDetails,
     ) -> Option<Selection<Point>> {
-        let is_empty = columns.start == columns.end;
+        let is_empty = positions.start == positions.end;
         let line_len = display_map.line_len(row);
-        if columns.start < line_len || (is_empty && columns.start == line_len) {
-            let start = DisplayPoint::new(row, columns.start);
-            let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
+
+        let start_col = display_map.column_for_x(row, positions.start, text_layout_details);
+        if start_col < line_len || (is_empty && start_col == line_len) {
+            let start = DisplayPoint::new(row, start_col);
+            let end_col = display_map.column_for_x(row, positions.end, text_layout_details);
+            let end = DisplayPoint::new(row, end_col);
 
             Some(Selection {
                 id: post_inc(&mut self.next_selection_id),
                 start: start.to_point(display_map),
                 end: end.to_point(display_map),
                 reversed,
-                goal: SelectionGoal::ColumnRange {
-                    start: columns.start,
-                    end: columns.end,
+                goal: SelectionGoal::HorizontalRange {
+                    start: positions.start,
+                    end: positions.end,
                 },
             })
         } else {

crates/text/src/selection.rs πŸ”—

@@ -7,8 +7,8 @@ pub enum SelectionGoal {
     None,
     HorizontalPosition(f32),
     HorizontalRange { start: f32, end: f32 },
-    Column(u32),
-    ColumnRange { start: u32, end: u32 },
+    WrappedHorizontalPosition((u32, f32)),
+    WrappedHorizontalRange { start: (u32, f32), end: (u32, f32) },
 }
 
 #[derive(Clone, Debug, PartialEq)]

crates/vim/src/motion.rs πŸ”—

@@ -1,5 +1,3 @@
-use std::cmp;
-
 use editor::{
     char_kind,
     display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
@@ -371,13 +369,13 @@ impl Motion {
             Backspace => (backspace(map, point, times), SelectionGoal::None),
             Down {
                 display_lines: false,
-            } => down(map, point, goal, times),
+            } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
             Down {
                 display_lines: true,
             } => down_display(map, point, goal, times, &text_layout_details),
             Up {
                 display_lines: false,
-            } => up(map, point, goal, times),
+            } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
             Up {
                 display_lines: true,
             } => up_display(map, point, goal, times, &text_layout_details),
@@ -536,35 +534,86 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
     point
 }
 
-fn down(
+pub(crate) fn start_of_relative_buffer_row(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+    times: isize,
+) -> DisplayPoint {
+    let start = map.display_point_to_fold_point(point, Bias::Left);
+    let target = start.row() as isize + times;
+    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
+
+    map.clip_point(
+        map.fold_point_to_display_point(
+            map.fold_snapshot
+                .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
+        ),
+        Bias::Right,
+    )
+}
+
+fn up_down_buffer_rows(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     mut goal: SelectionGoal,
-    times: usize,
+    times: isize,
+    text_layout_details: &TextLayoutDetails,
 ) -> (DisplayPoint, SelectionGoal) {
     let start = map.display_point_to_fold_point(point, Bias::Left);
+    let begin_folded_line = map.fold_point_to_display_point(
+        map.fold_snapshot
+            .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
+    );
+    let select_nth_wrapped_row = point.row() - begin_folded_line.row();
 
-    let goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
+    let (goal_wrap, goal_x) = match goal {
+        SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
+        SelectionGoal::WrappedHorizontalRange { end: (row, x), .. } => (row, x),
+        SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
+        SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
         _ => {
-            goal = SelectionGoal::Column(start.column());
-            start.column()
+            let x = map.x_for_point(point, text_layout_details);
+            goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
+            (select_nth_wrapped_row, x)
         }
     };
 
-    let new_row = cmp::min(
-        start.row() + times as u32,
-        map.fold_snapshot.max_point().row(),
-    );
-    let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
-    let point = map.fold_point_to_display_point(
+    let target = start.row() as isize + times;
+    let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
+
+    let mut begin_folded_line = map.fold_point_to_display_point(
         map.fold_snapshot
-            .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
+            .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
     );
 
-    // clip twice to "clip at end of line"
-    (map.clip_point(point, Bias::Left), goal)
+    let mut i = 0;
+    while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
+        let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
+        if map
+            .display_point_to_fold_point(next_folded_line, Bias::Right)
+            .row()
+            == new_row
+        {
+            i += 1;
+            begin_folded_line = next_folded_line;
+        } else {
+            break;
+        }
+    }
+
+    let new_col = if i == goal_wrap {
+        map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
+    } else {
+        map.line_len(begin_folded_line.row())
+    };
+
+    (
+        map.clip_point(
+            DisplayPoint::new(begin_folded_line.row(), new_col),
+            Bias::Left,
+        ),
+        goal,
+    )
 }
 
 fn down_display(
@@ -581,33 +630,6 @@ fn down_display(
     (point, goal)
 }
 
-pub(crate) fn up(
-    map: &DisplaySnapshot,
-    point: DisplayPoint,
-    mut goal: SelectionGoal,
-    times: usize,
-) -> (DisplayPoint, SelectionGoal) {
-    let start = map.display_point_to_fold_point(point, Bias::Left);
-
-    let goal_column = match goal {
-        SelectionGoal::Column(column) => column,
-        SelectionGoal::ColumnRange { end, .. } => end,
-        _ => {
-            goal = SelectionGoal::Column(start.column());
-            start.column()
-        }
-    };
-
-    let new_row = start.row().saturating_sub(times as u32);
-    let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
-    let point = map.fold_point_to_display_point(
-        map.fold_snapshot
-            .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
-    );
-
-    (map.clip_point(point, Bias::Left), goal)
-}
-
 fn up_display(
     map: &DisplaySnapshot,
     mut point: DisplayPoint,
@@ -894,7 +916,7 @@ fn find_backward(
 }
 
 fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
-    let correct_line = down(map, point, SelectionGoal::None, times).0;
+    let correct_line = start_of_relative_buffer_row(map, point, times as isize);
     first_non_whitespace(map, false, correct_line)
 }
 
@@ -904,7 +926,7 @@ pub(crate) fn next_line_end(
     times: usize,
 ) -> DisplayPoint {
     if times > 1 {
-        point = down(map, point, SelectionGoal::None, times - 1).0;
+        point = start_of_relative_buffer_row(map, point, times as isize - 1);
     }
     end_of_line(map, false, point)
 }

crates/vim/src/normal.rs πŸ”—

@@ -194,9 +194,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.move_cursors_with(|map, cursor, goal| {
-                    (right(map, cursor, 1), SelectionGoal::None)
-                });
+                s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
             });
         });
     });
@@ -219,7 +217,7 @@ fn insert_first_non_whitespace(
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.move_cursors_with(|map, cursor, goal| {
+                s.move_cursors_with(|map, cursor, _| {
                     (
                         first_non_whitespace(map, false, cursor),
                         SelectionGoal::None,
@@ -236,7 +234,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
         vim.switch_mode(Mode::Insert, false, cx);
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.move_cursors_with(|map, cursor, goal| {
+                s.move_cursors_with(|map, cursor, _| {
                     (next_line_end(map, cursor, 1), SelectionGoal::None)
                 });
             });
@@ -267,7 +265,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
                 editor.edit_with_autoindent(edits, cx);
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.move_cursors_with(|map, cursor, _| {
-                        let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
+                        let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
                         let insert_point = motion::end_of_line(map, false, previous_line);
                         (insert_point, SelectionGoal::None)
                     });
@@ -398,12 +396,26 @@ mod test {
 
     #[gpui::test]
     async fn test_j(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
-        cx.assert_all(indoc! {"
-            ˇThe qˇuick broˇwn
-            Λ‡fox jumps"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+                    aaˇaa
+                    πŸ˜ƒπŸ˜ƒ"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j"]).await;
+        cx.assert_shared_state(indoc! {"
+                    aaaa
+                    πŸ˜ƒΛ‡πŸ˜ƒ"
         })
         .await;
+
+        for marked_position in cx.each_marked_position(indoc! {"
+                    ˇThe qˇuick broˇwn
+                    Λ‡fox jumps"
+        }) {
+            cx.assert_neovim_compatible(&marked_position, ["j"]).await;
+        }
     }
 
     #[gpui::test]

crates/vim/src/normal/substitute.rs πŸ”—

@@ -3,11 +3,7 @@ use gpui::{actions, AppContext, WindowContext};
 use language::Point;
 use workspace::Workspace;
 
-use crate::{
-    motion::{right, Motion},
-    utils::copy_selections_content,
-    Mode, Vim,
-};
+use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
 
 actions!(vim, [Substitute, SubstituteLine]);
 

crates/vim/src/test.rs πŸ”—

@@ -652,3 +652,59 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
         Lorem Ipsum"})
         .await;
 }
+
+async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_wrap(12).await;
+
+    cx.set_shared_state(indoc! {"
+                aaˇaa
+                πŸ˜ƒπŸ˜ƒ"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+                aaaa
+                πŸ˜ƒΛ‡πŸ˜ƒ"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+                123456789012aaˇaa
+                123456789012πŸ˜ƒπŸ˜ƒ"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaa
+        123456789012πŸ˜ƒΛ‡πŸ˜ƒ"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+                123456789012aaˇaa
+                123456789012πŸ˜ƒπŸ˜ƒ"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaa
+        123456789012πŸ˜ƒΛ‡πŸ˜ƒ"
+    })
+    .await;
+
+    cx.set_shared_state(indoc! {"
+        123456789012aaaaˇaaaaaaaa123456789012
+        wow
+        123456789012πŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒ123456789012"
+    })
+    .await;
+    cx.simulate_shared_keystrokes(["j", "j"]).await;
+    cx.assert_shared_state(indoc! {"
+        123456789012aaaaaaaaaaaa123456789012
+        wow
+        123456789012πŸ˜ƒπŸ˜ƒΛ‡πŸ˜ƒπŸ˜ƒπŸ˜ƒπŸ˜ƒ123456789012"
+    })
+    .await;
+}

crates/vim/src/vim.rs πŸ”—

@@ -581,7 +581,7 @@ impl Setting for VimModeSetting {
 fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
-            if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
+            if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
                 vim.switch_mode(Mode::VisualBlock, false, cx);
             } else {
                 vim.switch_mode(Mode::Visual, false, cx)

crates/vim/src/visual.rs πŸ”—

@@ -140,17 +140,21 @@ pub fn visual_block_motion(
         SelectionGoal,
     ) -> Option<(DisplayPoint, SelectionGoal)>,
 ) {
+    let text_layout_details = TextLayoutDetails::new(editor, cx);
     editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
         let map = &s.display_map();
         let mut head = s.newest_anchor().head().to_display_point(map);
         let mut tail = s.oldest_anchor().tail().to_display_point(map);
 
         let (start, end) = match s.newest_anchor().goal {
-            SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
-            SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
-            _ => (tail.column(), head.column()),
+            SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
+            SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start + 10.0),
+            _ => (
+                map.x_for_point(tail, &text_layout_details),
+                map.x_for_point(head, &text_layout_details),
+            ),
         };
-        let goal = SelectionGoal::ColumnRange { start, end };
+        let goal = SelectionGoal::HorizontalRange { start, end };
 
         let was_reversed = tail.column() > head.column();
         if !was_reversed && !preserve_goal {
@@ -172,21 +176,39 @@ pub fn visual_block_motion(
             head = movement::saturating_right(map, head)
         }
 
-        let columns = if is_reversed {
-            head.column()..tail.column()
+        let positions = if is_reversed {
+            map.x_for_point(head, &text_layout_details)..map.x_for_point(tail, &text_layout_details)
         } else if head.column() == tail.column() {
-            head.column()..(head.column() + 1)
+            map.x_for_point(head, &text_layout_details)
+                ..map.x_for_point(head, &text_layout_details) + 10.0
         } else {
-            tail.column()..head.column()
+            map.x_for_point(tail, &text_layout_details)..map.x_for_point(head, &text_layout_details)
         };
 
         let mut selections = Vec::new();
         let mut row = tail.row();
 
         loop {
-            let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
-            let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
-            if columns.start <= map.line_len(row) {
+            let start = map.clip_point(
+                DisplayPoint::new(
+                    row,
+                    map.column_for_x(row, positions.start, &text_layout_details),
+                ),
+                Bias::Left,
+            );
+            let end = map.clip_point(
+                DisplayPoint::new(
+                    row,
+                    map.column_for_x(row, positions.end, &text_layout_details),
+                ),
+                Bias::Left,
+            );
+            if positions.start
+                <= map.x_for_point(
+                    DisplayPoint::new(row, map.line_len(row)),
+                    &text_layout_details,
+                )
+            {
                 let selection = Selection {
                     id: s.new_selection_id(),
                     start: start.to_point(map),

crates/vim/test_data/test_j.json πŸ”—

@@ -1,3 +1,6 @@
+{"Put":{"state":"aaΛ‡aa\nπŸ˜ƒπŸ˜ƒ"}}
+{"Key":"j"}
+{"Get":{"state":"aaaa\nπŸ˜ƒΛ‡πŸ˜ƒ","mode":"Normal"}}
 {"Put":{"state":"Λ‡The quick brown\nfox jumps"}}
 {"Key":"j"}
 {"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}