diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 87b8001b38e4e712c0296d986bc7b3b395c30296..489f6f0a408f8e885c22b10abc9371ddd7e1312e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -69,9 +69,14 @@ impl EditorElement { } let snapshot = self.snapshot(cx.app); - let position = paint.point_for_position(&snapshot, layout, position); + let (position, overshoot) = paint.point_for_position(&snapshot, layout, position); - if shift { + if shift && alt { + cx.dispatch_action(Select(SelectPhase::BeginColumnar { + position, + overshoot, + })); + } else if shift { cx.dispatch_action(Select(SelectPhase::Extend { position, click_count, @@ -136,10 +141,11 @@ impl EditorElement { let font_cache = cx.font_cache.clone(); let text_layout_cache = cx.text_layout_cache.clone(); let snapshot = self.snapshot(cx.app); - let position = paint.point_for_position(&snapshot, layout, position); + let (position, overshoot) = paint.point_for_position(&snapshot, layout, position); cx.dispatch_action(Select(SelectPhase::Update { position, + overshoot, scroll_position: (snapshot.scroll_position() + scroll_delta).clamp( Vector2F::zero(), layout.scroll_max(&font_cache, &text_layout_cache), @@ -685,6 +691,7 @@ impl Element for EditorElement { let text_width = size.x() - gutter_width; let text_offset = vec2f(-style.text.descent(cx.font_cache), 0.); let em_width = style.text.em_width(cx.font_cache); + let em_advance = style.text.em_advance(cx.font_cache); let overscroll = vec2f(em_width, 0.); let wrap_width = text_width - text_offset.x() - overscroll.x() - em_width; let snapshot = self.update_view(cx.app, |view, cx| { @@ -784,6 +791,7 @@ impl Element for EditorElement { block_layouts, line_height, em_width, + em_advance, selections, max_visible_line_width, }; @@ -912,6 +920,7 @@ pub struct LayoutState { block_layouts: Vec<(Range, BlockStyle)>, line_height: f32, em_width: f32, + em_advance: f32, selections: HashMap>>, overscroll: Vector2F, text_offset: Vector2F, @@ -980,7 +989,7 @@ impl PaintState { snapshot: &Snapshot, layout: &LayoutState, position: Vector2F, - ) -> DisplayPoint { + ) -> (DisplayPoint, u32) { let scroll_position = snapshot.scroll_position(); let position = position - self.text_bounds.origin(); let y = position.y().max(0.0).min(layout.size.y()); @@ -992,12 +1001,13 @@ impl PaintState { let column = if x >= 0.0 { line.index_for_x(x) .map(|ix| ix as u32) - .unwrap_or(snapshot.line_len(row)) + .unwrap_or_else(|| snapshot.line_len(row)) } else { 0 }; + let overshoot = (0f32.max(x - line.width()) / layout.em_advance) as u32; - DisplayPoint::new(row, column) + (DisplayPoint::new(row, column), overshoot) } } diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 0c84de1d964ddd6c32ccc6b58dcdc9b88198f5e5..056703f523aee0c3515de4aa83333f424bc73059 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -282,12 +282,17 @@ pub enum SelectPhase { add: bool, click_count: usize, }, + BeginColumnar { + position: DisplayPoint, + overshoot: u32, + }, Extend { position: DisplayPoint, click_count: usize, }, Update { position: DisplayPoint, + overshoot: u32, scroll_position: Vector2F, }, End, @@ -320,6 +325,7 @@ pub struct Editor { display_map: ModelHandle, selection_set_id: SelectionSetId, pending_selection: Option, + columnar_selection_tail: Option, next_selection_id: usize, add_selections_state: Option, autoclose_stack: Vec, @@ -453,6 +459,7 @@ impl Editor { display_map, selection_set_id, pending_selection: None, + columnar_selection_tail: None, next_selection_id, add_selections_state: None, autoclose_stack: Default::default(), @@ -656,14 +663,19 @@ impl Editor { add, click_count, } => self.begin_selection(*position, *add, *click_count, cx), + SelectPhase::BeginColumnar { + position, + overshoot, + } => self.begin_columnar_selection(*position, *overshoot, cx), SelectPhase::Extend { position, click_count, } => self.extend_selection(*position, *click_count, cx), SelectPhase::Update { position, + overshoot, scroll_position, - } => self.update_selection(*position, *scroll_position, cx), + } => self.update_selection(*position, *overshoot, *scroll_position, cx), SelectPhase::End => self.end_selection(cx), } } @@ -774,14 +786,45 @@ impl Editor { cx.notify(); } + fn begin_columnar_selection( + &mut self, + position: DisplayPoint, + overshoot: u32, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus_self(); + cx.emit(Event::Activate); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx); + + let tail = self.newest_selection::(cx).tail(); + self.columnar_selection_tail = Some(buffer.anchor_before(tail)); + + self.select_columns( + tail.to_display_point(&display_map), + position, + overshoot, + &display_map, + cx, + ); + } + fn update_selection( &mut self, position: DisplayPoint, + overshoot: u32, scroll_position: Vector2F, cx: &mut ViewContext, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - if let Some(PendingSelection { selection, mode }) = self.pending_selection.as_mut() { + + if let Some(tail) = self.columnar_selection_tail.as_ref() { + let tail = tail.to_display_point(&display_map); + self.select_columns(tail, position, overshoot, &display_map, cx); + } else if let Some(PendingSelection { selection, mode }) = self.pending_selection.as_mut() { let buffer = self.buffer.read(cx); let head; let tail; @@ -861,14 +904,55 @@ impl Editor { } fn end_selection(&mut self, cx: &mut ViewContext) { + self.columnar_selection_tail.take(); if self.pending_selection.is_some() { let selections = self.selections::(cx).collect::>(); self.update_selections(selections, false, cx); } } + fn select_columns( + &mut self, + tail: DisplayPoint, + head: DisplayPoint, + overshoot: u32, + display_map: &DisplayMapSnapshot, + cx: &mut ViewContext, + ) { + let start_row = cmp::min(tail.row(), head.row()); + let end_row = cmp::max(tail.row(), head.row()); + let start_column = cmp::min(tail.column(), head.column() + overshoot); + let end_column = cmp::max(tail.column(), head.column() + overshoot); + let reversed = start_column < tail.column(); + + let selections = (start_row..=end_row) + .filter_map(|row| { + if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { + let start = display_map + .clip_point(DisplayPoint::new(row, start_column), Bias::Left) + .to_point(&display_map); + let end = display_map + .clip_point(DisplayPoint::new(row, end_column), Bias::Right) + .to_point(&display_map); + Some(Selection { + id: post_inc(&mut self.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + }) + } else { + None + } + }) + .collect::>(); + + self.update_selections(selections, false, cx); + cx.notify(); + } + pub fn is_selecting(&self) -> bool { - self.pending_selection.is_some() + self.pending_selection.is_some() || self.columnar_selection_tail.is_some() } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { @@ -3483,7 +3567,7 @@ mod tests { ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); }); assert_eq!( @@ -3492,7 +3576,7 @@ mod tests { ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); }); assert_eq!( @@ -3502,7 +3586,7 @@ mod tests { editor.update(cx, |view, cx| { view.end_selection(cx); - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); }); assert_eq!( @@ -3512,7 +3596,7 @@ mod tests { editor.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), cx); + view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx); }); assert_eq!( @@ -3548,7 +3632,7 @@ mod tests { }); view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), Vector2F::zero(), cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); assert_eq!( view.selection_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -3557,7 +3641,7 @@ mod tests { view.update(cx, |view, cx| { view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); assert_eq!( view.selection_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -3573,11 +3657,11 @@ mod tests { view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); - view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); view.end_selection(cx); view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), cx); + view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx); view.end_selection(cx); assert_eq!( view.selection_ranges(cx), diff --git a/crates/gpui/src/font_cache.rs b/crates/gpui/src/font_cache.rs index 0509ecd437d134bd05f3f3c22cf9063286ae1140..8a6ae62ff9ff52182e1321830d8e9f99c0405662 100644 --- a/crates/gpui/src/font_cache.rs +++ b/crates/gpui/src/font_cache.rs @@ -157,6 +157,17 @@ impl FontCache { bounds.width() * self.em_scale(font_id, font_size) } + pub fn em_advance(&self, font_id: FontId, font_size: f32) -> f32 { + let glyph_id; + let advance; + { + let state = self.0.read(); + glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap(); + advance = state.fonts.advance(font_id, glyph_id).unwrap(); + } + advance.x() * self.em_scale(font_id, font_size) + } + pub fn line_height(&self, font_id: FontId, font_size: f32) -> f32 { let height = self.metric(font_id, |m| m.bounding_box.height()); (height * self.em_scale(font_id, font_size)).ceil() diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index 3dbfd660344faabcb8352b9f8fe22f6c36f779a3..6509360a626a9e8342afa1adbb90c9cedee327ce 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -151,6 +151,10 @@ impl TextStyle { font_cache.em_width(self.font_id, self.font_size) } + pub fn em_advance(&self, font_cache: &FontCache) -> f32 { + font_cache.em_advance(self.font_id, self.font_size) + } + pub fn descent(&self, font_cache: &FontCache) -> f32 { font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache) } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 7a55916043f3d0a60e8e3c71260bdaf68aa88c8e..0e1bb3fca97f9861a1caa8eaa6d07df62a54f1c1 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -148,6 +148,7 @@ pub trait FontSystem: Send + Sync { ) -> anyhow::Result; fn font_metrics(&self, font_id: FontId) -> FontMetrics; fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result; + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result; fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; fn rasterize_glyph( &self, diff --git a/crates/gpui/src/platform/mac/fonts.rs b/crates/gpui/src/platform/mac/fonts.rs index c7f03689ee677bb017bcd42224e182c84f9b2bf2..2453660abc92a2d3fe84b31634f7f6461006afdf 100644 --- a/crates/gpui/src/platform/mac/fonts.rs +++ b/crates/gpui/src/platform/mac/fonts.rs @@ -69,6 +69,10 @@ impl platform::FontSystem for FontSystem { self.0.read().typographic_bounds(font_id, glyph_id) } + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result { + self.0.read().advance(font_id, glyph_id) + } + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { self.0.read().glyph_for_char(font_id, ch) } @@ -137,6 +141,10 @@ impl FontSystemState { Ok(self.fonts[font_id.0].typographic_bounds(glyph_id)?) } + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> anyhow::Result { + Ok(self.fonts[font_id.0].advance(glyph_id)?) + } + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option { self.fonts[font_id.0].glyph_for_char(ch) }