Show correct number of characters selected (#16420)

Kirill Bulatov created

Change summary

crates/go_to_line/src/cursor_position.rs | 20 ++++-
crates/go_to_line/src/go_to_line.rs      | 79 ++++++++++++++++++++++++++
2 files changed, 93 insertions(+), 6 deletions(-)

Detailed changes

crates/go_to_line/src/cursor_position.rs 🔗

@@ -12,11 +12,11 @@ use ui::{
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
-#[derive(Copy, Clone, Default, PartialOrd, PartialEq)]
-struct SelectionStats {
-    lines: usize,
-    characters: usize,
-    selections: usize,
+#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
+pub(crate) struct SelectionStats {
+    pub lines: usize,
+    pub characters: usize,
+    pub selections: usize,
 }
 
 pub struct CursorPosition {
@@ -44,7 +44,10 @@ impl CursorPosition {
         self.selected_count.selections = editor.selections.count();
         let mut last_selection: Option<Selection<usize>> = None;
         for selection in editor.selections.all::<usize>(cx) {
-            self.selected_count.characters += selection.end - selection.start;
+            self.selected_count.characters += buffer
+                .text_for_range(selection.start..selection.end)
+                .map(|t| t.chars().count())
+                .sum::<usize>();
             if last_selection
                 .as_ref()
                 .map_or(true, |last_selection| selection.id > last_selection.id)
@@ -106,6 +109,11 @@ impl CursorPosition {
         }
         text.push(')');
     }
+
+    #[cfg(test)]
+    pub(crate) fn selection_stats(&self) -> &SelectionStats {
+        &self.selected_count
+    }
 }
 
 impl Render for CursorPosition {

crates/go_to_line/src/go_to_line.rs 🔗

@@ -221,6 +221,8 @@ impl Render for GoToLine {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use cursor_position::{CursorPosition, SelectionStats};
+    use editor::actions::SelectAll;
     use gpui::{TestAppContext, VisualTestContext};
     use indoc::indoc;
     use project::{FakeFs, Project};
@@ -335,6 +337,83 @@ mod tests {
         assert_single_caret_at_row(&editor, expected_highlighted_row, cx);
     }
 
+    #[gpui::test]
+    async fn test_unicode_characters_selection(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "a.rs": "ēlo"
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+        workspace.update(cx, |workspace, cx| {
+            let cursor_position = cx.new_view(|_| CursorPosition::new(workspace));
+            workspace.status_bar().update(cx, |status_bar, cx| {
+                status_bar.add_right_item(cursor_position, cx);
+            });
+        });
+
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().update(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+        let _buffer = project
+            .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+            .await
+            .unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "a.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        workspace.update(cx, |workspace, cx| {
+            assert_eq!(
+                &SelectionStats {
+                    lines: 0,
+                    characters: 0,
+                    selections: 1,
+                },
+                workspace
+                    .status_bar()
+                    .read(cx)
+                    .item_of_type::<CursorPosition>()
+                    .expect("missing cursor position item")
+                    .read(cx)
+                    .selection_stats(),
+                "No selections should be initially"
+            );
+        });
+        editor.update(cx, |editor, cx| editor.select_all(&SelectAll, cx));
+        workspace.update(cx, |workspace, cx| {
+            assert_eq!(
+                &SelectionStats {
+                    lines: 1,
+                    characters: 3,
+                    selections: 1,
+                },
+                workspace
+                    .status_bar()
+                    .read(cx)
+                    .item_of_type::<CursorPosition>()
+                    .expect("missing cursor position item")
+                    .read(cx)
+                    .selection_stats(),
+                "After selecting a text with multibyte unicode characters, the character count should be correct"
+            );
+        });
+    }
+
     fn open_go_to_line_view(
         workspace: &View<Workspace>,
         cx: &mut VisualTestContext,