From a801a4aeef93e24d235da51a9a1fdbe9f5d1d6d1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 27 Sep 2023 13:16:32 -0600 Subject: [PATCH 01/15] Remove some unnecessary Eqs --- .cargo/config.toml | 2 +- crates/language/src/buffer.rs | 4 ++-- crates/text/src/selection.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 9da6b3be080072d89d16a199e2d60d527eeacd07..e22bdb0f2c70a1ffda714674253cc533e9e7c1d1 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ xtask = "run --package xtask --" [build] # v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0"] +rustflags = ["-C", "symbol-mangling-version=v0", "-C", "link-arg=-fuse-ld=/opt/homebrew/Cellar/llvm/16.0.6/bin/ld64.lld"] diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 207c41e7cdf21a8ec4d61232e071296184b52a5f..19e5e290b9ce444cc3901840fca09449b730341b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -159,7 +159,7 @@ pub struct CodeAction { pub lsp_action: lsp::CodeAction, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq)] pub enum Operation { Buffer(text::Operation), @@ -182,7 +182,7 @@ pub enum Operation { }, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq)] pub enum Event { Operation(Operation), Edited, diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 205c27239d90699257816b5edfe3f6e38fa34dec..60d5e2f1c437086c246f7e4c94ba6536e0f80840 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -2,14 +2,14 @@ use crate::{Anchor, BufferSnapshot, TextDimension}; use std::cmp::Ordering; use std::ops::Range; -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum SelectionGoal { None, Column(u32), ColumnRange { start: u32, end: u32 }, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Selection { pub id: usize, pub start: T, From dacc8cb5f47ae8272afaf560979c7eb6d67e3354 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 27 Sep 2023 15:10:50 -0600 Subject: [PATCH 02/15] Begin to use pixels for column selection For zed-industries/community#759 For zed-industries/community#1966 Co-Authored-By: Julia --- crates/editor/src/display_map.rs | 381 ++++++++++++++++++++++++------- crates/editor/src/editor.rs | 51 ++++- crates/editor/src/element.rs | 59 +---- crates/editor/src/movement.rs | 50 +++- crates/text/src/selection.rs | 2 + 5 files changed, 382 insertions(+), 161 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index d97db9695ac4052f647a58d90fa1f23b4188004d..3d13447fc23749b3daea4049c9b76ef11fb0ff2c 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,22 +5,24 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer, - MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, EditorStyle, InlayId, + MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; use gpui::{ color::Color, - fonts::{FontId, HighlightStyle}, - Entity, ModelContext, ModelHandle, + fonts::{FontId, HighlightStyle, Underline}, + text_layout::{Line, RunStyle}, + AppContext, Entity, FontCache, ModelContext, ModelHandle, TextLayoutCache, }; use inlay_map::InlayMap; use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, }; -use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; +use lsp::DiagnosticSeverity; +use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; use wrap_map::WrapMap; @@ -316,6 +318,12 @@ pub struct Highlights<'a> { pub suggestion_highlight_style: Option, } +pub struct HighlightedChunk<'a> { + pub chunk: &'a str, + pub style: Option, + pub is_tab: bool, +} + pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, pub fold_snapshot: fold_map::FoldSnapshot, @@ -485,7 +493,7 @@ impl DisplaySnapshot { language_aware: bool, inlay_highlight_style: Option, suggestion_highlight_style: Option, - ) -> DisplayChunks<'_> { + ) -> DisplayChunks<'a> { self.block_snapshot.chunks( display_rows, language_aware, @@ -498,6 +506,174 @@ impl DisplaySnapshot { ) } + pub fn highlighted_chunks<'a>( + &'a self, + display_rows: Range, + style: &'a EditorStyle, + ) -> impl Iterator> { + self.chunks( + display_rows, + true, + Some(style.theme.hint), + Some(style.theme.suggestion), + ) + .map(|chunk| { + let mut highlight_style = chunk + .syntax_highlight_id + .and_then(|id| id.style(&style.syntax)); + + if let Some(chunk_highlight) = chunk.highlight_style { + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(chunk_highlight); + } else { + highlight_style = Some(chunk_highlight); + } + } + + let mut diagnostic_highlight = HighlightStyle::default(); + + if chunk.is_unnecessary { + diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); + } + + if let Some(severity) = chunk.diagnostic_severity { + // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. + if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { + let diagnostic_style = super::diagnostic_style(severity, true, style); + diagnostic_highlight.underline = Some(Underline { + color: Some(diagnostic_style.message.text.color), + thickness: 1.0.into(), + squiggly: true, + }); + } + } + + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(diagnostic_highlight); + } else { + highlight_style = Some(diagnostic_highlight); + } + + HighlightedChunk { + chunk: chunk.text, + style: highlight_style, + is_tab: chunk.is_tab, + } + }) + } + + fn layout_line_for_row( + &self, + display_row: u32, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, + ) -> Line { + let mut styles = Vec::new(); + let mut line = String::new(); + + 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 { + editor_style + .text + .clone() + .highlight(style, 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); + text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles) + } + + pub fn x_for_point( + &self, + display_point: DisplayPoint, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, + ) -> f32 { + let layout_line = self.layout_line_for_row( + display_point.row(), + font_cache, + text_layout_cache, + editor_style, + ); + layout_line.x_for_index(display_point.column() as usize) + } + + pub fn column_for_x( + &self, + display_row: u32, + x_coordinate: f32, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, + ) -> Option { + let layout_line = + self.layout_line_for_row(display_row, font_cache, text_layout_cache, editor_style); + layout_line.index_for_x(x_coordinate).map(|c| c 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, @@ -869,17 +1045,21 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat #[cfg(test)] pub mod tests { use super::*; - use crate::{movement, test::marked_display_snapshot}; + use crate::{ + movement, + test::{editor_test_context::EditorTestContext, marked_display_snapshot}, + }; use gpui::{color::Color, elements::*, test::observe, AppContext}; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, Buffer, Language, LanguageConfig, SelectionGoal, }; + use project::Project; use rand::{prelude::*, Rng}; use settings::SettingsStore; use smol::stream::StreamExt; use std::{env, sync::Arc}; - use theme::SyntaxTheme; + use theme::{SyntaxTheme, Theme}; use util::test::{marked_text_ranges, sample_text}; use Bias::*; @@ -1148,95 +1328,119 @@ pub mod tests { } #[gpui::test(retries = 5)] - fn test_soft_wraps(cx: &mut AppContext) { + async fn test_soft_wraps(cx: &mut gpui::TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - init_test(cx, |_| {}); + cx.update(|cx| { + init_test(cx, |_| {}); + }); - let font_cache = cx.font_cache(); + let mut cx = EditorTestContext::new(cx).await; + let editor = cx.editor.clone(); + let window = cx.window.clone(); - let family_id = font_cache - .load_family(&["Helvetica"], &Default::default()) - .unwrap(); - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 12.0; - let wrap_width = Some(64.); + cx.update_window(window, |cx| { + let editor_style = editor.read(&cx).style(cx); - let text = "one two three four five\nsix seven eight"; - let buffer = MultiBuffer::build_simple(text, cx); - let map = cx.add_model(|cx| { - DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx) - }); + let font_cache = cx.font_cache().clone(); - let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!( - snapshot.text_chunks(0).collect::(), - "one two \nthree four \nfive\nsix seven \neight" - ); - assert_eq!( - snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left), - DisplayPoint::new(0, 7) - ); - assert_eq!( - snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right), - DisplayPoint::new(1, 0) - ); - assert_eq!( - movement::right(&snapshot, DisplayPoint::new(0, 7)), - DisplayPoint::new(1, 0) - ); - assert_eq!( - movement::left(&snapshot, DisplayPoint::new(1, 0)), - DisplayPoint::new(0, 7) - ); - assert_eq!( - movement::up( - &snapshot, - DisplayPoint::new(1, 10), - SelectionGoal::None, - false - ), - (DisplayPoint::new(0, 7), SelectionGoal::Column(10)) - ); - assert_eq!( - movement::down( - &snapshot, - DisplayPoint::new(0, 7), - SelectionGoal::Column(10), - false - ), - (DisplayPoint::new(1, 10), SelectionGoal::Column(10)) - ); - assert_eq!( - movement::down( - &snapshot, + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 12.0; + let wrap_width = Some(64.); + + let text = "one two three four five\nsix seven eight"; + let buffer = MultiBuffer::build_simple(text, cx); + let map = cx.add_model(|cx| { + DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx) + }); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(0).collect::(), + "one two \nthree four \nfive\nsix seven \neight" + ); + assert_eq!( + snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left), + DisplayPoint::new(0, 7) + ); + assert_eq!( + snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right), + DisplayPoint::new(1, 0) + ); + assert_eq!( + movement::right(&snapshot, DisplayPoint::new(0, 7)), + DisplayPoint::new(1, 0) + ); + assert_eq!( + movement::left(&snapshot, DisplayPoint::new(1, 0)), + DisplayPoint::new(0, 7) + ); + + let x = snapshot.x_for_point( DisplayPoint::new(1, 10), - SelectionGoal::Column(10), - false - ), - (DisplayPoint::new(2, 4), SelectionGoal::Column(10)) - ); + cx.font_cache(), + cx.text_layout_cache(), + &editor_style, + ); + dbg!(x); + assert_eq!( + movement::up( + &snapshot, + DisplayPoint::new(1, 10), + SelectionGoal::None, + false, + cx.font_cache(), + cx.text_layout_cache(), + &editor_style, + ), + ( + DisplayPoint::new(0, 7), + SelectionGoal::HorizontalPosition(x) + ) + ); + assert_eq!( + movement::down( + &snapshot, + DisplayPoint::new(0, 7), + SelectionGoal::Column(10), + false + ), + (DisplayPoint::new(1, 10), SelectionGoal::Column(10)) + ); + assert_eq!( + movement::down( + &snapshot, + DisplayPoint::new(1, 10), + SelectionGoal::Column(10), + false + ), + (DisplayPoint::new(2, 4), SelectionGoal::Column(10)) + ); - let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); - buffer.update(cx, |buffer, cx| { - buffer.edit([(ix..ix, "and ")], None, cx); - }); + let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.edit([(ix..ix, "and ")], None, cx); + }); - let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!( - snapshot.text_chunks(1).collect::(), - "three four \nfive\nsix and \nseven eight" - ); + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(1).collect::(), + "three four \nfive\nsix and \nseven eight" + ); - // Re-wrap on font size changes - map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx)); + // Re-wrap on font size changes + map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx)); - let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!( - snapshot.text_chunks(1).collect::(), - "three \nfour five\nsix and \nseven \neight" - ) + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(1).collect::(), + "three \nfour five\nsix and \nseven \neight" + ) + }); } #[gpui::test] @@ -1731,6 +1935,9 @@ pub mod tests { cx.foreground().forbid_parking(); cx.set_global(SettingsStore::test(cx)); language::init(cx); + crate::init(cx); + Project::init_settings(cx); + theme::init((), cx); cx.update_global::(|store, cx| { store.update_user_settings::(cx, f); }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 24ffa64a6af73aef827cea17757e5c0e6a4e94c2..bf1aa2e6b5784c4ff4d684d4a403e0bbc622de53 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -48,9 +48,9 @@ use gpui::{ impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, - serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, - Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + serde_json, text_layout, 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}; @@ -5274,13 +5274,25 @@ impl Editor { return; } + let font_cache = cx.font_cache().clone(); + let text_layout_cache = cx.text_layout_cache().clone(); + let editor_style = self.style(cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } - let (cursor, goal) = movement::up(map, selection.start, selection.goal, false); + let (cursor, goal) = movement::up( + map, + selection.start, + selection.goal, + false, + &font_cache, + &text_layout_cache, + &editor_style, + ); selection.collapse_to(cursor, goal); }); }) @@ -5308,22 +5320,47 @@ impl Editor { Autoscroll::fit() }; + let font_cache = cx.font_cache().clone(); + let text_layout = cx.text_layout_cache().clone(); + let editor_style = self.style(cx); + self.change_selections(Some(autoscroll), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } - let (cursor, goal) = - movement::up_by_rows(map, selection.end, row_count, selection.goal, false); + let (cursor, goal) = movement::up_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + &font_cache, + &text_layout, + &editor_style, + ); selection.collapse_to(cursor, goal); }); }); } pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { + let font_cache = cx.font_cache().clone(); + let text_layout = cx.text_layout_cache().clone(); + let editor_style = self.style(cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false)) + s.move_heads_with(|map, head, goal| { + movement::up( + map, + head, + goal, + false, + &font_cache, + &text_layout, + &editor_style, + ) + }) }) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 924d66c21c5532fa79a2abb0af5ac1116cde463e..24cbadfd37927266e0395a4a8c9589bb9d0ab554 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -1584,56 +1584,7 @@ impl EditorElement { .collect() } else { let style = &self.style; - let chunks = snapshot - .chunks( - rows.clone(), - true, - Some(style.theme.hint), - Some(style.theme.suggestion), - ) - .map(|chunk| { - let mut highlight_style = chunk - .syntax_highlight_id - .and_then(|id| id.style(&style.syntax)); - - if let Some(chunk_highlight) = chunk.highlight_style { - if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(chunk_highlight); - } else { - highlight_style = Some(chunk_highlight); - } - } - - let mut diagnostic_highlight = HighlightStyle::default(); - - if chunk.is_unnecessary { - diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); - } - - if let Some(severity) = chunk.diagnostic_severity { - // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. - if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { - let diagnostic_style = super::diagnostic_style(severity, true, style); - diagnostic_highlight.underline = Some(Underline { - color: Some(diagnostic_style.message.text.color), - thickness: 1.0.into(), - squiggly: true, - }); - } - } - - if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(diagnostic_highlight); - } else { - highlight_style = Some(diagnostic_highlight); - } - - HighlightedChunk { - chunk: chunk.text, - style: highlight_style, - is_tab: chunk.is_tab, - } - }); + let chunks = snapshot.highlighted_chunks(rows.clone(), style); LineWithInvisibles::from_chunks( chunks, @@ -1870,12 +1821,6 @@ impl EditorElement { } } -struct HighlightedChunk<'a> { - chunk: &'a str, - style: Option, - is_tab: bool, -} - #[derive(Debug)] pub struct LineWithInvisibles { pub line: Line, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 974af4bc24070ffb46870badbcb57915a802c1c7..3403790681dbc24ba71a69d45b3972a97157254b 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,5 +1,6 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{char_kind, CharKind, ToOffset, ToPoint}; +use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; +use gpui::{FontCache, TextLayoutCache, WindowContext}; use language::Point; use std::ops::Range; @@ -47,8 +48,20 @@ pub fn up( start: DisplayPoint, goal: SelectionGoal, preserve_column_at_start: bool, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, ) -> (DisplayPoint, SelectionGoal) { - up_by_rows(map, start, 1, goal, preserve_column_at_start) + up_by_rows( + map, + start, + 1, + goal, + preserve_column_at_start, + font_cache, + text_layout_cache, + editor_style, + ) } pub fn down( @@ -66,11 +79,14 @@ pub fn up_by_rows( row_count: u32, goal: SelectionGoal, preserve_column_at_start: bool, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = match goal { - SelectionGoal::Column(column) => column, - SelectionGoal::ColumnRange { end, .. } => end, - _ => map.column_to_chars(start.row(), start.column()), + let mut goal_x = match goal { + SelectionGoal::HorizontalPosition(x) => x, + SelectionGoal::HorizontalRange { end, .. } => end, + _ => map.x_for_point(start, font_cache, text_layout_cache, editor_style), }; let prev_row = start.row().saturating_sub(row_count); @@ -79,19 +95,27 @@ pub fn up_by_rows( Bias::Left, ); if point.row() < start.row() { - *point.column_mut() = map.column_from_chars(point.row(), goal_column); + *point.column_mut() = map + .column_for_x( + point.row(), + goal_x, + font_cache, + text_layout_cache, + editor_style, + ) + .unwrap_or(point.column()); } else if preserve_column_at_start { return (start, goal); } else { point = DisplayPoint::new(0, 0); - goal_column = 0; + goal_x = 0.0; } let mut clipped_point = map.clip_point(point, Bias::Left); if clipped_point.row() < point.row() { clipped_point = map.clip_point(point, Bias::Right); } - (clipped_point, SelectionGoal::Column(goal_column)) + (clipped_point, SelectionGoal::HorizontalPosition(goal_x)) } pub fn down_by_rows( @@ -692,6 +716,7 @@ mod tests { #[gpui::test] fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) { + /* init_test(cx); let family_id = cx @@ -727,6 +752,7 @@ mod tests { cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); // Can't move up into the first excerpt's header @@ -737,7 +763,10 @@ mod tests { SelectionGoal::Column(2), false ), - (DisplayPoint::new(2, 0), SelectionGoal::Column(0)), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), ); assert_eq!( up( @@ -808,6 +837,7 @@ mod tests { ), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); + */ } fn init_test(cx: &mut gpui::AppContext) { diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 60d5e2f1c437086c246f7e4c94ba6536e0f80840..38831f92c238f22c7f257763cc557856fdc3e604 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -5,6 +5,8 @@ use std::ops::Range; #[derive(Copy, Clone, Debug, PartialEq)] pub enum SelectionGoal { None, + HorizontalPosition(f32), + HorizontalRange { start: f32, end: f32 }, Column(u32), ColumnRange { start: u32, end: u32 }, } From e7badb38e96ffedbf9c24780d671b2d6c8cc7cdd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 28 Sep 2023 20:35:06 -0600 Subject: [PATCH 03/15] Refactor to pass a TextLayoutDetails around --- crates/editor/src/display_map.rs | 44 +++++++++++------------------- crates/editor/src/editor.rs | 31 +++++---------------- crates/editor/src/movement.rs | 46 ++++++++++++++++++-------------- 3 files changed, 48 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 3d13447fc23749b3daea4049c9b76ef11fb0ff2c..45b4c0abede8e3cfd592771a935de0d4512db96b 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,8 +5,8 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, EditorStyle, InlayId, - MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, + EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; @@ -565,9 +565,11 @@ impl DisplaySnapshot { fn layout_line_for_row( &self, display_row: u32, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + TextLayoutDetails { + font_cache, + text_layout_cache, + editor_style, + }: &TextLayoutDetails, ) -> Line { let mut styles = Vec::new(); let mut line = String::new(); @@ -605,16 +607,9 @@ impl DisplaySnapshot { pub fn x_for_point( &self, display_point: DisplayPoint, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + text_layout_details: &TextLayoutDetails, ) -> f32 { - let layout_line = self.layout_line_for_row( - display_point.row(), - font_cache, - text_layout_cache, - editor_style, - ); + let layout_line = self.layout_line_for_row(display_point.row(), text_layout_details); layout_line.x_for_index(display_point.column() as usize) } @@ -622,12 +617,9 @@ impl DisplaySnapshot { &self, display_row: u32, x_coordinate: f32, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + text_layout_details: &TextLayoutDetails, ) -> Option { - let layout_line = - self.layout_line_for_row(display_row, font_cache, text_layout_cache, editor_style); + let layout_line = self.layout_line_for_row(display_row, text_layout_details); layout_line.index_for_x(x_coordinate).map(|c| c as u32) } @@ -1339,7 +1331,8 @@ pub mod tests { let window = cx.window.clone(); cx.update_window(window, |cx| { - let editor_style = editor.read(&cx).style(cx); + let text_layout_details = + editor.read_with(cx, |editor, cx| TextLayoutDetails::new(editor, cx)); let font_cache = cx.font_cache().clone(); @@ -1380,12 +1373,7 @@ pub mod tests { DisplayPoint::new(0, 7) ); - let x = snapshot.x_for_point( - DisplayPoint::new(1, 10), - cx.font_cache(), - cx.text_layout_cache(), - &editor_style, - ); + let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details); dbg!(x); assert_eq!( movement::up( @@ -1393,9 +1381,7 @@ pub mod tests { DisplayPoint::new(1, 10), SelectionGoal::None, false, - cx.font_cache(), - cx.text_layout_cache(), - &editor_style, + &text_layout_details, ), ( DisplayPoint::new(0, 7), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bf1aa2e6b5784c4ff4d684d4a403e0bbc622de53..081d33c8a0bfde27cdd8e94712fc14cb0bb9bd35 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -71,6 +71,7 @@ use link_go_to_definition::{ }; use log::error; use lsp::LanguageServerId; +use movement::TextLayoutDetails; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, @@ -5274,9 +5275,7 @@ impl Editor { return; } - let font_cache = cx.font_cache().clone(); - let text_layout_cache = cx.text_layout_cache().clone(); - let editor_style = self.style(cx); + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { let line_mode = s.line_mode; @@ -5289,9 +5288,7 @@ impl Editor { selection.start, selection.goal, false, - &font_cache, - &text_layout_cache, - &editor_style, + &text_layout_details, ); selection.collapse_to(cursor, goal); }); @@ -5320,9 +5317,7 @@ impl Editor { Autoscroll::fit() }; - let font_cache = cx.font_cache().clone(); - let text_layout = cx.text_layout_cache().clone(); - let editor_style = self.style(cx); + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(autoscroll), cx, |s| { let line_mode = s.line_mode; @@ -5336,9 +5331,7 @@ impl Editor { row_count, selection.goal, false, - &font_cache, - &text_layout, - &editor_style, + &text_layout_details, ); selection.collapse_to(cursor, goal); }); @@ -5346,20 +5339,10 @@ impl Editor { } pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { - let font_cache = cx.font_cache().clone(); - let text_layout = cx.text_layout_cache().clone(); - let editor_style = self.style(cx); + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_heads_with(|map, head, goal| { - movement::up( - map, - head, - goal, - false, - &font_cache, - &text_layout, - &editor_style, - ) + movement::up(map, head, goal, false, &text_layout_details) }) }) } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 3403790681dbc24ba71a69d45b3972a97157254b..836d5dda2fc454432acb34fe1e3e3c3f61f9abc3 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,8 +1,8 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; -use gpui::{FontCache, TextLayoutCache, WindowContext}; +use crate::{char_kind, CharKind, Editor, EditorStyle, ToOffset, ToPoint}; +use gpui::{text_layout, FontCache, TextLayoutCache, WindowContext}; use language::Point; -use std::ops::Range; +use std::{ops::Range, sync::Arc}; #[derive(Debug, PartialEq)] pub enum FindRange { @@ -10,6 +10,24 @@ pub enum FindRange { MultiLine, } +/// TextLayoutDetails encompasses everything we need to move vertically +/// taking into account variable width characters. +pub struct TextLayoutDetails { + pub font_cache: Arc, + pub text_layout_cache: Arc, + pub editor_style: EditorStyle, +} + +impl TextLayoutDetails { + pub fn new(editor: &Editor, cx: &WindowContext) -> TextLayoutDetails { + TextLayoutDetails { + font_cache: cx.font_cache().clone(), + text_layout_cache: cx.text_layout_cache().clone(), + editor_style: editor.style(cx), + } + } +} + pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; @@ -48,9 +66,7 @@ pub fn up( start: DisplayPoint, goal: SelectionGoal, preserve_column_at_start: bool, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { up_by_rows( map, @@ -58,9 +74,7 @@ pub fn up( 1, goal, preserve_column_at_start, - font_cache, - text_layout_cache, - editor_style, + text_layout_details, ) } @@ -79,14 +93,12 @@ pub fn up_by_rows( row_count: u32, goal: SelectionGoal, preserve_column_at_start: bool, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { let mut goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x, SelectionGoal::HorizontalRange { end, .. } => end, - _ => map.x_for_point(start, font_cache, text_layout_cache, editor_style), + _ => map.x_for_point(start, text_layout_details), }; let prev_row = start.row().saturating_sub(row_count); @@ -96,13 +108,7 @@ pub fn up_by_rows( ); if point.row() < start.row() { *point.column_mut() = map - .column_for_x( - point.row(), - goal_x, - font_cache, - text_layout_cache, - editor_style, - ) + .column_for_x(point.row(), goal_x, text_layout_details) .unwrap_or(point.column()); } else if preserve_column_at_start { return (start, goal); From ef7e2c5d86208a8ecf738ae16c355188fa5d357a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 28 Sep 2023 21:39:24 -0600 Subject: [PATCH 04/15] Get the project running! --- crates/editor/src/display_map.rs | 21 +- crates/editor/src/editor.rs | 37 +++- crates/editor/src/editor_tests.rs | 1 + crates/editor/src/movement.rs | 309 +++++++++++++++++----------- crates/vim/src/motion.rs | 32 ++- crates/vim/src/normal.rs | 34 +-- crates/vim/src/normal/change.rs | 31 ++- crates/vim/src/normal/delete.rs | 7 +- crates/vim/src/normal/paste.rs | 17 +- crates/vim/src/normal/substitute.rs | 26 ++- crates/vim/src/normal/yank.rs | 4 +- crates/vim/src/visual.rs | 15 +- 12 files changed, 352 insertions(+), 182 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 45b4c0abede8e3cfd592771a935de0d4512db96b..cebafbd651c17e7a80f0cac820f995cda0ed91c5 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1392,19 +1392,28 @@ pub mod tests { movement::down( &snapshot, DisplayPoint::new(0, 7), - SelectionGoal::Column(10), - false + SelectionGoal::HorizontalPosition(x), + false, + &text_layout_details ), - (DisplayPoint::new(1, 10), SelectionGoal::Column(10)) + ( + DisplayPoint::new(1, 10), + SelectionGoal::HorizontalPosition(x) + ) ); + dbg!("starting down..."); assert_eq!( movement::down( &snapshot, DisplayPoint::new(1, 10), - SelectionGoal::Column(10), - false + SelectionGoal::HorizontalPosition(x), + false, + &text_layout_details ), - (DisplayPoint::new(2, 4), SelectionGoal::Column(10)) + ( + DisplayPoint::new(2, 4), + SelectionGoal::HorizontalPosition(x) + ) ); let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 081d33c8a0bfde27cdd8e94712fc14cb0bb9bd35..e68b1f008f0526a4a7ce09bc195fa3db8e932fea 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4988,6 +4988,7 @@ impl Editor { } pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext) { + let text_layout_details = TextLayoutDetails::new(&self, cx); self.transact(cx, |this, cx| { let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| { let mut edits: Vec<(Range, String)> = Default::default(); @@ -5011,7 +5012,10 @@ impl Editor { *head.column_mut() += 1; head = display_map.clip_point(head, Bias::Right); - selection.collapse_to(head, SelectionGoal::Column(head.column())); + let goal = SelectionGoal::HorizontalPosition( + display_map.x_for_point(head, &text_layout_details), + ); + selection.collapse_to(head, goal); let transpose_start = display_map .buffer_snapshot @@ -5355,13 +5359,20 @@ impl Editor { return; } + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } - let (cursor, goal) = movement::down(map, selection.end, selection.goal, false); + let (cursor, goal) = movement::down( + map, + selection.end, + selection.goal, + false, + &text_layout_details, + ); selection.collapse_to(cursor, goal); }); }); @@ -5398,22 +5409,32 @@ impl Editor { Autoscroll::fit() }; + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(autoscroll), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } - let (cursor, goal) = - movement::down_by_rows(map, selection.end, row_count, selection.goal, false); + let (cursor, goal) = movement::down_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + &text_layout_details, + ); selection.collapse_to(cursor, goal); }); }); } pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false)) + s.move_heads_with(|map, head, goal| { + movement::down(map, head, goal, false, &text_layout_details) + }) }); } @@ -6286,6 +6307,7 @@ impl Editor { } pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { + let text_layout_details = TextLayoutDetails::new(&self, cx); self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); let mut edits = Vec::new(); @@ -6528,7 +6550,10 @@ impl Editor { point.row += 1; point = snapshot.clip_point(point, Bias::Left); let display_point = point.to_display_point(display_snapshot); - (display_point, SelectionGoal::Column(display_point.column())) + let goal = SelectionGoal::HorizontalPosition( + display_snapshot.x_for_point(display_point, &text_layout_details), + ); + (display_point, goal) }) }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dee27e0121256edaa38a90bb175d436eba768f96..affe9f60a2930167949d45c8762661358704fb9f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -847,6 +847,7 @@ fn test_move_cursor(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor_multibyte(cx: &mut TestAppContext) { + todo!(); init_test(cx, |_| {}); let view = cx diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 836d5dda2fc454432acb34fe1e3e3c3f61f9abc3..e2306a1b2daaa589896c07dbcffac034a46aea4f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -83,8 +83,16 @@ pub fn down( start: DisplayPoint, goal: SelectionGoal, preserve_column_at_end: bool, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { - down_by_rows(map, start, 1, goal, preserve_column_at_end) + down_by_rows( + map, + start, + 1, + goal, + preserve_column_at_end, + text_layout_details, + ) } pub fn up_by_rows( @@ -130,29 +138,32 @@ pub fn down_by_rows( row_count: u32, goal: SelectionGoal, preserve_column_at_end: bool, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = match goal { - SelectionGoal::Column(column) => column, - SelectionGoal::ColumnRange { end, .. } => end, - _ => map.column_to_chars(start.row(), start.column()), + let mut goal_x = match goal { + SelectionGoal::HorizontalPosition(x) => x, + SelectionGoal::HorizontalRange { end, .. } => end, + _ => map.x_for_point(start, text_layout_details), }; let new_row = start.row() + row_count; let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); if point.row() > start.row() { - *point.column_mut() = map.column_from_chars(point.row(), goal_column); + *point.column_mut() = map + .column_for_x(point.row(), goal_x, text_layout_details) + .unwrap_or(map.line_len(point.row())); } else if preserve_column_at_end { return (start, goal); } else { point = map.max_point(); - goal_column = map.column_to_chars(point.row(), point.column()) + goal_x = map.x_for_point(point, text_layout_details) } let mut clipped_point = map.clip_point(point, Bias::Right); if clipped_point.row() > point.row() { clipped_point = map.clip_point(point, Bias::Left); } - (clipped_point, SelectionGoal::Column(goal_column)) + (clipped_point, SelectionGoal::HorizontalPosition(goal_x)) } pub fn line_beginning( @@ -426,9 +437,12 @@ pub fn split_display_range_by_lines( mod tests { use super::*; use crate::{ - display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, - InlayId, MultiBuffer, + display_map::Inlay, + test::{editor_test_context::EditorTestContext, marked_display_snapshot}, + Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, }; + use language::language_settings::AllLanguageSettings; + use project::Project; use settings::SettingsStore; use util::post_inc; @@ -721,129 +735,173 @@ mod tests { } #[gpui::test] - fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) { - /* - init_test(cx); - - let family_id = cx - .font_cache() - .load_family(&["Helvetica"], &Default::default()) - .unwrap(); - let font_id = cx - .font_cache() - .select_font(family_id, &Default::default()) - .unwrap(); + async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + init_test(cx); + }); - let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer.clone(), - [ - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 4), - primary: None, - }, - ExcerptRange { - context: Point::new(2, 0)..Point::new(3, 2), - primary: None, - }, - ], - cx, + let mut cx = EditorTestContext::new(cx).await; + let editor = cx.editor.clone(); + let window = cx.window.clone(); + cx.update_window(window, |cx| { + let text_layout_details = + editor.read_with(cx, |editor, cx| TextLayoutDetails::new(editor, cx)); + + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(2, 0)..Point::new(3, 2), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + let display_map = + cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + + assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); + + let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details); + + // Can't move up into the first excerpt's header + assert_eq!( + up( + &snapshot, + DisplayPoint::new(2, 2), + SelectionGoal::HorizontalPosition(col_2_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), + ); + assert_eq!( + up( + &snapshot, + DisplayPoint::new(2, 0), + SelectionGoal::None, + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), ); - multibuffer - }); - let display_map = - cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details); - assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); + // Move up and down within first excerpt + assert_eq!( + up( + &snapshot, + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_4_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 3), + SelectionGoal::HorizontalPosition(col_4_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(2, 3), + SelectionGoal::HorizontalPosition(col_4_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_4_x) + ), + ); - // Can't move up into the first excerpt's header - assert_eq!( - up( - &snapshot, - DisplayPoint::new(2, 2), - SelectionGoal::Column(2), - false - ), - ( - DisplayPoint::new(2, 0), - SelectionGoal::HorizontalPosition(0.0) - ), - ); - assert_eq!( - up( - &snapshot, - DisplayPoint::new(2, 0), - SelectionGoal::None, - false - ), - (DisplayPoint::new(2, 0), SelectionGoal::Column(0)), - ); + let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details); - // Move up and down within first excerpt - assert_eq!( - up( - &snapshot, - DisplayPoint::new(3, 4), - SelectionGoal::Column(4), - false - ), - (DisplayPoint::new(2, 3), SelectionGoal::Column(4)), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(2, 3), - SelectionGoal::Column(4), - false - ), - (DisplayPoint::new(3, 4), SelectionGoal::Column(4)), - ); + // Move up and down across second excerpt's header + assert_eq!( + up( + &snapshot, + DisplayPoint::new(6, 5), + SelectionGoal::HorizontalPosition(col_5_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_5_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_5_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(6, 5), + SelectionGoal::HorizontalPosition(col_5_x) + ), + ); - // Move up and down across second excerpt's header - assert_eq!( - up( - &snapshot, - DisplayPoint::new(6, 5), - SelectionGoal::Column(5), - false - ), - (DisplayPoint::new(3, 4), SelectionGoal::Column(5)), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(3, 4), - SelectionGoal::Column(5), - false - ), - (DisplayPoint::new(6, 5), SelectionGoal::Column(5)), - ); + let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details); - // Can't move down off the end - assert_eq!( - down( - &snapshot, - DisplayPoint::new(7, 0), - SelectionGoal::Column(0), - false - ), - (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(7, 2), - SelectionGoal::Column(2), - false - ), - (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), - ); - */ + // Can't move down off the end + assert_eq!( + down( + &snapshot, + DisplayPoint::new(7, 0), + SelectionGoal::HorizontalPosition(0.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x) + ), + ); + }); } fn init_test(cx: &mut gpui::AppContext) { @@ -851,5 +909,6 @@ mod tests { theme::init((), cx); language::init(cx); crate::init(cx); + Project::init_settings(cx); } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a197121626860c75b8e3b9101c03b30920960ed1..2f1e376c3e1e9e32eb60ae3a741caa50db833516 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3,7 +3,7 @@ use std::cmp; use editor::{ char_kind, display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, - movement::{self, find_boundary, find_preceding_boundary, FindRange}, + movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails}, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; @@ -361,6 +361,7 @@ impl Motion { point: DisplayPoint, goal: SelectionGoal, maybe_times: Option, + text_layout_details: &TextLayoutDetails, ) -> Option<(DisplayPoint, SelectionGoal)> { let times = maybe_times.unwrap_or(1); use Motion::*; @@ -373,13 +374,13 @@ impl Motion { } => down(map, point, goal, times), Down { display_lines: true, - } => down_display(map, point, goal, times), + } => down_display(map, point, goal, times, &text_layout_details), Up { display_lines: false, } => up(map, point, goal, times), Up { display_lines: true, - } => up_display(map, point, goal, times), + } => up_display(map, point, goal, times, &text_layout_details), Right => (right(map, point, times), SelectionGoal::None), NextWordStart { ignore_punctuation } => ( next_word_start(map, point, *ignore_punctuation, times), @@ -442,10 +443,15 @@ impl Motion { selection: &mut Selection, times: Option, expand_to_surrounding_newline: bool, + text_layout_details: &TextLayoutDetails, ) -> bool { - if let Some((new_head, goal)) = - self.move_point(map, selection.head(), selection.goal, times) - { + if let Some((new_head, goal)) = self.move_point( + map, + selection.head(), + selection.goal, + times, + &text_layout_details, + ) { selection.set_head(new_head, goal); if self.linewise() { @@ -566,9 +572,10 @@ fn down_display( mut point: DisplayPoint, mut goal: SelectionGoal, times: usize, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { for _ in 0..times { - (point, goal) = movement::down(map, point, goal, true); + (point, goal) = movement::down(map, point, goal, true, text_layout_details); } (point, goal) @@ -606,9 +613,10 @@ fn up_display( mut point: DisplayPoint, mut goal: SelectionGoal, times: usize, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { for _ in 0..times { - (point, goal) = movement::up(map, point, goal, true); + (point, goal) = movement::up(map, point, goal, true, &text_layout_details); } (point, goal) @@ -707,7 +715,7 @@ fn previous_word_start( point } -fn first_non_whitespace( +pub(crate) fn first_non_whitespace( map: &DisplaySnapshot, display_lines: bool, from: DisplayPoint, @@ -890,7 +898,11 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> first_non_whitespace(map, false, correct_line) } -fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { +pub(crate) fn next_line_end( + map: &DisplaySnapshot, + mut point: DisplayPoint, + times: usize, +) -> DisplayPoint { if times > 1 { point = down(map, point, SelectionGoal::None, times - 1).0; } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 36eab2c4c0be91eda40868a8b773237ce008ae87..9c93f19fc7e33512b621bece2c957e00240c799d 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -12,13 +12,13 @@ mod yank; use std::sync::Arc; use crate::{ - motion::{self, Motion}, + motion::{self, first_non_whitespace, next_line_end, right, Motion}, object::Object, state::{Mode, Operator}, Vim, }; use collections::HashSet; -use editor::scroll::autoscroll::Autoscroll; +use editor::{movement::TextLayoutDetails, scroll::autoscroll::Autoscroll}; use editor::{Bias, DisplayPoint}; use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::SelectionGoal; @@ -177,10 +177,11 @@ pub(crate) fn move_cursor( cx: &mut WindowContext, ) { vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, goal| { motion - .move_point(map, cursor, goal, times) + .move_point(map, cursor, goal, times, &text_layout_details) .unwrap_or((cursor, goal)) }) }) @@ -193,8 +194,8 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext, cx: &m | Motion::StartOfLine { .. } ); vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); @@ -27,9 +28,15 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m s.move_with(|map, selection| { motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion { - expand_changed_word_selection(map, selection, times, ignore_punctuation) + expand_changed_word_selection( + map, + selection, + times, + ignore_punctuation, + &text_layout_details, + ) } else { - motion.expand_selection(map, selection, times, false) + motion.expand_selection(map, selection, times, false, &text_layout_details) }; }); }); @@ -81,6 +88,7 @@ fn expand_changed_word_selection( selection: &mut Selection, times: Option, ignore_punctuation: bool, + text_layout_details: &TextLayoutDetails, ) -> bool { if times.is_none() || times.unwrap() == 1 { let scope = map @@ -103,11 +111,22 @@ fn expand_changed_word_selection( }); true } else { - Motion::NextWordStart { ignore_punctuation } - .expand_selection(map, selection, None, false) + Motion::NextWordStart { ignore_punctuation }.expand_selection( + map, + selection, + None, + false, + &text_layout_details, + ) } } else { - Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false) + Motion::NextWordStart { ignore_punctuation }.expand_selection( + map, + selection, + times, + false, + &text_layout_details, + ) } } diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 517ece803365bef5734feac48013861add2cf1c2..1ad91ff308376aeecd01deee2eea89171aa701ea 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,12 +1,15 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::{HashMap, HashSet}; -use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias}; +use editor::{ + display_map::ToDisplayPoint, movement::TextLayoutDetails, scroll::autoscroll::Autoscroll, Bias, +}; use gpui::WindowContext; use language::Point; pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.stop_recording(); vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_columns: HashMap<_, _> = Default::default(); @@ -14,7 +17,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m s.move_with(|map, selection| { let original_head = selection.head(); original_columns.insert(selection.id, original_head.column()); - motion.expand_selection(map, selection, times, true); + motion.expand_selection(map, selection, times, true, &text_layout_details); // Motion::NextWordStart on an empty line should delete it. if let Motion::NextWordStart { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index dda8dea1e480fcbf07a7df7f66b7846b27ee3d32..7cb5261c493eefb0b5bfea9b4ad12cd57eae0e51 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -1,8 +1,10 @@ use std::{borrow::Cow, cmp}; use editor::{ - display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection, - DisplayPoint, + display_map::ToDisplayPoint, + movement::{self, TextLayoutDetails}, + scroll::autoscroll::Autoscroll, + ClipboardSelection, DisplayPoint, }; use gpui::{impl_actions, AppContext, ViewContext}; use language::{Bias, SelectionGoal}; @@ -30,6 +32,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -168,8 +171,14 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { let mut cursor = anchor.to_display_point(map); if *line_mode { if !before { - cursor = - movement::down(map, cursor, SelectionGoal::None, false).0; + cursor = movement::down( + map, + cursor, + SelectionGoal::None, + false, + &text_layout_details, + ) + .0; } cursor = movement::indented_line_beginning(map, cursor, true); } else if !is_multiline { diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index bb6e1abf92a0a687e32c7ee6a1e92787a1a82ba9..ddc937d03fd7f8af9e46081460004c930864d946 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -1,9 +1,13 @@ -use editor::movement; +use editor::movement::{self, TextLayoutDetails}; use gpui::{actions, AppContext, WindowContext}; use language::Point; use workspace::Workspace; -use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; +use crate::{ + motion::{right, Motion}, + utils::copy_selections_content, + Mode, Vim, +}; actions!(vim, [Substitute, SubstituteLine]); @@ -32,10 +36,17 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { if selection.start == selection.end { - Motion::Right.expand_selection(map, selection, count, true); + Motion::Right.expand_selection( + map, + selection, + count, + true, + &text_layout_details, + ); } if line_mode { // in Visual mode when the selection contains the newline at the end @@ -43,7 +54,13 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut if !selection.is_empty() && selection.end.column() == 0 { selection.end = movement::left(map, selection.end); } - Motion::CurrentLine.expand_selection(map, selection, None, false); + Motion::CurrentLine.expand_selection( + map, + selection, + None, + false, + &text_layout_details, + ); if let Some((point, _)) = (Motion::FirstNonWhitespace { display_lines: false, }) @@ -52,6 +69,7 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut selection.start, selection.goal, None, + &text_layout_details, ) { selection.start = point; } diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 7212a865bd56a8eea01db7efd020d714b26a17ee..b50fcdf7ecbc9691b3e230fbf9d4ba2419c7bfd8 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,9 +1,11 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::HashMap; +use editor::movement::TextLayoutDetails; use gpui::WindowContext; pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_positions: HashMap<_, _> = Default::default(); @@ -11,7 +13,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut s.move_with(|map, selection| { let original_position = (selection.head(), selection.goal); original_positions.insert(selection.id, original_position); - motion.expand_selection(map, selection, times, true); + motion.expand_selection(map, selection, times, true, &text_layout_details); }); }); copy_selections_content(editor, motion.linewise(), cx); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index eac823de610280c24bb83003e007a28c29e81bf5..bec91007e3f7c8818076a5a5ee1cda2c41f81ab2 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -4,7 +4,7 @@ use std::{cmp, sync::Arc}; use collections::HashMap; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, + movement::{self, TextLayoutDetails}, scroll::autoscroll::Autoscroll, Bias, DisplayPoint, Editor, }; @@ -57,6 +57,7 @@ pub fn init(cx: &mut AppContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); if vim.state().mode == Mode::VisualBlock && !matches!( motion, @@ -67,7 +68,7 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex { let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { - motion.move_point(map, point, goal, times) + motion.move_point(map, point, goal, times, &text_layout_details) }) } else { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { @@ -89,9 +90,13 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex current_head = movement::left(map, selection.end) } - let Some((new_head, goal)) = - motion.move_point(map, current_head, selection.goal, times) - else { + let Some((new_head, goal)) = motion.move_point( + map, + current_head, + selection.goal, + times, + &text_layout_details, + ) else { return; }; From 002e2cc42c41f4fcb847061ce0b25721fa1895d5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 28 Sep 2023 22:40:27 -0600 Subject: [PATCH 05/15] Round better for up/down --- crates/editor/src/display_map.rs | 4 ++-- crates/editor/src/movement.rs | 8 ++------ crates/gpui/src/text_layout.rs | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index cebafbd651c17e7a80f0cac820f995cda0ed91c5..424ff1518a7812be7be40f06fe8e413befe07657 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -618,9 +618,9 @@ impl DisplaySnapshot { display_row: u32, x_coordinate: f32, text_layout_details: &TextLayoutDetails, - ) -> Option { + ) -> u32 { let layout_line = self.layout_line_for_row(display_row, text_layout_details); - layout_line.index_for_x(x_coordinate).map(|c| c as u32) + layout_line.closest_index_for_x(x_coordinate) as u32 } // column_for_x(row, x) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index e2306a1b2daaa589896c07dbcffac034a46aea4f..38cf5cd6c1a1eac49a6fbed2ba0b00312688fad0 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -115,9 +115,7 @@ pub fn up_by_rows( Bias::Left, ); if point.row() < start.row() { - *point.column_mut() = map - .column_for_x(point.row(), goal_x, text_layout_details) - .unwrap_or(point.column()); + *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_start { return (start, goal); } else { @@ -149,9 +147,7 @@ pub fn down_by_rows( let new_row = start.row() + row_count; let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); if point.row() > start.row() { - *point.column_mut() = map - .column_for_x(point.row(), goal_x, text_layout_details) - .unwrap_or(map.line_len(point.row())); + *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_end { return (start, goal); } else { diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 97f4b7a12d7a2aa95159dc049e57c6b5a4eb2e21..7fb87b10df2ce3baf822fbe5a6fddb4955e5f134 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -266,6 +266,8 @@ impl Line { self.layout.len == 0 } + /// index_for_x returns the character containing the given x coordinate. + /// (e.g. to handle a mouse-click) pub fn index_for_x(&self, x: f32) -> Option { if x >= self.layout.width { None @@ -281,6 +283,28 @@ impl Line { } } + /// closest_index_for_x returns the character boundary closest to the given x coordinate + /// (e.g. to handle aligning up/down arrow keys) + pub fn closest_index_for_x(&self, x: f32) -> usize { + let mut prev_index = 0; + let mut prev_x = 0.0; + + for run in self.layout.runs.iter() { + for glyph in run.glyphs.iter() { + if glyph.position.x() >= x { + if glyph.position.x() - x < x - prev_x { + return glyph.index; + } else { + return prev_index; + } + } + prev_index = glyph.index; + prev_x = glyph.position.x(); + } + } + prev_index + } + pub fn paint( &self, origin: Vector2F, From ab050d18901e47d41fdcd11da3e950688ae2e665 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Oct 2023 19:10:01 -0600 Subject: [PATCH 06/15] Use Horizontal ranges everywhere --- 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(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 424ff1518a7812be7be40f06fe8e413befe07657..0f2b5665c6eff88a631e630b4e6751d5e91b4431 100644 --- a/crates/editor/src/display_map.rs +++ b/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, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e68b1f008f0526a4a7ce09bc195fa3db8e932fea..88db2f1dfe12ac069943c3675ee6d95197e7d186 100644 --- a/crates/editor/src/editor.rs +++ b/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) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(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 { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 24cbadfd37927266e0395a4a8c9589bb9d0ab554..30015eb760d98474d420a41ca13bc71c691d95fd 100644 --- a/crates/editor/src/element.rs +++ b/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}, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 38cf5cd6c1a1eac49a6fbed2ba0b00312688fad0..7e75ae5e5d22c3f114c936c649d8be367d247f55 100644 --- a/crates/editor/src/movement.rs +++ b/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), }; diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 6a21c898ef3617fc37bfe159be220cfe4360f884..2fa8ffe408742f6e1a3d9c80dd8d124b2ddaa1ec 100644 --- a/crates/editor/src/selections_collection.rs +++ b/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, + positions: &Range, reversed: bool, + text_layout_details: &TextLayoutDetails, ) -> Option> { - 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 { diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 38831f92c238f22c7f257763cc557856fdc3e604..e127083112caef9c8762179fe2c8a2cac276388b 100644 --- a/crates/text/src/selection.rs +++ b/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)] diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 2f1e376c3e1e9e32eb60ae3a741caa50db833516..36514f8cc46e046934529a73a7aa185d7d108148 100644 --- a/crates/vim/src/motion.rs +++ b/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) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 9c93f19fc7e33512b621bece2c957e00240c799d..0e883cd758472a3395571521a8169efd6bae8270 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -194,9 +194,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext, 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) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index bec91007e3f7c8818076a5a5ee1cda2c41f81ab2..ac4c5478a1d924def738fbc711aee3c3bb12c509 100644 --- a/crates/vim/src/visual.rs +++ b/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), diff --git a/crates/vim/test_data/test_j.json b/crates/vim/test_data/test_j.json index 64aaf65ef8960f66253d788fa97f4b4306bbee70..703f69d22c4c780d76ec9cd049aca0164b9cd624 100644 --- a/crates/vim/test_data/test_j.json +++ b/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"}} From cb76b2a6ad4371eec12cbee612d476f91fd59b97 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 10 Oct 2023 08:53:30 -0600 Subject: [PATCH 07/15] Make vim visual block work better --- crates/editor/src/display_map.rs | 11 +-- crates/editor/src/element.rs | 2 +- crates/editor/src/movement.rs | 2 - crates/editor/src/selections_collection.rs | 6 +- crates/text/src/selection.rs | 1 - crates/vim/src/motion.rs | 1 - crates/vim/src/test.rs | 1 + crates/vim/src/visual.rs | 73 ++++++++++++++----- .../test_visual_block_issue_2123.json | 5 ++ .../vim/test_data/test_wrapped_motions.json | 15 ++++ 10 files changed, 85 insertions(+), 32 deletions(-) create mode 100644 crates/vim/test_data/test_visual_block_issue_2123.json create mode 100644 crates/vim/test_data/test_wrapped_motions.json diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 0f2b5665c6eff88a631e630b4e6751d5e91b4431..a92c46e072e00cfe7cda9a091a5fe94e0e88cfac 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -509,11 +509,12 @@ impl DisplaySnapshot { pub fn highlighted_chunks<'a>( &'a self, display_rows: Range, + language_aware: bool, style: &'a EditorStyle, ) -> impl Iterator> { self.chunks( display_rows, - true, + language_aware, Some(style.theme.hint), Some(style.theme.suggestion), ) @@ -562,7 +563,7 @@ impl DisplaySnapshot { }) } - fn layout_line_for_row( + pub fn lay_out_line_for_row( &self, display_row: u32, TextLayoutDetails { @@ -575,7 +576,7 @@ impl DisplaySnapshot { let mut line = String::new(); let range = display_row..display_row + 1; - for chunk in self.highlighted_chunks(range, editor_style) { + for chunk in self.highlighted_chunks(range, false, editor_style) { line.push_str(chunk.chunk); let text_style = if let Some(style) = chunk.style { @@ -607,7 +608,7 @@ impl DisplaySnapshot { display_point: DisplayPoint, text_layout_details: &TextLayoutDetails, ) -> f32 { - let layout_line = self.layout_line_for_row(display_point.row(), text_layout_details); + let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details); layout_line.x_for_index(display_point.column() as usize) } @@ -617,7 +618,7 @@ impl DisplaySnapshot { x_coordinate: f32, text_layout_details: &TextLayoutDetails, ) -> u32 { - let layout_line = self.layout_line_for_row(display_row, text_layout_details); + let layout_line = self.lay_out_line_for_row(display_row, text_layout_details); layout_line.closest_index_for_x(x_coordinate) as u32 } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 30015eb760d98474d420a41ca13bc71c691d95fd..39d91fe8997e0378be2abcc875f5b238865cb0fc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1583,7 +1583,7 @@ impl EditorElement { .collect() } else { let style = &self.style; - let chunks = snapshot.highlighted_chunks(rows.clone(), style); + let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); LineWithInvisibles::from_chunks( chunks, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 7e75ae5e5d22c3f114c936c649d8be367d247f55..0305d853e6f433fa207ca821dfc8293a385300ae 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -107,7 +107,6 @@ pub fn up_by_rows( 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), }; @@ -144,7 +143,6 @@ pub fn down_by_rows( 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), }; diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 2fa8ffe408742f6e1a3d9c80dd8d124b2ddaa1ec..148604bd23f64c41ddd639414355aa37c2a5ca6a 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -313,10 +313,12 @@ impl SelectionsCollection { let is_empty = positions.start == positions.end; let line_len = display_map.line_len(row); - let start_col = display_map.column_for_x(row, positions.start, text_layout_details); + let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details); + + let start_col = layed_out_line.closest_index_for_x(positions.start) as u32; 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_col = layed_out_line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); Some(Selection { diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index e127083112caef9c8762179fe2c8a2cac276388b..480cb99d747783b7c7bfc100af8b57401781a984 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -8,7 +8,6 @@ pub enum SelectionGoal { HorizontalPosition(f32), HorizontalRange { start: f32, end: f32 }, WrappedHorizontalPosition((u32, f32)), - WrappedHorizontalRange { start: (u32, f32), end: (u32, f32) }, } #[derive(Clone, Debug, PartialEq)] diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 36514f8cc46e046934529a73a7aa185d7d108148..e8d954bc1321d3f518680c45c938aca51c8a038b 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -568,7 +568,6 @@ fn up_down_buffer_rows( 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), _ => { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index d6d9a1936018fb08df149609d23f0935d991868a..3c4867921215d9355dbceb08ba07b925445d1860 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -653,6 +653,7 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) { .await; } +#[gpui::test] async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ac4c5478a1d924def738fbc711aee3c3bb12c509..f4a450c932452810aadbc514bdc97e5e329de200 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -145,16 +145,18 @@ pub fn visual_block_motion( 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); + dbg!(head, tail); + dbg!(s.newest_anchor().goal); let (start, end) = match s.newest_anchor().goal { SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), - SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start + 10.0), + SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start), _ => ( map.x_for_point(tail, &text_layout_details), map.x_for_point(head, &text_layout_details), ), }; - let goal = SelectionGoal::HorizontalRange { start, end }; + let mut goal = SelectionGoal::HorizontalRange { start, end }; let was_reversed = tail.column() > head.column(); if !was_reversed && !preserve_goal { @@ -179,35 +181,44 @@ pub fn visual_block_motion( 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() { - map.x_for_point(head, &text_layout_details) - ..map.x_for_point(head, &text_layout_details) + 10.0 + let head_forward = movement::saturating_right(map, head); + map.x_for_point(head, &text_layout_details)..map.x_for_point(head, &text_layout_details) } else { map.x_for_point(tail, &text_layout_details)..map.x_for_point(head, &text_layout_details) }; + if !preserve_goal { + goal = SelectionGoal::HorizontalRange { + start: positions.start, + end: positions.end, + }; + } + let mut selections = Vec::new(); let mut row = tail.row(); loop { - let start = map.clip_point( - DisplayPoint::new( - row, - map.column_for_x(row, positions.start, &text_layout_details), - ), - Bias::Left, + let layed_out_line = map.lay_out_line_for_row(row, &text_layout_details); + let start = DisplayPoint::new( + row, + layed_out_line.closest_index_for_x(positions.start) as u32, ); - let end = map.clip_point( - DisplayPoint::new( - row, - map.column_for_x(row, positions.end, &text_layout_details), - ), - Bias::Left, + let mut end = DisplayPoint::new( + row, + layed_out_line.closest_index_for_x(positions.end) as u32, ); + if end <= start { + if start.column() == map.line_len(start.row()) { + end = start; + } else { + end = movement::saturating_right(map, start); + } + } + if positions.start - <= map.x_for_point( - DisplayPoint::new(row, map.line_len(row)), - &text_layout_details, - ) + <= + //map.x_for_point(DisplayPoint::new(row, map.line_len(row)), &text_layout_details) + layed_out_line.width() { let selection = Selection { id: s.new_selection_id(), @@ -915,6 +926,28 @@ mod test { .await; } + #[gpui::test] + async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "The ˇquick brown + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v", "right", "down"]) + .await; + cx.assert_shared_state(indoc! { + "The «quˇ»ick brown + fox «juˇ»mps over + the lazy dog + " + }) + .await; + } + #[gpui::test] async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/test_data/test_visual_block_issue_2123.json b/crates/vim/test_data/test_visual_block_issue_2123.json new file mode 100644 index 0000000000000000000000000000000000000000..0f48bcc8904f8aabc0b5df1e92f22b5c29fd6166 --- /dev/null +++ b/crates/vim/test_data/test_visual_block_issue_2123.json @@ -0,0 +1,5 @@ +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\n"}} +{"Key":"ctrl-v"} +{"Key":"right"} +{"Key":"down"} +{"Get":{"state":"The «quˇ»ick brown\nfox «juˇ»mps over\nthe lazy dog\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_wrapped_motions.json b/crates/vim/test_data/test_wrapped_motions.json new file mode 100644 index 0000000000000000000000000000000000000000..195a58f6b5cb94b94a65730e1995760e61b8c3ec --- /dev/null +++ b/crates/vim/test_data/test_wrapped_motions.json @@ -0,0 +1,15 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"Put":{"state":"aaˇaa\n😃😃"}} +{"Key":"j"} +{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}} +{"Put":{"state":"123456789012aaˇaa\n123456789012😃😃"}} +{"Key":"j"} +{"Get":{"state":"123456789012aaaa\n123456789012😃ˇ😃","mode":"Normal"}} +{"Put":{"state":"123456789012aaˇaa\n123456789012😃😃"}} +{"Key":"j"} +{"Get":{"state":"123456789012aaaa\n123456789012😃ˇ😃","mode":"Normal"}} +{"Put":{"state":"123456789012aaaaˇaaaaaaaa123456789012\nwow\n123456789012😃😃😃😃😃😃123456789012"}} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"123456789012aaaaaaaaaaaa123456789012\nwow\n123456789012😃😃ˇ😃😃😃😃123456789012","mode":"Normal"}} From 138fa45ecb6293e60174091a477d2d1bd1ba8958 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 18 Oct 2023 22:23:38 -0600 Subject: [PATCH 08/15] recert config change --- .cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index e22bdb0f2c70a1ffda714674253cc533e9e7c1d1..9da6b3be080072d89d16a199e2d60d527eeacd07 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ xtask = "run --package xtask --" [build] # v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0", "-C", "link-arg=-fuse-ld=/opt/homebrew/Cellar/llvm/16.0.6/bin/ld64.lld"] +rustflags = ["-C", "symbol-mangling-version=v0"] From 3eb8aa80852497f2e57107ff88715f5ef8bf3f49 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 18 Oct 2023 22:39:25 -0600 Subject: [PATCH 09/15] Refactor TextLayoutDetails construction --- crates/editor/src/display_map.rs | 2 +- crates/editor/src/editor.rs | 26 +++++++++++------ crates/editor/src/movement.rs | 16 ++--------- crates/vim/src/normal.rs | 6 ++-- crates/vim/src/normal/change.rs | 2 +- crates/vim/src/normal/delete.rs | 6 ++-- crates/vim/src/normal/paste.rs | 8 ++---- crates/vim/src/normal/substitute.rs | 4 +-- crates/vim/src/normal/yank.rs | 3 +- crates/vim/src/visual.rs | 43 +++++++++++++---------------- 10 files changed, 52 insertions(+), 64 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index a92c46e072e00cfe7cda9a091a5fe94e0e88cfac..6e179950f357166f90ae1744590d9b8e4024ed4c 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1288,7 +1288,7 @@ pub mod tests { cx.update_window(window, |cx| { let text_layout_details = - editor.read_with(cx, |editor, cx| TextLayoutDetails::new(editor, cx)); + editor.read_with(cx, |editor, cx| editor.text_layout_details(cx)); let font_cache = cx.font_cache().clone(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 88db2f1dfe12ac069943c3675ee6d95197e7d186..01b060fdb64ebb9007709ce1e104cc23412af7d7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3065,6 +3065,14 @@ impl Editor { .collect() } + pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails { + TextLayoutDetails { + font_cache: cx.font_cache().clone(), + text_layout_cache: cx.text_layout_cache().clone(), + editor_style: self.style(cx), + } + } + fn splice_inlay_hints( &self, to_remove: Vec, @@ -4988,7 +4996,7 @@ impl Editor { } pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext) { - let text_layout_details = TextLayoutDetails::new(&self, cx); + let text_layout_details = &self.text_layout_details(cx); self.transact(cx, |this, cx| { let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| { let mut edits: Vec<(Range, String)> = Default::default(); @@ -5279,7 +5287,7 @@ impl Editor { return; } - let text_layout_details = TextLayoutDetails::new(&self, cx); + let text_layout_details = &self.text_layout_details(cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { let line_mode = s.line_mode; @@ -5321,7 +5329,7 @@ impl Editor { Autoscroll::fit() }; - let text_layout_details = TextLayoutDetails::new(&self, cx); + let text_layout_details = &self.text_layout_details(cx); self.change_selections(Some(autoscroll), cx, |s| { let line_mode = s.line_mode; @@ -5343,7 +5351,7 @@ impl Editor { } pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { - let text_layout_details = TextLayoutDetails::new(&self, cx); + let text_layout_details = &self.text_layout_details(cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_heads_with(|map, head, goal| { movement::up(map, head, goal, false, &text_layout_details) @@ -5359,7 +5367,7 @@ impl Editor { return; } - let text_layout_details = TextLayoutDetails::new(&self, cx); + let text_layout_details = &self.text_layout_details(cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { @@ -5409,7 +5417,7 @@ impl Editor { Autoscroll::fit() }; - let text_layout_details = TextLayoutDetails::new(&self, cx); + let text_layout_details = &self.text_layout_details(cx); self.change_selections(Some(autoscroll), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { @@ -5430,7 +5438,7 @@ impl Editor { } pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { - let text_layout_details = TextLayoutDetails::new(&self, cx); + let text_layout_details = &self.text_layout_details(cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_heads_with(|map, head, goal| { movement::down(map, head, goal, false, &text_layout_details) @@ -5953,7 +5961,7 @@ impl Editor { fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(cx); - let text_layout_details = TextLayoutDetails::new(self, cx); + let text_layout_details = self.text_layout_details(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(); @@ -6315,7 +6323,7 @@ impl Editor { } pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { - let text_layout_details = TextLayoutDetails::new(&self, cx); + let text_layout_details = &self.text_layout_details(cx); self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); let mut edits = Vec::new(); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 0305d853e6f433fa207ca821dfc8293a385300ae..62fd931eccc101c977aea3d1533b5853bd023972 100644 --- a/crates/editor/src/movement.rs +++ b/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::{FontCache, TextLayoutCache, WindowContext}; +use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; +use gpui::{FontCache, TextLayoutCache}; use language::Point; use std::{ops::Range, sync::Arc}; @@ -18,16 +18,6 @@ pub struct TextLayoutDetails { pub editor_style: EditorStyle, } -impl TextLayoutDetails { - pub fn new(editor: &Editor, cx: &WindowContext) -> TextLayoutDetails { - TextLayoutDetails { - font_cache: cx.font_cache().clone(), - text_layout_cache: cx.text_layout_cache().clone(), - editor_style: editor.style(cx), - } - } -} - pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; @@ -743,7 +733,7 @@ mod tests { let window = cx.window.clone(); cx.update_window(window, |cx| { let text_layout_details = - editor.read_with(cx, |editor, cx| TextLayoutDetails::new(editor, cx)); + editor.read_with(cx, |editor, cx| editor.text_layout_details(cx)); let family_id = cx .font_cache() diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 0e883cd758472a3395571521a8169efd6bae8270..1277d58b419b4c9e7af792a7dd528ede269adc79 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -18,7 +18,7 @@ use crate::{ Vim, }; use collections::HashSet; -use editor::{movement::TextLayoutDetails, scroll::autoscroll::Autoscroll}; +use editor::scroll::autoscroll::Autoscroll; use editor::{Bias, DisplayPoint}; use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::SelectionGoal; @@ -177,7 +177,7 @@ pub(crate) fn move_cursor( cx: &mut WindowContext, ) { vim.update_active_editor(cx, |editor, cx| { - let text_layout_details = TextLayoutDetails::new(editor, cx); + let text_layout_details = editor.text_layout_details(cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, goal| { motion @@ -280,7 +280,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex vim.start_recording(cx); vim.switch_mode(Mode::Insert, false, cx); vim.update_active_editor(cx, |editor, cx| { - let text_layout_details = TextLayoutDetails::new(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); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 042897ec3c78c509353b13a1caffc0f9e8983f6b..bf2a25a98d5ccf622a7b802b84962d6aed6bf308 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -20,7 +20,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m | Motion::StartOfLine { .. } ); vim.update_active_editor(cx, |editor, cx| { - let text_layout_details = TextLayoutDetails::new(editor, cx); + let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 1ad91ff308376aeecd01deee2eea89171aa701ea..77e0e47be5954c4a79182c835d6d221f6195981d 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,15 +1,13 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::{HashMap, HashSet}; -use editor::{ - display_map::ToDisplayPoint, movement::TextLayoutDetails, scroll::autoscroll::Autoscroll, Bias, -}; +use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias}; use gpui::WindowContext; use language::Point; pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.stop_recording(); vim.update_active_editor(cx, |editor, cx| { - let text_layout_details = TextLayoutDetails::new(editor, cx); + let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_columns: HashMap<_, _> = Default::default(); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 7cb5261c493eefb0b5bfea9b4ad12cd57eae0e51..6141e7c66f29d192aac827a2fb8cccd053e1bee6 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -1,10 +1,8 @@ use std::{borrow::Cow, cmp}; use editor::{ - display_map::ToDisplayPoint, - movement::{self, TextLayoutDetails}, - scroll::autoscroll::Autoscroll, - ClipboardSelection, DisplayPoint, + display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection, + DisplayPoint, }; use gpui::{impl_actions, AppContext, ViewContext}; use language::{Bias, SelectionGoal}; @@ -32,7 +30,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); vim.update_active_editor(cx, |editor, cx| { - let text_layout_details = TextLayoutDetails::new(editor, cx); + let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index e244e1d5d64de60c4b40a15c26f269555ae42444..f0369a89bfffabe3283bd138661ad75168bc3ef2 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -1,4 +1,4 @@ -use editor::movement::{self, TextLayoutDetails}; +use editor::movement; use gpui::{actions, AppContext, WindowContext}; use language::Point; use workspace::Workspace; @@ -32,7 +32,7 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { - let text_layout_details = TextLayoutDetails::new(editor, cx); + let text_layout_details = editor.text_layout_details(cx); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { if selection.start == selection.end { diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index b50fcdf7ecbc9691b3e230fbf9d4ba2419c7bfd8..33833500fabc7c42e946aa1a4c790e09cc233744 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,11 +1,10 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::HashMap; -use editor::movement::TextLayoutDetails; use gpui::WindowContext; pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { - let text_layout_details = TextLayoutDetails::new(editor, cx); + let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_positions: HashMap<_, _> = Default::default(); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index f4a450c932452810aadbc514bdc97e5e329de200..3157374b246941676cb7f77f3405c9b7a065de64 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -4,7 +4,7 @@ use std::{cmp, sync::Arc}; use collections::HashMap; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, - movement::{self, TextLayoutDetails}, + movement, scroll::autoscroll::Autoscroll, Bias, DisplayPoint, Editor, }; @@ -57,7 +57,7 @@ pub fn init(cx: &mut AppContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - let text_layout_details = TextLayoutDetails::new(editor, cx); + let text_layout_details = editor.text_layout_details(cx); if vim.state().mode == Mode::VisualBlock && !matches!( motion, @@ -140,25 +140,23 @@ pub fn visual_block_motion( SelectionGoal, ) -> Option<(DisplayPoint, SelectionGoal)>, ) { - let text_layout_details = TextLayoutDetails::new(editor, cx); + let text_layout_details = editor.text_layout_details(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); - dbg!(head, tail); - dbg!(s.newest_anchor().goal); + + let mut head_x = map.x_for_point(head, &text_layout_details); + let mut tail_x = map.x_for_point(tail, &text_layout_details); let (start, end) = match s.newest_anchor().goal { SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start), - _ => ( - map.x_for_point(tail, &text_layout_details), - map.x_for_point(head, &text_layout_details), - ), + _ => (tail_x, head_x), }; let mut goal = SelectionGoal::HorizontalRange { start, end }; - let was_reversed = tail.column() > head.column(); + let was_reversed = head_x > tail_x; if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } @@ -167,24 +165,25 @@ pub fn visual_block_motion( return; }; head = new_head; + head_x = map.x_for_point(head, &text_layout_details); - let is_reversed = tail.column() > head.column(); + let is_reversed = tail_x > head_x; if was_reversed && !is_reversed { - tail = movement::left(map, tail) + tail = movement::left(map, tail); + tail_x = map.x_for_point(tail, &text_layout_details); } else if !was_reversed && is_reversed { - tail = movement::right(map, tail) + tail = movement::right(map, tail); + tail_x = map.x_for_point(tail, &text_layout_details); } if !is_reversed && !preserve_goal { - head = movement::saturating_right(map, head) + head = movement::saturating_right(map, head); + head_x = map.x_for_point(head, &text_layout_details); } 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() { - let head_forward = movement::saturating_right(map, head); - map.x_for_point(head, &text_layout_details)..map.x_for_point(head, &text_layout_details) + head_x..tail_x } else { - map.x_for_point(tail, &text_layout_details)..map.x_for_point(head, &text_layout_details) + tail_x..head_x }; if !preserve_goal { @@ -215,11 +214,7 @@ pub fn visual_block_motion( } } - if positions.start - <= - //map.x_for_point(DisplayPoint::new(row, map.line_len(row)), &text_layout_details) - layed_out_line.width() - { + if positions.start <= layed_out_line.width() { let selection = Selection { id: s.new_selection_id(), start: start.to_point(map), From b596b4153ffbc4b911c6519eacf36256f31b0745 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 20 Oct 2023 10:34:09 -0600 Subject: [PATCH 10/15] Fix test --- crates/editor/src/display_map.rs | 2 +- crates/editor/src/editor_tests.rs | 17 ++++++++--------- crates/editor/src/movement.rs | 1 - 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6e179950f357166f90ae1744590d9b8e4024ed4c..184990ea78dc759fa8e1d24f6ae57ac58cc0685c 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1007,7 +1007,7 @@ pub mod tests { use settings::SettingsStore; use smol::stream::StreamExt; use std::{env, sync::Arc}; - use theme::{SyntaxTheme, Theme}; + use theme::SyntaxTheme; use util::test::{marked_text_ranges, sample_text}; use Bias::*; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6cc78385a3ea56c84a55c58f705c2784010481e1..664db53e1ab82fe04600c7c2e9876b26e5a7d34c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -847,7 +847,6 @@ fn test_move_cursor(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor_multibyte(cx: &mut TestAppContext) { - todo!(); init_test(cx, |_| {}); let view = cx @@ -889,6 +888,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { ); view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + view.move_left(&MoveLeft, cx); assert_eq!( view.selections.display_ranges(cx), &[empty_range(1, "ab⋯".len())] @@ -933,22 +937,17 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { view.move_up(&MoveUp, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ⋯ⓔ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ⋯".len())] + &[empty_range(0, "ⓐⓑ".len())] ); view.move_left(&MoveLeft, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] + &[empty_range(0, "ⓐ".len())] ); view.move_left(&MoveLeft, cx); assert_eq!( view.selections.display_ranges(cx), - &[empty_range(0, "ⓐ".len())] + &[empty_range(0, "".len())] ); }); } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 8f5d0d802c06cdc1e20745b6db695250520ef5e1..580faf10506f38bb971bf548c3b65d911fb1fd45 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -429,7 +429,6 @@ mod tests { test::{editor_test_context::EditorTestContext, marked_display_snapshot}, Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, }; - use language::language_settings::AllLanguageSettings; use project::Project; use settings::SettingsStore; use util::post_inc; From 1c36134cf994e7cf753e700b15c76cd8f97d2e03 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 20 Oct 2023 11:17:19 -0600 Subject: [PATCH 11/15] Fix single column vim selections --- crates/editor/src/editor_tests.rs | 15 +++++++++++++-- crates/editor/src/movement.rs | 2 ++ crates/gpui/src/text_layout.rs | 6 +++++- crates/vim/src/normal/increment.rs | 12 +++++++++++- crates/vim/src/visual.rs | 8 ++++++-- crates/vim/test_data/test_increment_steps.json | 1 + 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 664db53e1ab82fe04600c7c2e9876b26e5a7d34c..8104b0ee9b681b6b16b91047f5531981b1a455a3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -851,7 +851,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { let view = cx .add_window(|cx| { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); build_editor(buffer.clone(), cx) }) .root(cx); @@ -869,7 +869,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { true, cx, ); - assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε\n"); + assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); view.move_right(&MoveRight, cx); assert_eq!( @@ -934,6 +934,17 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { view.selections.display_ranges(cx), &[empty_range(1, "ab⋯e".len())] ); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(2, "αβ⋯ε".len())] + ); + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(1, "ab⋯e".len())] + ); + view.move_up(&MoveUp, cx); assert_eq!( view.selections.display_ranges(cx), diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 580faf10506f38bb971bf548c3b65d911fb1fd45..1bb4154b1f744d24cba6b7ec23d25bf3117590d6 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -147,7 +147,9 @@ pub fn down_by_rows( goal_x = map.x_for_point(point, text_layout_details) } + dbg!(point); let mut clipped_point = map.clip_point(point, Bias::Right); + dbg!(clipped_point); if clipped_point.row() > point.row() { clipped_point = map.clip_point(point, Bias::Left); } diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 7fb87b10df2ce3baf822fbe5a6fddb4955e5f134..0d0b8bebb50f145f8ae68978b4ad15088f1e3f37 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -302,7 +302,11 @@ impl Line { prev_x = glyph.position.x(); } } - prev_index + if self.width() - x < x - prev_x { + prev_index + 1 + } else { + prev_index + } } pub fn paint( diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 9d62f8ab7b2f5210b4a68f73029b8181902a4bb1..ee70ab1f5d0d569669281b2b2b4f51cc60b6e149 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -255,8 +255,18 @@ mod test { 4 5"}) .await; - cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g", "g", "ctrl-x"]) + + cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g"]) + .await; + cx.assert_shared_state(indoc! {" + «1ˇ» + «2ˇ» + «3ˇ» 2 + «4ˇ» + «5ˇ»"}) .await; + + cx.simulate_shared_keystrokes(["g", "ctrl-x"]).await; cx.assert_shared_state(indoc! {" ˇ0 0 diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 3157374b246941676cb7f77f3405c9b7a065de64..05118e22f8caf5f58a084bbd3eae88550c3959fd 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -169,10 +169,10 @@ pub fn visual_block_motion( let is_reversed = tail_x > head_x; if was_reversed && !is_reversed { - tail = movement::left(map, tail); + tail = movement::saturating_left(map, tail); tail_x = map.x_for_point(tail, &text_layout_details); } else if !was_reversed && is_reversed { - tail = movement::right(map, tail); + tail = movement::saturating_right(map, tail); tail_x = map.x_for_point(tail, &text_layout_details); } if !is_reversed && !preserve_goal { @@ -180,8 +180,12 @@ pub fn visual_block_motion( head_x = map.x_for_point(head, &text_layout_details); } + dbg!(head, head_x, tail, tail_x); + let positions = if is_reversed { head_x..tail_x + } else if head_x == tail_x { + map.x_for_point(movement::saturating_left(map, tail), &text_layout_details)..head_x } else { tail_x..head_x }; diff --git a/crates/vim/test_data/test_increment_steps.json b/crates/vim/test_data/test_increment_steps.json index fffaf1fd299377574f108e69aaefd2eadb3d0fe8..2e8711d1cc5757207405a14c78b8d944f352d664 100644 --- a/crates/vim/test_data/test_increment_steps.json +++ b/crates/vim/test_data/test_increment_steps.json @@ -9,6 +9,7 @@ {"Key":"ctrl-v"} {"Key":"g"} {"Key":"g"} +{"Get":{"state":"«1ˇ»\n«2ˇ»\n«3ˇ» 2\n«4ˇ»\n«5ˇ»","mode":"VisualBlock"}} {"Key":"g"} {"Key":"ctrl-x"} {"Get":{"state":"ˇ0\n0\n0 2\n0\n0","mode":"Normal"}} From 352a554c748feb88b3f5a36a152a7e0863b3bc18 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 20 Oct 2023 11:30:32 -0600 Subject: [PATCH 12/15] Fix bug in vim visual block mode --- crates/gpui/src/text_layout.rs | 6 +----- crates/vim/src/visual.rs | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 0d0b8bebb50f145f8ae68978b4ad15088f1e3f37..7fb87b10df2ce3baf822fbe5a6fddb4955e5f134 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -302,11 +302,7 @@ impl Line { prev_x = glyph.position.x(); } } - if self.width() - x < x - prev_x { - prev_index + 1 - } else { - prev_index - } + prev_index } pub fn paint( diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 05118e22f8caf5f58a084bbd3eae88550c3959fd..0e108251e2e75ac34f7543c4c028c602ab1f97a3 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -156,7 +156,7 @@ pub fn visual_block_motion( }; let mut goal = SelectionGoal::HorizontalRange { start, end }; - let was_reversed = head_x > tail_x; + let was_reversed = tail_x > head_x; if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } @@ -184,8 +184,6 @@ pub fn visual_block_motion( let positions = if is_reversed { head_x..tail_x - } else if head_x == tail_x { - map.x_for_point(movement::saturating_left(map, tail), &text_layout_details)..head_x } else { tail_x..head_x }; From 43d682f6b887ea80260c9d9591fcecbc0100774d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 20 Oct 2023 12:46:14 -0600 Subject: [PATCH 13/15] Handle pixel-down to last line when no trailing newline --- crates/editor/src/display_map.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 184990ea78dc759fa8e1d24f6ae57ac58cc0685c..1d6deb910aa8d55d8f780cdfd8ce169662cfc940 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -574,6 +574,7 @@ impl DisplaySnapshot { ) -> Line { let mut styles = Vec::new(); let mut line = String::new(); + let mut ended_in_newline = false; let range = display_row..display_row + 1; for chunk in self.highlighted_chunks(range, false, editor_style) { @@ -589,6 +590,7 @@ impl DisplaySnapshot { } else { Cow::Borrowed(&editor_style.text) }; + ended_in_newline = chunk.chunk.ends_with("\n"); styles.push(( chunk.chunk.len(), @@ -600,6 +602,22 @@ impl DisplaySnapshot { )); } + // our pixel positioning logic assumes each line ends in \n, + // this is almost always true except for the last line which + // may have no trailing newline. + if !ended_in_newline && display_row == self.max_point().row() { + line.push_str("\n"); + + styles.push(( + "\n".len(), + RunStyle { + font_id: editor_style.text.font_id, + color: editor_style.text_color, + underline: editor_style.text.underline, + }, + )); + } + text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles) } From fae5b1e3912e7924cc3fa3f6c49bb1090668ee6d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 20 Oct 2023 12:55:41 -0600 Subject: [PATCH 14/15] Fix build columnar selection logic --- crates/editor/src/selections_collection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 148604bd23f64c41ddd639414355aa37c2a5ca6a..4b2dc855c39312fe62c38c621e086601901011e2 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -316,7 +316,7 @@ impl SelectionsCollection { let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details); let start_col = layed_out_line.closest_index_for_x(positions.start) as u32; - if start_col < line_len || (is_empty && start_col == line_len) { + if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) { let start = DisplayPoint::new(row, start_col); let end_col = layed_out_line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); From 2de34a905d1901d503a2053dc3c33504791574cb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 20 Oct 2023 14:45:37 -0600 Subject: [PATCH 15/15] Hide any circumstantial evidence that this didn't work perfectly first time --- crates/editor/src/movement.rs | 2 -- crates/vim/src/visual.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 1bb4154b1f744d24cba6b7ec23d25bf3117590d6..580faf10506f38bb971bf548c3b65d911fb1fd45 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -147,9 +147,7 @@ pub fn down_by_rows( goal_x = map.x_for_point(point, text_layout_details) } - dbg!(point); let mut clipped_point = map.clip_point(point, Bias::Right); - dbg!(clipped_point); if clipped_point.row() > point.row() { clipped_point = map.clip_point(point, Bias::Left); } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 0e108251e2e75ac34f7543c4c028c602ab1f97a3..5d6477ff5be0134c0bbde6bbcf292ec60c6527a8 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -180,8 +180,6 @@ pub fn visual_block_motion( head_x = map.x_for_point(head, &text_layout_details); } - dbg!(head, head_x, tail, tail_x); - let positions = if is_reversed { head_x..tail_x } else {