Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
967 changes: 893 additions & 74 deletions crates/base/src/input/base/element.rs

Large diffs are not rendered by default.

53 changes: 51 additions & 2 deletions crates/base/src/input/base/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::{ops::Range, rc::Rc};

use gpui::{Bounds, Half, Pixels, ShapedLine, TextAlign, px};

use crate::Side;

use super::{WrappingIndent, display_map::LineLayout};

#[derive(Clone, Default)]
Expand All @@ -10,6 +12,43 @@ pub(crate) struct WhitespaceIndicators {
pub(crate) tab: ShapedLine,
}

/// A column of the editor's gutter, ordered from the text outward with
/// `gutter_order`.
///
/// The order is the same on either [`Side`], so a gutter on the right is the
/// mirror of one on the left.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GutterColumn {
/// The fold icons.
FoldIcons,
/// The line numbers.
LineNumbers,
/// The gutter markers of the line decorations.
Markers,
}

impl GutterColumn {
/// The columns in their default order, from the text outward.
pub(super) const DEFAULT_ORDER: [Self; 3] = [Self::FoldIcons, Self::LineNumbers, Self::Markers];

/// Return every column once, those listed first and the rest in their
/// default order.
pub(super) fn order(columns: impl IntoIterator<Item = Self>) -> [Self; 3] {
let mut order = Self::DEFAULT_ORDER;
let mut len = 0;
for column in columns.into_iter().chain(Self::DEFAULT_ORDER) {
if len == order.len() {
break;
}
if !order[..len].contains(&column) {
order[len] = column;
len += 1;
}
}
order
}
}

#[derive(Clone)]
pub(super) struct LastLayout {
pub(super) visible_range: Range<usize>,
Expand All @@ -22,11 +61,21 @@ pub(super) struct LastLayout {
pub(super) wrap_width: Option<Pixels>,
pub(super) wrapping_indent: WrappingIndent,
pub(super) line_number_width: Pixels,
/// Width reserved at the left of the line numbers for gutter markers, zero
/// when none can be painted. Part of `line_number_width`.
/// Width reserved between the line numbers and the outer edge of the gutter
/// for gutter markers, zero when none can be painted. Part of
/// `line_number_width`.
pub(super) gutter_marker_width: Pixels,
/// The buffer row that inline completion ghost lines follow, and their height.
pub(super) ghost_lines: Option<(usize, Pixels)>,
pub(super) gutter_side: Side,
pub(super) gutter_order: [GutterColumn; 3],
/// The x of the text, relative to the input bounds.
pub(super) text_origin_x: Pixels,
/// The x of the gutter, relative to the input bounds.
pub(super) gutter_origin_x: Pixels,
/// The width of the text, without the gutter and the margin kept from a
/// scrollbar on the left.
pub(super) text_width: Pixels,
/// Width of one space in the editor font.
///
/// Past the end of a line there are no glyphs to hit-test against, so this is the
Expand Down
75 changes: 63 additions & 12 deletions crates/base/src/input/base/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use sum_tree::Bias;
use unicode_segmentation::*;

use super::{
DiagnosticSet, DisplayMap, InputContextMenuCapabilities, InputEditorStyle,
DiagnosticSet, DisplayMap, GutterColumn, InputContextMenuCapabilities, InputEditorStyle,
InputHighlighterFactory, MASK_CHAR, MaskPattern, NativeMenu, NumberStep, WrappingIndent,
blink_cursor::BlinkCursor,
change::Change,
Expand All @@ -35,6 +35,7 @@ use super::{
undo_manager::{EditIntent, UndoManager},
};
use crate::ScrollbarPlacement;
use crate::Side;
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
use crate::input::blink_cursor::CURSOR_WIDTH;
use crate::input::movement::MoveDirection;
Expand Down Expand Up @@ -432,6 +433,8 @@ pub struct InputBaseState<M: InputModeKind> {
pub(crate) scroll_size: gpui::Size<Pixels>,
pub(super) editor_scrollbar_snapshot: Cell<Option<EditorScrollbarSnapshot>>,
pub(super) scrollbar_placement: ScrollbarPlacement,
pub(super) gutter_side: Side,
pub(super) gutter_order: [GutterColumn; 3],
/// The unwrapped width of the longest line and what it was measured for.
pub(super) longest_line_width: Cell<Option<(LongestLineKey, Pixels)>>,
pub(super) editor_paddings: Edges<Pixels>,
Expand Down Expand Up @@ -506,6 +509,7 @@ pub struct InputPresentation {
masked: bool,
multi_line: bool,
code_editor: bool,
gutter_side: Side,
text_align: TextAlign,
placeholder: SharedString,
mask_placeholder: Option<String>,
Expand Down Expand Up @@ -547,6 +551,10 @@ impl InputPresentation {
self.code_editor
}

pub fn gutter_side(&self) -> Side {
self.gutter_side
}

pub fn text_align(&self) -> TextAlign {
self.text_align
}
Expand Down Expand Up @@ -591,6 +599,7 @@ impl<M: InputModeKind> InputBaseState<M> {
masked: self.masked,
multi_line: self.is_multi_line(),
code_editor: self.is_code_editor(),
gutter_side: self.gutter_side,
text_align: self.text_align,
placeholder: self.placeholder.clone(),
mask_placeholder: self.mask_pattern.placeholder(),
Expand Down Expand Up @@ -768,6 +777,8 @@ impl<M: InputModeKind> InputBaseState<M> {
scroll_size: gpui::size(px(0.), px(0.)),
editor_scrollbar_snapshot: Cell::new(None),
scrollbar_placement: ScrollbarPlacement::default(),
gutter_side: Side::Left,
gutter_order: GutterColumn::DEFAULT_ORDER,
longest_line_width: Cell::new(None),
editor_paddings: Edges::default(),
deferred_scroll_offset: None,
Expand Down Expand Up @@ -931,7 +942,7 @@ impl<M: InputModeKind> InputBaseState<M> {
let local_offset = offset.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
let sub_line_index = (pos.y / line_height) as usize;
let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
let adjusted_pos = point(pos.x + last_layout.text_origin_x, pos.y + y_offset);
return (vi, sub_line_index, Some(adjusted_pos));
}

Expand Down Expand Up @@ -2581,7 +2592,7 @@ impl<M: InputModeKind> InputBaseState<M> {
let line = &last_layout.lines[vi];
let local_offset = offset.saturating_sub(last_layout.visible_line_byte_offsets[vi]);
if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
let bounds_width = bounds.size.width - last_layout.line_number_width;
let bounds_width = last_layout.text_width;
let col_offset_x = pos.x;
if col_offset_x - safety_margin < -scroll_offset.x {
// If the position is out of the visible area, scroll to make it visible
Expand Down Expand Up @@ -3083,7 +3094,7 @@ impl<M: InputModeKind> InputBaseState<M> {
};

let line_height = last_layout.line_height;
let line_number_width = last_layout.line_number_width;
let text_origin_x = last_layout.text_origin_x;

// TIP: About the IBeam cursor
//
Expand All @@ -3095,7 +3106,7 @@ impl<M: InputModeKind> InputBaseState<M> {
//
// - included the input padding.
// - included the scroll offset.
let inner_position = position - bounds.origin - point(line_number_width, px(0.));
let inner_position = position - bounds.origin - point(text_origin_x, px(0.));

let mut y_offset = last_layout.visible_top;
// Position relative to the last line walked, kept for a pointer that ends up
Expand Down Expand Up @@ -4372,12 +4383,11 @@ impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
) -> Option<Bounds<Pixels>> {
let last_layout = self.last_layout.as_ref()?;
let line_height = last_layout.line_height;
let line_number_width = last_layout.line_number_width;
let range = self.range_from_utf16(&range_utf16);

let mut start_origin = None;
let mut end_origin = None;
let line_number_origin = point(line_number_width, px(0.));
let text_origin = point(last_layout.text_origin_x, px(0.));
let mut y_offset = last_layout.visible_top;

for (vi, line) in last_layout.lines.iter().enumerate() {
Expand Down Expand Up @@ -4413,16 +4423,16 @@ impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
let start_origin = start_origin.or_else(|| {
let offset = self.last_cursor.or(Some(self.cursor()))?;
let (_, _, origin) = self.line_and_position_for_offset(offset);
origin.map(|origin| origin - line_number_origin)
origin.map(|origin| origin - text_origin)
})?;
let mut end_origin = end_origin.unwrap_or(start_origin);
// Ensure at same line.
end_origin.y = start_origin.y;

Some(Bounds::from_corners(
bounds.origin + line_number_origin + start_origin,
bounds.origin + text_origin + start_origin,
// + line_height for show IME panel under the cursor line.
bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
bounds.origin + text_origin + point(end_origin.x, end_origin.y + line_height),
))
}

Expand Down Expand Up @@ -10673,8 +10683,9 @@ impl<M: crate::input::MultiLineMode> InputBaseState<M> {
/// The placement of the scrollbars, default is [`ScrollbarPlacement::BottomRight`].
///
/// On the left, the vertical scrollbar sits on the input's left edge, over
/// the line numbers. At the top, the horizontal scrollbar overlays the
/// first line, as at the bottom it overlays the last.
/// the gutter when that is on the left too; otherwise the text keeps clear
/// of it. At the top, the horizontal scrollbar overlays the first line, as
/// at the bottom it overlays the last.
pub fn scrollbar_placement(mut self, placement: ScrollbarPlacement) -> Self {
self.scrollbar_placement = placement;
self
Expand All @@ -10690,6 +10701,46 @@ impl<M: crate::input::MultiLineMode> InputBaseState<M> {
cx.notify();
}

/// The side of the gutter with the line numbers and fold icons, default is
/// [`Side::Left`].
///
/// [`Side::Right`] is for the left pane of a side-by-side diff, so that both
/// gutters face the center and corresponding line numbers sit next to each
/// other. On the right, the gutter is mirrored: its columns keep their order
/// from the text outward, see [`InputBaseState::gutter_order`], and the
/// line numbers are aligned toward the text. A vertical scrollbar on the
/// same side stays outermost.
pub fn gutter_side(mut self, side: Side) -> Self {
self.gutter_side = side;
self
}

/// See [`InputBaseState::gutter_side`].
pub fn set_gutter_side(&mut self, side: Side, cx: &mut Context<Self>) {
self.gutter_side = side;
cx.notify();
}

/// The order of the gutter's columns from the text outward, default is
/// `[GutterColumn::FoldIcons, GutterColumn::LineNumbers, GutterColumn::Markers]`.
///
/// A column left out follows the listed ones in its default order, and a
/// repeated one is ignored. The order holds on either side.
pub fn gutter_order(mut self, columns: impl IntoIterator<Item = GutterColumn>) -> Self {
self.gutter_order = GutterColumn::order(columns);
self
}

/// See [`InputBaseState::gutter_order`].
pub fn set_gutter_order(
&mut self,
columns: impl IntoIterator<Item = GutterColumn>,
cx: &mut Context<Self>,
) {
self.gutter_order = GutterColumn::order(columns);
cx.notify();
}

/// Set the soft wrap mode, default is true.
#[doc(hidden)]
pub fn soft_wrap(mut self, wrap: bool) -> Self {
Expand Down
10 changes: 10 additions & 0 deletions crates/base/src/input/editor/display_map/text_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,11 @@ mod tests {
line_number_width: px(0.),
gutter_marker_width: px(0.),
ghost_lines: None,
gutter_side: crate::Side::Left,
gutter_order: crate::input::GutterColumn::DEFAULT_ORDER,
text_origin_x: px(0.),
gutter_origin_x: px(0.),
text_width: px(0.),
space_width: px(0.),
cursor_bounds: None,
text_align: TextAlign::Left,
Expand Down Expand Up @@ -1914,6 +1919,11 @@ mod tests {
line_number_width: px(0.),
gutter_marker_width: px(0.),
ghost_lines: None,
gutter_side: crate::Side::Left,
gutter_order: crate::input::GutterColumn::DEFAULT_ORDER,
text_origin_x: px(0.),
gutter_origin_x: px(0.),
text_width: px(0.),
space_width: px(0.),
cursor_bounds: None,
text_align: TextAlign::Left,
Expand Down
2 changes: 1 addition & 1 deletion crates/base/src/input/editor/indent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ impl<M: InputModeKind> TextElement<M> {
px(0.)
};

let pos = point(x + last_layout.line_number_width, offset_y);
let pos = point(x + last_layout.text_origin_x, offset_y);

builder.move_to(pos);
builder.line_to(point(pos.x, pos.y + line_height));
Expand Down
10 changes: 6 additions & 4 deletions crates/base/src/input/editor/line_decorations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,12 @@ impl InputBaseState<EditorMode> {
/// range decorations, rows are not tracked across edits: the provider answers
/// for the text as it is when asked.
///
/// A background spans the row from the gutter to the right edge, across its
/// soft wraps, under the active line, indent guides, selections and text. A
/// marker is painted in a slot at the left of the line numbers, only while
/// they are shown; the gutter reserves the slot while any collection has a
/// A background spans the whole row, gutter included, across its soft wraps,
/// under the active line, indent guides, selections and text. A marker is
/// painted in a slot of its own, only while the line numbers are shown. In
/// the default order the slot sits between the line numbers and the outer
/// edge of the gutter; [`InputBaseState::gutter_order`] can move it nearer
/// the text. The gutter reserves the slot while any collection has a
/// provider and a marker renderer is set. Later collections paint over earlier
/// ones. Neither affects hit testing or focus. Collections live until
/// explicitly disposed or the editor is dropped.
Expand Down
1 change: 1 addition & 0 deletions crates/base/src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ pub(crate) use language::EditorLanguage;
pub use language::{LanguageProvider, set_language_config, set_language_provider};
pub(crate) use language_config::LanguageConfig;
pub use language_config::{AutoClosingPair, BracketPair, IndentationRules};
pub use layout::GutterColumn;
pub use line_decorations::{
GutterMarker, LineDecoration, LineDecorationCollection, LineDecorationProvider,
};
Expand Down
Loading
Loading