Rework go to line infrastructure (#23654)
Closes https://github.com/zed-industries/zed/issues/12024 https://github.com/user-attachments/assets/60ea3dbd-b594-4bf5-a44d-4bff925b815f * Fixes incorrect line selection for certain corner cases Before: <img width="1728" alt="image" src="https://github.com/user-attachments/assets/35aaee6c-c120-4bf1-9355-448a29d1b9b5" /> After: <img width="1728" alt="image" src="https://github.com/user-attachments/assets/abd97339-4594-4e8e-8605-50d74581ae86" /> * Reworks https://github.com/zed-industries/zed/pull/16420 to display selection length with less performance overhead. Improves the performance more, doing a single selections loop instead of two. * Fixes incorrect caret position display when text contains UTF-8 chars with size > 1 Also fixes tooltop values for this case * Fixes go to line to treat UTF-8 chars with size > 1 properly when navigating * Adds a way to fill go to line text editor with its tooltip on `Tab` Release Notes: - Fixed incorrect UTF-8 characters handling in `GoToLine` and caret position
This commit is contained in:
parent
7c0a39daa6
commit
da2bd4b8e9
5 changed files with 365 additions and 69 deletions
|
@ -1,9 +1,9 @@
|
||||||
use editor::{Editor, ToPoint};
|
use editor::{Editor, MultiBufferSnapshot};
|
||||||
use gpui::{AppContext, FocusHandle, FocusableView, Subscription, Task, View, WeakView};
|
use gpui::{AppContext, FocusHandle, FocusableView, Subscription, Task, View, WeakView};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsSources};
|
use settings::{Settings, SettingsSources};
|
||||||
use std::{fmt::Write, time::Duration};
|
use std::{fmt::Write, num::NonZeroU32, time::Duration};
|
||||||
use text::{Point, Selection};
|
use text::{Point, Selection};
|
||||||
use ui::{
|
use ui::{
|
||||||
div, Button, ButtonCommon, Clickable, FluentBuilder, IntoElement, LabelSize, ParentElement,
|
div, Button, ButtonCommon, Clickable, FluentBuilder, IntoElement, LabelSize, ParentElement,
|
||||||
|
@ -20,7 +20,7 @@ pub(crate) struct SelectionStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CursorPosition {
|
pub struct CursorPosition {
|
||||||
position: Option<(Point, bool)>,
|
position: Option<UserCaretPosition>,
|
||||||
selected_count: SelectionStats,
|
selected_count: SelectionStats,
|
||||||
context: Option<FocusHandle>,
|
context: Option<FocusHandle>,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
|
@ -28,6 +28,30 @@ pub struct CursorPosition {
|
||||||
_observe_active_editor: Option<Subscription>,
|
_observe_active_editor: Option<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A position in the editor, where user's caret is located at.
|
||||||
|
/// Lines are never zero as there is always at least one line in the editor.
|
||||||
|
/// Characters may start with zero as the caret may be at the beginning of a line, but all editors start counting characters from 1,
|
||||||
|
/// where "1" will mean "before the first character".
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct UserCaretPosition {
|
||||||
|
pub line: NonZeroU32,
|
||||||
|
pub character: NonZeroU32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserCaretPosition {
|
||||||
|
pub fn at_selection_end(selection: &Selection<Point>, snapshot: &MultiBufferSnapshot) -> Self {
|
||||||
|
let selection_end = selection.head();
|
||||||
|
let line_start = Point::new(selection_end.row, 0);
|
||||||
|
let chars_to_last_position = snapshot
|
||||||
|
.text_summary_for_range::<text::TextSummary, _>(line_start..selection_end)
|
||||||
|
.chars as u32;
|
||||||
|
Self {
|
||||||
|
line: NonZeroU32::new(selection_end.row + 1).expect("added 1"),
|
||||||
|
character: NonZeroU32::new(chars_to_last_position + 1).expect("added 1"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl CursorPosition {
|
impl CursorPosition {
|
||||||
pub fn new(workspace: &Workspace) -> Self {
|
pub fn new(workspace: &Workspace) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -73,21 +97,16 @@ impl CursorPosition {
|
||||||
cursor_position.context = None;
|
cursor_position.context = None;
|
||||||
}
|
}
|
||||||
editor::EditorMode::Full => {
|
editor::EditorMode::Full => {
|
||||||
let mut last_selection = None::<Selection<usize>>;
|
let mut last_selection = None::<Selection<Point>>;
|
||||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
if buffer.excerpts().count() > 0 {
|
if snapshot.excerpts().count() > 0 {
|
||||||
for selection in editor.selections.all::<usize>(cx) {
|
|
||||||
cursor_position.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
|
|
||||||
}) {
|
|
||||||
last_selection = Some(selection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for selection in editor.selections.all::<Point>(cx) {
|
for selection in editor.selections.all::<Point>(cx) {
|
||||||
|
let selection_summary = snapshot
|
||||||
|
.text_summary_for_range::<text::TextSummary, _>(
|
||||||
|
selection.start..selection.end,
|
||||||
|
);
|
||||||
|
cursor_position.selected_count.characters +=
|
||||||
|
selection_summary.chars;
|
||||||
if selection.end != selection.start {
|
if selection.end != selection.start {
|
||||||
cursor_position.selected_count.lines +=
|
cursor_position.selected_count.lines +=
|
||||||
(selection.end.row - selection.start.row) as usize;
|
(selection.end.row - selection.start.row) as usize;
|
||||||
|
@ -95,13 +114,15 @@ impl CursorPosition {
|
||||||
cursor_position.selected_count.lines += 1;
|
cursor_position.selected_count.lines += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if last_selection.as_ref().map_or(true, |last_selection| {
|
||||||
|
selection.id > last_selection.id
|
||||||
|
}) {
|
||||||
|
last_selection = Some(selection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor_position.position = last_selection.and_then(|s| {
|
cursor_position.position = last_selection
|
||||||
buffer
|
.map(|s| UserCaretPosition::at_selection_end(&s, &snapshot));
|
||||||
.point_to_buffer_point(s.head().to_point(&buffer))
|
|
||||||
.map(|(_, point, is_main_buffer)| (point, is_main_buffer))
|
|
||||||
});
|
|
||||||
cursor_position.context = Some(editor.focus_handle(cx));
|
cursor_position.context = Some(editor.focus_handle(cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,16 +183,19 @@ impl CursorPosition {
|
||||||
pub(crate) fn selection_stats(&self) -> &SelectionStats {
|
pub(crate) fn selection_stats(&self) -> &SelectionStats {
|
||||||
&self.selected_count
|
&self.selected_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn position(&self) -> Option<UserCaretPosition> {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for CursorPosition {
|
impl Render for CursorPosition {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
div().when_some(self.position, |el, (position, is_main_buffer)| {
|
div().when_some(self.position, |el, position| {
|
||||||
let mut text = format!(
|
let mut text = format!(
|
||||||
"{}{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
||||||
if is_main_buffer { "" } else { "(deleted) " },
|
position.line, position.character,
|
||||||
position.row + 1,
|
|
||||||
position.column + 1
|
|
||||||
);
|
);
|
||||||
self.write_position(&mut text, cx);
|
self.write_position(&mut text, cx);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
pub mod cursor_position;
|
pub mod cursor_position;
|
||||||
|
|
||||||
use cursor_position::LineIndicatorFormat;
|
use cursor_position::{LineIndicatorFormat, UserCaretPosition};
|
||||||
use editor::{scroll::Autoscroll, Anchor, Editor, MultiBuffer, ToPoint};
|
use editor::{
|
||||||
|
actions::Tab, scroll::Autoscroll, Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||||
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle,
|
||||||
FocusableView, Model, Render, SharedString, Styled, Subscription, View, ViewContext,
|
FocusableView, Model, Render, SharedString, Styled, Subscription, View, ViewContext,
|
||||||
|
@ -9,7 +11,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use text::Point;
|
use text::{Bias, Point};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||||
|
@ -23,7 +25,6 @@ pub fn init(cx: &mut AppContext) {
|
||||||
pub struct GoToLine {
|
pub struct GoToLine {
|
||||||
line_editor: View<Editor>,
|
line_editor: View<Editor>,
|
||||||
active_editor: View<Editor>,
|
active_editor: View<Editor>,
|
||||||
active_buffer: Model<Buffer>,
|
|
||||||
current_text: SharedString,
|
current_text: SharedString,
|
||||||
prev_scroll_position: Option<gpui::Point<f32>>,
|
prev_scroll_position: Option<gpui::Point<f32>>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
@ -67,10 +68,13 @@ impl GoToLine {
|
||||||
active_buffer: Model<Buffer>,
|
active_buffer: Model<Buffer>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (cursor, last_line, scroll_position) = active_editor.update(cx, |editor, cx| {
|
let (user_caret, last_line, scroll_position) = active_editor.update(cx, |editor, cx| {
|
||||||
let cursor = editor.selections.last::<Point>(cx).head();
|
let user_caret = UserCaretPosition::at_selection_end(
|
||||||
let snapshot = active_buffer.read(cx).snapshot();
|
&editor.selections.last::<Point>(cx),
|
||||||
|
&editor.buffer().read(cx).snapshot(cx),
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = active_buffer.read(cx).snapshot();
|
||||||
let last_line = editor
|
let last_line = editor
|
||||||
.buffer()
|
.buffer()
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -80,14 +84,32 @@ impl GoToLine {
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
(cursor, last_line, editor.scroll_position(cx))
|
(user_caret, last_line, editor.scroll_position(cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
let line = cursor.row + 1;
|
let line = user_caret.line.get();
|
||||||
let column = cursor.column + 1;
|
let column = user_caret.character.get();
|
||||||
|
|
||||||
let line_editor = cx.new_view(|cx| {
|
let line_editor = cx.new_view(|cx| {
|
||||||
let mut editor = Editor::single_line(cx);
|
let mut editor = Editor::single_line(cx);
|
||||||
|
let editor_handle = cx.view().downgrade();
|
||||||
|
editor
|
||||||
|
.register_action::<Tab>({
|
||||||
|
move |_, cx| {
|
||||||
|
let Some(editor) = editor_handle.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
if let Some(placeholder_text) = editor.placeholder_text(cx) {
|
||||||
|
if editor.text(cx).is_empty() {
|
||||||
|
let placeholder_text = placeholder_text.to_string();
|
||||||
|
editor.set_text(placeholder_text, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
editor.set_placeholder_text(format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"), cx);
|
editor.set_placeholder_text(format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"), cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
@ -103,7 +125,6 @@ impl GoToLine {
|
||||||
Self {
|
Self {
|
||||||
line_editor,
|
line_editor,
|
||||||
active_editor,
|
active_editor,
|
||||||
active_buffer,
|
|
||||||
current_text: current_text.into(),
|
current_text: current_text.into(),
|
||||||
prev_scroll_position: Some(scroll_position),
|
prev_scroll_position: Some(scroll_position),
|
||||||
_subscriptions: vec![line_editor_change, cx.on_release(Self::release)],
|
_subscriptions: vec![line_editor_change, cx.on_release(Self::release)],
|
||||||
|
@ -141,13 +162,18 @@ impl GoToLine {
|
||||||
fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
|
fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.active_editor.update(cx, |editor, cx| {
|
self.active_editor.update(cx, |editor, cx| {
|
||||||
editor.clear_row_highlights::<GoToLineRowHighlights>();
|
editor.clear_row_highlights::<GoToLineRowHighlights>();
|
||||||
let multibuffer = editor.buffer().read(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
let snapshot = multibuffer.snapshot(cx);
|
let Some(start) = self.anchor_from_query(&snapshot, cx) else {
|
||||||
let Some(start) = self.anchor_from_query(&multibuffer, cx) else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let start_point = start.to_point(&snapshot);
|
let mut start_point = start.to_point(&snapshot);
|
||||||
let end_point = start_point + Point::new(1, 0);
|
start_point.column = 0;
|
||||||
|
// Force non-empty range to ensure the line is highlighted.
|
||||||
|
let mut end_point = snapshot.clip_point(start_point + Point::new(0, 1), Bias::Left);
|
||||||
|
if start_point == end_point {
|
||||||
|
end_point = snapshot.clip_point(start_point + Point::new(1, 0), Bias::Left);
|
||||||
|
}
|
||||||
|
|
||||||
let end = snapshot.anchor_after(end_point);
|
let end = snapshot.anchor_after(end_point);
|
||||||
editor.highlight_rows::<GoToLineRowHighlights>(
|
editor.highlight_rows::<GoToLineRowHighlights>(
|
||||||
start..end,
|
start..end,
|
||||||
|
@ -162,25 +188,49 @@ impl GoToLine {
|
||||||
|
|
||||||
fn anchor_from_query(
|
fn anchor_from_query(
|
||||||
&self,
|
&self,
|
||||||
multibuffer: &MultiBuffer,
|
snapshot: &MultiBufferSnapshot,
|
||||||
cx: &ViewContext<Editor>,
|
cx: &ViewContext<Editor>,
|
||||||
) -> Option<Anchor> {
|
) -> Option<Anchor> {
|
||||||
let (Some(row), column) = self.line_column_from_query(cx) else {
|
let (query_row, query_char) = self.line_and_char_from_query(cx)?;
|
||||||
return None;
|
let row = query_row.saturating_sub(1);
|
||||||
};
|
let character = query_char.unwrap_or(0).saturating_sub(1);
|
||||||
let point = Point::new(row.saturating_sub(1), column.unwrap_or(0).saturating_sub(1));
|
|
||||||
multibuffer.buffer_point_to_anchor(&self.active_buffer, point, cx)
|
let start_offset = Point::new(row, 0).to_offset(snapshot);
|
||||||
|
const MAX_BYTES_IN_UTF_8: u32 = 4;
|
||||||
|
let max_end_offset = snapshot
|
||||||
|
.clip_point(
|
||||||
|
Point::new(row, character * MAX_BYTES_IN_UTF_8 + 1),
|
||||||
|
Bias::Right,
|
||||||
|
)
|
||||||
|
.to_offset(snapshot);
|
||||||
|
|
||||||
|
let mut chars_to_iterate = character;
|
||||||
|
let mut end_offset = start_offset;
|
||||||
|
'outer: for text_chunk in snapshot.text_for_range(start_offset..max_end_offset) {
|
||||||
|
let mut offset_increment = 0;
|
||||||
|
for c in text_chunk.chars() {
|
||||||
|
if chars_to_iterate == 0 {
|
||||||
|
end_offset += offset_increment;
|
||||||
|
break 'outer;
|
||||||
|
} else {
|
||||||
|
chars_to_iterate -= 1;
|
||||||
|
offset_increment += c.len_utf8();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end_offset += offset_increment;
|
||||||
|
}
|
||||||
|
Some(snapshot.anchor_before(snapshot.clip_offset(end_offset, Bias::Left)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_column_from_query(&self, cx: &AppContext) -> (Option<u32>, Option<u32>) {
|
fn line_and_char_from_query(&self, cx: &AppContext) -> Option<(u32, Option<u32>)> {
|
||||||
let input = self.line_editor.read(cx).text(cx);
|
let input = self.line_editor.read(cx).text(cx);
|
||||||
let mut components = input
|
let mut components = input
|
||||||
.splitn(2, FILE_ROW_COLUMN_DELIMITER)
|
.splitn(2, FILE_ROW_COLUMN_DELIMITER)
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.fuse();
|
.fuse();
|
||||||
let row = components.next().and_then(|row| row.parse::<u32>().ok());
|
let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
|
||||||
let column = components.next().and_then(|col| col.parse::<u32>().ok());
|
let column = components.next().and_then(|col| col.parse::<u32>().ok());
|
||||||
(row, column)
|
Some((row, column))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -189,8 +239,8 @@ impl GoToLine {
|
||||||
|
|
||||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
self.active_editor.update(cx, |editor, cx| {
|
self.active_editor.update(cx, |editor, cx| {
|
||||||
let multibuffer = editor.buffer().read(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
let Some(start) = self.anchor_from_query(&multibuffer, cx) else {
|
let Some(start) = self.anchor_from_query(&snapshot, cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||||
|
@ -207,15 +257,13 @@ impl GoToLine {
|
||||||
|
|
||||||
impl Render for GoToLine {
|
impl Render for GoToLine {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let mut help_text = self.current_text.clone();
|
let help_text = match self.line_and_char_from_query(cx) {
|
||||||
let query = self.line_column_from_query(cx);
|
Some((line, Some(character))) => {
|
||||||
if let Some(line) = query.0 {
|
format!("Go to line {line}, character {character}").into()
|
||||||
if let Some(column) = query.1 {
|
|
||||||
help_text = format!("Go to line {line}, column {column}").into();
|
|
||||||
} else {
|
|
||||||
help_text = format!("Go to line {line}").into();
|
|
||||||
}
|
}
|
||||||
}
|
Some((line, None)) => format!("Go to line {line}").into(),
|
||||||
|
None => self.current_text.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.w(rems(24.))
|
.w(rems(24.))
|
||||||
|
@ -244,13 +292,13 @@ impl Render for GoToLine {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use cursor_position::{CursorPosition, SelectionStats};
|
use cursor_position::{CursorPosition, SelectionStats, UserCaretPosition};
|
||||||
use editor::actions::SelectAll;
|
use editor::actions::{MoveRight, MoveToBeginning, SelectAll};
|
||||||
use gpui::{TestAppContext, VisualTestContext};
|
use gpui::{TestAppContext, VisualTestContext};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use project::{FakeFs, Project};
|
use project::{FakeFs, Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{num::NonZeroU32, sync::Arc, time::Duration};
|
||||||
use workspace::{AppState, Workspace};
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -439,6 +487,197 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_unicode_line_numbers(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let text = "ēlo你好";
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"a.rs": text
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.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();
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.move_to_beginning(&MoveToBeginning, cx)
|
||||||
|
});
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(200));
|
||||||
|
assert_eq!(
|
||||||
|
user_caret_position(1, 1),
|
||||||
|
current_position(&workspace, cx),
|
||||||
|
"Beginning of the line should be at first line, before any characters"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (i, c) in text.chars().enumerate() {
|
||||||
|
let i = i as u32 + 1;
|
||||||
|
editor.update(cx, |editor, cx| editor.move_right(&MoveRight, cx));
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(200));
|
||||||
|
assert_eq!(
|
||||||
|
user_caret_position(1, i + 1),
|
||||||
|
current_position(&workspace, cx),
|
||||||
|
"Wrong position for char '{c}' in string '{text}'",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| editor.move_right(&MoveRight, cx));
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(200));
|
||||||
|
assert_eq!(
|
||||||
|
user_caret_position(1, text.chars().count() as u32 + 1),
|
||||||
|
current_position(&workspace, cx),
|
||||||
|
"After reaching the end of the text, position should not change when moving right"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_go_into_unicode(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let text = "ēlo你好";
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"a.rs": text
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.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();
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.move_to_beginning(&MoveToBeginning, cx)
|
||||||
|
});
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(200));
|
||||||
|
assert_eq!(user_caret_position(1, 1), current_position(&workspace, cx));
|
||||||
|
|
||||||
|
for (i, c) in text.chars().enumerate() {
|
||||||
|
let i = i as u32 + 1;
|
||||||
|
let point = user_caret_position(1, i + 1);
|
||||||
|
go_to_point(point, user_caret_position(1, i), &workspace, cx);
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(200));
|
||||||
|
assert_eq!(
|
||||||
|
point,
|
||||||
|
current_position(&workspace, cx),
|
||||||
|
"When going to {point:?}, expecting the cursor to be at char '{c}' in string '{text}'",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
go_to_point(
|
||||||
|
user_caret_position(111, 222),
|
||||||
|
user_caret_position(1, text.chars().count() as u32 + 1),
|
||||||
|
&workspace,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(200));
|
||||||
|
assert_eq!(
|
||||||
|
user_caret_position(1, text.chars().count() as u32 + 1),
|
||||||
|
current_position(&workspace, cx),
|
||||||
|
"When going into too large point, should go to the end of the text"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_position(
|
||||||
|
workspace: &View<Workspace>,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> UserCaretPosition {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.status_bar()
|
||||||
|
.read(cx)
|
||||||
|
.item_of_type::<CursorPosition>()
|
||||||
|
.expect("missing cursor position item")
|
||||||
|
.read(cx)
|
||||||
|
.position()
|
||||||
|
.expect("No position found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_caret_position(line: u32, character: u32) -> UserCaretPosition {
|
||||||
|
UserCaretPosition {
|
||||||
|
line: NonZeroU32::new(line).unwrap(),
|
||||||
|
character: NonZeroU32::new(character).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn go_to_point(
|
||||||
|
new_point: UserCaretPosition,
|
||||||
|
expected_placeholder: UserCaretPosition,
|
||||||
|
workspace: &View<Workspace>,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) {
|
||||||
|
let go_to_line_view = open_go_to_line_view(workspace, cx);
|
||||||
|
go_to_line_view.update(cx, |go_to_line_view, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
go_to_line_view
|
||||||
|
.line_editor
|
||||||
|
.read(cx)
|
||||||
|
.placeholder_text(cx)
|
||||||
|
.expect("No placeholder text"),
|
||||||
|
format!(
|
||||||
|
"{}:{}",
|
||||||
|
expected_placeholder.line, expected_placeholder.character
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cx.simulate_input(&format!("{}:{}", new_point.line, new_point.character));
|
||||||
|
cx.dispatch_action(menu::Confirm);
|
||||||
|
}
|
||||||
|
|
||||||
fn open_go_to_line_view(
|
fn open_go_to_line_view(
|
||||||
workspace: &View<Workspace>,
|
workspace: &View<Workspace>,
|
||||||
cx: &mut VisualTestContext,
|
cx: &mut VisualTestContext,
|
||||||
|
|
|
@ -162,9 +162,11 @@ impl<'a> ChunkSlice<'a> {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn text_summary(&self) -> TextSummary {
|
pub fn text_summary(&self) -> TextSummary {
|
||||||
let (longest_row, longest_row_chars) = self.longest_row();
|
let mut chars = 0;
|
||||||
|
let (longest_row, longest_row_chars) = self.longest_row(&mut chars);
|
||||||
TextSummary {
|
TextSummary {
|
||||||
len: self.len(),
|
len: self.len(),
|
||||||
|
chars,
|
||||||
len_utf16: self.len_utf16(),
|
len_utf16: self.len_utf16(),
|
||||||
lines: self.lines(),
|
lines: self.lines(),
|
||||||
first_line_chars: self.first_line_chars(),
|
first_line_chars: self.first_line_chars(),
|
||||||
|
@ -229,16 +231,19 @@ impl<'a> ChunkSlice<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the longest row in the chunk and its length in characters.
|
/// Get the longest row in the chunk and its length in characters.
|
||||||
|
/// Calculate the total number of characters in the chunk along the way.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn longest_row(&self) -> (u32, u32) {
|
pub fn longest_row(&self, total_chars: &mut usize) -> (u32, u32) {
|
||||||
let mut chars = self.chars;
|
let mut chars = self.chars;
|
||||||
let mut newlines = self.newlines;
|
let mut newlines = self.newlines;
|
||||||
|
*total_chars = 0;
|
||||||
let mut row = 0;
|
let mut row = 0;
|
||||||
let mut longest_row = 0;
|
let mut longest_row = 0;
|
||||||
let mut longest_row_chars = 0;
|
let mut longest_row_chars = 0;
|
||||||
while newlines > 0 {
|
while newlines > 0 {
|
||||||
let newline_ix = newlines.trailing_zeros();
|
let newline_ix = newlines.trailing_zeros();
|
||||||
let row_chars = (chars & ((1 << newline_ix) - 1)).count_ones() as u8;
|
let row_chars = (chars & ((1 << newline_ix) - 1)).count_ones() as u8;
|
||||||
|
*total_chars += usize::from(row_chars);
|
||||||
if row_chars > longest_row_chars {
|
if row_chars > longest_row_chars {
|
||||||
longest_row = row;
|
longest_row = row;
|
||||||
longest_row_chars = row_chars;
|
longest_row_chars = row_chars;
|
||||||
|
@ -249,9 +254,11 @@ impl<'a> ChunkSlice<'a> {
|
||||||
chars >>= newline_ix;
|
chars >>= newline_ix;
|
||||||
chars >>= 1;
|
chars >>= 1;
|
||||||
row += 1;
|
row += 1;
|
||||||
|
*total_chars += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let row_chars = chars.count_ones() as u8;
|
let row_chars = chars.count_ones() as u8;
|
||||||
|
*total_chars += usize::from(row_chars);
|
||||||
if row_chars > longest_row_chars {
|
if row_chars > longest_row_chars {
|
||||||
(row, row_chars as u32)
|
(row, row_chars as u32)
|
||||||
} else {
|
} else {
|
||||||
|
@ -908,7 +915,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify longest row
|
// Verify longest row
|
||||||
let (longest_row, longest_chars) = chunk.longest_row();
|
let (longest_row, longest_chars) = chunk.longest_row(&mut 0);
|
||||||
let mut max_chars = 0;
|
let mut max_chars = 0;
|
||||||
let mut current_row = 0;
|
let mut current_row = 0;
|
||||||
let mut current_chars = 0;
|
let mut current_chars = 0;
|
||||||
|
|
|
@ -965,8 +965,10 @@ impl sum_tree::Summary for ChunkSummary {
|
||||||
/// Summary of a string of text.
|
/// Summary of a string of text.
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||||
pub struct TextSummary {
|
pub struct TextSummary {
|
||||||
/// Length in UTF-8
|
/// Length in bytes.
|
||||||
pub len: usize,
|
pub len: usize,
|
||||||
|
/// Length in UTF-8.
|
||||||
|
pub chars: usize,
|
||||||
/// Length in UTF-16 code units
|
/// Length in UTF-16 code units
|
||||||
pub len_utf16: OffsetUtf16,
|
pub len_utf16: OffsetUtf16,
|
||||||
/// A point representing the number of lines and the length of the last line
|
/// A point representing the number of lines and the length of the last line
|
||||||
|
@ -994,6 +996,7 @@ impl TextSummary {
|
||||||
pub fn newline() -> Self {
|
pub fn newline() -> Self {
|
||||||
Self {
|
Self {
|
||||||
len: 1,
|
len: 1,
|
||||||
|
chars: 1,
|
||||||
len_utf16: OffsetUtf16(1),
|
len_utf16: OffsetUtf16(1),
|
||||||
first_line_chars: 0,
|
first_line_chars: 0,
|
||||||
last_line_chars: 0,
|
last_line_chars: 0,
|
||||||
|
@ -1022,7 +1025,9 @@ impl<'a> From<&'a str> for TextSummary {
|
||||||
let mut last_line_len_utf16 = 0;
|
let mut last_line_len_utf16 = 0;
|
||||||
let mut longest_row = 0;
|
let mut longest_row = 0;
|
||||||
let mut longest_row_chars = 0;
|
let mut longest_row_chars = 0;
|
||||||
|
let mut chars = 0;
|
||||||
for c in text.chars() {
|
for c in text.chars() {
|
||||||
|
chars += 1;
|
||||||
len_utf16.0 += c.len_utf16();
|
len_utf16.0 += c.len_utf16();
|
||||||
|
|
||||||
if c == '\n' {
|
if c == '\n' {
|
||||||
|
@ -1047,6 +1052,7 @@ impl<'a> From<&'a str> for TextSummary {
|
||||||
|
|
||||||
TextSummary {
|
TextSummary {
|
||||||
len: text.len(),
|
len: text.len(),
|
||||||
|
chars,
|
||||||
len_utf16,
|
len_utf16,
|
||||||
lines,
|
lines,
|
||||||
first_line_chars,
|
first_line_chars,
|
||||||
|
@ -1103,6 +1109,7 @@ impl<'a> ops::AddAssign<&'a Self> for TextSummary {
|
||||||
self.last_line_len_utf16 = other.last_line_len_utf16;
|
self.last_line_len_utf16 = other.last_line_len_utf16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.chars += other.chars;
|
||||||
self.len += other.len;
|
self.len += other.len;
|
||||||
self.len_utf16 += other.len_utf16;
|
self.len_utf16 += other.len_utf16;
|
||||||
self.lines += other.lines;
|
self.lines += other.lines;
|
||||||
|
|
|
@ -261,10 +261,25 @@ fn test_text_summary_for_range() {
|
||||||
BufferId::new(1).unwrap(),
|
BufferId::new(1).unwrap(),
|
||||||
"ab\nefg\nhklm\nnopqrs\ntuvwxyz".into(),
|
"ab\nefg\nhklm\nnopqrs\ntuvwxyz".into(),
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
buffer.text_summary_for_range::<TextSummary, _>(0..2),
|
||||||
|
TextSummary {
|
||||||
|
len: 2,
|
||||||
|
chars: 2,
|
||||||
|
len_utf16: OffsetUtf16(2),
|
||||||
|
lines: Point::new(0, 2),
|
||||||
|
first_line_chars: 2,
|
||||||
|
last_line_chars: 2,
|
||||||
|
last_line_len_utf16: 2,
|
||||||
|
longest_row: 0,
|
||||||
|
longest_row_chars: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer.text_summary_for_range::<TextSummary, _>(1..3),
|
buffer.text_summary_for_range::<TextSummary, _>(1..3),
|
||||||
TextSummary {
|
TextSummary {
|
||||||
len: 2,
|
len: 2,
|
||||||
|
chars: 2,
|
||||||
len_utf16: OffsetUtf16(2),
|
len_utf16: OffsetUtf16(2),
|
||||||
lines: Point::new(1, 0),
|
lines: Point::new(1, 0),
|
||||||
first_line_chars: 1,
|
first_line_chars: 1,
|
||||||
|
@ -278,6 +293,7 @@ fn test_text_summary_for_range() {
|
||||||
buffer.text_summary_for_range::<TextSummary, _>(1..12),
|
buffer.text_summary_for_range::<TextSummary, _>(1..12),
|
||||||
TextSummary {
|
TextSummary {
|
||||||
len: 11,
|
len: 11,
|
||||||
|
chars: 11,
|
||||||
len_utf16: OffsetUtf16(11),
|
len_utf16: OffsetUtf16(11),
|
||||||
lines: Point::new(3, 0),
|
lines: Point::new(3, 0),
|
||||||
first_line_chars: 1,
|
first_line_chars: 1,
|
||||||
|
@ -291,6 +307,7 @@ fn test_text_summary_for_range() {
|
||||||
buffer.text_summary_for_range::<TextSummary, _>(0..20),
|
buffer.text_summary_for_range::<TextSummary, _>(0..20),
|
||||||
TextSummary {
|
TextSummary {
|
||||||
len: 20,
|
len: 20,
|
||||||
|
chars: 20,
|
||||||
len_utf16: OffsetUtf16(20),
|
len_utf16: OffsetUtf16(20),
|
||||||
lines: Point::new(4, 1),
|
lines: Point::new(4, 1),
|
||||||
first_line_chars: 2,
|
first_line_chars: 2,
|
||||||
|
@ -304,6 +321,7 @@ fn test_text_summary_for_range() {
|
||||||
buffer.text_summary_for_range::<TextSummary, _>(0..22),
|
buffer.text_summary_for_range::<TextSummary, _>(0..22),
|
||||||
TextSummary {
|
TextSummary {
|
||||||
len: 22,
|
len: 22,
|
||||||
|
chars: 22,
|
||||||
len_utf16: OffsetUtf16(22),
|
len_utf16: OffsetUtf16(22),
|
||||||
lines: Point::new(4, 3),
|
lines: Point::new(4, 3),
|
||||||
first_line_chars: 2,
|
first_line_chars: 2,
|
||||||
|
@ -317,6 +335,7 @@ fn test_text_summary_for_range() {
|
||||||
buffer.text_summary_for_range::<TextSummary, _>(7..22),
|
buffer.text_summary_for_range::<TextSummary, _>(7..22),
|
||||||
TextSummary {
|
TextSummary {
|
||||||
len: 15,
|
len: 15,
|
||||||
|
chars: 15,
|
||||||
len_utf16: OffsetUtf16(15),
|
len_utf16: OffsetUtf16(15),
|
||||||
lines: Point::new(2, 3),
|
lines: Point::new(2, 3),
|
||||||
first_line_chars: 4,
|
first_line_chars: 4,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue